fix(bubble): provider-tinted bar fill, tighten countdown gap

Two visible regressions from the previous pass:

1. Codex bars still rendered orange even though the accent stripe was
   white — `ring_color_for_percent` only knew the percentage, not the
   provider, so it returned the Claude-style orange/amber/red ramp for
   both models. Replaced with `bar_fill_color(model, is_dark, pct)`:

   - <60%   → accent_color_for(model, is_dark)  (Claude orange / Codex
              white-on-dark / Codex charcoal-on-light)
   - 60-80% → amber  (#E0A040)
   - 80-95% → red    (#C45020)
   - ≥95%   → deep red (#A01818)

   Alarm bands stay model-agnostic so an approaching limit always looks
   the same. The "safe" band carries the provider's identity. Mirrored
   into the panel via `bar_color_for(model, percent, is_dark)`.

2. Big visible gap between bar end and countdown text — the column
   was sized for the old combined "100% · 23h" probe string (~9 chars)
   but the bubble now renders countdown-only (~3 chars). Probe template
   shrunk to "999d" so the right column is no longer ~70 px reserved
   for ~14 px of text. Countdown also flipped from DT_RIGHT to DT_LEFT
   so "3h" / "19h" sit immediately next to the bar with just the
   `label_pad` gap, instead of floating at the bubble's far edge.

Inline-percent contrast calc uses `bar_fill_color` so the
luminance-based foreground colour responds to the new white/charcoal
Codex bars (black-on-white in safe band, white-on-amber/red in alarm).

Side effect: `ring_color_for_percent` is gone — it had no internal
callers outside the two replaced sites. `bar_fill_color` is now the
only public bar-color helper.
This commit is contained in:
tiennm99 committed 2026-05-16 11:47:06 +07:00
1 parent be8efef068
commit 6f5fb3fc80
2 files changed
+31 -22

No files matched your search

+28 -19
View File
@@ -877,7 +877,11 @@ fn check_fullscreen(bubble_hwnd: HWND) {
const ACCENT_STRIPE_W_LOGICAL: i32 = 4; const ACCENT_STRIPE_W_LOGICAL: i32 = 4;
const LABEL_PAD_LOGICAL: i32 = 6; const LABEL_PAD_LOGICAL: i32 = 6;
const COUNTDOWN_TEMPLATE: &str = "100% \u{00b7} 23h"; // Worst-case width-probe for the right-side countdown column. The bubble
// renders countdown-only (percent moved inline), so this is just "N{suffix}"
// for the longest reasonable duration. Bumped from "100% · 23h" which had
// been sized for the old combined string and left a big empty gap.
const COUNTDOWN_TEMPLATE: &str = "999d";
struct BarLayout { struct BarLayout {
/// Bubble width in pixels. /// Bubble width in pixels.
@@ -1175,8 +1179,8 @@ fn paint_bars(pixels: &mut [u32], layout: &BarLayout, inputs: &PaintInputs) {
} else { } else {
Color::from_hex("#D6D6D6") Color::from_hex("#D6D6D6")
}; };
paint_one_bar(pixels, layout, layout.row1_y, inputs.session_pct, track, inputs.pulse_phase); paint_one_bar(pixels, layout, layout.row1_y, inputs.session_pct, track, inputs);
paint_one_bar(pixels, layout, layout.row2_y, inputs.weekly_pct, track, inputs.pulse_phase); paint_one_bar(pixels, layout, layout.row2_y, inputs.weekly_pct, track, inputs);
} }
fn paint_one_bar( fn paint_one_bar(
@@ -1185,7 +1189,7 @@ fn paint_one_bar(
top: i32, top: i32,
pct: Option<f64>, pct: Option<f64>,
track: Color, track: Color,
pulse_phase: u32, inputs: &PaintInputs,
) { ) {
let bar_w = layout.bar_right - layout.bar_left; let bar_w = layout.bar_right - layout.bar_left;
if bar_w <= 0 { if bar_w <= 0 {
@@ -1204,10 +1208,10 @@ fn paint_one_bar(
if fill_w <= 0 { if fill_w <= 0 {
return; return;
} }
let mut accent_rgb = ring_color_for_percent(p); let mut accent_rgb = bar_fill_color(inputs.model, inputs.is_dark, p);
if p >= 95.0 { if p >= 95.0 {
// Slow brightness triangle: 0.85 → 1.15 over 24 ticks (≈1.9s @ 80ms). // Slow brightness triangle: 0.85 → 1.15 over 24 ticks (≈1.9s @ 80ms).
let t = pulse_triangle(pulse_phase); let t = pulse_triangle(inputs.pulse_phase);
accent_rgb = brighten(accent_rgb, t); accent_rgb = brighten(accent_rgb, t);
} }
let accent_packed = rgb_to_dib(accent_rgb); let accent_packed = rgb_to_dib(accent_rgb);
@@ -1276,8 +1280,8 @@ fn paint_text_layer(hdc: HDC, layout: &BarLayout, inputs: &PaintInputs) {
// Inline percent: drawn over the bar, contrast picked from the pixel // Inline percent: drawn over the bar, contrast picked from the pixel
// under the text (fill if covered, track otherwise). // under the text (fill if covered, track otherwise).
SelectObject(hdc, bold_font); SelectObject(hdc, bold_font);
draw_inline_percent(hdc, layout, layout.row1_y, inputs.session_pct, inputs.is_dark); draw_inline_percent(hdc, layout, layout.row1_y, inputs.session_pct, inputs.model, inputs.is_dark);
draw_inline_percent(hdc, layout, layout.row2_y, inputs.weekly_pct, inputs.is_dark); draw_inline_percent(hdc, layout, layout.row2_y, inputs.weekly_pct, inputs.model, inputs.is_dark);
// Countdown on the right. // Countdown on the right.
SelectObject(hdc, main_font); SelectObject(hdc, main_font);
@@ -1336,6 +1340,7 @@ fn draw_inline_percent(
layout: &BarLayout, layout: &BarLayout,
row_top: i32, row_top: i32,
pct: Option<f64>, pct: Option<f64>,
model: TrayIconKind,
is_dark: bool, is_dark: bool,
) { ) {
let Some(p) = pct else { let Some(p) = pct else {
@@ -1355,7 +1360,7 @@ fn draw_inline_percent(
let bar_w = layout.bar_right - layout.bar_left; let bar_w = layout.bar_right - layout.bar_left;
let fill_w = ((p.clamp(0.0, 100.0) / 100.0) * bar_w as f64).round() as i32; let fill_w = ((p.clamp(0.0, 100.0) / 100.0) * bar_w as f64).round() as i32;
let inset = (layout.bar_h / 4).max(2); let inset = (layout.bar_h / 4).max(2);
let fill_color = ring_color_for_percent(p); let fill_color = bar_fill_color(model, is_dark, p);
let track_color = if is_dark { let track_color = if is_dark {
Color::from_hex("#3A3A3A") Color::from_hex("#3A3A3A")
} else { } else {
@@ -1406,6 +1411,9 @@ fn draw_countdown(hdc: HDC, layout: &BarLayout, row_top: i32, text: &str) {
if text.is_empty() { if text.is_empty() {
return; return;
} }
// Left-align so the countdown sits right next to the bar with only the
// `label_pad` gap. Right-aligning to the bubble's far edge left a visible
// float between bar end and number.
let mut text_w = wide_str(text); let mut text_w = wide_str(text);
let len_no_nul = text_w.len().saturating_sub(1); let len_no_nul = text_w.len().saturating_sub(1);
let mut rect = RECT { let mut rect = RECT {
@@ -1419,7 +1427,7 @@ fn draw_countdown(hdc: HDC, layout: &BarLayout, row_top: i32, text: &str) {
hdc, hdc,
&mut text_w[..len_no_nul], &mut text_w[..len_no_nul],
&mut rect, &mut rect,
DT_RIGHT | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP | DT_END_ELLIPSIS, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_NOCLIP | DT_END_ELLIPSIS,
); );
} }
} }
@@ -1437,17 +1445,18 @@ fn apply_alpha_mask(pixels: &mut [u32], layout: &BarLayout) {
} }
} }
/// Discrete 4-band color "cliff" — sharper steps than a smooth gradient so /// Discrete 4-band fill color. The "safe" band uses the provider's identity
/// the visual difference between 75% and 85% is unmistakable at the bubble's /// color so Codex bars stay white-on-dark while Claude bars stay orange; the
/// 60-90 px bar width. /// warning bands are the same alarm palette regardless of provider so an
/// approaching-limit always looks the same to the eye.
/// ///
/// - <60% → Claude orange (#D97757) /// - <60% → provider accent (Claude `#D97757` / Codex theme-derived)
/// - 60–80% → amber (#E0A040) /// - 60–80% → amber (#E0A040)
/// - 80–95% → red (#C45020) /// - 80–95% → red (#C45020)
/// - ≥95% → deep red (#A01818) — paired with pulse animation in render /// - ≥95% → deep red (#A01818) — paired with pulse animation
pub fn ring_color_for_percent(percent: f64) -> Color { pub fn bar_fill_color(model: TrayIconKind, is_dark: bool, percent: f64) -> Color {
if percent < 60.0 { if percent < 60.0 {
Color::from_hex("#D97757") accent_color_for(model, is_dark)
} else if percent < 80.0 { } else if percent < 80.0 {
Color::from_hex("#E0A040") Color::from_hex("#E0A040")
} else if percent < 95.0 { } else if percent < 95.0 {
+3 -3
View File
@@ -269,7 +269,7 @@ fn paint(hwnd: HWND, hdc: HDC) {
} else { } else {
Color::from_hex("#D6D6D6") Color::from_hex("#D6D6D6")
}; };
let accent = bar_color_for(data.session_pct.max(data.weekly_pct), data.is_dark); let accent = bar_color_for(data.model, data.session_pct.max(data.weekly_pct), data.is_dark);
unsafe { unsafe {
let bg_brush = CreateSolidBrush(COLORREF(bg.into_colorref())); let bg_brush = CreateSolidBrush(COLORREF(bg.into_colorref()));
@@ -482,8 +482,8 @@ fn draw_text(
} }
} }
fn bar_color_for(percent: f64, _is_dark: bool) -> Color { fn bar_color_for(model: ProviderId, percent: f64, is_dark: bool) -> Color {
crate::bubble::ring_color_for_percent(percent) crate::bubble::bar_fill_color(model, is_dark, percent)
} }
fn clone_data() -> Option<PanelData> { fn clone_data() -> Option<PanelData> {