mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-06-06 22:12:26 +00:00
fix(ui): improve bubble visual hierarchy and contrast
- Lift inner time ring above WCAG 1.4.11 (track #303030 -> #404040, stroke floor 1 -> 2 logical, ring gap 3 -> 4). - Breathe tail bar/text with bar_text_gap=8 (was pad=6); right inset 12 -> 14 logical so text clears the stadium end-cap. - Reweight typography: head percent FW_BOLD, "5H" tag and tail percent FW_SEMIBOLD, tail countdown stays normal but takes muted color so the percent reads as the headline. - Tone down usage track (#3A3A3A/#D6D6D6 -> #2C2C2C/#E2E2E2) so fill dominates at low percentages. - Differentiate lane mass: usage bar 9%/5..12 -> 10%/6..12, time bar 5%/3..7 -> 4%/3..6, lane gap 5 -> 6. Time bar now reads as supporting context, not a competing quota. - Min-fill guard on weekly bar: sub-cap fills floor at one cap-diameter so 1% renders as a recognizable dot. - head_pad 4 -> 5; big-font ratio 26% -> 24% of head diameter (BOLD compensates for the size cut).
This commit is contained in:
+45
-24
@@ -1061,7 +1061,7 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo
|
||||
let height_px = scale_to_dpi(bubble_height_logical(size_logical), dpi);
|
||||
let head_diameter = height_px;
|
||||
|
||||
let head_pad = scale_to_dpi(4, dpi);
|
||||
let head_pad = scale_to_dpi(5, dpi);
|
||||
let ring_stroke_w = scale_to_dpi(3, dpi).clamp(2, 4) as f32;
|
||||
let ring_cx = (head_diameter as f32) / 2.0;
|
||||
let ring_cy = (height_px as f32) / 2.0;
|
||||
@@ -1069,11 +1069,14 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo
|
||||
// inside the head padding. ring_radius is the centerline radius.
|
||||
let ring_outer = (head_diameter as f32) / 2.0 - (head_pad as f32);
|
||||
let ring_radius = ring_outer - ring_stroke_w / 2.0;
|
||||
let time_ring_stroke_w = scale_to_dpi(2, dpi).clamp(1, 3) as f32;
|
||||
// Inner ring renders the remaining-time arc. Floor stroke at 2 logical so
|
||||
// it stays visible at smaller bubble sizes (clamp 1 produced a hairline
|
||||
// that disappeared into the track on dark themes).
|
||||
let time_ring_stroke_w = scale_to_dpi(2, dpi).clamp(2, 3) as f32;
|
||||
let time_ring_radius =
|
||||
(ring_radius - ring_stroke_w - scale_to_dpi(3, dpi) as f32).max(time_ring_stroke_w);
|
||||
(ring_radius - ring_stroke_w - scale_to_dpi(4, dpi) as f32).max(time_ring_stroke_w);
|
||||
|
||||
let big_font_px = (head_diameter * 26 / 100).max(scale_to_dpi(11, dpi));
|
||||
let big_font_px = (head_diameter * 24 / 100).max(scale_to_dpi(11, dpi));
|
||||
let small_font_px = ((big_font_px * 55) / 100).max(scale_to_dpi(9, dpi));
|
||||
let main_font_px = small_font_px;
|
||||
|
||||
@@ -1096,15 +1099,20 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo
|
||||
};
|
||||
|
||||
let tail_left = head_diameter;
|
||||
let tail_right = width_px - scale_to_dpi(12, dpi);
|
||||
let tail_right = width_px - scale_to_dpi(14, dpi);
|
||||
let pad = scale_to_dpi(6, dpi);
|
||||
// Breathing room between bar end and the right-aligned text. 6 logical
|
||||
// had the bar visually colliding with "100%" / countdown glyphs.
|
||||
let bar_text_gap = scale_to_dpi(8, dpi);
|
||||
|
||||
let countdown_w = measure_text_w(mem_dc, COUNTDOWN_TEMPLATE, main_font_px);
|
||||
let pct_reserve_w = measure_text_w(mem_dc, "100%", small_font_px) + scale_to_dpi(2, dpi);
|
||||
|
||||
let usage_bar_h = (height_px * 9 / 100).clamp(scale_to_dpi(5, dpi), scale_to_dpi(12, dpi));
|
||||
let time_bar_h = (height_px * 5 / 100).clamp(scale_to_dpi(3, dpi), scale_to_dpi(7, dpi));
|
||||
let lane_gap = scale_to_dpi(5, dpi);
|
||||
// Usage bar carries the primary signal; make it ~2.5x the mass of the
|
||||
// time bar so the two lanes don't read as two competing quotas.
|
||||
let usage_bar_h = (height_px * 10 / 100).clamp(scale_to_dpi(6, dpi), scale_to_dpi(12, dpi));
|
||||
let time_bar_h = (height_px * 4 / 100).clamp(scale_to_dpi(3, dpi), scale_to_dpi(6, dpi));
|
||||
let lane_gap = scale_to_dpi(6, dpi);
|
||||
let lanes_h = usage_bar_h + lane_gap + time_bar_h;
|
||||
let usage_bar_top = (height_px - lanes_h) / 2;
|
||||
let time_bar_top = usage_bar_top + usage_bar_h + lane_gap;
|
||||
@@ -1116,14 +1124,14 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo
|
||||
let content_w = (content_right - content_left).max(0);
|
||||
let bar_min = scale_to_dpi(8, dpi);
|
||||
let desired_text_w = countdown_w.max(pct_reserve_w);
|
||||
let text_w = if content_w >= desired_text_w + pad + bar_min {
|
||||
let text_w = if content_w >= desired_text_w + bar_text_gap + bar_min {
|
||||
desired_text_w
|
||||
} else {
|
||||
(content_w - pad - bar_min).max(0)
|
||||
(content_w - bar_text_gap - bar_min).max(0)
|
||||
};
|
||||
let text_left = content_right - text_w;
|
||||
let bar_left = content_left;
|
||||
let bar_right = (text_left - pad).max(bar_left + bar_min);
|
||||
let bar_right = (text_left - bar_text_gap).max(bar_left + bar_min);
|
||||
|
||||
BubbleLayout {
|
||||
canvas_w: width_px,
|
||||
@@ -1181,19 +1189,21 @@ fn paint_bubble_pixmap(layout: &BubbleLayout, inputs: &PaintInputs) -> Option<Pi
|
||||
Color::from_hex("#F3F3F3")
|
||||
};
|
||||
let track = if inputs.is_dark {
|
||||
Color::from_hex("#3A3A3A")
|
||||
Color::from_hex("#2C2C2C")
|
||||
} else {
|
||||
Color::from_hex("#D6D6D6")
|
||||
Color::from_hex("#E2E2E2")
|
||||
};
|
||||
// Inner-ring / time-bar neutral track. Lifted off the background to
|
||||
// clear WCAG 1.4.11 3:1 on dark themes (#303030 on #1F1F1F was ~1.13:1).
|
||||
let time_track = if inputs.is_dark {
|
||||
Color::from_hex("#303030")
|
||||
Color::from_hex("#404040")
|
||||
} else {
|
||||
Color::from_hex("#E0E0E0")
|
||||
};
|
||||
let time_fill = if inputs.is_dark {
|
||||
Color::from_hex("#9A9A9A")
|
||||
Color::from_hex("#B0B0B0")
|
||||
} else {
|
||||
Color::from_hex("#777777")
|
||||
Color::from_hex("#666666")
|
||||
};
|
||||
|
||||
// ---- Stadium background ----
|
||||
@@ -1306,7 +1316,12 @@ fn paint_bubble_pixmap(layout: &BubbleLayout, inputs: &PaintInputs) -> Option<Pi
|
||||
let t = pulse_triangle(inputs.pulse_phase);
|
||||
color = brighten(color, t);
|
||||
}
|
||||
let clipped_w = fill_w.min(bar_w);
|
||||
// Floor at one cap-diameter so a 1% reading still renders
|
||||
// as a recognizable dot rather than a sub-pixel sliver.
|
||||
let mut clipped_w = fill_w.min(bar_w);
|
||||
if clipped_w < bar_h {
|
||||
clipped_w = bar_h.min(bar_w);
|
||||
}
|
||||
paint_pill(&mut pixmap, bar_x, bar_y, clipped_w, bar_h, cap, color);
|
||||
}
|
||||
}
|
||||
@@ -1592,15 +1607,19 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) {
|
||||
Color::from_hex("#1F1F1F")
|
||||
};
|
||||
let muted_color = if inputs.is_dark {
|
||||
Color::from_hex("#888888")
|
||||
Color::from_hex("#A8A8A8")
|
||||
} else {
|
||||
Color::from_hex("#6E6E6E")
|
||||
Color::from_hex("#5E5E5E")
|
||||
};
|
||||
|
||||
let font_name = wide_str("Segoe UI");
|
||||
unsafe {
|
||||
let big_font = create_font(layout.big_font_px, &font_name, FW_SEMIBOLD.0 as i32);
|
||||
let small_font = create_font(layout.small_font_px, &font_name, FW_NORMAL.0 as i32);
|
||||
// Big head percent uses FW_BOLD to anchor the eye against the ring.
|
||||
// small_font is semibold because it carries both the "5H" window tag
|
||||
// and the tail weekly-percent — both want a touch more weight than
|
||||
// the countdown text rendered with main_font.
|
||||
let big_font = create_font(layout.big_font_px, &font_name, FW_BOLD.0 as i32);
|
||||
let small_font = create_font(layout.small_font_px, &font_name, FW_SEMIBOLD.0 as i32);
|
||||
let main_font = create_font(layout.main_font_px, &font_name, FW_NORMAL.0 as i32);
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
|
||||
@@ -1615,13 +1634,13 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) {
|
||||
SetTextColor(hdc, COLORREF(muted_color.into_colorref()));
|
||||
let head_label_rect_w = layout.head_label_rect.right - layout.head_label_rect.left;
|
||||
let head_label_text: &str = if inputs.session_text.is_empty() {
|
||||
"5h"
|
||||
"5H"
|
||||
} else if measure_text_w(hdc, &inputs.session_text, layout.small_font_px)
|
||||
<= head_label_rect_w
|
||||
{
|
||||
inputs.session_text.as_str()
|
||||
} else {
|
||||
"5h"
|
||||
"5H"
|
||||
};
|
||||
draw_text_in_rect(hdc, &layout.head_label_rect, head_label_text, DT_CENTER);
|
||||
|
||||
@@ -1655,8 +1674,10 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) {
|
||||
}
|
||||
|
||||
// Tail: weekly countdown aligned with its true remaining-time bar.
|
||||
// Muted color — the percent above is the headline; the countdown is
|
||||
// supporting context and should not compete for visual weight.
|
||||
SelectObject(hdc, main_font);
|
||||
SetTextColor(hdc, COLORREF(text_color.into_colorref()));
|
||||
SetTextColor(hdc, COLORREF(muted_color.into_colorref()));
|
||||
if !inputs.weekly_text.is_empty() {
|
||||
draw_tail_text_in_rect(hdc, &layout.tail_time_text_rect, &inputs.weekly_text, DT_RIGHT);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user