Refine portable dashboard layout lowering

This commit is contained in:
Meta Vens 2026-06-01 14:53:56 +01:00
commit fb5b492c04
5 changed files with 73 additions and 27 deletions

View file

@ -33,6 +33,21 @@ public sealed class AquariumUiDocument
}
public AquariumUiDocument Surface(string id, string title, float left, float top, float width, float height, Action<AquariumUiSurfaceBuilder> 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<AquariumUiSurfaceBuilder> compose)
{
var children = new List<AquariumUiElement>();
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<bool>? IsVisible = null,
float Weight = 1.0f,
float? FixedExtent = null,
IReadOnlyList<AquariumUiElement>? Children = null)
{
public bool Visible => IsVisible?.Invoke() ?? true;
@ -145,19 +163,19 @@ public sealed record AquariumUiElement(
public sealed class AquariumUiSurfaceBuilder(List<AquariumUiElement> children)
{
public AquariumUiSurfaceBuilder Vertical(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null)
public AquariumUiSurfaceBuilder Vertical(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null, float? fixedExtent = null)
{
var scopeChildren = new List<AquariumUiElement>();
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<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null)
public AquariumUiSurfaceBuilder Horizontal(string id, Action<AquariumUiSurfaceBuilder> compose, float weight = 1.0f, float gap = 8.0f, float padding = 0.0f, Func<bool>? isVisible = null, float? fixedExtent = null)
{
var scopeChildren = new List<AquariumUiElement>();
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<AquariumUiElement> children)
return Horizontal(id, compose, weight, isVisible: isVisible);
}
public AquariumUiSurfaceBuilder Pane(string id, string title, Action<AquariumUiSurfaceBuilder> compose, string tone = "neutral", float weight = 1.0f)
public AquariumUiSurfaceBuilder Pane(string id, string title, Action<AquariumUiSurfaceBuilder> compose, string tone = "neutral", float weight = 1.0f, float? fixedExtent = null)
{
var paneChildren = new List<AquariumUiElement>();
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;
}

View file

@ -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,
};

View file

@ -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,
};

View file

@ -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"}

View file

@ -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