From c4ad23236e7034904de73664ead7fcb56bcb0b54 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 23 Oct 2026 03:25:28 +0700 Subject: [PATCH] fix(ui): reserve the weekly percent column at its semibold weight The tail percent is drawn at weight 600 but its column was measured at 400, so "100%" was clipped at larger bubble sizes. The layout now asks the measure callback for the weight the text is drawn in. --- tests/js/layout.test.js | 16 ++++++++++++++++ ui/bubble.css | 1 + ui/bubble.js | 4 ++-- ui/layout.js | 11 ++++++++--- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/tests/js/layout.test.js b/tests/js/layout.test.js index 0875536..b8edfeb 100644 --- a/tests/js/layout.test.js +++ b/tests/js/layout.test.js @@ -15,6 +15,7 @@ import { remainingFraction, ringDash, scale, + TAIL_PCT_WEIGHT, tailLayout, timeFillWidth, usageFillWidth, @@ -130,6 +131,21 @@ describe('tailLayout', () => { assert.equal(tail.usageBarRect.r - tail.usageBarRect.l, 8); }); + it('reserves the percent column at the weight it is drawn in', () => { + const regular = 40; + const semibold = 61.3; + /** @type {Array} */ + const pctWeights = []; + const tail = tailLayout({ canvasW: 360, canvasH: 138, dpi: 96 }, (text, _px, weight) => { + if (text === '100%') { + pctWeights.push(weight); + } + return weight === TAIL_PCT_WEIGHT ? semibold : regular; + }); + assert.deepEqual(pctWeights, [600]); + assert.ok(tail.usagePctRect.r - tail.usagePctRect.l >= Math.ceil(semibold)); + }); + it('rounds fractional measurements up', () => { const exact = tailLayout({ canvasW: 360, canvasH: 138, dpi: 96 }, () => 40); const fractional = tailLayout({ canvasW: 360, canvasH: 138, dpi: 96 }, () => 39.2); diff --git a/ui/bubble.css b/ui/bubble.css index 3ea87e5..99ac6f2 100644 --- a/ui/bubble.css +++ b/ui/bubble.css @@ -101,6 +101,7 @@ body { } .tail-pct { + /* The layout reserves this column measured at TAIL_PCT_WEIGHT in layout.js; change both together. */ font-weight: 600; } diff --git a/ui/bubble.js b/ui/bubble.js index 19168c2..8074bf7 100644 --- a/ui/bubble.js +++ b/ui/bubble.js @@ -53,7 +53,7 @@ const DRAG_THRESHOLD_PX = 3; const PULSE_INTERVAL_MS = 80; const TIME_PROGRESS_INTERVAL_MS = 60_000; const FONT_STACK = '"Segoe UI", system-ui, sans-serif'; -/** Weight the layout's reserved text widths are measured at. */ +/** Weight text is measured at when the caller names none (regular). */ const MEASURE_WEIGHT = 400; const HEAD_LABEL_WEIGHT = 600; @@ -212,7 +212,7 @@ function relayout(p) { } const canvas = { canvasW: p.canvasW, canvasH: p.canvasH, dpi: p.dpi }; head = headMetrics(canvas); - tail = tailLayout(canvas, (text, px) => measure(text, px)); + tail = tailLayout(canvas, (text, px, weight) => measure(text, px, weight)); layoutKey = key; rings.setAttribute('viewBox', `0 0 ${p.canvasW} ${p.canvasH}`); diff --git a/ui/layout.js b/ui/layout.js index d646e93..a688377 100644 --- a/ui/layout.js +++ b/ui/layout.js @@ -15,6 +15,9 @@ const COUNTDOWN_TEMPLATE = '999d'; /** Widest percent text the tail reserves room for. */ const PCT_TEMPLATE = '100%'; +/** Font weight of the tail percent; keep in step with `.tail-pct` in bubble.css. */ +export const TAIL_PCT_WEIGHT = 600; + /** Usage at or above this percent pulses. */ export const PULSE_THRESHOLD_PCT = 95; @@ -67,10 +70,11 @@ const PULSE_STRENGTH = 0.3; */ /** - * Measures `text` set at `fontPx` (regular weight) in physical px. + * Measures `text` set at `fontPx` and `weight` in physical px. * @callback MeasureText * @param {string} text Text to measure. * @param {number} fontPx Font size in physical px. + * @param {number} [weight] CSS font weight; regular (400) when omitted. * @returns {number} Width in physical px. */ @@ -174,7 +178,7 @@ export function headMetrics(canvas) { * their right-aligned texts. When the tail is too narrow, the text column * shrinks (possibly to zero) before the bars drop below their minimum. * @param {Canvas} canvas Window canvas. - * @param {MeasureText} measure Regular-weight text measurement. + * @param {MeasureText} measure Text measurement. * @returns {TailLayout} Tail geometry in physical px. */ export function tailLayout(canvas, measure) { @@ -186,7 +190,7 @@ export function tailLayout(canvas, measure) { const barTextGap = scale(8, dpi); const countdownW = Math.ceil(measure(COUNTDOWN_TEMPLATE, fonts.main)); - const pctReserveW = Math.ceil(measure(PCT_TEMPLATE, fonts.small)) + scale(2, dpi); + const pctReserveW = Math.ceil(measure(PCT_TEMPLATE, fonts.small, TAIL_PCT_WEIGHT)) + scale(2, dpi); const usageBarH = clamp(idiv(h * 10, 100), scale(6, dpi), scale(12, dpi)); const timeBarH = clamp(idiv(h * 4, 100), scale(3, dpi), scale(6, dpi)); @@ -341,3 +345,4 @@ export function timeFillWidth(barW, fraction) { export function headLabelText(label, labelW, rectW) { return label === '' || labelW > rectW ? HEAD_LABEL_FALLBACK : label; } +