From 65fea0e5f570f836f98ce0de741248b5ce03cfb7 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Thu, 21 May 2026 13:38:51 +0200 Subject: [PATCH] fix(ui): render scroll-to-bottom button as a parent overlay The button was drawn inside the ##chat2-messages child via SetCursorPos, which inflated ContentSize.y / ScrollMaxY each frame (causing positional drift) and was clipped by the scrollbar's inner clip rect (causing right- edge cutoff). Move it to the parent window using screen-space coordinates captured before the child opens; the scroll state is cached inside the child while GetScrollMaxY/Y still refer to the child's scroll context. --- HellionChat/Ui/ChatLogWindow.cs | 80 ++++++++++++++++++++++----------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/HellionChat/Ui/ChatLogWindow.cs b/HellionChat/Ui/ChatLogWindow.cs index 36cec74..44ce1b6 100644 --- a/HellionChat/Ui/ChatLogWindow.cs +++ b/HellionChat/Ui/ChatLogWindow.cs @@ -780,6 +780,10 @@ public sealed class ChatLogWindow : Window // frame's scroll-snap check forces a jump to the live end. private bool _scrollToBottomRequested; + // UI-5: cached each frame inside the ##chat2-messages child. True when the + // user has scrolled up enough that the overlay button should be shown. + private bool _childScrolledUp; + public override void Draw() { DrewThisFrame = true; @@ -1547,14 +1551,37 @@ public sealed class ChatLogWindow : Window bool switchedTab ) { - using var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight)); - if (!child.Success) - return; + // 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; - if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps) - DrawLogTableStyle(tab, handler, switchedTab); - else - DrawLogNormalStyle(tab, handler, switchedTab); + using (var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight))) + { + if (child.Success) + { + if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps) + DrawLogTableStyle(tab, handler, switchedTab); + 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. + _childScrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f; + } + else + { + _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); } private void DrawLogNormalStyle(Tab tab, PayloadHandler handler, bool switchedTab) @@ -1567,8 +1594,6 @@ public sealed class ChatLogWindow : Window _scrollToBottomRequested = false; handler.Draw(); - - DrawScrollToBottomButton(); } private void DrawLogTableStyle(Tab tab, PayloadHandler handler, bool switchedTab) @@ -1604,27 +1629,30 @@ public sealed class ChatLogWindow : Window } } - // Draw the scroll-to-bottom button after EndTable so it renders as a - // post-table overlay, not inside an active table cell. The outer - // ItemSpacing=Zero push has ended; restore normal spacing so the button - // position matches the DrawLogNormalStyle path. - using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, oldItemSpacing)) - DrawScrollToBottomButton(); } - // UI-5: floating jump-to-latest button, shown only while the user has - // scrolled up from the live end. Pinned to the bottom-right of the visible - // viewport inside the scrolling child (cursor coords are content-space, the - // viewport is [ScrollY, ScrollY + WindowHeight]). - private void DrawScrollToBottomButton() + // 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. + private void DrawScrollToBottomButtonOverlay( + Vector2 childScreenPos, + float childWidth, + float childHeight) { - if (ImGui.GetScrollMaxY() - ImGui.GetScrollY() <= 1f) - return; + var size = ImGui.GetFrameHeight(); + var pad = 8f * ImGuiHelpers.GlobalScale; + var scrollbarWidth = ImGui.GetStyle().ScrollbarSize; - var size = ImGui.GetFrameHeight(); - var pad = 8f * ImGuiHelpers.GlobalScale; - ImGui.SetCursorPosX(ImGui.GetScrollX() + ImGui.GetWindowWidth() - size - pad); - ImGui.SetCursorPosY(ImGui.GetScrollY() + ImGui.GetWindowHeight() - size - pad); + var btnPos = new Vector2( + childScreenPos.X + childWidth - scrollbarWidth - size - pad, + childScreenPos.Y + childHeight - size - pad); + + ImGui.SetCursorScreenPos(btnPos); if (ImGuiUtil.IconButton( FontAwesomeIcon.ArrowDown,