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
+54 -26
View File
@@ -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,