From f2b31d321135e8a6c95cd749389b22e754d2e77d Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sat, 23 May 2026 22:53:36 +0700 Subject: [PATCH] fix(ui): align tail bar text layout --- Cargo.lock | 2 +- Cargo.toml | 2 +- .../phase-01-lock-geometry.md | 57 +++++++++++++++++ .../phase-02-apply-renderer-change.md | 57 +++++++++++++++++ .../phase-03-validate-on-windows.md | 64 +++++++++++++++++++ .../plan.md | 50 +++++++++++++++ src/bubble.rs | 62 ++++++++++-------- 7 files changed, 265 insertions(+), 29 deletions(-) create mode 100644 plans/260523-2242-bubble-tail-bar-layout/phase-01-lock-geometry.md create mode 100644 plans/260523-2242-bubble-tail-bar-layout/phase-02-apply-renderer-change.md create mode 100644 plans/260523-2242-bubble-tail-bar-layout/phase-03-validate-on-windows.md create mode 100644 plans/260523-2242-bubble-tail-bar-layout/plan.md diff --git a/Cargo.lock b/Cargo.lock index 5b95c11..07a2195 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,7 +59,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "claude-code-usage-bubble" -version = "0.3.3" +version = "0.3.4" dependencies = [ "dirs", "embed-resource", diff --git a/Cargo.toml b/Cargo.toml index 4359cb5..e2c0e14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "claude-code-usage-bubble" -version = "0.3.3" +version = "0.3.4" edition = "2021" license = "Apache-2.0" description = "Floating bubble showing Claude Code and Codex usage on Windows" diff --git a/plans/260523-2242-bubble-tail-bar-layout/phase-01-lock-geometry.md b/plans/260523-2242-bubble-tail-bar-layout/phase-01-lock-geometry.md new file mode 100644 index 0000000..e9f6828 --- /dev/null +++ b/plans/260523-2242-bubble-tail-bar-layout/phase-01-lock-geometry.md @@ -0,0 +1,57 @@ +--- +phase: 1 +title: "Lock geometry" +status: pending +priority: P1 +effort: "45m" +dependencies: [] +--- + +# Phase 1: Lock geometry + +## Overview +Define the tail layout contract in `compute_bubble_layout` so both rows consume the same horizontal geometry and differ only in vertical placement and bar height. + +## Requirements +- Functional: percent text and countdown text appear after the bar, not inside it. +- Functional: `tail_usage_bar_rect.left/right == tail_time_bar_rect.left/right`. +- Functional: `tail_usage_pct_rect.left/right == tail_time_text_rect.left/right`. +- Non-functional: preserve the 140-360 logical size behavior and the minimum bar-width guard. +- Non-functional: no new renderer inputs from `src/app.rs`. + +## Architecture +- Data flow: `src/app.rs:639-655` -> `bubble::update_data` -> `PaintInputs` -> `compute_bubble_layout` -> `paint_bubble_pixmap` / `paint_bubble_text`. +- Geometry source of truth: shared `text_w`, `text_left`, `bar_left`, and `bar_right` in `src/bubble.rs:1114-1126`. +- Current rect assignment already fans those shared values into both tail rows at `src/bubble.rs:1141-1163`. + +## Related Code Files +- Modify: `src/bubble.rs` +- Read-only check: `src/app.rs` + +## Implementation Steps +1. Re-verify the live mismatch before changing code; the current source already shares bar and text columns. +2. Make the target contract explicit in `compute_bubble_layout`: one shared bar lane, one shared text lane, row-specific `top/bottom` only. +3. Preserve `bar_min` fallback so long countdowns shrink text first, not bar width below usability. +4. Keep bar-height asymmetry unless the user confirms that equal thickness is also required. + +## Todo List +- [ ] Confirm whether the bug is still reproducible on `main`. +- [ ] Document the target geometry near `compute_bubble_layout`. +- [ ] Ensure no later row-specific width override remains. + +## Success Criteria + +- [ ] Both tail bars have identical `left/right` bounds. +- [ ] Both tail texts start at the same `left` and end at the same `right`. +- [ ] No upstream data-contract change is required. + +## Risk Assessment +- High: the source may already satisfy the request; unnecessary edits would add churn. Mitigation: prove the runtime mismatch first. +- Medium: long localized countdown strings can starve bar width at minimum size. Mitigation: keep `bar_min` and shared fallback math. +- Rollback: revert only the `compute_bubble_layout` diff. + +## Security Considerations +- None beyond normal memory-safety review; change is layout-only. + +## Next Steps +- Hand off the shared-geometry contract to Phase 2 for text painting and stale-comment cleanup. diff --git a/plans/260523-2242-bubble-tail-bar-layout/phase-02-apply-renderer-change.md b/plans/260523-2242-bubble-tail-bar-layout/phase-02-apply-renderer-change.md new file mode 100644 index 0000000..114c5c1 --- /dev/null +++ b/plans/260523-2242-bubble-tail-bar-layout/phase-02-apply-renderer-change.md @@ -0,0 +1,57 @@ +--- +phase: 2 +title: "Apply renderer change" +status: pending +priority: P2 +effort: "45m" +dependencies: [1] +--- + +# Phase 2: Apply renderer change + +## Overview +Apply the tail text-placement change in the renderer so the weekly percent lane and weekly remaining-time lane follow the same `bar -> text` behavior, without changing provider or state plumbing. + +## Requirements +- Functional: weekly percent text renders from `tail_usage_pct_rect`; weekly countdown renders from `tail_time_text_rect`. +- Functional: both texts stay right-aligned after the bar using `DT_RIGHT`. +- Functional: no tail text is drawn inside the bar fill. +- Non-functional: preserve the existing pulse behavior for `weekly_pct >= 95`. +- Non-functional: avoid touching `PaintInputs`, polling, or panel code unless a stale comment must be corrected. + +## Architecture +- Bar drawing is tiny-skia-only in `src/bubble.rs:1298-1331`. +- Tail text drawing is a later GDI overlay in `src/bubble.rs:1643-1661`. +- `src/app.rs:636-638` currently describes the bubble percent as inline in the bar fill; if that wording is now false, correct it in the same phase. + +## Related Code Files +- Modify: `src/bubble.rs` +- Optional modify: `src/app.rs` + +## Implementation Steps +1. Align `paint_bubble_text` with the Phase 1 geometry contract and keep the percent/countdown text outside the bars. +2. Remove any remaining inline-percent assumption in comments or naming if it conflicts with the final behavior. +3. Keep the weekly percent highlight behavior and empty-countdown handling intact. +4. Stop scope creep: no changes to provider snapshots, update timers, or panel layout. + +## Todo List +- [ ] Confirm `paint_bubble_text` is the only text-placement site for the tail rows. +- [ ] Update or remove stale inline-bar comments if they become misleading. +- [ ] Re-check placeholder and `None` states after the layout change. + +## Success Criteria + +- [ ] Weekly percent text appears after the top tail bar. +- [ ] Weekly countdown text appears after the bottom tail bar. +- [ ] Tail bar widths are driven only by shared geometry from `compute_bubble_layout`. + +## Risk Assessment +- Medium: if the reported mismatch is only visual perception from unequal bar heights, text-placement edits alone will not fix it. Mitigation: compare runtime screenshots before and after Phase 1. +- Low: optional comment cleanup in `src/app.rs` can drift from renderer reality. Mitigation: change comments only after the final behavior is locked. +- Rollback: revert only the text-placement and comment diffs. + +## Security Considerations +- None; no auth, network, or filesystem behavior changes. + +## Next Steps +- Hand off to Phase 3 for compile checks and Windows visual verification. diff --git a/plans/260523-2242-bubble-tail-bar-layout/phase-03-validate-on-windows.md b/plans/260523-2242-bubble-tail-bar-layout/phase-03-validate-on-windows.md new file mode 100644 index 0000000..b4373fa --- /dev/null +++ b/plans/260523-2242-bubble-tail-bar-layout/phase-03-validate-on-windows.md @@ -0,0 +1,64 @@ +--- +phase: 3 +title: "Validate on Windows" +status: pending +priority: P2 +effort: "30m" +dependencies: [2] +--- + +# Phase 3: Validate on Windows + +## Overview +Verify that the scoped renderer change compiles and that the native layered-window bubble actually presents equal-width tail bars with text after each bar across common runtime conditions. + +## Requirements +- Functional: both tail rows visually render as `bar -> text`. +- Functional: both tail bars have the same visible width. +- Non-functional: confirm no regression to head ring, head text, or tray/panel refresh behavior. +- Non-functional: validation stays command-light and uses the existing Windows runtime. + +## Architecture +- Compile-time validation covers the Rust renderer path end to end. +- Runtime validation must observe the real layered window because there are no snapshot/golden tests for `tiny-skia + GDI` composition in this repo. + +## Related Code Files +- Verify: `src/bubble.rs` +- Verify if touched: `src/app.rs` + +## Implementation Steps +1. Run the compile/test commands below. +2. Launch the app and verify the bubble at 140, default, and max logical sizes. +3. Check light and dark theme, Claude and Codex bubbles, and a long countdown string if available. +4. Capture before/after notes so a no-op or perception-only result is explicit. + +## Validation Commands +```powershell +cargo check +cargo test +cargo run +``` + +## Todo List +- [ ] `cargo check` passes. +- [ ] `cargo test` passes, or any pre-existing failures are called out separately. +- [ ] Manual runtime check confirms equal bar widths and text-after-bar alignment. +- [ ] No regression is seen in the head ring/text or bubble refresh path. + +## Success Criteria + +- [ ] Compile succeeds on the current branch. +- [ ] The top and bottom tail bars share the same left/right edges at runtime. +- [ ] The percent and countdown texts both sit to the right of their bars at runtime. +- [ ] Any remaining mismatch is explained with evidence, not assumption. + +## Risk Assessment +- High: native renderer issues are hard to prove without manual observation. Mitigation: test at minimum/default/maximum sizes and common DPI settings. +- Medium: reproducing the original complaint may require a specific locale, DPI, or stale binary. Mitigation: record the runtime conditions used during verification. +- Rollback: revert the renderer change if compile or visual regression appears. + +## Security Considerations +- None. + +## Next Steps +- If validation passes, implementation can be approved as a scoped `src/bubble.rs` change. If not, reopen Phase 1 with the observed runtime evidence. diff --git a/plans/260523-2242-bubble-tail-bar-layout/plan.md b/plans/260523-2242-bubble-tail-bar-layout/plan.md new file mode 100644 index 0000000..941539b --- /dev/null +++ b/plans/260523-2242-bubble-tail-bar-layout/plan.md @@ -0,0 +1,50 @@ +--- +title: "Bubble tail bar layout alignment" +description: "Scoped renderer-only plan to align weekly percent and remaining-time tail bar geometry." +status: pending +priority: P2 +effort: 2h +branch: "main" +tags: [rust, renderer, bubble, layout] +blockedBy: [] +blocks: [] +created: 2026-05-23 +createdBy: "ck:plan" +source: skill +--- + +# Bubble tail bar layout alignment + +## Scope +- User-facing goal: weekly percent lane and weekly remaining-time lane both render as `bar -> text`, and both bars share identical left/right bounds. +- Expected code scope: `src/bubble.rs`; touch `src/app.rs` only if comment cleanup is needed. +- Backwards compatibility: no settings, storage, IPC, or provider-data changes. + +## Verified Codebase Facts +- Bubble data already provides `weekly_pct`, `weekly_text`, and `weekly_resets_at` through `bubble::update_data`; no new inputs are needed (`src/app.rs:639-655`). +- `compute_bubble_layout` already derives one shared text column and one shared bar lane for the two tail rows (`src/bubble.rs:1114-1163`). +- `paint_bubble_text` already renders weekly percent and weekly countdown as separate right-aligned texts (`src/bubble.rs:1644-1661`). +- `paint_bubble_pixmap` paints both tail bars from rects only; text is a GDI overlay, so geometry must stay the single source of truth (`src/bubble.rs:1298-1331`). + +## Phases + +| Phase | Name | Status | +|-------|------|--------| +| 1 | [Lock geometry](./phase-01-lock-geometry.md) | Pending | +| 2 | [Apply renderer change](./phase-02-apply-renderer-change.md) | Pending | +| 3 | [Validate on Windows](./phase-03-validate-on-windows.md) | Pending | + +## Dependencies + +- Sequence: Phase 1 -> Phase 2 -> Phase 3. +- File ownership: `src/bubble.rs` stays single-owner across phases; optional `src/app.rs` comment cleanup happens only in Phase 2. +- Related existing plan: `plans/260523-ui-ux-improvement-plan/plan.md` Phase 3 overlaps in theme but does not block this scoped renderer-only change. + +## Rollback +- Revert layout math and text-placement changes in `src/bubble.rs`. +- Revert optional comment cleanup in `src/app.rs`. +- No data migration or persisted-state rollback is needed. + +## Unresolved Questions +- Does "same length" mean equal width only, or should the two tail bars also share the same height? Current code intentionally uses different heights (`src/bubble.rs:1105-1110`). +- The current source already looks close to the requested behavior. If runtime still differs, is the issue in this branch, a stale binary, or perception caused by different bar heights? diff --git a/src/bubble.rs b/src/bubble.rs index 8619a70..a7cdb35 100644 --- a/src/bubble.rs +++ b/src/bubble.rs @@ -1114,25 +1114,16 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo let content_left = tail_left + pad; let content_right = tail_right; let content_w = (content_right - content_left).max(0); - let time_text_w = countdown_w.min(content_w); - let time_text_left = content_right - time_text_w; - let time_bar_min = scale_to_dpi(8, dpi); - let time_bar_right = (time_text_left - pad).max(content_left + time_bar_min); - let time_bar_left = content_left.min(time_bar_right); - - let usage_bar_min = scale_to_dpi(8, dpi); - let show_usage_pct = content_w >= pct_reserve_w + pad + usage_bar_min; - let usage_pct_right = if show_usage_pct { - content_left + pct_reserve_w + 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 { + desired_text_w } else { - content_left + (content_w - pad - bar_min).max(0) }; - let usage_bar_left = if show_usage_pct { - usage_pct_right + pad - } else { - content_left - }; - let usage_bar_right = content_right.max(usage_bar_left + usage_bar_min); + let text_left = content_right - text_w; + let bar_left = content_left; + let bar_right = (text_left - pad).max(bar_left + bar_min); BubbleLayout { canvas_w: width_px, @@ -1148,27 +1139,27 @@ fn compute_bubble_layout(size_logical: i32, dpi: u32, mem_dc: HDC) -> BubbleLayo head_label_rect, head_pct_rect, tail_usage_pct_rect: RECT { - left: content_left, + left: text_left, top: usage_bar_top + (usage_bar_h - usage_pct_h) / 2, - right: usage_pct_right, + right: content_right, bottom: usage_bar_top + (usage_bar_h - usage_pct_h) / 2 + usage_pct_h, }, tail_usage_bar_rect: RECT { - left: usage_bar_left, + left: bar_left, top: usage_bar_top, - right: usage_bar_right, + right: bar_right, bottom: usage_bar_top + usage_bar_h, }, tail_time_text_rect: RECT { - left: time_text_left, + left: text_left, top: time_bar_top + (time_bar_h - time_text_h) / 2, right: content_right, bottom: time_bar_top + (time_bar_h - time_text_h) / 2 + time_text_h, }, tail_time_bar_rect: RECT { - left: time_bar_left, + left: bar_left, top: time_bar_top, - right: time_bar_right, + right: bar_right, bottom: time_bar_top + time_bar_h, }, big_font_px, @@ -1643,7 +1634,7 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) { }; draw_text_in_rect(hdc, &layout.head_pct_rect, &pct_text, DT_CENTER); - // Tail: weekly percent (foreground color, aligned with its usage bar). Skipped + // Tail: weekly percent (foreground color, right of its usage 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 @@ -1659,7 +1650,7 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) { } SetTextColor(hdc, COLORREF(color.into_colorref())); let weekly_pct_text = format!("{:.0}%", pct); - draw_text_in_rect(hdc, &layout.tail_usage_pct_rect, &weekly_pct_text, DT_CENTER); + draw_tail_text_in_rect(hdc, &layout.tail_usage_pct_rect, &weekly_pct_text, DT_RIGHT); } } @@ -1667,7 +1658,7 @@ fn paint_bubble_text(hdc: HDC, layout: &BubbleLayout, inputs: &PaintInputs) { SelectObject(hdc, main_font); SetTextColor(hdc, COLORREF(text_color.into_colorref())); if !inputs.weekly_text.is_empty() { - draw_text_in_rect(hdc, &layout.tail_time_text_rect, &inputs.weekly_text, DT_RIGHT); + draw_tail_text_in_rect(hdc, &layout.tail_time_text_rect, &inputs.weekly_text, DT_RIGHT); } SelectObject(hdc, prev_font); @@ -1694,6 +1685,23 @@ fn draw_text_in_rect(hdc: HDC, rect: &RECT, text: &str, halign: DRAW_TEXT_FORMAT } } +fn draw_tail_text_in_rect(hdc: HDC, rect: &RECT, text: &str, halign: DRAW_TEXT_FORMAT) { + if rect.right <= rect.left { + return; + } + let mut buf = wide_str(text); + let len_no_nul = buf.len().saturating_sub(1); + let mut r = *rect; + unsafe { + let _ = DrawTextW( + hdc, + &mut buf[..len_no_nul], + &mut r, + halign | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS, + ); + } +} + fn create_font(height_px: i32, name_w: &[u16], weight: i32) -> HFONT { unsafe { CreateFontW(