Skip to content

fix(ui): keep flexible children inside their parent, and let weight win - #104

Merged
nicolas-maman merged 3 commits into
mainfrom
fix/ui-flex-rounding-and-timer
Sep 4, 2026
Merged

fix(ui): keep flexible children inside their parent, and let weight win#104
nicolas-maman merged 3 commits into
mainfrom
fix/ui-flex-rounding-and-timer

Conversation

@nicolas-maman

Copy link
Copy Markdown
Contributor

What

Two reported bugs, plus a third found while confirming the second.

ui.timer stops during modal tracking (#97)

scheduledTimer registers in the default run-loop mode alone, and AppKit runs
slider drags, scrollbar drags, menu tracking and live window resize in
NSEventTrackingRunLoopMode, where a default-mode timer does not fire. An
animation or a clock stopped dead for as long as the mouse was held down and
jumped on release. The timer now goes into the common modes.

Flexible children overflow their parent by the rounding remainder (#101)

The layout was never wrong. Auto Layout divides a 302px row exactly:

96.5 + 6 + 97 + 6 + 96.5 = 302.0

The readback was wrong. It rounded each child's SIZE on its own, so those
three frames reported as 97 / 97 / 96.5 rounded up to 97, a total of 303 in a
302px parent. The report's own numbers show it: x=206 w=97 where the true
frame is 205.5 + 96.5. Positions already rounded consistently, sizes did not.

A reported size is now the distance between two ROUNDED EDGES, so adjacent
children tile: one child's trailing edge is the next one's leading edge, and
the row reads back as 97 + 97 + 96 = 290, plus 12 of gaps, exactly 302. The
remainder is spread rather than dumped on the last child, so the widest and
narrowest differ by at most a pixel, which is the distribution the issue asks
for.

Fixed in get_width / get_height and in the /widgets and /widget/N
geometry the driver reports, on AppKit and on GTK4. GTK4 had the same
independent rounding by truncation, which under-reports rather than overflows,
but breaks tiling the same way. Win32 already derived its sizes from integer
edges and needed no change.

weight() silently overridden on buttons

Confirming the weighted-pane half of #101 turned up a separate bug. A row of
buttons gets an equal-width chain by default, which is what gives the
calculator its grid-like rows. That chain is a required constraint and the
proportional shares from weight() are not, so weights of 16 / 62 / 22 came
out 500 / 500 / 500 in a 1500px row, with no diagnostic. set_width already
retracted the chain for exactly this reason; weight now does too.

Before and after, same program:

weighted=500/500/500   vs row=1500
weighted=240/930/330   vs row=1500

240 / 930 / 330 is 16% / 62% / 22% of 1500, and it sums to the row exactly, so
this also settles the "237 + 918 + 326 = 1481" case in the report.

Verification

  • aeb .all.ae builds clean, zero warnings.
  • flexround_demo reports buttons=97/97/96 in a 302px row and
    weighted=240/930/330 in a 1500px row. Both sum to their parent exactly.
  • Measured the raw fractional frames first, rather than trusting the rounded
    numbers, which is what showed the layout was already correct and moved the
    fix to the readback instead of to the constraints. An earlier attempt that
    lowered the equal-width constraint's priority changed nothing and was
    dropped.
  • spec_flexround_demo asserts relationships between the reported numbers,
    not per-backend pixel counts, so it means the same thing on AppKit and
    GTK4. Wired into ci.sh phase 5k6. Driver specs cannot run in the local
    sandbox, which blocks the bind, so CI is what exercises it; the spec itself
    compiles clean against the driver library.
  • No existing spec asserts an exact widget width, so nothing depends on the
    old numbers.

Closes #97
Closes #101

🤖 Generated with Claude Code

nicolas-maman and others added 3 commits September 4, 2026 16:13
The geometry a caller reads back is now the distance between two rounded
edges, never a rounded size. A stack divides its width exactly (96.5 + 6 +
97 + 6 + 96.5 fills a 302px row), but rounding each child's size on its own
reported 97/97/97 for a total of 303, so the last child looked like it hung
a pixel past the row and "every widget fits inside its parent", the one
invariant a layout audit wants to trust, was false by a pixel and compounded
with nesting. Edge rounding makes adjacent children tile: one child's
trailing edge is the next one's leading edge, so a 302px row reads back as
97 + 97 + 96. This covers get_width and get_height and the /widgets and
/widget/N geometry the driver reports, on AppKit and on GTK4, where the same
independent rounding truncated instead and under-reported. Win32 already
derived its sizes from integer edges.

Found while confirming the weighted half of the same report: weight() on
buttons was silently overridden by the equal-width chain a row of buttons
gets by default. That chain is a required constraint and the proportional
shares are not, so weights of 16 / 62 / 22 came out 500/500/500 in a 1500px
row. A weight is an explicit instruction to divide space unevenly, so it now
retracts the chain for that child exactly as an explicit width() already
did, and the same weights now give 240/930/330, summing to the row exactly.

ui.timer also keeps firing while a slider or scrollbar is being dragged, a
menu is open, or a window is resized live. scheduledTimer registers in the
default run-loop mode alone, and AppKit runs all of that tracking in
NSEventTrackingRunLoopMode, where a default-mode timer never fires, so an
animation or a clock stopped dead for as long as the mouse was held down.

flexround_demo and its driver spec assert the invariant rather than
per-backend pixel counts, so they mean the same thing on AppKit and GTK4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new spec claimed the three buttons end up within a pixel of each other.
That is AppKit behaviour: an equal-width chain is what gives a row of
buttons its grid-like look there, while GTK4 sizes each button to its own
label, so the row came out 60 / 71 and the assertion failed on Linux while
the three real invariants passed on both backends.

Replaced with the property the fix actually delivers, which is true on
either backend: adjacent children's reported edges are separated by exactly
the stack's spacing. Rounding each width on its own pushed the remainder
into the gaps, so a 6px gap read back as 5 or 7 depending on where the
fractions fell; edge-rounded geometry makes it 6 every time, because both
edges are offset by the same whole number before rounding.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@nicolas-maman
nicolas-maman merged commit 1922ac1 into main Sep 4, 2026
3 checks 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

1 participant