Skip to content

blitz-dom: no step between styling and layout, so an animating element's styles cannot be adjusted

Generated from repros/blitz-dom-no-step-between-style-and-layout/README.md when the site was built at 7aa23e2.

niso applies the user’s text size (Dynamic Type, Android’s font scale) by editing the used font size in the styles each restyle computes, between resolve and another layout pass (crates/niso-core/src/text_scale.rs): stylo’s servo build has no text zoom (Device::text_zoom is fixed at 1, style/device/servo.rs). Every resolve restyles the elements a running animation or transition is on before it lays out, so while one runs that element, and the descendants that inherit their font size from it, are drawn at their CSS size. niso’s test for it, text_stays_scaled_while_its_element_transitions (crates/niso-core/tests/text.rs), is ignored until blitz has the step.

An embedder that adjusts computed styles (here a 2x text zoom on #t) and lays them out:

  • Expected: #t laid out at twice its width, still or while its color transitions.
  • Actual: right while still, but at its CSS width in every frame of the transition, since the resolve that lays the edit out restyles #t first.
279.2px
zoomed 2x while its color transitions, at 0.2s: expected 279.2px, actual 139.6px
zoomed 2x while its color transitions, at 0.4s: expected 279.2px, actual 139.6px
zoomed 2x while its color transitions, at 0.6s: expected 279.2px, actual 139.6px

resolve-style.patch splits BaseDocument::resolve into two public halves, resolve_style(time) -> bool (messages, scroll animation, device changes and the restyle, running animations included; false when there is nothing to lay out) and resolve_boxes(time) (damage, box construction, layout, transforms, the paint tree, subdocuments). resolve calls both, so nothing else changes. An embedder edits styles in between and inserts damage on what it changed. With it (--features split):

279.2px
zoomed 2x while its color transitions, at 0.2s: expected 279.2px, actual 279.2px
zoomed 2x while its color transitions, at 0.4s: expected 279.2px, actual 279.2px
zoomed 2x while its color transitions, at 0.6s: expected 279.2px, actual 279.2px

niso-text-scale.patch is niso’s side: text_scale::resolve scales between the halves wherever niso resolved. With both patches, text_stays_scaled_while_its_element_transitions passes, its three cases under a 2x curve (the transitioning element, a child with a font size of its own, which already passes without, and a child inheriting its font size), as does the rest of tests/text.rs and tests/render.rs.

Terminal window
CARGO_TARGET_DIR=~/.capy/work/repro-target cargo run
CARGO_TARGET_DIR=~/.capy/work/repro-target sh verify.sh

cargo run exits 1 while the bug reproduces. verify.sh applies the patch to a copy of the pinned checkout, runs the repro both ways and niso’s test with both patches (restoring niso and the lock files afterwards), and exits 0 when the repro fails unpatched and everything passes patched.

Not checked. Tested at the pinned be60597f67b3cece0456d26e8d7ff5a88600bbc5; main was at b6ace6a0c33e17b654e8e6d0b14a7e04e83756fe on 2026-11-09.

None searched.

Title: Expose a step between styling and layout (resolve in two halves)

Body:

Embedders sometimes need to adjust computed styles before layout: a text zoom (servo’s stylo has none), a control sized to a native widget. Today the only way is to edit after resolve and call resolve again, but every resolve restyles the elements a running animation or transition is on before laying out, so on those elements the edit never reaches layout. The attached patch splits resolve into resolve_style and resolve_boxes (with resolve calling both, unchanged). The repro here zooms an element 2x: with the split it stays zoomed through its transition; without, it is at its CSS size in every frame. Tested at be60597.