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(); } }