From 44390549775ec535ee31fc60a5bc594f372f4147 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 20 Jul 2026 04:49:45 +0100 Subject: [PATCH] Lower live Eve surfaces without rebuilding UI state --- .../Runtime/EveUiToolkitSurfaceLowerer.cs | 32 +++- .../Runtime/EveUiToolkitSurfacePresenter.cs | 153 ++++++++++++++++++ .../EveUiToolkitSurfacePresenter.cs.meta | 2 + .../Editor/EveUiToolkitSurfaceLowererTests.cs | 77 ++++++++- .../package.json | 4 +- 5 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs create mode 100644 packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs.meta diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs index 9cdc677..00809f2 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -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; diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs new file mode 100644 index 0000000..812ef72 --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs @@ -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? 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() + .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 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