From fb5b492c04945eb5b59415feff63eee11ebdce5d Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 1 Jun 2026 14:53:56 +0100 Subject: [PATCH] Refine portable dashboard layout lowering --- .../Ui/AquariumUi.cs | 34 ++++++++++++++----- src/Aquarium.Engine/Render/D3D12Renderer.cs | 32 +++++++++++------ .../Render/DirectWriteOverlay.cs | 29 +++++++++++----- state/evidence.jsonl | 1 + state/map.yaml | 4 +++ 5 files changed, 73 insertions(+), 27 deletions(-) diff --git a/src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs b/src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs index 1abb797..d6bbdf3 100644 --- a/src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs +++ b/src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs @@ -33,6 +33,21 @@ public sealed class AquariumUiDocument } public AquariumUiDocument Surface(string id, string title, float left, float top, float width, float height, Action compose) + { + return Surface(id, title, left, top, width, height, contentPadding: 8.0f, rootGap: 8.0f, rootPadding: 8.0f, compose); + } + + public AquariumUiDocument Surface( + string id, + string title, + float left, + float top, + float width, + float height, + float contentPadding, + float rootGap, + float rootPadding, + Action compose) { var children = new List(); compose(new AquariumUiSurfaceBuilder(children)); @@ -41,7 +56,8 @@ public sealed class AquariumUiDocument id, title, new AquariumUiRect(left, top, width, height), - new AquariumUiElement(id + ".root", "dashboard", title, Role: "root", Layout: AquariumUiLayout.Vertical(8.0f, 8.0f), Children: children))); + new AquariumUiElement(id + ".root", "dashboard", title, Role: "root", Layout: AquariumUiLayout.Vertical(rootGap, rootPadding), Children: children), + Math.Max(0.0f, contentPadding))); return this; } @@ -65,7 +81,8 @@ public sealed record AquariumUiSurface( string Id, string Title, AquariumUiRect Bounds, - AquariumUiElement Root); + AquariumUiElement Root, + float ContentPadding = 8.0f); public sealed record AquariumUiRect(float Left, float Top, float Width, float Height); @@ -138,6 +155,7 @@ public sealed record AquariumUiElement( Action? Invoke = null, Func? IsVisible = null, float Weight = 1.0f, + float? FixedExtent = null, IReadOnlyList? Children = null) { public bool Visible => IsVisible?.Invoke() ?? true; @@ -145,19 +163,19 @@ public sealed record AquariumUiElement( public sealed class AquariumUiSurfaceBuilder(List children) { - public AquariumUiSurfaceBuilder Vertical(string id, Action compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func? isVisible = null) + public AquariumUiSurfaceBuilder Vertical(string id, Action compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func? isVisible = null, float? fixedExtent = null) { var scopeChildren = new List(); compose(new AquariumUiSurfaceBuilder(scopeChildren)); - children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Vertical(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), Children: scopeChildren)); + children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Vertical(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: scopeChildren)); return this; } - public AquariumUiSurfaceBuilder Horizontal(string id, Action compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func? isVisible = null) + public AquariumUiSurfaceBuilder Horizontal(string id, Action compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func? isVisible = null, float? fixedExtent = null) { var scopeChildren = new List(); compose(new AquariumUiSurfaceBuilder(scopeChildren)); - children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Horizontal(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), Children: scopeChildren)); + children.Add(new AquariumUiElement(id, "group", Layout: AquariumUiLayout.Horizontal(gap, padding), IsVisible: isVisible, Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: scopeChildren)); return this; } @@ -166,11 +184,11 @@ public sealed class AquariumUiSurfaceBuilder(List children) return Horizontal(id, compose, weight, isVisible: isVisible); } - public AquariumUiSurfaceBuilder Pane(string id, string title, Action compose, string tone = "neutral", float weight = 1.0f) + public AquariumUiSurfaceBuilder Pane(string id, string title, Action compose, string tone = "neutral", float weight = 1.0f, float? fixedExtent = null) { var paneChildren = new List(); compose(new AquariumUiSurfaceBuilder(paneChildren)); - children.Add(new AquariumUiElement(id, "pane", title, Style: new AquariumUiStyle("panel", tone), Layout: AquariumUiLayout.Vertical(8.0f, 10.0f), Weight: Math.Max(0.001f, weight), Children: paneChildren)); + children.Add(new AquariumUiElement(id, "pane", title, Style: new AquariumUiStyle("panel", tone), Layout: AquariumUiLayout.Vertical(8.0f, 10.0f), Weight: Math.Max(0.001f, weight), FixedExtent: fixedExtent, Children: paneChildren)); return this; } diff --git a/src/Aquarium.Engine/Render/D3D12Renderer.cs b/src/Aquarium.Engine/Render/D3D12Renderer.cs index 31f22dd..10c5892 100644 --- a/src/Aquarium.Engine/Render/D3D12Renderer.cs +++ b/src/Aquarium.Engine/Render/D3D12Renderer.cs @@ -671,8 +671,8 @@ public sealed class D3D12Renderer : IAquariumRenderer var surface = clientUiSurfaces[surfaceIndex]; var surfaceBounds = SurfaceBounds(surface); var hasTitle = !string.IsNullOrWhiteSpace(surface.Title); - var contentTop = hasTitle ? surfaceBounds.Top + 42.0f : surfaceBounds.Top + 8.0f; - var content = RectFromEdges(surfaceBounds.Left + 8.0f, contentTop, surfaceBounds.Right - 8.0f, surfaceBounds.Bottom - 8.0f); + var contentTop = hasTitle ? surfaceBounds.Top + 42.0f : surfaceBounds.Top + surface.ContentPadding; + var content = RectFromEdges(surfaceBounds.Left + surface.ContentPadding, contentTop, surfaceBounds.Right - surface.ContentPadding, surfaceBounds.Bottom - surface.ContentPadding); if (TryHitPreviewChildren(surface.Root.Children ?? [], content, surface.Root.Layout ?? AquariumUiLayout.Vertical(), point, out hit)) { return true; @@ -738,7 +738,10 @@ public sealed class D3D12Renderer : IAquariumRenderer case "group": return TryHitPreviewChildren(element.Children ?? [], bounds, element.Layout ?? AquariumUiLayout.Vertical(), point, out hit); case "pane": - return TryHitPreviewChildren(element.Children ?? [], RectFromEdges(bounds.Left + 6.0f, bounds.Top + 32.0f, bounds.Right - 6.0f, bounds.Bottom - 6.0f), element.Layout ?? AquariumUiLayout.Vertical(), point, out hit); + var paneContent = !string.IsNullOrWhiteSpace(element.Text) + ? RectFromEdges(bounds.Left + 6.0f, bounds.Top + 32.0f, bounds.Right - 6.0f, bounds.Bottom - 6.0f) + : bounds; + return TryHitPreviewChildren(element.Children ?? [], paneContent, element.Layout ?? AquariumUiLayout.Vertical(), point, out hit); case "card": return TryHitPreviewChildren(element.Children ?? [], bounds, element.Layout ?? AquariumUiLayout.Vertical(4.0f, 8.0f), point, out hit); case "preview": @@ -820,12 +823,15 @@ public sealed class D3D12Renderer : IAquariumRenderer return true; } - private Rect SurfaceBounds(AquariumUiSurface surface) => - RectFromEdges( - Math.Clamp(surface.Bounds.Left, 8.0f, Math.Max(8.0f, width - 80.0f)), - Math.Clamp(surface.Bounds.Top, 8.0f, Math.Max(8.0f, height - 48.0f)), - Math.Clamp(surface.Bounds.Left + surface.Bounds.Width, 88.0f, width - 8.0f), - Math.Clamp(surface.Bounds.Top + surface.Bounds.Height, 56.0f, height - 8.0f)); + private Rect SurfaceBounds(AquariumUiSurface surface) + { + var edgePadding = surface.ContentPadding <= 0.0f ? 0.0f : 8.0f; + return RectFromEdges( + Math.Clamp(surface.Bounds.Left, edgePadding, Math.Max(edgePadding, width - 80.0f)), + Math.Clamp(surface.Bounds.Top, edgePadding, Math.Max(edgePadding, height - 48.0f)), + Math.Clamp(surface.Bounds.Left + surface.Bounds.Width, 80.0f + edgePadding, width - edgePadding), + Math.Clamp(surface.Bounds.Top + surface.Bounds.Height, 48.0f + edgePadding, height - edgePadding)); + } private static string HitPreviewHandle(Rect itemBounds, Vector2 point) { @@ -855,6 +861,11 @@ public sealed class D3D12Renderer : IAquariumRenderer private static float? PreferredUiExtent(AquariumUiElement element, bool horizontal) { + if (element.FixedExtent is { } fixedExtent) + { + return Math.Max(0.0f, fixedExtent); + } + if (horizontal) { return null; @@ -899,7 +910,8 @@ public sealed class D3D12Renderer : IAquariumRenderer return element.Kind switch { - "pane" => contentExtent + 38.0f, + "pane" when !string.IsNullOrWhiteSpace(element.Text) => contentExtent + 38.0f, + "pane" => contentExtent, "card" => contentExtent, _ => contentExtent, }; diff --git a/src/Aquarium.Engine/Render/DirectWriteOverlay.cs b/src/Aquarium.Engine/Render/DirectWriteOverlay.cs index 0b40843..5ae2c80 100644 --- a/src/Aquarium.Engine/Render/DirectWriteOverlay.cs +++ b/src/Aquarium.Engine/Render/DirectWriteOverlay.cs @@ -194,11 +194,12 @@ internal sealed class DirectWriteOverlay : IDisposable private void DrawSurface(AquariumUiSurface surface) { + var edgePadding = surface.ContentPadding <= 0.0f ? 0.0f : 8.0f; var bounds = RectFromEdges( - Math.Clamp(surface.Bounds.Left, 8.0f, Math.Max(8.0f, width - 80.0f)), - Math.Clamp(surface.Bounds.Top, 8.0f, Math.Max(8.0f, height - 48.0f)), - Math.Clamp(surface.Bounds.Left + surface.Bounds.Width, 88.0f, width - 8.0f), - Math.Clamp(surface.Bounds.Top + surface.Bounds.Height, 56.0f, height - 8.0f)); + Math.Clamp(surface.Bounds.Left, edgePadding, Math.Max(edgePadding, width - 80.0f)), + Math.Clamp(surface.Bounds.Top, edgePadding, Math.Max(edgePadding, height - 48.0f)), + Math.Clamp(surface.Bounds.Left + surface.Bounds.Width, 80.0f + edgePadding, width - edgePadding), + Math.Clamp(surface.Bounds.Top + surface.Bounds.Height, 48.0f + edgePadding, height - edgePadding)); renderTarget.FillRectangle(bounds, panelBrush); renderTarget.DrawRectangle(bounds, outlineBrush, 1.0f); var hasTitle = !string.IsNullOrWhiteSpace(surface.Title); @@ -207,8 +208,8 @@ internal sealed class DirectWriteOverlay : IDisposable DrawHeader(surface.Title, titleFormat, RectFromEdges(bounds.Left + 12.0f, bounds.Top + 8.0f, bounds.Right - 12.0f, bounds.Top + 34.0f), primaryTextBrush); } - var contentTop = hasTitle ? bounds.Top + 42.0f : bounds.Top + 8.0f; - var content = RectFromEdges(bounds.Left + 8.0f, contentTop, bounds.Right - 8.0f, bounds.Bottom - 8.0f); + var contentTop = hasTitle ? bounds.Top + 42.0f : bounds.Top + surface.ContentPadding; + var content = RectFromEdges(bounds.Left + surface.ContentPadding, contentTop, bounds.Right - surface.ContentPadding, bounds.Bottom - surface.ContentPadding); DrawSurfaceChildren(surface.Root.Children ?? [], content, surface.Root.Layout ?? AquariumUiLayout.Vertical()); } @@ -256,8 +257,12 @@ internal sealed class DirectWriteOverlay : IDisposable DrawSurfaceChildren(element.Children ?? [], bounds, element.Layout ?? AquariumUiLayout.Vertical()); break; case "pane": - DrawSurfaceFrame(bounds, element.Text ?? element.Id, element.Style?.Tone ?? "neutral", drawTitle: true); - DrawSurfaceChildren(element.Children ?? [], RectFromEdges(bounds.Left + 6.0f, bounds.Top + 32.0f, bounds.Right - 6.0f, bounds.Bottom - 6.0f), element.Layout ?? AquariumUiLayout.Vertical()); + var hasPaneTitle = !string.IsNullOrWhiteSpace(element.Text); + DrawSurfaceFrame(bounds, element.Text, element.Style?.Tone ?? "neutral", drawTitle: hasPaneTitle); + var paneContent = hasPaneTitle + ? RectFromEdges(bounds.Left + 6.0f, bounds.Top + 32.0f, bounds.Right - 6.0f, bounds.Bottom - 6.0f) + : bounds; + DrawSurfaceChildren(element.Children ?? [], paneContent, element.Layout ?? AquariumUiLayout.Vertical()); break; case "card": DrawSurfaceFrame(bounds, null, element.Style?.Tone ?? "neutral", drawTitle: false); @@ -512,6 +517,11 @@ internal sealed class DirectWriteOverlay : IDisposable private static float? PreferredExtent(AquariumUiElement element, bool horizontal) { + if (element.FixedExtent is { } fixedExtent) + { + return Math.Max(0.0f, fixedExtent); + } + if (horizontal) { return null; @@ -556,7 +566,8 @@ internal sealed class DirectWriteOverlay : IDisposable return element.Kind switch { - "pane" => contentExtent + 38.0f, + "pane" when !string.IsNullOrWhiteSpace(element.Text) => contentExtent + 38.0f, + "pane" => contentExtent, "card" => contentExtent, _ => contentExtent, }; diff --git a/state/evidence.jsonl b/state/evidence.jsonl index d1e28a4..4e8fb9a 100644 --- a/state/evidence.jsonl +++ b/state/evidence.jsonl @@ -328,3 +328,4 @@ {"ts":"2026-05-31T22:55:00+01:00","type":"implementation","status":"ok","note":"CultUI preview lowering treats an empty preview label as an intentional chrome-free canvas: no nested frame/title is drawn, and padding collapses to two pixels. Mimir uses this for the central OBS-style program compositor so the preview fills the editor center instead of rendering as a card-within-a-pane.","files":["src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj; Mimir.App --headless --headless-width 1280 --headless-height 720 --capture-frame artifacts/runtime/mimir-editor-flat-2d-program-mixer-headless.png"} {"ts":"2026-06-01T13:48:00+01:00","type":"implementation","status":"ok","note":"CultUI preview elements now support direct compositor gestures. AquariumUiElement can carry a preview interaction callback, D3D12Renderer hit-tests preview item bodies and selected corner handles in normalized canvas coordinates, captures input while dragging, and DirectWriteOverlay draws visible selected corner handles. Clients still own gesture meaning; Fensalir owns the reusable UI hit-test and rendering affordance.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj -p:UseSharedCompilation=false; dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false"} {"ts":"2026-06-01T14:12:00+01:00","type":"implementation","status":"ok","note":"CultUI preview affordances now include hover/active state, empty-canvas hits, drag-end events, and normalized guide rendering. D3D12Renderer emits hover/begin/drag/end interactions for preview bodies, handles, and empty canvas; DirectWriteOverlay draws hovered/active selected handles and client-provided x/y guides. Added Up/Down/PageUp/PageDown/Control key codes for editor hotkeys.","files":["src/Aquarium.Engine.Contracts/Input/InputState.cs","src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Platform/Win32Window.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine.Contracts/Aquarium.Engine.Contracts.csproj -p:UseSharedCompilation=false; dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false"} +{"ts":"2026-06-01T15:05:00+01:00","type":"implementation","status":"ok","note":"Eve remains the portable dashboard authority; the C# Aquarium/CultUI UI contract is the reference API shape. Added content padding and fixed-extent layout fields to retained surface documents, made pane headers conditional, and kept DirectWrite drawing plus D3D12 preview hit-testing on the same document geometry. Mimir now uses this for an edge-to-edge headerless editor shell with fixed output controls and an expanding inspector.","files":["src/Aquarium.Engine.Contracts/Ui/AquariumUi.cs","src/Aquarium.Engine/Render/D3D12Renderer.cs","src/Aquarium.Engine/Render/DirectWriteOverlay.cs"],"source":"dotnet build src/Aquarium.Engine/Aquarium.Engine.csproj -p:UseSharedCompilation=false; dotnet build E:/Projects/Mimir/src/Mimir.App/Mimir.App.csproj -p:UseSharedCompilation=false; Mimir headless capture artifacts/runtime/mimir-output-panel-headless.png"} diff --git a/state/map.yaml b/state/map.yaml index b70c0b6..a8ce44f 100644 --- a/state/map.yaml +++ b/state/map.yaml @@ -89,6 +89,10 @@ invariants: direct SDF proxies, meshes, volumes, fractal probes, and Mimir sensor observations must publish bounded Form/Appearance/Transport field claims before they can participate in reusable field evidence. + - Eve is the portable retained-dashboard authority. The C# Aquarium/CultUI + contract is the reference API shape for those documents; Fensalir Direct2D + and D3D12 hit testing lower that document locally, but must not become the + owner of dashboard semantics or client policy. - Final composition reads the resolved HDR field texture produced by the shared reservoir history update pass. TubeField render lowerings inject visibility proposals into the shared field reservoir; they must not regain