Previous fix put the InvisibleButton absorber inside Begin, which covered the entire panel body — and the Settings panel's tab bar has its hit-testing in that same area. Tabs lost click priority to the absorber (their hover/click events were stolen) so the user couldn't switch tabs. Worse, the chat-panel drag the absorber was supposed to fix wasn't actually fixed because chat's body is covered by a BeginChild for the scrollable tail — clicks land in the child window, not the parent body, so the parent absorber never sees them. Right scope: scrollable BeginChild bodies. That's where the chat panel's empty-space clicks actually land, and where the parent- drag fall-through originates. Other panels (Settings, Vitals, Debug) don't use BeginChild for content — their bodies are filled with widgets that already absorb clicks naturally. The fix: · Begin reverts to ImGui default (title bar drags, body of widget- filled panels naturally absorbs through the widgets themselves). · BeginChild grows the InvisibleButton absorber inside, so empty- space clicks inside a scroll region don't fall through to the parent's window-drag init. Net effect: · Chat panel: empty clicks in the scroll tail no longer drag the parent window. · Settings panel: tabs are clickable again. · Vitals, Debug: unchanged. dotnet build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
256 lines
9.3 KiB
C#
256 lines
9.3 KiB
C#
using System.Numerics;
|
|
using AcDream.UI.Abstractions;
|
|
using ImGuiNET;
|
|
|
|
namespace AcDream.UI.ImGui;
|
|
|
|
/// <summary>
|
|
/// <see cref="IPanelRenderer"/> 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.
|
|
/// </summary>
|
|
public sealed class ImGuiPanelRenderer : IPanelRenderer
|
|
{
|
|
/// <inheritdoc />
|
|
public bool Begin(string title) => ImGuiNET.ImGui.Begin(title);
|
|
|
|
/// <inheritdoc />
|
|
public void End() => ImGuiNET.ImGui.End();
|
|
|
|
/// <inheritdoc />
|
|
public void Text(string text) => ImGuiNET.ImGui.TextUnformatted(text);
|
|
|
|
/// <inheritdoc />
|
|
public void SameLine() => ImGuiNET.ImGui.SameLine();
|
|
|
|
/// <inheritdoc />
|
|
public void Separator() => ImGuiNET.ImGui.Separator();
|
|
|
|
/// <inheritdoc />
|
|
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 ---------------------------------
|
|
|
|
/// <inheritdoc />
|
|
public void TextColored(Vector4 rgba, string text)
|
|
=> ImGuiNET.ImGui.TextColored(rgba, text);
|
|
|
|
/// <inheritdoc />
|
|
public bool CollapsingHeader(string label, bool defaultOpen = true)
|
|
=> ImGuiNET.ImGui.CollapsingHeader(
|
|
label,
|
|
defaultOpen ? ImGuiTreeNodeFlags.DefaultOpen : ImGuiTreeNodeFlags.None);
|
|
|
|
/// <inheritdoc />
|
|
public bool TreeNode(string label) => ImGuiNET.ImGui.TreeNode(label);
|
|
|
|
/// <inheritdoc />
|
|
public void TreePop() => ImGuiNET.ImGui.TreePop();
|
|
|
|
/// <inheritdoc />
|
|
public bool Checkbox(string label, ref bool value)
|
|
=> ImGuiNET.ImGui.Checkbox(label, ref value);
|
|
|
|
/// <inheritdoc />
|
|
public bool Button(string label) => ImGuiNET.ImGui.Button(label);
|
|
|
|
/// <inheritdoc />
|
|
public bool Combo(string label, ref int selectedIndex, string[] items)
|
|
=> ImGuiNET.ImGui.Combo(label, ref selectedIndex, items, items.Length);
|
|
|
|
/// <inheritdoc />
|
|
public bool SliderFloat(string label, ref float value, float min, float max)
|
|
=> ImGuiNET.ImGui.SliderFloat(label, ref value, min, max);
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void BeginTable(string id, int columns)
|
|
=> ImGuiNET.ImGui.BeginTable(id, columns);
|
|
|
|
/// <inheritdoc />
|
|
public void TableNextColumn() => ImGuiNET.ImGui.TableNextColumn();
|
|
|
|
/// <inheritdoc />
|
|
public void EndTable() => ImGuiNET.ImGui.EndTable();
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void Spacing() => ImGuiNET.ImGui.Spacing();
|
|
|
|
/// <inheritdoc />
|
|
public void Dummy(Vector2 size) => ImGuiNET.ImGui.Dummy(size);
|
|
|
|
/// <inheritdoc />
|
|
public void TextWrapped(string text) => ImGuiNET.ImGui.TextWrapped(text);
|
|
|
|
// -- Phase J Tier 3 — scrollable child for chat-style layouts ---------
|
|
|
|
/// <inheritdoc />
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public void EndChild() => ImGuiNET.ImGui.EndChild();
|
|
|
|
/// <inheritdoc />
|
|
public float FrameHeightWithSpacing() => ImGuiNET.ImGui.GetFrameHeightWithSpacing();
|
|
|
|
/// <inheritdoc />
|
|
public void SetScrollHereY(float ratio) => ImGuiNET.ImGui.SetScrollHereY(ratio);
|
|
|
|
/// <inheritdoc />
|
|
public void SetKeyboardFocusHere() => ImGuiNET.ImGui.SetKeyboardFocusHere();
|
|
|
|
// -- Phase K.3 — main menu bar -----------------------------------------
|
|
|
|
/// <inheritdoc />
|
|
public bool BeginMainMenuBar() => ImGuiNET.ImGui.BeginMainMenuBar();
|
|
|
|
/// <inheritdoc />
|
|
public void EndMainMenuBar() => ImGuiNET.ImGui.EndMainMenuBar();
|
|
|
|
/// <inheritdoc />
|
|
public bool BeginMenu(string label) => ImGuiNET.ImGui.BeginMenu(label);
|
|
|
|
/// <inheritdoc />
|
|
public void EndMenu() => ImGuiNET.ImGui.EndMenu();
|
|
|
|
/// <inheritdoc />
|
|
public bool MenuItem(string label, string? shortcut = null)
|
|
=> shortcut is null
|
|
? ImGuiNET.ImGui.MenuItem(label)
|
|
: ImGuiNET.ImGui.MenuItem(label, shortcut);
|
|
|
|
// -- Tab bar -----------------------------------------------------------
|
|
|
|
/// <inheritdoc />
|
|
public bool BeginTabBar(string id) => ImGuiNET.ImGui.BeginTabBar(id);
|
|
|
|
/// <inheritdoc />
|
|
public void EndTabBar() => ImGuiNET.ImGui.EndTabBar();
|
|
|
|
/// <inheritdoc />
|
|
public bool BeginTabItem(string label) => ImGuiNET.ImGui.BeginTabItem(label);
|
|
|
|
/// <inheritdoc />
|
|
public void EndTabItem() => ImGuiNET.ImGui.EndTabItem();
|
|
|
|
// -- Selectable / copyable text ---------------------------------------
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
}
|