From 2818fcca8c68bc34077d08daae9f3149bc86d9c2 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 26 Apr 2026 23:04:10 +0200 Subject: [PATCH] =?UTF-8?q?fix(ui):=20scope=20title-bar-only-drag=20absorb?= =?UTF-8?q?er=20to=20BeginChild=20=E2=80=94=20Settings=20tabs=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs | 56 ++++++++++++---------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs index 94f09d4..4aa94ae 100644 --- a/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs +++ b/src/AcDream.UI.ImGui/ImGuiPanelRenderer.cs @@ -14,30 +14,7 @@ namespace AcDream.UI.ImGui; public sealed class ImGuiPanelRenderer : IPanelRenderer { /// - 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); /// public void End() => ImGuiNET.ImGui.End(); @@ -178,12 +155,41 @@ public sealed class ImGuiPanelRenderer : IPanelRenderer /// 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; + } /// public void EndChild() => ImGuiNET.ImGui.EndChild();