Skip to content

SetMetricScale: an audit that finds the widgets ignoring it, and the first fixes - #208

Merged
tannevaled merged 1 commit into
mainfrom
feat/hidpi-metric-audit
Aug 15, 2026
Merged

SetMetricScale: an audit that finds the widgets ignoring it, and the first fixes#208
tannevaled merged 1 commit into
mainfrom
feat/hidpi-metric-audit

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

SetMetricScale is documented as the thing a HiDPI host sets once so that "the whole toolkit lays out and paints crisp at that resolution". Ten files out of a hundred and sixty route anything through it. The rest carry raw pixel constants that stay put on a screen with twice the pixels, so the chrome shrinks to half its intended size while the window around it is sharp — and nothing anywhere says so.

Now that all four go-widgets/window back-ends can hand a widget tree a 2× framebuffer, this is the half that was missing.

The instrument

TestMetricScaleAudit finds them, instead of my reading a hundred files and guessing which numbers are pixels. It draws a widget at scale 1 and at scale 2 and compares the run lengths along three rows and three columns: a widget whose metrics scale gives the same runs, twice as wide.

Three things it had to learn, each from being wrong:

  • Widths, not boundary positions. Positions move by one pixel when a one-pixel border fails to scale, which is indistinguishable from the rounding of an odd metric — that version declared the very defect this was written to find to be within tolerance.
  • Runs no wider than one logical pixel are dropped. An anti-aliased edge is a coverage ramp, not a metric, and at twice the scale it may resolve into two steps where it had one. In logical units, because a border that does scale is one pixel at 1× and two at 2×.
  • Three lines per axis, reported only when all three disagree. A metric that does not scale is wrong on every line; a diagonal crossing an exact scanline is not, and calling that a defect would have me contorting a widget to satisfy a probe.

It measures chrome, not text: type carries its own scale and a centred baseline rounds. Whether text should follow the metric scale is a real question and a separate one.

The fixes, over eighteen widgets

  • strokeRect / strokeRoundRect ask for a stroke one logical pixel wide. This one line fixes Button, Card, ProgressBar, Chip, Entry, Alert, Banner, Frame and every other framed widget at once. It needed painter#17 (v0.11.0), which until now took a stroke width and discarded it.
  • CheckButton: the box, the label gap, and the checkmark — a hard-coded twelve-pixel path that sat in the corner of a box twice its size. The classic 12px tick is kept byte-for-byte for the classic 12px box.
  • Switch: the knob inset. Expander/Accordion: the header height, through a new ExpanderHeaderHeight(). Gauge: the arc thickness.

Control

audit
with the fixes green, 18 widgets
revert the stroke width 5 of 6 original widgets flagged
revert only the checkbox scaling exactly CheckButton flagged

The catalogue is eighteen widgets of roughly a hundred and forty. Growing it is the mechanism for the rest: every widget added either passes or names its own defect.

🤖 Generated with Claude Code

…first fixes

The knob is documented as the thing a HiDPI host sets once so "the whole toolkit
lays out and paints crisp at that resolution". Ten files out of a hundred and
sixty route anything through it. The rest carry raw pixel constants that stay
put on a screen with twice the pixels, so the chrome shrinks to half its
intended size while the window around it is sharp -- and nothing says so.

TestMetricScaleAudit finds them instead of my reading a hundred files and
guessing which numbers are pixels. It draws a widget at scale 1 and at scale 2
and compares the RUN LENGTHS along three rows and three columns: a widget whose
metrics scale gives the same runs, twice as wide.

Three things that instrument had to learn, each from being wrong:

  - Widths, not boundary positions. Positions move by one pixel when a
    one-pixel border fails to scale, which is indistinguishable from the
    rounding of an odd metric -- that version declared the very defect this was
    written to find to be within tolerance.
  - Runs no wider than one LOGICAL pixel are dropped: an anti-aliased edge is a
    coverage ramp, not a metric, and at twice the scale it may resolve into two
    steps where it had one. In logical units, because a border that DOES scale
    is one pixel at 1x and two at 2x.
  - Three lines per axis, reported only when all three disagree. A metric that
    does not scale is wrong on every line; a diagonal crossing an exact scanline
    is not, and calling that a defect would have me contorting a widget to
    satisfy a probe.

It measures chrome, not text: type carries its own scale and a centred baseline
rounds, so a scanline landing one glyph row off reports arithmetic. Whether text
should follow the metric scale is a real question and a separate one.

The fixes it produced, over eighteen widgets:

  - strokeRect / strokeRoundRect ask for a stroke one LOGICAL pixel wide. This
    one line fixes Button, Card, ProgressBar, Chip, Entry, Alert, Banner, Frame
    and every other framed widget at once -- and it needed painter v0.11.0,
    which until now took a stroke width and discarded it.
  - CheckButton: the box, the label gap, and the checkmark, which was a
    hard-coded twelve-pixel path sitting in the corner of a box twice its size.
    The classic 12px tick is kept byte-for-byte for the classic 12px box.
  - Switch: the knob inset.
  - Expander/Accordion: the header height, through a new ExpanderHeaderHeight().
  - Gauge: the arc thickness.

Controlled both ways: with the fixes the audit is green; reverting the stroke
width flags five of the six original widgets, and reverting only the checkbox
flags exactly CheckButton.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tannevaled
tannevaled merged commit 68ca63f into main Aug 15, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant