Lower live Eve surfaces without rebuilding UI state
This commit is contained in:
parent
45b828d5b3
commit
4439054977
5 changed files with 261 additions and 7 deletions
|
|
@ -46,6 +46,9 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
element.userData = component;
|
||||
ApplyGeneratedLayout(element, component);
|
||||
|
||||
if (IsHidden(component) || IsExternalProjectionRoot(component.Kind))
|
||||
return element;
|
||||
|
||||
foreach (var child in component.Children)
|
||||
{
|
||||
var loweredChild = LowerComponent(child, document, commandSink);
|
||||
|
|
@ -129,7 +132,11 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
card.style.flexDirection = FlexDirection.Column;
|
||||
var title = component.GetProp("title");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
card.Add(TitleLabel(title));
|
||||
{
|
||||
var titleLabel = TitleLabel(title);
|
||||
titleLabel.name = "title";
|
||||
card.Add(titleLabel);
|
||||
}
|
||||
return card;
|
||||
}
|
||||
case "metric":
|
||||
|
|
@ -137,8 +144,12 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
var metric = new VisualElement();
|
||||
metric.AddToClassList("eve-metric");
|
||||
metric.style.flexDirection = FlexDirection.Column;
|
||||
metric.Add(MutedLabel(component.GetProp("label")));
|
||||
metric.Add(ValueLabel(component.GetProp("value")));
|
||||
var label = MutedLabel(component.GetProp("label"));
|
||||
label.name = "label";
|
||||
metric.Add(label);
|
||||
var value = ValueLabel(component.GetProp("value"));
|
||||
value.name = "value";
|
||||
metric.Add(value);
|
||||
return metric;
|
||||
}
|
||||
case "progress":
|
||||
|
|
@ -741,6 +752,21 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
return kind;
|
||||
}
|
||||
|
||||
internal static bool IsHidden(EveSurfaceComponent component) =>
|
||||
component.Layout != null &&
|
||||
component.Layout.TryGetValue("display", out var display) &&
|
||||
string.Equals(display, "none", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static bool IsExternalProjectionRoot(string kind)
|
||||
{
|
||||
kind = NormalizeKind(kind);
|
||||
return string.Equals(kind, "world.scene3d", StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "world.scene2d", StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "field.volume3d", StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "field.particles3d", StringComparison.Ordinal) ||
|
||||
string.Equals(kind, "layer.reactive", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string SafeName(string value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "eve.component" : value;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using GameCult.Eve.Surface;
|
||||
using UnityEngine.UIElements;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityUIToolkit
|
||||
{
|
||||
public sealed class EveUiToolkitSurfacePresenter
|
||||
{
|
||||
private readonly EveUiToolkitSurfaceLowerer _lowerer;
|
||||
private string _structure = "";
|
||||
|
||||
public EveUiToolkitSurfacePresenter(EveUiToolkitSurfaceOptions? options = null)
|
||||
{
|
||||
_lowerer = new EveUiToolkitSurfaceLowerer(options);
|
||||
}
|
||||
|
||||
public VisualElement? Root { get; private set; }
|
||||
|
||||
public bool Present(
|
||||
EveSurfaceDocument document,
|
||||
Action<EveSurfaceCommandRequest>? commandSink = null)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
var structure = StructureOf(document);
|
||||
if (Root == null || !string.Equals(_structure, structure, StringComparison.Ordinal))
|
||||
{
|
||||
Root = _lowerer.Lower(document, commandSink);
|
||||
_structure = structure;
|
||||
return true;
|
||||
}
|
||||
|
||||
var elements = Root.Query<VisualElement>()
|
||||
.ToList()
|
||||
.Where(element => element.userData is EveSurfaceComponent)
|
||||
.ToDictionary(element => ((EveSurfaceComponent)element.userData).Id, StringComparer.Ordinal);
|
||||
if (Root.userData is EveSurfaceComponent rootComponent)
|
||||
elements[rootComponent.Id] = Root;
|
||||
ApplyDynamic(document.Surface.Root, elements);
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void ApplyDynamic(
|
||||
EveSurfaceComponent component,
|
||||
IReadOnlyDictionary<string, VisualElement> elements)
|
||||
{
|
||||
if (!elements.TryGetValue(component.Id, out var element))
|
||||
return;
|
||||
element.userData = component;
|
||||
switch (NormalizeKind(component.Kind))
|
||||
{
|
||||
case "pane":
|
||||
case "modal":
|
||||
case "card":
|
||||
var title = element.Q<Label>("title");
|
||||
if (title != null) title.text = component.GetProp("title");
|
||||
break;
|
||||
case "metric":
|
||||
var label = element.Q<Label>("label");
|
||||
var value = element.Q<Label>("value");
|
||||
if (label != null) label.text = component.GetProp("label");
|
||||
if (value != null) value.text = component.GetProp("value");
|
||||
break;
|
||||
case "progress":
|
||||
if (element is ProgressBar progress)
|
||||
{
|
||||
progress.title = component.GetProp("label");
|
||||
progress.value = ParseRatio(component.GetProp("ratio", component.GetProp("value")));
|
||||
}
|
||||
break;
|
||||
case "text":
|
||||
if (element is Label text)
|
||||
text.text = component.GetProp("text", component.GetProp("value", component.GetProp("title")));
|
||||
break;
|
||||
}
|
||||
|
||||
if (EveUiToolkitSurfaceLowerer.IsHidden(component) ||
|
||||
EveUiToolkitSurfaceLowerer.IsExternalProjectionRoot(component.Kind))
|
||||
return;
|
||||
foreach (var child in component.Children)
|
||||
ApplyDynamic(child, elements);
|
||||
}
|
||||
|
||||
private static string StructureOf(EveSurfaceDocument document)
|
||||
{
|
||||
var value = new StringBuilder();
|
||||
value.Append(document.ProviderId).Append('\n')
|
||||
.Append(document.Surface.Id).Append('\n');
|
||||
AppendStructure(value, document.Surface.Root);
|
||||
foreach (var command in document.Commands)
|
||||
value.Append("command\t").Append(command.Command).Append('\t').Append(command.Operation).Append('\n');
|
||||
return value.ToString();
|
||||
}
|
||||
|
||||
private static void AppendStructure(StringBuilder value, EveSurfaceComponent component)
|
||||
{
|
||||
var kind = NormalizeKind(component.Kind);
|
||||
value.Append(component.Id).Append('\t').Append(kind).Append('\t');
|
||||
foreach (var entry in component.Layout.OrderBy(entry => entry.Key, StringComparer.Ordinal))
|
||||
value.Append("l:").Append(entry.Key).Append('=').Append(entry.Value).Append(';');
|
||||
foreach (var entry in component.Style.OrderBy(entry => entry.Key, StringComparer.Ordinal))
|
||||
value.Append("s:").Append(entry.Key).Append('=').Append(entry.Value).Append(';');
|
||||
foreach (var entry in component.Props.OrderBy(entry => entry.Key, StringComparer.Ordinal))
|
||||
{
|
||||
if (IsDynamicProp(kind, entry.Key)) continue;
|
||||
value.Append("p:").Append(entry.Key).Append('=').Append(entry.Value).Append(';');
|
||||
}
|
||||
value.Append('\n');
|
||||
|
||||
if (EveUiToolkitSurfaceLowerer.IsHidden(component) ||
|
||||
EveUiToolkitSurfaceLowerer.IsExternalProjectionRoot(component.Kind))
|
||||
return;
|
||||
foreach (var child in component.Children)
|
||||
AppendStructure(value, child);
|
||||
}
|
||||
|
||||
private static bool IsDynamicProp(string kind, string key)
|
||||
{
|
||||
if (kind == "metric") return key == "label" || key == "value";
|
||||
if (kind == "progress") return key == "label" || key == "ratio" || key == "value";
|
||||
if (kind == "text") return key == "text" || key == "value" || key == "title";
|
||||
if (kind == "pane" || kind == "modal" || kind == "card")
|
||||
return key == "title";
|
||||
if (string.IsNullOrWhiteSpace(kind) || kind.StartsWith("world.", StringComparison.Ordinal) ||
|
||||
kind.StartsWith("entity.", StringComparison.Ordinal))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string NormalizeKind(string kind)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(kind)) return "";
|
||||
if (kind == "panel.dialogue") return "panel";
|
||||
if (kind.StartsWith("text.", StringComparison.Ordinal)) return "text";
|
||||
if (kind.StartsWith("control.button.", StringComparison.Ordinal)) return "control.button";
|
||||
if (kind.StartsWith("control.text.", StringComparison.Ordinal)) return "control.text";
|
||||
return kind;
|
||||
}
|
||||
|
||||
private static float ParseRatio(string value) =>
|
||||
float.TryParse(
|
||||
value,
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var ratio)
|
||||
? UnityEngine.Mathf.Clamp01(ratio)
|
||||
: 0f;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6f8fa58aaf9a4c39a88263d18fcb75a1
|
||||
|
|
@ -142,6 +142,50 @@ namespace GameCult.Eve.UnityUIToolkit.Tests
|
|||
Assert.That(root.style.bottom.value.value, Is.EqualTo(32f));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PresenterUpdatesCockpitValuesWithoutRebuildingTheSurface()
|
||||
{
|
||||
var presenter = new EveUiToolkitSurfacePresenter();
|
||||
var first = CockpitDocument("0.25", "301.5");
|
||||
|
||||
Assert.That(presenter.Present(first), Is.True);
|
||||
var root = presenter.Root;
|
||||
var hull = root!.Q<ProgressBar>("hull");
|
||||
var temperature = root.Q<VisualElement>("temperature").Q<Label>("value");
|
||||
Assert.That(hull.value, Is.EqualTo(0.25f));
|
||||
Assert.That(temperature.text, Is.EqualTo("301.5"));
|
||||
Assert.That(root.Q<VisualElement>("world-metric"), Is.Null,
|
||||
"Scene projection subtrees must not become empty UI hierarchies.");
|
||||
|
||||
Assert.That(presenter.Present(CockpitDocument("0.75", "318")), Is.False);
|
||||
Assert.That(presenter.Root, Is.SameAs(root));
|
||||
Assert.That(hull.value, Is.EqualTo(0.75f));
|
||||
Assert.That(temperature.text, Is.EqualTo("318"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PresenterRebuildsWhenProviderUiStructureChanges()
|
||||
{
|
||||
var presenter = new EveUiToolkitSurfacePresenter();
|
||||
presenter.Present(CockpitDocument("0.25", "301.5"));
|
||||
var firstRoot = presenter.Root;
|
||||
var changedRoot = Component(
|
||||
"root",
|
||||
"surface",
|
||||
children: new[]
|
||||
{
|
||||
Component("status", "metric", new Dictionary<string, string>
|
||||
{
|
||||
["label"] = "Status",
|
||||
["value"] = "Docked"
|
||||
})
|
||||
});
|
||||
|
||||
Assert.That(presenter.Present(Document(changedRoot)), Is.True);
|
||||
Assert.That(presenter.Root, Is.Not.SameAs(firstRoot));
|
||||
Assert.That(presenter.Root!.Q<VisualElement>("status"), Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void InventoryDropBuildsAdvertisedTypedOperationPayload()
|
||||
{
|
||||
|
|
@ -225,16 +269,45 @@ namespace GameCult.Eve.UnityUIToolkit.Tests
|
|||
Array.Empty<EveCommandTemplate>());
|
||||
}
|
||||
|
||||
private static EveSurfaceDocument CockpitDocument(string hull, string temperature)
|
||||
{
|
||||
var worldMetric = Component("world-metric", "metric", new Dictionary<string, string>
|
||||
{
|
||||
["label"] = "Should not mount",
|
||||
["value"] = "hidden with scene"
|
||||
});
|
||||
var world = Component("world", "world.scene3d", children: new[] { worldMetric });
|
||||
var cockpit = Component(
|
||||
"cockpit",
|
||||
"pane",
|
||||
new Dictionary<string, string> { ["title"] = "Pilot" },
|
||||
new[]
|
||||
{
|
||||
Component("hull", "progress", new Dictionary<string, string>
|
||||
{
|
||||
["label"] = "Hull",
|
||||
["ratio"] = hull
|
||||
}),
|
||||
Component("temperature", "metric", new Dictionary<string, string>
|
||||
{
|
||||
["label"] = "Cockpit",
|
||||
["value"] = temperature
|
||||
})
|
||||
});
|
||||
return Document(Component("root", "surface", children: new[] { world, cockpit }));
|
||||
}
|
||||
|
||||
private static EveSurfaceComponent Component(
|
||||
string id,
|
||||
string kind,
|
||||
IReadOnlyDictionary<string, string>? props = null)
|
||||
IReadOnlyDictionary<string, string>? props = null,
|
||||
IReadOnlyList<EveSurfaceComponent>? children = null)
|
||||
{
|
||||
return new EveSurfaceComponent(
|
||||
id,
|
||||
kind,
|
||||
props ?? EmptyProps(),
|
||||
Array.Empty<EveSurfaceComponent>());
|
||||
children ?? Array.Empty<EveSurfaceComponent>());
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> EmptyProps()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-uitoolkit",
|
||||
"displayName": "Eve UI Toolkit Lowering",
|
||||
"version": "0.1.1",
|
||||
"version": "0.1.2",
|
||||
"unity": "2021.3",
|
||||
"description": "Unity UI Toolkit lowering for gamecult.eve.surface.v1 documents.",
|
||||
"dependencies": {
|
||||
"org.gamecult.eve.surface": "0.1.0"
|
||||
"org.gamecult.eve.surface": "0.2.4"
|
||||
},
|
||||
"keywords": [
|
||||
"Eve",
|
||||
|
|
|
|||
Reference in a new issue