diff options
author | Emilio Cobos Álvarez <emilio@crisal.io> | 2018-11-05 10:39:46 +0000 |
---|---|---|
committer | Emilio Cobos Álvarez <emilio@crisal.io> | 2018-11-05 12:33:16 +0100 |
commit | b7da1bac888b7464164cf7223528beb25826f627 (patch) | |
tree | cd5c2ff73674870e59499551e08379454b174609 /components/style/custom_properties.rs | |
parent | 5af6abfb784c57ea7d0a81563a98fed8e46f3461 (diff) | |
download | servo-b7da1bac888b7464164cf7223528beb25826f627.tar.gz servo-b7da1bac888b7464164cf7223528beb25826f627.zip |
style: Implement the env() function with hardcoded zeros for safe-area-inset.
Intent to Implement and Ship: https://groups.google.com/d/msg/mozilla.dev.platform/EVKyR1B87T0/_l-_qK8SAAAJ
Differential Revision: https://phabricator.services.mozilla.com/D9609
Diffstat (limited to 'components/style/custom_properties.rs')
-rw-r--r-- | components/style/custom_properties.rs | 372 |
1 files changed, 274 insertions, 98 deletions
diff --git a/components/style/custom_properties.rs b/components/style/custom_properties.rs index 331d2a74342..344e38af8d1 100644 --- a/components/style/custom_properties.rs +++ b/components/style/custom_properties.rs @@ -21,6 +21,59 @@ use std::fmt::{self, Write}; use std::hash::Hash; use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +/// The environment from which to get `env` function values. +/// +/// TODO(emilio): If this becomes a bit more complex we should probably move it +/// to the `media_queries` module, or something. +pub struct CssEnvironment; + +struct EnvironmentVariable { + name: Atom, + value: VariableValue, +} + +macro_rules! make_variable { + ($name:expr, $value:expr) => {{ + EnvironmentVariable { + name: $name, + value: { + // TODO(emilio): We could make this be more efficient (though a + // bit less convenient). + let mut input = ParserInput::new($value); + let mut input = Parser::new(&mut input); + + let (first_token_type, css, last_token_type) = + parse_self_contained_declaration_value(&mut input, None).unwrap(); + + VariableValue { + css: css.into_owned(), + first_token_type, + last_token_type, + references: Default::default(), + references_environment: false, + } + }, + } + }}; +} + +lazy_static! { + static ref ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [ + make_variable!(atom!("safe-area-inset-top"), "0px"), + make_variable!(atom!("safe-area-inset-bottom"), "0px"), + make_variable!(atom!("safe-area-inset-left"), "0px"), + make_variable!(atom!("safe-area-inset-right"), "0px"), + ]; +} + +impl CssEnvironment { + #[inline] + fn get(&self, name: &Atom) -> Option<&VariableValue> { + let var = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name)?; + Some(&var.value) + } +} + /// A custom property name is just an `Atom`. /// /// Note that this does not include the `--` prefix @@ -48,6 +101,12 @@ pub struct VariableValue { first_token_type: TokenSerializationType, last_token_type: TokenSerializationType, + /// Whether a variable value has a reference to an environment variable. + /// + /// If this is the case, we need to perform variable substitution on the + /// value. + references_environment: bool, + /// Custom property names in var() functions. references: PrecomputedHashSet<Name>, } @@ -216,6 +275,14 @@ where } } +/// A struct holding information about the external references to that a custom +/// property value may have. +#[derive(Default)] +struct VarOrEnvReferences { + custom_property_references: PrecomputedHashSet<Name>, + references_environment: bool, +} + impl VariableValue { fn empty() -> Self { Self { @@ -223,6 +290,7 @@ impl VariableValue { last_token_type: TokenSerializationType::nothing(), first_token_type: TokenSerializationType::nothing(), references: PrecomputedHashSet::default(), + references_environment: false, } } @@ -273,7 +341,7 @@ impl VariableValue { /// Parse a custom property value. pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Arc<Self>, ParseError<'i>> { - let mut references = PrecomputedHashSet::default(); + let mut references = VarOrEnvReferences::default(); let (first_token_type, css, last_token_type) = parse_self_contained_declaration_value(input, Some(&mut references))?; @@ -282,7 +350,8 @@ impl VariableValue { css: css.into_owned(), first_token_type, last_token_type, - references, + references: references.custom_property_references, + references_environment: references.references_environment, })) } } @@ -297,7 +366,7 @@ pub fn parse_non_custom_with_var<'i, 't>( fn parse_self_contained_declaration_value<'i, 't>( input: &mut Parser<'i, 't>, - references: Option<&mut PrecomputedHashSet<Name>>, + references: Option<&mut VarOrEnvReferences>, ) -> Result<(TokenSerializationType, Cow<'i, str>, TokenSerializationType), ParseError<'i>> { let start_position = input.position(); let mut missing_closing_characters = String::new(); @@ -317,7 +386,7 @@ fn parse_self_contained_declaration_value<'i, 't>( /// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value> fn parse_declaration_value<'i, 't>( input: &mut Parser<'i, 't>, - references: Option<&mut PrecomputedHashSet<Name>>, + references: Option<&mut VarOrEnvReferences>, missing_closing_characters: &mut String, ) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { @@ -334,7 +403,7 @@ fn parse_declaration_value<'i, 't>( /// invalid at the top level fn parse_declaration_value_block<'i, 't>( input: &mut Parser<'i, 't>, - mut references: Option<&mut PrecomputedHashSet<Name>>, + mut references: Option<&mut VarOrEnvReferences>, missing_closing_characters: &mut String, ) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { let mut token_start = input.position(); @@ -407,6 +476,12 @@ fn parse_declaration_value_block<'i, 't>( parse_var_function(input, references.as_mut().map(|r| &mut **r)) })?; input.reset(&args_start); + } else if name.eq_ignore_ascii_case("env") { + let args_start = input.state(); + input.parse_nested_block(|input| { + parse_env_function(input, references.as_mut().map(|r| &mut **r)) + })?; + input.reset(&args_start); } nested!(); check_closed!(")"); @@ -468,29 +543,48 @@ fn parse_declaration_value_block<'i, 't>( } } +fn parse_fallback<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + // Exclude `!` and `;` at the top level + // https://drafts.csswg.org/css-syntax/#typedef-declaration-value + input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { + // At least one non-comment token. + input.next_including_whitespace()?; + // Skip until the end. + while let Ok(_) = input.next_including_whitespace_and_comments() {} + Ok(()) + }) +} + // If the var function is valid, return Ok((custom_property_name, fallback)) fn parse_var_function<'i, 't>( input: &mut Parser<'i, 't>, - references: Option<&mut PrecomputedHashSet<Name>>, + references: Option<&mut VarOrEnvReferences>, ) -> Result<(), ParseError<'i>> { let name = input.expect_ident_cloned()?; - let name: Result<_, ParseError> = parse_name(&name).map_err(|()| { + let name = parse_name(&name).map_err(|()| { input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone())) - }); - let name = name?; + })?; if input.try(|input| input.expect_comma()).is_ok() { - // Exclude `!` and `;` at the top level - // https://drafts.csswg.org/css-syntax/#typedef-declaration-value - input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { - // At least one non-comment token. - input.next_including_whitespace()?; - // Skip until the end. - while let Ok(_) = input.next_including_whitespace_and_comments() {} - Ok(()) - })?; + parse_fallback(input)?; } if let Some(refs) = references { - refs.insert(Atom::from(name)); + refs.custom_property_references.insert(Atom::from(name)); + } + Ok(()) +} + +fn parse_env_function<'i, 't>( + input: &mut Parser<'i, 't>, + references: Option<&mut VarOrEnvReferences>, +) -> Result<(), ParseError<'i>> { + // TODO(emilio): This should be <custom-ident> per spec, but no other + // browser does that, see https://github.com/w3c/csswg-drafts/issues/3262. + input.expect_ident()?; + if input.try(|input| input.expect_comma()).is_ok() { + parse_fallback(input)?; + } + if let Some(references) = references { + references.references_environment = true; } Ok(()) } @@ -502,25 +596,26 @@ pub struct CustomPropertiesBuilder<'a> { may_have_cycles: bool, custom_properties: Option<CustomPropertiesMap>, inherited: Option<&'a Arc<CustomPropertiesMap>>, + environment: &'a CssEnvironment, } impl<'a> CustomPropertiesBuilder<'a> { /// Create a new builder, inheriting from a given custom properties map. - pub fn new(inherited: Option<&'a Arc<CustomPropertiesMap>>) -> Self { + pub fn new( + inherited: Option<&'a Arc<CustomPropertiesMap>>, + environment: &'a CssEnvironment, + ) -> Self { Self { seen: PrecomputedHashSet::default(), may_have_cycles: false, custom_properties: None, inherited, + environment, } } /// Cascade a given custom property declaration. - pub fn cascade( - &mut self, - name: &'a Name, - specified_value: &CustomDeclarationValue, - ) { + pub fn cascade(&mut self, name: &'a Name, specified_value: &CustomDeclarationValue) { let was_already_present = !self.seen.insert(name); if was_already_present { return; @@ -540,8 +635,31 @@ impl<'a> CustomPropertiesBuilder<'a> { let map = self.custom_properties.as_mut().unwrap(); match *specified_value { CustomDeclarationValue::Value(ref unparsed_value) => { - self.may_have_cycles |= !unparsed_value.references.is_empty(); - map.insert(name.clone(), (*unparsed_value).clone()); + let has_references = !unparsed_value.references.is_empty(); + self.may_have_cycles |= has_references; + + // If the variable value has no references and it has an + // environment variable here, perform substitution here instead + // of forcing a full traversal in `substitute_all` afterwards. + let value = if !has_references && unparsed_value.references_environment { + let invalid = Default::default(); // Irrelevant, since there are no references. + let result = substitute_references_in_value( + unparsed_value, + &map, + &invalid, + &self.environment, + ); + match result { + Ok(new_value) => Arc::new(new_value), + Err(..) => { + map.remove(name); + return; + }, + } + } else { + (*unparsed_value).clone() + }; + map.insert(name.clone(), value); }, CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword { CSSWideKeyword::Initial => { @@ -553,11 +671,7 @@ impl<'a> CustomPropertiesBuilder<'a> { } } - fn value_may_affect_style( - &self, - name: &Name, - value: &CustomDeclarationValue, - ) -> bool { + fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool { match *value { CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) | CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => { @@ -596,8 +710,8 @@ impl<'a> CustomPropertiesBuilder<'a> { /// Returns the final map of applicable custom properties. /// - /// If there was any specified property, we've created a new map and now we need - /// to remove any potential cycles, and wrap it in an arc. + /// If there was any specified property, we've created a new map and now we + /// need to remove any potential cycles, and wrap it in an arc. /// /// Otherwise, just use the inherited custom properties map. pub fn build(mut self) -> Option<Arc<CustomPropertiesMap>> { @@ -605,9 +719,8 @@ impl<'a> CustomPropertiesBuilder<'a> { Some(m) => m, None => return self.inherited.cloned(), }; - if self.may_have_cycles { - substitute_all(&mut map); + substitute_all(&mut map, self.environment); } Some(Arc::new(map)) } @@ -616,7 +729,7 @@ impl<'a> CustomPropertiesBuilder<'a> { /// Resolve all custom properties to either substituted or invalid. /// /// It does cycle dependencies removal at the same time as substitution. -fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) { +fn substitute_all(custom_properties_map: &mut CustomPropertiesMap, environment: &CssEnvironment) { // The cycle dependencies removal in this function is a variant // of Tarjan's algorithm. It is mostly based on the pseudo-code // listed in @@ -664,6 +777,8 @@ fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) { map: &'a mut CustomPropertiesMap, /// The set of invalid custom properties. invalid: &'a mut PrecomputedHashSet<Name>, + /// The environment to substitute `env()` variables. + environment: &'a CssEnvironment, } /// This function combines the traversal for cycle removal and value @@ -686,11 +801,23 @@ fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) { /// * There is no such variable at all. fn traverse<'a>(name: Name, context: &mut Context<'a>) -> Option<usize> { // Some shortcut checks. - let (name, value) = if let Some(value) = context.map.get(&name) { - // This variable has been resolved. Return the signal value. - if value.references.is_empty() || context.invalid.contains(&name) { + let (name, value) = { + let value = context.map.get(&name)?; + + // Nothing to resolve. + if value.references.is_empty() { + debug_assert!( + !value.references_environment, + "Should've been handled earlier" + ); return None; } + + // This variable has already been resolved. + if context.invalid.contains(&name) { + return None; + } + // Whether this variable has been visited in this traversal. let key; match context.index_map.entry(name) { @@ -702,12 +829,10 @@ fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) { entry.insert(context.count); }, } + // Hold a strong reference to the value so that we don't // need to keep reference to context.map. (key, value.clone()) - } else { - // The variable doesn't exist at all. - return None; }; // Add new entry to the information table. @@ -793,29 +918,20 @@ fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) { // Now we have shown that this variable is not in a loop, and // all of its dependencies should have been resolved. We can // start substitution now. - let mut computed_value = ComputedValue::empty(); - let mut input = ParserInput::new(&value.css); - let mut input = Parser::new(&mut input); - let mut position = (input.position(), value.first_token_type); - let result = substitute_block( - &mut input, - &mut position, - &mut computed_value, - &mut |name, partial_computed_value| { - if let Some(value) = context.map.get(name) { - if !context.invalid.contains(name) { - partial_computed_value.push_variable(value); - return Ok(value.last_token_type); - } - } - Err(()) - }, + let result = substitute_references_in_value( + &value, + &context.map, + &context.invalid, + &context.environment, ); - if let Ok(last_token_type) = result { - computed_value.push_from(position, &input, last_token_type); - context.map.insert(name, Arc::new(computed_value)); - } else { - context.invalid.insert(name); + + match result { + Ok(computed_value) => { + context.map.insert(name, Arc::new(computed_value)); + }, + Err(..) => { + context.invalid.insert(name); + }, } // All resolved, so return the signal value. @@ -834,6 +950,7 @@ fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) { var_info: SmallVec::new(), map: custom_properties_map, invalid: &mut invalid, + environment, }; traverse(name, &mut context); } @@ -841,25 +958,51 @@ fn substitute_all(custom_properties_map: &mut CustomPropertiesMap) { custom_properties_map.remove_set(&invalid); } +/// Replace `var()` and `env()` functions in a pre-existing variable value. +fn substitute_references_in_value<'i>( + value: &'i VariableValue, + custom_properties: &CustomPropertiesMap, + invalid_custom_properties: &PrecomputedHashSet<Name>, + environment: &CssEnvironment, +) -> Result<ComputedValue, ParseError<'i>> { + debug_assert!(!value.references.is_empty() || value.references_environment); + + let mut input = ParserInput::new(&value.css); + let mut input = Parser::new(&mut input); + let mut position = (input.position(), value.first_token_type); + let mut computed_value = ComputedValue::empty(); + + let last_token_type = substitute_block( + &mut input, + &mut position, + &mut computed_value, + custom_properties, + invalid_custom_properties, + environment, + )?; + + computed_value.push_from(position, &input, last_token_type); + Ok(computed_value) +} + /// Replace `var()` functions in an arbitrary bit of input. /// -/// The `substitute_one` callback is called for each `var()` function in `input`. -/// If the variable has its initial value, -/// the callback should return `Err(())` and leave `partial_computed_value` unchanged. +/// If the variable has its initial value, the callback should return `Err(())` +/// and leave `partial_computed_value` unchanged. +/// /// Otherwise, it should push the value of the variable (with its own `var()` functions replaced) /// to `partial_computed_value` and return `Ok(last_token_type of what was pushed)` /// /// Return `Err(())` if `input` is invalid at computed-value time. /// or `Ok(last_token_type that was pushed to partial_computed_value)` otherwise. -fn substitute_block<'i, 't, F>( +fn substitute_block<'i, 't>( input: &mut Parser<'i, 't>, position: &mut (SourcePosition, TokenSerializationType), partial_computed_value: &mut ComputedValue, - substitute_one: &mut F, -) -> Result<TokenSerializationType, ParseError<'i>> -where - F: FnMut(&Name, &mut ComputedValue) -> Result<TokenSerializationType, ()>, -{ + custom_properties: &CustomPropertiesMap, + invalid_custom_properties: &PrecomputedHashSet<Name>, + env: &CssEnvironment, +) -> Result<TokenSerializationType, ParseError<'i>> { let mut last_token_type = TokenSerializationType::nothing(); let mut set_position_at_next_iteration = false; loop { @@ -883,27 +1026,49 @@ where Err(..) => break, }; match token { - Token::Function(ref name) if name.eq_ignore_ascii_case("var") => { + Token::Function(ref name) + if name.eq_ignore_ascii_case("var") || name.eq_ignore_ascii_case("env") => + { + let is_env = name.eq_ignore_ascii_case("env"); + partial_computed_value.push( input.slice(position.0..before_this_token), position.1, last_token_type, ); input.parse_nested_block(|input| { - // parse_var_function() ensures neither .unwrap() will fail. - let name = input.expect_ident_cloned().unwrap(); - let name = Atom::from(parse_name(&name).unwrap()); - - if let Ok(last) = substitute_one(&name, partial_computed_value) { - last_token_type = last; + // parse_var_function() / parse_env_function() ensure neither .unwrap() will fail. + let name = { + let name = input.expect_ident().unwrap(); + if is_env { + Atom::from(&**name) + } else { + Atom::from(parse_name(&name).unwrap()) + } + }; + + let value = if is_env { + env.get(&name) + } else { + if invalid_custom_properties.contains(&name) { + None + } else { + custom_properties.get(&name).map(|v| &**v) + } + }; + + if let Some(v) = value { + last_token_type = v.last_token_type; + partial_computed_value.push_variable(v); // Skip over the fallback, as `parse_nested_block` would return `Err` - // if we don’t consume all of `input`. + // if we don't consume all of `input`. // FIXME: Add a specialized method to cssparser to do this with less work. - while let Ok(_) = input.next() {} + while input.next().is_ok() {} } else { input.expect_comma()?; let after_comma = input.state(); - let first_token_type = input.next_including_whitespace_and_comments() + let first_token_type = input + .next_including_whitespace_and_comments() // parse_var_function() ensures that .unwrap() will not fail. .unwrap() .serialization_type(); @@ -913,23 +1078,31 @@ where input, &mut position, partial_computed_value, - substitute_one, + custom_properties, + invalid_custom_properties, + env, )?; partial_computed_value.push_from(position, input, last_token_type); } Ok(()) })?; set_position_at_next_iteration = true - }, - + } Token::Function(_) | Token::ParenthesisBlock | Token::CurlyBracketBlock | Token::SquareBracketBlock => { input.parse_nested_block(|input| { - substitute_block(input, position, partial_computed_value, substitute_one) + substitute_block( + input, + position, + partial_computed_value, + custom_properties, + invalid_custom_properties, + env, + ) })?; - // It’s the same type for CloseCurlyBracket and CloseSquareBracket. + // It's the same type for CloseCurlyBracket and CloseSquareBracket. last_token_type = Token::CloseParenthesis.serialization_type(); }, @@ -945,29 +1118,32 @@ where Ok(last_token_type) } -/// Replace `var()` functions for a non-custom property. +/// Replace `var()` and `env()` functions for a non-custom property. +/// /// Return `Err(())` for invalid at computed time. pub fn substitute<'i>( input: &'i str, first_token_type: TokenSerializationType, computed_values_map: Option<&Arc<CustomPropertiesMap>>, + env: &CssEnvironment, ) -> Result<String, ParseError<'i>> { let mut substituted = ComputedValue::empty(); let mut input = ParserInput::new(input); let mut input = Parser::new(&mut input); let mut position = (input.position(), first_token_type); + let invalid = PrecomputedHashSet::default(); + let empty_map = CustomPropertiesMap::new(); + let custom_properties = match computed_values_map { + Some(m) => &**m, + None => &empty_map, + }; let last_token_type = substitute_block( &mut input, &mut position, &mut substituted, - &mut |name, substituted| { - if let Some(value) = computed_values_map.and_then(|map| map.get(name)) { - substituted.push_variable(value); - Ok(value.last_token_type) - } else { - Err(()) - } - }, + &custom_properties, + &invalid, + env, )?; substituted.push_from(position, &input, last_token_type); Ok(substituted.css) |