From e50aa3522aa9b879109194e7d30dd0552be8c2c7 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 23 May 2026 20:32:31 +0700 Subject: [PATCH] feat(bubble): show 5h countdown in head and 7d percent in tail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stadium bubble previously dropped the 5h reset countdown (only the ring + percent were visible in the head) and never showed the 7d percent as a number (only the tail bar fill suggested it). Two more glanceable data points now live on the bubble face without reopening the panel. Head: the small "5h" tag is replaced by the live 5h countdown (e.g. "2h14m"). Falls back to the literal "5h" when no countdown is available yet (cold start) or when the localized string would overflow the rect — DT_NOCLIP would otherwise leak wide CJK glyphs ("4시간 32분") onto the ring stroke at the 140-logical minimum width. Tail: a new "X%" reading sits between the "7d" label and the bar (layout reads "7d 62% ▰▰▰▰▰▱▱▱ 6d4h"). Foreground text color — not the bar accent — because Codex teal #10A37F on the light theme background only hits ~3.2:1 contrast, below WCAG AA for small text; adjacency to the bar carries the visual grouping without hue. The text brightens in sync with the bar fill when weekly_pct >= 95%. compute_bubble_layout reserves room for a "100%"-sized rect between label and bar; if that would push the bar below the 20-logical minimum, the % rect collapses to zero width and the layout falls back to the original label→bar→countdown geometry, so the 140-logical bubble keeps its bar. No new graphics dependencies; tiny-skia + GDI hybrid render path unchanged. session_text plumbing in src/app.rs was already wired but unused in the render — now consumed. cargo check: clean. cargo test: 2/2. cargo clippy: 13 warnings (unchanged baseline). --- src/bubble.rs | 66 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 4 deletions(-) diff --git a/src/bubble.rs b/src/bubble.rs index a945aad..509e484 100644 --- a/src/bubble.rs +++ b/src/bubble.rs @@ -974,6 +974,7 @@ struct BubbleLayout { head_label_rect: RECT, head_pct_rect: RECT, tail_label_rect: RECT, + tail_pct_rect: RECT, tail_bar_rect: RECT, tail_countdown_rect: RECT, big_font_px: i32, @@ -1023,13 +1024,30 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo let countdown_w = measure_text_w(mem_dc, COUNTDOWN_TEMPLATE, main_font_px); let label_w = measure_text_w(mem_dc, "7d", small_font_px); + let pct_reserve_w = measure_text_w(mem_dc, "100%", small_font_px) + scale_to_dpi(2, dpi); let tail_label_left = tail_left + pad; let tail_label_right = tail_label_left + label_w; let tail_countdown_right = tail_right; let tail_countdown_left = tail_countdown_right - countdown_w; - let tail_bar_left = tail_label_right + pad; - let tail_bar_right = (tail_countdown_left - pad).max(tail_bar_left + scale_to_dpi(20, dpi)); + + // Try to seat a 7d% text between the "7d" label and the bar. If that would + // squeeze the bar below the 20-logical minimum, collapse the % rect (zero + // width) and fall back to the original label→bar→countdown layout. This + // keeps the 140-logical bubble legible without dropping the bar. + let bar_min = scale_to_dpi(20, dpi); + let (tail_pct_left, tail_pct_right, tail_bar_left) = { + let pct_left = tail_label_right + pad; + let pct_right = pct_left + pct_reserve_w; + let bar_left = pct_right + pad; + if (tail_countdown_left - pad) - bar_left >= bar_min { + (pct_left, pct_right, bar_left) + } else { + let bar_left = tail_label_right + pad; + (bar_left, bar_left, bar_left) + } + }; + let tail_bar_right = (tail_countdown_left - pad).max(tail_bar_left + bar_min); let tail_bar_h = scale_to_dpi(5, dpi); let tail_bar_top = (height_px - tail_bar_h) / 2; @@ -1050,6 +1068,12 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo right: tail_label_right, bottom: height_px, }, + tail_pct_rect: RECT { + left: tail_pct_left, + top: 0, + right: tail_pct_right, + bottom: height_px, + }, tail_bar_rect: RECT { left: tail_bar_left, top: tail_bar_top, @@ -1443,9 +1467,24 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) { let prev_font = SelectObject(hdc, small_font); - // Head: "5h" label (muted, centered horizontally). + // Head: 5h countdown text if available, otherwise the static "5h" tag. + // The ring already signals "this is the 5h window", so the countdown + // is the more useful glanceable info when we have it. Fall back to + // "5h" when the localized countdown would overflow the rect (e.g., + // wide CJK strings like "999시간" at the 140-logical minimum width) — + // DT_NOCLIP would otherwise leak the glyphs onto the ring stroke. SetTextColor(hdc, COLORREF(muted_color.into_colorref())); - draw_text_in_rect(hdc, &layout.head_label_rect, "5h", DT_CENTER); + 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" + } else if measure_text_w(hdc, &inputs.session_text, layout.small_font_px) + <= head_label_rect_w + { + inputs.session_text.as_str() + } else { + "5h" + }; + draw_text_in_rect(hdc, &layout.head_label_rect, head_label_text, DT_CENTER); // Head: big "X%" glyph centered. SelectObject(hdc, big_font); @@ -1461,6 +1500,25 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) { SetTextColor(hdc, COLORREF(muted_color.into_colorref())); draw_text_in_rect(hdc, &layout.tail_label_rect, "7d", DT_LEFT); + // Tail: 7d percent (foreground color, between label and bar). Skipped + // when the layout collapsed the rect at small widths. Foreground — + // not the accent color the bar uses — because Codex teal #10A37F on + // the light theme background only hits ~3.2:1 contrast, below WCAG + // AA for small text. Adjacency to the bar carries the visual + // grouping; we don't need hue to do it too. + if let Some(pct) = inputs.weekly_pct { + if layout.tail_pct_rect.right > layout.tail_pct_rect.left { + let mut color = text_color; + if pct >= 95.0 { + let t = pulse_triangle(inputs.pulse_phase); + color = brighten(color, t); + } + SetTextColor(hdc, COLORREF(color.into_colorref())); + let weekly_pct_text = format!("{:.0}%", pct); + draw_text_in_rect(hdc, &layout.tail_pct_rect, &weekly_pct_text, DT_CENTER); + } + } + // Tail: countdown (right-aligned). SelectObject(hdc, main_font); SetTextColor(hdc, COLORREF(text_color.into_colorref()));