Skip to content

stylo: servo has no user-preference media features, and its system colors are fixed

Generated from repros/stylo-servo-user-preference-media-features/README.md when the site was built at 7aa23e2.

No workaround yet. DECISIONS.md:34 makes the platform’s user settings live CSS inputs: Increase Contrast is prefers-contrast: more, reduced motion and transparency are their media queries, and the iOS tint and Material You palette are the system colors AccentColor and AccentColorText. Servo-engine stylo, which blitz uses, has neither:

  • style/servo/media_features.rs:163 lists 15 features and none of prefers-reduced-motion, prefers-reduced-transparency or prefers-contrast, so they parse as <general-enclosed> and never match. @media (prefers-reduced-motion: no-preference) is false on every device, so the radio example’s drift animation, written to run only then, never runs.
  • Device::system_color (style/device/servo.rs:377) returns hard-coded light and dark values (AccentColor is always #0066cc, or #0a84ff when dark), and MediaData has no field for the host to supply its own.

niso can only send the platform palette as custom properties (Dom::set_platform_style), which pages that use AccentColor don’t see.

$ cargo run
expected: each is a known media feature
(prefers-reduced-motion: no-preference) unknown <general-enclosed>, never matches
(prefers-reduced-motion: reduce) unknown <general-enclosed>, never matches
(prefers-reduced-transparency: reduce) unknown <general-enclosed>, never matches
(prefers-contrast: more) unknown <general-enclosed>, never matches
(prefers-contrast: no-preference) unknown <general-enclosed>, never matches

cargo run --features host-preferences builds the embedder side of the patch below: a MediaData carrying the user’s preferences and an AccentColor of its own. Without the patch it does not compile, because MediaData has no prefers_* or system_colors field and there is no other way to set them.

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

Exit status 1 while the bug reproduces.

servo-user-preferences.patch is against stylo 2dca345 (5 files, +138 -34) and also applies cleanly to main 7f0189de128e6dbd2b504acd3aebee3befa78358. Apply it from the stylo root with git apply or patch -p1.

  • prefers-contrast, prefers-reduced-motion and prefers-reduced-transparency for the servo engine, read from new MediaData fields. The value enums move from gecko/media_features.rs to queries/values.rs, shared by both engines beside PrefersColorScheme, and gain Default (no-preference). This is the shape of servo/stylo#288, rebased onto MediaData.
  • MediaData::system_colors: Option<Arc<SystemColors>> holds light and dark lists indexed by SystemColor as usize. Device::system_color returns the host’s entry when there is one and the current default otherwise. The Arc keeps MediaData cheap to clone and keeps it Clone + PartialEq + Debug.

Adding public fields to MediaData breaks embedders that build it with a struct literal, as blitz does (blitz-dom/src/stylo_device.rs:76), so blitz has to set the new fields too.

With the patch applied to a copy of the stylo source, and a copy of this repro pointed at it through [patch."https://github.com/servo/stylo"] (stylo and stylo_traits by path):

$ cargo run
expected: each is a known media feature
(prefers-reduced-motion: no-preference) known media feature
...
$ cargo run --features host-preferences
...
host: reduced motion, no transparency preference, more contrast
(prefers-reduced-motion: reduce) matches: true (expected true)
(prefers-reduced-motion: no-preference) matches: false (expected false)
(prefers-reduced-motion) matches: true (expected true)
(prefers-reduced-transparency) matches: false (expected false)
(prefers-contrast: more) matches: true (expected true)
(prefers-contrast) matches: true (expected true)
light AccentColor computes to rgb(255, 45, 85) (expected rgb(255, 45, 85))
light Canvas computes to rgb(255, 255, 255) (expected rgb(255, 255, 255))
dark AccentColor computes to rgb(255, 55, 95) (expected rgb(255, 55, 95))

Both exit 0. The gecko half of the enum move is not built here, since that needs a Firefox tree; it only replaces three local enums with imports of identical ones.

Reproduces on servo/stylo main at 7f0189de128e6dbd2b504acd3aebee3befa78358: MEDIA_FEATURES is still the same 15, and Device::system_color still hard-codes every color with no embedder input.

To post on servo/stylo#288, or as a new PR that references it:

Title: servo: user-preference media features from MediaData, and embedder-provided system colors

Body:

Servo-engine stylo has no prefers-reduced-motion, prefers-reduced-transparency or prefers-contrast, so @media (prefers-reduced-motion: no-preference) never matches, and Device::system_color hard-codes every system color, so an embedder can’t give AccentColor the platform’s tint. #288 covered the media features but predates MediaData. This rebases it onto MediaData and adds the colors:

  • MediaData gains prefers_reduced_motion, prefers_reduced_transparency and prefers_contrast (enums moved to queries/values.rs as in #288, now with Default = no-preference), evaluated by three new entries in MEDIA_FEATURES.
  • MediaData gains system_colors: Option<Arc<SystemColors>>, light and dark lists indexed by SystemColor as usize. Device::system_color uses an entry when the embedder supplied one and the current default otherwise.
// Cargo.toml: style = { package = "stylo", git = "https://github.com/servo/stylo", rev = "..." },
// style_traits = { package = "stylo_traits", ... }, cssparser = "0.38", url = "2.5"
use cssparser::Parser;
use style::context::QuirksMode;
use style::media_queries::MediaList;
use style::parser::ParserContext;
use style::queries::QueryCondition;
use style::stylesheets::{CssRuleType, Origin, UrlExtraData};
use style_traits::ParsingMode;
fn main() {
let url = UrlExtraData::from(url::Url::parse("about:blank").unwrap());
let mut context = ParserContext::new(Origin::Author, &url, Some(CssRuleType::Media), ParsingMode::DEFAULT,
QuirksMode::NoQuirks, Default::default(), None, None, Default::default());
let list = MediaList::parse(&mut context, &mut Parser::new("(prefers-reduced-motion: no-preference)"));
// Fails: the condition is GeneralEnclosed, so it never matches.
assert!(matches!(list.media_queries[0].condition, Some(QueryCondition::Feature(_))));
}

Tested at 2dca34592ced37c6aa01733e94206a767194cf9d; the patch applies unchanged to main 7f0189de128e6dbd2b504acd3aebee3befa78358. Adding MediaData fields is a breaking change for embedders that build it with a struct literal (Servo, Blitz).