using System.Numerics;
using AcDream.App.UI;
using ImGuiNET;
namespace AcDream.App.Studio;
///
/// All canvas mouse events gathered by in one frame.
/// All coordinates are already mapped to panel-local pixels (origin top-left, same as UiRoot).
///
public readonly struct CanvasInputEvent
{
/// Mouse is currently hovering the canvas image. When false all other fields are 0 / false.
public readonly bool IsHovered;
/// Panel-local pixel coordinate of the mouse this frame (valid when ).
public readonly int MouseX;
/// Panel-local pixel coordinate of the mouse this frame (valid when ).
public readonly int MouseY;
/// Left-button went down this frame.
public readonly bool LeftDown;
/// Left-button came up this frame.
public readonly bool LeftUp;
/// Mouse-wheel scroll delta (lines, positive = up). Zero when no scroll.
public readonly int ScrollDelta;
public CanvasInputEvent(bool hovered, int mx, int my, bool ld, bool lu, int scroll)
{
IsHovered = hovered;
MouseX = mx;
MouseY = my;
LeftDown = ld;
LeftUp = lu;
ScrollDelta = scroll;
}
}
///
/// Four-pane ImGui IDE for the acdream UI Studio:
///
/// - Toolbar — panel picker (slug combo) across the top.
/// - Canvas — shows the panel FBO texture; in Interact mode mouse events
/// are forwarded to the panel UiHost (buttons/tabs respond); in Inspect mode a
/// left-click hit-tests and selects the element under the cursor.
/// - Tree — recursive ImGui tree of the element hierarchy; clicking a node
/// sets .
/// - Properties — shows the element's geometry,
/// anchors, and z-order.
///
///
/// Coordinate mapping for the canvas:
/// The FBO is rendered at the full window size and displayed 1:1 inside the Canvas ImGui
/// sub-window. After ImGui.Image we call ImGui.GetItemRectMin() to get the
/// screen-space top-left of the drawn image (accounting for the sub-window's title bar,
/// padding, and any scrolling). Subtracting that from the raw mouse screen position gives
/// panel-local pixels directly — no additional scale factor is needed because the image is
/// drawn 1:1.
///
/// V-flip — no extra Y inversion needed:
/// The FBO origin is bottom-left (GL convention), so we pass uv0=(0,1), uv1=(1,0) to
/// ImGui.Image to flip V. After this flip, displayed row 0 (top of the image on
/// screen) corresponds to panel Y=0 (the top of the UI panel), matching UiRoot's
/// top-left origin. Therefore the panel-local Y computed above maps directly into UiRoot
/// without further inversion — do NOT flip Y again.
///
/// Layout: the four panes call SetNextWindowPos + SetNextWindowSize
/// with ImGuiCond.FirstUseEver so they start docked but can be freely dragged.
///
public sealed class StudioInspector
{
/// Currently selected element (set by tree-click or canvas-click in Inspect mode).
public UiElement? Selected { get; set; }
///
/// When true (default) canvas mouse events are forwarded to the panel UiHost so elements
/// respond to clicks. When false a canvas click hit-tests and selects an element in the
/// inspector tree instead. Toggle via the "Interact / Inspect" checkbox in the toolbar.
///
public bool InteractMode { get; set; } = true;
// ── Toolbar ───────────────────────────────────────────────────────────────────
///
/// Draw the "Studio" toolbar window (top strip) containing a slug combo-box and
/// the Interact / Inspect mode toggle. Returns the newly-selected slug when the
/// user picks a different panel, or null when unchanged.
/// The mode toggle sets : checked = Interact (panel
/// elements respond to clicks), unchecked = Inspect (clicks select elements in the
/// tree).
///
/// All available panel slugs (from UiDumpModel.ListSlugs).
/// The slug of the panel currently loaded.
/// Studio window width (pixels).
public string? DrawToolbar(IReadOnlyList 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.SameLine();
bool interact = InteractMode;
if (ImGui.Checkbox("Interact", ref interact))
InteractMode = interact;
if (ImGui.IsItemHovered())
ImGui.SetTooltip("Interact: canvas clicks reach the panel (buttons/tabs respond).\nUncheck to Inspect: clicks select elements in the tree.");
ImGui.End();
return result;
}
// ── Canvas ────────────────────────────────────────────────────────────────────
///
/// Draw the "Canvas" ImGui window containing the panel FBO texture and return all
/// canvas mouse events for this frame as a .
///
/// Coordinate mapping: After ImGui.Image, GetItemRectMin()
/// returns the actual screen-space top-left of the drawn image (accounting for the
/// sub-window title bar, padding, and scrolling). Subtracting that from the raw ImGui
/// mouse position gives panel-local pixels directly — no scale factor because the
/// image is drawn 1:1.
///
/// V-flip — no extra Y inversion: we pass uv0=(0,1) / uv1=(1,0) so the
/// GL bottom-left origin is flipped to top-left on screen. After the flip, screen
/// row 0 = panel Y 0 (top of the UI), so the computed Y already matches UiRoot's
/// top-left origin — do NOT flip Y again.
///
/// If is non-null a bright-green 2-pixel outline is
/// drawn over it using the window draw list.
///
public CanvasInputEvent 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);
// V-flip: FBO origin is bottom-left; ImGui images expect top-left.
// uv0 = bottom-left of texture = top of the panel in screen space.
// uv1 = top-right of texture = bottom of the panel in screen space.
var uv0 = new Vector2(0f, 1f);
var uv1 = new Vector2(1f, 0f);
ImGui.Image(panelTex, imageSize, uv0, uv1);
// rectMin: screen-space top-left of the image AFTER ImGui.Image + any chrome offset.
// This is what lets us translate raw mouse screen coords into panel-local pixels.
var rectMin = ImGui.GetItemRectMin();
// ── Selection highlight ───────────────────────────────────────────────
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);
}
// ── Gather canvas mouse events ────────────────────────────────────────
// IsItemHovered is true when the mouse is over the Image item (not just the window).
bool hovered = ImGui.IsItemHovered();
int mx = 0, my = 0;
bool leftDown = false, leftUp = false;
int scroll = 0;
if (hovered)
{
var mousePos = ImGui.GetMousePos();
// Panel-local pixel = mouse offset from the image's screen-space top-left.
// Scale is 1:1 (image drawn at full FBO size). Y needs no extra flip — see summary.
int ix = (int)(mousePos.X - rectMin.X);
int iy = (int)(mousePos.Y - rectMin.Y);
// Clamp to image bounds (mouse can be on the image edge pixel).
if (ix >= 0 && ix < width && iy >= 0 && iy < height)
{
mx = ix;
my = iy;
leftDown = ImGui.IsMouseClicked(ImGuiMouseButton.Left);
leftUp = ImGui.IsMouseReleased(ImGuiMouseButton.Left);
float wheelY = ImGui.GetIO().MouseWheel;
scroll = (int)wheelY; // positive = scroll up
}
else
{
// Mouse is over ImGui chrome (title bar, padding) adjacent to image — not over the panel.
hovered = false;
}
}
ImGui.End();
return new CanvasInputEvent(hovered, mx, my, leftDown, leftUp, scroll);
}
// ── Tree ──────────────────────────────────────────────────────────────────────
/// Draw the "Tree" ImGui window. Clicking a node sets .
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();
}
private void DrawTreeNode(UiElement el)
{
// Label: EventId (hex) + C# type name, e.g. "0x10000001 [UiDatElement]"
string label = $"0x{el.EventId:X8} [{el.GetType().Name}]";
bool isSelected = ReferenceEquals(el, Selected);
bool hasChildren = el.Children.Count > 0;
ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags.OpenOnArrow
| ImGuiTreeNodeFlags.SpanAvailWidth;
if (!hasChildren)
flags |= ImGuiTreeNodeFlags.Leaf;
if (isSelected)
flags |= ImGuiTreeNodeFlags.Selected;
bool open = ImGui.TreeNodeEx(label, flags);
// Click on the node label (not the arrow) selects it.
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
Selected = el;
if (open)
{
foreach (var child in el.Children)
DrawTreeNode(child);
ImGui.TreePop();
}
}
// ── Properties ───────────────────────────────────────────────────────────────
/// Draw the "Properties" ImGui window for .
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;
if (el is null)
{
ImGui.TextUnformatted("(nothing selected)");
ImGui.End();
return;
}
ImGui.TextUnformatted($"Id (EventId): 0x{el.EventId:X8}");
ImGui.TextUnformatted($"Type: {el.GetType().Name}");
ImGui.TextUnformatted($"Name: {el.Name ?? "(null)"}");
ImGui.Separator();
ImGui.TextUnformatted($"Rect: ({el.Left}, {el.Top}, {el.Width} x {el.Height})");
ImGui.TextUnformatted($"Anchors: {el.Anchors}");
ImGui.TextUnformatted($"ZOrder: {el.ZOrder}");
ImGui.Separator();
ImGui.TextUnformatted($"Visible: {el.Visible}");
ImGui.TextUnformatted($"Enabled: {el.Enabled}");
ImGui.TextUnformatted($"ClickThrough: {el.ClickThrough}");
ImGui.TextUnformatted($"Draggable: {el.Draggable}");
ImGui.TextUnformatted($"Resizable: {el.Resizable}");
ImGui.TextUnformatted($"IsDragSource: {el.IsDragSource}");
ImGui.TextUnformatted($"HandlesClick: {el.HandlesClick}");
ImGui.TextUnformatted($"Opacity: {el.Opacity:F2}");
ImGui.Separator();
var sp = el.ScreenPosition;
ImGui.TextUnformatted($"ScreenPos: ({sp.X:F1}, {sp.Y:F1})");
ImGui.TextUnformatted($"Children: {el.Children.Count}");
ImGui.End();
}
}