mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-06-06 14:12:14 +00:00
feat(bubble): apply full UX review — labels, inline percent, accent, snaps
Implements every recommendation from the UI/UX review of the two-bar
bubble: readability bump, faster glanceability, per-provider identity,
smarter resize curve, and a richer snap behavior set.
Readability + sizing:
- Default width 180 → 200, min 120 → 140 (Windows shell minimum legible
status-text floor is 12 px; previous breakpoint produced 11 px text)
- Discrete breakpoint table replaces the linear bar_h = h/4 formula —
140/200/280/360 widths map to (bar_h, font, row_gap) of
(12,11,4)/(16,13,6)/(20,15,8)/(24,17,10)
- Aspect tapers 3:1 → 2.8:1 → 2.6:1 as width grows so bars stay
proportionally chunky at large sizes
- Right-column width now derived from GetTextExtentPoint32W of
"100% · 23h" against the real font instead of the bar_h * 6 heuristic
Glanceability:
- ring_color_for_percent rewritten as a 4-band cliff: orange < 60,
amber 60-80, red 80-95, deep red ≥ 95 — gradient hid the 75 vs 85
difference at narrow bar widths
- Percent moved inside the bar (right-aligned with auto-contrast picked
from luminance of the underlying fill or track)
- Right column now shows only the countdown ("2h 14m"), not the
combined "X% · 2h 14m" string. i18n::format_countdown is now pub so
the bubble can request countdown-only; the panel still renders the
combined string via format_window
- "5h" / "7d" muted labels added in a new left column
- Rows whose bar is ≥ 95 % get a 15 %-red blush behind the entire row;
fill color also brightens via a slow triangle wave (TIMER_PULSE,
80 ms, only armed when at least one bar is in the alarm band)
Per-provider identity:
- 4-px vertical accent stripe at the left edge — Claude orange
(#D97757), Codex green (#10A37F) — disambiguates the two bubbles
when both are enabled. Mirrored into the expanded panel so the
identity carries across surfaces
Snap behavior:
- Corner snap: release within 32 px of a work-area corner slams the
bubble into the corner with a 12-px inset
- Taskbar-adjacency: SHAppBarMessage(ABM_GETTASKBARPOS) supplies the
taskbar rect/edge; bubble docks against the inner face with a
4-px gap on the docked edge
- Peer-Y alignment: when a second bubble exists within ±8 px on Y
at release time, the dragged bubble snaps to share its top edge
- WM_SETTINGCHANGE handler re-clamps bubbles into the new work area
when taskbar moves / auto-hides / DPI changes
Color packing:
- Direct DIB writes use rgb_to_dib (B,G,R,X memory order) instead of
into_colorref (R,G,B,X COLORREF order). Already corrected in the
previous redesign for the bar fills, but the accent stripe and blush
go through the same path now
This commit is contained in:
+9
-2
@@ -502,8 +502,15 @@ fn propagate_to_ui() {
|
||||
let entry = snap.snapshots.get(&id);
|
||||
let session_pct = entry.map(|s| s.windows.primary.utilization);
|
||||
let weekly_pct = entry.map(|s| s.windows.secondary.utilization);
|
||||
let session_text = entry.map(|s| s.primary_text.clone()).unwrap_or_default();
|
||||
let weekly_text = entry.map(|s| s.secondary_text.clone()).unwrap_or_default();
|
||||
// The bubble paints the percent inline inside the bar fill, so it
|
||||
// only needs the countdown string on the right. The panel still
|
||||
// shows the combined "X% · Yh" string via `primary_text`.
|
||||
let session_text = entry
|
||||
.map(|s| i18n::format_countdown(s.windows.primary.resets_at, &snap.i18n_strings))
|
||||
.unwrap_or_default();
|
||||
let weekly_text = entry
|
||||
.map(|s| i18n::format_countdown(s.windows.secondary.resets_at, &snap.i18n_strings))
|
||||
.unwrap_or_default();
|
||||
bubble::update_data(
|
||||
hwnd.to_hwnd(),
|
||||
session_pct,
|
||||
|
||||
+675
-153
File diff suppressed because it is too large
Load Diff
+3
-1
@@ -198,7 +198,9 @@ pub fn format_window(window: &crate::usage::Window, strings: &LocaleStrings) ->
|
||||
}
|
||||
}
|
||||
|
||||
fn format_countdown(resets_at: Option<SystemTime>, strings: &LocaleStrings) -> String {
|
||||
/// Countdown only — used by the bubble, which renders the percent inside the
|
||||
/// bar fill and only needs the time-to-reset on the right.
|
||||
pub fn format_countdown(resets_at: Option<SystemTime>, strings: &LocaleStrings) -> String {
|
||||
let Some(reset) = resets_at else {
|
||||
return String::new();
|
||||
};
|
||||
|
||||
@@ -276,6 +276,23 @@ fn paint(hwnd: HWND, hdc: HDC) {
|
||||
FillRect(hdc, &rc, bg_brush);
|
||||
let _ = DeleteObject(bg_brush);
|
||||
|
||||
// 4-px accent stripe matching the bubble — same provider color so the
|
||||
// identity carries across both surfaces.
|
||||
let stripe_color = match data.model {
|
||||
ProviderId::Claude => Color::from_hex("#D97757"),
|
||||
ProviderId::ChatGpt => Color::from_hex("#10A37F"),
|
||||
};
|
||||
let stripe_w = scaled(4);
|
||||
let stripe_rect = RECT {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: stripe_w,
|
||||
bottom: rc.bottom,
|
||||
};
|
||||
let stripe_brush = CreateSolidBrush(COLORREF(stripe_color.into_colorref()));
|
||||
FillRect(hdc, &stripe_rect, stripe_brush);
|
||||
let _ = DeleteObject(stripe_brush);
|
||||
|
||||
// Header row: model label
|
||||
let header = match data.model {
|
||||
ProviderId::Claude => data.strings.claude_label.clone(),
|
||||
|
||||
Reference in New Issue
Block a user