diff --git a/packages/org.gamecult.eve.surface/README.md b/packages/org.gamecult.eve.surface/README.md new file mode 100644 index 0000000..eb65c17 --- /dev/null +++ b/packages/org.gamecult.eve.surface/README.md @@ -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. diff --git a/packages/org.gamecult.eve.surface/README.md.meta b/packages/org.gamecult.eve.surface/README.md.meta new file mode 100644 index 0000000..4bc751a --- /dev/null +++ b/packages/org.gamecult.eve.surface/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: fe5aa27636bead047b2aecb14fdc59f2 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.surface/Runtime.meta b/packages/org.gamecult.eve.surface/Runtime.meta new file mode 100644 index 0000000..f150917 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2164f6fea1216214d895e7dc99c46ecf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs index 6418b4e..2d88eb5 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs @@ -16,6 +16,7 @@ namespace GameCult.Eve.Surface private readonly List _children = new(); private readonly List _styles = new(); private readonly List _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 build) + { + _root = BuildContainer(id, "partition", Props(("split", "y")), build); + return this; + } + + public EveSurfaceBuilder RootRow(string id, Action build) + { + _root = BuildContainer(id, "partition", Props(("split", "x")), build); + return this; + } + + public EveSurfaceBuilder RootGrid( + string id, + int columns, + int rows, + Action 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 build) + { + _children.Add(BuildContainer(id, "partition", Props(("split", "x")), build)); + return this; + } + + public EveSurfaceBuilder Column(string id, Action build) + { + _children.Add(BuildContainer(id, "partition", Props(("split", "y")), build)); + return this; + } + + public EveSurfaceBuilder Grid(string id, int columns, int rows, Action 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 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 props, + Action 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(StringComparer.Ordinal); } + internal static Dictionary 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 _props; + private readonly Action _addCommand; + private readonly List _children = new(); + private readonly List _stateBindings = new(); + private readonly List _embeddedDocuments = new(); + + internal EveSurfaceContainerBuilder( + string id, + string kind, + IReadOnlyDictionary props, + Action addCommand) + { + _id = id ?? ""; + _kind = kind ?? "partition"; + _props = new Dictionary(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 build) + { + _children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "x")), build)); + return this; + } + + public EveSurfaceContainerBuilder Column(string id, Action build) + { + _children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "y")), build)); + return this; + } + + public EveSurfaceContainerBuilder Grid( + string id, + int columns, + int rows, + Action 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 build) + { + _children.Add(BuildContainer(id, "pane", EveSurfaceBuilder.Props(("title", title ?? "")), build)); + return this; + } + + public EveSurfaceContainerBuilder Card(string id, string title, Action build) + { + _children.Add(BuildContainer(id, "card", EveSurfaceBuilder.Props(("title", title ?? "")), build)); + return this; + } + + public EveSurfaceContainerBuilder Modal(string id, string title, Action build) + { + _children.Add(BuildContainer(id, "modal", EveSurfaceBuilder.Props(("title", title ?? "")), build)); + return this; + } + + public EveSurfaceContainerBuilder FieldRow( + string id, + string label, + Action 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? 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 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(), + 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(), + Array.Empty(), + 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 props, + CultMeshStateBindingDescriptor? binding = null, + IReadOnlyList? children = null, + Action? configure = null) + { + var builder = new EveSurfaceContainerBuilder(id, kind, props, _addCommand); + configure?.Invoke(builder); + var component = new EveSurfaceComponent( + id ?? "", + kind ?? "", + new Dictionary(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal), + children ?? Array.Empty(), + binding == null ? Array.Empty() : new[] { binding }); + if (builder._props.Count != (props?.Count ?? 0)) + { + component = new EveSurfaceComponent( + id ?? "", + kind ?? "", + new Dictionary(builder._props, StringComparer.Ordinal), + children ?? Array.Empty(), + binding == null ? Array.Empty() : 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 props, + Action 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; diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs.meta new file mode 100644 index 0000000..adc41da --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 97ac7f757692aef4db64d75d0df6fe51 \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs index 9a52ea9..0e5a3a3 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs @@ -128,7 +128,9 @@ namespace GameCult.Eve.Surface IReadOnlyDictionary props, IReadOnlyList children, IReadOnlyList stateBindings, - IReadOnlyList embeddedDocuments) + IReadOnlyList embeddedDocuments, + IReadOnlyDictionary? layout = null, + IReadOnlyDictionary? style = null) { Id = id ?? ""; Kind = kind ?? ""; @@ -136,6 +138,8 @@ namespace GameCult.Eve.Surface Children = children ?? Array.Empty(); StateBindings = stateBindings ?? Array.Empty(); EmbeddedDocuments = embeddedDocuments ?? Array.Empty(); + Layout = layout ?? new Dictionary(StringComparer.Ordinal); + Style = style ?? new Dictionary(StringComparer.Ordinal); } [Key(0)] @@ -156,6 +160,12 @@ namespace GameCult.Eve.Surface [Key(5)] public IReadOnlyList EmbeddedDocuments { get; } + [Key(6)] + public IReadOnlyDictionary Layout { get; } + + [Key(7)] + public IReadOnlyDictionary Style { get; } + public string GetProp(string key, string fallback = "") { return Props.TryGetValue(key, out var value) ? value : fallback; diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs.meta new file mode 100644 index 0000000..ebd5f02 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6516c42104b235f42b15efeb4639bb73 \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef.meta b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef.meta new file mode 100644 index 0000000..d8f543e --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a9004278c14816345941af2f2ce673ca +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.surface/package.json.meta b/packages/org.gamecult.eve.surface/package.json.meta new file mode 100644 index 0000000..8e89223 --- /dev/null +++ b/packages/org.gamecult.eve.surface/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 49f1738bd94075e489c82190cf660b1d +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-uitoolkit/README.md b/packages/org.gamecult.eve.unity-uitoolkit/README.md index 2bce5f8..57498ba 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/README.md +++ b/packages/org.gamecult.eve.unity-uitoolkit/README.md @@ -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. diff --git a/packages/org.gamecult.eve.unity-uitoolkit/README.md.meta b/packages/org.gamecult.eve.unity-uitoolkit/README.md.meta new file mode 100644 index 0000000..8844c0d --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/README.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 9dabe4b47bd743f41a4c0903fd309eae +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime.meta b/packages/org.gamecult.eve.unity-uitoolkit/Runtime.meta new file mode 100644 index 0000000..b653c25 --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 40e1d9868e9fdbf408ac5bc46239c817 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs index d28fb8f..edebc77 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -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(StringComparer.Ordinal); + var style = component.Style ?? new Dictionary(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 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 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)) diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs.meta b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs.meta new file mode 100644 index 0000000..ab8e870 --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bef5882c69652c54fb26b07d63e5bcdd \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs index 53cac24..4b04a38 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs @@ -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? embeddedDocumentResolver = null) + { + EmbeddedDocumentResolver = embeddedDocumentResolver; + } + + public Func? EmbeddedDocumentResolver { get; } } } diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs.meta b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs.meta new file mode 100644 index 0000000..a0208aa --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e2fd027205cc9214eaa7dd84fe75d7bd \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/GameCult.Eve.UnityUIToolkit.asmdef.meta b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/GameCult.Eve.UnityUIToolkit.asmdef.meta new file mode 100644 index 0000000..e9be7c4 --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/GameCult.Eve.UnityUIToolkit.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 093779c1124fb5241b9c16e18173520a +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-uitoolkit/package.json.meta b/packages/org.gamecult.eve.unity-uitoolkit/package.json.meta new file mode 100644 index 0000000..5f395fc --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 6c6c93dde27b8dc42bf3866cd5c6c3cb +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: