fix(chat): draw the jump-to-latest pill on top of the messages

The pill was drawn on the parent window's draw list after EndChild, but ImGui
renders a child window ON TOP of its parent — so the message text covered it.
Draw it instead on the thread child's own draw list, after the message loop
(later draw commands render on top), with a PushClipRect so the child's content
padding / scrollbar doesn't clip it. Clickability is unchanged (hand-drawn +
IsMouseHoveringRect / IsMouseClicked test the cursor directly), and it stays
pinned to the visible bottom-right corner via absolute screen coordinates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 01:23:23 -05:00
parent 57fa6470b7
commit 4c43f2e082

View File

@@ -1398,35 +1398,38 @@ void RenderChatTab(App* app)
atBottom = ImGui::GetScrollMaxY() <= 0.0f || ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 4.0f;
// Re-arm auto-scroll once the user returns to the bottom (after the wheel cooldown).
if (atBottom && s_chat_scroll_cooldown <= 0.0f) s_chat_auto_scroll = true;
}
ImGui::EndChild();
ImGui::PopStyleVar(2); // ##ChatMessages WindowPadding + zeroed vertical ItemSpacing
// Jump-to-latest pill when scrolled up (Q9). Hand-drawn (rect + IsMouseHoveringRect) rather than
// ImGui::Button: the pill overlaps the ##ChatMessages child, which owns mouse-hover in that
// region, so a real widget on the parent window never registers the click. IsMouseHoveringRect /
// IsMouseClicked test the cursor position directly, independent of window hover ownership.
if (!atBottom) {
ImDrawList* pdl = ImGui::GetWindowDrawList();
const ImVec2 pmin = jumpPillMin, pmax = jumpPillMax;
const float pw = pmax.x - pmin.x, ph = pmax.y - pmin.y;
const bool hov = ImGui::IsMouseHoveringRect(pmin, pmax);
pdl->AddRectFilled(pmin, pmax,
hov ? material::Primary() : material::WithAlpha(material::Primary(), 220),
ph * 0.5f);
ImFont* lf = ImGui::GetFont(); const float lsz = ImGui::GetFontSize();
const char* lbl = TR("chat_jump_latest");
const ImVec2 tszp = lf->CalcTextSizeA(lsz, FLT_MAX, 0.0f, lbl);
pdl->AddText(lf, lsz, ImVec2(pmin.x + (pw - tszp.x) * 0.5f, pmin.y + (ph - tszp.y) * 0.5f),
IM_COL32(255, 255, 255, 235), lbl);
if (hov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
s_scroll_to_cid = sel->cid;
s_chat_auto_scroll = true; // clicking Latest re-arms follow-the-newest
s_chat_scroll_cooldown = 0.0f;
// Jump-to-latest pill when scrolled up (Q9). Drawn LAST and INSIDE the thread child (on the
// child's own draw list) so it sits on top of the messages — a parent-window draw after
// EndChild renders UNDER the child and gets covered by the text. PushClipRect keeps the
// child's content padding / scrollbar from clipping it. Hand-drawn + IsMouseHoveringRect so
// the click lands despite the child owning hover here (a real widget would still miss it).
if (!atBottom) {
const ImVec2 pmin = jumpPillMin, pmax = jumpPillMax;
const float pw = pmax.x - pmin.x, ph = pmax.y - pmin.y;
const bool hov = ImGui::IsMouseHoveringRect(pmin, pmax);
dl->PushClipRect(pmin, pmax, false);
dl->AddRectFilled(pmin, pmax,
hov ? material::Primary() : material::WithAlpha(material::Primary(), 220),
ph * 0.5f);
ImFont* lf = ImGui::GetFont(); const float lsz = ImGui::GetFontSize();
const char* lbl = TR("chat_jump_latest");
const ImVec2 tszp = lf->CalcTextSizeA(lsz, FLT_MAX, 0.0f, lbl);
dl->AddText(lf, lsz, ImVec2(pmin.x + (pw - tszp.x) * 0.5f, pmin.y + (ph - tszp.y) * 0.5f),
IM_COL32(255, 255, 255, 235), lbl);
dl->PopClipRect();
if (hov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
s_scroll_to_cid = sel->cid;
s_chat_auto_scroll = true; // clicking Latest re-arms follow-the-newest
s_chat_scroll_cooldown = 0.0f;
}
}
}
}
ImGui::EndChild();
ImGui::PopStyleVar(2); // ##ChatMessages WindowPadding + zeroed vertical ItemSpacing
// Composer is rendered OUTSIDE this box, directly below it (see after EndChild).
} else {
if (convs.empty()) centeredEmptyState(ICON_MD_FORUM, TR("chat_empty_title"), TR("chat_empty_start"));