From bd75f2453c6085ccf6dd7ab4bb8adc3e3f7a4170 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Thu, 21 May 2026 14:22:58 +0200 Subject: [PATCH] fix(ui): move scroll-to-bottom button into the chat header toolbar Drop the three-attempt floating overlay entirely. The button now lives in the chat header toolbar (DrawScrollToBottomToolbarButton), visible only when the user is scrolled above the live end. Toolbar layout: honorific slot, scroll button, pop-out button flush-right -- pop-out position unchanged. --- HellionChat/Ui/ChatLogWindow.cs | 113 ++++++++++---------------------- HellionChat/Ui/Popout.cs | 2 +- 2 files changed, 35 insertions(+), 80 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 965cb80..54a586a 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -1544,20 +1544,8 @@ public sealed class ChatLogWindow : Window CurrentHideState = HideState.User; } - internal void DrawMessageLog( - Tab tab, - PayloadHandler handler, - float childHeight, - bool switchedTab, - string ownerId = "main" - ) + internal void DrawMessageLog(Tab tab, PayloadHandler handler, float childHeight, bool switchedTab) { - // Capture these before entering the child so we can position the overlay - // button in the parent window's coordinate space afterward. Inside the - // child the same queries would return child-local values. - var childScreenPos = ImGui.GetCursorScreenPos(); - var childWidth = ImGui.GetContentRegionAvail().X; - using (var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight))) { if (child.Success) @@ -1567,8 +1555,9 @@ public sealed class ChatLogWindow : Window else DrawLogNormalStyle(tab, handler, switchedTab); - // Cache scroll state while we are still inside the child so - // GetScrollMaxY / GetScrollY refer to the child's scroll context. + // Cached for the header toolbar's scroll-to-bottom button, which is + // drawn one frame later. GetScrollMaxY / GetScrollY here refer to + // the child's scroll context. _childScrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f; } else @@ -1576,13 +1565,6 @@ public sealed class ChatLogWindow : Window _childScrolledUp = false; } } - - // Draw the overlay button in the parent window, outside the child. - // This prevents the button from inflating ScrollMaxY inside the child - // (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, ownerId); } private void DrawLogNormalStyle(Tab tab, PayloadHandler handler, bool switchedTab) @@ -1632,61 +1614,6 @@ public sealed class ChatLogWindow : Window } - // 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, - 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.SetNextWindowPos(btnPos, ImGuiCond.Always); - ImGui.SetNextWindowSize(new Vector2(size, size), ImGuiCond.Always); - - 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( Tab tab, PayloadHandler handler, @@ -2390,14 +2317,42 @@ public sealed class ChatLogWindow : Window Plugin.WantedTab = null; } - // DrawChatHeaderToolbar: renders the pop-out button for the active tab. - // v1.3.0 also renders the optional Honorific title slot left of it. + // DrawChatHeaderToolbar: renders the honorific title slot, the optional + // scroll-to-bottom button, and the pop-out button for the active tab. + // v1.3.0 added the title slot; v1.5.5 added the scroll-to-bottom button. private void DrawChatHeaderToolbar(Tab tab) { DrawHonorificTitleSlot(); + DrawScrollToBottomToolbarButton(); DrawPopOutButton(tab); } + // Draws an arrow-down button in the toolbar when the user has scrolled up + // from the live end of the chat log. Clicking it requests a snap to bottom. + // + // _childScrolledUp is set at the end of DrawMessageLog, which runs AFTER + // DrawChatHeaderToolbar in the same frame. So this button always reflects the + // previous frame's scroll state, a one-frame lag that is imperceptible in use. + // + // Both this button and DrawPopOutButton use SetCursorPosX with absolute + // positioning (cursorX + GetContentRegionAvail().X - N * iconWidth). Because + // each call computes its own target X from the right edge, they are independent + // of each other and of what the cursor position happens to be at call time. + // The pop-out button lands at rightEdge - iconWidth regardless of call order. + private void DrawScrollToBottomToolbarButton() + { + if (!_childScrolledUp) + return; + + var avail = ImGui.GetContentRegionAvail().X; + var iconWidth = ImGui.GetFrameHeight(); + var spacing = ImGui.GetStyle().ItemSpacing.X; + ImGui.SetCursorPosX(ImGui.GetCursorPosX() + avail - 2 * iconWidth - spacing); + + if (ImGuiUtil.IconButton(FontAwesomeIcon.ArrowDown, tooltip: HellionStrings.ChatLog_ScrollToBottom_Tooltip)) + _scrollToBottomRequested = true; + } + private void DrawPopOutButton(Tab tab) { var avail = ImGui.GetContentRegionAvail().X; diff --git a/HellionChat/Ui/Popout.cs b/HellionChat/Ui/Popout.cs index 4d9c2e7..750cc55 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, Tab.Identifier.ToString()); + ChatLogWindow.DrawMessageLog(Tab, handler, logHeight, false); if (inputEnabled && InputBar != null) {