using System.Numerics; using AcDream.UI.Abstractions; using ImGuiNET; namespace AcDream.UI.ImGui; /// /// implemented as thin wrappers around /// ImGui.NET calls. This is the ONLY place where ImGuiNET types appear /// outside of bootstrap plumbing — panels that need a feature must /// extend the abstraction here, not by importing ImGuiNET in panel /// files. /// public sealed class ImGuiPanelRenderer : IPanelRenderer { /// public bool Begin(string title) => ImGuiNET.ImGui.Begin(title); /// public void End() => ImGuiNET.ImGui.End(); /// public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text); /// public void SameLine() => ImGuiNET.ImGui.SameLine(); /// public void Separator() => ImGuiNET.ImGui.Separator(); /// public void ProgressBar(float fraction, float width, string? overlay = null) { // Clamp defensively; ImGui clamps internally but the abstraction // contract promises to handle out-of-range values. if (fraction < 0f) fraction = 0f; else if (fraction > 1f) fraction = 1f; var size = new Vector2(width, 0f); // height 0 → ImGui picks based on font ImGuiNET.ImGui.ProgressBar(fraction, size, overlay ?? string.Empty); } // -- Phase I.1 widget extensions --------------------------------- /// public void TextColored(Vector4 rgba, string text) => ImGuiNET.ImGui.TextColored(rgba, text); /// public bool CollapsingHeader(string label, bool defaultOpen = true) => ImGuiNET.ImGui.CollapsingHeader( label, defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None); /// public bool TreeNode(string label) => ImGuiNET.ImGui.TreeNode(label); /// public void TreePop() => ImGuiNET.ImGui.TreePop(); /// public bool Checkbox(string label, ref bool value) => ImGuiNET.ImGui.Checkbox(label, ref value); /// public bool Button(string label) => ImGuiNET.ImGui.Button(label); /// public bool Combo(string label, ref int selectedIndex, string[] items) => ImGuiNET.ImGui.Combo(label, ref selectedIndex, items, items.Length); /// public bool SliderFloat(string label, ref float value, float min, float max) => ImGuiNET.ImGui.SliderFloat(label, ref value, min, max); /// public void PlotLines( string label, float[] values, int count, int offset = 0, string? overlay = null, float? min = null, float? max = null, Vector2? size = null) { // ImGui.NET 1.91.6.1's PlotLines binding takes `ref float values` // (pointer-to-first-element semantics) plus a separate values_count // and values_offset. The "no fixed bound" / "default size" sentinels // are float.MaxValue and Vector2.Zero respectively — we pass those // when the caller leaves the optional args null. if (count <= 0 || values.Length == 0) { // Nothing to plot — emit the label so layout doesn't shift but // skip the native call (ref to values[0] would NRE on empty). ImGuiNET.ImGui.TextUnformatted(label); return; } float scaleMin = min ?? float.MaxValue; float scaleMax = max ?? float.MaxValue; Vector2 graphSize = size ?? Vector2.Zero; ImGuiNET.ImGui.PlotLines( label, ref values[0], count, offset, overlay ?? string.Empty, scaleMin, scaleMax, graphSize); } /// public void BeginTable(string id, int columns) => ImGuiNET.ImGui.BeginTable(id, columns); /// public void TableNextColumn() => ImGuiNET.ImGui.TableNextColumn(); /// public void EndTable() => ImGuiNET.ImGui.EndTable(); /// public bool InputTextSubmit(string label, ref string buffer, int maxLen, out string? submitted) { // EnterReturnsTrue: the call returns true on the frame the user // pressed Enter. On every other frame ImGui still mutates `buffer` // as the user types; we just don't surface a submit. bool entered = ImGuiNET.ImGui.InputText( label, ref buffer, (uint)maxLen, ImGuiInputTextFlags.EnterReturnsTrue); if (entered) { submitted = buffer; buffer = string.Empty; // contract: clear for next frame return true; } submitted = null; return false; } /// public void Spacing() => ImGuiNET.ImGui.Spacing(); /// public void Dummy(Vector2 size) => ImGuiNET.ImGui.Dummy(size); /// public void TextWrapped(string text) => ImGuiNET.ImGui.TextWrapped(text); // -- Phase J Tier 3 — scrollable child for chat-style layouts --------- /// public bool BeginChild(string id, Vector2 size, bool border = false) { // ImGuiChildFlags has changed names across ImGui.NET versions // (Border vs Borders); 0x01 is the stable bit value for "draw // a border". Casting from a numeric literal sidesteps the // version-skew without requiring a hard reference to either // enum spelling. bool open = ImGuiNET.ImGui.BeginChild(id, size, (ImGuiChildFlags)(border ? 0x01 : 0)); if (open) { // Title-bar-only drag fix (chat tail specifically): empty // clicks inside a scrollable child fall through to the // parent window for drag-init, which is exactly what the // user reported in the chat panel ("clicking anywhere // moves the window"). An InvisibleButton sized to the // child's content region absorbs those clicks so they // don't propagate. Real widgets drawn afterwards still // claim their own clicks (click priority = "last drawn, // first checked"). Wheel scrolling is window-level, not // item-level, so the absorber doesn't interfere with // the chat tail's auto-scroll. // // Scoped to BeginChild only (NOT Begin) because Begin's // body might host tab bars whose hit-testing competes with // an absorber on equal terms — adding it at Begin level // broke Settings tab clicks. var avail = ImGuiNET.ImGui.GetContentRegionAvail(); if (avail.X > 0f && avail.Y > 0f) { var savedCursor = ImGuiNET.ImGui.GetCursorPos(); ImGuiNET.ImGui.InvisibleButton("##childbodyabsorb", avail); ImGuiNET.ImGui.SetCursorPos(savedCursor); } } return open; } /// public void EndChild() => ImGuiNET.ImGui.EndChild(); /// public float FrameHeightWithSpacing() => ImGuiNET.ImGui.GetFrameHeightWithSpacing(); /// public void SetScrollHereY(float ratio) => ImGuiNET.ImGui.SetScrollHereY(ratio); /// public void SetKeyboardFocusHere() => ImGuiNET.ImGui.SetKeyboardFocusHere(); // -- Phase K.3 — main menu bar ----------------------------------------- /// public bool BeginMainMenuBar() => ImGuiNET.ImGui.BeginMainMenuBar(); /// public void EndMainMenuBar() => ImGuiNET.ImGui.EndMainMenuBar(); /// public bool BeginMenu(string label) => ImGuiNET.ImGui.BeginMenu(label); /// public void EndMenu() => ImGuiNET.ImGui.EndMenu(); /// public bool MenuItem(string label, string? shortcut = null) => shortcut is null ? ImGuiNET.ImGui.MenuItem(label) : ImGuiNET.ImGui.MenuItem(label, shortcut); // -- Tab bar ----------------------------------------------------------- /// public bool BeginTabBar(string id) => ImGuiNET.ImGui.BeginTabBar(id); /// public void EndTabBar() => ImGuiNET.ImGui.EndTabBar(); /// public bool BeginTabItem(string label) => ImGuiNET.ImGui.BeginTabItem(label); /// public void EndTabItem() => ImGuiNET.ImGui.EndTabItem(); // -- Selectable / copyable text --------------------------------------- /// public void TextMultilineReadOnly(string id, string content, Vector2 size) { // ImGui's InputTextMultiline takes a `ref string` even with the // ReadOnly flag — we just hand it a local copy. maxLength caps // what the user could type if ReadOnly were ever cleared; we // size it to the current content (+1 for ImGui's internal NUL // terminator in some bindings). Min of 1 keeps the empty case // from confusing native bindings. string buffer = content; uint maxLen = (uint)System.Math.Max(content.Length + 1, 1); ImGuiNET.ImGui.InputTextMultiline(id, ref buffer, maxLen, size, ImGuiInputTextFlags.ReadOnly); } }