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:
2026-05-16 11:47:06 +07:00
parent be8efef068
commit 6f5fb3fc80
2 changed files with 31 additions and 22 deletions
+28 -19
View File
@@ -877,7 +877,11 @@ fn check_fullscreen(bubble_hwnd: HWND) {
const ACCENT_STRIPE_W_LOGICAL: i32 = 4;
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 {
/// Bubble width in pixels.
@@ -1175,8 +1179,8 @@ fn paint_bars(pixels: &mut [u32], layout: &BarLayout, inputs: &PaintInputs) {
} else {
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.row2_y, inputs.weekly_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);
}
fn paint_one_bar(
@@ -1185,7 +1189,7 @@ fn paint_one_bar(
top: i32,
pct: Option<f64>,
track: Color,
pulse_phase: u32,
inputs: &PaintInputs,
) {
let bar_w = layout.bar_right - layout.bar_left;
if bar_w <= 0 {
@@ -1204,10 +1208,10 @@ fn paint_one_bar(
if fill_w <= 0 {
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 {
// 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);
}
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
// under the text (fill if covered, track otherwise).
SelectObject(hdc, bold_font);
draw_inline_percent(hdc, layout, layout.row1_y, inputs.session_pct, inputs.is_dark);
draw_inline_percent(hdc, layout, layout.row2_y, inputs.weekly_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.model, inputs.is_dark);
// Countdown on the right.
SelectObject(hdc, main_font);
@@ -1336,6 +1340,7 @@ fn draw_inline_percent(
layout: &BarLayout,
row_top: i32,
pct: Option<f64>,
model: TrayIconKind,
is_dark: bool,
) {
let Some(p) = pct else {
@@ -1355,7 +1360,7 @@ fn draw_inline_percent(
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 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 {
Color::from_hex("#3A3A3A")
} else {
@@ -1406,6 +1411,9 @@ fn draw_countdown(hdc: HDC, layout: &BarLayout, row_top: i32, text: &str) {
if text.is_empty() {
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 len_no_nul = text_w.len().saturating_sub(1);
let mut rect = RECT {
@@ -1419,7 +1427,7 @@ fn draw_countdown(hdc: HDC, layout: &BarLayout, row_top: i32, text: &str) {
hdc,
&mut text_w[..len_no_nul],
&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
/// the visual difference between 75% and 85% is unmistakable at the bubble's
/// 60-90 px bar width.
/// Discrete 4-band fill color. The "safe" band uses the provider's identity
/// color so Codex bars stay white-on-dark while Claude bars stay orange; the
/// 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)
/// - 6080% → amber (#E0A040)
/// - 8095% → red (#C45020)
/// - ≥95% → deep red (#A01818) — paired with pulse animation in render
pub fn ring_color_for_percent(percent: f64) -> Color {
/// - <60% → provider accent (Claude `#D97757` / Codex theme-derived)
/// - 6080% → amber (#E0A040)
/// - 8095% → red (#C45020)
/// - ≥95% → deep red (#A01818) — paired with pulse animation
pub fn bar_fill_color(model: TrayIconKind, is_dark: bool, percent: f64) -> Color {
if percent < 60.0 {
Color::from_hex("#D97757")
accent_color_for(model, is_dark)
} else if percent < 80.0 {
Color::from_hex("#E0A040")
} else if percent < 95.0 {
+3 -3
View File
@@ -269,7 +269,7 @@ fn paint(hwnd: HWND, hdc: HDC) {
} else {
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 {
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 {
crate::bubble::ring_color_for_percent(percent)
fn bar_color_for(model: ProviderId, percent: f64, is_dark: bool) -> Color {
crate::bubble::bar_fill_color(model, is_dark, percent)
}
fn clone_data() -> Option<PanelData> {