From c909d1646b888caea3a33b13717f9e717c9a88c1 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Thu, 21 May 2026 14:10:01 +0200 Subject: [PATCH] fix(ui): draw scroll-to-bottom button in a standalone overlay window Button drawn in the parent window over the ##chat2-messages child was never clickable: ImGui resolves g.HoveredWindow to the child for that screen rect, so ItemHoverable rejects any item submitted in the parent. A top-level Begin/End window is a sibling in the window list and wins the hit-test for its own rect. ownerId parameter keeps the window name distinct between the main window and each pop-out, preventing Begin/End collisions when both render in the same frame. --- HellionChat/Ui/ChatLogWindow.cs | 59 ++++++++++++++++++++++++--------- HellionChat/Ui/Popout.cs | 2 +- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 44ce1b6..965cb80 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1548,7 +1548,8 @@ public sealed class ChatLogWindow : Window Tab tab, PayloadHandler handler, float childHeight, - bool switchedTab + bool switchedTab, + string ownerId = "main" ) { // Capture these before entering the child so we can position the overlay @@ -1581,7 +1582,7 @@ public sealed class ChatLogWindow : Window // (which caused it to drift) and avoids the scrollbar's inner clip rect // (which caused it to be cut off on the right edge). if (_childScrolledUp) - DrawScrollToBottomButtonOverlay(childScreenPos, childWidth, childHeight); + DrawScrollToBottomButtonOverlay(childScreenPos, childWidth, childHeight, ownerId); } private void DrawLogNormalStyle(Tab tab, PayloadHandler handler, bool switchedTab) @@ -1631,33 +1632,59 @@ public sealed class ChatLogWindow : Window } - // UI-5: floating jump-to-latest button, drawn in the PARENT window after the - // ##chat2-messages child closes. Placing it here prevents two bugs: - // 1. Inside the child, SetCursorPos adds to CursorMaxPos.y and inflates - // ContentSize.y / ScrollMaxY every frame, so the button chased its own - // shadow and never landed at a stable position. - // 2. GetWindowWidth() inside the child includes the scrollbar column, so the - // button overlapped the scrollbar and was clipped by the inner clip rect. - // Screen-space positioning via SetCursorScreenPos is immune to both. + // UI-5: floating jump-to-latest button. + // + // Why a standalone overlay window rather than drawing in the parent? + // When this button was drawn in the parent window after the ##chat2-messages + // child closed, ImGui's hit-test still resolved g.HoveredWindow to the child + // (the child occupies the same screen rect). ItemHoverable then rejected the + // button submitted in the parent, so it was visible but never clickable. + // + // A top-level Begin/End window is a sibling in the window list, not nested + // under the child, so it wins the hit-test for its own rect and the button + // inside it is fully clickable. + // + // The ownerId parameter makes the window name unique per calling context so + // the main window and each pop-out don't share a single ImGui window entry. private void DrawScrollToBottomButtonOverlay( Vector2 childScreenPos, float childWidth, - float childHeight) + float childHeight, + string ownerId) { var size = ImGui.GetFrameHeight(); var pad = 8f * ImGuiHelpers.GlobalScale; var scrollbarWidth = ImGui.GetStyle().ScrollbarSize; + // Position confirmed correct in-game: bottom-right of the chat child, + // inset by pad, and pulled left of the vertical scrollbar. var btnPos = new Vector2( childScreenPos.X + childWidth - scrollbarWidth - size - pad, childScreenPos.Y + childHeight - size - pad); - ImGui.SetCursorScreenPos(btnPos); + ImGui.SetNextWindowPos(btnPos, ImGuiCond.Always); + ImGui.SetNextWindowSize(new Vector2(size, size), ImGuiCond.Always); - if (ImGuiUtil.IconButton( - FontAwesomeIcon.ArrowDown, - tooltip: HellionStrings.ChatLog_ScrollToBottom_Tooltip)) - _scrollToBottomRequested = true; + const ImGuiWindowFlags overlayFlags = + ImGuiWindowFlags.NoTitleBar + | ImGuiWindowFlags.NoResize + | ImGuiWindowFlags.NoMove + | ImGuiWindowFlags.NoScrollbar + | ImGuiWindowFlags.NoScrollWithMouse + | ImGuiWindowFlags.NoSavedSettings + | ImGuiWindowFlags.NoFocusOnAppearing + | ImGuiWindowFlags.NoBackground; + + // End() must be called unconditionally after Begin() — ImGui rule. + if (ImGui.Begin($"##scroll-to-bottom-overlay-{ownerId}", overlayFlags)) + { + if (ImGuiUtil.IconButton( + FontAwesomeIcon.ArrowDown, + tooltip: HellionStrings.ChatLog_ScrollToBottom_Tooltip)) + _scrollToBottomRequested = true; + } + + ImGui.End(); } private void DrawMessages( diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index 750cc55..4d9c2e7 100644 --- a/HellionChat/Ui/Popout.cs +++ b/HellionChat/Ui/Popout.cs @@ -118,7 +118,7 @@ internal class Popout : Window var handler = ChatLogWindow.HandlerLender.Borrow(); var logHeight = ImGui.GetContentRegionAvail().Y - inputBarHeight - hintBannerHeight; - ChatLogWindow.DrawMessageLog(Tab, handler, logHeight, false); + ChatLogWindow.DrawMessageLog(Tab, handler, logHeight, false, Tab.Identifier.ToString()); if (inputEnabled && InputBar != null) {