fix(ui): scope title-bar-only-drag absorber to BeginChild — Settings tabs work

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>
This commit is contained in:
Erik 2026-04-26 23:04:10 +02:00
parent 627325559c
commit 2818fcca8c

View file

@ -14,30 +14,7 @@ namespace AcDream.UI.ImGui;
public sealed class ImGuiPanelRenderer : IPanelRenderer
{
/// <inheritdoc />
public bool Begin(string title)
{
bool open = ImGuiNET.ImGui.Begin(title);
if (open)
{
// Title-bar-only drag: ImGui's default lets the user drag the
// window by clicking on the empty body background (because a
// window-drag is initiated whenever a body click lands without
// any widget being "active"). Filling the body with an
// InvisibleButton absorbs those stray clicks — real widgets
// drawn afterwards still claim their own clicks because click
// priority is "last drawn, first checked", so the button
// catches only empty-space clicks. Net effect: title bar
// still drags (ImGui default), body never does.
var avail = ImGuiNET.ImGui.GetContentRegionAvail();
if (avail.X > 0f && avail.Y > 0f)
{
var savedCursor = ImGuiNET.ImGui.GetCursorPos();
ImGuiNET.ImGui.InvisibleButton("##bodydragabsorb", avail);
ImGuiNET.ImGui.SetCursorPos(savedCursor);
}
}
return open;
}
public bool Begin(string title) => ImGuiNET.ImGui.Begin(title);
/// <inheritdoc />
public void End() => ImGuiNET.ImGui.End();
@ -178,12 +155,41 @@ public sealed class ImGuiPanelRenderer : IPanelRenderer
/// <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.
=> ImGuiNET.ImGui.BeginChild(id, size, (ImGuiChildFlags)(border ? 0x01 : 0));
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();