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.
This commit is contained in:
2026-05-21 14:22:58 +02:00
parent c909d1646b
commit bd75f2453c
2 changed files with 35 additions and 80 deletions
+34 -79
View File
@@ -1544,20 +1544,8 @@ public sealed class ChatLogWindow : Window
CurrentHideState = HideState.User; CurrentHideState = HideState.User;
} }
internal void DrawMessageLog( internal void DrawMessageLog(Tab tab, PayloadHandler handler, float childHeight, bool switchedTab)
Tab tab,
PayloadHandler handler,
float childHeight,
bool switchedTab,
string ownerId = "main"
)
{ {
// 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))) using (var child = ImRaii.Child("##chat2-messages", new Vector2(-1, childHeight)))
{ {
if (child.Success) if (child.Success)
@@ -1567,8 +1555,9 @@ public sealed class ChatLogWindow : Window
else else
DrawLogNormalStyle(tab, handler, switchedTab); DrawLogNormalStyle(tab, handler, switchedTab);
// Cache scroll state while we are still inside the child so // Cached for the header toolbar's scroll-to-bottom button, which is
// GetScrollMaxY / GetScrollY refer to the child's scroll context. // drawn one frame later. GetScrollMaxY / GetScrollY here refer to
// the child's scroll context.
_childScrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f; _childScrolledUp = ImGui.GetScrollMaxY() - ImGui.GetScrollY() > 1f;
} }
else else
@@ -1576,13 +1565,6 @@ public sealed class ChatLogWindow : Window
_childScrolledUp = false; _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) 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( private void DrawMessages(
Tab tab, Tab tab,
PayloadHandler handler, PayloadHandler handler,
@@ -2390,14 +2317,42 @@ public sealed class ChatLogWindow : Window
Plugin.WantedTab = null; Plugin.WantedTab = null;
} }
// DrawChatHeaderToolbar: renders the pop-out button for the active tab. // DrawChatHeaderToolbar: renders the honorific title slot, the optional
// v1.3.0 also renders the optional Honorific title slot left of it. // 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) private void DrawChatHeaderToolbar(Tab tab)
{ {
DrawHonorificTitleSlot(); DrawHonorificTitleSlot();
DrawScrollToBottomToolbarButton();
DrawPopOutButton(tab); 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) private void DrawPopOutButton(Tab tab)
{ {
var avail = ImGui.GetContentRegionAvail().X; var avail = ImGui.GetContentRegionAvail().X;
+1 -1
View File
@@ -118,7 +118,7 @@ internal class Popout : Window
var handler = ChatLogWindow.HandlerLender.Borrow(); var handler = ChatLogWindow.HandlerLender.Borrow();
var logHeight = ImGui.GetContentRegionAvail().Y - inputBarHeight - hintBannerHeight; 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) if (inputEnabled && InputBar != null)
{ {