stylo: servo ignores the viewport variants (sv*, lv*, dv* all equal vh) and `env(safe-area-inset-*)` is always 0, with no embedder input for either
Generated from repros/stylo-servo-viewport-variants-and-safe-area/README.md when the site was built at 7aa23e2.
What niso does
Section titled “What niso does”A page that asks for <meta name="viewport" content="viewport-fit=cover"> is laid out under the status bar, the
Dynamic Island or camera cutout, and the home indicator or navigation bar. To keep its content clear of them it needs
the safe-area insets, which the web gives as env(safe-area-inset-top|right|bottom|left), and to size things to the
part of the screen the bars leave it needs the small, large and dynamic viewport units (svh, lvh, dvh and the
rest). Servo-engine stylo, which blitz uses, has neither:
env()readsDevice::safe_area_insets()(style/custom_properties.rs:66-80), which for the servo engine returnsSideOffsets2D::zero()(style/device/servo.rs:499). Gecko’s asks the embedder (style/device/gecko.rs:443).- Stylo parses the viewport variants, and resolves them through
Device::au_viewport_size_for_viewport_unit_resolution(variant). Servo’s ignores the variant (style/device/servo.rs:215-223: “Servo doesn’t have dynamic UA interfaces that affect the viewport”), so100svh,100lvhand100dvhall equal100vh. Gecko’s sizes each variant from its toolbar state (style/device/gecko.rs:294-340) and records when a page used the dynamic one, so the embedder knows to restyle when it changes; servo never setsused_dynamic_viewport_size. MediaData, the embedder’s input to the device, has no field for any of it.
So niso’s hosts publish the insets as custom properties instead, through the platform stylesheet
(Dom::set_platform_style): --niso-safe-area-inset-top and the rest, set only for cover pages. Pages write
padding-top: max(env(safe-area-inset-top), var(--niso-safe-area-inset-top, 0px));which works in browsers (the custom property is unset there) and keeps working once stylo reads real insets. There is no
such workaround for the viewport units; pages get vh for all three until stylo resolves them. The sizes niso would give
each variant are in DECISIONS.md and at ROOT in niso_runtime.h.
Same code on main (7f0189de128e6dbd2b504acd3aebee3befa78358): safe_area_insets is still SideOffsets2D::zero() and
the variant is still ignored. No issue or PR found in servo/stylo or servo/servo.
Expected vs actual
Section titled “Expected vs actual”An iPhone 17 in portrait with a cover page: the viewport is the 402x874 screen, and the status bar (62 high) and home indicator (34) stay on it.
$ cargo runsmall viewport (sv*): expected [402.0, 778.0], actual [402.0, 874.0]large viewport (lv*): expected [402.0, 874.0], actual [402.0, 874.0]dynamic viewport (dv*): expected [402.0, 874.0], actual [402.0, 874.0]env(safe-area-inset-top right bottom left): expected [62.0, 0.0, 34.0, 0.0], actual [0.0, 0.0, 0.0, 0.0]servo resolves every viewport variant against the one viewport size, its safe area insets are always zero, and MediaData has no way to set eithercargo run --features host-viewport builds the embedder side of the patch below: a MediaData carrying the sizes and
insets. Without the patch it does not compile, because MediaData has no viewport_metrics field.
CARGO_TARGET_DIR=~/.capy/work/repro-target cargo runExit status 1 while the bug reproduces.
The patch
Section titled “The patch”servo-viewport-metrics.patch is against stylo 2dca345 (1 file, +37 -5). It gives servo’s Device one embedder input
for both, MediaData::viewport_metrics, a ViewportMetrics:
small,largeanddynamic: the size each viewport variant resolves against, each the viewport size whenNone.vh(the UA-default variant) keeps the viewport size. Resolving a dynamic unit setsused_dynamic_viewport_size, as Gecko does.safe_area_insets: whatDevice::safe_area_insetsreturns.
Its default is today’s behaviour. Apply it from the stylo root with patch -p1. With it applied to a copy of the
checkout and every stylo package [patch]ed to that copy, cargo run --features host-viewport prints the expected
values for all four rows and exits 0. Embedders change the metrics the way
they change the viewport size, with Device::set_media_data or media_data_mut, which already restyle as needed.
Adding a public field to MediaData breaks embedders that build it with a struct literal, as blitz does
(blitz-dom/src/stylo_device.rs), so blitz has to set it too (.. with the default, or a setter on its document beside
the viewport’s). It sits next to the MediaData fields in stylo-servo-user-preference-media-features; the two patches
touch neighbouring lines of the same struct.
niso does not use the patch: per DECISIONS.md it does not fork stylo. The recommendation is to send the patch
upstream (servo/stylo), then blitz’s half.
Issue text
Section titled “Issue text”servo: viewport-percentage variants (sv, lv, dv*) resolve like vh and
env(safe-area-inset-*)is always 0, with no embedder input for either**
Device::au_viewport_size_for_viewport_unit_resolutionfor the servo engine ignores itsViewportVariant, andDevice::safe_area_insetsreturnsSideOffsets2D::zero().MediaDatahas no field for either, so an embedder that shows system UI over or around the page (a phone, a browser with retractable toolbars) cannot give pages the small, large and dynamic viewports or the safe area. Gecko asks its embedder for both. Proposal: aviewport_metrics: ViewportMetricsfield onMediaDatawith optional sizes for the small, large and dynamic viewports (the viewport size when unset) and the safe area insets, which those twoDevicemethods return, andused_dynamic_viewport_sizeset when a dynamic unit is resolved. Patch and repro attached.
Not filed.