From 6755e187acf231b93d743278039eee726e4199f7 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 21 Sep 2026 22:19:15 +0800 Subject: [PATCH 1/2] feat(css): expose preferred text scale through the Servo environment --- style/custom_properties.rs | 12 ++++++++++++ style/device/servo.rs | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/style/custom_properties.rs b/style/custom_properties.rs index 8ac9e51dd5..810670d7b3 100644 --- a/style/custom_properties.rs +++ b/style/custom_properties.rs @@ -227,6 +227,18 @@ impl CssEnvironment { return None; } + #[cfg(feature = "servo")] + if *name == Atom::from("preferred-text-scale") { + return Some(VariableValue::from_token( + Token::Number { + has_sign: false, + value: device.preferred_text_scale(), + int_value: None, + }, + url_data, + )); + } + if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) { return Some((var.evaluator)(device, url_data)); } diff --git a/style/device/servo.rs b/style/device/servo.rs index 10dba0298d..736c51b27a 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -84,6 +84,8 @@ impl Default for ServoMediaFeaturePreferences { pub(super) struct ExtraDeviceData { /// The current media type used by de device. media_type: MediaType, + /// User preferred text scale exposed through CSS env(). + preferred_text_scale: f32, /// The current viewport size, in CSS pixels. viewport_size: Size2D, /// The current screen size, in device pixels. @@ -148,6 +150,7 @@ impl Device { body_text_color: AtomicU32::new(AbsoluteColor::BLACK.to_nscolor()), extra: ExtraDeviceData { media_type, + preferred_text_scale: 1.0, viewport_size, device_size, device_pixel_ratio, @@ -162,6 +165,17 @@ impl Device { } } + /// User preferred text scale exposed by `env(preferred-text-scale)`. + pub fn preferred_text_scale(&self) -> f32 { + self.extra.preferred_text_scale + } + + /// Set the CSS environment text scale. The embedder must invalidate styles + /// that depend on environment variables when this value changes. + pub fn set_preferred_text_scale(&mut self, scale: f32) { + self.extra.preferred_text_scale = scale; + } + /// Returns the computed line-height for the font in a given computed values instance. /// /// If you pass down an element, then the used line-height is returned. From 3d44ed30808997861946a3f4b84a20318c4d451c Mon Sep 17 00:00:00 2001 From: lanyue-llk <270302213+lanyue-llk@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:10:39 +0800 Subject: [PATCH 2/2] fix(style): validate preferred text scale at device boundary --- style/device/servo.rs | 68 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/style/device/servo.rs b/style/device/servo.rs index 736c51b27a..0c734d5d80 100644 --- a/style/device/servo.rs +++ b/style/device/servo.rs @@ -173,6 +173,10 @@ impl Device { /// Set the CSS environment text scale. The embedder must invalidate styles /// that depend on environment variables when this value changes. pub fn set_preferred_text_scale(&mut self, scale: f32) { + assert!( + scale.is_finite() && scale > 0.0, + "preferred text scale must be finite and positive" + ); self.extra.preferred_text_scale = scale; } @@ -585,6 +589,70 @@ fn used_color_scheme_is_dark( mod tests { use super::*; + #[derive(Debug)] + struct TestFontMetrics; + + impl FontMetricsProvider for TestFontMetrics { + fn query_font_metrics( + &self, + _vertical: bool, + _font: &Font, + _base_size: CSSPixelLength, + _flags: QueryFontMetricsFlags, + ) -> FontMetrics { + FontMetrics::default() + } + + fn base_size_for_generic(&self, _generic: GenericFontFamily) -> Length { + Length::new(16.0) + } + } + + #[test] + fn preferred_text_scale_environment_tracks_device_and_rejects_invalid_values() { + let mut device = Device::new( + MediaType::screen(), + QuirksMode::NoQuirks, + euclid::size2(100.0, 100.0), + euclid::size2(100.0, 100.0), + Scale::new(1.0), + Box::new(TestFontMetrics), + ComputedValues::initial_values_with_font_override(Font::initial_values()), + PrefersColorScheme::Light, + PointerCapabilities::default(), + PointerCapabilities::default(), + ); + let url_data = crate::stylesheets::UrlExtraData::from( + url::Url::parse("https://example.test/").unwrap(), + ); + let text_scale = crate::Atom::from("preferred-text-scale"); + let value = |device: &Device| { + device + .environment() + .get(&text_scale, device, &url_data) + .unwrap() + .css + .parse::() + .unwrap() + }; + assert_eq!(value(&device), 1.0); + device.set_preferred_text_scale(1.5); + assert_eq!(value(&device), 1.5); + device.set_preferred_text_scale(2.0); + assert_eq!(value(&device), 2.0); + assert!(device + .environment() + .get(&crate::Atom::from("unknown"), &device, &url_data) + .is_none()); + for invalid in [0.0, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] { + assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + device.set_preferred_text_scale(invalid); + })) + .is_err()); + assert_eq!(value(&device), 2.0); + } + } + #[test] fn used_color_scheme_honors_supported_schemes_and_user_preference() { assert!(!used_color_scheme_is_dark(