feat(studio): UX pass — selection highlight, in-studio panel picker, fixed layout

Fix 1 (Selection highlight): StudioInspector.DrawCanvas draws a bright-green
2px outline over Selected using the window draw list. ScreenPosition maps
directly to FBO pixel space; rectMin offsets into screen space.

Fix 2 (In-studio panel picker): DrawToolbar (BeginCombo over ListSlugs output).
StudioWindow resolves _dumpFile once in OnLoad, tracks _currentSlug, and calls
LoadDumpPanel on combo change. LoadDumpPanel removes old root via RemoveChild,
loads new slug, resets Selected. No relaunch needed.

Fix 3 (Fixed layout): All four panes use SetNextWindowPos + SetNextWindowSize
with ImGuiCond.FirstUseEver. Toolbar (0,0 x windowW x 40), tree (0,40 x 280),
canvas (280,40 x centre), props (right-340,40 x 340). User can drag from these.

Build + dotnet test green (609 passed, 0 failed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-06-25 17:58:50 +02:00
parent 2fe7b4a057
commit c03a241884
2 changed files with 163 additions and 17 deletions

View file

@ -5,10 +5,12 @@ using ImGuiNET;
namespace AcDream.App.Studio;
/// <summary>
/// Three-pane ImGui IDE for the acdream UI Studio:
/// Four-pane ImGui IDE for the acdream UI Studio:
/// <list type="bullet">
/// <item><b>Toolbar</b> — panel picker (slug combo) across the top.</item>
/// <item><b>Canvas</b> — shows the panel FBO texture; click-to-inspect returns the
/// panel-local pixel coordinate of the click.</item>
/// panel-local pixel coordinate of the click; draws a green outline over
/// <see cref="Selected"/>.</item>
/// <item><b>Tree</b> — recursive ImGui tree of the element hierarchy; clicking a node
/// sets <see cref="Selected"/>.</item>
/// <item><b>Properties</b> — shows the <see cref="Selected"/> element's geometry,
@ -27,12 +29,55 @@ namespace AcDream.App.Studio;
/// the panel renders right-side-up in the canvas. Because V is flipped, image-local
/// pixel y = 0 is the TOP of the panel, matching the UiRoot's top-left origin — so
/// click y maps directly without further inversion.</para>
///
/// <para>Layout: <see cref="DrawToolbar"/> / <see cref="DrawCanvas"/> /
/// <see cref="DrawTree"/> / <see cref="DrawProperties"/> each call
/// <c>SetNextWindowPos</c> + <c>SetNextWindowSize</c> with
/// <c>ImGuiCond.FirstUseEver</c> so the panes start in a docked-style arrangement
/// that the user can freely drag from.</para>
/// </summary>
public sealed class StudioInspector
{
/// <summary>Currently selected element (set by tree-click or canvas-click).</summary>
public UiElement? Selected { get; set; }
// ── Toolbar ───────────────────────────────────────────────────────────────────
/// <summary>
/// Draw the "Studio" toolbar window (top strip) containing a slug combo-box.
/// Returns the newly-selected slug when the user picks a different panel, or
/// null when the selection is unchanged.
/// </summary>
/// <param name="slugs">All available panel slugs (from <c>UiDumpModel.ListSlugs</c>).</param>
/// <param name="current">The slug of the panel currently loaded.</param>
/// <param name="windowW">Studio window width (pixels).</param>
public string? DrawToolbar(IReadOnlyList<string> slugs, string? current, int windowW)
{
ImGui.SetNextWindowPos(new Vector2(0f, 0f), ImGuiCond.FirstUseEver);
ImGui.SetNextWindowSize(new Vector2(windowW, 40f), ImGuiCond.FirstUseEver);
ImGui.Begin("Studio",
ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse);
ImGui.SetNextItemWidth(300f);
string? result = null;
string preview = current ?? "(none)";
if (ImGui.BeginCombo("Panel", preview))
{
foreach (var slug in slugs)
{
bool selected = string.Equals(slug, current, StringComparison.OrdinalIgnoreCase);
if (ImGui.Selectable(slug, selected) && !selected)
result = slug;
if (selected)
ImGui.SetItemDefaultFocus();
}
ImGui.EndCombo();
}
ImGui.End();
return result;
}
// ── Canvas ────────────────────────────────────────────────────────────────────
/// <summary>
@ -42,9 +87,18 @@ public sealed class StudioInspector
///
/// <para>The texture is drawn with uv0=(0,1) / uv1=(1,0) to flip V, correcting
/// the GL bottom-left FBO origin to the ImGui top-left convention.</para>
///
/// <para>If <see cref="Selected"/> is non-null and has non-zero size, a bright-green
/// 2-pixel outline is drawn over the element's screen-space rect using the window
/// draw list. <see cref="UiElement.ScreenPosition"/> gives the absolute top-left in
/// root/panel space, which maps 1:1 to FBO pixels (the image is drawn 1:1 and both
/// the element tree and the FBO share the same top-left origin).</para>
/// </summary>
public (int x, int y)? DrawCanvas(nint panelTex, int width, int height)
public (int x, int y)? DrawCanvas(nint panelTex, int width, int height,
int windowX, int windowW, int windowY, int windowH)
{
ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver);
ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver);
ImGui.Begin("Canvas");
var imageSize = new Vector2(width, height);
@ -55,13 +109,29 @@ public sealed class StudioInspector
var uv1 = new Vector2(1f, 0f);
ImGui.Image(panelTex, imageSize, uv0, uv1);
// ── Selection highlight ───────────────────────────────────────────────
// After ImGui.Image the item rect gives us the screen-space top-left of
// the drawn texture. ScreenPosition is in root/panel space (top-left origin,
// same coordinate system the FBO was drawn in), so offsetting by rectMin
// converts directly to screen space for the draw list.
var rectMin = ImGui.GetItemRectMin();
var el = Selected;
if (el is not null && el.Width > 0f && el.Height > 0f)
{
var sp = el.ScreenPosition;
var p0 = new Vector2(rectMin.X + sp.X, rectMin.Y + sp.Y);
var p1 = new Vector2(p0.X + el.Width, p0.Y + el.Height);
var dl = ImGui.GetWindowDrawList();
dl.AddRect(p0, p1,
ImGui.GetColorU32(new Vector4(0.2f, 1f, 0.4f, 1f)),
0f, ImDrawFlags.None, 2f);
}
(int x, int y)? result = null;
// Detect a left-click on the image.
if (ImGui.IsItemHovered() && ImGui.IsMouseClicked(ImGuiMouseButton.Left))
{
// Image item rect: top-left of the drawn image in screen (display) coords.
var rectMin = ImGui.GetItemRectMin();
var mousePos = ImGui.GetMousePos();
// Panel-local pixel = mouse offset from image top-left.
@ -81,8 +151,10 @@ public sealed class StudioInspector
// ── Tree ──────────────────────────────────────────────────────────────────────
/// <summary>Draw the "Tree" ImGui window. Clicking a node sets <see cref="Selected"/>.</summary>
public void DrawTree(UiElement root)
public void DrawTree(UiElement root, int windowX, int windowY, int windowW, int windowH)
{
ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver);
ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver);
ImGui.Begin("Tree");
DrawTreeNode(root);
ImGui.End();
@ -120,8 +192,10 @@ public sealed class StudioInspector
// ── Properties ───────────────────────────────────────────────────────────────
/// <summary>Draw the "Properties" ImGui window for <see cref="Selected"/>.</summary>
public void DrawProperties()
public void DrawProperties(int windowX, int windowY, int windowW, int windowH)
{
ImGui.SetNextWindowPos(new Vector2(windowX, windowY), ImGuiCond.FirstUseEver);
ImGui.SetNextWindowSize(new Vector2(windowW, windowH), ImGuiCond.FirstUseEver);
ImGui.Begin("Properties");
var el = Selected;