fix(ui): clamp tooltips to a comfortable width instead of spanning the screen

Audited every tooltip. The shared helper material::Tooltip wrapped ImGui's
SetTooltip with no width limit, so a long tooltip laid out on one line and
stretched across the whole display — most visible on the newly reworded
note/consolidation tooltips. 258 call sites go through this helper, so the fix is
central:

- material::Tooltip / BeginTooltip now push a text wrap position (420 logical px,
  DPI-scaled) so long text wraps to a comfortable column; short text is
  unaffected (it's already narrower). A caller that pushes its own wrap pos nests
  cleanly. Also fixes a latent style-stack leak when ImGui::BeginTooltip returns
  false (the pushed style is now popped).

Six sites bypassed the helper with raw ImGui tooltips. The two that actually
stretched are migrated to material::Tooltip: the alert/bell toast-hover showing a
full (now ~250-char) message, and the wallets dialog's folder-path hover. The
chat over-cap tooltip is migrated too for consistency. The remaining three
(two wallet-recovery dialog tips, the chat composer's parts tooltip) already
clamp via their own PushTextWrapPos and are left as-is.

Build + tests + hygiene green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-26 14:45:39 -05:00
co-authored by Claude Opus 4.8
parent d83169c963
commit fc9f8d52c3
4 changed files with 38 additions and 11 deletions
+33 -8
View File
@@ -36,27 +36,52 @@ inline void PopTooltipStyle()
ImGui::PopStyleVar(); ImGui::PopStyleVar();
} }
// Drop-in replacement for ImGui::SetTooltip (same printf-style signature). (No IM_FMTARGS: GCC // Comfortable maximum tooltip text width (logical px, scaled by DPI). Long tooltip text wraps at this
// rejects the format attribute on an inline definition; call sites already passed SetTooltip's check.) // instead of stretching across the whole screen; short text is unaffected (it's narrower than the cap).
inline constexpr float kTooltipWrapWidth = 420.0f;
// The wrap position (window-local x) to clamp a just-begun tooltip's text to. Called right after
// ImGui::BeginTooltip, when the cursor sits at the tooltip's content origin.
inline float tooltipWrapPosX()
{
return ImGui::GetCursorPosX() + kTooltipWrapWidth * Layout::dpiScale();
}
// Drop-in replacement for ImGui::SetTooltip (same printf-style signature), with the text clamped to a
// comfortable width. (No IM_FMTARGS: GCC rejects the format attribute on an inline definition; call sites
// already passed SetTooltip's check.)
inline void Tooltip(const char* fmt, ...) inline void Tooltip(const char* fmt, ...)
{ {
PushTooltipStyle(); PushTooltipStyle();
va_list args; if (ImGui::BeginTooltip()) {
va_start(args, fmt); ImGui::PushTextWrapPos(tooltipWrapPosX());
ImGui::SetTooltipV(fmt, args); va_list args;
va_end(args); va_start(args, fmt);
ImGui::TextV(fmt, args);
va_end(args);
ImGui::PopTextWrapPos();
ImGui::EndTooltip();
}
PopTooltipStyle(); PopTooltipStyle();
} }
// Drop-in replacements for ImGui::BeginTooltip / ImGui::EndTooltip (must be paired, as before). // Drop-in replacements for ImGui::BeginTooltip / ImGui::EndTooltip (must be paired, as before). The begin
// clamps text to a comfortable width by default; a caller that pushes its own wrap pos nests cleanly (it
// pops back to this one). Text-only callers need do nothing; non-text content ignores the wrap pos.
inline bool BeginTooltip() inline bool BeginTooltip()
{ {
PushTooltipStyle(); PushTooltipStyle();
return ImGui::BeginTooltip(); if (ImGui::BeginTooltip()) {
ImGui::PushTextWrapPos(tooltipWrapPosX());
return true;
}
PopTooltipStyle(); // not shown: EndTooltip won't run, so undo the style push here
return false;
} }
inline void EndTooltip() inline void EndTooltip()
{ {
ImGui::PopTextWrapPos(); // pair the wrap pos pushed in BeginTooltip
ImGui::EndTooltip(); ImGui::EndTooltip();
PopTooltipStyle(); PopTooltipStyle();
} }
+2 -1
View File
@@ -6,6 +6,7 @@
#include "schema/ui_schema.h" #include "schema/ui_schema.h"
#include "material/type.h" #include "material/type.h"
#include "material/draw_helpers.h" #include "material/draw_helpers.h"
#include "material/tooltip_style.h" // material::Tooltip (width-clamped)
#include "../embedded/IconsMaterialDesign.h" #include "../embedded/IconsMaterialDesign.h"
#include "imgui.h" #include "imgui.h"
@@ -210,7 +211,7 @@ void Notifications::render()
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) requestHistory(); if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) requestHistory();
} }
if (toast_hovered_ && clipped) if (toast_hovered_ && clipped)
ImGui::SetTooltip("%s", notif.message.c_str()); material::Tooltip("%s", notif.message.c_str()); // wrapped: a long alert message mustn't span the screen
} }
} // namespace ui } // namespace ui
+1 -1
View File
@@ -2216,7 +2216,7 @@ void RenderChatTab(App* app)
// When Send is disabled *because the message is over the cap*, explain why on hover — the // When Send is disabled *because the message is over the cap*, explain why on hover — the
// gauge went red but the numeric reason is no longer always-visible. // gauge went red but the numeric reason is no longer always-visible.
if (overCap && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) if (overCap && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
ImGui::SetTooltip("%s %d / %d", TR("chat_len_over"), used, kBodyMaxBytes); material::Tooltip("%s %d / %d", TR("chat_len_over"), used, kBodyMaxBytes);
ImGui::PopStyleColor(3); ImGui::PopStyleColor(3);
if (submit && s_compose[0] != '\0' && !overCap) { if (submit && s_compose[0] != '\0' && !overCap) {
+2 -1
View File
@@ -39,6 +39,7 @@
#include "../layout.h" #include "../layout.h"
#include "../material/colors.h" #include "../material/colors.h"
#include "../material/draw_helpers.h" #include "../material/draw_helpers.h"
#include "../material/tooltip_style.h" // material::Tooltip (width-clamped)
#include "../material/type.h" #include "../material/type.h"
#include "folder_picker.h" #include "folder_picker.h"
@@ -557,7 +558,7 @@ public:
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
const float textW = listW - bh2 - 2.0f * Layout::spacingSm(); const float textW = listW - bh2 - 2.0f * Layout::spacingSm();
Type().textColored(TypeStyle::Caption, OnSurface(), elideFront(folder, textW).c_str()); Type().textColored(TypeStyle::Caption, OnSurface(), elideFront(folder, textW).c_str());
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", folder.c_str()); if (ImGui::IsItemHovered()) material::Tooltip("%s", folder.c_str()); // wrapped: long paths
ImGui::PopID(); ImGui::PopID();
} }
if (!toRemove.empty()) { if (!toRemove.empty()) {