- Lift inner time ring above WCAG 1.4.11 (track #303030 -> #404040, stroke floor 1 -> 2 logical, ring gap 3 -> 4). - Breathe tail bar/text with bar_text_gap=8 (was pad=6); right inset 12 -> 14 logical so text clears the stadium end-cap. - Reweight typography: head percent FW_BOLD, "5H" tag and tail percent FW_SEMIBOLD, tail countdown stays normal but takes muted color so the percent reads as the headline. - Tone down usage track (#3A3A3A/#D6D6D6 -> #2C2C2C/#E2E2E2) so fill dominates at low percentages. - Differentiate lane mass: usage bar 9%/5..12 -> 10%/6..12, time bar 5%/3..7 -> 4%/3..6, lane gap 5 -> 6. Time bar now reads as supporting context, not a competing quota. - Min-fill guard on weekly bar: sub-cap fills floor at one cap-diameter so 1% renders as a recognizable dot. - head_pad 4 -> 5; big-font ratio 26% -> 24% of head diameter (BOLD compensates for the size cut).
10 KiB
UI Rendering Code Review
Scope: src/bubble.rs (renderer), src/usage_color.rs, src/os/color.rs,
drawing portions of src/panel.rs, src/tray/badge.rs.
Findings
-
src/bubble.rs:1178-1197— severity: high. Eight neutral surface colours (#1F1F1F,#F3F3F3,#3A3A3A,#D6D6D6,#303030,#E0E0E0,#9A9A9A,#777777) are hex literals insidepaint_bubble_pixmapand duplicated near-verbatim inpanel.rs:279-293(#1F1F1F,#FAFAFA,#EAEAEA,#3A3A3A,#D6D6D6) plusbubble.rs:1590-1597text colours. Three surfaces silently disagree (#F3F3F3bubble bg vs#FAFAFApanel bg) and a designer cannot retune the palette without grepping. Fix: centralise into apalettemodule alongsideusage_color.rsexposingbg(is_dark),track(is_dark),time_track(is_dark),time_fill(is_dark),text(is_dark),muted(is_dark); havepanel.rsconsume the same helpers. -
src/tray/badge.rs:53, 112, 114— severity: high. Badge colours go throughpaint.set_color_rgba8(0x3a, 0x3a, 0x3a, 255)and raw[u8; 3]arrays instead ofRgb/os::color.#3A3A3Aalready exists as the shared "track" colour inbubble.rs:1184, but the tray hard-codes it. The two Claude/Codex base tints (#2A1F1C,#1A1F26) also live only here. Fix: route through the same palette module, even if the badge keeps its own dark inner-disk variants (named constants beat magic byte arrays). -
src/bubble.rs:1064-1117— severity: high. Padding/gap literalsscale_to_dpi(2|4|5|6|8|12, dpi)appear 15+ times insidecompute_bubble_layoutwith no naming. Same logical "edge padding" (scale_to_dpi(4, dpi)) is used forhead_pad, head-label left/right, head-pct left/right (lines 1064, 1086, 1088, 1092, 1094); same "small nudge" (scale_to_dpi(2, dpi)) is the ring stroke clamp, label/pct row vertical breathing room, pct-reserve gap, and time-text padding. A designer tweaking head-text padding will touch four lines and miss the fifth. Fix: hoist named DPI-scaled constants at the top of the function (HEAD_PAD,TEXT_VPAD,LANE_GAP,TAIL_PAD,RIGHT_INSET,BAR_MIN_W) and reuse — same patternpanel.rsuses with its*_LOGICALconstants (line 24-30). -
src/panel.rs:333-340— severity: med. Bar-x / bar-w / row-y math mixesscaled(PADDING_LOGICAL),scaled(LABEL_W_LOGICAL),scaled(RIGHT_TEXT_W_LOGICAL)with barescaled(4),scaled(8),scaled(24),scaled(18)— four un-named "small" values doing semantically distinct jobs (label-bar gap, bar-text gap, header height, row-1 offset). Fix: name them (LABEL_BAR_GAP_LOGICAL,BAR_TEXT_GAP_LOGICAL,HEADER_H_LOGICAL,HEADER_OFFSET_LOGICAL) so the row geometry is auditable in one place. -
src/panel.rs:340— severity: med. `row2_y = row1_y + scale_to_dpi(BAR_HEIGHT_LOGICAL, dpi) + scale_to_dpi(ROW_GAP_LOGICAL, dpi)- scaled(8)
. The trailing+ scaled(8)is an unexplained extra gap on top ofROW_GAP_LOGICAL; this is exactly the inconsistencyROW_GAP_LOGICALwas created to prevent. Fix: fold intoROW_GAP_LOGICAL(16) or rename the extra into a labelledROW_TEXT_GAP_LOGICAL`.
- scaled(8)
-
src/bubble.rs:1099, 1126— severity: med.tail_right = width_px - scale_to_dpi(12, dpi)andbar_right = (text_left - pad).max(bar_left + bar_min). The12is the right-edge inset to clear the stadium's right end-cap; this is conceptuallycorner_radius / 2-ish but encoded as a constant that won't track if aspect ratio changes. Fix: derive fromlayout.corner_radiusor hoist aTAIL_RIGHT_INSETconstant with a comment tying it to the end-cap curvature. -
src/bubble.rs:1360-1377andsrc/tray/badge.rs:86-106— severity: med.build_arcis duplicated verbatim between the bubble renderer and the tray badge — same 64-segment sampling, sameFRAC_PI_2start, same edge-case.max(1)segment count. Fix: lift into a smallgeometry/tiny_skia_helpersmodule shared by both call sites; change neither call site to keep behaviour identical. -
src/bubble.rs:1339-1352— severity: med.paint_pillis a perfect helper candidate forpanel.rs's bar drawing —panel.rsusesFillRectrectangles with hard corners (draw_rowlines 406-428), visually inconsistent with the bubble's rounded pill caps. Fix: extractpaint_pillto a shared rendering helper module and have panel use it so the two surfaces have matching bar geometry. (Cross-surface consistency was the stated reason forusage_color.rsexisting — same logic applies to bar shape.) -
src/bubble.rs:1080-1083, 1111-1112— severity: med.head_label_h,head_pct_h,time_text_h,usage_pct_hall addscale_to_dpi(2, dpi)of "breathing room" to a font height, but never call it that — and the computed rect height is then used byDrawTextWwithDT_VCENTERso a too-tight value would clip ascenders/descenders. Currently safe because 2 px (logical) ≈ font leading, but the magic2is load-bearing. Fix:const FONT_VPAD_LOGICAL: i32 = 2;with a one-line comment "ascender/ descender slack for DT_VCENTER". -
src/bubble.rs:1141-1146, 1153-1158— severity: low. The vertical centring expressionusage_bar_top + (usage_bar_h - usage_pct_h) / 2is computed twice fortail_usage_pct_rect(top + bottom). Tiny but if a designer asks "where does the % text sit relative to the bar?" they have to mentally simplify. Fix: computepct_text_top/time_text_topas named locals before the struct literal. -
src/bubble.rs:1076, 1077— severity: low.big_font_px = head_diameter * 26 / 100;small_font_px = big_font_px * 55 / 100. The 26 % and 55 % ratios are the core typographic scale of the head text — promote toBIG_FONT_RATIO_PCT,SMALL_TO_BIG_FONT_PCTconstants with a "tweak these to retune head proportions" comment. -
src/bubble.rs:1078— severity: low (dead-code adjacent).main_font_px = small_font_px;—main_font_pxis identical tosmall_font_pxbut kept as a separate field onBubbleLayout(line 1056) and used for the countdown (line 1604, 1658). If the intent is "may diverge in future", document it; otherwise drop the duplicate field and usesmall_font_pxdirectly. -
src/bubble.rs:1085-1096— severity: low.head_label_rectandhead_pct_rectboth useleft: scale_to_dpi(4, dpi)andright: head_diameter - scale_to_dpi(4, dpi)— identical horizontal extents. Could share a singlehead_text_left/head_text_rightpair to make "head text is centered in the head circle" structurally visible. -
src/bubble.rs:1216 vs 1339-1352— severity: low. Stadium background uses inline two-circle-plus-rect path; the pill helper does the same shape. The stadium could callpaint_pill(pixmap, 0.0, 0.0, w, h, h/2.0, bg)and shed ~15 lines. Worth doing oncepaint_pillmoves to a shared module (finding 8). -
src/bubble.rs:1653— severity: low (text layout).draw_tail_text_in_rectis called withDT_RIGHT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS. TheDT_END_ELLIPSISon a right-aligned 3-char string ("100%") inside a tight rect will produce1…if the rect collapses by even a pixel — fine, but worth confirming thepct_reserve_w(line 1103) leaves a 1-px AA safety margin. Current+ scale_to_dpi(2, dpi)looks adequate. No fix needed; flag for future locale changes. -
src/bubble.rs:1674-1685— severity: low.draw_text_in_rectalways usesDT_NOCLIP;draw_tail_text_in_rectusesDT_END_ELLIPSIS(noDT_NOCLIP). The two helpers diverge silently on whether text may escape its rect. Document the contract on each helper ("head text trusts layout, tail text fits-or-ellipsises"). -
src/panel.rs:447-501— severity: low.draw_textcreates and destroys a font on every call (4× perpaint). Same anti-pattern inbubble.rs:paint_bubble_textis amortised by cachingbig_font,small_font,main_fontfor the whole paint. Not a correctness issue but means the panel allocates 4 GDI fonts on every InvalidateRect. Cache by(size, bold)keyed on the HDC.
Quick wins
-
Centralise the neutral palette (findings 1, 2). One new module
palette.rsexposingbg / track / time_track / time_fill / text / mutedplus tray-specific tints. Replace allColor::from_hex(...)calls inbubble.rs:1178-1197,bubble.rs:1589-1598,panel.rs:279-293, and the byte arrays intray/badge.rs. ~20-line diff, kills cross-surface drift. -
Name the padding constants in
compute_bubble_layout(finding 3, 9). Add ~6constdeclarations at the top of the function — keeps them locally scoped, matchespanel.rsstyle. Designer can retune metrics from one block. -
Lift
build_arcto a sharedtiny_skia_helpersmodule (finding 7). Two-file delete-and-import. Identical behaviour. -
Promote
paint_pilland reuse inpanel.rsrows + stadium bg (findings 8, 14). Makes bar visual style consistent between bubble and panel; bonus simplification of the stadium fill. -
Drop / rename
main_font_px(finding 12). Either delete the field and usesmall_font_pxdirectly, or split the constants (MAIN_FONT_RATIO) so future divergence is intentional.
Defer
- Finding 4-6 (panel/bubble padding naming, derived
TAIL_RIGHT_INSET): worth doing alongside the palette refactor but not blocking. - Finding 10, 13 (local temporaries for centring math, shared
head_text_*extents): pure readability, low payoff. - Finding 15 (ellipsis safety margin on locale changes): keep an eye on
this when adding a locale wider than
999시간. - Finding 17 (panel GDI font caching): only matters if the panel starts refreshing more often than once per poll cycle.
Out-of-scope sighting (one-line flag as instructed): compute_bubble_layout
takes an HDC purely to measure text — couples geometry calc to a live GDI
device. Not a render bug, but it makes the function untestable without a
window and is worth refactoring when convenient.
Status: DONE Summary: Renderer is functionally solid; the dominant problem is duplicated/un-named geometry and colour literals scattered across bubble / panel / badge that drift independently and resist designer-led tuning.