Expand Eve surface package lowering support
This commit is contained in:
parent
a3c9dcd2a0
commit
614844ee2b
18 changed files with 858 additions and 2 deletions
29
packages/org.gamecult.eve.surface/README.md
Normal file
29
packages/org.gamecult.eve.surface/README.md
Normal 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.
|
||||
7
packages/org.gamecult.eve.surface/README.md.meta
Normal file
7
packages/org.gamecult.eve.surface/README.md.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fe5aa27636bead047b2aecb14fdc59f2
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
packages/org.gamecult.eve.surface/Runtime.meta
Normal file
8
packages/org.gamecult.eve.surface/Runtime.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2164f6fea1216214d895e7dc99c46ecf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 97ac7f757692aef4db64d75d0df6fe51
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6516c42104b235f42b15efeb4639bb73
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a9004278c14816345941af2f2ce673ca
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
7
packages/org.gamecult.eve.surface/package.json.meta
Normal file
7
packages/org.gamecult.eve.surface/package.json.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 49f1738bd94075e489c82190cf660b1d
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
7
packages/org.gamecult.eve.unity-uitoolkit/README.md.meta
Normal file
7
packages/org.gamecult.eve.unity-uitoolkit/README.md.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9dabe4b47bd743f41a4c0903fd309eae
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
packages/org.gamecult.eve.unity-uitoolkit/Runtime.meta
Normal file
8
packages/org.gamecult.eve.unity-uitoolkit/Runtime.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 40e1d9868e9fdbf408ac5bc46239c817
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bef5882c69652c54fb26b07d63e5bcdd
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e2fd027205cc9214eaa7dd84fe75d7bd
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 093779c1124fb5241b9c16e18173520a
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6c6c93dde27b8dc42bf3866cd5c6c3cb
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
Reference in a new issue