Skip to content

win32: opacity on a child widget fades it (tweened); screenshot reads the screen; curve tests theme-agnostic - #169

Merged
nicolas-maman merged 1 commit into
mainfrom
fix/transitions-tests-theme-agnostic
Sep 19, 2026
Merged

nicolas-maman merged 1 commit into
mainfrom
fix/transitions-tests-theme-agnostic

Conversation

@nicolas-maman

Copy link
Copy Markdown
Contributor

Closes TODO.md's "win32 child-widget opacity (and the capture path that would prove it)" — both halves, with measurements.

What was wrong

  • opacity(v) in a build block (text("…") { opacity(0.25) }) reaches aether_ui_set_opacity, which on win32 was top-level-only: the widget stayed opaque while GTK4 and AppKit faded it. style_opacity (the CSS path) had used a layered child window all along, so the two verbs disagreed on one backend.
  • Nothing could prove any of it on Windows: the curve tests read "ink" (255 − brightness), which under a dark system theme rises as a label fades, so they reported "the label did not fade" against a fade that was there; and the driver's PrintWindow capture, mid-tween of a layered child, now and then returns a frame without the child.

What changed

Backend (backend/aether_ui_win32.c)

  • One opacity path, w32_apply_opacity: model value recorded at once (what the driver reports), presentation tweened when a transition was declared, snapped otherwise — for a top-level and a child alike.
  • A child given WS_EX_LAYERED before its top-level has ever been shown never paints (measured: absent from the screen, a forced redraw did not bring it back, the same alpha applied after the show fades it). So the alpha is owed while the window is unmapped and settled from WM_WINDOWPOSCHANGED / SWP_SHOWWINDOW, whichever path showed the window.
  • A transition declaration makes the widget layered at full alpha up front (the way a CoreAnimation view is layer-backed before it animates): the switch to a layered window, not the alpha, is what cost the first frames of a tween.
  • /screenshot reads the composed frame from the screen when the window is on screen and every corner and the centre belong to it (after raising it without activation); PrintWindow(PW_RENDERFULLCONTENT) otherwise — the headless case, still fine for still frames.

Tests

  • test_easing_curve.sh / test_spring_curve.sh: contrast against the ground (mean distance from the frame's median) instead of ink; median-of-three over the samples and the middle of three start captures, so a single dropped frame is not a point on the curve. Header notes rewritten to what was measured.
  • tests/win32/win32_runtime_test.c: child_opacity_is_owed_until_the_window_shows.

Measured (Windows 11, window on screen, screen grabs — not the driver)

  • { opacity(0.25) } label: 38.9 mean brightness against 60.3 opaque; a { opacity(0.4) } button 39.0 against 50.0.
  • 1200ms ease-out fade: label region 59.9 → 46.1 → 36.1 (before, 400ms in, settled).
  • Spring: 56.2 → 49.7 → 43.8 → 32.8 at 320ms (past the settled 35.7) → 35.7 — the designed overshoot.
  • Both curve tests pass on win32: ease-out early/late travel ratio ~5–6×; spring peak progress 1.10 at 360ms.
  • 74 headless driver specs identical to main (same five box-local failures), 16 on-screen specs pass, runtime test passes.

CHANGELOG under [current]; TODO.md section closed.

…creen

The ABI's aether_ui_set_opacity was top-level-only on win32, so a widget
styled { opacity(0.25) } in a build block stayed opaque while GTK4 and
AppKit faded it; style_opacity (the CSS path) already used a layered child
window. Both now share one path, tweened when a transition was declared.
Two things had to be learned for a widget styled during the build: a
child given WS_EX_LAYERED before its top-level has been shown never
paints, so the alpha is owed until the window shows (WM_WINDOWPOSCHANGED
with SWP_SHOWWINDOW); and the switch to a layered window, not the alpha,
is what cost the first frames of a tween, so a transition declaration
makes the widget layered at full alpha up front.

The driver's /screenshot reads the composed frame from the screen when
the window is on screen and unobscured (raised without activation), and
falls back to PrintWindow otherwise; PrintWindow mid-tween of a layered
child now and then returns a frame without the child. The two curve tests
measure contrast against the ground rather than "ink" (a dark theme read a
fade as no fade), filter single-frame holes and anchor on the middle of
three start captures. Both pass on win32: ease-out ratio ~5-6x, spring
peak progress ~1.10 at 360ms, the designed curve. TODO.md's "win32
child-widget opacity" is closed with the measurements.

Verified natively: runtime test (owed alpha), 74 headless specs unchanged
against main, 16 on-screen specs, screen grabs of the probe and demo.
@nicolas-maman
nicolas-maman merged commit c7fcbb9 into main Sep 19, 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

Development

Successfully merging this pull request may close these issues.

1 participant