Thing/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs
2026-06-16 20:16:30 +01:00

269 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public sealed class EveUiToolkitSurfaceLowerer
{
private readonly EveUiToolkitSurfaceOptions _options;
public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null)
{
_options = options ?? EveUiToolkitSurfaceOptions.Default;
}
public VisualElement Lower(EveSurfaceDocument document, Action<EveSurfaceCommandRequest>? commandSink = null)
{
if (document == null) throw new ArgumentNullException(nameof(document));
var root = LowerComponent(document.Surface.Root, document, commandSink);
root.name = string.IsNullOrWhiteSpace(root.name) ? document.Surface.Id : root.name;
root.AddToClassList("eve-surface-root");
ApplyStyleTokens(root, document.Surface.Styles);
return root;
}
private VisualElement LowerComponent(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
var element = CreateElement(component, document, commandSink);
element.name = SafeName(component.Id);
element.AddToClassList("eve-component");
element.AddToClassList($"eve-kind-{SafeClass(component.Kind)}");
element.userData = component;
foreach (var child in component.Children)
element.Add(LowerComponent(child, document, commandSink));
return element;
}
private VisualElement CreateElement(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
switch (NormalizeKind(component.Kind))
{
case "surface":
{
var element = new VisualElement();
element.style.flexGrow = 1;
element.style.flexDirection = FlexDirection.Column;
return element;
}
case "grid":
{
var element = new VisualElement();
element.style.flexDirection = FlexDirection.Row;
element.style.flexWrap = Wrap.Wrap;
element.style.alignItems = Align.Stretch;
return element;
}
case "card":
{
var card = new VisualElement();
card.AddToClassList("eve-card");
card.style.flexDirection = FlexDirection.Column;
var title = component.GetProp("title");
if (!string.IsNullOrWhiteSpace(title))
card.Add(TitleLabel(title));
return card;
}
case "metric":
{
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")));
return metric;
}
case "inspector.kv":
{
var inspector = new VisualElement();
inspector.AddToClassList("eve-inspector");
inspector.style.flexDirection = FlexDirection.Column;
return inspector;
}
case "row":
{
var row = new VisualElement();
row.AddToClassList("eve-row");
row.style.flexDirection = FlexDirection.Row;
row.style.flexWrap = Wrap.Wrap;
foreach (var prop in component.Props)
row.Add(FieldLabel(prop.Key, prop.Value));
return row;
}
case "text":
{
return BodyLabel(component.GetProp("text", component.GetProp("value", component.GetProp("title"))));
}
case "control.button":
{
var label = component.GetProp("label", component.GetProp("title", "Invoke"));
var command = component.GetProp("command", component.GetProp("action", "invoke"));
return new Button(() => EmitCommand(document, component, command, commandSink)) { text = label };
}
case "control.text":
{
var label = component.GetProp("label", component.GetProp("title", "Value"));
var command = component.GetProp("command", component.GetProp("action", "set"));
var value = component.GetProp("value");
var field = new TextField(label);
field.SetValueWithoutNotify(value);
field.RegisterValueChangedCallback(evt =>
{
if (string.Equals(evt.newValue, value, StringComparison.Ordinal))
return;
var payload = new Dictionary<string, string>(component.Props, StringComparer.Ordinal)
{
["value"] = evt.newValue ?? ""
};
EmitCommand(document, component, command, payload, commandSink);
});
return field;
}
default:
{
var element = new VisualElement();
element.AddToClassList("eve-unknown");
var title = component.GetProp("title", component.GetProp("label"));
if (!string.IsNullOrWhiteSpace(title))
element.Add(BodyLabel(title));
return element;
}
}
}
private static void EmitCommand(
EveSurfaceDocument document,
EveSurfaceComponent component,
string command,
Action<EveSurfaceCommandRequest>? commandSink)
{
EmitCommand(
document,
component,
command,
new Dictionary<string, string>(component.Props, StringComparer.Ordinal),
commandSink);
}
private static void EmitCommand(
EveSurfaceDocument document,
EveSurfaceComponent component,
string command,
IReadOnlyDictionary<string, string> payload,
Action<EveSurfaceCommandRequest>? commandSink)
{
if (commandSink == null || string.IsNullOrWhiteSpace(command))
return;
commandSink(new EveSurfaceCommandRequest(
document.ProviderId,
document.Surface.Id,
command,
payload,
DateTimeOffset.UtcNow,
"unity-uitoolkit"));
}
private static Label TitleLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-title");
label.style.unityFontStyleAndWeight = FontStyle.Bold;
return label;
}
private static Label BodyLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-text");
return label;
}
private static Label MutedLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-muted");
return label;
}
private static Label ValueLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-value");
label.style.unityFontStyleAndWeight = FontStyle.Bold;
return label;
}
private static VisualElement FieldLabel(string key, string value)
{
var field = new VisualElement();
field.AddToClassList("eve-field");
field.style.flexDirection = FlexDirection.Row;
field.Add(MutedLabel(key));
field.Add(BodyLabel(value));
return field;
}
private static void ApplyStyleTokens(VisualElement root, IReadOnlyList<EveStyleToken> tokens)
{
foreach (var token in tokens)
{
if (string.IsNullOrWhiteSpace(token.Name) || string.IsNullOrWhiteSpace(token.Value))
continue;
root.AddToClassList($"eve-style-token-{SafeClass(token.Name)}");
}
}
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 string SafeName(string value)
{
return string.IsNullOrWhiteSpace(value) ? "eve.component" : value;
}
private static string SafeClass(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "unknown";
var characters = value.ToCharArray();
for (var index = 0; index < characters.Length; index++)
{
if (!char.IsLetterOrDigit(characters[index]))
characters[index] = '-';
}
return new string(characters).Trim('-').ToLowerInvariant();
}
}
}