Expand Eve surface package lowering support

This commit is contained in:
Meta Vens 2026-07-08 07:40:59 +01:00
commit 614844ee2b
18 changed files with 858 additions and 2 deletions

View file

@ -0,0 +1,29 @@
# Eve Surface Contract
This package defines the retained CultUI surface document that every Eve
runtime lowers.
Components own normal child layout through `Children`, live CultMesh value
bindings through `StateBindings`, and nested synced document regions through
`EmbeddedDocuments`. An embedded document slot names the child document id,
schema, presentation kind, and route hint; renderers resolve that child through
CultMesh instead of rebuilding it with runtime-local projection code.
See `../../docs/surface-contract-v1.md` for the full contract and
`../../web/fixtures/cultui-embedded-surface.json` for the parity fixture.
Runtime discovery and required feature coverage live in
`../../tools/parity/parity-manifest.json`; active GUI runtimes must list
`embeddedDocuments` as a supported feature and require the `embedded-surface`
fixture.
Verification entrypoints:
- Browser/TypeScript: `node --test web\eve-dsl.test.mjs`
- Semantic runtime matrix: `powershell -ExecutionPolicy Bypass -File .\scripts\run-parity-harness.ps1`
- Flutter GUI contract, when Flutter is installed: `flutter test --plain-name embedded_surface_fixture_contract`
- Rust/CultMesh document sync: run `cargo test rust_preserves_cultui_embedded_surface_slots_through_typed_document_sync` from `E:/Projects/CultLib/packages/cultnet-rs`
The parity manifest is the discovery source for web, Flutter desktop/Linux/Android,
iOS/UIKit, Android/Kotlin, Unity UI Toolkit, and Rust contract coverage. Do not
add a new renderer claim without adding its nested-surface fixture requirement
there.

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fe5aa27636bead047b2aecb14fdc59f2
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2164f6fea1216214d895e7dc99c46ecf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -16,6 +16,7 @@ namespace GameCult.Eve.Surface
private readonly List<EveSurfaceComponent> _children = new();
private readonly List<EveStyleToken> _styles = new();
private readonly List<EveCommandTemplate> _commands = new();
private EveSurfaceComponent? _root;
private long _version = 1;
private string _providerId = "gamecult";
private string _providerKind = "cultui.surface";
@ -90,6 +91,64 @@ namespace GameCult.Eve.Surface
return this;
}
public EveSurfaceBuilder RootColumn(string id, Action<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(id, "partition", Props(("split", "y")), build);
return this;
}
public EveSurfaceBuilder RootRow(string id, Action<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(id, "partition", Props(("split", "x")), build);
return this;
}
public EveSurfaceBuilder RootGrid(
string id,
int columns,
int rows,
Action<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(
id,
"grid",
Props(
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
build);
return this;
}
public EveSurfaceBuilder Row(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", Props(("split", "x")), build));
return this;
}
public EveSurfaceBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", Props(("split", "y")), build));
return this;
}
public EveSurfaceBuilder Grid(string id, int columns, int rows, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(
id,
"grid",
Props(
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
build));
return this;
}
public EveSurfaceBuilder Modal(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "modal", Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceBuilder EmbeddedDocument(
string slotId,
string documentId,
@ -149,7 +208,7 @@ namespace GameCult.Eve.Surface
string.IsNullOrWhiteSpace(_updatedAtUtc) ? DateTime.UtcNow.ToString("O") : _updatedAtUtc,
new EveSurfaceTree(
_surfaceId,
new EveSurfaceComponent(
_root ?? new EveSurfaceComponent(
$"{_surfaceId}.root",
"surface",
EmptyProps(),
@ -167,6 +226,18 @@ namespace GameCult.Eve.Surface
return this;
}
private EveSurfaceComponent BuildContainer(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
Action<EveSurfaceContainerBuilder> build)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var container = new EveSurfaceContainerBuilder(id, kind, props, AddCommand);
build(container);
return container.Build();
}
private void AddCommand(EveCommandTemplate command)
{
if (command != null)
@ -208,6 +279,12 @@ namespace GameCult.Eve.Surface
return new Dictionary<string, string>(StringComparer.Ordinal);
}
internal static Dictionary<string, string> Props(params (string Key, string Value)[] values)
{
return (values ?? Array.Empty<(string Key, string Value)>())
.ToDictionary(value => value.Key, value => value.Value ?? "", StringComparer.Ordinal);
}
internal static string Slug(string? value)
{
if (string.IsNullOrWhiteSpace(value))
@ -230,6 +307,426 @@ namespace GameCult.Eve.Surface
}
}
public sealed class EveSurfaceContainerBuilder
{
private readonly string _id;
private readonly string _kind;
private readonly Dictionary<string, string> _props;
private readonly Action<EveCommandTemplate> _addCommand;
private readonly List<EveSurfaceComponent> _children = new();
private readonly List<CultMeshStateBindingDescriptor> _stateBindings = new();
private readonly List<EveEmbeddedDocumentSlot> _embeddedDocuments = new();
internal EveSurfaceContainerBuilder(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
Action<EveCommandTemplate> addCommand)
{
_id = id ?? "";
_kind = kind ?? "partition";
_props = new Dictionary<string, string>(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal);
_addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand));
}
public EveSurfaceContainerBuilder Gap(int value) => Prop("gap", value);
public EveSurfaceContainerBuilder Padding(int value) => Prop("padding", value);
public EveSurfaceContainerBuilder Size(string value) => Prop("size", value);
public EveSurfaceContainerBuilder Min(string value) => Prop("min", value);
public EveSurfaceContainerBuilder Max(string value) => Prop("max", value);
public EveSurfaceContainerBuilder Align(string value) => Prop("align", value);
public EveSurfaceContainerBuilder Clip(string value = "hidden") => Prop("clip", value);
public EveSurfaceContainerBuilder Scroll(string value = "y") => Prop("scroll", value);
public EveSurfaceContainerBuilder Region(string value) => Prop("region", value);
public EveSurfaceContainerBuilder Bind(CultMeshStateBindingDescriptor binding)
{
if (binding != null)
_stateBindings.Add(binding);
return this;
}
public EveSurfaceContainerBuilder Row(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "x")), build));
return this;
}
public EveSurfaceContainerBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "y")), build));
return this;
}
public EveSurfaceContainerBuilder Grid(
string id,
int columns,
int rows,
Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(
id,
"grid",
EveSurfaceBuilder.Props(
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
build));
return this;
}
public EveSurfaceContainerBuilder Pane(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "pane", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder Card(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "card", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder Modal(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "modal", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder FieldRow(
string id,
string label,
Action<EveSurfaceContainerBuilder> build)
{
return Row(id, row => row
.Gap(8)
.Label($"{id}.label", label, labelPart => labelPart.Size("12rem"))
.Column($"{id}.field", field =>
{
field.Size("1fr");
build(field);
}));
}
public EveSurfaceContainerBuilder Label(
string id,
string text,
Action<EveSurfaceContainerBuilder>? configure = null)
{
return Leaf(id, "label", EveSurfaceBuilder.Props(("value", text ?? "")), null, null, configure);
}
public EveSurfaceContainerBuilder Text(string id, string text)
{
return Leaf(id, "text", EveSurfaceBuilder.Props(("value", text ?? "")));
}
public EveSurfaceContainerBuilder Title(string id, string text)
{
return Leaf(id, "text.title", EveSurfaceBuilder.Props(("value", text ?? "")));
}
public EveSurfaceContainerBuilder Metric(
string id,
string label,
string value,
CultMeshStateBindingDescriptor? binding = null)
{
return Leaf(
id,
"metric",
EveSurfaceBuilder.Props(("label", label ?? ""), ("value", value ?? "")),
binding);
}
public EveSurfaceContainerBuilder Progress(
string id,
string label,
string value,
string min,
string max,
CultMeshStateBindingDescriptor? binding = null)
{
return Leaf(
id,
"progress",
EveSurfaceBuilder.Props(
("label", label ?? ""),
("value", value ?? ""),
("min", min ?? ""),
("max", max ?? "")),
binding);
}
public EveSurfaceContainerBuilder Button(string id, string label, string command)
{
return Button(id, label, CultMesh.OperationBinding(command, label));
}
public EveSurfaceContainerBuilder Button(
string id,
string label,
CultMeshOperationBindingDescriptor operation)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
_children.Add(EveSurfaceBuilder.ButtonComponent(id, label, operation));
return this;
}
public EveSurfaceContainerBuilder PopupButton(
string id,
string label,
string title,
Action<EveSurfaceContainerBuilder> build,
string anchor = "viewport",
string placement = "center",
int offsetX = 0,
int offsetY = 0)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var popup = BuildContainer(
$"{id}.popup",
"modal",
EveSurfaceBuilder.Props(
("title", title ?? label ?? ""),
("anchor", anchor ?? "viewport"),
("placement", placement ?? "center"),
("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("presentation", "popover")),
build);
_children.Add(new EveSurfaceComponent(
id ?? "",
"control.popup",
EveSurfaceBuilder.Props(
("label", label ?? ""),
("title", title ?? label ?? ""),
("anchor", anchor ?? "viewport"),
("placement", placement ?? "center"),
("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture))),
new[] { popup }));
return this;
}
public EveSurfaceContainerBuilder EnumField(
string id,
string label,
string value,
CultMeshStateBindingDescriptor optionsBinding,
CultMeshOperationBindingDescriptor setValue,
string anchor = "trigger",
string placement = "right-down")
{
return FieldRow(id, label, field => field.PopupButton(
$"{id}.dropdown",
value,
label,
popup => popup.OptionList($"{id}.options", label, optionsBinding, setValue),
anchor,
placement));
}
public EveSurfaceContainerBuilder OptionList(
string id,
string label,
CultMeshStateBindingDescriptor itemsBinding,
CultMeshOperationBindingDescriptor operation)
{
if (itemsBinding == null) throw new ArgumentNullException(nameof(itemsBinding));
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
_children.Add(new EveSurfaceComponent(
id ?? "",
"options",
EveSurfaceBuilder.Props(
("label", label ?? ""),
("command", operation.OperationId),
("operationId", operation.OperationId),
("schemaId", operation.SchemaId)),
Array.Empty<EveSurfaceComponent>(),
new[] { itemsBinding }));
return this;
}
public EveSurfaceContainerBuilder TextInput(
string id,
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control("control.input.text", id, label, value, operation, binding);
}
public EveSurfaceContainerBuilder Toggle(
string id,
string label,
bool value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control("control.toggle", id, label, value ? "true" : "false", operation, binding);
}
public EveSurfaceContainerBuilder Slider(
string id,
string label,
string value,
string min,
string max,
string step,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control(
"control.slider",
id,
label,
value,
operation,
binding,
("min", min ?? ""),
("max", max ?? ""),
("step", step ?? ""));
}
public EveSurfaceContainerBuilder Select(
string id,
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control("control.select", id, label, value, operation, binding);
}
public EveSurfaceContainerBuilder EmbeddedDocument(
string id,
string slotId,
string documentId,
string schemaId,
string presentationKind,
CultMeshRouteHint? routeHint = null)
{
_children.Add(new EveSurfaceComponent(
id ?? "",
"surface.slot",
EveSurfaceBuilder.Props(
("slotId", slotId ?? ""),
("documentId", documentId ?? ""),
("schemaId", schemaId ?? ""),
("presentationKind", presentationKind ?? "")),
Array.Empty<EveSurfaceComponent>(),
Array.Empty<CultMeshStateBindingDescriptor>(),
new[]
{
new EveEmbeddedDocumentSlot(
slotId ?? "",
documentId ?? "",
schemaId ?? "",
presentationKind ?? "",
routeHint)
}));
return this;
}
internal EveSurfaceComponent Build()
{
return new EveSurfaceComponent(
_id,
_kind,
_props,
_children.ToArray(),
_stateBindings.ToArray(),
_embeddedDocuments.ToArray());
}
private EveSurfaceContainerBuilder Prop(string key, int value)
{
_props[key] = value.ToString(System.Globalization.CultureInfo.InvariantCulture);
return this;
}
private EveSurfaceContainerBuilder Prop(string key, string value)
{
_props[key] = value ?? "";
return this;
}
private EveSurfaceContainerBuilder Leaf(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
CultMeshStateBindingDescriptor? binding = null,
IReadOnlyList<EveSurfaceComponent>? children = null,
Action<EveSurfaceContainerBuilder>? configure = null)
{
var builder = new EveSurfaceContainerBuilder(id, kind, props, _addCommand);
configure?.Invoke(builder);
var component = new EveSurfaceComponent(
id ?? "",
kind ?? "",
new Dictionary<string, string>(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal),
children ?? Array.Empty<EveSurfaceComponent>(),
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : new[] { binding });
if (builder._props.Count != (props?.Count ?? 0))
{
component = new EveSurfaceComponent(
id ?? "",
kind ?? "",
new Dictionary<string, string>(builder._props, StringComparer.Ordinal),
children ?? Array.Empty<EveSurfaceComponent>(),
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : new[] { binding });
}
_children.Add(component);
return this;
}
private EveSurfaceContainerBuilder Control(
string kind,
string id,
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding,
params (string Key, string Value)[] extraProps)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
var props = EveSurfaceBuilder.Props(
new[]
{
("label", label ?? ""),
("value", value ?? ""),
("command", operation.OperationId),
("operationId", operation.OperationId),
("schemaId", operation.SchemaId)
}.Concat(extraProps ?? Array.Empty<(string Key, string Value)>()).ToArray());
return Leaf(kind: kind, id: id, props: props, binding: binding);
}
private EveSurfaceComponent BuildContainer(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
Action<EveSurfaceContainerBuilder> build)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var container = new EveSurfaceContainerBuilder(id, kind, props, _addCommand);
build(container);
return container.Build();
}
}
public sealed class EveSurfaceGroupBuilder
{
private readonly string _id;

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97ac7f757692aef4db64d75d0df6fe51

View file

@ -128,7 +128,9 @@ namespace GameCult.Eve.Surface
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children,
IReadOnlyList<CultMeshStateBindingDescriptor> stateBindings,
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments)
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments,
IReadOnlyDictionary<string, string>? layout = null,
IReadOnlyDictionary<string, string>? style = null)
{
Id = id ?? "";
Kind = kind ?? "";
@ -136,6 +138,8 @@ namespace GameCult.Eve.Surface
Children = children ?? Array.Empty<EveSurfaceComponent>();
StateBindings = stateBindings ?? Array.Empty<CultMeshStateBindingDescriptor>();
EmbeddedDocuments = embeddedDocuments ?? Array.Empty<EveEmbeddedDocumentSlot>();
Layout = layout ?? new Dictionary<string, string>(StringComparer.Ordinal);
Style = style ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
[Key(0)]
@ -156,6 +160,12 @@ namespace GameCult.Eve.Surface
[Key(5)]
public IReadOnlyList<EveEmbeddedDocumentSlot> EmbeddedDocuments { get; }
[Key(6)]
public IReadOnlyDictionary<string, string> Layout { get; }
[Key(7)]
public IReadOnlyDictionary<string, string> Style { get; }
public string GetProp(string key, string fallback = "")
{
return Props.TryGetValue(key, out var value) ? value : fallback;

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6516c42104b235f42b15efeb4639bb73

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a9004278c14816345941af2f2ce673ca
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 49f1738bd94075e489c82190cf660b1d
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -10,3 +10,23 @@ component kinds degrade to inert containers instead of gaining local semantics.
This package lives in the Eve repository as the shared Unity lowering target.
Aetheria and other Unity consumers should import it from Eve instead of
carrying local copies.
Nested CultUI regions use component `EmbeddedDocuments`. Pass an
`EmbeddedDocumentResolver` through `EveUiToolkitSurfaceOptions`; the lowerer
mounts the resolved child surface under the slot while preserving the child
document's command surface id.
Discovery and test coverage for this feature live in the shared Eve parity
matrix: `../../tools/parity/parity-manifest.json` names `embeddedDocuments` and
`../../web/fixtures/cultui-embedded-surface.json` is the canonical fixture.
Unity consumers should keep verifier coverage that builds a parent surface with
a resolver-backed child, then checks for the embedded visual element rather than
copying child state into a local adapter.
For Aetheria, the Unity evidence path is:
- `dotnet build GameCult.Aetheria.State.Unity.csproj --no-restore --nologo -v:minimal`
- `dotnet run --project Aetheria.State.Verify\Aetheria.State.Verify.csproj --no-restore`
Those checks sit alongside Eve's shared browser, Flutter, iOS, Android/Kotlin,
and Rust contract tests, all discoverable from the parity manifest.

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9dabe4b47bd743f41a4c0903fd309eae
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 40e1d9868e9fdbf408ac5bc46239c817
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using GameCult.Eve.Surface;
using UnityEngine;
using UnityEngine.UIElements;
@ -38,6 +40,7 @@ namespace GameCult.Eve.UnityUIToolkit
element.AddToClassList("eve-component");
element.AddToClassList($"eve-kind-{SafeClass(component.Kind)}");
element.userData = component;
ApplyGeneratedLayout(element, component);
foreach (var child in component.Children)
element.Add(LowerComponent(child, document, commandSink));
@ -90,6 +93,17 @@ namespace GameCult.Eve.UnityUIToolkit
element.style.alignItems = Align.Stretch;
return element;
}
case "partition":
{
var element = new VisualElement();
element.style.flexDirection = component.GetProp("split") == "x"
? FlexDirection.Row
: FlexDirection.Column;
element.style.flexWrap = Wrap.Wrap;
return element;
}
case "pane":
case "modal":
case "card":
{
var card = new VisualElement();
@ -109,6 +123,25 @@ namespace GameCult.Eve.UnityUIToolkit
metric.Add(ValueLabel(component.GetProp("value")));
return metric;
}
case "progress":
{
var progress = new VisualElement();
progress.AddToClassList("eve-progress");
progress.style.flexDirection = FlexDirection.Column;
progress.Add(MutedLabel(component.GetProp("label")));
progress.Add(ValueLabel(component.GetProp("value")));
return progress;
}
case "options":
{
var options = new VisualElement();
options.AddToClassList("eve-options");
options.style.flexDirection = FlexDirection.Column;
var label = component.GetProp("label");
if (!string.IsNullOrWhiteSpace(label))
options.Add(TitleLabel(label));
return options;
}
case "inspector.kv":
{
var inspector = new VisualElement();
@ -131,12 +164,14 @@ namespace GameCult.Eve.UnityUIToolkit
return BodyLabel(component.GetProp("text", component.GetProp("value", component.GetProp("title"))));
}
case "control.button":
case "control.popup":
{
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":
case "control.input.text":
{
var label = component.GetProp("label", component.GetProp("title", "Value"));
var command = component.GetProp("command", component.GetProp("action", "set"));
@ -265,6 +300,194 @@ namespace GameCult.Eve.UnityUIToolkit
}
}
private static void ApplyGeneratedLayout(VisualElement element, EveSurfaceComponent component)
{
var layout = component.Layout ?? new Dictionary<string, string>(StringComparer.Ordinal);
var style = component.Style ?? new Dictionary<string, string>(StringComparer.Ordinal);
if (TryGet(layout, "display", out var display))
element.style.display = string.Equals(display, "none", StringComparison.OrdinalIgnoreCase)
? DisplayStyle.None
: DisplayStyle.Flex;
if (TryGet(layout, "direction", out var direction))
{
if (string.Equals(direction, "horizontal", StringComparison.OrdinalIgnoreCase) ||
string.Equals(direction, "row", StringComparison.OrdinalIgnoreCase))
element.style.flexDirection = FlexDirection.Row;
else if (string.Equals(direction, "vertical", StringComparison.OrdinalIgnoreCase) ||
string.Equals(direction, "column", StringComparison.OrdinalIgnoreCase))
element.style.flexDirection = FlexDirection.Column;
}
if (TryGet(layout, "flexWrap", out var flexWrap))
element.style.flexWrap = string.Equals(flexWrap, "wrap", StringComparison.OrdinalIgnoreCase) ? Wrap.Wrap : Wrap.NoWrap;
if (TryGet(layout, "alignItems", out var alignItems) && TryParseAlign(alignItems, out var align))
element.style.alignItems = align;
if (TryGet(layout, "justifyContent", out var justifyContent) && TryParseJustify(justifyContent, out var justify))
element.style.justifyContent = justify;
if (TryGet(layout, "width", out var width))
element.style.width = ParseLength(width);
if (TryGet(layout, "minWidth", out var minWidth))
element.style.minWidth = ParseLength(minWidth);
if (TryGet(layout, "maxWidth", out var maxWidth))
element.style.maxWidth = ParseLength(maxWidth);
if (TryGet(layout, "height", out var height))
element.style.height = ParseLength(height);
if (TryGet(layout, "minHeight", out var minHeight))
element.style.minHeight = ParseLength(minHeight);
if (TryGet(layout, "maxHeight", out var maxHeight))
element.style.maxHeight = ParseLength(maxHeight);
if (TryGet(layout, "padding", out var padding))
ApplyBoxLength(padding, value =>
{
element.style.paddingTop = value;
element.style.paddingRight = value;
element.style.paddingBottom = value;
element.style.paddingLeft = value;
});
if (TryGet(layout, "margin", out var margin))
ApplyBoxLength(margin, value =>
{
element.style.marginTop = value;
element.style.marginRight = value;
element.style.marginBottom = value;
element.style.marginLeft = value;
});
if (TryGet(layout, "overflow", out var overflow))
element.style.overflow = string.Equals(overflow, "hidden", StringComparison.OrdinalIgnoreCase)
? Overflow.Hidden
: Overflow.Visible;
if (TryGet(style, "background", out var background) && TryParseColor(background, out var backgroundColor))
element.style.backgroundColor = backgroundColor;
if (TryGet(style, "color", out var color) && TryParseColor(color, out var textColor))
element.style.color = textColor;
if (TryGet(style, "borderWidth", out var borderWidth))
ApplyBoxLength(borderWidth, value =>
{
element.style.borderTopWidth = value;
element.style.borderRightWidth = value;
element.style.borderBottomWidth = value;
element.style.borderLeftWidth = value;
});
if (TryGet(style, "borderColor", out var borderColor) && TryParseColor(borderColor, out var border))
{
element.style.borderTopColor = border;
element.style.borderRightColor = border;
element.style.borderBottomColor = border;
element.style.borderLeftColor = border;
}
if (TryGet(style, "borderRadius", out var borderRadius))
ApplyBoxLength(borderRadius, value =>
{
element.style.borderTopLeftRadius = value;
element.style.borderTopRightRadius = value;
element.style.borderBottomRightRadius = value;
element.style.borderBottomLeftRadius = value;
});
if (TryGet(style, "fontSize", out var fontSize))
element.style.fontSize = ParseLength(fontSize);
if (TryGet(style, "fontWeight", out var fontWeight) &&
(string.Equals(fontWeight, "bold", StringComparison.OrdinalIgnoreCase) || fontWeight == "700"))
element.style.unityFontStyleAndWeight = FontStyle.Bold;
}
private static bool TryGet(IReadOnlyDictionary<string, string> values, string key, out string value)
{
if (values.TryGetValue(key, out value) && !string.IsNullOrWhiteSpace(value))
return true;
value = "";
return false;
}
private static StyleLength ParseLength(string value)
{
value = FirstToken(value);
if (string.Equals(value, "auto", StringComparison.OrdinalIgnoreCase))
return StyleKeyword.Auto;
if (value.EndsWith("%", StringComparison.Ordinal) &&
float.TryParse(value.Substring(0, value.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out var percent))
return new Length(percent, LengthUnit.Percent);
if (value.EndsWith("px", StringComparison.OrdinalIgnoreCase))
value = value.Substring(0, value.Length - 2);
if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var pixels))
return new Length(pixels, LengthUnit.Pixel);
return StyleKeyword.Null;
}
private static void ApplyBoxLength(string value, Action<StyleLength> apply)
{
apply(ParseLength(value));
}
private static bool TryParseColor(string value, out Color color)
{
value = value.Trim();
if (value.StartsWith("var(", StringComparison.Ordinal))
{
color = default;
return false;
}
return ColorUtility.TryParseHtmlString(value, out color);
}
private static bool TryParseAlign(string value, out Align align)
{
switch (value)
{
case "center":
align = Align.Center;
return true;
case "end":
case "flex-end":
align = Align.FlexEnd;
return true;
case "stretch":
align = Align.Stretch;
return true;
case "start":
case "flex-start":
align = Align.FlexStart;
return true;
default:
align = Align.Auto;
return false;
}
}
private static bool TryParseJustify(string value, out Justify justify)
{
switch (value)
{
case "center":
justify = Justify.Center;
return true;
case "end":
case "flex-end":
justify = Justify.FlexEnd;
return true;
case "space-between":
justify = Justify.SpaceBetween;
return true;
case "space-around":
justify = Justify.SpaceAround;
return true;
case "start":
case "flex-start":
justify = Justify.FlexStart;
return true;
default:
justify = Justify.FlexStart;
return false;
}
}
private static string FirstToken(string value)
{
return (value ?? "").Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
}
private static string NormalizeKind(string kind)
{
if (string.IsNullOrWhiteSpace(kind))

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bef5882c69652c54fb26b07d63e5bcdd

View file

@ -1,3 +1,6 @@
using System;
using GameCult.Eve.Surface;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
@ -5,5 +8,13 @@ namespace GameCult.Eve.UnityUIToolkit
public sealed class EveUiToolkitSurfaceOptions
{
public static EveUiToolkitSurfaceOptions Default { get; } = new EveUiToolkitSurfaceOptions();
public EveUiToolkitSurfaceOptions(
Func<EveEmbeddedDocumentSlot, EveSurfaceDocument?>? embeddedDocumentResolver = null)
{
EmbeddedDocumentResolver = embeddedDocumentResolver;
}
public Func<EveEmbeddedDocumentSlot, EveSurfaceDocument?>? EmbeddedDocumentResolver { get; }
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e2fd027205cc9214eaa7dd84fe75d7bd

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 093779c1124fb5241b9c16e18173520a
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 6c6c93dde27b8dc42bf3866cd5c6c3cb
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: