Skip to main content

style/substitution/
env.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The `env()` arbitrary substitution function.
6//! https://drafts.csswg.org/css-env-1/#funcdef-env
7
8use super::{
9    ArbitraryDeclarationValue, ParsedArgument, Substitution, SubstitutionArgument,
10    SubstitutionParserContext, SubstitutionReplacementContext, parse_optional_fallback,
11};
12use crate::custom_properties::{Name, VariableValue};
13use crate::device::Device;
14use crate::parser::{Parse, ParserContext};
15use crate::stylesheets::UrlExtraData;
16use crate::{Atom, derives::*};
17use cssparser::{Delimiter, Parser};
18use style_traits::ParseError;
19
20/// Environment variable name.
21#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
22pub struct EnvName(pub Name);
23
24impl Parse for EnvName {
25    fn parse(_: &ParserContext, input: &mut Parser) -> Result<Self, ParseError> {
26        // TODO(emilio): For env() this should be <custom-ident> per spec, but no other browser does
27        // that, see https://github.com/w3c/csswg-drafts/issues/3262.
28        Ok(EnvName(Atom::from(input.expect_ident()?.as_ref())))
29    }
30}
31
32/// Data specific to an `env()` function.
33#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
34pub struct EnvFunction {
35    /// The referenced environment variable name.
36    pub name: SubstitutionArgument<EnvName>,
37    /// The fallback for this function, if any.
38    pub fallback: Option<ArbitraryDeclarationValue>,
39}
40
41impl EnvFunction {
42    /// Parses the `env()` function arguments according to its argument grammar and syntax:
43    /// - <env-args> = env(<declaration-value>, <declaration-value>?)
44    /// - env() = env(<custom-ident> <integer [0,∞]>*, <declaration-value>?)
45    ///
46    /// TODO(Bug 2081127) - Support the optional index arguments.
47    pub fn parse(
48        input: &mut Parser,
49        context: &mut SubstitutionParserContext,
50    ) -> Result<Self, ParseError> {
51        let name = SubstitutionArgument::try_parse(input, context, Delimiter::Comma)?;
52        let fallback = parse_optional_fallback(input, context)?;
53        Ok(Self { name, fallback })
54    }
55
56    /// Replaces the `env()` function with its substitution value.
57    /// https://drafts.csswg.org/css-env-1/#substitute-an-env
58    pub fn replace<'a>(
59        &'a self,
60        source: &'a VariableValue,
61        context: &mut SubstitutionReplacementContext<'a, '_, '_, '_>,
62    ) -> Result<Substitution<'a>, ()> {
63        let name = self.replace_name(source, context);
64        let device = context.stylist.device();
65        let mut substitution = name
66            .value
67            .and_then(|name| {
68                device
69                    .environment()
70                    .get(&name.0, device, &context.parser_context(&source.url_data))
71                    .map(|v| Substitution::from_value(v, /* attr_tainted */ false))
72            })
73            .ok_or(())
74            .or_else(|_| {
75                context.substitute_declaration_value(self.fallback.as_ref().ok_or(())?, source)
76            })?;
77        substitution.attr_tainted |= name.attr_tainted;
78        Ok(substitution)
79    }
80
81    /// Returns the name, substituting and parsing it first if it has references.
82    pub fn replace_name<'a>(
83        &'a self,
84        source: &'a VariableValue,
85        context: &mut SubstitutionReplacementContext<'a, '_, '_, '_>,
86    ) -> ParsedArgument<'a, EnvName> {
87        self.name.substitute_and_parse(source, context)
88    }
89
90    /// Calls `f` for each arbitrary declaration value nested in the function.
91    pub fn for_each_unparsed_value<F>(&self, mut f: F)
92    where
93        F: FnMut(&ArbitraryDeclarationValue),
94    {
95        if let SubstitutionArgument::Unparsed(ref arg) = self.name {
96            f(arg);
97        }
98        if let Some(ref fallback) = self.fallback {
99            f(fallback);
100        }
101    }
102}
103
104/// The environment from which to get `env` function values.
105///
106/// TODO(emilio): If this becomes a bit more complex we should probably move it
107/// to the `media_queries` module, or something.
108#[derive(Debug, MallocSizeOf)]
109pub struct CssEnvironment;
110
111type EnvironmentEvaluator = fn(device: &Device, url_data: &UrlExtraData) -> VariableValue;
112
113struct EnvironmentVariable {
114    name: Atom,
115    evaluator: EnvironmentEvaluator,
116}
117
118macro_rules! make_variable {
119    ($name:expr, $evaluator:expr) => {{
120        EnvironmentVariable {
121            name: $name,
122            evaluator: $evaluator,
123        }
124    }};
125}
126
127fn get_safearea_inset_top(device: &Device, url_data: &UrlExtraData) -> VariableValue {
128    VariableValue::pixels(device.safe_area_insets().top, url_data)
129}
130
131fn get_safearea_inset_bottom(device: &Device, url_data: &UrlExtraData) -> VariableValue {
132    VariableValue::pixels(device.safe_area_insets().bottom, url_data)
133}
134
135fn get_safearea_inset_left(device: &Device, url_data: &UrlExtraData) -> VariableValue {
136    VariableValue::pixels(device.safe_area_insets().left, url_data)
137}
138
139fn get_safearea_inset_right(device: &Device, url_data: &UrlExtraData) -> VariableValue {
140    VariableValue::pixels(device.safe_area_insets().right, url_data)
141}
142
143#[cfg(feature = "gecko")]
144fn get_content_preferred_color_scheme(device: &Device, url_data: &UrlExtraData) -> VariableValue {
145    use crate::queries::values::PrefersColorScheme;
146    let prefers_color_scheme = unsafe {
147        crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme(
148            device.document(),
149            /* use_content = */ true,
150        )
151    };
152    VariableValue::ident(
153        match prefers_color_scheme {
154            PrefersColorScheme::Light => "light",
155            PrefersColorScheme::Dark => "dark",
156        },
157        url_data,
158    )
159}
160
161#[cfg(feature = "servo")]
162fn get_content_preferred_color_scheme(_device: &Device, url_data: &UrlExtraData) -> VariableValue {
163    // TODO: Add an implementation for Servo.
164    VariableValue::ident("light", url_data)
165}
166
167fn get_scrollbar_inline_size(device: &Device, url_data: &UrlExtraData) -> VariableValue {
168    VariableValue::pixels(device.scrollbar_inline_size().px(), url_data)
169}
170
171fn get_hairline(device: &Device, url_data: &UrlExtraData) -> VariableValue {
172    VariableValue::pixels(
173        app_units::Au(device.app_units_per_device_pixel()).to_f32_px(),
174        url_data,
175    )
176}
177
178static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [
179    make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top),
180    make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom),
181    make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left),
182    make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right),
183];
184
185#[cfg(feature = "gecko")]
186macro_rules! lnf_int {
187    ($id:ident) => {
188        unsafe {
189            crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt(
190                crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32,
191            )
192        }
193    };
194}
195
196#[cfg(feature = "servo")]
197macro_rules! lnf_int {
198    ($id:ident) => {
199        // TODO: Add an implementation for Servo.
200        0
201    };
202}
203
204macro_rules! lnf_int_variable {
205    ($atom:expr, $id:ident, $ctor:ident) => {{
206        fn __eval(_: &Device, url_data: &UrlExtraData) -> VariableValue {
207            VariableValue::$ctor(lnf_int!($id), url_data)
208        }
209        make_variable!($atom, __eval)
210    }};
211}
212
213fn eval_gtk_csd_titlebar_radius(device: &Device, url_data: &UrlExtraData) -> VariableValue {
214    let int_pixels = lnf_int!(TitlebarRadius);
215    let unzoomed_scale =
216        device.device_pixel_ratio_ignoring_full_zoom().get() / device.device_pixel_ratio().get();
217    VariableValue::pixels(int_pixels as f32 * unzoomed_scale, url_data)
218}
219
220fn get_submit_string(device: &Device, url_data: &UrlExtraData) -> VariableValue {
221    VariableValue::string(device.submit_string(), url_data)
222}
223
224static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 10] = [
225    make_variable!(
226        atom!("-moz-gtk-csd-titlebar-radius"),
227        eval_gtk_csd_titlebar_radius
228    ),
229    lnf_int_variable!(
230        atom!("-moz-gtk-csd-tooltip-radius"),
231        TooltipRadius,
232        int_pixels
233    ),
234    lnf_int_variable!(
235        atom!("-moz-gtk-csd-close-button-position"),
236        GTKCSDCloseButtonPosition,
237        integer
238    ),
239    lnf_int_variable!(
240        atom!("-moz-gtk-csd-minimize-button-position"),
241        GTKCSDMinimizeButtonPosition,
242        integer
243    ),
244    lnf_int_variable!(
245        atom!("-moz-gtk-csd-maximize-button-position"),
246        GTKCSDMaximizeButtonPosition,
247        integer
248    ),
249    lnf_int_variable!(
250        atom!("-moz-overlay-scrollbar-fade-duration"),
251        ScrollbarFadeDuration,
252        int_ms
253    ),
254    make_variable!(
255        atom!("-moz-content-preferred-color-scheme"),
256        get_content_preferred_color_scheme
257    ),
258    make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size),
259    make_variable!(atom!("hairline"), get_hairline),
260    make_variable!(atom!("-moz-submit-string"), get_submit_string),
261];
262
263impl CssEnvironment {
264    /// Get an env() variable, either custom or not.
265    #[inline]
266    pub fn get(
267        &self,
268        name: &Atom,
269        device: &Device,
270        parser_context: &ParserContext,
271    ) -> Option<VariableValue> {
272        #[cfg(feature = "gecko")]
273        let is_link_parameter = name.as_slice().starts_with(&[b'-' as u16, b'-' as u16]);
274        #[cfg(feature = "servo")]
275        let is_link_parameter = name.starts_with("--");
276        if is_link_parameter {
277            let param = device
278                .link_parameters()?
279                .0
280                .iter()
281                .find(|p| p.name.0 == *name)?;
282            let mut parser = cssparser::Parser::new(param.value.0.as_ref());
283            // need to carry around full variable value https://bugzilla.mozilla.org/show_bug.cgi?id=2028998
284            return VariableValue::parse(parser_context, &mut parser).ok();
285        }
286
287        if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) {
288            return Some((var.evaluator)(device, parser_context.url_data));
289        }
290        if !parser_context.chrome_rules_enabled() {
291            return None;
292        }
293        let var = CHROME_ENVIRONMENT_VARIABLES
294            .iter()
295            .find(|var| var.name == *name)?;
296        Some((var.evaluator)(device, parser_context.url_data))
297    }
298}