Refine portable dashboard layout lowering
This commit is contained in:
parent
fc38dd0d69
commit
fb5b492c04
5 changed files with 73 additions and 27 deletions
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue