feat(studio): ImGui inspector — canvas FBO, element tree, props, click-to-inspect
Task 3 of the acdream UI Studio plan. The studio previously drew the panel straight to the window; it now renders the panel into an off-screen FBO (PanelFbo) and displays it in an ImGui Canvas pane alongside a Tree pane (recursive element hierarchy, click-to-select) and a Properties pane (EventId/type/rect/anchors/ZOrder/flags of the selected element). Click-to-inspect: a left-click inside the Canvas calls UiElement.HitTest on the panel root (same-assembly internal access) and selects the topmost element at the cursor, wiring the canvas directly to the tree selection. PanelFbo lifecycle mirrors PaperdollViewportRenderer (RGBA8 color + Depth24Stencil8 renderbuffer, GLStateScope-sealed, lazy resize on size change). V-flip in DrawCanvas (uv0=(0,1)/uv1=(1,0)) corrects GL's bottom-left FBO origin to ImGui's top-left convention so click Y maps directly to panel-local Y without inversion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
df6b5b3b39
commit
d2240974ec
3 changed files with 359 additions and 4 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.UI;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.Options;
|
||||
using Silk.NET.Input;
|
||||
|
|
@ -17,6 +18,15 @@ namespace AcDream.App.Studio;
|
|||
///
|
||||
/// Usage: dotnet run -- ui-studio [dat-dir] [--layout 0xNNNN] [--markup path]
|
||||
///
|
||||
/// Task 3 adds an ImGui IDE on top of the panel FBO:
|
||||
/// <list type="bullet">
|
||||
/// <item>Canvas pane — the panel rendered off-screen via <see cref="PanelFbo"/>.</item>
|
||||
/// <item>Tree pane — the element hierarchy; click-to-select.</item>
|
||||
/// <item>Properties pane — geometry/anchors/flags of the selected element.</item>
|
||||
/// <item>Click-to-inspect — a left-click in the canvas selects the topmost
|
||||
/// element under the cursor via <see cref="UiRoot.HitTest"/>.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// The window is intentionally thin: no game world, no physics, no streaming —
|
||||
/// just GL + UiHost + the layout under test, identical to how the panel
|
||||
/// appears inside <c>GameWindow</c>.
|
||||
|
|
@ -31,6 +41,12 @@ public sealed class StudioWindow : IDisposable
|
|||
private RenderStack? _stack;
|
||||
private LayoutSource? _source;
|
||||
|
||||
// Task 3 additions.
|
||||
private AcDream.UI.ImGui.ImGuiBootstrapper? _imgui;
|
||||
private PanelFbo? _panelFbo;
|
||||
private StudioInspector? _inspector;
|
||||
private UiElement? _panelRoot; // top-level element added to UiRoot (for hit-test + tree)
|
||||
|
||||
public StudioWindow(StudioOptions opts)
|
||||
{
|
||||
_opts = opts ?? throw new ArgumentNullException(nameof(opts));
|
||||
|
|
@ -99,10 +115,16 @@ public sealed class StudioWindow : IDisposable
|
|||
// Load the panel described by options and add it to the UI tree.
|
||||
_source = new LayoutSource(_dats, _stack.ResolveChrome, _stack.VitalsDatFont);
|
||||
var root = _source.Load(_opts);
|
||||
_panelRoot = root;
|
||||
if (root is not null)
|
||||
_stack.UiHost.Root.AddChild(root);
|
||||
else
|
||||
Console.Error.WriteLine($"[studio] panel load failed: {_source.LastError}");
|
||||
|
||||
// Task 3: ImGui IDE. The studio is always "devtools" — no gate flag needed.
|
||||
_imgui = new AcDream.UI.ImGui.ImGuiBootstrapper(gl, _window!, input);
|
||||
_panelFbo = new PanelFbo(gl);
|
||||
_inspector = new StudioInspector();
|
||||
}
|
||||
|
||||
private double _dt;
|
||||
|
|
@ -114,18 +136,61 @@ public sealed class StudioWindow : IDisposable
|
|||
|
||||
private void OnRender(double dt)
|
||||
{
|
||||
if (_stack is null) return;
|
||||
if (_stack is null || _imgui is null || _panelFbo is null || _inspector is null)
|
||||
return;
|
||||
|
||||
var gl = _stack.Gl;
|
||||
gl.ClearColor(0.18f, 0.18f, 0.18f, 1f);
|
||||
int w = _window!.Size.X;
|
||||
int h = _window!.Size.Y;
|
||||
|
||||
// 1. Tick the UI widgets.
|
||||
_stack.UiHost.Tick(_dt);
|
||||
|
||||
// 2. Render the panel into the off-screen FBO; get the color texture.
|
||||
// The FBO is the same logical size as the window, so element rects map 1:1 to
|
||||
// FBO pixels — no scale factor needed when displaying the canvas at full size.
|
||||
uint panelTex = _panelFbo.Render(w, h, _stack.UiHost);
|
||||
|
||||
// 3. Clear the window back-buffer (the dark ImGui background shows behind panes).
|
||||
gl.ClearColor(0.1f, 0.1f, 0.1f, 1f);
|
||||
gl.Clear(ColorBufferBit | DepthBufferBit);
|
||||
|
||||
_stack.UiHost.Tick(_dt);
|
||||
_stack.UiHost.Draw(new Vector2(_window!.Size.X, _window!.Size.Y));
|
||||
// 4. Begin the ImGui frame.
|
||||
_imgui.BeginFrame((float)dt);
|
||||
|
||||
// 5. Canvas pane — show the FBO texture; detect clicks.
|
||||
(int x, int y)? click = null;
|
||||
if (panelTex != 0)
|
||||
click = _inspector.DrawCanvas((nint)panelTex, w, h);
|
||||
|
||||
// 6. If the user clicked inside the canvas, hit-test the panel tree and select.
|
||||
if (click is { } c && _panelRoot is not null)
|
||||
{
|
||||
// UiElement.HitTest is internal — accessible from AcDream.App.Studio (same assembly).
|
||||
// We call it on the panel root with coords relative to its top-left.
|
||||
var screenPos = _panelRoot.ScreenPosition;
|
||||
var hit = _panelRoot.HitTest(c.x - screenPos.X, c.y - screenPos.Y);
|
||||
if (hit is not null)
|
||||
_inspector.Selected = hit;
|
||||
}
|
||||
|
||||
// 7. Element tree pane.
|
||||
if (_panelRoot is not null)
|
||||
_inspector.DrawTree(_panelRoot);
|
||||
|
||||
// 8. Properties pane.
|
||||
_inspector.DrawProperties();
|
||||
|
||||
// 9. Finalise ImGui and flush draw data to the window.
|
||||
_imgui.Render();
|
||||
}
|
||||
|
||||
private void OnClosing()
|
||||
{
|
||||
_imgui?.Dispose();
|
||||
_panelFbo?.Dispose();
|
||||
_imgui = null;
|
||||
_panelFbo = null;
|
||||
_stack?.DrawDispatcher.Dispose();
|
||||
_stack?.MeshAdapter.Dispose();
|
||||
_stack?.TextureCache.Dispose();
|
||||
|
|
@ -139,6 +204,10 @@ public sealed class StudioWindow : IDisposable
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
_imgui?.Dispose();
|
||||
_panelFbo?.Dispose();
|
||||
_imgui = null;
|
||||
_panelFbo = null;
|
||||
_window?.Dispose();
|
||||
_window = null;
|
||||
// If OnClosing wasn't called (e.g. exception before Run()), clean up anyway.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue