fix(ui): align tail bar text layout

This commit is contained in:
2026-05-23 22:53:36 +07:00
parent 5c2b14fc03
commit f2b31d3211
7 changed files with 265 additions and 29 deletions
Generated
+1 -1
View File
@@ -59,7 +59,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "claude-code-usage-bubble"
version = "0.3.3"
version = "0.3.4"
dependencies = [
"dirs",
"embed-resource",
+1 -1
View File
@@ -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"
@@ -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.
@@ -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.
@@ -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.
@@ -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?
+35 -27
View File
@@ -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(