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.
This commit is contained in:
2026-05-21 13:38:51 +02:00
parent 3de6e4a3cb
commit 65fea0e5f5
+49 -21
View File
@@ -780,6 +780,10 @@ public sealed class ChatLogWindow : Window
// frame's scroll-snap check forces a jump to the live end. // frame's scroll-snap check forces a jump to the live end.
private bool _scrollToBottomRequested; 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() public override void Draw()
{ {
DrewThisFrame = true; DrewThisFrame = true;
@@ -1547,14 +1551,37 @@ public sealed class ChatLogWindow : Window
bool switchedTab bool switchedTab
) )
{ {
using var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight)); // Capture these before entering the child so we can position the overlay
if (!child.Success) // button in the parent window's coordinate space afterward. Inside the
return; // 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)
{
if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps) if (tab.DisplayTimestamp && Plugin.Config.PrettierTimestamps)
DrawLogTableStyle(tab, handler, switchedTab); DrawLogTableStyle(tab, handler, switchedTab);
else else
DrawLogNormalStyle(tab, handler, switchedTab); 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) private void DrawLogNormalStyle(Tab tab, PayloadHandler handler, bool switchedTab)
@@ -1567,8 +1594,6 @@ public sealed class ChatLogWindow : Window
_scrollToBottomRequested = false; _scrollToBottomRequested = false;
handler.Draw(); handler.Draw();
DrawScrollToBottomButton();
} }
private void DrawLogTableStyle(Tab tab, PayloadHandler handler, bool switchedTab) 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 // UI-5: floating jump-to-latest button, drawn in the PARENT window after the
// scrolled up from the live end. Pinned to the bottom-right of the visible // ##chat2-messages child closes. Placing it here prevents two bugs:
// viewport inside the scrolling child (cursor coords are content-space, the // 1. Inside the child, SetCursorPos adds to CursorMaxPos.y and inflates
// viewport is [ScrollY, ScrollY + WindowHeight]). // ContentSize.y / ScrollMaxY every frame, so the button chased its own
private void DrawScrollToBottomButton() // 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 size = ImGui.GetFrameHeight();
var pad = 8f * ImGuiHelpers.GlobalScale; var pad = 8f * ImGuiHelpers.GlobalScale;
ImGui.SetCursorPosX(ImGui.GetScrollX() + ImGui.GetWindowWidth() - size - pad); var scrollbarWidth = ImGui.GetStyle().ScrollbarSize;
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( if (ImGuiUtil.IconButton(
FontAwesomeIcon.ArrowDown, FontAwesomeIcon.ArrowDown,