diff options
Diffstat (limited to 'servo/components')
358 files changed, 133035 insertions, 0 deletions
diff --git a/servo/components/derive_common/Cargo.toml b/servo/components/derive_common/Cargo.toml new file mode 100644 index 0000000000..b493988026 --- /dev/null +++ b/servo/components/derive_common/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "derive_common" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +[lib] +path = "lib.rs" + +[dependencies] +darling = { version = "0.20", default-features = false } +proc-macro2 = "1" +quote = "1" +syn = { version = "2", default-features = false, features = ["clone-impls", "parsing"] } +synstructure = "0.13" diff --git a/servo/components/derive_common/cg.rs b/servo/components/derive_common/cg.rs new file mode 100644 index 0000000000..73301af2ff --- /dev/null +++ b/servo/components/derive_common/cg.rs @@ -0,0 +1,396 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use darling::{FromDeriveInput, FromField, FromVariant}; +use proc_macro2::{Span, TokenStream}; +use quote::TokenStreamExt; +use syn::{self, AngleBracketedGenericArguments, AssocType, DeriveInput, Field}; +use syn::{GenericArgument, GenericParam, Ident, Path}; +use syn::{PathArguments, PathSegment, QSelf, Type, TypeArray, TypeGroup}; +use syn::{TypeParam, TypeParen, TypePath, TypeSlice, TypeTuple}; +use syn::{Variant, WherePredicate}; +use synstructure::{self, BindStyle, BindingInfo, VariantAst, VariantInfo}; + +/// Given an input type which has some where clauses already, like: +/// +/// struct InputType<T> +/// where +/// T: Zero, +/// { +/// ... +/// } +/// +/// Add the necessary `where` clauses so that the output type of a trait +/// fulfils them. +/// +/// For example: +/// +/// ```ignore +/// <T as ToComputedValue>::ComputedValue: Zero, +/// ``` +/// +/// This needs to run before adding other bounds to the type parameters. +pub fn propagate_clauses_to_output_type( + where_clause: &mut Option<syn::WhereClause>, + generics: &syn::Generics, + trait_path: &Path, + trait_output: &Ident, +) { + let where_clause = match *where_clause { + Some(ref mut clause) => clause, + None => return, + }; + let mut extra_bounds = vec![]; + for pred in &where_clause.predicates { + let ty = match *pred { + syn::WherePredicate::Type(ref ty) => ty, + ref predicate => panic!("Unhanded complex where predicate: {:?}", predicate), + }; + + let path = match ty.bounded_ty { + syn::Type::Path(ref p) => &p.path, + ref ty => panic!("Unhanded complex where type: {:?}", ty), + }; + + assert!( + ty.lifetimes.is_none(), + "Unhanded complex lifetime bound: {:?}", + ty, + ); + + let ident = match path_to_ident(path) { + Some(i) => i, + None => panic!("Unhanded complex where type path: {:?}", path), + }; + + if generics.type_params().any(|param| param.ident == *ident) { + extra_bounds.push(ty.clone()); + } + } + + for bound in extra_bounds { + let ty = bound.bounded_ty; + let bounds = bound.bounds; + where_clause + .predicates + .push(parse_quote!(<#ty as #trait_path>::#trait_output: #bounds)) + } +} + +pub fn add_predicate(where_clause: &mut Option<syn::WhereClause>, pred: WherePredicate) { + where_clause + .get_or_insert(parse_quote!(where)) + .predicates + .push(pred); +} + +pub fn fmap_match<F>(input: &DeriveInput, bind_style: BindStyle, f: F) -> TokenStream +where + F: FnMut(&BindingInfo) -> TokenStream, +{ + fmap2_match(input, bind_style, f, |_| None) +} + +pub fn fmap2_match<F, G>( + input: &DeriveInput, + bind_style: BindStyle, + mut f: F, + mut g: G, +) -> TokenStream +where + F: FnMut(&BindingInfo) -> TokenStream, + G: FnMut(&BindingInfo) -> Option<TokenStream>, +{ + let mut s = synstructure::Structure::new(input); + s.variants_mut().iter_mut().for_each(|v| { + v.bind_with(|_| bind_style); + }); + s.each_variant(|variant| { + let (mapped, mapped_fields) = value(variant, "mapped"); + let fields_pairs = variant.bindings().iter().zip(mapped_fields.iter()); + let mut computations = quote!(); + computations.append_all(fields_pairs.map(|(field, mapped_field)| { + let expr = f(field); + quote! { let #mapped_field = #expr; } + })); + computations.append_all( + mapped_fields + .iter() + .map(|mapped_field| match g(mapped_field) { + Some(expr) => quote! { let #mapped_field = #expr; }, + None => quote!(), + }), + ); + computations.append_all(mapped); + Some(computations) + }) +} + +pub fn fmap_trait_output(input: &DeriveInput, trait_path: &Path, trait_output: &Ident) -> Path { + let segment = PathSegment { + ident: input.ident.clone(), + arguments: PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args: input + .generics + .params + .iter() + .map(|arg| match arg { + &GenericParam::Lifetime(ref data) => { + GenericArgument::Lifetime(data.lifetime.clone()) + }, + &GenericParam::Type(ref data) => { + let ident = &data.ident; + GenericArgument::Type(parse_quote!(<#ident as #trait_path>::#trait_output)) + }, + &GenericParam::Const(ref inner) => { + let ident = &inner.ident; + GenericArgument::Const(parse_quote!(#ident)) + }, + }) + .collect(), + colon2_token: Default::default(), + gt_token: Default::default(), + lt_token: Default::default(), + }), + }; + segment.into() +} + +pub fn map_type_params<F>(ty: &Type, params: &[&TypeParam], self_type: &Path, f: &mut F) -> Type +where + F: FnMut(&Ident) -> Type, +{ + match *ty { + Type::Slice(ref inner) => Type::from(TypeSlice { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }), + Type::Array(ref inner) => { + //ref ty, ref expr) => { + Type::from(TypeArray { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }) + }, + ref ty @ Type::Never(_) => ty.clone(), + Type::Tuple(ref inner) => Type::from(TypeTuple { + elems: inner + .elems + .iter() + .map(|ty| map_type_params(&ty, params, self_type, f)) + .collect(), + ..inner.clone() + }), + Type::Path(TypePath { + qself: None, + ref path, + }) => { + if let Some(ident) = path_to_ident(path) { + if params.iter().any(|ref param| ¶m.ident == ident) { + return f(ident); + } + if ident == "Self" { + return Type::from(TypePath { + qself: None, + path: self_type.clone(), + }); + } + } + Type::from(TypePath { + qself: None, + path: map_type_params_in_path(path, params, self_type, f), + }) + }, + Type::Path(TypePath { + ref qself, + ref path, + }) => Type::from(TypePath { + qself: qself.as_ref().map(|qself| QSelf { + ty: Box::new(map_type_params(&qself.ty, params, self_type, f)), + position: qself.position, + ..qself.clone() + }), + path: map_type_params_in_path(path, params, self_type, f), + }), + Type::Paren(ref inner) => Type::from(TypeParen { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }), + Type::Group(ref inner) => Type::from(TypeGroup { + elem: Box::new(map_type_params(&inner.elem, params, self_type, f)), + ..inner.clone() + }), + ref ty => panic!("type {:?} cannot be mapped yet", ty), + } +} + +fn map_type_params_in_path<F>( + path: &Path, + params: &[&TypeParam], + self_type: &Path, + f: &mut F, +) -> Path +where + F: FnMut(&Ident) -> Type, +{ + Path { + leading_colon: path.leading_colon, + segments: path + .segments + .iter() + .map(|segment| PathSegment { + ident: segment.ident.clone(), + arguments: match segment.arguments { + PathArguments::AngleBracketed(ref data) => { + PathArguments::AngleBracketed(AngleBracketedGenericArguments { + args: data + .args + .iter() + .map(|arg| match arg { + ty @ &GenericArgument::Lifetime(_) => ty.clone(), + &GenericArgument::Type(ref data) => GenericArgument::Type( + map_type_params(data, params, self_type, f), + ), + &GenericArgument::AssocType(ref data) => { + GenericArgument::AssocType(AssocType { + ty: map_type_params(&data.ty, params, self_type, f), + ..data.clone() + }) + }, + ref arg => panic!("arguments {:?} cannot be mapped yet", arg), + }) + .collect(), + ..data.clone() + }) + }, + ref arg @ PathArguments::None => arg.clone(), + ref parameters => panic!("parameters {:?} cannot be mapped yet", parameters), + }, + }) + .collect(), + } +} + +fn path_to_ident(path: &Path) -> Option<&Ident> { + match *path { + Path { + leading_colon: None, + ref segments, + } if segments.len() == 1 => { + if segments[0].arguments.is_empty() { + Some(&segments[0].ident) + } else { + None + } + }, + _ => None, + } +} + +pub fn parse_field_attrs<A>(field: &Field) -> A +where + A: FromField, +{ + match A::from_field(field) { + Ok(attrs) => attrs, + Err(e) => panic!("failed to parse field attributes: {}", e), + } +} + +pub fn parse_input_attrs<A>(input: &DeriveInput) -> A +where + A: FromDeriveInput, +{ + match A::from_derive_input(input) { + Ok(attrs) => attrs, + Err(e) => panic!("failed to parse input attributes: {}", e), + } +} + +pub fn parse_variant_attrs_from_ast<A>(variant: &VariantAst) -> A +where + A: FromVariant, +{ + let v = Variant { + ident: variant.ident.clone(), + attrs: variant.attrs.to_vec(), + fields: variant.fields.clone(), + discriminant: variant.discriminant.clone(), + }; + parse_variant_attrs(&v) +} + +pub fn parse_variant_attrs<A>(variant: &Variant) -> A +where + A: FromVariant, +{ + match A::from_variant(variant) { + Ok(attrs) => attrs, + Err(e) => panic!("failed to parse variant attributes: {}", e), + } +} + +pub fn ref_pattern<'a>( + variant: &'a VariantInfo, + prefix: &str, +) -> (TokenStream, Vec<BindingInfo<'a>>) { + let mut v = variant.clone(); + v.bind_with(|_| BindStyle::Ref); + v.bindings_mut().iter_mut().for_each(|b| { + b.binding = Ident::new(&format!("{}_{}", b.binding, prefix), Span::call_site()) + }); + (v.pat(), v.bindings().to_vec()) +} + +pub fn value<'a>(variant: &'a VariantInfo, prefix: &str) -> (TokenStream, Vec<BindingInfo<'a>>) { + let mut v = variant.clone(); + v.bindings_mut().iter_mut().for_each(|b| { + b.binding = Ident::new(&format!("{}_{}", b.binding, prefix), Span::call_site()) + }); + v.bind_with(|_| BindStyle::Move); + (v.pat(), v.bindings().to_vec()) +} + +/// Transforms "FooBar" to "foo-bar". +/// +/// If the first Camel segment is "Moz", "Webkit", or "Servo", the result string +/// is prepended with "-". +pub fn to_css_identifier(mut camel_case: &str) -> String { + camel_case = camel_case.trim_end_matches('_'); + let mut first = true; + let mut result = String::with_capacity(camel_case.len()); + while let Some(segment) = split_camel_segment(&mut camel_case) { + if first { + match segment { + "Moz" | "Webkit" | "Servo" => first = false, + _ => {}, + } + } + if !first { + result.push('-'); + } + first = false; + result.push_str(&segment.to_lowercase()); + } + result +} + +/// Transforms foo-bar to FOO_BAR. +pub fn to_scream_case(css_case: &str) -> String { + css_case.to_uppercase().replace('-', "_") +} + +/// Given "FooBar", returns "Foo" and sets `camel_case` to "Bar". +fn split_camel_segment<'input>(camel_case: &mut &'input str) -> Option<&'input str> { + let index = match camel_case.chars().next() { + None => return None, + Some(ch) => ch.len_utf8(), + }; + let end_position = camel_case[index..] + .find(char::is_uppercase) + .map_or(camel_case.len(), |pos| index + pos); + let result = &camel_case[..end_position]; + *camel_case = &camel_case[end_position..]; + Some(result) +} diff --git a/servo/components/derive_common/lib.rs b/servo/components/derive_common/lib.rs new file mode 100644 index 0000000000..1441535144 --- /dev/null +++ b/servo/components/derive_common/lib.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +extern crate darling; +extern crate proc_macro2; +#[macro_use] +extern crate quote; +#[macro_use] +extern crate syn; +extern crate synstructure; + +pub mod cg; diff --git a/servo/components/malloc_size_of/Cargo.toml b/servo/components/malloc_size_of/Cargo.toml new file mode 100644 index 0000000000..cd5deaea44 --- /dev/null +++ b/servo/components/malloc_size_of/Cargo.toml @@ -0,0 +1,53 @@ +[package] +name = "malloc_size_of" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MIT/Apache-2.0" +publish = false + +[lib] +path = "lib.rs" + +[features] +servo = [ + "accountable-refcell", + "content-security-policy", + "crossbeam-channel", + "hyper", + "hyper_serde", + "keyboard-types", + "serde", + "serde_bytes", + "string_cache", + "time", + "url", + "uuid", + "webrender_api", + "xml5ever", +] + +[dependencies] +accountable-refcell = { version = "0.2.0", optional = true } +app_units = "0.7" +content-security-policy = { version = "0.4.0", features = ["serde"], optional = true } +crossbeam-channel = { version = "0.4", optional = true } +cssparser = "0.33" +dom = { path = "../../../dom/base/rust" } +euclid = "0.22" +hyper = { version = "0.12", optional = true } +hyper_serde = { version = "0.11", optional = true } +keyboard-types = { version = "0.4.3", optional = true } +selectors = { path = "../selectors" } +serde = { version = "1.0.27", optional = true } +serde_bytes = { version = "0.11", optional = true } +servo_arc = { path = "../servo_arc" } +smallbitvec = "2.3.0" +smallvec = "1.0" +string_cache = { version = "0.8", optional = true } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +time = { version = "0.1.17", optional = true } +url = { version = "2.4", optional = true } +uuid = { version = "0.8", features = ["v4"], optional = true } +void = "1.0.2" +webrender_api = { git = "https://github.com/servo/webrender", optional = true } +xml5ever = { version = "0.16", optional = true } diff --git a/servo/components/malloc_size_of/LICENSE-APACHE b/servo/components/malloc_size_of/LICENSE-APACHE new file mode 100644 index 0000000000..16fe87b06e --- /dev/null +++ b/servo/components/malloc_size_of/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/servo/components/malloc_size_of/LICENSE-MIT b/servo/components/malloc_size_of/LICENSE-MIT new file mode 100644 index 0000000000..31aa79387f --- /dev/null +++ b/servo/components/malloc_size_of/LICENSE-MIT @@ -0,0 +1,23 @@ +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/servo/components/malloc_size_of/lib.rs b/servo/components/malloc_size_of/lib.rs new file mode 100644 index 0000000000..32d7f03bfb --- /dev/null +++ b/servo/components/malloc_size_of/lib.rs @@ -0,0 +1,1003 @@ +// Copyright 2016-2017 The Servo Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! A crate for measuring the heap usage of data structures in a way that +//! integrates with Firefox's memory reporting, particularly the use of +//! mozjemalloc and DMD. In particular, it has the following features. +//! - It isn't bound to a particular heap allocator. +//! - It provides traits for both "shallow" and "deep" measurement, which gives +//! flexibility in the cases where the traits can't be used. +//! - It allows for measuring blocks even when only an interior pointer can be +//! obtained for heap allocations, e.g. `HashSet` and `HashMap`. (This relies +//! on the heap allocator having suitable support, which mozjemalloc has.) +//! - It allows handling of types like `Rc` and `Arc` by providing traits that +//! are different to the ones for non-graph structures. +//! +//! Suggested uses are as follows. +//! - When possible, use the `MallocSizeOf` trait. (Deriving support is +//! provided by the `malloc_size_of_derive` crate.) +//! - If you need an additional synchronization argument, provide a function +//! that is like the standard trait method, but with the extra argument. +//! - If you need multiple measurements for a type, provide a function named +//! `add_size_of` that takes a mutable reference to a struct that contains +//! the multiple measurement fields. +//! - When deep measurement (via `MallocSizeOf`) cannot be implemented for a +//! type, shallow measurement (via `MallocShallowSizeOf`) in combination with +//! iteration can be a useful substitute. +//! - `Rc` and `Arc` are always tricky, which is why `MallocSizeOf` is not (and +//! should not be) implemented for them. +//! - If an `Rc` or `Arc` is known to be a "primary" reference and can always +//! be measured, it should be measured via the `MallocUnconditionalSizeOf` +//! trait. +//! - If an `Rc` or `Arc` should be measured only if it hasn't been seen +//! before, it should be measured via the `MallocConditionalSizeOf` trait. +//! - Using universal function call syntax is a good idea when measuring boxed +//! fields in structs, because it makes it clear that the Box is being +//! measured as well as the thing it points to. E.g. +//! `<Box<_> as MallocSizeOf>::size_of(field, ops)`. +//! +//! Note: WebRender has a reduced fork of this crate, so that we can avoid +//! publishing this crate on crates.io. + +#[cfg(feature = "servo")] +extern crate accountable_refcell; +extern crate app_units; +#[cfg(feature = "servo")] +extern crate content_security_policy; +#[cfg(feature = "servo")] +extern crate crossbeam_channel; +extern crate cssparser; +extern crate euclid; +#[cfg(feature = "servo")] +extern crate hyper; +#[cfg(feature = "servo")] +extern crate hyper_serde; +#[cfg(feature = "servo")] +extern crate keyboard_types; +extern crate selectors; +#[cfg(feature = "servo")] +extern crate serde; +#[cfg(feature = "servo")] +extern crate serde_bytes; +extern crate servo_arc; +extern crate smallbitvec; +extern crate smallvec; +#[cfg(feature = "servo")] +extern crate string_cache; +#[cfg(feature = "servo")] +extern crate time; +#[cfg(feature = "url")] +extern crate url; +#[cfg(feature = "servo")] +extern crate uuid; +extern crate void; +#[cfg(feature = "webrender_api")] +extern crate webrender_api; +#[cfg(feature = "servo")] +extern crate xml5ever; + +#[cfg(feature = "servo")] +use content_security_policy as csp; +#[cfg(feature = "servo")] +use serde_bytes::ByteBuf; +use std::hash::{BuildHasher, Hash}; +use std::mem::size_of; +use std::ops::Range; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_void; +#[cfg(feature = "servo")] +use uuid::Uuid; +use void::Void; + +/// A C function that takes a pointer to a heap allocation and returns its size. +type VoidPtrToSizeFn = unsafe extern "C" fn(ptr: *const c_void) -> usize; + +/// A closure implementing a stateful predicate on pointers. +type VoidPtrToBoolFnMut = dyn FnMut(*const c_void) -> bool; + +/// Operations used when measuring heap usage of data structures. +pub struct MallocSizeOfOps { + /// A function that returns the size of a heap allocation. + size_of_op: VoidPtrToSizeFn, + + /// Like `size_of_op`, but can take an interior pointer. Optional because + /// not all allocators support this operation. If it's not provided, some + /// memory measurements will actually be computed estimates rather than + /// real and accurate measurements. + enclosing_size_of_op: Option<VoidPtrToSizeFn>, + + /// Check if a pointer has been seen before, and remember it for next time. + /// Useful when measuring `Rc`s and `Arc`s. Optional, because many places + /// don't need it. + have_seen_ptr_op: Option<Box<VoidPtrToBoolFnMut>>, +} + +impl MallocSizeOfOps { + pub fn new( + size_of: VoidPtrToSizeFn, + malloc_enclosing_size_of: Option<VoidPtrToSizeFn>, + have_seen_ptr: Option<Box<VoidPtrToBoolFnMut>>, + ) -> Self { + MallocSizeOfOps { + size_of_op: size_of, + enclosing_size_of_op: malloc_enclosing_size_of, + have_seen_ptr_op: have_seen_ptr, + } + } + + /// Check if an allocation is empty. This relies on knowledge of how Rust + /// handles empty allocations, which may change in the future. + fn is_empty<T: ?Sized>(ptr: *const T) -> bool { + // The correct condition is this: + // `ptr as usize <= ::std::mem::align_of::<T>()` + // But we can't call align_of() on a ?Sized T. So we approximate it + // with the following. 256 is large enough that it should always be + // larger than the required alignment, but small enough that it is + // always in the first page of memory and therefore not a legitimate + // address. + return ptr as *const usize as usize <= 256; + } + + /// Call `size_of_op` on `ptr`, first checking that the allocation isn't + /// empty, because some types (such as `Vec`) utilize empty allocations. + pub unsafe fn malloc_size_of<T: ?Sized>(&self, ptr: *const T) -> usize { + if MallocSizeOfOps::is_empty(ptr) { + 0 + } else { + (self.size_of_op)(ptr as *const c_void) + } + } + + /// Is an `enclosing_size_of_op` available? + pub fn has_malloc_enclosing_size_of(&self) -> bool { + self.enclosing_size_of_op.is_some() + } + + /// Call `enclosing_size_of_op`, which must be available, on `ptr`, which + /// must not be empty. + pub unsafe fn malloc_enclosing_size_of<T>(&self, ptr: *const T) -> usize { + assert!(!MallocSizeOfOps::is_empty(ptr)); + (self.enclosing_size_of_op.unwrap())(ptr as *const c_void) + } + + /// Call `have_seen_ptr_op` on `ptr`. + pub fn have_seen_ptr<T>(&mut self, ptr: *const T) -> bool { + let have_seen_ptr_op = self + .have_seen_ptr_op + .as_mut() + .expect("missing have_seen_ptr_op"); + have_seen_ptr_op(ptr as *const c_void) + } +} + +/// Trait for measuring the "deep" heap usage of a data structure. This is the +/// most commonly-used of the traits. +pub trait MallocSizeOf { + /// Measure the heap usage of all descendant heap-allocated structures, but + /// not the space taken up by the value itself. + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// Trait for measuring the "shallow" heap usage of a container. +pub trait MallocShallowSizeOf { + /// Measure the heap usage of immediate heap-allocated descendant + /// structures, but not the space taken up by the value itself. Anything + /// beyond the immediate descendants must be measured separately, using + /// iteration. + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// Like `MallocSizeOf`, but with a different name so it cannot be used +/// accidentally with derive(MallocSizeOf). For use with types like `Rc` and +/// `Arc` when appropriate (e.g. when measuring a "primary" reference). +pub trait MallocUnconditionalSizeOf { + /// Measure the heap usage of all heap-allocated descendant structures, but + /// not the space taken up by the value itself. + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// `MallocUnconditionalSizeOf` combined with `MallocShallowSizeOf`. +pub trait MallocUnconditionalShallowSizeOf { + /// `unconditional_size_of` combined with `shallow_size_of`. + fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// Like `MallocSizeOf`, but only measures if the value hasn't already been +/// measured. For use with types like `Rc` and `Arc` when appropriate (e.g. +/// when there is no "primary" reference). +pub trait MallocConditionalSizeOf { + /// Measure the heap usage of all heap-allocated descendant structures, but + /// not the space taken up by the value itself, and only if that heap usage + /// hasn't already been measured. + fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +/// `MallocConditionalSizeOf` combined with `MallocShallowSizeOf`. +pub trait MallocConditionalShallowSizeOf { + /// `conditional_size_of` combined with `shallow_size_of`. + fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize; +} + +impl MallocSizeOf for String { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } +} + +impl<'a, T: ?Sized> MallocSizeOf for &'a T { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + // Zero makes sense for a non-owning reference. + 0 + } +} + +impl<T: ?Sized> MallocShallowSizeOf for Box<T> { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(&**self) } + } +} + +impl<T: MallocSizeOf + ?Sized> MallocSizeOf for Box<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.shallow_size_of(ops) + (**self).size_of(ops) + } +} + +impl MallocSizeOf for () { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +impl<T1, T2> MallocSizeOf for (T1, T2) +where + T1: MallocSizeOf, + T2: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + self.1.size_of(ops) + } +} + +impl<T1, T2, T3> MallocSizeOf for (T1, T2, T3) +where + T1: MallocSizeOf, + T2: MallocSizeOf, + T3: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) + } +} + +impl<T1, T2, T3, T4> MallocSizeOf for (T1, T2, T3, T4) +where + T1: MallocSizeOf, + T2: MallocSizeOf, + T3: MallocSizeOf, + T4: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + self.1.size_of(ops) + self.2.size_of(ops) + self.3.size_of(ops) + } +} + +impl<T: MallocSizeOf> MallocSizeOf for Option<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if let Some(val) = self.as_ref() { + val.size_of(ops) + } else { + 0 + } + } +} + +impl<T: MallocSizeOf, E: MallocSizeOf> MallocSizeOf for Result<T, E> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + Ok(ref x) => x.size_of(ops), + Err(ref e) => e.size_of(ops), + } + } +} + +impl<T: MallocSizeOf + Copy> MallocSizeOf for std::cell::Cell<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.get().size_of(ops) + } +} + +impl<T: MallocSizeOf> MallocSizeOf for std::cell::RefCell<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.borrow().size_of(ops) + } +} + +impl<'a, B: ?Sized + ToOwned> MallocSizeOf for std::borrow::Cow<'a, B> +where + B::Owned: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + std::borrow::Cow::Borrowed(_) => 0, + std::borrow::Cow::Owned(ref b) => b.size_of(ops), + } + } +} + +impl<T: MallocSizeOf> MallocSizeOf for [T] { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +#[cfg(feature = "servo")] +impl MallocShallowSizeOf for ByteBuf { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } +} + +#[cfg(feature = "servo")] +impl MallocSizeOf for ByteBuf { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl<T> MallocShallowSizeOf for Vec<T> { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } +} + +impl<T: MallocSizeOf> MallocSizeOf for Vec<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl<T> MallocShallowSizeOf for std::collections::VecDeque<T> { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.has_malloc_enclosing_size_of() { + if let Some(front) = self.front() { + // The front element is an interior pointer. + unsafe { ops.malloc_enclosing_size_of(&*front) } + } else { + // This assumes that no memory is allocated when the VecDeque is empty. + 0 + } + } else { + // An estimate. + self.capacity() * size_of::<T>() + } + } +} + +impl<T: MallocSizeOf> MallocSizeOf for std::collections::VecDeque<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl<A: smallvec::Array> MallocShallowSizeOf for smallvec::SmallVec<A> { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if self.spilled() { + unsafe { ops.malloc_size_of(self.as_ptr()) } + } else { + 0 + } + } +} + +impl<A> MallocSizeOf for smallvec::SmallVec<A> +where + A: smallvec::Array, + A::Item: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +impl<T> MallocShallowSizeOf for thin_vec::ThinVec<T> { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if self.capacity() == 0 { + // If it's the singleton we might not be a heap pointer. + return 0; + } + + assert_eq!( + std::mem::size_of::<Self>(), + std::mem::size_of::<*const ()>() + ); + unsafe { ops.malloc_size_of(*(self as *const Self as *const *const ())) } + } +} + +impl<T: MallocSizeOf> MallocSizeOf for thin_vec::ThinVec<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for elem in self.iter() { + n += elem.size_of(ops); + } + n + } +} + +macro_rules! malloc_size_of_hash_set { + ($ty:ty) => { + impl<T, S> MallocShallowSizeOf for $ty + where + T: Eq + Hash, + S: BuildHasher, + { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.has_malloc_enclosing_size_of() { + // The first value from the iterator gives us an interior pointer. + // `ops.malloc_enclosing_size_of()` then gives us the storage size. + // This assumes that the `HashSet`'s contents (values and hashes) + // are all stored in a single contiguous heap allocation. + self.iter() + .next() + .map_or(0, |t| unsafe { ops.malloc_enclosing_size_of(t) }) + } else { + // An estimate. + self.capacity() * (size_of::<T>() + size_of::<usize>()) + } + } + } + + impl<T, S> MallocSizeOf for $ty + where + T: Eq + Hash + MallocSizeOf, + S: BuildHasher, + { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for t in self.iter() { + n += t.size_of(ops); + } + n + } + } + }; +} + +malloc_size_of_hash_set!(std::collections::HashSet<T, S>); + +macro_rules! malloc_size_of_hash_map { + ($ty:ty) => { + impl<K, V, S> MallocShallowSizeOf for $ty + where + K: Eq + Hash, + S: BuildHasher, + { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + // See the implementation for std::collections::HashSet for details. + if ops.has_malloc_enclosing_size_of() { + self.values() + .next() + .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) }) + } else { + self.capacity() * (size_of::<V>() + size_of::<K>() + size_of::<usize>()) + } + } + } + + impl<K, V, S> MallocSizeOf for $ty + where + K: Eq + Hash + MallocSizeOf, + V: MallocSizeOf, + S: BuildHasher, + { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for (k, v) in self.iter() { + n += k.size_of(ops); + n += v.size_of(ops); + } + n + } + } + }; +} + +malloc_size_of_hash_map!(std::collections::HashMap<K, V, S>); + +impl<K, V> MallocShallowSizeOf for std::collections::BTreeMap<K, V> +where + K: Eq + Hash, +{ + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.has_malloc_enclosing_size_of() { + self.values() + .next() + .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) }) + } else { + self.len() * (size_of::<V>() + size_of::<K>() + size_of::<usize>()) + } + } +} + +impl<K, V> MallocSizeOf for std::collections::BTreeMap<K, V> +where + K: Eq + Hash + MallocSizeOf, + V: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.shallow_size_of(ops); + for (k, v) in self.iter() { + n += k.size_of(ops); + n += v.size_of(ops); + } + n + } +} + +// PhantomData is always 0. +impl<T> MallocSizeOf for std::marker::PhantomData<T> { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +// XXX: we don't want MallocSizeOf to be defined for Rc and Arc. If negative +// trait bounds are ever allowed, this code should be uncommented. +// (We do have a compile-fail test for this: +// rc_arc_must_not_derive_malloc_size_of.rs) +//impl<T> !MallocSizeOf for Arc<T> { } +//impl<T> !MallocShallowSizeOf for Arc<T> { } + +impl<T> MallocUnconditionalShallowSizeOf for servo_arc::Arc<T> { + fn unconditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.heap_ptr()) } + } +} + +impl<T: MallocSizeOf> MallocUnconditionalSizeOf for servo_arc::Arc<T> { + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.unconditional_shallow_size_of(ops) + (**self).size_of(ops) + } +} + +impl<T> MallocConditionalShallowSizeOf for servo_arc::Arc<T> { + fn conditional_shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.have_seen_ptr(self.heap_ptr()) { + 0 + } else { + self.unconditional_shallow_size_of(ops) + } + } +} + +impl<T: MallocSizeOf> MallocConditionalSizeOf for servo_arc::Arc<T> { + fn conditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if ops.have_seen_ptr(self.heap_ptr()) { + 0 + } else { + self.unconditional_size_of(ops) + } + } +} + +/// If a mutex is stored directly as a member of a data type that is being measured, +/// it is the unique owner of its contents and deserves to be measured. +/// +/// If a mutex is stored inside of an Arc value as a member of a data type that is being measured, +/// the Arc will not be automatically measured so there is no risk of overcounting the mutex's +/// contents. +impl<T: MallocSizeOf> MallocSizeOf for std::sync::Mutex<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + (*self.lock().unwrap()).size_of(ops) + } +} + +impl MallocSizeOf for smallbitvec::SmallBitVec { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + if let Some(ptr) = self.heap_ptr() { + unsafe { ops.malloc_size_of(ptr) } + } else { + 0 + } + } +} + +impl<T: MallocSizeOf, Unit> MallocSizeOf for euclid::Length<T, Unit> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + } +} + +impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Scale<T, Src, Dst> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + } +} + +impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Point2D<T, U> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.x.size_of(ops) + self.y.size_of(ops) + } +} + +impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Rect<T, U> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.origin.size_of(ops) + self.size.size_of(ops) + } +} + +impl<T: MallocSizeOf, U> MallocSizeOf for euclid::SideOffsets2D<T, U> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.top.size_of(ops) + + self.right.size_of(ops) + + self.bottom.size_of(ops) + + self.left.size_of(ops) + } +} + +impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Size2D<T, U> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.width.size_of(ops) + self.height.size_of(ops) + } +} + +impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Transform2D<T, Src, Dst> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.m11.size_of(ops) + + self.m12.size_of(ops) + + self.m21.size_of(ops) + + self.m22.size_of(ops) + + self.m31.size_of(ops) + + self.m32.size_of(ops) + } +} + +impl<T: MallocSizeOf, Src, Dst> MallocSizeOf for euclid::Transform3D<T, Src, Dst> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.m11.size_of(ops) + + self.m12.size_of(ops) + + self.m13.size_of(ops) + + self.m14.size_of(ops) + + self.m21.size_of(ops) + + self.m22.size_of(ops) + + self.m23.size_of(ops) + + self.m24.size_of(ops) + + self.m31.size_of(ops) + + self.m32.size_of(ops) + + self.m33.size_of(ops) + + self.m34.size_of(ops) + + self.m41.size_of(ops) + + self.m42.size_of(ops) + + self.m43.size_of(ops) + + self.m44.size_of(ops) + } +} + +impl<T: MallocSizeOf, U> MallocSizeOf for euclid::Vector2D<T, U> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.x.size_of(ops) + self.y.size_of(ops) + } +} + +impl MallocSizeOf for selectors::parser::AncestorHashes { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let selectors::parser::AncestorHashes { ref packed_hashes } = *self; + packed_hashes.size_of(ops) + } +} + +impl<Impl: selectors::parser::SelectorImpl> MallocUnconditionalSizeOf + for selectors::parser::Selector<Impl> +where + Impl::NonTSPseudoClass: MallocSizeOf, + Impl::PseudoElement: MallocSizeOf, +{ + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + + // It's OK to measure this ThinArc directly because it's the + // "primary" reference. (The secondary references are on the + // Stylist.) + n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) }; + for component in self.iter_raw_match_order() { + n += component.size_of(ops); + } + + n + } +} + +impl<Impl: selectors::parser::SelectorImpl> MallocUnconditionalSizeOf + for selectors::parser::SelectorList<Impl> +where + Impl::NonTSPseudoClass: MallocSizeOf, + Impl::PseudoElement: MallocSizeOf, +{ + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + + // It's OK to measure this ThinArc directly because it's the "primary" reference. (The + // secondary references are on the Stylist.) + n += unsafe { ops.malloc_size_of(self.thin_arc_heap_ptr()) }; + if self.len() > 1 { + for selector in self.slice().iter() { + n += selector.size_of(ops); + } + } + n + } +} + +impl<Impl: selectors::parser::SelectorImpl> MallocUnconditionalSizeOf + for selectors::parser::Component<Impl> +where + Impl::NonTSPseudoClass: MallocSizeOf, + Impl::PseudoElement: MallocSizeOf, +{ + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + use selectors::parser::Component; + + match self { + Component::AttributeOther(ref attr_selector) => attr_selector.size_of(ops), + Component::Negation(ref components) => components.unconditional_size_of(ops), + Component::NonTSPseudoClass(ref pseudo) => (*pseudo).size_of(ops), + Component::Slotted(ref selector) | Component::Host(Some(ref selector)) => { + selector.unconditional_size_of(ops) + }, + Component::Is(ref list) | Component::Where(ref list) => list.unconditional_size_of(ops), + Component::Has(ref relative_selectors) => relative_selectors.size_of(ops), + Component::NthOf(ref nth_of_data) => nth_of_data.size_of(ops), + Component::PseudoElement(ref pseudo) => (*pseudo).size_of(ops), + Component::Combinator(..) | + Component::ExplicitAnyNamespace | + Component::ExplicitNoNamespace | + Component::DefaultNamespace(..) | + Component::Namespace(..) | + Component::ExplicitUniversalType | + Component::LocalName(..) | + Component::ID(..) | + Component::Part(..) | + Component::Class(..) | + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeInNoNamespace { .. } | + Component::Root | + Component::Empty | + Component::Scope | + Component::ParentSelector | + Component::Nth(..) | + Component::Host(None) | + Component::RelativeSelectorAnchor | + Component::Invalid(..) => 0, + } + } +} + +impl<Impl: selectors::parser::SelectorImpl> MallocSizeOf + for selectors::attr::AttrSelectorWithOptionalNamespace<Impl> +{ + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +impl MallocSizeOf for Void { + #[inline] + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + void::unreachable(*self) + } +} + +#[cfg(feature = "servo")] +impl<Static: string_cache::StaticAtomSet> MallocSizeOf for string_cache::Atom<Static> { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +/// For use on types where size_of() returns 0. +#[macro_export] +macro_rules! malloc_size_of_is_0( + ($($ty:ty),+) => ( + $( + impl $crate::MallocSizeOf for $ty { + #[inline(always)] + fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize { + 0 + } + } + )+ + ); + ($($ty:ident<$($gen:ident),+>),+) => ( + $( + impl<$($gen: $crate::MallocSizeOf),+> $crate::MallocSizeOf for $ty<$($gen),+> { + #[inline(always)] + fn size_of(&self, _: &mut $crate::MallocSizeOfOps) -> usize { + 0 + } + } + )+ + ); +); + +malloc_size_of_is_0!(bool, char, str); +malloc_size_of_is_0!(u8, u16, u32, u64, u128, usize); +malloc_size_of_is_0!(i8, i16, i32, i64, i128, isize); +malloc_size_of_is_0!(f32, f64); + +malloc_size_of_is_0!(std::sync::atomic::AtomicBool); +malloc_size_of_is_0!(std::sync::atomic::AtomicIsize); +malloc_size_of_is_0!(std::sync::atomic::AtomicUsize); +malloc_size_of_is_0!(std::num::NonZeroUsize); + +malloc_size_of_is_0!(Range<u8>, Range<u16>, Range<u32>, Range<u64>, Range<usize>); +malloc_size_of_is_0!(Range<i8>, Range<i16>, Range<i32>, Range<i64>, Range<isize>); +malloc_size_of_is_0!(Range<f32>, Range<f64>); + +malloc_size_of_is_0!(app_units::Au); + +malloc_size_of_is_0!(cssparser::TokenSerializationType, cssparser::SourceLocation, cssparser::SourcePosition); + +malloc_size_of_is_0!(dom::ElementState, dom::DocumentState); + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(csp::Destination); + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(Uuid); + +#[cfg(feature = "url")] +impl MallocSizeOf for url::Host { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + url::Host::Domain(ref s) => s.size_of(ops), + _ => 0, + } + } +} +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::BorderRadius); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::BorderStyle); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::BoxShadowClipMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ColorF); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ComplexClipRegion); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ExtendMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::FilterOp); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ExternalScrollId); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::FontInstanceKey); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::GradientStop); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::GlyphInstance); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::NinePatchBorder); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ImageKey); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::ImageRendering); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::LineStyle); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::MixBlendMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::NormalBorder); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::RepeatMode); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::StickyOffsetBounds); +#[cfg(feature = "webrender_api")] +malloc_size_of_is_0!(webrender_api::TransformStyle); + +#[cfg(feature = "servo")] +impl MallocSizeOf for keyboard_types::Key { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match self { + keyboard_types::Key::Character(ref s) => s.size_of(ops), + _ => 0, + } + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(keyboard_types::Modifiers); + +#[cfg(feature = "servo")] +impl MallocSizeOf for xml5ever::QualName { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.prefix.size_of(ops) + self.ns.size_of(ops) + self.local.size_of(ops) + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(time::Duration); +#[cfg(feature = "servo")] +malloc_size_of_is_0!(time::Tm); + +#[cfg(feature = "servo")] +impl<T> MallocSizeOf for hyper_serde::Serde<T> +where + for<'de> hyper_serde::De<T>: serde::Deserialize<'de>, + for<'a> hyper_serde::Ser<'a, T>: serde::Serialize, + T: MallocSizeOf, +{ + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.0.size_of(ops) + } +} + +// Placeholder for unique case where internals of Sender cannot be measured. +// malloc size of is 0 macro complains about type supplied! +#[cfg(feature = "servo")] +impl<T> MallocSizeOf for crossbeam_channel::Sender<T> { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +#[cfg(feature = "servo")] +impl MallocSizeOf for hyper::StatusCode { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + 0 + } +} + +/// Measurable that defers to inner value and used to verify MallocSizeOf implementation in a +/// struct. +#[derive(Clone)] +pub struct Measurable<T: MallocSizeOf>(pub T); + +impl<T: MallocSizeOf> Deref for Measurable<T> { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl<T: MallocSizeOf> DerefMut for Measurable<T> { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +#[cfg(feature = "servo")] +impl<T: MallocSizeOf> MallocSizeOf for accountable_refcell::RefCell<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.borrow().size_of(ops) + } +} diff --git a/servo/components/selectors/CHANGES.md b/servo/components/selectors/CHANGES.md new file mode 100644 index 0000000000..b1e9511dca --- /dev/null +++ b/servo/components/selectors/CHANGES.md @@ -0,0 +1 @@ +- `parser.rs` no longer wraps values in quotes (`"..."`) but expects their `to_css` impl to already wrap it ([Gecko Bug 1854809](https://bugzilla.mozilla.org/show_bug.cgi?id=1854809)) diff --git a/servo/components/selectors/Cargo.toml b/servo/components/selectors/Cargo.toml new file mode 100644 index 0000000000..88360d7dd2 --- /dev/null +++ b/servo/components/selectors/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "selectors" +version = "0.22.0" +authors = ["The Servo Project Developers"] +documentation = "https://docs.rs/selectors/" +description = "CSS Selectors matching for Rust" +repository = "https://github.com/servo/servo" +readme = "README.md" +keywords = ["css", "selectors"] +license = "MPL-2.0" +build = "build.rs" + +[lib] +name = "selectors" +path = "lib.rs" + +[features] +bench = [] + +[dependencies] +bitflags = "2" +cssparser = "0.33" +derive_more = { version = "0.99", default-features = false, features = ["add", "add_assign"] } +fxhash = "0.2" +log = "0.4" +phf = "0.11" +precomputed-hash = "0.1" +servo_arc = { version = "0.1", path = "../servo_arc" } +smallvec = "1.0" +to_shmem = { path = "../to_shmem" } +to_shmem_derive = { path = "../to_shmem_derive" } +new_debug_unreachable = "1" + +[build-dependencies] +phf_codegen = "0.11" diff --git a/servo/components/selectors/README.md b/servo/components/selectors/README.md new file mode 100644 index 0000000000..dac4a7ff91 --- /dev/null +++ b/servo/components/selectors/README.md @@ -0,0 +1,25 @@ +rust-selectors +============== + +* [![Build Status](https://travis-ci.com/servo/rust-selectors.svg?branch=master)]( + https://travis-ci.com/servo/rust-selectors) +* [Documentation](https://docs.rs/selectors/) +* [crates.io](https://crates.io/crates/selectors) + +CSS Selectors library for Rust. +Includes parsing and serilization of selectors, +as well as matching against a generic tree of elements. +Pseudo-elements and most pseudo-classes are generic as well. + +**Warning:** breaking changes are made to this library fairly frequently +(13 times in 2016, for example). +However you can use this crate without updating it that often, +old versions stay available on crates.io and Cargo will only automatically update +to versions that are numbered as compatible. + +To see how to use this library with your own tree representation, +see [Kuchiki’s `src/select.rs`](https://github.com/kuchiki-rs/kuchiki/blob/master/src/select.rs). +(Note however that Kuchiki is not always up to date with the latest rust-selectors version, +so that code may need to be tweaked.) +If you don’t already have a tree data structure, +consider using [Kuchiki](https://github.com/kuchiki-rs/kuchiki) itself. diff --git a/servo/components/selectors/attr.rs b/servo/components/selectors/attr.rs new file mode 100644 index 0000000000..fee2962237 --- /dev/null +++ b/servo/components/selectors/attr.rs @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::parser::SelectorImpl; +use cssparser::ToCss; +use std::fmt; + +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct AttrSelectorWithOptionalNamespace<Impl: SelectorImpl> { + #[shmem(field_bound)] + pub namespace: Option<NamespaceConstraint<(Impl::NamespacePrefix, Impl::NamespaceUrl)>>, + #[shmem(field_bound)] + pub local_name: Impl::LocalName, + pub local_name_lower: Impl::LocalName, + #[shmem(field_bound)] + pub operation: ParsedAttrSelectorOperation<Impl::AttrValue>, +} + +impl<Impl: SelectorImpl> AttrSelectorWithOptionalNamespace<Impl> { + pub fn namespace(&self) -> Option<NamespaceConstraint<&Impl::NamespaceUrl>> { + self.namespace.as_ref().map(|ns| match ns { + NamespaceConstraint::Any => NamespaceConstraint::Any, + NamespaceConstraint::Specific((_, ref url)) => NamespaceConstraint::Specific(url), + }) + } +} + +#[derive(Clone, Eq, PartialEq, ToShmem)] +pub enum NamespaceConstraint<NamespaceUrl> { + Any, + + /// Empty string for no namespace + Specific(NamespaceUrl), +} + +#[derive(Clone, Eq, PartialEq, ToShmem)] +pub enum ParsedAttrSelectorOperation<AttrValue> { + Exists, + WithValue { + operator: AttrSelectorOperator, + case_sensitivity: ParsedCaseSensitivity, + value: AttrValue, + }, +} + +#[derive(Clone, Eq, PartialEq)] +pub enum AttrSelectorOperation<AttrValue> { + Exists, + WithValue { + operator: AttrSelectorOperator, + case_sensitivity: CaseSensitivity, + value: AttrValue, + }, +} + +impl<AttrValue> AttrSelectorOperation<AttrValue> { + pub fn eval_str(&self, element_attr_value: &str) -> bool + where + AttrValue: AsRef<str>, + { + match *self { + AttrSelectorOperation::Exists => true, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity, + ref value, + } => operator.eval_str(element_attr_value, value.as_ref(), case_sensitivity), + } + } +} + +#[derive(Clone, Copy, Eq, PartialEq, ToShmem)] +pub enum AttrSelectorOperator { + Equal, + Includes, + DashMatch, + Prefix, + Substring, + Suffix, +} + +impl ToCss for AttrSelectorOperator { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // https://drafts.csswg.org/cssom/#serializing-selectors + // See "attribute selector". + dest.write_str(match *self { + AttrSelectorOperator::Equal => "=", + AttrSelectorOperator::Includes => "~=", + AttrSelectorOperator::DashMatch => "|=", + AttrSelectorOperator::Prefix => "^=", + AttrSelectorOperator::Substring => "*=", + AttrSelectorOperator::Suffix => "$=", + }) + } +} + +impl AttrSelectorOperator { + pub fn eval_str( + self, + element_attr_value: &str, + attr_selector_value: &str, + case_sensitivity: CaseSensitivity, + ) -> bool { + let e = element_attr_value.as_bytes(); + let s = attr_selector_value.as_bytes(); + let case = case_sensitivity; + match self { + AttrSelectorOperator::Equal => case.eq(e, s), + AttrSelectorOperator::Prefix => e.len() >= s.len() && case.eq(&e[..s.len()], s), + AttrSelectorOperator::Suffix => { + e.len() >= s.len() && case.eq(&e[(e.len() - s.len())..], s) + }, + AttrSelectorOperator::Substring => { + case.contains(element_attr_value, attr_selector_value) + }, + AttrSelectorOperator::Includes => element_attr_value + .split(SELECTOR_WHITESPACE) + .any(|part| case.eq(part.as_bytes(), s)), + AttrSelectorOperator::DashMatch => { + case.eq(e, s) || (e.get(s.len()) == Some(&b'-') && case.eq(&e[..s.len()], s)) + }, + } + } +} + +/// The definition of whitespace per CSS Selectors Level 3 § 4. +pub static SELECTOR_WHITESPACE: &[char] = &[' ', '\t', '\n', '\r', '\x0C']; + +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +pub enum ParsedCaseSensitivity { + /// 's' was specified. + ExplicitCaseSensitive, + /// 'i' was specified. + AsciiCaseInsensitive, + /// No flags were specified and HTML says this is a case-sensitive attribute. + CaseSensitive, + /// No flags were specified and HTML says this is a case-insensitive attribute. + AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CaseSensitivity { + CaseSensitive, + AsciiCaseInsensitive, +} + +impl CaseSensitivity { + pub fn eq(self, a: &[u8], b: &[u8]) -> bool { + match self { + CaseSensitivity::CaseSensitive => a == b, + CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b), + } + } + + pub fn contains(self, haystack: &str, needle: &str) -> bool { + match self { + CaseSensitivity::CaseSensitive => haystack.contains(needle), + CaseSensitivity::AsciiCaseInsensitive => { + if let Some((&n_first_byte, n_rest)) = needle.as_bytes().split_first() { + haystack.bytes().enumerate().any(|(i, byte)| { + if !byte.eq_ignore_ascii_case(&n_first_byte) { + return false; + } + let after_this_byte = &haystack.as_bytes()[i + 1..]; + match after_this_byte.get(..n_rest.len()) { + None => false, + Some(haystack_slice) => haystack_slice.eq_ignore_ascii_case(n_rest), + } + }) + } else { + // any_str.contains("") == true, + // though these cases should be handled with *NeverMatches and never go here. + true + } + }, + } + } +} diff --git a/servo/components/selectors/bloom.rs b/servo/components/selectors/bloom.rs new file mode 100644 index 0000000000..98461d1ba2 --- /dev/null +++ b/servo/components/selectors/bloom.rs @@ -0,0 +1,422 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Counting and non-counting Bloom filters tuned for use as ancestor filters +//! for selector matching. + +use std::fmt::{self, Debug}; + +// The top 8 bits of the 32-bit hash value are not used by the bloom filter. +// Consumers may rely on this to pack hashes more efficiently. +pub const BLOOM_HASH_MASK: u32 = 0x00ffffff; +const KEY_SIZE: usize = 12; + +const ARRAY_SIZE: usize = 1 << KEY_SIZE; +const KEY_MASK: u32 = (1 << KEY_SIZE) - 1; + +/// A counting Bloom filter with 8-bit counters. +pub type BloomFilter = CountingBloomFilter<BloomStorageU8>; + +/// A counting Bloom filter with parameterized storage to handle +/// counters of different sizes. For now we assume that having two hash +/// functions is enough, but we may revisit that decision later. +/// +/// The filter uses an array with 2**KeySize entries. +/// +/// Assuming a well-distributed hash function, a Bloom filter with +/// array size M containing N elements and +/// using k hash function has expected false positive rate exactly +/// +/// $ (1 - (1 - 1/M)^{kN})^k $ +/// +/// because each array slot has a +/// +/// $ (1 - 1/M)^{kN} $ +/// +/// chance of being 0, and the expected false positive rate is the +/// probability that all of the k hash functions will hit a nonzero +/// slot. +/// +/// For reasonable assumptions (M large, kN large, which should both +/// hold if we're worried about false positives) about M and kN this +/// becomes approximately +/// +/// $$ (1 - \exp(-kN/M))^k $$ +/// +/// For our special case of k == 2, that's $(1 - \exp(-2N/M))^2$, +/// or in other words +/// +/// $$ N/M = -0.5 * \ln(1 - \sqrt(r)) $$ +/// +/// where r is the false positive rate. This can be used to compute +/// the desired KeySize for a given load N and false positive rate r. +/// +/// If N/M is assumed small, then the false positive rate can +/// further be approximated as 4*N^2/M^2. So increasing KeySize by +/// 1, which doubles M, reduces the false positive rate by about a +/// factor of 4, and a false positive rate of 1% corresponds to +/// about M/N == 20. +/// +/// What this means in practice is that for a few hundred keys using a +/// KeySize of 12 gives false positive rates on the order of 0.25-4%. +/// +/// Similarly, using a KeySize of 10 would lead to a 4% false +/// positive rate for N == 100 and to quite bad false positive +/// rates for larger N. +#[derive(Clone, Default)] +pub struct CountingBloomFilter<S> +where + S: BloomStorage, +{ + storage: S, +} + +impl<S> CountingBloomFilter<S> +where + S: BloomStorage, +{ + /// Creates a new bloom filter. + #[inline] + pub fn new() -> Self { + Default::default() + } + + #[inline] + pub fn clear(&mut self) { + self.storage = Default::default(); + } + + // Slow linear accessor to make sure the bloom filter is zeroed. This should + // never be used in release builds. + #[cfg(debug_assertions)] + pub fn is_zeroed(&self) -> bool { + self.storage.is_zeroed() + } + + #[cfg(not(debug_assertions))] + pub fn is_zeroed(&self) -> bool { + unreachable!() + } + + /// Inserts an item with a particular hash into the bloom filter. + #[inline] + pub fn insert_hash(&mut self, hash: u32) { + self.storage.adjust_first_slot(hash, true); + self.storage.adjust_second_slot(hash, true); + } + + /// Removes an item with a particular hash from the bloom filter. + #[inline] + pub fn remove_hash(&mut self, hash: u32) { + self.storage.adjust_first_slot(hash, false); + self.storage.adjust_second_slot(hash, false); + } + + /// Check whether the filter might contain an item with the given hash. + /// This can sometimes return true even if the item is not in the filter, + /// but will never return false for items that are actually in the filter. + #[inline] + pub fn might_contain_hash(&self, hash: u32) -> bool { + !self.storage.first_slot_is_empty(hash) && !self.storage.second_slot_is_empty(hash) + } +} + +impl<S> Debug for CountingBloomFilter<S> +where + S: BloomStorage, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut slots_used = 0; + for i in 0..ARRAY_SIZE { + if !self.storage.slot_is_empty(i) { + slots_used += 1; + } + } + write!(f, "BloomFilter({}/{})", slots_used, ARRAY_SIZE) + } +} + +pub trait BloomStorage: Clone + Default { + fn slot_is_empty(&self, index: usize) -> bool; + fn adjust_slot(&mut self, index: usize, increment: bool); + fn is_zeroed(&self) -> bool; + + #[inline] + fn first_slot_is_empty(&self, hash: u32) -> bool { + self.slot_is_empty(Self::first_slot_index(hash)) + } + + #[inline] + fn second_slot_is_empty(&self, hash: u32) -> bool { + self.slot_is_empty(Self::second_slot_index(hash)) + } + + #[inline] + fn adjust_first_slot(&mut self, hash: u32, increment: bool) { + self.adjust_slot(Self::first_slot_index(hash), increment) + } + + #[inline] + fn adjust_second_slot(&mut self, hash: u32, increment: bool) { + self.adjust_slot(Self::second_slot_index(hash), increment) + } + + #[inline] + fn first_slot_index(hash: u32) -> usize { + hash1(hash) as usize + } + + #[inline] + fn second_slot_index(hash: u32) -> usize { + hash2(hash) as usize + } +} + +/// Storage class for a CountingBloomFilter that has 8-bit counters. +pub struct BloomStorageU8 { + counters: [u8; ARRAY_SIZE], +} + +impl BloomStorage for BloomStorageU8 { + #[inline] + fn adjust_slot(&mut self, index: usize, increment: bool) { + let slot = &mut self.counters[index]; + if *slot != 0xff { + // full + if increment { + *slot += 1; + } else { + *slot -= 1; + } + } + } + + #[inline] + fn slot_is_empty(&self, index: usize) -> bool { + self.counters[index] == 0 + } + + #[inline] + fn is_zeroed(&self) -> bool { + self.counters.iter().all(|x| *x == 0) + } +} + +impl Default for BloomStorageU8 { + fn default() -> Self { + BloomStorageU8 { + counters: [0; ARRAY_SIZE], + } + } +} + +impl Clone for BloomStorageU8 { + fn clone(&self) -> Self { + BloomStorageU8 { + counters: self.counters, + } + } +} + +/// Storage class for a CountingBloomFilter that has 1-bit counters. +pub struct BloomStorageBool { + counters: [u8; ARRAY_SIZE / 8], +} + +impl BloomStorage for BloomStorageBool { + #[inline] + fn adjust_slot(&mut self, index: usize, increment: bool) { + let bit = 1 << (index % 8); + let byte = &mut self.counters[index / 8]; + + // Since we have only one bit for storage, decrementing it + // should never do anything. Assert against an accidental + // decrementing of a bit that was never set. + assert!( + increment || (*byte & bit) != 0, + "should not decrement if slot is already false" + ); + + if increment { + *byte |= bit; + } + } + + #[inline] + fn slot_is_empty(&self, index: usize) -> bool { + let bit = 1 << (index % 8); + (self.counters[index / 8] & bit) == 0 + } + + #[inline] + fn is_zeroed(&self) -> bool { + self.counters.iter().all(|x| *x == 0) + } +} + +impl Default for BloomStorageBool { + fn default() -> Self { + BloomStorageBool { + counters: [0; ARRAY_SIZE / 8], + } + } +} + +impl Clone for BloomStorageBool { + fn clone(&self) -> Self { + BloomStorageBool { + counters: self.counters, + } + } +} + +#[inline] +fn hash1(hash: u32) -> u32 { + hash & KEY_MASK +} + +#[inline] +fn hash2(hash: u32) -> u32 { + (hash >> KEY_SIZE) & KEY_MASK +} + +#[test] +fn create_and_insert_some_stuff() { + use fxhash::FxHasher; + use std::hash::{Hash, Hasher}; + use std::mem::transmute; + + fn hash_as_str(i: usize) -> u32 { + let mut hasher = FxHasher::default(); + let s = i.to_string(); + s.hash(&mut hasher); + let hash: u64 = hasher.finish(); + (hash >> 32) as u32 ^ (hash as u32) + } + + let mut bf = BloomFilter::new(); + + // Statically assert that ARRAY_SIZE is a multiple of 8, which + // BloomStorageBool relies on. + unsafe { + transmute::<[u8; ARRAY_SIZE % 8], [u8; 0]>([]); + } + + for i in 0_usize..1000 { + bf.insert_hash(hash_as_str(i)); + } + + for i in 0_usize..1000 { + assert!(bf.might_contain_hash(hash_as_str(i))); + } + + let false_positives = (1001_usize..2000) + .filter(|i| bf.might_contain_hash(hash_as_str(*i))) + .count(); + + assert!(false_positives < 190, "{} is not < 190", false_positives); // 19%. + + for i in 0_usize..100 { + bf.remove_hash(hash_as_str(i)); + } + + for i in 100_usize..1000 { + assert!(bf.might_contain_hash(hash_as_str(i))); + } + + let false_positives = (0_usize..100) + .filter(|i| bf.might_contain_hash(hash_as_str(*i))) + .count(); + + assert!(false_positives < 20, "{} is not < 20", false_positives); // 20%. + + bf.clear(); + + for i in 0_usize..2000 { + assert!(!bf.might_contain_hash(hash_as_str(i))); + } +} + +#[cfg(feature = "bench")] +#[cfg(test)] +mod bench { + extern crate test; + use super::BloomFilter; + + #[derive(Default)] + struct HashGenerator(u32); + + impl HashGenerator { + fn next(&mut self) -> u32 { + // Each hash is split into two twelve-bit segments, which are used + // as an index into an array. We increment each by 64 so that we + // hit the next cache line, and then another 1 so that our wrapping + // behavior leads us to different entries each time. + // + // Trying to simulate cold caches is rather difficult with the cargo + // benchmarking setup, so it may all be moot depending on the number + // of iterations that end up being run. But we might as well. + self.0 += (65) + (65 << super::KEY_SIZE); + self.0 + } + } + + #[bench] + fn create_insert_1000_remove_100_lookup_100(b: &mut test::Bencher) { + b.iter(|| { + let mut gen1 = HashGenerator::default(); + let mut gen2 = HashGenerator::default(); + let mut bf = BloomFilter::new(); + for _ in 0_usize..1000 { + bf.insert_hash(gen1.next()); + } + for _ in 0_usize..100 { + bf.remove_hash(gen2.next()); + } + for _ in 100_usize..200 { + test::black_box(bf.might_contain_hash(gen2.next())); + } + }); + } + + #[bench] + fn might_contain_10(b: &mut test::Bencher) { + let bf = BloomFilter::new(); + let mut gen = HashGenerator::default(); + b.iter(|| { + for _ in 0..10 { + test::black_box(bf.might_contain_hash(gen.next())); + } + }); + } + + #[bench] + fn clear(b: &mut test::Bencher) { + let mut bf = Box::new(BloomFilter::new()); + b.iter(|| test::black_box(&mut bf).clear()); + } + + #[bench] + fn insert_10(b: &mut test::Bencher) { + let mut bf = BloomFilter::new(); + let mut gen = HashGenerator::default(); + b.iter(|| { + for _ in 0..10 { + test::black_box(bf.insert_hash(gen.next())); + } + }); + } + + #[bench] + fn remove_10(b: &mut test::Bencher) { + let mut bf = BloomFilter::new(); + let mut gen = HashGenerator::default(); + // Note: this will underflow, and that's ok. + b.iter(|| { + for _ in 0..10 { + bf.remove_hash(gen.next()) + } + }); + } +} diff --git a/servo/components/selectors/build.rs b/servo/components/selectors/build.rs new file mode 100644 index 0000000000..c5c3803991 --- /dev/null +++ b/servo/components/selectors/build.rs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +extern crate phf_codegen; + +use std::env; +use std::fs::File; +use std::io::{BufWriter, Write}; +use std::path::Path; + +fn main() { + let path = Path::new(&env::var_os("OUT_DIR").unwrap()) + .join("ascii_case_insensitive_html_attributes.rs"); + let mut file = BufWriter::new(File::create(&path).unwrap()); + + let mut set = phf_codegen::Set::new(); + for name in ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES.split_whitespace() { + set.entry(name); + } + write!( + &mut file, + "{{ static SET: ::phf::Set<&'static str> = {}; &SET }}", + set.build(), + ) + .unwrap(); +} + +/// <https://html.spec.whatwg.org/multipage/#selectors> +static ASCII_CASE_INSENSITIVE_HTML_ATTRIBUTES: &str = r#" + accept + accept-charset + align + alink + axis + bgcolor + charset + checked + clear + codetype + color + compact + declare + defer + dir + direction + disabled + enctype + face + frame + hreflang + http-equiv + lang + language + link + media + method + multiple + nohref + noresize + noshade + nowrap + readonly + rel + rev + rules + scope + scrolling + selected + shape + target + text + type + valign + valuetype + vlink +"#; diff --git a/servo/components/selectors/builder.rs b/servo/components/selectors/builder.rs new file mode 100644 index 0000000000..2406cd84f6 --- /dev/null +++ b/servo/components/selectors/builder.rs @@ -0,0 +1,391 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helper module to build up a selector safely and efficiently. +//! +//! Our selector representation is designed to optimize matching, and has +//! several requirements: +//! * All simple selectors and combinators are stored inline in the same buffer +//! as Component instances. +//! * We store the top-level compound selectors from right to left, i.e. in +//! matching order. +//! * We store the simple selectors for each combinator from left to right, so +//! that we match the cheaper simple selectors first. +//! +//! Meeting all these constraints without extra memmove traffic during parsing +//! is non-trivial. This module encapsulates those details and presents an +//! easy-to-use API for the parser. + +use crate::parser::{Combinator, Component, RelativeSelector, Selector, SelectorImpl}; +use crate::sink::Push; +use servo_arc::{Arc, ThinArc}; +use smallvec::{self, SmallVec}; +use std::cmp; +use std::iter; +use std::ptr; +use std::slice; + +/// Top-level SelectorBuilder struct. This should be stack-allocated by the +/// consumer and never moved (because it contains a lot of inline data that +/// would be slow to memmov). +/// +/// After instantation, callers may call the push_simple_selector() and +/// push_combinator() methods to append selector data as it is encountered +/// (from left to right). Once the process is complete, callers should invoke +/// build(), which transforms the contents of the SelectorBuilder into a heap- +/// allocated Selector and leaves the builder in a drained state. +#[derive(Debug)] +pub struct SelectorBuilder<Impl: SelectorImpl> { + /// The entire sequence of simple selectors, from left to right, without combinators. + /// + /// We make this large because the result of parsing a selector is fed into a new + /// Arc-ed allocation, so any spilled vec would be a wasted allocation. Also, + /// Components are large enough that we don't have much cache locality benefit + /// from reserving stack space for fewer of them. + simple_selectors: SmallVec<[Component<Impl>; 32]>, + /// The combinators, and the length of the compound selector to their left. + combinators: SmallVec<[(Combinator, usize); 16]>, + /// The length of the current compount selector. + current_len: usize, +} + +impl<Impl: SelectorImpl> Default for SelectorBuilder<Impl> { + #[inline(always)] + fn default() -> Self { + SelectorBuilder { + simple_selectors: SmallVec::new(), + combinators: SmallVec::new(), + current_len: 0, + } + } +} + +impl<Impl: SelectorImpl> Push<Component<Impl>> for SelectorBuilder<Impl> { + fn push(&mut self, value: Component<Impl>) { + self.push_simple_selector(value); + } +} + +impl<Impl: SelectorImpl> SelectorBuilder<Impl> { + /// Pushes a simple selector onto the current compound selector. + #[inline(always)] + pub fn push_simple_selector(&mut self, ss: Component<Impl>) { + assert!(!ss.is_combinator()); + self.simple_selectors.push(ss); + self.current_len += 1; + } + + /// Completes the current compound selector and starts a new one, delimited + /// by the given combinator. + #[inline(always)] + pub fn push_combinator(&mut self, c: Combinator) { + self.combinators.push((c, self.current_len)); + self.current_len = 0; + } + + /// Returns true if combinators have ever been pushed to this builder. + #[inline(always)] + pub fn has_combinators(&self) -> bool { + !self.combinators.is_empty() + } + + /// Consumes the builder, producing a Selector. + #[inline(always)] + pub fn build(&mut self) -> ThinArc<SpecificityAndFlags, Component<Impl>> { + // Compute the specificity and flags. + let sf = specificity_and_flags(self.simple_selectors.iter()); + self.build_with_specificity_and_flags(sf) + } + + /// Builds with an explicit SpecificityAndFlags. This is separated from build() so + /// that unit tests can pass an explicit specificity. + #[inline(always)] + pub(crate) fn build_with_specificity_and_flags( + &mut self, + spec: SpecificityAndFlags, + ) -> ThinArc<SpecificityAndFlags, Component<Impl>> { + // Create the Arc using an iterator that drains our buffers. + // Panic-safety: if SelectorBuilderIter is not iterated to the end, some simple selectors + // will safely leak. + let raw_simple_selectors = unsafe { + let simple_selectors_len = self.simple_selectors.len(); + self.simple_selectors.set_len(0); + std::slice::from_raw_parts(self.simple_selectors.as_ptr(), simple_selectors_len) + }; + let (rest, current) = split_from_end(raw_simple_selectors, self.current_len); + let iter = SelectorBuilderIter { + current_simple_selectors: current.iter(), + rest_of_simple_selectors: rest, + combinators: self.combinators.drain(..).rev(), + }; + + Arc::from_header_and_iter(spec, iter) + } +} + +struct SelectorBuilderIter<'a, Impl: SelectorImpl> { + current_simple_selectors: slice::Iter<'a, Component<Impl>>, + rest_of_simple_selectors: &'a [Component<Impl>], + combinators: iter::Rev<smallvec::Drain<'a, [(Combinator, usize); 16]>>, +} + +impl<'a, Impl: SelectorImpl> ExactSizeIterator for SelectorBuilderIter<'a, Impl> { + fn len(&self) -> usize { + self.current_simple_selectors.len() + + self.rest_of_simple_selectors.len() + + self.combinators.len() + } +} + +impl<'a, Impl: SelectorImpl> Iterator for SelectorBuilderIter<'a, Impl> { + type Item = Component<Impl>; + #[inline(always)] + fn next(&mut self) -> Option<Self::Item> { + if let Some(simple_selector_ref) = self.current_simple_selectors.next() { + // Move a simple selector out of this slice iterator. + // This is safe because we’ve called SmallVec::set_len(0) above, + // so SmallVec::drop won’t drop this simple selector. + unsafe { Some(ptr::read(simple_selector_ref)) } + } else { + self.combinators.next().map(|(combinator, len)| { + let (rest, current) = split_from_end(self.rest_of_simple_selectors, len); + self.rest_of_simple_selectors = rest; + self.current_simple_selectors = current.iter(); + Component::Combinator(combinator) + }) + } + } + + fn size_hint(&self) -> (usize, Option<usize>) { + (self.len(), Some(self.len())) + } +} + +fn split_from_end<T>(s: &[T], at: usize) -> (&[T], &[T]) { + s.split_at(s.len() - at) +} + +/// Flags that indicate at which point of parsing a selector are we. +#[derive(Clone, Copy, Default, Eq, PartialEq, ToShmem)] +pub(crate) struct SelectorFlags(u8); + +bitflags! { + impl SelectorFlags: u8 { + const HAS_PSEUDO = 1 << 0; + const HAS_SLOTTED = 1 << 1; + const HAS_PART = 1 << 2; + const HAS_PARENT = 1 << 3; + const HAS_NON_FEATURELESS_COMPONENT = 1 << 4; + const HAS_HOST = 1 << 5; + } +} + +impl core::fmt::Debug for SelectorFlags { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + if self.is_empty() { + write!(f, "{:#x}", Self::empty().bits()) + } else { + bitflags::parser::to_writer(self, f) + } + } +} + +impl SelectorFlags { + /// When you nest a pseudo-element with something like: + /// + /// ::before { & { .. } } + /// + /// It is not supposed to work, because :is(::before) is invalid. We can't propagate the + /// pseudo-flags from inner to outer selectors, to avoid breaking our invariants. + pub(crate) fn for_nesting() -> Self { + Self::all() - (Self::HAS_PSEUDO | Self::HAS_SLOTTED | Self::HAS_PART) + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ToShmem)] +pub struct SpecificityAndFlags { + /// There are two free bits here, since we use ten bits for each specificity + /// kind (id, class, element). + pub(crate) specificity: u32, + /// There's padding after this field due to the size of the flags. + pub(crate) flags: SelectorFlags, +} + +const MAX_10BIT: u32 = (1u32 << 10) - 1; + +#[derive(Add, AddAssign, Clone, Copy, Default, Eq, Ord, PartialEq, PartialOrd)] +pub(crate) struct Specificity { + id_selectors: u32, + class_like_selectors: u32, + element_selectors: u32, +} + +impl From<u32> for Specificity { + #[inline] + fn from(value: u32) -> Specificity { + assert!(value <= MAX_10BIT << 20 | MAX_10BIT << 10 | MAX_10BIT); + Specificity { + id_selectors: value >> 20, + class_like_selectors: (value >> 10) & MAX_10BIT, + element_selectors: value & MAX_10BIT, + } + } +} + +impl From<Specificity> for u32 { + #[inline] + fn from(specificity: Specificity) -> u32 { + cmp::min(specificity.id_selectors, MAX_10BIT) << 20 | + cmp::min(specificity.class_like_selectors, MAX_10BIT) << 10 | + cmp::min(specificity.element_selectors, MAX_10BIT) + } +} + +pub(crate) fn specificity_and_flags<Impl>(iter: slice::Iter<Component<Impl>>) -> SpecificityAndFlags +where + Impl: SelectorImpl, +{ + complex_selector_specificity_and_flags(iter).into() +} + +fn complex_selector_specificity_and_flags<Impl>( + iter: slice::Iter<Component<Impl>>, +) -> SpecificityAndFlags +where + Impl: SelectorImpl, +{ + fn component_specificity<Impl>( + simple_selector: &Component<Impl>, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + ) where + Impl: SelectorImpl, + { + match *simple_selector { + Component::Combinator(..) => {}, + Component::ParentSelector => flags.insert(SelectorFlags::HAS_PARENT), + Component::Part(..) => { + flags.insert(SelectorFlags::HAS_PART); + specificity.element_selectors += 1 + }, + Component::PseudoElement(..) => { + flags.insert(SelectorFlags::HAS_PSEUDO); + specificity.element_selectors += 1 + }, + Component::LocalName(..) => { + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + specificity.element_selectors += 1 + }, + Component::Slotted(ref selector) => { + flags.insert( + SelectorFlags::HAS_SLOTTED | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ); + specificity.element_selectors += 1; + // Note that due to the way ::slotted works we only compete with + // other ::slotted rules, so the above rule doesn't really + // matter, but we do it still for consistency with other + // pseudo-elements. + // + // See: https://github.com/w3c/csswg-drafts/issues/1915 + *specificity += Specificity::from(selector.specificity()); + flags.insert(selector.flags()); + }, + Component::Host(ref selector) => { + flags.insert(SelectorFlags::HAS_HOST); + specificity.class_like_selectors += 1; + if let Some(ref selector) = *selector { + // See: https://github.com/w3c/csswg-drafts/issues/1915 + *specificity += Specificity::from(selector.specificity()); + flags.insert(selector.flags() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + } + }, + Component::ID(..) => { + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + specificity.id_selectors += 1; + }, + Component::Class(..) | + Component::AttributeInNoNamespace { .. } | + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeOther(..) | + Component::Root | + Component::Empty | + Component::Scope | + Component::Nth(..) | + Component::NonTSPseudoClass(..) => { + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + specificity.class_like_selectors += 1; + }, + Component::NthOf(ref nth_of_data) => { + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of the :nth-last-child() pseudo-class, + // like the :nth-child() pseudo-class, combines the + // specificity of a regular pseudo-class with that of its + // selector argument S. + specificity.class_like_selectors += 1; + let sf = selector_list_specificity_and_flags(nth_of_data.selectors().iter()); + *specificity += Specificity::from(sf.specificity); + flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + }, + // https://drafts.csswg.org/selectors/#specificity-rules: + // + // The specificity of an :is(), :not(), or :has() pseudo-class + // is replaced by the specificity of the most specific complex + // selector in its selector list argument. + Component::Where(ref list) | + Component::Negation(ref list) | + Component::Is(ref list) => { + let sf = selector_list_specificity_and_flags(list.slice().iter()); + if !matches!(*simple_selector, Component::Where(..)) { + *specificity += Specificity::from(sf.specificity); + } + flags.insert(sf.flags); + }, + Component::Has(ref relative_selectors) => { + let sf = relative_selector_list_specificity_and_flags(relative_selectors); + *specificity += Specificity::from(sf.specificity); + flags.insert(sf.flags | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + }, + Component::ExplicitUniversalType | + Component::ExplicitAnyNamespace | + Component::ExplicitNoNamespace | + Component::DefaultNamespace(..) | + Component::Namespace(..) | + Component::RelativeSelectorAnchor | + Component::Invalid(..) => { + // Does not affect specificity + flags.insert(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT); + }, + } + } + + let mut specificity = Default::default(); + let mut flags = Default::default(); + for simple_selector in iter { + component_specificity(&simple_selector, &mut specificity, &mut flags); + } + SpecificityAndFlags { + specificity: specificity.into(), + flags, + } +} + +/// Finds the maximum specificity of elements in the list and returns it. +pub(crate) fn selector_list_specificity_and_flags<'a, Impl: SelectorImpl>( + itr: impl Iterator<Item = &'a Selector<Impl>>, +) -> SpecificityAndFlags { + let mut specificity = 0; + let mut flags = SelectorFlags::empty(); + for selector in itr { + specificity = std::cmp::max(specificity, selector.specificity()); + flags.insert(selector.flags()); + } + SpecificityAndFlags { specificity, flags } +} + +pub(crate) fn relative_selector_list_specificity_and_flags<Impl: SelectorImpl>( + list: &[RelativeSelector<Impl>], +) -> SpecificityAndFlags { + selector_list_specificity_and_flags(list.iter().map(|rel| &rel.selector)) +} diff --git a/servo/components/selectors/context.rs b/servo/components/selectors/context.rs new file mode 100644 index 0000000000..84ee262dfe --- /dev/null +++ b/servo/components/selectors/context.rs @@ -0,0 +1,418 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::attr::CaseSensitivity; +use crate::bloom::BloomFilter; +use crate::nth_index_cache::{NthIndexCache, NthIndexCacheInner}; +use crate::parser::{Selector, SelectorImpl}; +use crate::relative_selector::cache::RelativeSelectorCache; +use crate::relative_selector::filter::RelativeSelectorFilterMap; +use crate::tree::{Element, OpaqueElement}; + +/// What kind of selector matching mode we should use. +/// +/// There are two modes of selector matching. The difference is only noticeable +/// in presence of pseudo-elements. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum MatchingMode { + /// Don't ignore any pseudo-element selectors. + Normal, + + /// Ignores any stateless pseudo-element selectors in the rightmost sequence + /// of simple selectors. + /// + /// This is useful, for example, to match against ::before when you aren't a + /// pseudo-element yourself. + /// + /// For example, in presence of `::before:hover`, it would never match, but + /// `::before` would be ignored as in "matching". + /// + /// It's required for all the selectors you match using this mode to have a + /// pseudo-element. + ForStatelessPseudoElement, +} + +/// The mode to use when matching unvisited and visited links. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum VisitedHandlingMode { + /// All links are matched as if they are unvisted. + AllLinksUnvisited, + /// All links are matched as if they are visited and unvisited (both :link + /// and :visited match). + /// + /// This is intended to be used from invalidation code, to be conservative + /// about whether we need to restyle a link. + AllLinksVisitedAndUnvisited, + /// A element's "relevant link" is the element being matched if it is a link + /// or the nearest ancestor link. The relevant link is matched as though it + /// is visited, and all other links are matched as if they are unvisited. + RelevantLinkVisited, +} + +impl VisitedHandlingMode { + #[inline] + pub fn matches_visited(&self) -> bool { + matches!( + *self, + VisitedHandlingMode::RelevantLinkVisited | + VisitedHandlingMode::AllLinksVisitedAndUnvisited + ) + } + + #[inline] + pub fn matches_unvisited(&self) -> bool { + matches!( + *self, + VisitedHandlingMode::AllLinksUnvisited | + VisitedHandlingMode::AllLinksVisitedAndUnvisited + ) + } +} + +/// Whether we need to set selector invalidation flags on elements for this +/// match request. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NeedsSelectorFlags { + No, + Yes, +} + +/// Whether we're matching in the contect of invalidation. +#[derive(PartialEq)] +pub enum MatchingForInvalidation { + No, + Yes, +} + +/// Which quirks mode is this document in. +/// +/// See: https://quirks.spec.whatwg.org/ +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum QuirksMode { + /// Quirks mode. + Quirks, + /// Limited quirks mode. + LimitedQuirks, + /// No quirks mode. + NoQuirks, +} + +impl QuirksMode { + #[inline] + pub fn classes_and_ids_case_sensitivity(self) -> CaseSensitivity { + match self { + QuirksMode::NoQuirks | QuirksMode::LimitedQuirks => CaseSensitivity::CaseSensitive, + QuirksMode::Quirks => CaseSensitivity::AsciiCaseInsensitive, + } + } +} + +/// Whether or not this matching considered relative selector. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum RelativeSelectorMatchingState { + /// Was not considered for any relative selector. + None, + /// Relative selector was considered for a match, but the element to be + /// under matching would not anchor the relative selector. i.e. The + /// relative selector was not part of the first compound selector (in match + /// order). + Considered, + /// Same as above, but the relative selector was part of the first compound + /// selector (in match order). + ConsideredAnchor, +} + +impl RelativeSelectorMatchingState { + /// Update the matching state to indicate that the relative selector matching + /// happened in the subject position. + pub fn considered_anchor(&mut self) { + *self = Self::ConsideredAnchor; + } + + /// Update the matching state to indicate that the relative selector matching + /// happened in a non-subject position. + pub fn considered(&mut self) { + // Being considered an anchor is stronger (e.g. `:has(.a):is(:has(.b) .c)`). + if *self == Self::ConsideredAnchor { + *self = Self::ConsideredAnchor; + } else { + *self = Self::Considered; + } + } +} + +/// Set of caches (And cache-likes) that speed up expensive selector matches. +#[derive(Default)] +pub struct SelectorCaches { + /// A cache to speed up nth-index-like selectors. + pub nth_index: NthIndexCache, + /// A cache to speed up relative selector matches. See module documentation. + pub relative_selector: RelativeSelectorCache, + /// A map of bloom filters to fast-reject relative selector matches. + pub relative_selector_filter_map: RelativeSelectorFilterMap, +} + +/// Data associated with the matching process for a element. This context is +/// used across many selectors for an element, so it's not appropriate for +/// transient data that applies to only a single selector. +pub struct MatchingContext<'a, Impl> +where + Impl: SelectorImpl, +{ + /// Input with the matching mode we should use when matching selectors. + matching_mode: MatchingMode, + /// Input with the bloom filter used to fast-reject selectors. + pub bloom_filter: Option<&'a BloomFilter>, + /// The element which is going to match :scope pseudo-class. It can be + /// either one :scope element, or the scoping element. + /// + /// Note that, although in theory there can be multiple :scope elements, + /// in current specs, at most one is specified, and when there is one, + /// scoping element is not relevant anymore, so we use a single field for + /// them. + /// + /// When this is None, :scope will match the root element. + /// + /// See https://drafts.csswg.org/selectors-4/#scope-pseudo + pub scope_element: Option<OpaqueElement>, + + /// The current shadow host we're collecting :host rules for. + pub current_host: Option<OpaqueElement>, + + /// Controls how matching for links is handled. + visited_handling: VisitedHandlingMode, + + /// The current nesting level of selectors that we're matching. + nesting_level: usize, + + /// Whether we're inside a negation or not. + in_negation: bool, + + /// An optional hook function for checking whether a pseudo-element + /// should match when matching_mode is ForStatelessPseudoElement. + pub pseudo_element_matching_fn: Option<&'a dyn Fn(&Impl::PseudoElement) -> bool>, + + /// Extra implementation-dependent matching data. + pub extra_data: Impl::ExtraMatchingData<'a>, + + /// The current element we're anchoring on for evaluating the relative selector. + current_relative_selector_anchor: Option<OpaqueElement>, + pub considered_relative_selector: RelativeSelectorMatchingState, + + quirks_mode: QuirksMode, + needs_selector_flags: NeedsSelectorFlags, + + /// Whether we're matching in the contect of invalidation. + matching_for_invalidation: MatchingForInvalidation, + + /// Caches to speed up expensive selector matches. + pub selector_caches: &'a mut SelectorCaches, + + classes_and_ids_case_sensitivity: CaseSensitivity, + _impl: ::std::marker::PhantomData<Impl>, +} + +impl<'a, Impl> MatchingContext<'a, Impl> +where + Impl: SelectorImpl, +{ + /// Constructs a new `MatchingContext`. + pub fn new( + matching_mode: MatchingMode, + bloom_filter: Option<&'a BloomFilter>, + selector_caches: &'a mut SelectorCaches, + quirks_mode: QuirksMode, + needs_selector_flags: NeedsSelectorFlags, + matching_for_invalidation: MatchingForInvalidation, + ) -> Self { + Self::new_for_visited( + matching_mode, + bloom_filter, + selector_caches, + VisitedHandlingMode::AllLinksUnvisited, + quirks_mode, + needs_selector_flags, + matching_for_invalidation, + ) + } + + /// Constructs a new `MatchingContext` for use in visited matching. + pub fn new_for_visited( + matching_mode: MatchingMode, + bloom_filter: Option<&'a BloomFilter>, + selector_caches: &'a mut SelectorCaches, + visited_handling: VisitedHandlingMode, + quirks_mode: QuirksMode, + needs_selector_flags: NeedsSelectorFlags, + matching_for_invalidation: MatchingForInvalidation, + ) -> Self { + Self { + matching_mode, + bloom_filter, + visited_handling, + quirks_mode, + classes_and_ids_case_sensitivity: quirks_mode.classes_and_ids_case_sensitivity(), + needs_selector_flags, + matching_for_invalidation, + scope_element: None, + current_host: None, + nesting_level: 0, + in_negation: false, + pseudo_element_matching_fn: None, + extra_data: Default::default(), + current_relative_selector_anchor: None, + considered_relative_selector: RelativeSelectorMatchingState::None, + selector_caches, + _impl: ::std::marker::PhantomData, + } + } + + // Grab a reference to the appropriate cache. + #[inline] + pub fn nth_index_cache( + &mut self, + is_of_type: bool, + is_from_end: bool, + selectors: &[Selector<Impl>], + ) -> &mut NthIndexCacheInner { + self.selector_caches + .nth_index + .get(is_of_type, is_from_end, selectors) + } + + /// Whether we're matching a nested selector. + #[inline] + pub fn is_nested(&self) -> bool { + self.nesting_level != 0 + } + + /// Whether we're matching inside a :not(..) selector. + #[inline] + pub fn in_negation(&self) -> bool { + self.in_negation + } + + /// The quirks mode of the document. + #[inline] + pub fn quirks_mode(&self) -> QuirksMode { + self.quirks_mode + } + + /// The matching-mode for this selector-matching operation. + #[inline] + pub fn matching_mode(&self) -> MatchingMode { + self.matching_mode + } + + /// Whether we need to set selector flags. + #[inline] + pub fn needs_selector_flags(&self) -> bool { + self.needs_selector_flags == NeedsSelectorFlags::Yes + } + + /// Whether or not we're matching to invalidate. + #[inline] + pub fn matching_for_invalidation(&self) -> bool { + self.matching_for_invalidation == MatchingForInvalidation::Yes + } + + /// The case-sensitivity for class and ID selectors + #[inline] + pub fn classes_and_ids_case_sensitivity(&self) -> CaseSensitivity { + self.classes_and_ids_case_sensitivity + } + + /// Runs F with a deeper nesting level. + #[inline] + pub fn nest<F, R>(&mut self, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.nesting_level += 1; + let result = f(self); + self.nesting_level -= 1; + result + } + + /// Runs F with a deeper nesting level, and marking ourselves in a negation, + /// for a :not(..) selector, for example. + #[inline] + pub fn nest_for_negation<F, R>(&mut self, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + let old_in_negation = self.in_negation; + self.in_negation = true; + let result = self.nest(f); + self.in_negation = old_in_negation; + result + } + + #[inline] + pub fn visited_handling(&self) -> VisitedHandlingMode { + self.visited_handling + } + + /// Runs F with a different VisitedHandlingMode. + #[inline] + pub fn with_visited_handling_mode<F, R>( + &mut self, + handling_mode: VisitedHandlingMode, + f: F, + ) -> R + where + F: FnOnce(&mut Self) -> R, + { + let original_handling_mode = self.visited_handling; + self.visited_handling = handling_mode; + let result = f(self); + self.visited_handling = original_handling_mode; + result + } + + /// Runs F with a given shadow host which is the root of the tree whose + /// rules we're matching. + #[inline] + pub fn with_shadow_host<F, E, R>(&mut self, host: Option<E>, f: F) -> R + where + E: Element, + F: FnOnce(&mut Self) -> R, + { + let original_host = self.current_host.take(); + self.current_host = host.map(|h| h.opaque()); + let result = f(self); + self.current_host = original_host; + result + } + + /// Returns the current shadow host whose shadow root we're matching rules + /// against. + #[inline] + pub fn shadow_host(&self) -> Option<OpaqueElement> { + self.current_host + } + + /// Runs F with a deeper nesting level, with the given element as the anchor, + /// for a :has(...) selector, for example. + #[inline] + pub fn nest_for_relative_selector<F, R>(&mut self, anchor: OpaqueElement, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + debug_assert!( + self.current_relative_selector_anchor.is_none(), + "Nesting should've been rejected at parse time" + ); + self.current_relative_selector_anchor = Some(anchor); + let result = self.nest(f); + self.current_relative_selector_anchor = None; + result + } + + /// Returns the current anchor element to evaluate the relative selector against. + #[inline] + pub fn relative_selector_anchor(&self) -> Option<OpaqueElement> { + self.current_relative_selector_anchor + } +} diff --git a/servo/components/selectors/lib.rs b/servo/components/selectors/lib.rs new file mode 100644 index 0000000000..d909059ccf --- /dev/null +++ b/servo/components/selectors/lib.rs @@ -0,0 +1,41 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Make |cargo bench| work. +#![cfg_attr(feature = "bench", feature(test))] + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate cssparser; +#[macro_use] +extern crate debug_unreachable; +#[macro_use] +extern crate derive_more; +extern crate fxhash; +#[macro_use] +extern crate log; +extern crate phf; +extern crate precomputed_hash; +extern crate servo_arc; +extern crate smallvec; +extern crate to_shmem; +#[macro_use] +extern crate to_shmem_derive; + +pub mod attr; +pub mod bloom; +mod builder; +pub mod context; +pub mod matching; +mod nth_index_cache; +pub mod parser; +pub mod relative_selector; +pub mod sink; +mod tree; +pub mod visitor; + +pub use crate::nth_index_cache::NthIndexCache; +pub use crate::parser::{Parser, SelectorImpl, SelectorList}; +pub use crate::tree::{Element, OpaqueElement}; diff --git a/servo/components/selectors/matching.rs b/servo/components/selectors/matching.rs new file mode 100644 index 0000000000..763f65d547 --- /dev/null +++ b/servo/components/selectors/matching.rs @@ -0,0 +1,1370 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::attr::{ + AttrSelectorOperation, AttrSelectorWithOptionalNamespace, CaseSensitivity, NamespaceConstraint, + ParsedAttrSelectorOperation, ParsedCaseSensitivity, +}; +use crate::bloom::{BloomFilter, BLOOM_HASH_MASK}; +use crate::parser::{ + AncestorHashes, Combinator, Component, LocalName, NthSelectorData, RelativeSelectorMatchHint, +}; +use crate::parser::{ + NonTSPseudoClass, RelativeSelector, Selector, SelectorImpl, SelectorIter, SelectorList, +}; +use crate::relative_selector::cache::RelativeSelectorCachedMatch; +use crate::tree::Element; +use smallvec::SmallVec; +use std::borrow::Borrow; + +pub use crate::context::*; + +// The bloom filter for descendant CSS selectors will have a <1% false +// positive rate until it has this many selectors in it, then it will +// rapidly increase. +pub static RECOMMENDED_SELECTOR_BLOOM_FILTER_SIZE: usize = 4096; + +bitflags! { + /// Set of flags that are set on either the element or its parent (depending + /// on the flag) if the element could potentially match a selector. + #[derive(Clone, Copy)] + pub struct ElementSelectorFlags: usize { + /// When a child is added or removed from the parent, all the children + /// must be restyled, because they may match :nth-last-child, + /// :last-of-type, :nth-last-of-type, or :only-of-type. + const HAS_SLOW_SELECTOR = 1 << 0; + + /// When a child is added or removed from the parent, any later + /// children must be restyled, because they may match :nth-child, + /// :first-of-type, or :nth-of-type. + const HAS_SLOW_SELECTOR_LATER_SIBLINGS = 1 << 1; + + /// HAS_SLOW_SELECTOR* was set by the presence of :nth (But not of). + const HAS_SLOW_SELECTOR_NTH = 1 << 2; + + /// When a DOM mutation occurs on a child that might be matched by + /// :nth-last-child(.. of <selector list>), earlier children must be + /// restyled, and HAS_SLOW_SELECTOR will be set (which normally + /// indicates that all children will be restyled). + /// + /// Similarly, when a DOM mutation occurs on a child that might be + /// matched by :nth-child(.. of <selector list>), later children must be + /// restyled, and HAS_SLOW_SELECTOR_LATER_SIBLINGS will be set. + const HAS_SLOW_SELECTOR_NTH_OF = 1 << 3; + + /// When a child is added or removed from the parent, the first and + /// last children must be restyled, because they may match :first-child, + /// :last-child, or :only-child. + const HAS_EDGE_CHILD_SELECTOR = 1 << 4; + + /// The element has an empty selector, so when a child is appended we + /// might need to restyle the parent completely. + const HAS_EMPTY_SELECTOR = 1 << 5; + + /// The element may anchor a relative selector. + const ANCHORS_RELATIVE_SELECTOR = 1 << 6; + + /// The element may anchor a relative selector that is not the subject + /// of the whole selector. + const ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT = 1 << 7; + + /// The element is reached by a relative selector search in the sibling direction. + const RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING = 1 << 8; + + /// The element is reached by a relative selector search in the ancestor direction. + const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR = 1 << 9; + + // The element is reached by a relative selector search in both sibling and ancestor directions. + const RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING = + Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING.bits() | + Self::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR.bits(); + } +} + +impl ElementSelectorFlags { + /// Returns the subset of flags that apply to the element. + pub fn for_self(self) -> ElementSelectorFlags { + self & (ElementSelectorFlags::HAS_EMPTY_SELECTOR | + ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR | + ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT | + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING | + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) + } + + /// Returns the subset of flags that apply to the parent. + pub fn for_parent(self) -> ElementSelectorFlags { + self & (ElementSelectorFlags::HAS_SLOW_SELECTOR | + ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS | + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH | + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF | + ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) + } +} + +/// Holds per-compound-selector data. +struct LocalMatchingContext<'a, 'b: 'a, Impl: SelectorImpl> { + shared: &'a mut MatchingContext<'b, Impl>, + rightmost: SubjectOrPseudoElement, + quirks_data: Option<SelectorIter<'a, Impl>>, +} + +#[inline(always)] +pub fn matches_selector_list<E>( + selector_list: &SelectorList<E::Impl>, + element: &E, + context: &mut MatchingContext<E::Impl>, +) -> bool +where + E: Element, +{ + // This is pretty much any(..) but manually inlined because the compiler + // refuses to do so from querySelector / querySelectorAll. + for selector in selector_list.slice() { + let matches = matches_selector(selector, 0, None, element, context); + if matches { + return true; + } + } + + false +} + +#[inline(always)] +fn may_match(hashes: &AncestorHashes, bf: &BloomFilter) -> bool { + // Check the first three hashes. Note that we can check for zero before + // masking off the high bits, since if any of the first three hashes is + // zero the fourth will be as well. We also take care to avoid the + // special-case complexity of the fourth hash until we actually reach it, + // because we usually don't. + // + // To be clear: this is all extremely hot. + for i in 0..3 { + let packed = hashes.packed_hashes[i]; + if packed == 0 { + // No more hashes left - unable to fast-reject. + return true; + } + + if !bf.might_contain_hash(packed & BLOOM_HASH_MASK) { + // Hooray! We fast-rejected on this hash. + return false; + } + } + + // Now do the slighty-more-complex work of synthesizing the fourth hash, + // and check it against the filter if it exists. + let fourth = hashes.fourth_hash(); + fourth == 0 || bf.might_contain_hash(fourth) +} + +/// A result of selector matching, includes 3 failure types, +/// +/// NotMatchedAndRestartFromClosestLaterSibling +/// NotMatchedAndRestartFromClosestDescendant +/// NotMatchedGlobally +/// +/// When NotMatchedGlobally appears, stop selector matching completely since +/// the succeeding selectors never matches. +/// It is raised when +/// Child combinator cannot find the candidate element. +/// Descendant combinator cannot find the candidate element. +/// +/// When NotMatchedAndRestartFromClosestDescendant appears, the selector +/// matching does backtracking and restarts from the closest Descendant +/// combinator. +/// It is raised when +/// NextSibling combinator cannot find the candidate element. +/// LaterSibling combinator cannot find the candidate element. +/// Child combinator doesn't match on the found element. +/// +/// When NotMatchedAndRestartFromClosestLaterSibling appears, the selector +/// matching does backtracking and restarts from the closest LaterSibling +/// combinator. +/// It is raised when +/// NextSibling combinator doesn't match on the found element. +/// +/// For example, when the selector "d1 d2 a" is provided and we cannot *find* +/// an appropriate ancestor element for "d1", this selector matching raises +/// NotMatchedGlobally since even if "d2" is moved to more upper element, the +/// candidates for "d1" becomes less than before and d1 . +/// +/// The next example is siblings. When the selector "b1 + b2 ~ d1 a" is +/// provided and we cannot *find* an appropriate brother element for b1, +/// the selector matching raises NotMatchedAndRestartFromClosestDescendant. +/// The selectors ("b1 + b2 ~") doesn't match and matching restart from "d1". +/// +/// The additional example is child and sibling. When the selector +/// "b1 + c1 > b2 ~ d1 a" is provided and the selector "b1" doesn't match on +/// the element, this "b1" raises NotMatchedAndRestartFromClosestLaterSibling. +/// However since the selector "c1" raises +/// NotMatchedAndRestartFromClosestDescendant. So the selector +/// "b1 + c1 > b2 ~ " doesn't match and restart matching from "d1". +#[derive(Clone, Copy, Eq, PartialEq)] +enum SelectorMatchingResult { + Matched, + NotMatchedAndRestartFromClosestLaterSibling, + NotMatchedAndRestartFromClosestDescendant, + NotMatchedGlobally, +} + +/// Matches a selector, fast-rejecting against a bloom filter. +/// +/// We accept an offset to allow consumers to represent and match against +/// partial selectors (indexed from the right). We use this API design, rather +/// than having the callers pass a SelectorIter, because creating a SelectorIter +/// requires dereferencing the selector to get the length, which adds an +/// unncessary cache miss for cases when we can fast-reject with AncestorHashes +/// (which the caller can store inline with the selector pointer). +#[inline(always)] +pub fn matches_selector<E>( + selector: &Selector<E::Impl>, + offset: usize, + hashes: Option<&AncestorHashes>, + element: &E, + context: &mut MatchingContext<E::Impl>, +) -> bool +where + E: Element, +{ + // Use the bloom filter to fast-reject. + if let Some(hashes) = hashes { + if let Some(filter) = context.bloom_filter { + if !may_match(hashes, filter) { + return false; + } + } + } + matches_complex_selector( + selector.iter_from(offset), + element, + context, + if selector.is_rightmost(offset) { + SubjectOrPseudoElement::Yes + } else { + SubjectOrPseudoElement::No + }, + ) +} + +/// Whether a compound selector matched, and whether it was the rightmost +/// selector inside the complex selector. +pub enum CompoundSelectorMatchingResult { + /// The selector was fully matched. + FullyMatched, + /// The compound selector matched, and the next combinator offset is + /// `next_combinator_offset`. + Matched { next_combinator_offset: usize }, + /// The selector didn't match. + NotMatched, +} + +/// Matches a compound selector belonging to `selector`, starting at offset +/// `from_offset`, matching left to right. +/// +/// Requires that `from_offset` points to a `Combinator`. +/// +/// NOTE(emilio): This doesn't allow to match in the leftmost sequence of the +/// complex selector, but it happens to be the case we don't need it. +pub fn matches_compound_selector_from<E>( + selector: &Selector<E::Impl>, + mut from_offset: usize, + context: &mut MatchingContext<E::Impl>, + element: &E, +) -> CompoundSelectorMatchingResult +where + E: Element, +{ + if cfg!(debug_assertions) && from_offset != 0 { + selector.combinator_at_parse_order(from_offset - 1); // This asserts. + } + + let mut local_context = LocalMatchingContext { + shared: context, + // We have no info if this is an outer selector. This function is called in + // an invalidation context, which only calls this for non-subject (i.e. + // Non-rightmost) positions. + rightmost: SubjectOrPseudoElement::No, + quirks_data: None, + }; + + // Find the end of the selector or the next combinator, then match + // backwards, so that we match in the same order as + // matches_complex_selector, which is usually faster. + let start_offset = from_offset; + for component in selector.iter_raw_parse_order_from(from_offset) { + if matches!(*component, Component::Combinator(..)) { + debug_assert_ne!(from_offset, 0, "Selector started with a combinator?"); + break; + } + + from_offset += 1; + } + + debug_assert!(from_offset >= 1); + debug_assert!(from_offset <= selector.len()); + + let iter = selector.iter_from(selector.len() - from_offset); + debug_assert!( + iter.clone().next().is_some() || + (from_offset != selector.len() && + matches!( + selector.combinator_at_parse_order(from_offset), + Combinator::SlotAssignment | Combinator::PseudoElement + )), + "Got the math wrong: {:?} | {:?} | {} {}", + selector, + selector.iter_raw_match_order().as_slice(), + from_offset, + start_offset + ); + + for component in iter { + if !matches_simple_selector(component, element, &mut local_context) { + return CompoundSelectorMatchingResult::NotMatched; + } + } + + if from_offset != selector.len() { + return CompoundSelectorMatchingResult::Matched { + next_combinator_offset: from_offset, + }; + } + + CompoundSelectorMatchingResult::FullyMatched +} + +/// Matches a complex selector. +#[inline(always)] +fn matches_complex_selector<E>( + mut iter: SelectorIter<E::Impl>, + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + // If this is the special pseudo-element mode, consume the ::pseudo-element + // before proceeding, since the caller has already handled that part. + if context.matching_mode() == MatchingMode::ForStatelessPseudoElement && !context.is_nested() { + // Consume the pseudo. + match *iter.next().unwrap() { + Component::PseudoElement(ref pseudo) => { + if let Some(ref f) = context.pseudo_element_matching_fn { + if !f(pseudo) { + return false; + } + } + }, + ref other => { + debug_assert!( + false, + "Used MatchingMode::ForStatelessPseudoElement \ + in a non-pseudo selector {:?}", + other + ); + return false; + }, + } + + if !iter.matches_for_stateless_pseudo_element() { + return false; + } + + // Advance to the non-pseudo-element part of the selector. + let next_sequence = iter.next_sequence().unwrap(); + debug_assert_eq!(next_sequence, Combinator::PseudoElement); + } + + let result = matches_complex_selector_internal(iter, element, context, rightmost); + + matches!(result, SelectorMatchingResult::Matched) +} + +/// Matches each selector of a list as a complex selector +fn matches_complex_selector_list<E: Element>( + list: &[Selector<E::Impl>], + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool { + for selector in list { + if matches_complex_selector(selector.iter(), element, context, rightmost) { + return true; + } + } + false +} + +fn matches_relative_selector<E: Element>( + relative_selector: &RelativeSelector<E::Impl>, + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool { + // Overall, we want to mark the path that we've traversed so that when an element + // is invalidated, we early-reject unnecessary relative selector invalidations. + if relative_selector.match_hint.is_descendant_direction() { + if context.needs_selector_flags() { + element.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ); + } + let mut next_element = element.first_element_child(); + while let Some(el) = next_element { + if context.needs_selector_flags() { + el.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ); + } + let mut matched = matches_complex_selector( + relative_selector.selector.iter(), + &el, + context, + rightmost, + ); + if !matched && relative_selector.match_hint.is_subtree() { + matched = matches_relative_selector_subtree( + &relative_selector.selector, + &el, + context, + rightmost, + ); + } + if matched { + return true; + } + next_element = el.next_sibling_element(); + } + } else { + debug_assert!( + matches!( + relative_selector.match_hint, + RelativeSelectorMatchHint::InNextSibling | + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSibling | + RelativeSelectorMatchHint::InSiblingSubtree + ), + "Not descendant direction, but also not sibling direction?" + ); + if context.needs_selector_flags() { + element.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ); + } + let sibling_flag = if relative_selector.match_hint.is_subtree() { + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING + } else { + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING + }; + let mut next_element = element.next_sibling_element(); + while let Some(el) = next_element { + if context.needs_selector_flags() { + el.apply_selector_flags(sibling_flag); + } + let matched = if relative_selector.match_hint.is_subtree() { + matches_relative_selector_subtree( + &relative_selector.selector, + &el, + context, + rightmost, + ) + } else { + matches_complex_selector(relative_selector.selector.iter(), &el, context, rightmost) + }; + if matched { + return true; + } + if relative_selector.match_hint.is_next_sibling() { + break; + } + next_element = el.next_sibling_element(); + } + } + return false; +} + +fn relative_selector_match_early<E: Element>( + selector: &RelativeSelector<E::Impl>, + element: &E, + context: &mut MatchingContext<E::Impl>, +) -> Option<bool> { + if context.matching_for_invalidation() { + // In the context of invalidation, we can't use caching/filtering due to + // now/then matches. DOM structure also may have changed, so just pretend + // that we always match. + return Some(!context.in_negation()); + } + // See if we can return a cached result. + if let Some(cached) = context + .selector_caches + .relative_selector + .lookup(element.opaque(), selector) + { + return Some(cached.matched()); + } + // See if we can fast-reject. + if context + .selector_caches + .relative_selector_filter_map + .fast_reject(element, selector, context.quirks_mode()) + { + // Alright, add as unmatched to cache. + context.selector_caches.relative_selector.add( + element.opaque(), + selector, + RelativeSelectorCachedMatch::NotMatched, + ); + return Some(false); + } + None +} + +fn match_relative_selectors<E: Element>( + selectors: &[RelativeSelector<E::Impl>], + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool { + if context.relative_selector_anchor().is_some() { + // FIXME(emilio): This currently can happen with nesting, and it's not fully + // correct, arguably. But the ideal solution isn't super-clear either. For now, + // cope with it and explicitly reject it at match time. See [1] for discussion. + // + // [1]: https://github.com/w3c/csswg-drafts/issues/9600 + return false; + } + context.nest_for_relative_selector(element.opaque(), |context| { + do_match_relative_selectors(selectors, element, context, rightmost) + }) +} + +/// Matches a relative selector in a list of relative selectors. +fn do_match_relative_selectors<E: Element>( + selectors: &[RelativeSelector<E::Impl>], + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool { + // Due to style sharing implications (See style sharing code), we mark the current styling context + // to mark elements considered for :has matching. Additionally, we want to mark the elements themselves, + // since we don't want to indiscriminately invalidate every element as a potential anchor. + if rightmost == SubjectOrPseudoElement::Yes { + context.considered_relative_selector.considered_anchor(); + if context.needs_selector_flags() { + element.apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR); + } + } else { + context.considered_relative_selector.considered(); + if context.needs_selector_flags() { + element + .apply_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT); + } + } + + for relative_selector in selectors.iter() { + if let Some(result) = relative_selector_match_early(relative_selector, element, context) { + if result { + return true; + } + // Early return indicates no match, continue to next selector. + continue; + } + + let matched = matches_relative_selector(relative_selector, element, context, rightmost); + context.selector_caches.relative_selector.add( + element.opaque(), + relative_selector, + if matched { + RelativeSelectorCachedMatch::Matched + } else { + RelativeSelectorCachedMatch::NotMatched + }, + ); + if matched { + return true; + } + } + + false +} + +fn matches_relative_selector_subtree<E: Element>( + selector: &Selector<E::Impl>, + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool { + let mut current = element.first_element_child(); + + while let Some(el) = current { + if context.needs_selector_flags() { + el.apply_selector_flags( + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ); + } + if matches_complex_selector(selector.iter(), &el, context, rightmost) { + return true; + } + + if matches_relative_selector_subtree(selector, &el, context, rightmost) { + return true; + } + + current = el.next_sibling_element(); + } + + false +} + +/// Whether the :hover and :active quirk applies. +/// +/// https://quirks.spec.whatwg.org/#the-active-and-hover-quirk +fn hover_and_active_quirk_applies<Impl: SelectorImpl>( + selector_iter: &SelectorIter<Impl>, + context: &MatchingContext<Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool { + if context.quirks_mode() != QuirksMode::Quirks { + return false; + } + + if context.is_nested() { + return false; + } + + // This compound selector had a pseudo-element to the right that we + // intentionally skipped. + if rightmost == SubjectOrPseudoElement::Yes && + context.matching_mode() == MatchingMode::ForStatelessPseudoElement + { + return false; + } + + selector_iter.clone().all(|simple| match *simple { + Component::LocalName(_) | + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeInNoNamespace { .. } | + Component::AttributeOther(_) | + Component::ID(_) | + Component::Class(_) | + Component::PseudoElement(_) | + Component::Negation(_) | + Component::Empty | + Component::Nth(_) | + Component::NthOf(_) => false, + Component::NonTSPseudoClass(ref pseudo_class) => pseudo_class.is_active_or_hover(), + _ => true, + }) +} + +#[derive(Clone, Copy, PartialEq)] +enum SubjectOrPseudoElement { + Yes, + No, +} + +fn host_for_part<E>(element: &E, context: &MatchingContext<E::Impl>) -> Option<E> +where + E: Element, +{ + let scope = context.current_host; + let mut curr = element.containing_shadow_host()?; + if scope == Some(curr.opaque()) { + return Some(curr); + } + loop { + let parent = curr.containing_shadow_host(); + if parent.as_ref().map(|h| h.opaque()) == scope { + return Some(curr); + } + curr = parent?; + } +} + +fn assigned_slot<E>(element: &E, context: &MatchingContext<E::Impl>) -> Option<E> +where + E: Element, +{ + debug_assert!(element + .assigned_slot() + .map_or(true, |s| s.is_html_slot_element())); + let scope = context.current_host?; + let mut current_slot = element.assigned_slot()?; + while current_slot.containing_shadow_host().unwrap().opaque() != scope { + current_slot = current_slot.assigned_slot()?; + } + Some(current_slot) +} + +#[inline(always)] +fn next_element_for_combinator<E>( + element: &E, + combinator: Combinator, + selector: &SelectorIter<E::Impl>, + context: &MatchingContext<E::Impl>, +) -> Option<E> +where + E: Element, +{ + match combinator { + Combinator::NextSibling | Combinator::LaterSibling => element.prev_sibling_element(), + Combinator::Child | Combinator::Descendant => { + match element.parent_element() { + Some(e) => return Some(e), + None => {}, + } + + if !element.parent_node_is_shadow_root() { + return None; + } + + // https://drafts.csswg.org/css-scoping/#host-element-in-tree: + // + // For the purpose of Selectors, a shadow host also appears in + // its shadow tree, with the contents of the shadow tree treated + // as its children. (In other words, the shadow host is treated as + // replacing the shadow root node.) + // + // and also: + // + // When considered within its own shadow trees, the shadow host is + // featureless. Only the :host, :host(), and :host-context() + // pseudo-classes are allowed to match it. + // + // Since we know that the parent is a shadow root, we necessarily + // are in a shadow tree of the host, and the next selector will only + // match if the selector is a featureless :host selector. + if !selector.clone().is_featureless_host_selector() { + return None; + } + + element.containing_shadow_host() + }, + Combinator::Part => host_for_part(element, context), + Combinator::SlotAssignment => assigned_slot(element, context), + Combinator::PseudoElement => element.pseudo_element_originating_element(), + } +} + +fn matches_complex_selector_internal<E>( + mut selector_iter: SelectorIter<E::Impl>, + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> SelectorMatchingResult +where + E: Element, +{ + debug!( + "Matching complex selector {:?} for {:?}", + selector_iter, element + ); + + let matches_compound_selector = + matches_compound_selector(&mut selector_iter, element, context, rightmost); + + let combinator = selector_iter.next_sequence(); + if combinator.map_or(false, |c| c.is_sibling()) { + if context.needs_selector_flags() { + element.apply_selector_flags(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS); + } + } + + if !matches_compound_selector { + return SelectorMatchingResult::NotMatchedAndRestartFromClosestLaterSibling; + } + + let combinator = match combinator { + None => return SelectorMatchingResult::Matched, + Some(c) => c, + }; + + let (candidate_not_found, mut rightmost) = match combinator { + Combinator::NextSibling | Combinator::LaterSibling => { + (SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant, SubjectOrPseudoElement::No) + }, + Combinator::Child | + Combinator::Descendant | + Combinator::SlotAssignment | + Combinator::Part => (SelectorMatchingResult::NotMatchedGlobally, SubjectOrPseudoElement::No), + Combinator::PseudoElement => (SelectorMatchingResult::NotMatchedGlobally, rightmost), + }; + + // Stop matching :visited as soon as we find a link, or a combinator for + // something that isn't an ancestor. + let mut visited_handling = if combinator.is_sibling() { + VisitedHandlingMode::AllLinksUnvisited + } else { + context.visited_handling() + }; + + let mut element = element.clone(); + loop { + if element.is_link() { + visited_handling = VisitedHandlingMode::AllLinksUnvisited; + } + + element = match next_element_for_combinator(&element, combinator, &selector_iter, &context) + { + None => return candidate_not_found, + Some(next_element) => next_element, + }; + + let result = context.with_visited_handling_mode(visited_handling, |context| { + matches_complex_selector_internal( + selector_iter.clone(), + &element, + context, + rightmost, + ) + }); + + if !matches!(combinator, Combinator::PseudoElement) { + rightmost = SubjectOrPseudoElement::No; + } + + match (result, combinator) { + // Return the status immediately. + (SelectorMatchingResult::Matched, _) | + (SelectorMatchingResult::NotMatchedGlobally, _) | + (_, Combinator::NextSibling) => { + return result; + }, + + // Upgrade the failure status to + // NotMatchedAndRestartFromClosestDescendant. + (_, Combinator::PseudoElement) | (_, Combinator::Child) => { + return SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant; + }, + + // If the failure status is + // NotMatchedAndRestartFromClosestDescendant and combinator is + // Combinator::LaterSibling, give up this Combinator::LaterSibling + // matching and restart from the closest descendant combinator. + ( + SelectorMatchingResult::NotMatchedAndRestartFromClosestDescendant, + Combinator::LaterSibling, + ) => { + return result; + }, + + // The Combinator::Descendant combinator and the status is + // NotMatchedAndRestartFromClosestLaterSibling or + // NotMatchedAndRestartFromClosestDescendant, or the + // Combinator::LaterSibling combinator and the status is + // NotMatchedAndRestartFromClosestDescendant, we can continue to + // matching on the next candidate element. + _ => {}, + } + } +} + +#[inline] +fn matches_local_name<E>(element: &E, local_name: &LocalName<E::Impl>) -> bool +where + E: Element, +{ + let name = select_name(element, &local_name.name, &local_name.lower_name).borrow(); + element.has_local_name(name) +} + +fn matches_part<E>( + element: &E, + parts: &[<E::Impl as SelectorImpl>::Identifier], + context: &mut MatchingContext<E::Impl>, +) -> bool +where + E: Element, +{ + let mut hosts = SmallVec::<[E; 4]>::new(); + + let mut host = match element.containing_shadow_host() { + Some(h) => h, + None => return false, + }; + + let current_host = context.current_host; + if current_host != Some(host.opaque()) { + loop { + let outer_host = host.containing_shadow_host(); + if outer_host.as_ref().map(|h| h.opaque()) == current_host { + break; + } + let outer_host = match outer_host { + Some(h) => h, + None => return false, + }; + // TODO(emilio): if worth it, we could early return if + // host doesn't have the exportparts attribute. + hosts.push(host); + host = outer_host; + } + } + + // Translate the part into the right scope. + parts.iter().all(|part| { + let mut part = part.clone(); + for host in hosts.iter().rev() { + part = match host.imported_part(&part) { + Some(p) => p, + None => return false, + }; + } + element.is_part(&part) + }) +} + +fn matches_host<E>( + element: &E, + selector: Option<&Selector<E::Impl>>, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + let host = match context.shadow_host() { + Some(h) => h, + None => return false, + }; + if host != element.opaque() { + return false; + } + selector.map_or(true, |selector| { + context + .nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost)) + }) +} + +fn matches_slotted<E>( + element: &E, + selector: &Selector<E::Impl>, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + // <slots> are never flattened tree slottables. + if element.is_html_slot_element() { + return false; + } + context.nest(|context| matches_complex_selector(selector.iter(), element, context, rightmost)) +} + +fn matches_rare_attribute_selector<E>( + element: &E, + attr_sel: &AttrSelectorWithOptionalNamespace<E::Impl>, +) -> bool +where + E: Element, +{ + let empty_string; + let namespace = match attr_sel.namespace() { + Some(ns) => ns, + None => { + empty_string = crate::parser::namespace_empty_string::<E::Impl>(); + NamespaceConstraint::Specific(&empty_string) + }, + }; + element.attr_matches( + &namespace, + select_name(element, &attr_sel.local_name, &attr_sel.local_name_lower), + &match attr_sel.operation { + ParsedAttrSelectorOperation::Exists => AttrSelectorOperation::Exists, + ParsedAttrSelectorOperation::WithValue { + operator, + case_sensitivity, + ref value, + } => AttrSelectorOperation::WithValue { + operator, + case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element), + value, + }, + }, + ) +} + +/// Determines whether the given element matches the given compound selector. +#[inline] +fn matches_compound_selector<E>( + selector_iter: &mut SelectorIter<E::Impl>, + element: &E, + context: &mut MatchingContext<E::Impl>, + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + let quirks_data = if context.quirks_mode() == QuirksMode::Quirks { + Some(selector_iter.clone()) + } else { + None + }; + let mut local_context = LocalMatchingContext { + shared: context, + rightmost, + quirks_data, + }; + selector_iter.all(|simple| matches_simple_selector(simple, element, &mut local_context)) +} + +/// Determines whether the given element matches the given single selector. +fn matches_simple_selector<E>( + selector: &Component<E::Impl>, + element: &E, + context: &mut LocalMatchingContext<E::Impl>, +) -> bool +where + E: Element, +{ + debug_assert!(context.shared.is_nested() || !context.shared.in_negation()); + let rightmost = context.rightmost; + match *selector { + Component::ID(ref id) => { + element.has_id(id, context.shared.classes_and_ids_case_sensitivity()) + }, + Component::Class(ref class) => { + element.has_class(class, context.shared.classes_and_ids_case_sensitivity()) + }, + Component::LocalName(ref local_name) => matches_local_name(element, local_name), + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + } => element.has_attr_in_no_namespace(select_name(element, local_name, local_name_lower)), + Component::AttributeInNoNamespace { + ref local_name, + ref value, + operator, + case_sensitivity, + } => element.attr_matches( + &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::<E::Impl>()), + local_name, + &AttrSelectorOperation::WithValue { + operator, + case_sensitivity: to_unconditional_case_sensitivity(case_sensitivity, element), + value, + }, + ), + Component::AttributeOther(ref attr_sel) => { + matches_rare_attribute_selector(element, attr_sel) + }, + Component::Part(ref parts) => matches_part(element, parts, &mut context.shared), + Component::Slotted(ref selector) => { + matches_slotted(element, selector, &mut context.shared, rightmost) + }, + Component::PseudoElement(ref pseudo) => { + element.match_pseudo_element(pseudo, context.shared) + }, + Component::ExplicitUniversalType | Component::ExplicitAnyNamespace => true, + Component::Namespace(_, ref url) | Component::DefaultNamespace(ref url) => { + element.has_namespace(&url.borrow()) + }, + Component::ExplicitNoNamespace => { + let ns = crate::parser::namespace_empty_string::<E::Impl>(); + element.has_namespace(&ns.borrow()) + }, + Component::NonTSPseudoClass(ref pc) => { + if let Some(ref iter) = context.quirks_data { + if pc.is_active_or_hover() && + !element.is_link() && + hover_and_active_quirk_applies(iter, context.shared, context.rightmost) + { + return false; + } + } + element.match_non_ts_pseudo_class(pc, &mut context.shared) + }, + Component::Root => element.is_root(), + Component::Empty => { + if context.shared.needs_selector_flags() { + element.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR); + } + element.is_empty() + }, + Component::Host(ref selector) => { + matches_host(element, selector.as_ref(), &mut context.shared, rightmost) + }, + Component::ParentSelector | Component::Scope => match context.shared.scope_element { + Some(ref scope_element) => element.opaque() == *scope_element, + None => element.is_root(), + }, + Component::Nth(ref nth_data) => { + matches_generic_nth_child(element, context.shared, nth_data, &[], rightmost) + }, + Component::NthOf(ref nth_of_data) => context.shared.nest(|context| { + matches_generic_nth_child( + element, + context, + nth_of_data.nth_data(), + nth_of_data.selectors(), + rightmost, + ) + }), + Component::Is(ref list) | Component::Where(ref list) => context.shared.nest(|context| { + matches_complex_selector_list(list.slice(), element, context, rightmost) + }), + Component::Negation(ref list) => context.shared.nest_for_negation(|context| { + !matches_complex_selector_list(list.slice(), element, context, rightmost) + }), + Component::Has(ref relative_selectors) => { + match_relative_selectors(relative_selectors, element, context.shared, rightmost) + }, + Component::Combinator(_) => unsafe { + debug_unreachable!("Shouldn't try to selector-match combinators") + }, + Component::RelativeSelectorAnchor => { + let anchor = context.shared.relative_selector_anchor(); + // We may match inner relative selectors, in which case we want to always match. + anchor.map_or(true, |a| a == element.opaque()) + }, + Component::Invalid(..) => false, + } +} + +#[inline(always)] +pub fn select_name<'a, E: Element, T: PartialEq>( + element: &E, + local_name: &'a T, + local_name_lower: &'a T, +) -> &'a T { + if local_name == local_name_lower || element.is_html_element_in_html_document() { + local_name_lower + } else { + local_name + } +} + +#[inline(always)] +pub fn to_unconditional_case_sensitivity<'a, E: Element>( + parsed: ParsedCaseSensitivity, + element: &E, +) -> CaseSensitivity { + match parsed { + ParsedCaseSensitivity::CaseSensitive | ParsedCaseSensitivity::ExplicitCaseSensitive => { + CaseSensitivity::CaseSensitive + }, + ParsedCaseSensitivity::AsciiCaseInsensitive => CaseSensitivity::AsciiCaseInsensitive, + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => { + if element.is_html_element_in_html_document() { + CaseSensitivity::AsciiCaseInsensitive + } else { + CaseSensitivity::CaseSensitive + } + }, + } +} + +fn matches_generic_nth_child<E>( + element: &E, + context: &mut MatchingContext<E::Impl>, + nth_data: &NthSelectorData, + selectors: &[Selector<E::Impl>], + rightmost: SubjectOrPseudoElement, +) -> bool +where + E: Element, +{ + if element.ignores_nth_child_selectors() { + return false; + } + let has_selectors = !selectors.is_empty(); + let selectors_match = + !has_selectors || matches_complex_selector_list(selectors, element, context, rightmost); + if context.matching_for_invalidation() { + // Skip expensive indexing math in invalidation. + return selectors_match && !context.in_negation(); + } + + let NthSelectorData { ty, a, b, .. } = *nth_data; + let is_of_type = ty.is_of_type(); + if ty.is_only() { + debug_assert!( + !has_selectors, + ":only-child and :only-of-type cannot have a selector list!" + ); + return matches_generic_nth_child( + element, + context, + &NthSelectorData::first(is_of_type), + selectors, + rightmost, + ) && matches_generic_nth_child( + element, + context, + &NthSelectorData::last(is_of_type), + selectors, + rightmost, + ); + } + + let is_from_end = ty.is_from_end(); + + // It's useful to know whether this can only select the first/last element + // child for optimization purposes, see the `HAS_EDGE_CHILD_SELECTOR` flag. + let is_edge_child_selector = nth_data.is_simple_edge() && !has_selectors; + + if context.needs_selector_flags() { + let mut flags = if is_edge_child_selector { + ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR + } else if is_from_end { + ElementSelectorFlags::HAS_SLOW_SELECTOR + } else { + ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS + }; + flags |= if has_selectors { + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF + } else { + ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH + }; + element.apply_selector_flags(flags); + } + + if !selectors_match { + return false; + } + + // :first/last-child are rather trivial to match, don't bother with the + // cache. + if is_edge_child_selector { + return if is_from_end { + element.next_sibling_element() + } else { + element.prev_sibling_element() + } + .is_none(); + } + + // Lookup or compute the index. + let index = if let Some(i) = context + .nth_index_cache(is_of_type, is_from_end, selectors) + .lookup(element.opaque()) + { + i + } else { + let i = nth_child_index( + element, + context, + selectors, + is_of_type, + is_from_end, + /* check_cache = */ true, + rightmost, + ); + context + .nth_index_cache(is_of_type, is_from_end, selectors) + .insert(element.opaque(), i); + i + }; + debug_assert_eq!( + index, + nth_child_index( + element, + context, + selectors, + is_of_type, + is_from_end, + /* check_cache = */ false, + rightmost, + ), + "invalid cache" + ); + + // Is there a non-negative integer n such that An+B=index? + match index.checked_sub(b) { + None => false, + Some(an) => match an.checked_div(a) { + Some(n) => n >= 0 && a * n == an, + None /* a == 0 */ => an == 0, + }, + } +} + +#[inline] +fn nth_child_index<E>( + element: &E, + context: &mut MatchingContext<E::Impl>, + selectors: &[Selector<E::Impl>], + is_of_type: bool, + is_from_end: bool, + check_cache: bool, + rightmost: SubjectOrPseudoElement, +) -> i32 +where + E: Element, +{ + // The traversal mostly processes siblings left to right. So when we walk + // siblings to the right when computing NthLast/NthLastOfType we're unlikely + // to get cache hits along the way. As such, we take the hit of walking the + // siblings to the left checking the cache in the is_from_end case (this + // matches what Gecko does). The indices-from-the-left is handled during the + // regular look further below. + if check_cache && + is_from_end && + !context + .nth_index_cache(is_of_type, is_from_end, selectors) + .is_empty() + { + let mut index: i32 = 1; + let mut curr = element.clone(); + while let Some(e) = curr.prev_sibling_element() { + curr = e; + let matches = if is_of_type { + element.is_same_type(&curr) + } else if !selectors.is_empty() { + matches_complex_selector_list(selectors, &curr, context, rightmost) + } else { + true + }; + if !matches { + continue; + } + if let Some(i) = context + .nth_index_cache(is_of_type, is_from_end, selectors) + .lookup(curr.opaque()) + { + return i - index; + } + index += 1; + } + } + + let mut index: i32 = 1; + let mut curr = element.clone(); + let next = |e: E| { + if is_from_end { + e.next_sibling_element() + } else { + e.prev_sibling_element() + } + }; + while let Some(e) = next(curr) { + curr = e; + let matches = if is_of_type { + element.is_same_type(&curr) + } else if !selectors.is_empty() { + matches_complex_selector_list(selectors, &curr, context, rightmost) + } else { + true + }; + if !matches { + continue; + } + // If we're computing indices from the left, check each element in the + // cache. We handle the indices-from-the-right case at the top of this + // function. + if !is_from_end && check_cache { + if let Some(i) = context + .nth_index_cache(is_of_type, is_from_end, selectors) + .lookup(curr.opaque()) + { + return i + index; + } + } + index += 1; + } + + index +} diff --git a/servo/components/selectors/nth_index_cache.rs b/servo/components/selectors/nth_index_cache.rs new file mode 100644 index 0000000000..b4b41578d0 --- /dev/null +++ b/servo/components/selectors/nth_index_cache.rs @@ -0,0 +1,102 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::hash::Hash; + +use crate::{parser::Selector, tree::OpaqueElement, SelectorImpl}; +use fxhash::FxHashMap; + +/// A cache to speed up matching of nth-index-like selectors. +/// +/// See [1] for some discussion around the design tradeoffs. +/// +/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1401855#c3 +#[derive(Default)] +pub struct NthIndexCache { + nth: NthIndexCacheInner, + nth_of_selectors: NthIndexOfSelectorsCaches, + nth_last: NthIndexCacheInner, + nth_last_of_selectors: NthIndexOfSelectorsCaches, + nth_of_type: NthIndexCacheInner, + nth_last_of_type: NthIndexCacheInner, +} + +impl NthIndexCache { + /// Gets the appropriate cache for the given parameters. + pub fn get<Impl: SelectorImpl>( + &mut self, + is_of_type: bool, + is_from_end: bool, + selectors: &[Selector<Impl>], + ) -> &mut NthIndexCacheInner { + if is_of_type { + return if is_from_end { + &mut self.nth_last_of_type + } else { + &mut self.nth_of_type + }; + } + if !selectors.is_empty() { + return if is_from_end { + self.nth_last_of_selectors.lookup(selectors) + } else { + self.nth_of_selectors.lookup(selectors) + }; + } + if is_from_end { + &mut self.nth_last + } else { + &mut self.nth + } + } +} + +#[derive(Hash, Eq, PartialEq)] +struct SelectorListCacheKey(usize); + +/// Use the selector list's pointer as the cache key +impl SelectorListCacheKey { + // :nth-child of selectors are reference-counted with `ThinArc`, so we know their pointers are stable. + fn new<Impl: SelectorImpl>(selectors: &[Selector<Impl>]) -> Self { + Self(selectors.as_ptr() as usize) + } +} + +/// Use a different map of cached indices per :nth-child's or :nth-last-child's selector list +#[derive(Default)] +pub struct NthIndexOfSelectorsCaches(FxHashMap<SelectorListCacheKey, NthIndexCacheInner>); + +/// Get or insert a map of cached incides for the selector list of this +/// particular :nth-child or :nth-last-child pseudoclass +impl NthIndexOfSelectorsCaches { + pub fn lookup<Impl: SelectorImpl>( + &mut self, + selectors: &[Selector<Impl>], + ) -> &mut NthIndexCacheInner { + self.0 + .entry(SelectorListCacheKey::new(selectors)) + .or_default() + } +} + +/// The concrete per-pseudo-class cache. +#[derive(Default)] +pub struct NthIndexCacheInner(FxHashMap<OpaqueElement, i32>); + +impl NthIndexCacheInner { + /// Does a lookup for a given element in the cache. + pub fn lookup(&mut self, el: OpaqueElement) -> Option<i32> { + self.0.get(&el).copied() + } + + /// Inserts an entry into the cache. + pub fn insert(&mut self, element: OpaqueElement, index: i32) { + self.0.insert(element, index); + } + + /// Returns whether the cache is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} diff --git a/servo/components/selectors/parser.rs b/servo/components/selectors/parser.rs new file mode 100644 index 0000000000..792d4eb8bc --- /dev/null +++ b/servo/components/selectors/parser.rs @@ -0,0 +1,4483 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::attr::{AttrSelectorOperator, AttrSelectorWithOptionalNamespace}; +use crate::attr::{NamespaceConstraint, ParsedAttrSelectorOperation, ParsedCaseSensitivity}; +use crate::bloom::BLOOM_HASH_MASK; +use crate::builder::{ + relative_selector_list_specificity_and_flags, selector_list_specificity_and_flags, + SelectorBuilder, SelectorFlags, Specificity, SpecificityAndFlags, +}; +use crate::context::QuirksMode; +use crate::sink::Push; +use crate::visitor::SelectorListKind; +pub use crate::visitor::SelectorVisitor; +use cssparser::parse_nth; +use cssparser::{BasicParseError, BasicParseErrorKind, ParseError, ParseErrorKind}; +use cssparser::{CowRcStr, Delimiter, SourceLocation}; +use cssparser::{Parser as CssParser, ToCss, Token}; +use precomputed_hash::PrecomputedHash; +use servo_arc::{Arc, ArcUnionBorrow, ThinArc, ThinArcUnion, UniqueArc}; +use smallvec::SmallVec; +use std::borrow::{Borrow, Cow}; +use std::fmt::{self, Debug}; +use std::iter::Rev; +use std::slice; + +/// A trait that represents a pseudo-element. +pub trait PseudoElement: Sized + ToCss { + /// The `SelectorImpl` this pseudo-element is used for. + type Impl: SelectorImpl; + + /// Whether the pseudo-element supports a given state selector to the right + /// of it. + fn accepts_state_pseudo_classes(&self) -> bool { + false + } + + /// Whether this pseudo-element is valid after a ::slotted(..) pseudo. + fn valid_after_slotted(&self) -> bool { + false + } +} + +/// A trait that represents a pseudo-class. +pub trait NonTSPseudoClass: Sized + ToCss { + /// The `SelectorImpl` this pseudo-element is used for. + type Impl: SelectorImpl; + + /// Whether this pseudo-class is :active or :hover. + fn is_active_or_hover(&self) -> bool; + + /// Whether this pseudo-class belongs to: + /// + /// https://drafts.csswg.org/selectors-4/#useraction-pseudos + fn is_user_action_state(&self) -> bool; + + fn visit<V>(&self, _visitor: &mut V) -> bool + where + V: SelectorVisitor<Impl = Self::Impl>, + { + true + } +} + +/// Returns a Cow::Borrowed if `s` is already ASCII lowercase, and a +/// Cow::Owned if `s` had to be converted into ASCII lowercase. +fn to_ascii_lowercase(s: &str) -> Cow<str> { + if let Some(first_uppercase) = s.bytes().position(|byte| byte >= b'A' && byte <= b'Z') { + let mut string = s.to_owned(); + string[first_uppercase..].make_ascii_lowercase(); + string.into() + } else { + s.into() + } +} + +bitflags! { + /// Flags that indicate at which point of parsing a selector are we. + #[derive(Copy, Clone)] + struct SelectorParsingState: u8 { + /// Whether we should avoid adding default namespaces to selectors that + /// aren't type or universal selectors. + const SKIP_DEFAULT_NAMESPACE = 1 << 0; + + /// Whether we've parsed a ::slotted() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + const AFTER_SLOTTED = 1 << 1; + /// Whether we've parsed a ::part() pseudo-element already. + /// + /// If so, then we can only parse a subset of pseudo-elements, and + /// whatever comes after them if so. + const AFTER_PART = 1 << 2; + /// Whether we've parsed a pseudo-element (as in, an + /// `Impl::PseudoElement` thus not accounting for `::slotted` or + /// `::part`) already. + /// + /// If so, then other pseudo-elements and most other selectors are + /// disallowed. + const AFTER_PSEUDO_ELEMENT = 1 << 3; + /// Whether we've parsed a non-stateful pseudo-element (again, as-in + /// `Impl::PseudoElement`) already. If so, then other pseudo-classes are + /// disallowed. If this flag is set, `AFTER_PSEUDO_ELEMENT` must be set + /// as well. + const AFTER_NON_STATEFUL_PSEUDO_ELEMENT = 1 << 4; + + /// Whether we are after any of the pseudo-like things. + const AFTER_PSEUDO = Self::AFTER_PART.bits() | Self::AFTER_SLOTTED.bits() | Self::AFTER_PSEUDO_ELEMENT.bits(); + + /// Whether we explicitly disallow combinators. + const DISALLOW_COMBINATORS = 1 << 5; + + /// Whether we explicitly disallow pseudo-element-like things. + const DISALLOW_PSEUDOS = 1 << 6; + + /// Whether we explicitly disallow relative selectors (i.e. `:has()`). + const DISALLOW_RELATIVE_SELECTOR = 1 << 7; + } +} + +impl SelectorParsingState { + #[inline] + fn allows_pseudos(self) -> bool { + // NOTE(emilio): We allow pseudos after ::part and such. + !self.intersects(Self::AFTER_PSEUDO_ELEMENT | Self::DISALLOW_PSEUDOS) + } + + #[inline] + fn allows_slotted(self) -> bool { + !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS) + } + + #[inline] + fn allows_part(self) -> bool { + !self.intersects(Self::AFTER_PSEUDO | Self::DISALLOW_PSEUDOS) + } + + #[inline] + fn allows_non_functional_pseudo_classes(self) -> bool { + !self.intersects(Self::AFTER_SLOTTED | Self::AFTER_NON_STATEFUL_PSEUDO_ELEMENT) + } + + #[inline] + fn allows_tree_structural_pseudo_classes(self) -> bool { + !self.intersects(Self::AFTER_PSEUDO) + } + + #[inline] + fn allows_combinators(self) -> bool { + !self.intersects(Self::DISALLOW_COMBINATORS) + } +} + +pub type SelectorParseError<'i> = ParseError<'i, SelectorParseErrorKind<'i>>; + +#[derive(Clone, Debug, PartialEq)] +pub enum SelectorParseErrorKind<'i> { + NoQualifiedNameInAttributeSelector(Token<'i>), + EmptySelector, + DanglingCombinator, + NonCompoundSelector, + NonPseudoElementAfterSlotted, + InvalidPseudoElementAfterSlotted, + InvalidPseudoElementInsideWhere, + InvalidState, + UnexpectedTokenInAttributeSelector(Token<'i>), + PseudoElementExpectedColon(Token<'i>), + PseudoElementExpectedIdent(Token<'i>), + NoIdentForPseudo(Token<'i>), + UnsupportedPseudoClassOrElement(CowRcStr<'i>), + UnexpectedIdent(CowRcStr<'i>), + ExpectedNamespace(CowRcStr<'i>), + ExpectedBarInAttr(Token<'i>), + BadValueInAttr(Token<'i>), + InvalidQualNameInAttr(Token<'i>), + ExplicitNamespaceUnexpectedToken(Token<'i>), + ClassNeedsIdent(Token<'i>), +} + +macro_rules! with_all_bounds { + ( + [ $( $InSelector: tt )* ] + [ $( $CommonBounds: tt )* ] + [ $( $FromStr: tt )* ] + ) => { + /// This trait allows to define the parser implementation in regards + /// of pseudo-classes/elements + /// + /// NB: We need Clone so that we can derive(Clone) on struct with that + /// are parameterized on SelectorImpl. See + /// <https://github.com/rust-lang/rust/issues/26925> + pub trait SelectorImpl: Clone + Debug + Sized + 'static { + type ExtraMatchingData<'a>: Sized + Default; + type AttrValue: $($InSelector)*; + type Identifier: $($InSelector)* + PrecomputedHash; + type LocalName: $($InSelector)* + Borrow<Self::BorrowedLocalName> + PrecomputedHash; + type NamespaceUrl: $($CommonBounds)* + Default + Borrow<Self::BorrowedNamespaceUrl> + PrecomputedHash; + type NamespacePrefix: $($InSelector)* + Default; + type BorrowedNamespaceUrl: ?Sized + Eq; + type BorrowedLocalName: ?Sized + Eq; + + /// non tree-structural pseudo-classes + /// (see: https://drafts.csswg.org/selectors/#structural-pseudos) + type NonTSPseudoClass: $($CommonBounds)* + NonTSPseudoClass<Impl = Self>; + + /// pseudo-elements + type PseudoElement: $($CommonBounds)* + PseudoElement<Impl = Self>; + + /// Whether attribute hashes should be collected for filtering + /// purposes. + fn should_collect_attr_hash(_name: &Self::LocalName) -> bool { + false + } + } + } +} + +macro_rules! with_bounds { + ( [ $( $CommonBounds: tt )* ] [ $( $FromStr: tt )* ]) => { + with_all_bounds! { + [$($CommonBounds)* + $($FromStr)* + ToCss] + [$($CommonBounds)*] + [$($FromStr)*] + } + } +} + +with_bounds! { + [Clone + Eq] + [for<'a> From<&'a str>] +} + +pub trait Parser<'i> { + type Impl: SelectorImpl; + type Error: 'i + From<SelectorParseErrorKind<'i>>; + + /// Whether to parse the `::slotted()` pseudo-element. + fn parse_slotted(&self) -> bool { + false + } + + /// Whether to parse the `::part()` pseudo-element. + fn parse_part(&self) -> bool { + false + } + + /// Whether to parse the selector list of nth-child() or nth-last-child(). + fn parse_nth_child_of(&self) -> bool { + false + } + + /// Whether to parse the `:where` pseudo-class. + fn parse_is_and_where(&self) -> bool { + false + } + + /// Whether to parse the :has pseudo-class. + fn parse_has(&self) -> bool { + false + } + + /// Whether to parse the '&' delimiter as a parent selector. + fn parse_parent_selector(&self) -> bool { + false + } + + /// Whether the given function name is an alias for the `:is()` function. + fn is_is_alias(&self, _name: &str) -> bool { + false + } + + /// Whether to parse the `:host` pseudo-class. + fn parse_host(&self) -> bool { + false + } + + /// Whether to allow forgiving selector-list parsing. + fn allow_forgiving_selectors(&self) -> bool { + true + } + + /// This function can return an "Err" pseudo-element in order to support CSS2.1 + /// pseudo-elements. + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<<Self::Impl as SelectorImpl>::NonTSPseudoClass, ParseError<'i, Self::Error>> { + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut CssParser<'i, 't>, + _after_part: bool, + ) -> Result<<Self::Impl as SelectorImpl>::NonTSPseudoClass, ParseError<'i, Self::Error>> { + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<<Self::Impl as SelectorImpl>::PseudoElement, ParseError<'i, Self::Error>> { + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_functional_pseudo_element<'t>( + &self, + name: CowRcStr<'i>, + arguments: &mut CssParser<'i, 't>, + ) -> Result<<Self::Impl as SelectorImpl>::PseudoElement, ParseError<'i, Self::Error>> { + Err( + arguments.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn default_namespace(&self) -> Option<<Self::Impl as SelectorImpl>::NamespaceUrl> { + None + } + + fn namespace_for_prefix( + &self, + _prefix: &<Self::Impl as SelectorImpl>::NamespacePrefix, + ) -> Option<<Self::Impl as SelectorImpl>::NamespaceUrl> { + None + } +} + +/// A selector list is a tagged pointer with either a single selector, or a ThinArc<()> of multiple +/// selectors. +#[derive(Clone, Eq, Debug, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct SelectorList<Impl: SelectorImpl>( + #[shmem(field_bound)] ThinArcUnion<SpecificityAndFlags, Component<Impl>, (), Selector<Impl>>, +); + +impl<Impl: SelectorImpl> SelectorList<Impl> { + pub fn from_one(selector: Selector<Impl>) -> Self { + #[cfg(debug_assertions)] + let selector_repr = unsafe { *(&selector as *const _ as *const usize) }; + let list = Self(ThinArcUnion::from_first(selector.into_data())); + #[cfg(debug_assertions)] + debug_assert_eq!( + selector_repr, + unsafe { *(&list as *const _ as *const usize) }, + "We rely on the same bit representation for the single selector variant" + ); + list + } + + pub fn from_iter(mut iter: impl ExactSizeIterator<Item = Selector<Impl>>) -> Self { + if iter.len() == 1 { + Self::from_one(iter.next().unwrap()) + } else { + Self(ThinArcUnion::from_second(ThinArc::from_header_and_iter( + (), + iter, + ))) + } + } + + #[inline] + pub fn slice(&self) -> &[Selector<Impl>] { + match self.0.borrow() { + ArcUnionBorrow::First(..) => { + // SAFETY: see from_one. + let selector: &Selector<Impl> = unsafe { std::mem::transmute(self) }; + std::slice::from_ref(selector) + }, + ArcUnionBorrow::Second(list) => list.get().slice(), + } + } + + #[inline] + pub fn len(&self) -> usize { + match self.0.borrow() { + ArcUnionBorrow::First(..) => 1, + ArcUnionBorrow::Second(list) => list.len(), + } + } + + /// Returns the address on the heap of the ThinArc for memory reporting. + pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void { + match self.0.borrow() { + ArcUnionBorrow::First(s) => s.with_arc(|a| a.heap_ptr()), + ArcUnionBorrow::Second(s) => s.with_arc(|a| a.heap_ptr()), + } + } +} + +/// Uniquely identify a selector based on its components, which is behind ThinArc and +/// is therefore stable. +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +pub struct SelectorKey(usize); + +impl SelectorKey { + /// Create a new key based on the given selector. + pub fn new<Impl: SelectorImpl>(selector: &Selector<Impl>) -> Self { + Self(selector.0.slice().as_ptr() as usize) + } +} + +/// Whether or not we're using forgiving parsing mode +#[derive(PartialEq)] +enum ForgivingParsing { + /// Discard the entire selector list upon encountering any invalid selector. + /// This is the default behavior for almost all of CSS. + No, + /// Ignore invalid selectors, potentially creating an empty selector list. + /// + /// This is the error recovery mode of :is() and :where() + Yes, +} + +/// Flag indicating if we're parsing relative selectors. +#[derive(Copy, Clone, PartialEq)] +pub enum ParseRelative { + /// Expect selectors to start with a combinator, assuming descendant combinator if not present. + ForHas, + /// Allow selectors to start with a combinator, prepending a parent selector if so. Do nothing + /// otherwise + ForNesting, + /// Treat as parse error if any selector begins with a combinator. + No, +} + +impl<Impl: SelectorImpl> SelectorList<Impl> { + /// Returns a selector list with a single `&` + pub fn ampersand() -> Self { + Self::from_one(Selector::ampersand()) + } + + /// Parse a comma-separated list of Selectors. + /// <https://drafts.csswg.org/selectors/#grouping> + /// + /// Return the Selectors or Err if there is an invalid selector. + pub fn parse<'i, 't, P>( + parser: &P, + input: &mut CssParser<'i, 't>, + parse_relative: ParseRelative, + ) -> Result<Self, ParseError<'i, P::Error>> + where + P: Parser<'i, Impl = Impl>, + { + Self::parse_with_state( + parser, + input, + SelectorParsingState::empty(), + ForgivingParsing::No, + parse_relative, + ) + } + + #[inline] + fn parse_with_state<'i, 't, P>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + recovery: ForgivingParsing, + parse_relative: ParseRelative, + ) -> Result<Self, ParseError<'i, P::Error>> + where + P: Parser<'i, Impl = Impl>, + { + let mut values = SmallVec::<[_; 4]>::new(); + let forgiving = recovery == ForgivingParsing::Yes && parser.allow_forgiving_selectors(); + loop { + let selector = input.parse_until_before(Delimiter::Comma, |input| { + let start = input.position(); + let mut selector = parse_selector(parser, input, state, parse_relative); + if forgiving && (selector.is_err() || input.expect_exhausted().is_err()) { + input.expect_no_error_token()?; + selector = Ok(Selector::new_invalid(input.slice_from(start))); + } + selector + })?; + + values.push(selector); + + match input.next() { + Ok(&Token::Comma) => {}, + Ok(_) => unreachable!(), + Err(_) => break, + } + } + Ok(Self::from_iter(values.into_iter())) + } + + /// Replaces the parent selector in all the items of the selector list. + pub fn replace_parent_selector(&self, parent: &SelectorList<Impl>) -> Self { + Self::from_iter( + self.slice() + .iter() + .map(|selector| selector.replace_parent_selector(parent)), + ) + } + + /// Creates a SelectorList from a Vec of selectors. Used in tests. + #[allow(dead_code)] + pub(crate) fn from_vec(v: Vec<Selector<Impl>>) -> Self { + SelectorList::from_iter(v.into_iter()) + } +} + +/// Parses one compound selector suitable for nested stuff like :-moz-any, etc. +fn parse_inner_compound_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result<Selector<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + parse_selector( + parser, + input, + state | SelectorParsingState::DISALLOW_PSEUDOS | SelectorParsingState::DISALLOW_COMBINATORS, + ParseRelative::No, + ) +} + +/// Ancestor hashes for the bloom filter. We precompute these and store them +/// inline with selectors to optimize cache performance during matching. +/// This matters a lot. +/// +/// We use 4 hashes, which is copied from Gecko, who copied it from WebKit. +/// Note that increasing the number of hashes here will adversely affect the +/// cache hit when fast-rejecting long lists of Rules with inline hashes. +/// +/// Because the bloom filter only uses the bottom 24 bits of the hash, we pack +/// the fourth hash into the upper bits of the first three hashes in order to +/// shrink Rule (whose size matters a lot). This scheme minimizes the runtime +/// overhead of the packing for the first three hashes (we just need to mask +/// off the upper bits) at the expense of making the fourth somewhat more +/// complicated to assemble, because we often bail out before checking all the +/// hashes. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AncestorHashes { + pub packed_hashes: [u32; 3], +} + +pub(crate) fn collect_selector_hashes<'a, Impl: SelectorImpl, Iter>( + iter: Iter, + quirks_mode: QuirksMode, + hashes: &mut [u32; 4], + len: &mut usize, + create_inner_iterator: fn(&'a Selector<Impl>) -> Iter, +) -> bool +where + Iter: Iterator<Item = &'a Component<Impl>>, +{ + for component in iter { + let hash = match *component { + Component::LocalName(LocalName { + ref name, + ref lower_name, + }) => { + // Only insert the local-name into the filter if it's all + // lowercase. Otherwise we would need to test both hashes, and + // our data structures aren't really set up for that. + if name != lower_name { + continue; + } + name.precomputed_hash() + }, + Component::DefaultNamespace(ref url) | Component::Namespace(_, ref url) => { + url.precomputed_hash() + }, + // In quirks mode, class and id selectors should match + // case-insensitively, so just avoid inserting them into the filter. + Component::ID(ref id) if quirks_mode != QuirksMode::Quirks => id.precomputed_hash(), + Component::Class(ref class) if quirks_mode != QuirksMode::Quirks => { + class.precomputed_hash() + }, + Component::AttributeInNoNamespace { ref local_name, .. } + if Impl::should_collect_attr_hash(local_name) => + { + // AttributeInNoNamespace is only used when local_name == + // local_name_lower. + local_name.precomputed_hash() + }, + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + .. + } => { + // Only insert the local-name into the filter if it's all + // lowercase. Otherwise we would need to test both hashes, and + // our data structures aren't really set up for that. + if local_name != local_name_lower || !Impl::should_collect_attr_hash(local_name) { + continue; + } + local_name.precomputed_hash() + }, + Component::AttributeOther(ref selector) => { + if selector.local_name != selector.local_name_lower || + !Impl::should_collect_attr_hash(&selector.local_name) + { + continue; + } + selector.local_name.precomputed_hash() + }, + Component::Is(ref list) | Component::Where(ref list) => { + // :where and :is OR their selectors, so we can't put any hash + // in the filter if there's more than one selector, as that'd + // exclude elements that may match one of the other selectors. + let slice = list.slice(); + if slice.len() == 1 && + !collect_selector_hashes( + create_inner_iterator(&slice[0]), + quirks_mode, + hashes, + len, + create_inner_iterator, + ) + { + return false; + } + continue; + }, + _ => continue, + }; + + hashes[*len] = hash & BLOOM_HASH_MASK; + *len += 1; + if *len == hashes.len() { + return false; + } + } + true +} + +fn collect_ancestor_hashes<Impl: SelectorImpl>( + iter: SelectorIter<Impl>, + quirks_mode: QuirksMode, + hashes: &mut [u32; 4], + len: &mut usize, +) { + collect_selector_hashes(AncestorIter::new(iter), quirks_mode, hashes, len, |s| { + AncestorIter(s.iter()) + }); +} + +impl AncestorHashes { + pub fn new<Impl: SelectorImpl>(selector: &Selector<Impl>, quirks_mode: QuirksMode) -> Self { + // Compute ancestor hashes for the bloom filter. + let mut hashes = [0u32; 4]; + let mut len = 0; + collect_ancestor_hashes(selector.iter(), quirks_mode, &mut hashes, &mut len); + debug_assert!(len <= 4); + + // Now, pack the fourth hash (if it exists) into the upper byte of each of + // the other three hashes. + if len == 4 { + let fourth = hashes[3]; + hashes[0] |= (fourth & 0x000000ff) << 24; + hashes[1] |= (fourth & 0x0000ff00) << 16; + hashes[2] |= (fourth & 0x00ff0000) << 8; + } + + AncestorHashes { + packed_hashes: [hashes[0], hashes[1], hashes[2]], + } + } + + /// Returns the fourth hash, reassembled from parts. + pub fn fourth_hash(&self) -> u32 { + ((self.packed_hashes[0] & 0xff000000) >> 24) | + ((self.packed_hashes[1] & 0xff000000) >> 16) | + ((self.packed_hashes[2] & 0xff000000) >> 8) + } +} + +#[inline] +pub fn namespace_empty_string<Impl: SelectorImpl>() -> Impl::NamespaceUrl { + // Rust type’s default, not default namespace + Impl::NamespaceUrl::default() +} + +type SelectorData<Impl> = ThinArc<SpecificityAndFlags, Component<Impl>>; + +/// A Selector stores a sequence of simple selectors and combinators. The +/// iterator classes allow callers to iterate at either the raw sequence level or +/// at the level of sequences of simple selectors separated by combinators. Most +/// callers want the higher-level iterator. +/// +/// We store compound selectors internally right-to-left (in matching order). +/// Additionally, we invert the order of top-level compound selectors so that +/// each one matches left-to-right. This is because matching namespace, local name, +/// id, and class are all relatively cheap, whereas matching pseudo-classes might +/// be expensive (depending on the pseudo-class). Since authors tend to put the +/// pseudo-classes on the right, it's faster to start matching on the left. +/// +/// This reordering doesn't change the semantics of selector matching, and we +/// handle it in to_css to make it invisible to serialization. +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +#[repr(transparent)] +pub struct Selector<Impl: SelectorImpl>(#[shmem(field_bound)] SelectorData<Impl>); + +impl<Impl: SelectorImpl> Selector<Impl> { + /// See Arc::mark_as_intentionally_leaked + pub fn mark_as_intentionally_leaked(&self) { + self.0.mark_as_intentionally_leaked() + } + + fn ampersand() -> Self { + Self(ThinArc::from_header_and_iter( + SpecificityAndFlags { + specificity: 0, + flags: SelectorFlags::HAS_PARENT, + }, + std::iter::once(Component::ParentSelector), + )) + } + + #[inline] + pub fn specificity(&self) -> u32 { + self.0.header.specificity + } + + #[inline] + pub(crate) fn flags(&self) -> SelectorFlags { + self.0.header.flags + } + + #[inline] + pub fn has_pseudo_element(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_PSEUDO) + } + + #[inline] + pub fn has_parent_selector(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_PARENT) + } + + #[inline] + pub fn is_slotted(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_SLOTTED) + } + + #[inline] + pub fn is_part(&self) -> bool { + self.flags().intersects(SelectorFlags::HAS_PART) + } + + #[inline] + pub fn parts(&self) -> Option<&[Impl::Identifier]> { + if !self.is_part() { + return None; + } + + let mut iter = self.iter(); + if self.has_pseudo_element() { + // Skip the pseudo-element. + for _ in &mut iter {} + + let combinator = iter.next_sequence()?; + debug_assert_eq!(combinator, Combinator::PseudoElement); + } + + for component in iter { + if let Component::Part(ref part) = *component { + return Some(part); + } + } + + debug_assert!(false, "is_part() lied somehow?"); + None + } + + #[inline] + pub fn pseudo_element(&self) -> Option<&Impl::PseudoElement> { + if !self.has_pseudo_element() { + return None; + } + + for component in self.iter() { + if let Component::PseudoElement(ref pseudo) = *component { + return Some(pseudo); + } + } + + debug_assert!(false, "has_pseudo_element lied!"); + None + } + + /// Whether this selector (pseudo-element part excluded) matches every element. + /// + /// Used for "pre-computed" pseudo-elements in components/style/stylist.rs + #[inline] + pub fn is_universal(&self) -> bool { + self.iter_raw_match_order().all(|c| { + matches!( + *c, + Component::ExplicitUniversalType | + Component::ExplicitAnyNamespace | + Component::Combinator(Combinator::PseudoElement) | + Component::PseudoElement(..) + ) + }) + } + + /// Returns an iterator over this selector in matching order (right-to-left). + /// When a combinator is reached, the iterator will return None, and + /// next_sequence() may be called to continue to the next sequence. + #[inline] + pub fn iter(&self) -> SelectorIter<Impl> { + SelectorIter { + iter: self.iter_raw_match_order(), + next_combinator: None, + } + } + + /// Same as `iter()`, but skips `RelativeSelectorAnchor` and its associated combinator. + #[inline] + pub fn iter_skip_relative_selector_anchor(&self) -> SelectorIter<Impl> { + if cfg!(debug_assertions) { + let mut selector_iter = self.iter_raw_parse_order_from(0); + assert!( + matches!( + selector_iter.next().unwrap(), + Component::RelativeSelectorAnchor + ), + "Relative selector does not start with RelativeSelectorAnchor" + ); + assert!( + selector_iter.next().unwrap().is_combinator(), + "Relative combinator does not exist" + ); + } + + SelectorIter { + iter: self.0.slice()[..self.len() - 2].iter(), + next_combinator: None, + } + } + + /// Whether this selector is a featureless :host selector, with no combinators to the left, and + /// optionally has a pseudo-element to the right. + #[inline] + pub fn is_featureless_host_selector_or_pseudo_element(&self) -> bool { + let flags = self.flags(); + flags.intersects(SelectorFlags::HAS_HOST) && + !flags.intersects(SelectorFlags::HAS_NON_FEATURELESS_COMPONENT) + } + + /// Returns an iterator over this selector in matching order (right-to-left), + /// skipping the rightmost |offset| Components. + #[inline] + pub fn iter_from(&self, offset: usize) -> SelectorIter<Impl> { + let iter = self.0.slice()[offset..].iter(); + SelectorIter { + iter, + next_combinator: None, + } + } + + /// Returns the combinator at index `index` (zero-indexed from the right), + /// or panics if the component is not a combinator. + #[inline] + pub fn combinator_at_match_order(&self, index: usize) -> Combinator { + match self.0.slice()[index] { + Component::Combinator(c) => c, + ref other => panic!( + "Not a combinator: {:?}, {:?}, index: {}", + other, self, index + ), + } + } + + /// Returns an iterator over the entire sequence of simple selectors and + /// combinators, in matching order (from right to left). + #[inline] + pub fn iter_raw_match_order(&self) -> slice::Iter<Component<Impl>> { + self.0.slice().iter() + } + + /// Returns the combinator at index `index` (zero-indexed from the left), + /// or panics if the component is not a combinator. + #[inline] + pub fn combinator_at_parse_order(&self, index: usize) -> Combinator { + match self.0.slice()[self.len() - index - 1] { + Component::Combinator(c) => c, + ref other => panic!( + "Not a combinator: {:?}, {:?}, index: {}", + other, self, index + ), + } + } + + /// Returns an iterator over the sequence of simple selectors and + /// combinators, in parse order (from left to right), starting from + /// `offset`. + #[inline] + pub fn iter_raw_parse_order_from(&self, offset: usize) -> Rev<slice::Iter<Component<Impl>>> { + self.0.slice()[..self.len() - offset].iter().rev() + } + + /// Creates a Selector from a vec of Components, specified in parse order. Used in tests. + #[allow(dead_code)] + pub(crate) fn from_vec( + vec: Vec<Component<Impl>>, + specificity: u32, + flags: SelectorFlags, + ) -> Self { + let mut builder = SelectorBuilder::default(); + for component in vec.into_iter() { + if let Some(combinator) = component.as_combinator() { + builder.push_combinator(combinator); + } else { + builder.push_simple_selector(component); + } + } + let spec = SpecificityAndFlags { specificity, flags }; + Selector(builder.build_with_specificity_and_flags(spec)) + } + + #[inline] + fn into_data(self) -> SelectorData<Impl> { + self.0 + } + + pub fn replace_parent_selector(&self, parent: &SelectorList<Impl>) -> Self { + let parent_specificity_and_flags = + selector_list_specificity_and_flags(parent.slice().iter()); + + let mut specificity = Specificity::from(self.specificity()); + let mut flags = self.flags() - SelectorFlags::HAS_PARENT; + + fn replace_parent_on_selector_list<Impl: SelectorImpl>( + orig: &[Selector<Impl>], + parent: &SelectorList<Impl>, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + propagate_specificity: bool, + flags_to_propagate: SelectorFlags, + ) -> Option<SelectorList<Impl>> { + if !orig.iter().any(|s| s.has_parent_selector()) { + return None; + } + + let result = SelectorList::from_iter(orig.iter().map(|s| { + if !s.has_parent_selector() { + return s.clone(); + } + s.replace_parent_selector(parent) + })); + + let result_specificity_and_flags = + selector_list_specificity_and_flags(result.slice().iter()); + if propagate_specificity { + *specificity += Specificity::from( + result_specificity_and_flags.specificity - + selector_list_specificity_and_flags(orig.iter()).specificity, + ); + } + flags.insert( + result_specificity_and_flags + .flags + .intersection(flags_to_propagate), + ); + Some(result) + } + + fn replace_parent_on_relative_selector_list<Impl: SelectorImpl>( + orig: &[RelativeSelector<Impl>], + parent: &SelectorList<Impl>, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + flags_to_propagate: SelectorFlags, + ) -> Vec<RelativeSelector<Impl>> { + let mut any = false; + + let result = orig + .iter() + .map(|s| { + if !s.selector.has_parent_selector() { + return s.clone(); + } + any = true; + RelativeSelector { + match_hint: s.match_hint, + selector: s.selector.replace_parent_selector(parent), + } + }) + .collect(); + + if !any { + return result; + } + + let result_specificity_and_flags = + relative_selector_list_specificity_and_flags(&result); + flags.insert( + result_specificity_and_flags + .flags + .intersection(flags_to_propagate), + ); + *specificity += Specificity::from( + result_specificity_and_flags.specificity - + relative_selector_list_specificity_and_flags(orig).specificity, + ); + result + } + + fn replace_parent_on_selector<Impl: SelectorImpl>( + orig: &Selector<Impl>, + parent: &SelectorList<Impl>, + specificity: &mut Specificity, + flags: &mut SelectorFlags, + flags_to_propagate: SelectorFlags, + ) -> Selector<Impl> { + if !orig.has_parent_selector() { + return orig.clone(); + } + let new_selector = orig.replace_parent_selector(parent); + *specificity += Specificity::from(new_selector.specificity() - orig.specificity()); + flags.insert(new_selector.flags().intersection(flags_to_propagate)); + new_selector + } + + let mut items = if !self.has_parent_selector() { + // Implicit `&` plus descendant combinator. + let iter = self.iter_raw_match_order(); + let len = iter.len() + 2; + specificity += Specificity::from(parent_specificity_and_flags.specificity); + flags.insert( + parent_specificity_and_flags + .flags + .intersection(SelectorFlags::for_nesting()), + ); + let iter = iter + .cloned() + .chain(std::iter::once(Component::Combinator( + Combinator::Descendant, + ))) + .chain(std::iter::once(Component::Is(parent.clone()))); + UniqueArc::from_header_and_iter_with_size(Default::default(), iter, len) + } else { + let iter = self.iter_raw_match_order().map(|component| { + use self::Component::*; + match *component { + LocalName(..) | + ID(..) | + Class(..) | + AttributeInNoNamespaceExists { .. } | + AttributeInNoNamespace { .. } | + AttributeOther(..) | + ExplicitUniversalType | + ExplicitAnyNamespace | + ExplicitNoNamespace | + DefaultNamespace(..) | + Namespace(..) | + Root | + Empty | + Scope | + Nth(..) | + NonTSPseudoClass(..) | + PseudoElement(..) | + Combinator(..) | + Host(None) | + Part(..) | + Invalid(..) | + RelativeSelectorAnchor => component.clone(), + ParentSelector => { + specificity += Specificity::from(parent_specificity_and_flags.specificity); + flags.insert( + parent_specificity_and_flags + .flags + .intersection(SelectorFlags::for_nesting()), + ); + Is(parent.clone()) + }, + Negation(ref selectors) => { + Negation( + replace_parent_on_selector_list( + selectors.slice(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ true, + SelectorFlags::for_nesting(), + ) + .unwrap_or_else(|| selectors.clone()), + ) + }, + Is(ref selectors) => { + Is(replace_parent_on_selector_list( + selectors.slice(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ true, + SelectorFlags::for_nesting(), + ) + .unwrap_or_else(|| selectors.clone())) + }, + Where(ref selectors) => { + Where( + replace_parent_on_selector_list( + selectors.slice(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ false, + SelectorFlags::for_nesting(), + ) + .unwrap_or_else(|| selectors.clone()), + ) + }, + Has(ref selectors) => Has(replace_parent_on_relative_selector_list( + selectors, + parent, + &mut specificity, + &mut flags, + SelectorFlags::for_nesting(), + ) + .into_boxed_slice()), + + Host(Some(ref selector)) => Host(Some(replace_parent_on_selector( + selector, + parent, + &mut specificity, + &mut flags, + SelectorFlags::for_nesting() - SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ))), + NthOf(ref data) => { + let selectors = replace_parent_on_selector_list( + data.selectors(), + parent, + &mut specificity, + &mut flags, + /* propagate_specificity = */ true, + SelectorFlags::for_nesting(), + ); + NthOf(match selectors { + Some(s) => { + NthOfSelectorData::new(data.nth_data(), s.slice().iter().cloned()) + }, + None => data.clone(), + }) + }, + Slotted(ref selector) => Slotted(replace_parent_on_selector( + selector, + parent, + &mut specificity, + &mut flags, + SelectorFlags::for_nesting(), + )), + } + }); + UniqueArc::from_header_and_iter(Default::default(), iter) + }; + *items.header_mut() = SpecificityAndFlags { + specificity: specificity.into(), + flags, + }; + Selector(items.shareable()) + } + + /// Returns count of simple selectors and combinators in the Selector. + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the address on the heap of the ThinArc for memory reporting. + pub fn thin_arc_heap_ptr(&self) -> *const ::std::os::raw::c_void { + self.0.heap_ptr() + } + + /// Traverse selector components inside `self`. + /// + /// Implementations of this method should call `SelectorVisitor` methods + /// or other impls of `Visit` as appropriate based on the fields of `Self`. + /// + /// A return value of `false` indicates terminating the traversal. + /// It should be propagated with an early return. + /// On the contrary, `true` indicates that all fields of `self` have been traversed: + /// + /// ```rust,ignore + /// if !visitor.visit_simple_selector(&self.some_simple_selector) { + /// return false; + /// } + /// if !self.some_component.visit(visitor) { + /// return false; + /// } + /// true + /// ``` + pub fn visit<V>(&self, visitor: &mut V) -> bool + where + V: SelectorVisitor<Impl = Impl>, + { + let mut current = self.iter(); + let mut combinator = None; + loop { + if !visitor.visit_complex_selector(combinator) { + return false; + } + + for selector in &mut current { + if !selector.visit(visitor) { + return false; + } + } + + combinator = current.next_sequence(); + if combinator.is_none() { + break; + } + } + + true + } + + /// Parse a selector, without any pseudo-element. + #[inline] + pub fn parse<'i, 't, P>( + parser: &P, + input: &mut CssParser<'i, 't>, + ) -> Result<Self, ParseError<'i, P::Error>> + where + P: Parser<'i, Impl = Impl>, + { + parse_selector( + parser, + input, + SelectorParsingState::empty(), + ParseRelative::No, + ) + } + + pub fn new_invalid(s: &str) -> Self { + fn check_for_parent(input: &mut CssParser, has_parent: &mut bool) { + while let Ok(t) = input.next() { + match *t { + Token::Function(_) | + Token::ParenthesisBlock | + Token::CurlyBracketBlock | + Token::SquareBracketBlock => { + let _ = input.parse_nested_block( + |i| -> Result<(), ParseError<'_, BasicParseError>> { + check_for_parent(i, has_parent); + Ok(()) + }, + ); + }, + Token::Delim('&') => { + *has_parent = true; + }, + _ => {}, + } + if *has_parent { + break; + } + } + } + let mut has_parent = false; + { + let mut parser = cssparser::ParserInput::new(s); + let mut parser = CssParser::new(&mut parser); + check_for_parent(&mut parser, &mut has_parent); + } + Self(ThinArc::from_header_and_iter( + SpecificityAndFlags { + specificity: 0, + flags: if has_parent { + SelectorFlags::HAS_PARENT + } else { + SelectorFlags::empty() + }, + }, + std::iter::once(Component::Invalid(Arc::new(String::from(s.trim())))), + )) + } + + /// Is the compound starting at the offset the subject compound, or referring to its pseudo-element? + pub fn is_rightmost(&self, offset: usize) -> bool { + // There can really be only one pseudo-element, and it's not really valid for anything else to + // follow it. + offset == 0 || matches!(self.combinator_at_match_order(offset - 1), Combinator::PseudoElement) + } +} + +#[derive(Clone)] +pub struct SelectorIter<'a, Impl: 'a + SelectorImpl> { + iter: slice::Iter<'a, Component<Impl>>, + next_combinator: Option<Combinator>, +} + +impl<'a, Impl: 'a + SelectorImpl> SelectorIter<'a, Impl> { + /// Prepares this iterator to point to the next sequence to the left, + /// returning the combinator if the sequence was found. + #[inline] + pub fn next_sequence(&mut self) -> Option<Combinator> { + self.next_combinator.take() + } + + /// Whether this selector is a featureless host selector, with no + /// combinators to the left. + #[inline] + pub(crate) fn is_featureless_host_selector(&mut self) -> bool { + self.selector_length() > 0 && + self.all(|component| component.matches_featureless_host()) && + self.next_sequence().is_none() + } + + #[inline] + pub(crate) fn matches_for_stateless_pseudo_element(&mut self) -> bool { + let first = match self.next() { + Some(c) => c, + // Note that this is the common path that we keep inline: the + // pseudo-element not having anything to its right. + None => return true, + }; + self.matches_for_stateless_pseudo_element_internal(first) + } + + #[inline(never)] + fn matches_for_stateless_pseudo_element_internal(&mut self, first: &Component<Impl>) -> bool { + if !first.matches_for_stateless_pseudo_element() { + return false; + } + for component in self { + // The only other parser-allowed Components in this sequence are + // state pseudo-classes, or one of the other things that can contain + // them. + if !component.matches_for_stateless_pseudo_element() { + return false; + } + } + true + } + + /// Returns remaining count of the simple selectors and combinators in the Selector. + #[inline] + pub fn selector_length(&self) -> usize { + self.iter.len() + } +} + +impl<'a, Impl: SelectorImpl> Iterator for SelectorIter<'a, Impl> { + type Item = &'a Component<Impl>; + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + debug_assert!( + self.next_combinator.is_none(), + "You should call next_sequence!" + ); + match *self.iter.next()? { + Component::Combinator(c) => { + self.next_combinator = Some(c); + None + }, + ref x => Some(x), + } + } +} + +impl<'a, Impl: SelectorImpl> fmt::Debug for SelectorIter<'a, Impl> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let iter = self.iter.clone().rev(); + for component in iter { + component.to_css(f)? + } + Ok(()) + } +} + +/// An iterator over all combinators in a selector. Does not traverse selectors within psuedoclasses. +struct CombinatorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>); +impl<'a, Impl: 'a + SelectorImpl> CombinatorIter<'a, Impl> { + fn new(inner: SelectorIter<'a, Impl>) -> Self { + let mut result = CombinatorIter(inner); + result.consume_non_combinators(); + result + } + + fn consume_non_combinators(&mut self) { + while self.0.next().is_some() {} + } +} + +impl<'a, Impl: SelectorImpl> Iterator for CombinatorIter<'a, Impl> { + type Item = Combinator; + fn next(&mut self) -> Option<Self::Item> { + let result = self.0.next_sequence(); + self.consume_non_combinators(); + result + } +} + +/// An iterator over all simple selectors belonging to ancestors. +struct AncestorIter<'a, Impl: 'a + SelectorImpl>(SelectorIter<'a, Impl>); +impl<'a, Impl: 'a + SelectorImpl> AncestorIter<'a, Impl> { + /// Creates an AncestorIter. The passed-in iterator is assumed to point to + /// the beginning of the child sequence, which will be skipped. + fn new(inner: SelectorIter<'a, Impl>) -> Self { + let mut result = AncestorIter(inner); + result.skip_until_ancestor(); + result + } + + /// Skips a sequence of simple selectors and all subsequent sequences until + /// a non-pseudo-element ancestor combinator is reached. + fn skip_until_ancestor(&mut self) { + loop { + while self.0.next().is_some() {} + // If this is ever changed to stop at the "pseudo-element" + // combinator, we will need to fix the way we compute hashes for + // revalidation selectors. + if self.0.next_sequence().map_or(true, |x| { + matches!(x, Combinator::Child | Combinator::Descendant) + }) { + break; + } + } + } +} + +impl<'a, Impl: SelectorImpl> Iterator for AncestorIter<'a, Impl> { + type Item = &'a Component<Impl>; + fn next(&mut self) -> Option<Self::Item> { + // Grab the next simple selector in the sequence if available. + let next = self.0.next(); + if next.is_some() { + return next; + } + + // See if there are more sequences. If so, skip any non-ancestor sequences. + if let Some(combinator) = self.0.next_sequence() { + if !matches!(combinator, Combinator::Child | Combinator::Descendant) { + self.skip_until_ancestor(); + } + } + + self.0.next() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +pub enum Combinator { + Child, // > + Descendant, // space + NextSibling, // + + LaterSibling, // ~ + /// A dummy combinator we use to the left of pseudo-elements. + /// + /// It serializes as the empty string, and acts effectively as a child + /// combinator in most cases. If we ever actually start using a child + /// combinator for this, we will need to fix up the way hashes are computed + /// for revalidation selectors. + PseudoElement, + /// Another combinator used for ::slotted(), which represent the jump from + /// a node to its assigned slot. + SlotAssignment, + /// Another combinator used for `::part()`, which represents the jump from + /// the part to the containing shadow host. + Part, +} + +impl Combinator { + /// Returns true if this combinator is a child or descendant combinator. + #[inline] + pub fn is_ancestor(&self) -> bool { + matches!( + *self, + Combinator::Child | + Combinator::Descendant | + Combinator::PseudoElement | + Combinator::SlotAssignment + ) + } + + /// Returns true if this combinator is a pseudo-element combinator. + #[inline] + pub fn is_pseudo_element(&self) -> bool { + matches!(*self, Combinator::PseudoElement) + } + + /// Returns true if this combinator is a next- or later-sibling combinator. + #[inline] + pub fn is_sibling(&self) -> bool { + matches!(*self, Combinator::NextSibling | Combinator::LaterSibling) + } +} + +/// An enum for the different types of :nth- pseudoclasses +#[derive(Copy, Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub enum NthType { + Child, + LastChild, + OnlyChild, + OfType, + LastOfType, + OnlyOfType, +} + +impl NthType { + pub fn is_only(self) -> bool { + self == Self::OnlyChild || self == Self::OnlyOfType + } + + pub fn is_of_type(self) -> bool { + self == Self::OfType || self == Self::LastOfType || self == Self::OnlyOfType + } + + pub fn is_from_end(self) -> bool { + self == Self::LastChild || self == Self::LastOfType + } +} + +/// The properties that comprise an :nth- pseudoclass as of Selectors 3 (e.g., +/// nth-child(An+B)). +/// https://www.w3.org/TR/selectors-3/#nth-child-pseudo +#[derive(Copy, Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct NthSelectorData { + pub ty: NthType, + pub is_function: bool, + pub a: i32, + pub b: i32, +} + +impl NthSelectorData { + /// Returns selector data for :only-{child,of-type} + #[inline] + pub const fn only(of_type: bool) -> Self { + Self { + ty: if of_type { + NthType::OnlyOfType + } else { + NthType::OnlyChild + }, + is_function: false, + a: 0, + b: 1, + } + } + + /// Returns selector data for :first-{child,of-type} + #[inline] + pub const fn first(of_type: bool) -> Self { + Self { + ty: if of_type { + NthType::OfType + } else { + NthType::Child + }, + is_function: false, + a: 0, + b: 1, + } + } + + /// Returns selector data for :last-{child,of-type} + #[inline] + pub const fn last(of_type: bool) -> Self { + Self { + ty: if of_type { + NthType::LastOfType + } else { + NthType::LastChild + }, + is_function: false, + a: 0, + b: 1, + } + } + + /// Returns true if this is an edge selector that is not `:*-of-type`` + #[inline] + pub fn is_simple_edge(&self) -> bool { + self.a == 0 && self.b == 1 && !self.ty.is_of_type() + } + + /// Writes the beginning of the selector. + #[inline] + fn write_start<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result { + dest.write_str(match self.ty { + NthType::Child if self.is_function => ":nth-child(", + NthType::Child => ":first-child", + NthType::LastChild if self.is_function => ":nth-last-child(", + NthType::LastChild => ":last-child", + NthType::OfType if self.is_function => ":nth-of-type(", + NthType::OfType => ":first-of-type", + NthType::LastOfType if self.is_function => ":nth-last-of-type(", + NthType::LastOfType => ":last-of-type", + NthType::OnlyChild => ":only-child", + NthType::OnlyOfType => ":only-of-type", + }) + } + + /// Serialize <an+b> (part of the CSS Syntax spec, but currently only used here). + /// <https://drafts.csswg.org/css-syntax-3/#serialize-an-anb-value> + #[inline] + fn write_affine<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result { + match (self.a, self.b) { + (0, 0) => dest.write_char('0'), + + (1, 0) => dest.write_char('n'), + (-1, 0) => dest.write_str("-n"), + (_, 0) => write!(dest, "{}n", self.a), + + (0, _) => write!(dest, "{}", self.b), + (1, _) => write!(dest, "n{:+}", self.b), + (-1, _) => write!(dest, "-n{:+}", self.b), + (_, _) => write!(dest, "{}n{:+}", self.a, self.b), + } + } +} + +/// The properties that comprise an :nth- pseudoclass as of Selectors 4 (e.g., +/// nth-child(An+B [of S]?)). +/// https://www.w3.org/TR/selectors-4/#nth-child-pseudo +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct NthOfSelectorData<Impl: SelectorImpl>( + #[shmem(field_bound)] ThinArc<NthSelectorData, Selector<Impl>>, +); + +impl<Impl: SelectorImpl> NthOfSelectorData<Impl> { + /// Returns selector data for :nth-{,last-}{child,of-type}(An+B [of S]) + #[inline] + pub fn new<I>(nth_data: &NthSelectorData, selectors: I) -> Self + where + I: Iterator<Item = Selector<Impl>> + ExactSizeIterator, + { + Self(ThinArc::from_header_and_iter(*nth_data, selectors)) + } + + /// Returns the An+B part of the selector + #[inline] + pub fn nth_data(&self) -> &NthSelectorData { + &self.0.header + } + + /// Returns the selector list part of the selector + #[inline] + pub fn selectors(&self) -> &[Selector<Impl>] { + self.0.slice() + } +} + +/// Flag indicating where a given relative selector's match would be contained. +#[derive(Clone, Copy, Eq, PartialEq, ToShmem)] +pub enum RelativeSelectorMatchHint { + /// Within this element's subtree. + InSubtree, + /// Within this element's direct children. + InChild, + /// This element's next sibling. + InNextSibling, + /// Within this element's next sibling's subtree. + InNextSiblingSubtree, + /// Within this element's subsequent siblings. + InSibling, + /// Across this element's subsequent siblings and their subtrees. + InSiblingSubtree, +} + +impl RelativeSelectorMatchHint { + /// Create a new relative selector match hint based on its composition. + pub fn new( + relative_combinator: Combinator, + has_child_or_descendants: bool, + has_adjacent_or_next_siblings: bool, + ) -> Self { + match relative_combinator { + Combinator::Descendant => RelativeSelectorMatchHint::InSubtree, + Combinator::Child => { + if !has_child_or_descendants { + RelativeSelectorMatchHint::InChild + } else { + // Technically, for any composition that consists of child combinators only, + // the search space is depth-constrained, but it's probably not worth optimizing for. + RelativeSelectorMatchHint::InSubtree + } + }, + Combinator::NextSibling => { + if !has_child_or_descendants && !has_adjacent_or_next_siblings { + RelativeSelectorMatchHint::InNextSibling + } else if !has_child_or_descendants && has_adjacent_or_next_siblings { + RelativeSelectorMatchHint::InSibling + } else if has_child_or_descendants && !has_adjacent_or_next_siblings { + // Match won't cross multiple siblings. + RelativeSelectorMatchHint::InNextSiblingSubtree + } else { + RelativeSelectorMatchHint::InSiblingSubtree + } + }, + Combinator::LaterSibling => { + if !has_child_or_descendants { + RelativeSelectorMatchHint::InSibling + } else { + // Even if the match may not cross multiple siblings, we have to look until + // we find a match anyway. + RelativeSelectorMatchHint::InSiblingSubtree + } + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { + debug_assert!(false, "Unexpected relative combinator"); + RelativeSelectorMatchHint::InSubtree + }, + } + } + + /// Is the match traversal direction towards the descendant of this element (As opposed to siblings)? + pub fn is_descendant_direction(&self) -> bool { + matches!(*self, Self::InChild | Self::InSubtree) + } + + /// Is the match traversal terminated at the next sibling? + pub fn is_next_sibling(&self) -> bool { + matches!(*self, Self::InNextSibling | Self::InNextSiblingSubtree) + } + + /// Does the match involve matching the subtree? + pub fn is_subtree(&self) -> bool { + matches!( + *self, + Self::InSubtree | Self::InSiblingSubtree | Self::InNextSiblingSubtree + ) + } +} + +/// Count of combinators in a given relative selector, not traversing selectors of pseudoclasses. +#[derive(Clone, Copy)] +pub struct RelativeSelectorCombinatorCount { + relative_combinator: Combinator, + pub child_or_descendants: usize, + pub adjacent_or_next_siblings: usize, +} + +impl RelativeSelectorCombinatorCount { + /// Create a new relative selector combinator count from a given relative selector. + pub fn new<Impl: SelectorImpl>(relative_selector: &RelativeSelector<Impl>) -> Self { + let mut result = RelativeSelectorCombinatorCount { + relative_combinator: relative_selector.selector.combinator_at_parse_order(1), + child_or_descendants: 0, + adjacent_or_next_siblings: 0, + }; + + for combinator in CombinatorIter::new( + relative_selector + .selector + .iter_skip_relative_selector_anchor(), + ) { + match combinator { + Combinator::Descendant | Combinator::Child => { + result.child_or_descendants += 1; + }, + Combinator::NextSibling | Combinator::LaterSibling => { + result.adjacent_or_next_siblings += 1; + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { + continue + }, + }; + } + result + } + + /// Get the match hint based on the current combinator count. + pub fn get_match_hint(&self) -> RelativeSelectorMatchHint { + RelativeSelectorMatchHint::new( + self.relative_combinator, + self.child_or_descendants != 0, + self.adjacent_or_next_siblings != 0, + ) + } +} + +/// Storage for a relative selector. +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct RelativeSelector<Impl: SelectorImpl> { + /// Match space constraining hint. + pub match_hint: RelativeSelectorMatchHint, + /// The selector. Guaranteed to contain `RelativeSelectorAnchor` and the relative combinator in parse order. + #[shmem(field_bound)] + pub selector: Selector<Impl>, +} + +bitflags! { + /// Composition of combinators in a given selector, not traversing selectors of pseudoclasses. + #[derive(Clone, Debug, Eq, PartialEq)] + struct CombinatorComposition: u8 { + const DESCENDANTS = 1 << 0; + const SIBLINGS = 1 << 1; + } +} + +impl CombinatorComposition { + fn for_relative_selector<Impl: SelectorImpl>(inner_selector: &Selector<Impl>) -> Self { + let mut result = CombinatorComposition::empty(); + for combinator in CombinatorIter::new(inner_selector.iter_skip_relative_selector_anchor()) { + match combinator { + Combinator::Descendant | Combinator::Child => { + result.insert(Self::DESCENDANTS); + }, + Combinator::NextSibling | Combinator::LaterSibling => { + result.insert(Self::SIBLINGS); + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => { + continue + }, + }; + if result.is_all() { + break; + } + } + return result; + } +} + +impl<Impl: SelectorImpl> RelativeSelector<Impl> { + fn from_selector_list(selector_list: SelectorList<Impl>) -> Box<[Self]> { + let vec: Vec<Self> = selector_list + .slice() + .iter() + .map(|selector| { + // It's more efficient to keep track of all this during the parse time, but that seems like a lot of special + // case handling for what it's worth. + if cfg!(debug_assertions) { + let relative_selector_anchor = selector.iter_raw_parse_order_from(0).next(); + debug_assert!( + relative_selector_anchor.is_some(), + "Relative selector is empty" + ); + debug_assert!( + matches!( + relative_selector_anchor.unwrap(), + Component::RelativeSelectorAnchor + ), + "Relative selector anchor is missing" + ); + } + // Leave a hint for narrowing down the search space when we're matching. + let composition = CombinatorComposition::for_relative_selector(&selector); + let match_hint = RelativeSelectorMatchHint::new( + selector.combinator_at_parse_order(1), + composition.intersects(CombinatorComposition::DESCENDANTS), + composition.intersects(CombinatorComposition::SIBLINGS), + ); + RelativeSelector { + match_hint, + selector: selector.clone(), + } + }) + .collect(); + vec.into_boxed_slice() + } +} + +/// A CSS simple selector or combinator. We store both in the same enum for +/// optimal packing and cache performance, see [1]. +/// +/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1357973 +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub enum Component<Impl: SelectorImpl> { + LocalName(LocalName<Impl>), + + ID(#[shmem(field_bound)] Impl::Identifier), + Class(#[shmem(field_bound)] Impl::Identifier), + + AttributeInNoNamespaceExists { + #[shmem(field_bound)] + local_name: Impl::LocalName, + local_name_lower: Impl::LocalName, + }, + // Used only when local_name is already lowercase. + AttributeInNoNamespace { + local_name: Impl::LocalName, + operator: AttrSelectorOperator, + #[shmem(field_bound)] + value: Impl::AttrValue, + case_sensitivity: ParsedCaseSensitivity, + }, + // Use a Box in the less common cases with more data to keep size_of::<Component>() small. + AttributeOther(Box<AttrSelectorWithOptionalNamespace<Impl>>), + + ExplicitUniversalType, + ExplicitAnyNamespace, + + ExplicitNoNamespace, + DefaultNamespace(#[shmem(field_bound)] Impl::NamespaceUrl), + Namespace( + #[shmem(field_bound)] Impl::NamespacePrefix, + #[shmem(field_bound)] Impl::NamespaceUrl, + ), + + /// Pseudo-classes + Negation(SelectorList<Impl>), + Root, + Empty, + Scope, + ParentSelector, + Nth(NthSelectorData), + NthOf(NthOfSelectorData<Impl>), + NonTSPseudoClass(#[shmem(field_bound)] Impl::NonTSPseudoClass), + /// The ::slotted() pseudo-element: + /// + /// https://drafts.csswg.org/css-scoping/#slotted-pseudo + /// + /// The selector here is a compound selector, that is, no combinators. + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put ::slotted() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + Slotted(Selector<Impl>), + /// The `::part` pseudo-element. + /// https://drafts.csswg.org/css-shadow-parts/#part + Part(#[shmem(field_bound)] Box<[Impl::Identifier]>), + /// The `:host` pseudo-class: + /// + /// https://drafts.csswg.org/css-scoping/#host-selector + /// + /// NOTE(emilio): This should support a list of selectors, but as of this + /// writing no other browser does, and that allows them to put :host() + /// in the rule hash, so we do that too. + /// + /// See https://github.com/w3c/csswg-drafts/issues/2158 + Host(Option<Selector<Impl>>), + /// The `:where` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#zero-matches + /// + /// The inner argument is conceptually a SelectorList, but we move the + /// selectors to the heap to keep Component small. + Where(SelectorList<Impl>), + /// The `:is` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#matches-pseudo + /// + /// Same comment as above re. the argument. + Is(SelectorList<Impl>), + /// The `:has` pseudo-class. + /// + /// https://drafts.csswg.org/selectors/#has-pseudo + /// + /// Same comment as above re. the argument. + Has(Box<[RelativeSelector<Impl>]>), + /// An invalid selector inside :is() / :where(). + Invalid(Arc<String>), + /// An implementation-dependent pseudo-element selector. + PseudoElement(#[shmem(field_bound)] Impl::PseudoElement), + + Combinator(Combinator), + + /// Used only for relative selectors, which starts with a combinator + /// (With an implied descendant combinator if not specified). + /// + /// https://drafts.csswg.org/selectors-4/#typedef-relative-selector + RelativeSelectorAnchor, +} + +impl<Impl: SelectorImpl> Component<Impl> { + /// Returns true if this is a combinator. + #[inline] + pub fn is_combinator(&self) -> bool { + matches!(*self, Component::Combinator(_)) + } + + /// Returns true if this is a :host() selector. + #[inline] + pub fn is_host(&self) -> bool { + matches!(*self, Component::Host(..)) + } + + /// Returns true if this is a :host() selector. + #[inline] + pub fn matches_featureless_host(&self) -> bool { + match *self { + Component::Host(..) => true, + Component::Where(ref l) | Component::Is(ref l) => { + // TODO(emilio): For now we use .all() rather than .any(), because not doing so + // brings up a fair amount of extra complexity (we can't make the decision on + // whether to walk out statically). + l.slice() + .iter() + .all(|i| i.is_featureless_host_selector_or_pseudo_element()) + }, + _ => false, + } + } + + /// Returns the value as a combinator if applicable, None otherwise. + pub fn as_combinator(&self) -> Option<Combinator> { + match *self { + Component::Combinator(c) => Some(c), + _ => None, + } + } + + /// Whether this component is valid after a pseudo-element. Only intended + /// for sanity-checking. + pub fn maybe_allowed_after_pseudo_element(&self) -> bool { + match *self { + Component::NonTSPseudoClass(..) => true, + Component::Negation(ref selectors) | + Component::Is(ref selectors) | + Component::Where(ref selectors) => selectors.slice().iter().all(|selector| { + selector + .iter_raw_match_order() + .all(|c| c.maybe_allowed_after_pseudo_element()) + }), + _ => false, + } + } + + /// Whether a given selector should match for stateless pseudo-elements. + /// + /// This is a bit subtle: Only selectors that return true in + /// `maybe_allowed_after_pseudo_element` should end up here, and + /// `NonTSPseudoClass` never matches (as it is a stateless pseudo after + /// all). + fn matches_for_stateless_pseudo_element(&self) -> bool { + debug_assert!( + self.maybe_allowed_after_pseudo_element(), + "Someone messed up pseudo-element parsing: {:?}", + *self + ); + match *self { + Component::Negation(ref selectors) => !selectors.slice().iter().all(|selector| { + selector + .iter_raw_match_order() + .all(|c| c.matches_for_stateless_pseudo_element()) + }), + Component::Is(ref selectors) | Component::Where(ref selectors) => { + selectors.slice().iter().any(|selector| { + selector + .iter_raw_match_order() + .all(|c| c.matches_for_stateless_pseudo_element()) + }) + }, + _ => false, + } + } + + pub fn visit<V>(&self, visitor: &mut V) -> bool + where + V: SelectorVisitor<Impl = Impl>, + { + use self::Component::*; + if !visitor.visit_simple_selector(self) { + return false; + } + + match *self { + Slotted(ref selector) => { + if !selector.visit(visitor) { + return false; + } + }, + Host(Some(ref selector)) => { + if !selector.visit(visitor) { + return false; + } + }, + AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + } => { + if !visitor.visit_attribute_selector( + &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()), + local_name, + local_name_lower, + ) { + return false; + } + }, + AttributeInNoNamespace { ref local_name, .. } => { + if !visitor.visit_attribute_selector( + &NamespaceConstraint::Specific(&namespace_empty_string::<Impl>()), + local_name, + local_name, + ) { + return false; + } + }, + AttributeOther(ref attr_selector) => { + let empty_string; + let namespace = match attr_selector.namespace() { + Some(ns) => ns, + None => { + empty_string = crate::parser::namespace_empty_string::<Impl>(); + NamespaceConstraint::Specific(&empty_string) + }, + }; + if !visitor.visit_attribute_selector( + &namespace, + &attr_selector.local_name, + &attr_selector.local_name_lower, + ) { + return false; + } + }, + + NonTSPseudoClass(ref pseudo_class) => { + if !pseudo_class.visit(visitor) { + return false; + } + }, + Negation(ref list) | Is(ref list) | Where(ref list) => { + let list_kind = SelectorListKind::from_component(self); + debug_assert!(!list_kind.is_empty()); + if !visitor.visit_selector_list(list_kind, list.slice()) { + return false; + } + }, + NthOf(ref nth_of_data) => { + if !visitor.visit_selector_list(SelectorListKind::NTH_OF, nth_of_data.selectors()) { + return false; + } + }, + Has(ref list) => { + if !visitor.visit_relative_selector_list(list) { + return false; + } + }, + _ => {}, + } + + true + } + + // Returns true if this has any selector that requires an index calculation. e.g. + // :nth-child, :first-child, etc. For nested selectors, return true only if the + // indexed selector is in its subject compound. + pub fn has_indexed_selector_in_subject(&self) -> bool { + match *self { + Component::NthOf(..) | Component::Nth(..) => return true, + Component::Is(ref selectors) | + Component::Where(ref selectors) | + Component::Negation(ref selectors) => { + // Check the subject compound. + for selector in selectors.slice() { + let mut iter = selector.iter(); + while let Some(c) = iter.next() { + if c.has_indexed_selector_in_subject() { + return true; + } + } + } + }, + _ => (), + }; + false + } +} + +#[derive(Clone, Eq, PartialEq, ToShmem)] +#[shmem(no_bounds)] +pub struct LocalName<Impl: SelectorImpl> { + #[shmem(field_bound)] + pub name: Impl::LocalName, + pub lower_name: Impl::LocalName, +} + +impl<Impl: SelectorImpl> Debug for Selector<Impl> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Selector(")?; + self.to_css(f)?; + write!( + f, + ", specificity = {:#x}, flags = {:?})", + self.specificity(), + self.flags() + ) + } +} + +impl<Impl: SelectorImpl> Debug for Component<Impl> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(f) + } +} +impl<Impl: SelectorImpl> Debug for AttrSelectorWithOptionalNamespace<Impl> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(f) + } +} +impl<Impl: SelectorImpl> Debug for LocalName<Impl> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(f) + } +} + +fn serialize_selector_list<'a, Impl, I, W>(iter: I, dest: &mut W) -> fmt::Result +where + Impl: SelectorImpl, + I: Iterator<Item = &'a Selector<Impl>>, + W: fmt::Write, +{ + let mut first = true; + for selector in iter { + if !first { + dest.write_str(", ")?; + } + first = false; + selector.to_css(dest)?; + } + Ok(()) +} + +impl<Impl: SelectorImpl> ToCss for SelectorList<Impl> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + serialize_selector_list(self.slice().iter(), dest) + } +} + +impl<Impl: SelectorImpl> ToCss for Selector<Impl> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // Compound selectors invert the order of their contents, so we need to + // undo that during serialization. + // + // This two-iterator strategy involves walking over the selector twice. + // We could do something more clever, but selector serialization probably + // isn't hot enough to justify it, and the stringification likely + // dominates anyway. + // + // NB: A parse-order iterator is a Rev<>, which doesn't expose as_slice(), + // which we need for |split|. So we split by combinators on a match-order + // sequence and then reverse. + + let mut combinators = self + .iter_raw_match_order() + .rev() + .filter_map(|x| x.as_combinator()); + let compound_selectors = self + .iter_raw_match_order() + .as_slice() + .split(|x| x.is_combinator()) + .rev(); + + let mut combinators_exhausted = false; + for compound in compound_selectors { + debug_assert!(!combinators_exhausted); + + // https://drafts.csswg.org/cssom/#serializing-selectors + if compound.is_empty() { + continue; + } + if let Component::RelativeSelectorAnchor = compound.first().unwrap() { + debug_assert!( + compound.len() == 1, + "RelativeLeft should only be a simple selector" + ); + combinators.next().unwrap().to_css_relative(dest)?; + continue; + } + + // 1. If there is only one simple selector in the compound selectors + // which is a universal selector, append the result of + // serializing the universal selector to s. + // + // Check if `!compound.empty()` first--this can happen if we have + // something like `... > ::before`, because we store `>` and `::` + // both as combinators internally. + // + // If we are in this case, after we have serialized the universal + // selector, we skip Step 2 and continue with the algorithm. + let (can_elide_namespace, first_non_namespace) = match compound[0] { + Component::ExplicitAnyNamespace | + Component::ExplicitNoNamespace | + Component::Namespace(..) => (false, 1), + Component::DefaultNamespace(..) => (true, 1), + _ => (true, 0), + }; + let mut perform_step_2 = true; + let next_combinator = combinators.next(); + if first_non_namespace == compound.len() - 1 { + match (next_combinator, &compound[first_non_namespace]) { + // We have to be careful here, because if there is a + // pseudo element "combinator" there isn't really just + // the one simple selector. Technically this compound + // selector contains the pseudo element selector as well + // -- Combinator::PseudoElement, just like + // Combinator::SlotAssignment, don't exist in the + // spec. + (Some(Combinator::PseudoElement), _) | + (Some(Combinator::SlotAssignment), _) => (), + (_, &Component::ExplicitUniversalType) => { + // Iterate over everything so we serialize the namespace + // too. + for simple in compound.iter() { + simple.to_css(dest)?; + } + // Skip step 2, which is an "otherwise". + perform_step_2 = false; + }, + _ => (), + } + } + + // 2. Otherwise, for each simple selector in the compound selectors + // that is not a universal selector of which the namespace prefix + // maps to a namespace that is not the default namespace + // serialize the simple selector and append the result to s. + // + // See https://github.com/w3c/csswg-drafts/issues/1606, which is + // proposing to change this to match up with the behavior asserted + // in cssom/serialize-namespaced-type-selectors.html, which the + // following code tries to match. + if perform_step_2 { + for simple in compound.iter() { + if let Component::ExplicitUniversalType = *simple { + // Can't have a namespace followed by a pseudo-element + // selector followed by a universal selector in the same + // compound selector, so we don't have to worry about the + // real namespace being in a different `compound`. + if can_elide_namespace { + continue; + } + } + simple.to_css(dest)?; + } + } + + // 3. If this is not the last part of the chain of the selector + // append a single SPACE (U+0020), followed by the combinator + // ">", "+", "~", ">>", "||", as appropriate, followed by another + // single SPACE (U+0020) if the combinator was not whitespace, to + // s. + match next_combinator { + Some(c) => c.to_css(dest)?, + None => combinators_exhausted = true, + }; + + // 4. If this is the last part of the chain of the selector and + // there is a pseudo-element, append "::" followed by the name of + // the pseudo-element, to s. + // + // (we handle this above) + } + + Ok(()) + } +} + +impl Combinator { + fn to_css_internal<W>(&self, dest: &mut W, prefix_space: bool) -> fmt::Result + where + W: fmt::Write, + { + if matches!( + *self, + Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment + ) { + return Ok(()); + } + if prefix_space { + dest.write_char(' ')?; + } + match *self { + Combinator::Child => dest.write_str("> "), + Combinator::Descendant => Ok(()), + Combinator::NextSibling => dest.write_str("+ "), + Combinator::LaterSibling => dest.write_str("~ "), + Combinator::PseudoElement | Combinator::Part | Combinator::SlotAssignment => unsafe { + debug_unreachable!("Already handled") + }, + } + } + + fn to_css_relative<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + self.to_css_internal(dest, false) + } +} + +impl ToCss for Combinator { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + self.to_css_internal(dest, true) + } +} + +impl<Impl: SelectorImpl> ToCss for Component<Impl> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + use self::Component::*; + + match *self { + Combinator(ref c) => c.to_css(dest), + Slotted(ref selector) => { + dest.write_str("::slotted(")?; + selector.to_css(dest)?; + dest.write_char(')') + }, + Part(ref part_names) => { + dest.write_str("::part(")?; + for (i, name) in part_names.iter().enumerate() { + if i != 0 { + dest.write_char(' ')?; + } + name.to_css(dest)?; + } + dest.write_char(')') + }, + PseudoElement(ref p) => p.to_css(dest), + ID(ref s) => { + dest.write_char('#')?; + s.to_css(dest) + }, + Class(ref s) => { + dest.write_char('.')?; + s.to_css(dest) + }, + LocalName(ref s) => s.to_css(dest), + ExplicitUniversalType => dest.write_char('*'), + + DefaultNamespace(_) => Ok(()), + ExplicitNoNamespace => dest.write_char('|'), + ExplicitAnyNamespace => dest.write_str("*|"), + Namespace(ref prefix, _) => { + prefix.to_css(dest)?; + dest.write_char('|') + }, + + AttributeInNoNamespaceExists { ref local_name, .. } => { + dest.write_char('[')?; + local_name.to_css(dest)?; + dest.write_char(']') + }, + AttributeInNoNamespace { + ref local_name, + operator, + ref value, + case_sensitivity, + .. + } => { + dest.write_char('[')?; + local_name.to_css(dest)?; + operator.to_css(dest)?; + value.to_css(dest)?; + match case_sensitivity { + ParsedCaseSensitivity::CaseSensitive | + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}, + ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + dest.write_char(']') + }, + AttributeOther(ref attr_selector) => attr_selector.to_css(dest), + + // Pseudo-classes + Root => dest.write_str(":root"), + Empty => dest.write_str(":empty"), + Scope => dest.write_str(":scope"), + ParentSelector => dest.write_char('&'), + Host(ref selector) => { + dest.write_str(":host")?; + if let Some(ref selector) = *selector { + dest.write_char('(')?; + selector.to_css(dest)?; + dest.write_char(')')?; + } + Ok(()) + }, + Nth(ref nth_data) => { + nth_data.write_start(dest)?; + if nth_data.is_function { + nth_data.write_affine(dest)?; + dest.write_char(')')?; + } + Ok(()) + }, + NthOf(ref nth_of_data) => { + let nth_data = nth_of_data.nth_data(); + nth_data.write_start(dest)?; + debug_assert!( + nth_data.is_function, + "A selector must be a function to hold An+B notation" + ); + nth_data.write_affine(dest)?; + debug_assert!( + matches!(nth_data.ty, NthType::Child | NthType::LastChild), + "Only :nth-child or :nth-last-child can be of a selector list" + ); + debug_assert!( + !nth_of_data.selectors().is_empty(), + "The selector list should not be empty" + ); + dest.write_str(" of ")?; + serialize_selector_list(nth_of_data.selectors().iter(), dest)?; + dest.write_char(')') + }, + Is(ref list) | Where(ref list) | Negation(ref list) => { + match *self { + Where(..) => dest.write_str(":where(")?, + Is(..) => dest.write_str(":is(")?, + Negation(..) => dest.write_str(":not(")?, + _ => unreachable!(), + } + serialize_selector_list(list.slice().iter(), dest)?; + dest.write_str(")") + }, + Has(ref list) => { + dest.write_str(":has(")?; + let mut first = true; + for RelativeSelector { ref selector, .. } in list.iter() { + if !first { + dest.write_str(", ")?; + } + first = false; + selector.to_css(dest)?; + } + dest.write_str(")") + }, + NonTSPseudoClass(ref pseudo) => pseudo.to_css(dest), + Invalid(ref css) => dest.write_str(css), + RelativeSelectorAnchor => Ok(()), + } + } +} + +impl<Impl: SelectorImpl> ToCss for AttrSelectorWithOptionalNamespace<Impl> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_char('[')?; + match self.namespace { + Some(NamespaceConstraint::Specific((ref prefix, _))) => { + prefix.to_css(dest)?; + dest.write_char('|')? + }, + Some(NamespaceConstraint::Any) => dest.write_str("*|")?, + None => {}, + } + self.local_name.to_css(dest)?; + match self.operation { + ParsedAttrSelectorOperation::Exists => {}, + ParsedAttrSelectorOperation::WithValue { + operator, + case_sensitivity, + ref value, + } => { + operator.to_css(dest)?; + value.to_css(dest)?; + match case_sensitivity { + ParsedCaseSensitivity::CaseSensitive | + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument => {}, + ParsedCaseSensitivity::AsciiCaseInsensitive => dest.write_str(" i")?, + ParsedCaseSensitivity::ExplicitCaseSensitive => dest.write_str(" s")?, + } + }, + } + dest.write_char(']') + } +} + +impl<Impl: SelectorImpl> ToCss for LocalName<Impl> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + self.name.to_css(dest) + } +} + +/// Build up a Selector. +/// selector : simple_selector_sequence [ combinator simple_selector_sequence ]* ; +/// +/// `Err` means invalid selector. +fn parse_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + mut state: SelectorParsingState, + parse_relative: ParseRelative, +) -> Result<Selector<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let mut builder = SelectorBuilder::default(); + + // Helps rewind less, but also simplifies dealing with relative combinators below. + input.skip_whitespace(); + + if parse_relative != ParseRelative::No { + let combinator = try_parse_combinator::<P, Impl>(input); + match parse_relative { + ParseRelative::ForHas => { + builder.push_simple_selector(Component::RelativeSelectorAnchor); + // Do we see a combinator? If so, push that. Otherwise, push a descendant + // combinator. + builder.push_combinator(combinator.unwrap_or(Combinator::Descendant)); + }, + ParseRelative::ForNesting => { + if let Ok(combinator) = combinator { + builder.push_simple_selector(Component::ParentSelector); + builder.push_combinator(combinator); + } + }, + ParseRelative::No => unreachable!(), + } + } + 'outer_loop: loop { + // Parse a sequence of simple selectors. + let empty = parse_compound_selector(parser, &mut state, input, &mut builder)?; + if empty { + return Err(input.new_custom_error(if builder.has_combinators() { + SelectorParseErrorKind::DanglingCombinator + } else { + SelectorParseErrorKind::EmptySelector + })); + } + + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + debug_assert!(state.intersects( + SelectorParsingState::AFTER_PSEUDO_ELEMENT | + SelectorParsingState::AFTER_SLOTTED | + SelectorParsingState::AFTER_PART + )); + break; + } + + let combinator = if let Ok(c) = try_parse_combinator::<P, Impl>(input) { + c + } else { + break 'outer_loop; + }; + + if !state.allows_combinators() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + + builder.push_combinator(combinator); + } + return Ok(Selector(builder.build())); +} + +fn try_parse_combinator<'i, 't, P, Impl>(input: &mut CssParser<'i, 't>) -> Result<Combinator, ()> { + let mut any_whitespace = false; + loop { + let before_this_token = input.state(); + match input.next_including_whitespace() { + Err(_e) => return Err(()), + Ok(&Token::WhiteSpace(_)) => any_whitespace = true, + Ok(&Token::Delim('>')) => { + return Ok(Combinator::Child); + }, + Ok(&Token::Delim('+')) => { + return Ok(Combinator::NextSibling); + }, + Ok(&Token::Delim('~')) => { + return Ok(Combinator::LaterSibling); + }, + Ok(_) => { + input.reset(&before_this_token); + if any_whitespace { + return Ok(Combinator::Descendant); + } else { + return Err(()); + } + }, + } + } +} + +/// * `Err(())`: Invalid selector, abort +/// * `Ok(false)`: Not a type selector, could be something else. `input` was not consumed. +/// * `Ok(true)`: Length 0 (`*|*`), 1 (`*|E` or `ns|*`) or 2 (`|E` or `ns|E`) +fn parse_type_selector<'i, 't, P, Impl, S>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + sink: &mut S, +) -> Result<bool, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, + S: Push<Component<Impl>>, +{ + match parse_qualified_name(parser, input, /* in_attr_selector = */ false) { + Err(ParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput), + .. + }) | + Ok(OptionalQName::None(_)) => Ok(false), + Ok(OptionalQName::Some(namespace, local_name)) => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + match namespace { + QNamePrefix::ImplicitAnyNamespace => {}, + QNamePrefix::ImplicitDefaultNamespace(url) => { + sink.push(Component::DefaultNamespace(url)) + }, + QNamePrefix::ExplicitNamespace(prefix, url) => { + sink.push(match parser.default_namespace() { + Some(ref default_url) if url == *default_url => { + Component::DefaultNamespace(url) + }, + _ => Component::Namespace(prefix, url), + }) + }, + QNamePrefix::ExplicitNoNamespace => sink.push(Component::ExplicitNoNamespace), + QNamePrefix::ExplicitAnyNamespace => { + match parser.default_namespace() { + // Element type selectors that have no namespace + // component (no namespace separator) represent elements + // without regard to the element's namespace (equivalent + // to "*|") unless a default namespace has been declared + // for namespaced selectors (e.g. in CSS, in the style + // sheet). If a default namespace has been declared, + // such selectors will represent only elements in the + // default namespace. + // -- Selectors § 6.1.1 + // So we'll have this act the same as the + // QNamePrefix::ImplicitAnyNamespace case. + None => {}, + Some(_) => sink.push(Component::ExplicitAnyNamespace), + } + }, + QNamePrefix::ImplicitNoNamespace => { + unreachable!() // Not returned with in_attr_selector = false + }, + } + match local_name { + Some(name) => sink.push(Component::LocalName(LocalName { + lower_name: to_ascii_lowercase(&name).as_ref().into(), + name: name.as_ref().into(), + })), + None => sink.push(Component::ExplicitUniversalType), + } + Ok(true) + }, + Err(e) => Err(e), + } +} + +#[derive(Debug)] +enum SimpleSelectorParseResult<Impl: SelectorImpl> { + SimpleSelector(Component<Impl>), + PseudoElement(Impl::PseudoElement), + SlottedPseudo(Selector<Impl>), + PartPseudo(Box<[Impl::Identifier]>), +} + +#[derive(Debug)] +enum QNamePrefix<Impl: SelectorImpl> { + ImplicitNoNamespace, // `foo` in attr selectors + ImplicitAnyNamespace, // `foo` in type selectors, without a default ns + ImplicitDefaultNamespace(Impl::NamespaceUrl), // `foo` in type selectors, with a default ns + ExplicitNoNamespace, // `|foo` + ExplicitAnyNamespace, // `*|foo` + ExplicitNamespace(Impl::NamespacePrefix, Impl::NamespaceUrl), // `prefix|foo` +} + +enum OptionalQName<'i, Impl: SelectorImpl> { + Some(QNamePrefix<Impl>, Option<CowRcStr<'i>>), + None(Token<'i>), +} + +/// * `Err(())`: Invalid selector, abort +/// * `Ok(None(token))`: Not a simple selector, could be something else. `input` was not consumed, +/// but the token is still returned. +/// * `Ok(Some(namespace, local_name))`: `None` for the local name means a `*` universal selector +fn parse_qualified_name<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + in_attr_selector: bool, +) -> Result<OptionalQName<'i, Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let default_namespace = |local_name| { + let namespace = match parser.default_namespace() { + Some(url) => QNamePrefix::ImplicitDefaultNamespace(url), + None => QNamePrefix::ImplicitAnyNamespace, + }; + Ok(OptionalQName::Some(namespace, local_name)) + }; + + let explicit_namespace = |input: &mut CssParser<'i, 't>, namespace| { + let location = input.current_source_location(); + match input.next_including_whitespace() { + Ok(&Token::Delim('*')) if !in_attr_selector => Ok(OptionalQName::Some(namespace, None)), + Ok(&Token::Ident(ref local_name)) => { + Ok(OptionalQName::Some(namespace, Some(local_name.clone()))) + }, + Ok(t) if in_attr_selector => { + let e = SelectorParseErrorKind::InvalidQualNameInAttr(t.clone()); + Err(location.new_custom_error(e)) + }, + Ok(t) => Err(location.new_custom_error( + SelectorParseErrorKind::ExplicitNamespaceUnexpectedToken(t.clone()), + )), + Err(e) => Err(e.into()), + } + }; + + let start = input.state(); + match input.next_including_whitespace() { + Ok(Token::Ident(value)) => { + let value = value.clone(); + let after_ident = input.state(); + match input.next_including_whitespace() { + Ok(&Token::Delim('|')) => { + let prefix = value.as_ref().into(); + let result = parser.namespace_for_prefix(&prefix); + let url = result.ok_or( + after_ident + .source_location() + .new_custom_error(SelectorParseErrorKind::ExpectedNamespace(value)), + )?; + explicit_namespace(input, QNamePrefix::ExplicitNamespace(prefix, url)) + }, + _ => { + input.reset(&after_ident); + if in_attr_selector { + Ok(OptionalQName::Some( + QNamePrefix::ImplicitNoNamespace, + Some(value), + )) + } else { + default_namespace(Some(value)) + } + }, + } + }, + Ok(Token::Delim('*')) => { + let after_star = input.state(); + match input.next_including_whitespace() { + Ok(&Token::Delim('|')) => { + explicit_namespace(input, QNamePrefix::ExplicitAnyNamespace) + }, + _ if !in_attr_selector => { + input.reset(&after_star); + default_namespace(None) + }, + result => { + let t = result?; + Err(after_star + .source_location() + .new_custom_error(SelectorParseErrorKind::ExpectedBarInAttr(t.clone()))) + }, + } + }, + Ok(Token::Delim('|')) => explicit_namespace(input, QNamePrefix::ExplicitNoNamespace), + Ok(t) => { + let t = t.clone(); + input.reset(&start); + Ok(OptionalQName::None(t)) + }, + Err(e) => { + input.reset(&start); + Err(e.into()) + }, + } +} + +fn parse_attribute_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, +) -> Result<Component<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let namespace; + let local_name; + + input.skip_whitespace(); + + match parse_qualified_name(parser, input, /* in_attr_selector = */ true)? { + OptionalQName::None(t) => { + return Err(input.new_custom_error( + SelectorParseErrorKind::NoQualifiedNameInAttributeSelector(t), + )); + }, + OptionalQName::Some(_, None) => unreachable!(), + OptionalQName::Some(ns, Some(ln)) => { + local_name = ln; + namespace = match ns { + QNamePrefix::ImplicitNoNamespace | QNamePrefix::ExplicitNoNamespace => None, + QNamePrefix::ExplicitNamespace(prefix, url) => { + Some(NamespaceConstraint::Specific((prefix, url))) + }, + QNamePrefix::ExplicitAnyNamespace => Some(NamespaceConstraint::Any), + QNamePrefix::ImplicitAnyNamespace | QNamePrefix::ImplicitDefaultNamespace(_) => { + unreachable!() // Not returned with in_attr_selector = true + }, + } + }, + } + + let location = input.current_source_location(); + let operator = match input.next() { + // [foo] + Err(_) => { + let local_name_lower = to_ascii_lowercase(&local_name).as_ref().into(); + let local_name = local_name.as_ref().into(); + if let Some(namespace) = namespace { + return Ok(Component::AttributeOther(Box::new( + AttrSelectorWithOptionalNamespace { + namespace: Some(namespace), + local_name, + local_name_lower, + operation: ParsedAttrSelectorOperation::Exists, + }, + ))); + } else { + return Ok(Component::AttributeInNoNamespaceExists { + local_name, + local_name_lower, + }); + } + }, + + // [foo=bar] + Ok(&Token::Delim('=')) => AttrSelectorOperator::Equal, + // [foo~=bar] + Ok(&Token::IncludeMatch) => AttrSelectorOperator::Includes, + // [foo|=bar] + Ok(&Token::DashMatch) => AttrSelectorOperator::DashMatch, + // [foo^=bar] + Ok(&Token::PrefixMatch) => AttrSelectorOperator::Prefix, + // [foo*=bar] + Ok(&Token::SubstringMatch) => AttrSelectorOperator::Substring, + // [foo$=bar] + Ok(&Token::SuffixMatch) => AttrSelectorOperator::Suffix, + Ok(t) => { + return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedTokenInAttributeSelector(t.clone()), + )); + }, + }; + + let value = match input.expect_ident_or_string() { + Ok(t) => t.clone(), + Err(BasicParseError { + kind: BasicParseErrorKind::UnexpectedToken(t), + location, + }) => return Err(location.new_custom_error(SelectorParseErrorKind::BadValueInAttr(t))), + Err(e) => return Err(e.into()), + }; + + let attribute_flags = parse_attribute_flags(input)?; + let value = value.as_ref().into(); + let local_name_lower; + let local_name_is_ascii_lowercase; + let case_sensitivity; + { + let local_name_lower_cow = to_ascii_lowercase(&local_name); + case_sensitivity = + attribute_flags.to_case_sensitivity(local_name_lower_cow.as_ref(), namespace.is_some()); + local_name_lower = local_name_lower_cow.as_ref().into(); + local_name_is_ascii_lowercase = matches!(local_name_lower_cow, Cow::Borrowed(..)); + } + let local_name = local_name.as_ref().into(); + if namespace.is_some() || !local_name_is_ascii_lowercase { + Ok(Component::AttributeOther(Box::new( + AttrSelectorWithOptionalNamespace { + namespace, + local_name, + local_name_lower, + operation: ParsedAttrSelectorOperation::WithValue { + operator, + case_sensitivity, + value, + }, + }, + ))) + } else { + Ok(Component::AttributeInNoNamespace { + local_name, + operator, + value, + case_sensitivity, + }) + } +} + +/// An attribute selector can have 's' or 'i' as flags, or no flags at all. +enum AttributeFlags { + // Matching should be case-sensitive ('s' flag). + CaseSensitive, + // Matching should be case-insensitive ('i' flag). + AsciiCaseInsensitive, + // No flags. Matching behavior depends on the name of the attribute. + CaseSensitivityDependsOnName, +} + +impl AttributeFlags { + fn to_case_sensitivity(self, local_name: &str, have_namespace: bool) -> ParsedCaseSensitivity { + match self { + AttributeFlags::CaseSensitive => ParsedCaseSensitivity::ExplicitCaseSensitive, + AttributeFlags::AsciiCaseInsensitive => ParsedCaseSensitivity::AsciiCaseInsensitive, + AttributeFlags::CaseSensitivityDependsOnName => { + if !have_namespace && + include!(concat!( + env!("OUT_DIR"), + "/ascii_case_insensitive_html_attributes.rs" + )) + .contains(local_name) + { + ParsedCaseSensitivity::AsciiCaseInsensitiveIfInHtmlElementInHtmlDocument + } else { + ParsedCaseSensitivity::CaseSensitive + } + }, + } + } +} + +fn parse_attribute_flags<'i, 't>( + input: &mut CssParser<'i, 't>, +) -> Result<AttributeFlags, BasicParseError<'i>> { + let location = input.current_source_location(); + let token = match input.next() { + Ok(t) => t, + Err(..) => { + // Selectors spec says language-defined; HTML says it depends on the + // exact attribute name. + return Ok(AttributeFlags::CaseSensitivityDependsOnName); + }, + }; + + let ident = match *token { + Token::Ident(ref i) => i, + ref other => return Err(location.new_basic_unexpected_token_error(other.clone())), + }; + + Ok(match_ignore_ascii_case! { + ident, + "i" => AttributeFlags::AsciiCaseInsensitive, + "s" => AttributeFlags::CaseSensitive, + _ => return Err(location.new_basic_unexpected_token_error(token.clone())), + }) +} + +/// Level 3: Parse **one** simple_selector. (Though we might insert a second +/// implied "<defaultns>|*" type selector.) +fn parse_negation<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result<Component<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let list = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS, + ForgivingParsing::No, + ParseRelative::No, + )?; + + Ok(Component::Negation(list)) +} + +/// simple_selector_sequence +/// : [ type_selector | universal ] [ HASH | class | attrib | pseudo | negation ]* +/// | [ HASH | class | attrib | pseudo | negation ]+ +/// +/// `Err(())` means invalid selector. +/// `Ok(true)` is an empty selector +fn parse_compound_selector<'i, 't, P, Impl>( + parser: &P, + state: &mut SelectorParsingState, + input: &mut CssParser<'i, 't>, + builder: &mut SelectorBuilder<Impl>, +) -> Result<bool, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + input.skip_whitespace(); + + let mut empty = true; + if parse_type_selector(parser, input, *state, builder)? { + empty = false; + } + + loop { + let result = match parse_one_simple_selector(parser, input, *state)? { + None => break, + Some(result) => result, + }; + + if empty { + if let Some(url) = parser.default_namespace() { + // If there was no explicit type selector, but there is a + // default namespace, there is an implicit "<defaultns>|*" type + // selector. Except for :host() or :not() / :is() / :where(), + // where we ignore it. + // + // https://drafts.csswg.org/css-scoping/#host-element-in-tree: + // + // When considered within its own shadow trees, the shadow + // host is featureless. Only the :host, :host(), and + // :host-context() pseudo-classes are allowed to match it. + // + // https://drafts.csswg.org/selectors-4/#featureless: + // + // A featureless element does not match any selector at all, + // except those it is explicitly defined to match. If a + // given selector is allowed to match a featureless element, + // it must do so while ignoring the default namespace. + // + // https://drafts.csswg.org/selectors-4/#matches + // + // Default namespace declarations do not affect the compound + // selector representing the subject of any selector within + // a :is() pseudo-class, unless that compound selector + // contains an explicit universal selector or type selector. + // + // (Similar quotes for :where() / :not()) + // + let ignore_default_ns = state + .intersects(SelectorParsingState::SKIP_DEFAULT_NAMESPACE) || + matches!( + result, + SimpleSelectorParseResult::SimpleSelector(Component::Host(..)) + ); + if !ignore_default_ns { + builder.push_simple_selector(Component::DefaultNamespace(url)); + } + } + } + + empty = false; + + match result { + SimpleSelectorParseResult::SimpleSelector(s) => { + builder.push_simple_selector(s); + }, + SimpleSelectorParseResult::PartPseudo(part_names) => { + state.insert(SelectorParsingState::AFTER_PART); + builder.push_combinator(Combinator::Part); + builder.push_simple_selector(Component::Part(part_names)); + }, + SimpleSelectorParseResult::SlottedPseudo(selector) => { + state.insert(SelectorParsingState::AFTER_SLOTTED); + builder.push_combinator(Combinator::SlotAssignment); + builder.push_simple_selector(Component::Slotted(selector)); + }, + SimpleSelectorParseResult::PseudoElement(p) => { + state.insert(SelectorParsingState::AFTER_PSEUDO_ELEMENT); + if !p.accepts_state_pseudo_classes() { + state.insert(SelectorParsingState::AFTER_NON_STATEFUL_PSEUDO_ELEMENT); + } + builder.push_combinator(Combinator::PseudoElement); + builder.push_simple_selector(Component::PseudoElement(p)); + }, + } + } + Ok(empty) +} + +fn parse_is_where<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + component: impl FnOnce(SelectorList<Impl>) -> Component<Impl>, +) -> Result<Component<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + debug_assert!(parser.parse_is_and_where()); + // https://drafts.csswg.org/selectors/#matches-pseudo: + // + // Pseudo-elements cannot be represented by the matches-any + // pseudo-class; they are not valid within :is(). + // + let inner = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS, + ForgivingParsing::Yes, + ParseRelative::No, + )?; + Ok(component(inner)) +} + +fn parse_has<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result<Component<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + debug_assert!(parser.parse_has()); + if state.intersects(SelectorParsingState::DISALLOW_RELATIVE_SELECTOR) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + // Nested `:has()` is disallowed, mark it as such. + // Note: The spec defines ":has-allowed pseudo-element," but there's no + // pseudo-element defined as such at the moment. + // https://w3c.github.io/csswg-drafts/selectors-4/#has-allowed-pseudo-element + let inner = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS | + SelectorParsingState::DISALLOW_RELATIVE_SELECTOR, + ForgivingParsing::No, + ParseRelative::ForHas, + )?; + Ok(Component::Has(RelativeSelector::from_selector_list(inner))) +} + +fn parse_functional_pseudo_class<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + name: CowRcStr<'i>, + state: SelectorParsingState, +) -> Result<Component<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + match_ignore_ascii_case! { &name, + "nth-child" => return parse_nth_pseudo_class(parser, input, state, NthType::Child), + "nth-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::OfType), + "nth-last-child" => return parse_nth_pseudo_class(parser, input, state, NthType::LastChild), + "nth-last-of-type" => return parse_nth_pseudo_class(parser, input, state, NthType::LastOfType), + "is" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Is), + "where" if parser.parse_is_and_where() => return parse_is_where(parser, input, state, Component::Where), + "has" if parser.parse_has() => return parse_has(parser, input, state), + "host" => { + if !state.allows_tree_structural_pseudo_classes() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + return Ok(Component::Host(Some(parse_inner_compound_selector(parser, input, state)?))); + }, + "not" => { + return parse_negation(parser, input, state) + }, + _ => {} + } + + if parser.parse_is_and_where() && parser.is_is_alias(&name) { + return parse_is_where(parser, input, state, Component::Is); + } + + if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT | SelectorParsingState::AFTER_SLOTTED) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + + let after_part = state.intersects(SelectorParsingState::AFTER_PART); + P::parse_non_ts_functional_pseudo_class(parser, name, input, after_part).map(Component::NonTSPseudoClass) +} + +fn parse_nth_pseudo_class<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, + ty: NthType, +) -> Result<Component<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + if !state.allows_tree_structural_pseudo_classes() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let (a, b) = parse_nth(input)?; + let nth_data = NthSelectorData { + ty, + is_function: true, + a, + b, + }; + if !parser.parse_nth_child_of() || ty.is_of_type() { + return Ok(Component::Nth(nth_data)); + } + + // Try to parse "of <selector-list>". + if input.try_parse(|i| i.expect_ident_matching("of")).is_err() { + return Ok(Component::Nth(nth_data)); + } + // Whitespace between "of" and the selector list is optional + // https://github.com/w3c/csswg-drafts/issues/8285 + let selectors = SelectorList::parse_with_state( + parser, + input, + state | + SelectorParsingState::SKIP_DEFAULT_NAMESPACE | + SelectorParsingState::DISALLOW_PSEUDOS, + ForgivingParsing::No, + ParseRelative::No, + )?; + Ok(Component::NthOf(NthOfSelectorData::new( + &nth_data, + selectors.slice().iter().cloned(), + ))) +} + +/// Returns whether the name corresponds to a CSS2 pseudo-element that +/// can be specified with the single colon syntax (in addition to the +/// double-colon syntax, which can be used for all pseudo-elements). +fn is_css2_pseudo_element(name: &str) -> bool { + // ** Do not add to this list! ** + match_ignore_ascii_case! { name, + "before" | "after" | "first-line" | "first-letter" => true, + _ => false, + } +} + +/// Parse a simple selector other than a type selector. +/// +/// * `Err(())`: Invalid selector, abort +/// * `Ok(None)`: Not a simple selector, could be something else. `input` was not consumed. +/// * `Ok(Some(_))`: Parsed a simple selector or pseudo-element +fn parse_one_simple_selector<'i, 't, P, Impl>( + parser: &P, + input: &mut CssParser<'i, 't>, + state: SelectorParsingState, +) -> Result<Option<SimpleSelectorParseResult<Impl>>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + let start = input.state(); + let token = match input.next_including_whitespace().map(|t| t.clone()) { + Ok(t) => t, + Err(..) => { + input.reset(&start); + return Ok(None); + }, + }; + + Ok(Some(match token { + Token::IDHash(id) => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let id = Component::ID(id.as_ref().into()); + SimpleSelectorParseResult::SimpleSelector(id) + }, + Token::Delim(delim) if delim == '.' || (delim == '&' && parser.parse_parent_selector()) => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let location = input.current_source_location(); + SimpleSelectorParseResult::SimpleSelector(if delim == '&' { + Component::ParentSelector + } else { + let class = match *input.next_including_whitespace()? { + Token::Ident(ref class) => class, + ref t => { + let e = SelectorParseErrorKind::ClassNeedsIdent(t.clone()); + return Err(location.new_custom_error(e)); + }, + }; + Component::Class(class.as_ref().into()) + }) + }, + Token::SquareBracketBlock => { + if state.intersects(SelectorParsingState::AFTER_PSEUDO) { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let attr = input.parse_nested_block(|input| parse_attribute_selector(parser, input))?; + SimpleSelectorParseResult::SimpleSelector(attr) + }, + Token::Colon => { + let location = input.current_source_location(); + let (is_single_colon, next_token) = match input.next_including_whitespace()?.clone() { + Token::Colon => (false, input.next_including_whitespace()?.clone()), + t => (true, t), + }; + let (name, is_functional) = match next_token { + Token::Ident(name) => (name, false), + Token::Function(name) => (name, true), + t => { + let e = SelectorParseErrorKind::PseudoElementExpectedIdent(t); + return Err(input.new_custom_error(e)); + }, + }; + let is_pseudo_element = !is_single_colon || is_css2_pseudo_element(&name); + if is_pseudo_element { + if !state.allows_pseudos() { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + let pseudo_element = if is_functional { + if P::parse_part(parser) && name.eq_ignore_ascii_case("part") { + if !state.allows_part() { + return Err( + input.new_custom_error(SelectorParseErrorKind::InvalidState) + ); + } + let names = input.parse_nested_block(|input| { + let mut result = Vec::with_capacity(1); + result.push(input.expect_ident()?.as_ref().into()); + while !input.is_exhausted() { + result.push(input.expect_ident()?.as_ref().into()); + } + Ok(result.into_boxed_slice()) + })?; + return Ok(Some(SimpleSelectorParseResult::PartPseudo(names))); + } + if P::parse_slotted(parser) && name.eq_ignore_ascii_case("slotted") { + if !state.allows_slotted() { + return Err( + input.new_custom_error(SelectorParseErrorKind::InvalidState) + ); + } + let selector = input.parse_nested_block(|input| { + parse_inner_compound_selector(parser, input, state) + })?; + return Ok(Some(SimpleSelectorParseResult::SlottedPseudo(selector))); + } + input.parse_nested_block(|input| { + P::parse_functional_pseudo_element(parser, name, input) + })? + } else { + P::parse_pseudo_element(parser, location, name)? + }; + + if state.intersects(SelectorParsingState::AFTER_SLOTTED) && + !pseudo_element.valid_after_slotted() + { + return Err(input.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + SimpleSelectorParseResult::PseudoElement(pseudo_element) + } else { + let pseudo_class = if is_functional { + input.parse_nested_block(|input| { + parse_functional_pseudo_class(parser, input, name, state) + })? + } else { + parse_simple_pseudo_class(parser, location, name, state)? + }; + SimpleSelectorParseResult::SimpleSelector(pseudo_class) + } + }, + _ => { + input.reset(&start); + return Ok(None); + }, + })) +} + +fn parse_simple_pseudo_class<'i, P, Impl>( + parser: &P, + location: SourceLocation, + name: CowRcStr<'i>, + state: SelectorParsingState, +) -> Result<Component<Impl>, ParseError<'i, P::Error>> +where + P: Parser<'i, Impl = Impl>, + Impl: SelectorImpl, +{ + if !state.allows_non_functional_pseudo_classes() { + return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + + if state.allows_tree_structural_pseudo_classes() { + match_ignore_ascii_case! { &name, + "first-child" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ false))), + "last-child" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ false))), + "only-child" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ false))), + "root" => return Ok(Component::Root), + "empty" => return Ok(Component::Empty), + "scope" => return Ok(Component::Scope), + "host" if P::parse_host(parser) => return Ok(Component::Host(None)), + "first-of-type" => return Ok(Component::Nth(NthSelectorData::first(/* of_type = */ true))), + "last-of-type" => return Ok(Component::Nth(NthSelectorData::last(/* of_type = */ true))), + "only-of-type" => return Ok(Component::Nth(NthSelectorData::only(/* of_type = */ true))), + _ => {}, + } + } + + let pseudo_class = P::parse_non_ts_pseudo_class(parser, location, name)?; + if state.intersects(SelectorParsingState::AFTER_PSEUDO_ELEMENT) && + !pseudo_class.is_user_action_state() + { + return Err(location.new_custom_error(SelectorParseErrorKind::InvalidState)); + } + Ok(Component::NonTSPseudoClass(pseudo_class)) +} + +// NB: pub module in order to access the DummyParser +#[cfg(test)] +pub mod tests { + use super::*; + use crate::builder::SelectorFlags; + use crate::parser; + use cssparser::{serialize_identifier, Parser as CssParser, ParserInput, ToCss}; + use std::collections::HashMap; + use std::fmt; + + #[derive(Clone, Debug, Eq, PartialEq)] + pub enum PseudoClass { + Hover, + Active, + Lang(String), + } + + #[derive(Clone, Debug, Eq, PartialEq)] + pub enum PseudoElement { + Before, + After, + Highlight(String), + } + + impl parser::PseudoElement for PseudoElement { + type Impl = DummySelectorImpl; + + fn accepts_state_pseudo_classes(&self) -> bool { + true + } + + fn valid_after_slotted(&self) -> bool { + true + } + } + + impl parser::NonTSPseudoClass for PseudoClass { + type Impl = DummySelectorImpl; + + #[inline] + fn is_active_or_hover(&self) -> bool { + matches!(*self, PseudoClass::Active | PseudoClass::Hover) + } + + #[inline] + fn is_user_action_state(&self) -> bool { + self.is_active_or_hover() + } + } + + impl ToCss for PseudoClass { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + PseudoClass::Hover => dest.write_str(":hover"), + PseudoClass::Active => dest.write_str(":active"), + PseudoClass::Lang(ref lang) => { + dest.write_str(":lang(")?; + serialize_identifier(lang, dest)?; + dest.write_char(')') + }, + } + } + } + + impl ToCss for PseudoElement { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + PseudoElement::Before => dest.write_str("::before"), + PseudoElement::After => dest.write_str("::after"), + PseudoElement::Highlight(ref name) => { + dest.write_str("::highlight(")?; + serialize_identifier(&name, dest)?; + dest.write_char(')') + }, + } + } + } + + #[derive(Clone, Debug, PartialEq)] + pub struct DummySelectorImpl; + + #[derive(Default)] + pub struct DummyParser { + default_ns: Option<DummyAtom>, + ns_prefixes: HashMap<DummyAtom, DummyAtom>, + } + + impl DummyParser { + fn default_with_namespace(default_ns: DummyAtom) -> DummyParser { + DummyParser { + default_ns: Some(default_ns), + ns_prefixes: Default::default(), + } + } + } + + impl SelectorImpl for DummySelectorImpl { + type ExtraMatchingData<'a> = std::marker::PhantomData<&'a ()>; + type AttrValue = DummyAttrValue; + type Identifier = DummyAtom; + type LocalName = DummyAtom; + type NamespaceUrl = DummyAtom; + type NamespacePrefix = DummyAtom; + type BorrowedLocalName = DummyAtom; + type BorrowedNamespaceUrl = DummyAtom; + type NonTSPseudoClass = PseudoClass; + type PseudoElement = PseudoElement; + } + + #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] + pub struct DummyAttrValue(String); + + impl ToCss for DummyAttrValue { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + use std::fmt::Write; + + dest.write_char('"')?; + write!(cssparser::CssStringWriter::new(dest), "{}", &self.0)?; + dest.write_char('"') + } + } + + impl<'a> From<&'a str> for DummyAttrValue { + fn from(string: &'a str) -> Self { + Self(string.into()) + } + } + + #[derive(Clone, Debug, Default, Eq, Hash, PartialEq)] + pub struct DummyAtom(String); + + impl ToCss for DummyAtom { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + serialize_identifier(&self.0, dest) + } + } + + impl From<String> for DummyAtom { + fn from(string: String) -> Self { + DummyAtom(string) + } + } + + impl<'a> From<&'a str> for DummyAtom { + fn from(string: &'a str) -> Self { + DummyAtom(string.into()) + } + } + + impl PrecomputedHash for DummyAtom { + fn precomputed_hash(&self) -> u32 { + self.0.as_ptr() as u32 + } + } + + impl<'i> Parser<'i> for DummyParser { + type Impl = DummySelectorImpl; + type Error = SelectorParseErrorKind<'i>; + + fn parse_slotted(&self) -> bool { + true + } + + fn parse_nth_child_of(&self) -> bool { + true + } + + fn parse_is_and_where(&self) -> bool { + true + } + + fn parse_has(&self) -> bool { + true + } + + fn parse_parent_selector(&self) -> bool { + true + } + + fn parse_part(&self) -> bool { + true + } + + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<PseudoClass, SelectorParseError<'i>> { + match_ignore_ascii_case! { &name, + "hover" => return Ok(PseudoClass::Hover), + "active" => return Ok(PseudoClass::Active), + _ => {} + } + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut CssParser<'i, 't>, + after_part: bool, + ) -> Result<PseudoClass, SelectorParseError<'i>> { + match_ignore_ascii_case! { &name, + "lang" if !after_part => { + let lang = parser.expect_ident_or_string()?.as_ref().to_owned(); + return Ok(PseudoClass::Lang(lang)); + }, + _ => {} + } + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<PseudoElement, SelectorParseError<'i>> { + match_ignore_ascii_case! { &name, + "before" => return Ok(PseudoElement::Before), + "after" => return Ok(PseudoElement::After), + _ => {} + } + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_functional_pseudo_element<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut CssParser<'i, 't>, + ) -> Result<PseudoElement, SelectorParseError<'i>> { + match_ignore_ascii_case! {&name, + "highlight" => return Ok(PseudoElement::Highlight(parser.expect_ident()?.as_ref().to_owned())), + _ => {} + } + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn default_namespace(&self) -> Option<DummyAtom> { + self.default_ns.clone() + } + + fn namespace_for_prefix(&self, prefix: &DummyAtom) -> Option<DummyAtom> { + self.ns_prefixes.get(prefix).cloned() + } + } + + fn parse<'i>( + input: &'i str, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + parse_relative(input, ParseRelative::No) + } + + fn parse_relative<'i>( + input: &'i str, + parse_relative: ParseRelative, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + parse_ns_relative(input, &DummyParser::default(), parse_relative) + } + + fn parse_expected<'i, 'a>( + input: &'i str, + expected: Option<&'a str>, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + parse_ns_expected(input, &DummyParser::default(), expected) + } + + fn parse_relative_expected<'i, 'a>( + input: &'i str, + parse_relative: ParseRelative, + expected: Option<&'a str>, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + parse_ns_relative_expected(input, &DummyParser::default(), parse_relative, expected) + } + + fn parse_ns<'i>( + input: &'i str, + parser: &DummyParser, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + parse_ns_relative(input, parser, ParseRelative::No) + } + + fn parse_ns_relative<'i>( + input: &'i str, + parser: &DummyParser, + parse_relative: ParseRelative, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + parse_ns_relative_expected(input, parser, parse_relative, None) + } + + fn parse_ns_expected<'i, 'a>( + input: &'i str, + parser: &DummyParser, + expected: Option<&'a str>, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + parse_ns_relative_expected(input, parser, ParseRelative::No, expected) + } + + fn parse_ns_relative_expected<'i, 'a>( + input: &'i str, + parser: &DummyParser, + parse_relative: ParseRelative, + expected: Option<&'a str>, + ) -> Result<SelectorList<DummySelectorImpl>, SelectorParseError<'i>> { + let mut parser_input = ParserInput::new(input); + let result = SelectorList::parse( + parser, + &mut CssParser::new(&mut parser_input), + parse_relative, + ); + if let Ok(ref selectors) = result { + // We can't assume that the serialized parsed selector will equal + // the input; for example, if there is no default namespace, '*|foo' + // should serialize to 'foo'. + assert_eq!( + selectors.to_css_string(), + match expected { + Some(x) => x, + None => input, + } + ); + } + result + } + + fn specificity(a: u32, b: u32, c: u32) -> u32 { + a << 20 | b << 10 | c + } + + #[test] + fn test_empty() { + let mut input = ParserInput::new(":empty"); + let list = SelectorList::parse( + &DummyParser::default(), + &mut CssParser::new(&mut input), + ParseRelative::No, + ); + assert!(list.is_ok()); + } + + const MATHML: &str = "http://www.w3.org/1998/Math/MathML"; + const SVG: &str = "http://www.w3.org/2000/svg"; + + #[test] + fn test_parsing() { + assert!(parse("").is_err()); + assert!(parse(":lang(4)").is_err()); + assert!(parse(":lang(en US)").is_err()); + assert_eq!( + parse("EeÉ"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::LocalName(LocalName { + name: DummyAtom::from("EeÉ"), + lower_name: DummyAtom::from("eeÉ"), + })], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("|e"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitNoNamespace, + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // When the default namespace is not set, *| should be elided. + // https://github.com/servo/servo/pull/17537 + assert_eq!( + parse_expected("*|e", Some("e")), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + })], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // When the default namespace is set, *| should _not_ be elided (as foo + // is no longer equivalent to *|foo--the former is only for foo in the + // default namespace). + // https://github.com/servo/servo/issues/16020 + assert_eq!( + parse_ns( + "*|e", + &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) + ), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitAnyNamespace, + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("*"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("|*"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitNoNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_expected("*|*", Some("*")), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns( + "*|*", + &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")) + ), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitAnyNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse(".foo:lang(en-US)"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Class(DummyAtom::from("foo")), + Component::NonTSPseudoClass(PseudoClass::Lang("en-US".to_owned())), + ], + specificity(0, 2, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("#bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::ID(DummyAtom::from("bar"))], + specificity(1, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("e.foo#bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + Component::Class(DummyAtom::from("foo")), + Component::ID(DummyAtom::from("bar")), + ], + specificity(1, 1, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("e.foo #bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + Component::Class(DummyAtom::from("foo")), + Component::Combinator(Combinator::Descendant), + Component::ID(DummyAtom::from("bar")), + ], + specificity(1, 1, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace does not apply to attribute selectors + // https://github.com/mozilla/servo/pull/1652 + let mut parser = DummyParser::default(); + assert_eq!( + parse_ns("[Foo]", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::AttributeInNoNamespaceExists { + local_name: DummyAtom::from("Foo"), + local_name_lower: DummyAtom::from("foo"), + }], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert!(parse_ns("svg|circle", &parser).is_err()); + parser + .ns_prefixes + .insert(DummyAtom("svg".into()), DummyAtom(SVG.into())); + assert_eq!( + parse_ns("svg|circle", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Namespace(DummyAtom("svg".into()), SVG.into()), + Component::LocalName(LocalName { + name: DummyAtom::from("circle"), + lower_name: DummyAtom::from("circle"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns("svg|*", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Namespace(DummyAtom("svg".into()), SVG.into()), + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace does not apply to attribute selectors + // https://github.com/mozilla/servo/pull/1652 + // but it does apply to implicit type selectors + // https://github.com/servo/rust-selectors/pull/82 + parser.default_ns = Some(MATHML.into()); + assert_eq!( + parse_ns("[Foo]", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::AttributeInNoNamespaceExists { + local_name: DummyAtom::from("Foo"), + local_name_lower: DummyAtom::from("foo"), + }, + ], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace does apply to type selectors + assert_eq!( + parse_ns("e", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns("*", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns("*|*", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ExplicitAnyNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // Default namespace applies to universal and type selectors inside :not and :matches, + // but not otherwise. + assert_eq!( + parse_ns(":not(.cl)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Class(DummyAtom::from("cl"))], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])), + ], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns(":not(*)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )]),), + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns(":not(e)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::Negation(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::DefaultNamespace(MATHML.into()), + Component::LocalName(LocalName { + name: DummyAtom::from("e"), + lower_name: DummyAtom::from("e"), + }), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse("[attr|=\"foo\"]"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::AttributeInNoNamespace { + local_name: DummyAtom::from("attr"), + operator: AttrSelectorOperator::DashMatch, + value: DummyAttrValue::from("foo"), + case_sensitivity: ParsedCaseSensitivity::CaseSensitive, + }], + specificity(0, 1, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // https://github.com/mozilla/servo/issues/1723 + assert_eq!( + parse("::before"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::Before), + ], + specificity(0, 0, 1), + SelectorFlags::HAS_PSEUDO, + )])) + ); + assert_eq!( + parse("::before:hover"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::Before), + Component::NonTSPseudoClass(PseudoClass::Hover), + ], + specificity(0, 1, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO, + )])) + ); + assert_eq!( + parse("::before:hover:hover"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::Before), + Component::NonTSPseudoClass(PseudoClass::Hover), + Component::NonTSPseudoClass(PseudoClass::Hover), + ], + specificity(0, 2, 1), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO, + )])) + ); + assert!(parse("::before:hover:lang(foo)").is_err()); + assert!(parse("::before:hover .foo").is_err()); + assert!(parse("::before .foo").is_err()); + assert!(parse("::before ~ bar").is_err()); + assert!(parse("::before:active").is_ok()); + + // https://github.com/servo/servo/issues/15335 + assert!(parse(":: before").is_err()); + assert_eq!( + parse("div ::after"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::LocalName(LocalName { + name: DummyAtom::from("div"), + lower_name: DummyAtom::from("div"), + }), + Component::Combinator(Combinator::Descendant), + Component::Combinator(Combinator::PseudoElement), + Component::PseudoElement(PseudoElement::After), + ], + specificity(0, 0, 2), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT | SelectorFlags::HAS_PSEUDO, + )])) + ); + assert_eq!( + parse("#d1 > .ok"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ID(DummyAtom::from("d1")), + Component::Combinator(Combinator::Child), + Component::Class(DummyAtom::from("ok")), + ], + (1 << 20) + (1 << 10) + (0 << 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + parser.default_ns = None; + assert!(parse(":not(#provel.old)").is_ok()); + assert!(parse(":not(#provel > old)").is_ok()); + assert!(parse("table[rules]:not([rules=\"none\"]):not([rules=\"\"])").is_ok()); + // https://github.com/servo/servo/issues/16017 + assert_eq!( + parse_ns(":not(*)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Negation(SelectorList::from_vec(vec![ + Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ) + ]))], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + assert_eq!( + parse_ns(":not(|*)", &parser), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Negation(SelectorList::from_vec(vec![ + Selector::from_vec( + vec![ + Component::ExplicitNoNamespace, + Component::ExplicitUniversalType, + ], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ) + ]))], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + // *| should be elided if there is no default namespace. + // https://github.com/servo/servo/pull/17537 + assert_eq!( + parse_ns_expected(":not(*|*)", &parser, Some(":not(*)")), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![Component::Negation(SelectorList::from_vec(vec![ + Selector::from_vec( + vec![Component::ExplicitUniversalType], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + ) + ]))], + specificity(0, 0, 0), + SelectorFlags::HAS_NON_FEATURELESS_COMPONENT, + )])) + ); + + assert!(parse("::highlight(foo)").is_ok()); + + assert!(parse("::slotted()").is_err()); + assert!(parse("::slotted(div)").is_ok()); + assert!(parse("::slotted(div).foo").is_err()); + assert!(parse("::slotted(div + bar)").is_err()); + assert!(parse("::slotted(div) + foo").is_err()); + + assert!(parse("::part()").is_err()); + assert!(parse("::part(42)").is_err()); + assert!(parse("::part(foo bar)").is_ok()); + assert!(parse("::part(foo):hover").is_ok()); + assert!(parse("::part(foo) + bar").is_err()); + + assert!(parse("div ::slotted(div)").is_ok()); + assert!(parse("div + slot::slotted(div)").is_ok()); + assert!(parse("div + slot::slotted(div.foo)").is_ok()); + assert!(parse("slot::slotted(div,foo)::first-line").is_err()); + assert!(parse("::slotted(div)::before").is_ok()); + assert!(parse("slot::slotted(div,foo)").is_err()); + + assert!(parse("foo:where()").is_ok()); + assert!(parse("foo:where(div, foo, .bar baz)").is_ok()); + assert!(parse("foo:where(::before)").is_ok()); + } + + #[test] + fn parent_selector() { + assert!(parse("foo &").is_ok()); + assert_eq!( + parse("#foo &.bar"), + Ok(SelectorList::from_vec(vec![Selector::from_vec( + vec![ + Component::ID(DummyAtom::from("foo")), + Component::Combinator(Combinator::Descendant), + Component::ParentSelector, + Component::Class(DummyAtom::from("bar")), + ], + (1 << 20) + (1 << 10) + (0 << 0), + SelectorFlags::HAS_PARENT | SelectorFlags::HAS_NON_FEATURELESS_COMPONENT + )])) + ); + + let parent = parse(".bar, div .baz").unwrap(); + let child = parse("#foo &.bar").unwrap(); + assert_eq!( + child.replace_parent_selector(&parent), + parse("#foo :is(.bar, div .baz).bar").unwrap() + ); + + let has_child = parse("#foo:has(&.bar)").unwrap(); + assert_eq!( + has_child.replace_parent_selector(&parent), + parse("#foo:has(:is(.bar, div .baz).bar)").unwrap() + ); + + let child = parse("#foo").unwrap(); + assert_eq!( + child.replace_parent_selector(&parent), + parse(":is(.bar, div .baz) #foo").unwrap() + ); + + let child = + parse_relative_expected("+ #foo", ParseRelative::ForNesting, Some("& + #foo")).unwrap(); + assert_eq!(child, parse("& + #foo").unwrap()); + } + + #[test] + fn test_pseudo_iter() { + let list = parse("q::before").unwrap(); + let selector = &list.slice()[0]; + assert!(!selector.is_universal()); + let mut iter = selector.iter(); + assert_eq!( + iter.next(), + Some(&Component::PseudoElement(PseudoElement::Before)) + ); + assert_eq!(iter.next(), None); + let combinator = iter.next_sequence(); + assert_eq!(combinator, Some(Combinator::PseudoElement)); + assert!(matches!(iter.next(), Some(&Component::LocalName(..)))); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_sequence(), None); + } + + #[test] + fn test_universal() { + let list = parse_ns( + "*|*::before", + &DummyParser::default_with_namespace(DummyAtom::from("https://mozilla.org")), + ) + .unwrap(); + let selector = &list.slice()[0]; + assert!(selector.is_universal()); + } + + #[test] + fn test_empty_pseudo_iter() { + let list = parse("::before").unwrap(); + let selector = &list.slice()[0]; + assert!(selector.is_universal()); + let mut iter = selector.iter(); + assert_eq!( + iter.next(), + Some(&Component::PseudoElement(PseudoElement::Before)) + ); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_sequence(), Some(Combinator::PseudoElement)); + assert_eq!(iter.next(), None); + assert_eq!(iter.next_sequence(), None); + } + + struct TestVisitor { + seen: Vec<String>, + } + + impl SelectorVisitor for TestVisitor { + type Impl = DummySelectorImpl; + + fn visit_simple_selector(&mut self, s: &Component<DummySelectorImpl>) -> bool { + let mut dest = String::new(); + s.to_css(&mut dest).unwrap(); + self.seen.push(dest); + true + } + } + + #[test] + fn visitor() { + let mut test_visitor = TestVisitor { seen: vec![] }; + parse(":not(:hover) ~ label").unwrap().slice()[0].visit(&mut test_visitor); + assert!(test_visitor.seen.contains(&":hover".into())); + + let mut test_visitor = TestVisitor { seen: vec![] }; + parse("::before:hover").unwrap().slice()[0].visit(&mut test_visitor); + assert!(test_visitor.seen.contains(&":hover".into())); + } +} diff --git a/servo/components/selectors/relative_selector/cache.rs b/servo/components/selectors/relative_selector/cache.rs new file mode 100644 index 0000000000..d7681aa3a4 --- /dev/null +++ b/servo/components/selectors/relative_selector/cache.rs @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use fxhash::FxHashMap; +/// Relative selector cache. This is useful for following cases. +/// First case is non-subject relative selector: Imagine `.anchor:has(<..>) ~ .foo`, with DOM +/// `.anchor + .foo + .. + .foo`. Each match on `.foo` triggers `:has()` traversal that +/// yields the same result. This is simple enough, since we just need to store +/// the exact match on that anchor pass/fail. +/// Second case is `querySelectorAll`: Imagine `querySelectorAll(':has(.a)')`, with DOM +/// `div > .. > div > .a`. When the we perform the traversal at the top div, +/// we basically end up evaluating `:has(.a)` for all anchors, which could be reused. +/// Also consider the sibling version: `querySelectorAll(':has(~ .a)')` with DOM +/// `div + .. + div + .a`. +/// TODO(dshin): Second case is not yet handled. That is tracked in Bug 1845291. +use std::hash::Hash; + +use crate::parser::{RelativeSelector, SelectorKey}; +use crate::{tree::OpaqueElement, SelectorImpl}; + +/// Match data for a given element and a selector. +#[derive(Clone, Copy)] +pub enum RelativeSelectorCachedMatch { + /// This selector matches this element. + Matched, + /// This selector does not match this element. + NotMatched, +} + +impl RelativeSelectorCachedMatch { + /// Is the cached result a match? + pub fn matched(&self) -> bool { + matches!(*self, Self::Matched) + } +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +struct Key { + element: OpaqueElement, + selector: SelectorKey, +} + +impl Key { + pub fn new<Impl: SelectorImpl>( + element: OpaqueElement, + selector: &RelativeSelector<Impl>, + ) -> Self { + Key { + element, + selector: SelectorKey::new(&selector.selector), + } + } +} + +/// Cache to speed up matching of relative selectors. +#[derive(Default)] +pub struct RelativeSelectorCache { + cache: FxHashMap<Key, RelativeSelectorCachedMatch>, +} + +impl RelativeSelectorCache { + /// Add a relative selector match into the cache. + pub fn add<Impl: SelectorImpl>( + &mut self, + anchor: OpaqueElement, + selector: &RelativeSelector<Impl>, + matched: RelativeSelectorCachedMatch, + ) { + self.cache.insert(Key::new(anchor, selector), matched); + } + + /// Check if we have a cache entry for the element. + pub fn lookup<Impl: SelectorImpl>( + &mut self, + element: OpaqueElement, + selector: &RelativeSelector<Impl>, + ) -> Option<RelativeSelectorCachedMatch> { + self.cache.get(&Key::new(element, selector)).copied() + } +} diff --git a/servo/components/selectors/relative_selector/filter.rs b/servo/components/selectors/relative_selector/filter.rs new file mode 100644 index 0000000000..3c3eb7d2fa --- /dev/null +++ b/servo/components/selectors/relative_selector/filter.rs @@ -0,0 +1,159 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/// Bloom filter for relative selectors. +use fxhash::FxHashMap; + +use crate::bloom::BloomFilter; +use crate::context::QuirksMode; +use crate::parser::{collect_selector_hashes, RelativeSelector, RelativeSelectorMatchHint}; +use crate::tree::{Element, OpaqueElement}; +use crate::SelectorImpl; + +enum Entry { + /// Filter lookup happened once. Construction of the filter is expensive, + /// so this is set when the element for subtree traversal is encountered. + Lookup, + /// Filter lookup happened more than once, and the filter for this element's + /// subtree traversal is constructed. Could use special handlings for pseudo-classes + /// such as `:hover` and `:focus`, see Bug 1845503. + HasFilter(Box<BloomFilter>), +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +enum TraversalKind { + Children, + Descendants, +} + +fn add_to_filter<E: Element>(element: &E, filter: &mut BloomFilter, kind: TraversalKind) -> bool { + let mut child = element.first_element_child(); + while let Some(e) = child { + if !e.add_element_unique_hashes(filter) { + return false; + } + if kind == TraversalKind::Descendants { + if !add_to_filter(&e, filter, kind) { + return false; + } + } + child = e.next_sibling_element(); + } + true +} + +#[derive(Clone, Copy, Hash, Eq, PartialEq)] +struct Key(OpaqueElement, TraversalKind); + +/// Map of bloom filters for fast-rejecting relative selectors. +#[derive(Default)] +pub struct RelativeSelectorFilterMap { + map: FxHashMap<Key, Entry>, +} + +fn fast_reject<Impl: SelectorImpl>( + selector: &RelativeSelector<Impl>, + quirks_mode: QuirksMode, + filter: &BloomFilter, +) -> bool { + let mut hashes = [0u32; 4]; + let mut len = 0; + // For inner selectors, we only collect from the single rightmost compound. + // This is because inner selectors can cause breakouts: e.g. `.anchor:has(:is(.a .b) .c)` + // can match when `.a` is the ancestor of `.anchor`. Including `.a` would possibly fast + // reject the subtree for not having `.a`, even if the selector would match. + // Technically, if the selector's traversal is non-sibling subtree, we can traverse + // inner selectors up to the point where a descendant/child combinator is encountered + // (e.g. In `.anchor:has(:is(.a ~ .b) .c)`, `.a` is guaranteed to be the a descendant + // of `.anchor`). While that can be separately handled, well, this is simpler. + collect_selector_hashes( + selector.selector.iter(), + quirks_mode, + &mut hashes, + &mut len, + |s| s.iter(), + ); + for i in 0..len { + if !filter.might_contain_hash(hashes[i]) { + // Definitely rejected. + return true; + } + } + false +} + +impl RelativeSelectorFilterMap { + fn get_filter<E: Element>(&mut self, element: &E, kind: TraversalKind) -> Option<&BloomFilter> { + // Insert flag to indicate that we looked up the filter once, and + // create the filter if and only if that flag is there. + let key = Key(element.opaque(), kind); + let entry = self + .map + .entry(key) + .and_modify(|entry| { + if !matches!(entry, Entry::Lookup) { + return; + } + let mut filter = BloomFilter::new(); + // Go through all children/descendants of this element and add their hashes. + if add_to_filter(element, &mut filter, kind) { + *entry = Entry::HasFilter(Box::new(filter)); + } + }) + .or_insert(Entry::Lookup); + match entry { + Entry::Lookup => None, + Entry::HasFilter(ref filter) => Some(filter.as_ref()), + } + } + + /// Potentially reject the given selector for this element. + /// This may seem redundant in presence of the cache, but the cache keys into the + /// selector-element pair specifically, while this filter keys to the element + /// and the traversal kind, so it is useful for handling multiple selectors + /// that effectively end up looking at the same(-ish, for siblings) subtree. + pub fn fast_reject<Impl: SelectorImpl, E: Element>( + &mut self, + element: &E, + selector: &RelativeSelector<Impl>, + quirks_mode: QuirksMode, + ) -> bool { + if matches!( + selector.match_hint, + RelativeSelectorMatchHint::InNextSibling + ) { + // Don't bother. + return false; + } + let is_sibling = matches!( + selector.match_hint, + RelativeSelectorMatchHint::InSibling | + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSiblingSubtree + ); + let is_subtree = matches!( + selector.match_hint, + RelativeSelectorMatchHint::InSubtree | + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSiblingSubtree + ); + let kind = if is_subtree { + TraversalKind::Descendants + } else { + TraversalKind::Children + }; + if is_sibling { + // Contain the entirety of the parent's children/subtree in the filter, and use that. + // This is less likely to reject, especially for sibling subtree matches; however, it's less + // expensive memory-wise, compared to storing filters for each sibling. + element.parent_element().map_or(false, |parent| { + self.get_filter(&parent, kind) + .map_or(false, |filter| fast_reject(selector, quirks_mode, filter)) + }) + } else { + self.get_filter(element, kind) + .map_or(false, |filter| fast_reject(selector, quirks_mode, filter)) + } + } +} diff --git a/servo/components/selectors/relative_selector/mod.rs b/servo/components/selectors/relative_selector/mod.rs new file mode 100644 index 0000000000..6dd39f7327 --- /dev/null +++ b/servo/components/selectors/relative_selector/mod.rs @@ -0,0 +1,6 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +pub mod cache; +pub mod filter; diff --git a/servo/components/selectors/sink.rs b/servo/components/selectors/sink.rs new file mode 100644 index 0000000000..dcdd7ff259 --- /dev/null +++ b/servo/components/selectors/sink.rs @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Small helpers to abstract over different containers. +#![deny(missing_docs)] + +use smallvec::{Array, SmallVec}; + +/// A trait to abstract over a `push` method that may be implemented for +/// different kind of types. +/// +/// Used to abstract over `Array`, `SmallVec` and `Vec`, and also to implement a +/// type which `push` method does only tweak a byte when we only need to check +/// for the presence of something. +pub trait Push<T> { + /// Push a value into self. + fn push(&mut self, value: T); +} + +impl<T> Push<T> for Vec<T> { + fn push(&mut self, value: T) { + Vec::push(self, value); + } +} + +impl<A: Array> Push<A::Item> for SmallVec<A> { + fn push(&mut self, value: A::Item) { + SmallVec::push(self, value); + } +} diff --git a/servo/components/selectors/tree.rs b/servo/components/selectors/tree.rs new file mode 100644 index 0000000000..c1ea8ff5ae --- /dev/null +++ b/servo/components/selectors/tree.rs @@ -0,0 +1,168 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Traits that nodes must implement. Breaks the otherwise-cyclic dependency +//! between layout and style. + +use crate::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use crate::bloom::BloomFilter; +use crate::matching::{ElementSelectorFlags, MatchingContext}; +use crate::parser::SelectorImpl; +use std::fmt::Debug; +use std::ptr::NonNull; + +/// Opaque representation of an Element, for identity comparisons. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct OpaqueElement(NonNull<()>); + +unsafe impl Send for OpaqueElement {} + +impl OpaqueElement { + /// Creates a new OpaqueElement from an arbitrarily-typed pointer. + pub fn new<T>(ptr: &T) -> Self { + unsafe { + OpaqueElement(NonNull::new_unchecked( + ptr as *const T as *const () as *mut (), + )) + } + } +} + +pub trait Element: Sized + Clone + Debug { + type Impl: SelectorImpl; + + /// Converts self into an opaque representation. + fn opaque(&self) -> OpaqueElement; + + fn parent_element(&self) -> Option<Self>; + + /// Whether the parent node of this element is a shadow root. + fn parent_node_is_shadow_root(&self) -> bool; + + /// The host of the containing shadow root, if any. + fn containing_shadow_host(&self) -> Option<Self>; + + /// The parent of a given pseudo-element, after matching a pseudo-element + /// selector. + /// + /// This is guaranteed to be called in a pseudo-element. + fn pseudo_element_originating_element(&self) -> Option<Self> { + debug_assert!(self.is_pseudo_element()); + self.parent_element() + } + + /// Whether we're matching on a pseudo-element. + fn is_pseudo_element(&self) -> bool; + + /// Skips non-element nodes + fn prev_sibling_element(&self) -> Option<Self>; + + /// Skips non-element nodes + fn next_sibling_element(&self) -> Option<Self>; + + /// Skips non-element nodes + fn first_element_child(&self) -> Option<Self>; + + fn is_html_element_in_html_document(&self) -> bool; + + fn has_local_name(&self, local_name: &<Self::Impl as SelectorImpl>::BorrowedLocalName) -> bool; + + /// Empty string for no namespace + fn has_namespace(&self, ns: &<Self::Impl as SelectorImpl>::BorrowedNamespaceUrl) -> bool; + + /// Whether this element and the `other` element have the same local name and namespace. + fn is_same_type(&self, other: &Self) -> bool; + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&<Self::Impl as SelectorImpl>::NamespaceUrl>, + local_name: &<Self::Impl as SelectorImpl>::LocalName, + operation: &AttrSelectorOperation<&<Self::Impl as SelectorImpl>::AttrValue>, + ) -> bool; + + fn has_attr_in_no_namespace( + &self, + local_name: &<Self::Impl as SelectorImpl>::LocalName, + ) -> bool { + self.attr_matches( + &NamespaceConstraint::Specific(&crate::parser::namespace_empty_string::<Self::Impl>()), + local_name, + &AttrSelectorOperation::Exists, + ) + } + + fn match_non_ts_pseudo_class( + &self, + pc: &<Self::Impl as SelectorImpl>::NonTSPseudoClass, + context: &mut MatchingContext<Self::Impl>, + ) -> bool; + + fn match_pseudo_element( + &self, + pe: &<Self::Impl as SelectorImpl>::PseudoElement, + context: &mut MatchingContext<Self::Impl>, + ) -> bool; + + /// Sets selector flags on the elemnt itself or the parent, depending on the + /// flags, which indicate what kind of work may need to be performed when + /// DOM state changes. + fn apply_selector_flags(&self, flags: ElementSelectorFlags); + + /// Whether this element is a `link`. + fn is_link(&self) -> bool; + + /// Returns whether the element is an HTML <slot> element. + fn is_html_slot_element(&self) -> bool; + + /// Returns the assigned <slot> element this element is assigned to. + /// + /// Necessary for the `::slotted` pseudo-class. + fn assigned_slot(&self) -> Option<Self> { + None + } + + fn has_id( + &self, + id: &<Self::Impl as SelectorImpl>::Identifier, + case_sensitivity: CaseSensitivity, + ) -> bool; + + fn has_class( + &self, + name: &<Self::Impl as SelectorImpl>::Identifier, + case_sensitivity: CaseSensitivity, + ) -> bool; + + /// Returns the mapping from the `exportparts` attribute in the reverse + /// direction, that is, in an outer-tree -> inner-tree direction. + fn imported_part( + &self, + name: &<Self::Impl as SelectorImpl>::Identifier, + ) -> Option<<Self::Impl as SelectorImpl>::Identifier>; + + fn is_part(&self, name: &<Self::Impl as SelectorImpl>::Identifier) -> bool; + + /// Returns whether this element matches `:empty`. + /// + /// That is, whether it does not contain any child element or any non-zero-length text node. + /// See http://dev.w3.org/csswg/selectors-3/#empty-pseudo + fn is_empty(&self) -> bool; + + /// Returns whether this element matches `:root`, + /// i.e. whether it is the root element of a document. + /// + /// Note: this can be false even if `.parent_element()` is `None` + /// if the parent node is a `DocumentFragment`. + fn is_root(&self) -> bool; + + /// Returns whether this element should ignore matching nth child + /// selector. + fn ignores_nth_child_selectors(&self) -> bool { + false + } + + /// Add hashes unique to this element to the given filter, returning true + /// if any got added. + fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool; +} diff --git a/servo/components/selectors/visitor.rs b/servo/components/selectors/visitor.rs new file mode 100644 index 0000000000..d5befbc68b --- /dev/null +++ b/servo/components/selectors/visitor.rs @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Visitor traits for selectors. + +#![deny(missing_docs)] + +use crate::attr::NamespaceConstraint; +use crate::parser::{Combinator, Component, RelativeSelector, Selector, SelectorImpl}; + +/// A trait to visit selector properties. +/// +/// All the `visit_foo` methods return a boolean indicating whether the +/// traversal should continue or not. +pub trait SelectorVisitor: Sized { + /// The selector implementation this visitor wants to visit. + type Impl: SelectorImpl; + + /// Visit an attribute selector that may match (there are other selectors + /// that may never match, like those containing whitespace or the empty + /// string). + fn visit_attribute_selector( + &mut self, + _namespace: &NamespaceConstraint<&<Self::Impl as SelectorImpl>::NamespaceUrl>, + _local_name: &<Self::Impl as SelectorImpl>::LocalName, + _local_name_lower: &<Self::Impl as SelectorImpl>::LocalName, + ) -> bool { + true + } + + /// Visit a simple selector. + fn visit_simple_selector(&mut self, _: &Component<Self::Impl>) -> bool { + true + } + + /// Visit a nested relative selector list. The caller is responsible to call visit + /// into the internal selectors if / as needed. + /// + /// The default implementation skips it altogether. + fn visit_relative_selector_list(&mut self, _list: &[RelativeSelector<Self::Impl>]) -> bool { + true + } + + /// Visit a nested selector list. The caller is responsible to call visit + /// into the internal selectors if / as needed. + /// + /// The default implementation does this. + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<Self::Impl>], + ) -> bool { + for nested in list { + if !nested.visit(self) { + return false; + } + } + true + } + + /// Visits a complex selector. + /// + /// Gets the combinator to the right of the selector, or `None` if the + /// selector is the rightmost one. + fn visit_complex_selector(&mut self, _combinator_to_right: Option<Combinator>) -> bool { + true + } +} + +bitflags! { + /// The kinds of components the visitor is visiting the selector list of, if any + #[derive(Clone, Copy, Default)] + pub struct SelectorListKind: u8 { + /// The visitor is inside :not(..) + const NEGATION = 1 << 0; + /// The visitor is inside :is(..) + const IS = 1 << 1; + /// The visitor is inside :where(..) + const WHERE = 1 << 2; + /// The visitor is inside :nth-child(.. of <selector list>) or + /// :nth-last-child(.. of <selector list>) + const NTH_OF = 1 << 3; + /// The visitor is inside :has(..) + const HAS = 1 << 4; + } +} + +impl SelectorListKind { + /// Construct a SelectorListKind for the corresponding component. + pub fn from_component<Impl: SelectorImpl>(component: &Component<Impl>) -> Self { + match component { + Component::Negation(_) => SelectorListKind::NEGATION, + Component::Is(_) => SelectorListKind::IS, + Component::Where(_) => SelectorListKind::WHERE, + Component::NthOf(_) => SelectorListKind::NTH_OF, + _ => SelectorListKind::empty(), + } + } + + /// Whether the visitor is inside :not(..) + pub fn in_negation(&self) -> bool { + self.intersects(SelectorListKind::NEGATION) + } + + /// Whether the visitor is inside :is(..) + pub fn in_is(&self) -> bool { + self.intersects(SelectorListKind::IS) + } + + /// Whether the visitor is inside :where(..) + pub fn in_where(&self) -> bool { + self.intersects(SelectorListKind::WHERE) + } + + /// Whether the visitor is inside :nth-child(.. of <selector list>) or + /// :nth-last-child(.. of <selector list>) + pub fn in_nth_of(&self) -> bool { + self.intersects(SelectorListKind::NTH_OF) + } + + /// Whether the visitor is inside :has(..) + pub fn in_has(&self) -> bool { + self.intersects(SelectorListKind::HAS) + } + + /// Whether this nested selector is relevant for nth-of dependencies. + pub fn relevant_to_nth_of_dependencies(&self) -> bool { + // Order of nesting for `:has` and `:nth-child(.. of ..)` doesn't matter, because: + // * `:has(:nth-child(.. of ..))`: The location of the anchoring element is + // independent from where `:nth-child(.. of ..)` is applied. + // * `:nth-child(.. of :has(..))`: Invalidations inside `:has` must first use the + // `:has` machinary to find the anchor, then carry out the remaining invalidation. + self.in_nth_of() && !self.in_has() + } +} diff --git a/servo/components/servo_arc/Cargo.toml b/servo/components/servo_arc/Cargo.toml new file mode 100644 index 0000000000..03b1004bac --- /dev/null +++ b/servo/components/servo_arc/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "servo_arc" +version = "0.1.1" +authors = ["The Servo Project Developers"] +license = "MIT/Apache-2.0" +repository = "https://github.com/servo/servo" +description = "A fork of std::sync::Arc with some extra functionality and without weak references" + +[lib] +name = "servo_arc" +path = "lib.rs" + +[features] +gecko_refcount_logging = [] +servo = ["serde"] + +[dependencies] +serde = { version = "1.0", optional = true } +stable_deref_trait = "1.0.0" diff --git a/servo/components/servo_arc/lib.rs b/servo/components/servo_arc/lib.rs new file mode 100644 index 0000000000..1438ccebfd --- /dev/null +++ b/servo/components/servo_arc/lib.rs @@ -0,0 +1,1195 @@ +// Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT +// file at the top-level directory of this distribution and at +// http://rust-lang.org/COPYRIGHT. +// +// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or +// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license +// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your +// option. This file may not be copied, modified, or distributed +// except according to those terms. + +//! Fork of Arc for Servo. This has the following advantages over std::sync::Arc: +//! +//! * We don't waste storage on the weak reference count. +//! * We don't do extra RMU operations to handle the possibility of weak references. +//! * We can experiment with arena allocation (todo). +//! * We can add methods to support our custom use cases [1]. +//! * We have support for dynamically-sized types (see from_header_and_iter). +//! * We have support for thin arcs to unsized types (see ThinArc). +//! * We have support for references to static data, which don't do any +//! refcounting. +//! +//! [1]: https://bugzilla.mozilla.org/show_bug.cgi?id=1360883 + +// The semantics of `Arc` are already documented in the Rust docs, so we don't +// duplicate those here. +#![allow(missing_docs)] + +#[cfg(feature = "servo")] +extern crate serde; +extern crate stable_deref_trait; + +#[cfg(feature = "servo")] +use serde::{Deserialize, Serialize}; +use stable_deref_trait::{CloneStableDeref, StableDeref}; +use std::alloc::{self, Layout}; +use std::borrow; +use std::cmp::Ordering; +use std::convert::From; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::iter::{ExactSizeIterator, Iterator}; +use std::marker::PhantomData; +use std::mem::{self, align_of, size_of}; +use std::ops::{Deref, DerefMut}; +use std::os::raw::c_void; +use std::process; +use std::ptr; +use std::sync::atomic; +use std::sync::atomic::Ordering::{Acquire, Relaxed, Release}; +use std::{isize, usize}; + +/// A soft limit on the amount of references that may be made to an `Arc`. +/// +/// Going above this limit will abort your program (although not +/// necessarily) at _exactly_ `MAX_REFCOUNT + 1` references. +const MAX_REFCOUNT: usize = (isize::MAX) as usize; + +/// Special refcount value that means the data is not reference counted, +/// and that the `Arc` is really acting as a read-only static reference. +const STATIC_REFCOUNT: usize = usize::MAX; + +/// An atomically reference counted shared pointer +/// +/// See the documentation for [`Arc`] in the standard library. Unlike the +/// standard library `Arc`, this `Arc` does not support weak reference counting. +/// +/// See the discussion in https://github.com/rust-lang/rust/pull/60594 for the +/// usage of PhantomData. +/// +/// [`Arc`]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[repr(C)] +pub struct Arc<T: ?Sized> { + p: ptr::NonNull<ArcInner<T>>, + phantom: PhantomData<T>, +} + +/// An `Arc` that is known to be uniquely owned +/// +/// When `Arc`s are constructed, they are known to be +/// uniquely owned. In such a case it is safe to mutate +/// the contents of the `Arc`. Normally, one would just handle +/// this by mutating the data on the stack before allocating the +/// `Arc`, however it's possible the data is large or unsized +/// and you need to heap-allocate it earlier in such a way +/// that it can be freely converted into a regular `Arc` once you're +/// done. +/// +/// `UniqueArc` exists for this purpose, when constructed it performs +/// the same allocations necessary for an `Arc`, however it allows mutable access. +/// Once the mutation is finished, you can call `.shareable()` and get a regular `Arc` +/// out of it. +/// +/// Ignore the doctest below there's no way to skip building with refcount +/// logging during doc tests (see rust-lang/rust#45599). +/// +/// ```rust,ignore +/// # use servo_arc::UniqueArc; +/// let data = [1, 2, 3, 4, 5]; +/// let mut x = UniqueArc::new(data); +/// x[4] = 7; // mutate! +/// let y = x.shareable(); // y is an Arc<T> +/// ``` +pub struct UniqueArc<T: ?Sized>(Arc<T>); + +impl<T> UniqueArc<T> { + #[inline] + /// Construct a new UniqueArc + pub fn new(data: T) -> Self { + UniqueArc(Arc::new(data)) + } + + /// Construct an uninitialized arc + #[inline] + pub fn new_uninit() -> UniqueArc<mem::MaybeUninit<T>> { + unsafe { + let layout = Layout::new::<ArcInner<mem::MaybeUninit<T>>>(); + let ptr = alloc::alloc(layout); + let mut p = ptr::NonNull::new(ptr) + .unwrap_or_else(|| alloc::handle_alloc_error(layout)) + .cast::<ArcInner<mem::MaybeUninit<T>>>(); + ptr::write(&mut p.as_mut().count, atomic::AtomicUsize::new(1)); + + #[cfg(feature = "gecko_refcount_logging")] + { + NS_LogCtor(p.as_ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8) + } + + UniqueArc(Arc { + p, + phantom: PhantomData, + }) + } + } + + #[inline] + /// Convert to a shareable Arc<T> once we're done mutating it + pub fn shareable(self) -> Arc<T> { + self.0 + } +} + +impl<T> UniqueArc<mem::MaybeUninit<T>> { + /// Convert to an initialized Arc. + #[inline] + pub unsafe fn assume_init(this: Self) -> UniqueArc<T> { + UniqueArc(Arc { + p: mem::ManuallyDrop::new(this).0.p.cast(), + phantom: PhantomData, + }) + } +} + +impl<T> Deref for UniqueArc<T> { + type Target = T; + fn deref(&self) -> &T { + &*self.0 + } +} + +impl<T> DerefMut for UniqueArc<T> { + fn deref_mut(&mut self) -> &mut T { + // We know this to be uniquely owned + unsafe { &mut (*self.0.ptr()).data } + } +} + +unsafe impl<T: ?Sized + Sync + Send> Send for Arc<T> {} +unsafe impl<T: ?Sized + Sync + Send> Sync for Arc<T> {} + +/// The object allocated by an Arc<T> +#[repr(C)] +struct ArcInner<T: ?Sized> { + count: atomic::AtomicUsize, + data: T, +} + +unsafe impl<T: ?Sized + Sync + Send> Send for ArcInner<T> {} +unsafe impl<T: ?Sized + Sync + Send> Sync for ArcInner<T> {} + +/// Computes the offset of the data field within ArcInner. +fn data_offset<T>() -> usize { + let size = size_of::<ArcInner<()>>(); + let align = align_of::<T>(); + // https://github.com/rust-lang/rust/blob/1.36.0/src/libcore/alloc.rs#L187-L207 + size.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1) +} + +impl<T> Arc<T> { + /// Construct an `Arc<T>` + #[inline] + pub fn new(data: T) -> Self { + let ptr = Box::into_raw(Box::new(ArcInner { + count: atomic::AtomicUsize::new(1), + data, + })); + + #[cfg(feature = "gecko_refcount_logging")] + unsafe { + // FIXME(emilio): Would be so amazing to have + // std::intrinsics::type_name() around, so that we could also report + // a real size. + NS_LogCtor(ptr as *mut _, b"ServoArc\0".as_ptr() as *const _, 8); + } + + unsafe { + Arc { + p: ptr::NonNull::new_unchecked(ptr), + phantom: PhantomData, + } + } + } + + /// Construct an intentionally-leaked arc. + #[inline] + pub fn new_leaked(data: T) -> Self { + let arc = Self::new(data); + arc.mark_as_intentionally_leaked(); + arc + } + + /// Convert the Arc<T> to a raw pointer, suitable for use across FFI + /// + /// Note: This returns a pointer to the data T, which is offset in the allocation. + #[inline] + pub fn into_raw(this: Self) -> *const T { + let ptr = unsafe { &((*this.ptr()).data) as *const _ }; + mem::forget(this); + ptr + } + + /// Reconstruct the Arc<T> from a raw pointer obtained from into_raw() + /// + /// Note: This raw pointer will be offset in the allocation and must be preceded + /// by the atomic count. + #[inline] + pub unsafe fn from_raw(ptr: *const T) -> Self { + // To find the corresponding pointer to the `ArcInner` we need + // to subtract the offset of the `data` field from the pointer. + let ptr = (ptr as *const u8).sub(data_offset::<T>()); + Arc { + p: ptr::NonNull::new_unchecked(ptr as *mut ArcInner<T>), + phantom: PhantomData, + } + } + + /// Like from_raw, but returns an addrefed arc instead. + #[inline] + pub unsafe fn from_raw_addrefed(ptr: *const T) -> Self { + let arc = Self::from_raw(ptr); + mem::forget(arc.clone()); + arc + } + + /// Create a new static Arc<T> (one that won't reference count the object) + /// and place it in the allocation provided by the specified `alloc` + /// function. + /// + /// `alloc` must return a pointer into a static allocation suitable for + /// storing data with the `Layout` passed into it. The pointer returned by + /// `alloc` will not be freed. + #[inline] + pub unsafe fn new_static<F>(alloc: F, data: T) -> Arc<T> + where + F: FnOnce(Layout) -> *mut u8, + { + let ptr = alloc(Layout::new::<ArcInner<T>>()) as *mut ArcInner<T>; + + let x = ArcInner { + count: atomic::AtomicUsize::new(STATIC_REFCOUNT), + data, + }; + + ptr::write(ptr, x); + + Arc { + p: ptr::NonNull::new_unchecked(ptr), + phantom: PhantomData, + } + } + + /// Produce a pointer to the data that can be converted back + /// to an Arc. This is basically an `&Arc<T>`, without the extra indirection. + /// It has the benefits of an `&T` but also knows about the underlying refcount + /// and can be converted into more `Arc<T>`s if necessary. + #[inline] + pub fn borrow_arc<'a>(&'a self) -> ArcBorrow<'a, T> { + ArcBorrow(&**self) + } + + /// Returns the address on the heap of the Arc itself -- not the T within it -- for memory + /// reporting. + /// + /// If this is a static reference, this returns null. + pub fn heap_ptr(&self) -> *const c_void { + if self.inner().count.load(Relaxed) == STATIC_REFCOUNT { + ptr::null() + } else { + self.p.as_ptr() as *const ArcInner<T> as *const c_void + } + } +} + +impl<T: ?Sized> Arc<T> { + #[inline] + fn inner(&self) -> &ArcInner<T> { + // This unsafety is ok because while this arc is alive we're guaranteed + // that the inner pointer is valid. Furthermore, we know that the + // `ArcInner` structure itself is `Sync` because the inner data is + // `Sync` as well, so we're ok loaning out an immutable pointer to these + // contents. + unsafe { &*self.ptr() } + } + + #[inline(always)] + fn record_drop(&self) { + #[cfg(feature = "gecko_refcount_logging")] + unsafe { + NS_LogDtor(self.ptr() as *mut _, b"ServoArc\0".as_ptr() as *const _, 8); + } + } + + /// Marks this `Arc` as intentionally leaked for the purposes of refcount + /// logging. + /// + /// It's a logic error to call this more than once, but it's not unsafe, as + /// it'd just report negative leaks. + #[inline(always)] + pub fn mark_as_intentionally_leaked(&self) { + self.record_drop(); + } + + // Non-inlined part of `drop`. Just invokes the destructor and calls the + // refcount logging machinery if enabled. + #[inline(never)] + unsafe fn drop_slow(&mut self) { + self.record_drop(); + let _ = Box::from_raw(self.ptr()); + } + + /// Test pointer equality between the two Arcs, i.e. they must be the _same_ + /// allocation + #[inline] + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + this.ptr() as *const () == other.ptr() as *const () + } + + fn ptr(&self) -> *mut ArcInner<T> { + self.p.as_ptr() + } + + /// Returns a raw ptr to the underlying allocation. + pub fn raw_ptr(&self) -> *const c_void { + self.p.as_ptr() as *const _ + } +} + +#[cfg(feature = "gecko_refcount_logging")] +extern "C" { + fn NS_LogCtor( + aPtr: *mut std::os::raw::c_void, + aTypeName: *const std::os::raw::c_char, + aSize: u32, + ); + fn NS_LogDtor( + aPtr: *mut std::os::raw::c_void, + aTypeName: *const std::os::raw::c_char, + aSize: u32, + ); +} + +impl<T: ?Sized> Clone for Arc<T> { + #[inline] + fn clone(&self) -> Self { + // NOTE(emilio): If you change anything here, make sure that the + // implementation in layout/style/ServoStyleConstsInlines.h matches! + // + // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since + // `count` never changes between STATIC_REFCOUNT and other values. + if self.inner().count.load(Relaxed) != STATIC_REFCOUNT { + // Using a relaxed ordering is alright here, as knowledge of the + // original reference prevents other threads from erroneously deleting + // the object. + // + // As explained in the [Boost documentation][1], Increasing the + // reference counter can always be done with memory_order_relaxed: New + // references to an object can only be formed from an existing + // reference, and passing an existing reference from one thread to + // another must already provide any required synchronization. + // + // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html) + let old_size = self.inner().count.fetch_add(1, Relaxed); + + // However we need to guard against massive refcounts in case someone + // is `mem::forget`ing Arcs. If we don't do this the count can overflow + // and users will use-after free. We racily saturate to `isize::MAX` on + // the assumption that there aren't ~2 billion threads incrementing + // the reference count at once. This branch will never be taken in + // any realistic program. + // + // We abort because such a program is incredibly degenerate, and we + // don't care to support it. + if old_size > MAX_REFCOUNT { + process::abort(); + } + } + + unsafe { + Arc { + p: ptr::NonNull::new_unchecked(self.ptr()), + phantom: PhantomData, + } + } + } +} + +impl<T: ?Sized> Deref for Arc<T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + &self.inner().data + } +} + +impl<T: Clone> Arc<T> { + /// Makes a mutable reference to the `Arc`, cloning if necessary + /// + /// This is functionally equivalent to [`Arc::make_mut`][mm] from the standard library. + /// + /// If this `Arc` is uniquely owned, `make_mut()` will provide a mutable + /// reference to the contents. If not, `make_mut()` will create a _new_ `Arc` + /// with a copy of the contents, update `this` to point to it, and provide + /// a mutable reference to its contents. + /// + /// This is useful for implementing copy-on-write schemes where you wish to + /// avoid copying things if your `Arc` is not shared. + /// + /// [mm]: https://doc.rust-lang.org/stable/std/sync/struct.Arc.html#method.make_mut + #[inline] + pub fn make_mut(this: &mut Self) -> &mut T { + if !this.is_unique() { + // Another pointer exists; clone + *this = Arc::new((**this).clone()); + } + + unsafe { + // This unsafety is ok because we're guaranteed that the pointer + // returned is the *only* pointer that will ever be returned to T. Our + // reference count is guaranteed to be 1 at this point, and we required + // the Arc itself to be `mut`, so we're returning the only possible + // reference to the inner data. + &mut (*this.ptr()).data + } + } +} + +impl<T: ?Sized> Arc<T> { + /// Provides mutable access to the contents _if_ the `Arc` is uniquely owned. + #[inline] + pub fn get_mut(this: &mut Self) -> Option<&mut T> { + if this.is_unique() { + unsafe { + // See make_mut() for documentation of the threadsafety here. + Some(&mut (*this.ptr()).data) + } + } else { + None + } + } + + /// Whether or not the `Arc` is a static reference. + #[inline] + pub fn is_static(&self) -> bool { + // Using a relaxed ordering to check for STATIC_REFCOUNT is safe, since + // `count` never changes between STATIC_REFCOUNT and other values. + self.inner().count.load(Relaxed) == STATIC_REFCOUNT + } + + /// Whether or not the `Arc` is uniquely owned (is the refcount 1?) and not + /// a static reference. + #[inline] + pub fn is_unique(&self) -> bool { + // See the extensive discussion in [1] for why this needs to be Acquire. + // + // [1] https://github.com/servo/servo/issues/21186 + self.inner().count.load(Acquire) == 1 + } +} + +impl<T: ?Sized> Drop for Arc<T> { + #[inline] + fn drop(&mut self) { + // NOTE(emilio): If you change anything here, make sure that the + // implementation in layout/style/ServoStyleConstsInlines.h matches! + if self.is_static() { + return; + } + + // Because `fetch_sub` is already atomic, we do not need to synchronize + // with other threads unless we are going to delete the object. + if self.inner().count.fetch_sub(1, Release) != 1 { + return; + } + + // FIXME(bholley): Use the updated comment when [2] is merged. + // + // This load is needed to prevent reordering of use of the data and + // deletion of the data. Because it is marked `Release`, the decreasing + // of the reference count synchronizes with this `Acquire` load. This + // means that use of the data happens before decreasing the reference + // count, which happens before this load, which happens before the + // deletion of the data. + // + // As explained in the [Boost documentation][1], + // + // > It is important to enforce any possible access to the object in one + // > thread (through an existing reference) to *happen before* deleting + // > the object in a different thread. This is achieved by a "release" + // > operation after dropping a reference (any access to the object + // > through this reference must obviously happened before), and an + // > "acquire" operation before deleting the object. + // + // [1]: (www.boost.org/doc/libs/1_55_0/doc/html/atomic/usage_examples.html) + // [2]: https://github.com/rust-lang/rust/pull/41714 + self.inner().count.load(Acquire); + + unsafe { + self.drop_slow(); + } + } +} + +impl<T: ?Sized + PartialEq> PartialEq for Arc<T> { + fn eq(&self, other: &Arc<T>) -> bool { + Self::ptr_eq(self, other) || *(*self) == *(*other) + } + + fn ne(&self, other: &Arc<T>) -> bool { + !Self::ptr_eq(self, other) && *(*self) != *(*other) + } +} + +impl<T: ?Sized + PartialOrd> PartialOrd for Arc<T> { + fn partial_cmp(&self, other: &Arc<T>) -> Option<Ordering> { + (**self).partial_cmp(&**other) + } + + fn lt(&self, other: &Arc<T>) -> bool { + *(*self) < *(*other) + } + + fn le(&self, other: &Arc<T>) -> bool { + *(*self) <= *(*other) + } + + fn gt(&self, other: &Arc<T>) -> bool { + *(*self) > *(*other) + } + + fn ge(&self, other: &Arc<T>) -> bool { + *(*self) >= *(*other) + } +} +impl<T: ?Sized + Ord> Ord for Arc<T> { + fn cmp(&self, other: &Arc<T>) -> Ordering { + (**self).cmp(&**other) + } +} +impl<T: ?Sized + Eq> Eq for Arc<T> {} + +impl<T: ?Sized + fmt::Display> fmt::Display for Arc<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Display::fmt(&**self, f) + } +} + +impl<T: ?Sized + fmt::Debug> fmt::Debug for Arc<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&**self, f) + } +} + +impl<T: ?Sized> fmt::Pointer for Arc<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Pointer::fmt(&self.ptr(), f) + } +} + +impl<T: Default> Default for Arc<T> { + fn default() -> Arc<T> { + Arc::new(Default::default()) + } +} + +impl<T: ?Sized + Hash> Hash for Arc<T> { + fn hash<H: Hasher>(&self, state: &mut H) { + (**self).hash(state) + } +} + +impl<T> From<T> for Arc<T> { + #[inline] + fn from(t: T) -> Self { + Arc::new(t) + } +} + +impl<T: ?Sized> borrow::Borrow<T> for Arc<T> { + #[inline] + fn borrow(&self) -> &T { + &**self + } +} + +impl<T: ?Sized> AsRef<T> for Arc<T> { + #[inline] + fn as_ref(&self) -> &T { + &**self + } +} + +unsafe impl<T: ?Sized> StableDeref for Arc<T> {} +unsafe impl<T: ?Sized> CloneStableDeref for Arc<T> {} + +#[cfg(feature = "servo")] +impl<'de, T: Deserialize<'de>> Deserialize<'de> for Arc<T> { + fn deserialize<D>(deserializer: D) -> Result<Arc<T>, D::Error> + where + D: ::serde::de::Deserializer<'de>, + { + T::deserialize(deserializer).map(Arc::new) + } +} + +#[cfg(feature = "servo")] +impl<T: Serialize> Serialize for Arc<T> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: ::serde::ser::Serializer, + { + (**self).serialize(serializer) + } +} + +/// Structure to allow Arc-managing some fixed-sized data and a variably-sized +/// slice in a single allocation. +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[derive(Debug, Eq)] +#[repr(C)] +pub struct HeaderSlice<H, T> { + /// The fixed-sized data. + pub header: H, + + /// The length of the slice at our end. + len: usize, + + /// The dynamically-sized data. + data: [T; 0], +} + +impl<H: PartialEq, T: PartialEq> PartialEq for HeaderSlice<H, T> { + fn eq(&self, other: &Self) -> bool { + self.header == other.header && self.slice() == other.slice() + } +} + +impl<H, T> Drop for HeaderSlice<H, T> { + fn drop(&mut self) { + unsafe { + let mut ptr = self.data_mut(); + for _ in 0..self.len { + std::ptr::drop_in_place(ptr); + ptr = ptr.offset(1); + } + } + } +} + +impl<H, T> HeaderSlice<H, T> { + /// Returns the dynamically sized slice in this HeaderSlice. + #[inline(always)] + pub fn slice(&self) -> &[T] { + unsafe { std::slice::from_raw_parts(self.data(), self.len) } + } + + #[inline(always)] + fn data(&self) -> *const T { + std::ptr::addr_of!(self.data) as *const _ + } + + #[inline(always)] + fn data_mut(&mut self) -> *mut T { + std::ptr::addr_of_mut!(self.data) as *mut _ + } + + /// Returns the dynamically sized slice in this HeaderSlice. + #[inline(always)] + pub fn slice_mut(&mut self) -> &mut [T] { + unsafe { std::slice::from_raw_parts_mut(self.data_mut(), self.len) } + } + + /// Returns the len of the slice. + #[inline(always)] + pub fn len(&self) -> usize { + self.len + } +} + +#[inline(always)] +fn divide_rounding_up(dividend: usize, divisor: usize) -> usize { + (dividend + divisor - 1) / divisor +} + +impl<H, T> Arc<HeaderSlice<H, T>> { + /// Creates an Arc for a HeaderSlice using the given header struct and + /// iterator to generate the slice. + /// + /// `is_static` indicates whether to create a static Arc. + /// + /// `alloc` is used to get a pointer to the memory into which the + /// dynamically sized ArcInner<HeaderSlice<H, T>> value will be + /// written. If `is_static` is true, then `alloc` must return a + /// pointer into some static memory allocation. If it is false, + /// then `alloc` must return an allocation that can be dellocated + /// by calling Box::from_raw::<ArcInner<HeaderSlice<H, T>>> on it. + #[inline] + pub fn from_header_and_iter_alloc<F, I>( + alloc: F, + header: H, + mut items: I, + num_items: usize, + is_static: bool, + ) -> Self + where + F: FnOnce(Layout) -> *mut u8, + I: Iterator<Item = T>, + { + assert_ne!(size_of::<T>(), 0, "Need to think about ZST"); + + let size = size_of::<ArcInner<HeaderSlice<H, T>>>() + size_of::<T>() * num_items; + let inner_align = align_of::<ArcInner<HeaderSlice<H, T>>>(); + debug_assert!(inner_align >= align_of::<T>()); + + let ptr: *mut ArcInner<HeaderSlice<H, T>>; + unsafe { + // Allocate the buffer. + let layout = if inner_align <= align_of::<usize>() { + Layout::from_size_align_unchecked(size, align_of::<usize>()) + } else if inner_align <= align_of::<u64>() { + // On 32-bit platforms <T> may have 8 byte alignment while usize + // has 4 byte aligment. Use u64 to avoid over-alignment. + // This branch will compile away in optimized builds. + Layout::from_size_align_unchecked(size, align_of::<u64>()) + } else { + panic!("Over-aligned type not handled"); + }; + + let buffer = alloc(layout); + ptr = buffer as *mut ArcInner<HeaderSlice<H, T>>; + + // Write the data. + // + // Note that any panics here (i.e. from the iterator) are safe, since + // we'll just leak the uninitialized memory. + let count = if is_static { + atomic::AtomicUsize::new(STATIC_REFCOUNT) + } else { + atomic::AtomicUsize::new(1) + }; + ptr::write(&mut ((*ptr).count), count); + ptr::write(&mut ((*ptr).data.header), header); + ptr::write(&mut ((*ptr).data.len), num_items); + if num_items != 0 { + let mut current = std::ptr::addr_of_mut!((*ptr).data.data) as *mut T; + for _ in 0..num_items { + ptr::write( + current, + items + .next() + .expect("ExactSizeIterator over-reported length"), + ); + current = current.offset(1); + } + // We should have consumed the buffer exactly, maybe accounting + // for some padding from the alignment. + debug_assert!( + (buffer.add(size) as usize - current as *mut u8 as usize) < inner_align + ); + } + assert!( + items.next().is_none(), + "ExactSizeIterator under-reported length" + ); + } + #[cfg(feature = "gecko_refcount_logging")] + unsafe { + if !is_static { + // FIXME(emilio): Would be so amazing to have + // std::intrinsics::type_name() around. + NS_LogCtor(ptr as *mut _, b"ServoArc\0".as_ptr() as *const _, 8) + } + } + + // Return the fat Arc. + assert_eq!( + size_of::<Self>(), + size_of::<usize>(), + "The Arc should be thin" + ); + unsafe { + Arc { + p: ptr::NonNull::new_unchecked(ptr), + phantom: PhantomData, + } + } + } + + /// Creates an Arc for a HeaderSlice using the given header struct and iterator to generate the + /// slice. Panics if num_items doesn't match the number of items. + #[inline] + pub fn from_header_and_iter_with_size<I>(header: H, items: I, num_items: usize) -> Self + where + I: Iterator<Item = T>, + { + Arc::from_header_and_iter_alloc( + |layout| { + // align will only ever be align_of::<usize>() or align_of::<u64>() + let align = layout.align(); + unsafe { + if align == mem::align_of::<usize>() { + Self::allocate_buffer::<usize>(layout.size()) + } else { + assert_eq!(align, mem::align_of::<u64>()); + Self::allocate_buffer::<u64>(layout.size()) + } + } + }, + header, + items, + num_items, + /* is_static = */ false, + ) + } + + /// Creates an Arc for a HeaderSlice using the given header struct and + /// iterator to generate the slice. The resulting Arc will be fat. + #[inline] + pub fn from_header_and_iter<I>(header: H, items: I) -> Self + where + I: Iterator<Item = T> + ExactSizeIterator, + { + let len = items.len(); + Self::from_header_and_iter_with_size(header, items, len) + } + + #[inline] + unsafe fn allocate_buffer<W>(size: usize) -> *mut u8 { + // We use Vec because the underlying allocation machinery isn't + // available in stable Rust. To avoid alignment issues, we allocate + // words rather than bytes, rounding up to the nearest word size. + let words_to_allocate = divide_rounding_up(size, mem::size_of::<W>()); + let mut vec = Vec::<W>::with_capacity(words_to_allocate); + vec.set_len(words_to_allocate); + Box::into_raw(vec.into_boxed_slice()) as *mut W as *mut u8 + } +} + +/// This is functionally equivalent to Arc<(H, [T])> +/// +/// When you create an `Arc` containing a dynamically sized type like a slice, the `Arc` is +/// represented on the stack as a "fat pointer", where the length of the slice is stored alongside +/// the `Arc`'s pointer. In some situations you may wish to have a thin pointer instead, perhaps +/// for FFI compatibility or space efficiency. `ThinArc` solves this by storing the length in the +/// allocation itself, via `HeaderSlice`. +pub type ThinArc<H, T> = Arc<HeaderSlice<H, T>>; + +/// See `ArcUnion`. This is a version that works for `ThinArc`s. +pub type ThinArcUnion<H1, T1, H2, T2> = ArcUnion<HeaderSlice<H1, T1>, HeaderSlice<H2, T2>>; + +impl<H, T> UniqueArc<HeaderSlice<H, T>> { + #[inline] + pub fn from_header_and_iter<I>(header: H, items: I) -> Self + where + I: Iterator<Item = T> + ExactSizeIterator, + { + Self(Arc::from_header_and_iter(header, items)) + } + + #[inline] + pub fn from_header_and_iter_with_size<I>(header: H, items: I, num_items: usize) -> Self + where + I: Iterator<Item = T>, + { + Self(Arc::from_header_and_iter_with_size( + header, items, num_items, + )) + } + + /// Returns a mutable reference to the header. + pub fn header_mut(&mut self) -> &mut H { + // We know this to be uniquely owned + unsafe { &mut (*self.0.ptr()).data.header } + } + + /// Returns a mutable reference to the slice. + pub fn data_mut(&mut self) -> &mut [T] { + // We know this to be uniquely owned + unsafe { (*self.0.ptr()).data.slice_mut() } + } +} + +/// A "borrowed `Arc`". This is a pointer to +/// a T that is known to have been allocated within an +/// `Arc`. +/// +/// This is equivalent in guarantees to `&Arc<T>`, however it is +/// a bit more flexible. To obtain an `&Arc<T>` you must have +/// an `Arc<T>` instance somewhere pinned down until we're done with it. +/// It's also a direct pointer to `T`, so using this involves less pointer-chasing +/// +/// However, C++ code may hand us refcounted things as pointers to T directly, +/// so we have to conjure up a temporary `Arc` on the stack each time. +/// +/// `ArcBorrow` lets us deal with borrows of known-refcounted objects +/// without needing to worry about where the `Arc<T>` is. +#[derive(Debug, Eq, PartialEq)] +pub struct ArcBorrow<'a, T: 'a>(&'a T); + +impl<'a, T> Copy for ArcBorrow<'a, T> {} +impl<'a, T> Clone for ArcBorrow<'a, T> { + #[inline] + fn clone(&self) -> Self { + *self + } +} + +impl<'a, T> ArcBorrow<'a, T> { + /// Clone this as an `Arc<T>`. This bumps the refcount. + #[inline] + pub fn clone_arc(&self) -> Arc<T> { + let arc = unsafe { Arc::from_raw(self.0) }; + // addref it! + mem::forget(arc.clone()); + arc + } + + /// For constructing from a reference known to be Arc-backed, + /// e.g. if we obtain such a reference over FFI + #[inline] + pub unsafe fn from_ref(r: &'a T) -> Self { + ArcBorrow(r) + } + + /// Compare two `ArcBorrow`s via pointer equality. Will only return + /// true if they come from the same allocation + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + this.0 as *const T == other.0 as *const T + } + + /// Temporarily converts |self| into a bonafide Arc and exposes it to the + /// provided callback. The refcount is not modified. + #[inline] + pub fn with_arc<F, U>(&self, f: F) -> U + where + F: FnOnce(&Arc<T>) -> U, + T: 'static, + { + // Synthesize transient Arc, which never touches the refcount. + let transient = unsafe { mem::ManuallyDrop::new(Arc::from_raw(self.0)) }; + + // Expose the transient Arc to the callback, which may clone it if it wants. + let result = f(&transient); + + // Forward the result. + result + } + + /// Similar to deref, but uses the lifetime |a| rather than the lifetime of + /// self, which is incompatible with the signature of the Deref trait. + #[inline] + pub fn get(&self) -> &'a T { + self.0 + } +} + +impl<'a, T> Deref for ArcBorrow<'a, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.0 + } +} + +/// A tagged union that can represent `Arc<A>` or `Arc<B>` while only consuming a +/// single word. The type is also `NonNull`, and thus can be stored in an Option +/// without increasing size. +/// +/// This is functionally equivalent to +/// `enum ArcUnion<A, B> { First(Arc<A>), Second(Arc<B>)` but only takes up +/// up a single word of stack space. +/// +/// This could probably be extended to support four types if necessary. +pub struct ArcUnion<A, B> { + p: ptr::NonNull<()>, + phantom_a: PhantomData<A>, + phantom_b: PhantomData<B>, +} + +unsafe impl<A: Sync + Send, B: Send + Sync> Send for ArcUnion<A, B> {} +unsafe impl<A: Sync + Send, B: Send + Sync> Sync for ArcUnion<A, B> {} + +impl<A: PartialEq, B: PartialEq> PartialEq for ArcUnion<A, B> { + fn eq(&self, other: &Self) -> bool { + use crate::ArcUnionBorrow::*; + match (self.borrow(), other.borrow()) { + (First(x), First(y)) => x == y, + (Second(x), Second(y)) => x == y, + (_, _) => false, + } + } +} + +impl<A: Eq, B: Eq> Eq for ArcUnion<A, B> {} + +/// This represents a borrow of an `ArcUnion`. +#[derive(Debug)] +pub enum ArcUnionBorrow<'a, A: 'a, B: 'a> { + First(ArcBorrow<'a, A>), + Second(ArcBorrow<'a, B>), +} + +impl<A, B> ArcUnion<A, B> { + unsafe fn new(ptr: *mut ()) -> Self { + ArcUnion { + p: ptr::NonNull::new_unchecked(ptr), + phantom_a: PhantomData, + phantom_b: PhantomData, + } + } + + /// Returns true if the two values are pointer-equal. + #[inline] + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + this.p == other.p + } + + #[inline] + pub fn ptr(&self) -> ptr::NonNull<()> { + self.p + } + + /// Returns an enum representing a borrow of either A or B. + #[inline] + pub fn borrow(&self) -> ArcUnionBorrow<A, B> { + if self.is_first() { + let ptr = self.p.as_ptr() as *const ArcInner<A>; + let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) }; + ArcUnionBorrow::First(borrow) + } else { + let ptr = ((self.p.as_ptr() as usize) & !0x1) as *const ArcInner<B>; + let borrow = unsafe { ArcBorrow::from_ref(&(*ptr).data) }; + ArcUnionBorrow::Second(borrow) + } + } + + /// Creates an `ArcUnion` from an instance of the first type. + pub fn from_first(other: Arc<A>) -> Self { + let union = unsafe { Self::new(other.ptr() as *mut _) }; + mem::forget(other); + union + } + + /// Creates an `ArcUnion` from an instance of the second type. + pub fn from_second(other: Arc<B>) -> Self { + let union = unsafe { Self::new(((other.ptr() as usize) | 0x1) as *mut _) }; + mem::forget(other); + union + } + + /// Returns true if this `ArcUnion` contains the first type. + pub fn is_first(&self) -> bool { + self.p.as_ptr() as usize & 0x1 == 0 + } + + /// Returns true if this `ArcUnion` contains the second type. + pub fn is_second(&self) -> bool { + !self.is_first() + } + + /// Returns a borrow of the first type if applicable, otherwise `None`. + pub fn as_first(&self) -> Option<ArcBorrow<A>> { + match self.borrow() { + ArcUnionBorrow::First(x) => Some(x), + ArcUnionBorrow::Second(_) => None, + } + } + + /// Returns a borrow of the second type if applicable, otherwise None. + pub fn as_second(&self) -> Option<ArcBorrow<B>> { + match self.borrow() { + ArcUnionBorrow::First(_) => None, + ArcUnionBorrow::Second(x) => Some(x), + } + } +} + +impl<A, B> Clone for ArcUnion<A, B> { + fn clone(&self) -> Self { + match self.borrow() { + ArcUnionBorrow::First(x) => ArcUnion::from_first(x.clone_arc()), + ArcUnionBorrow::Second(x) => ArcUnion::from_second(x.clone_arc()), + } + } +} + +impl<A, B> Drop for ArcUnion<A, B> { + fn drop(&mut self) { + match self.borrow() { + ArcUnionBorrow::First(x) => unsafe { + let _ = Arc::from_raw(&*x); + }, + ArcUnionBorrow::Second(x) => unsafe { + let _ = Arc::from_raw(&*x); + }, + } + } +} + +impl<A: fmt::Debug, B: fmt::Debug> fmt::Debug for ArcUnion<A, B> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fmt::Debug::fmt(&self.borrow(), f) + } +} + +#[cfg(test)] +mod tests { + use super::{Arc, ThinArc}; + use std::clone::Clone; + use std::ops::Drop; + use std::sync::atomic; + use std::sync::atomic::Ordering::{Acquire, SeqCst}; + + #[derive(PartialEq)] + struct Canary(*mut atomic::AtomicUsize); + + impl Drop for Canary { + fn drop(&mut self) { + unsafe { + (*self.0).fetch_add(1, SeqCst); + } + } + } + + #[test] + fn empty_thin() { + let x = Arc::from_header_and_iter(100u32, std::iter::empty::<i32>()); + assert_eq!(x.header, 100); + assert!(x.slice().is_empty()); + } + + #[test] + fn thin_assert_padding() { + #[derive(Clone, Default)] + #[repr(C)] + struct Padded { + i: u16, + } + + // The header will have more alignment than `Padded` + let items = vec![Padded { i: 0xdead }, Padded { i: 0xbeef }]; + let a = ThinArc::from_header_and_iter(0i32, items.into_iter()); + assert_eq!(a.len(), 2); + assert_eq!(a.slice()[0].i, 0xdead); + assert_eq!(a.slice()[1].i, 0xbeef); + } + + #[test] + fn slices_and_thin() { + let mut canary = atomic::AtomicUsize::new(0); + let c = Canary(&mut canary as *mut atomic::AtomicUsize); + let v = vec![5, 6]; + { + let x = Arc::from_header_and_iter(c, v.into_iter()); + let _ = x.clone(); + let _ = x == x; + } + assert_eq!(canary.load(Acquire), 1); + } +} diff --git a/servo/components/style/Cargo.toml b/servo/components/style/Cargo.toml new file mode 100644 index 0000000000..acf1bcf6fe --- /dev/null +++ b/servo/components/style/Cargo.toml @@ -0,0 +1,91 @@ +[package] +name = "style" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +build = "build.rs" +edition = "2018" + +# https://github.com/rust-lang/cargo/issues/3544 +links = "servo_style_crate" + +[lib] +name = "style" +path = "lib.rs" +doctest = false + +[features] +gecko = ["nsstring", "serde", "style_traits/gecko", "bindgen", "regex", "toml", "mozbuild"] +servo = ["serde", "style_traits/servo", "servo_atoms", "servo_config", "html5ever", + "cssparser/serde", "encoding_rs", "malloc_size_of/servo", "arrayvec/use_union", + "servo_url", "string_cache", "to_shmem/servo", "servo_arc/servo"] +servo-layout-2013 = [] +servo-layout-2020 = [] +gecko_debug = [] +gecko_refcount_logging = [] + +[dependencies] +app_units = "0.7" +arrayvec = "0.7" +atomic_refcell = "0.1" +bitflags = "2" +byteorder = "1.0" +cssparser = "0.33" +derive_more = { version = "0.99", default-features = false, features = ["add", "add_assign", "deref", "deref_mut", "from"] } +dom = { path = "../../../dom/base/rust" } +new_debug_unreachable = "1.0" +encoding_rs = {version = "0.8", optional = true} +euclid = "0.22" +fxhash = "0.2" +html5ever = {version = "0.24", optional = true} +icu_segmenter = { version = "1.4", default-features = false, features = ["auto", "compiled_data"] } +indexmap = {version = "1.0", features = ["std"]} +itertools = "0.10" +itoa = "1.0" +lazy_static = "1" +log = "0.4" +malloc_size_of = { path = "../malloc_size_of" } +malloc_size_of_derive = { path = "../../../xpcom/rust/malloc_size_of_derive" } +matches = "0.1" +nsstring = {path = "../../../xpcom/rust/nsstring/", optional = true} +num_cpus = {version = "1.1.0"} +num-integer = "0.1" +num-traits = "0.2" +num-derive = "0.4" +owning_ref = "0.4" +parking_lot = "0.12" +precomputed-hash = "0.1.1" +rayon = "1" +rayon-core = "1" +selectors = { path = "../selectors" } +serde = {version = "1.0", optional = true, features = ["derive"]} +servo_arc = { path = "../servo_arc" } +servo_atoms = {path = "../atoms", optional = true} +servo_config = {path = "../config", optional = true} +smallbitvec = "2.3.0" +smallvec = "1.0" +static_assertions = "1.1" +static_prefs = { path = "../../../modules/libpref/init/static_prefs" } +string_cache = { version = "0.8", optional = true } +style_derive = {path = "../style_derive"} +style_traits = {path = "../style_traits"} +servo_url = {path = "../url", optional = true} +to_shmem = {path = "../to_shmem"} +to_shmem_derive = {path = "../to_shmem_derive"} +time = "0.1" +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } +uluru = "3.0" +unicode-bidi = { version = "0.3", default-features = false } +void = "1.0.2" +gecko-profiler = { path = "../../../tools/profiler/rust-api" } + +[build-dependencies] +lazy_static = "1" +log = { version = "0.4", features = ["std"] } +bindgen = {version = "0.69", optional = true, default-features = false} +regex = {version = "1.0", optional = true, default-features = false, features = ["perf", "std"]} +walkdir = "2.1.4" +toml = {version = "0.5", optional = true, default-features = false} +mozbuild = {version = "0.1", optional = true} diff --git a/servo/components/style/README.md b/servo/components/style/README.md new file mode 100644 index 0000000000..96457e1b30 --- /dev/null +++ b/servo/components/style/README.md @@ -0,0 +1,6 @@ +servo-style +=========== + +Style system for Servo, using [rust-cssparser](https://github.com/servo/rust-cssparser) for parsing. + + * [Documentation](https://github.com/servo/servo/blob/master/docs/components/style.md). diff --git a/servo/components/style/animation.rs b/servo/components/style/animation.rs new file mode 100644 index 0000000000..b865120aba --- /dev/null +++ b/servo/components/style/animation.rs @@ -0,0 +1,1415 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS transitions and animations. + +// NOTE(emilio): This code isn't really executed in Gecko, but we don't want to +// compile it out so that people remember it exists. + +use crate::context::{CascadeInputs, SharedStyleContext}; +use crate::dom::{OpaqueNode, TDocument, TElement, TNode}; +use crate::properties::animated_properties::{AnimationValue, AnimationValueMap}; +use crate::properties::longhands::animation_direction::computed_value::single_value::T as AnimationDirection; +use crate::properties::longhands::animation_fill_mode::computed_value::single_value::T as AnimationFillMode; +use crate::properties::longhands::animation_play_state::computed_value::single_value::T as AnimationPlayState; +use crate::properties::AnimationDeclarations; +use crate::properties::{ + ComputedValues, Importance, LonghandId, PropertyDeclarationBlock, PropertyDeclarationId, + PropertyDeclarationIdSet, +}; +use crate::rule_tree::CascadeLevel; +use crate::selector_parser::PseudoElement; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::style_resolver::StyleResolverForElement; +use crate::stylesheets::keyframes_rule::{KeyframesAnimation, KeyframesStep, KeyframesStepValue}; +use crate::stylesheets::layer_rule::LayerOrder; +use crate::values::animated::{Animate, Procedure}; +use crate::values::computed::{Time, TimingFunction}; +use crate::values::generics::easing::BeforeFlag; +use crate::Atom; +use fxhash::FxHashMap; +use parking_lot::RwLock; +use servo_arc::Arc; +use std::fmt; + +/// Represents an animation for a given property. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct PropertyAnimation { + /// The value we are animating from. + from: AnimationValue, + + /// The value we are animating to. + to: AnimationValue, + + /// The timing function of this `PropertyAnimation`. + timing_function: TimingFunction, + + /// The duration of this `PropertyAnimation` in seconds. + pub duration: f64, +} + +impl PropertyAnimation { + /// Returns the given property longhand id. + pub fn property_id(&self) -> PropertyDeclarationId { + debug_assert_eq!(self.from.id(), self.to.id()); + self.from.id() + } + + fn from_property_declaration( + property_declaration: &PropertyDeclarationId, + timing_function: TimingFunction, + duration: Time, + old_style: &ComputedValues, + new_style: &ComputedValues, + ) -> Option<PropertyAnimation> { + // FIXME(emilio): Handle the case where old_style and new_style's writing mode differ. + let property_declaration = property_declaration.to_physical(new_style.writing_mode); + let from = AnimationValue::from_computed_values(property_declaration, old_style)?; + let to = AnimationValue::from_computed_values(property_declaration, new_style)?; + let duration = duration.seconds() as f64; + + if from == to || duration == 0.0 { + return None; + } + + Some(PropertyAnimation { + from, + to, + timing_function, + duration, + }) + } + + /// The output of the timing function given the progress ration of this animation. + fn timing_function_output(&self, progress: f64) -> f64 { + let epsilon = 1. / (200. * self.duration); + // FIXME: Need to set the before flag correctly. + // In order to get the before flag, we have to know the current animation phase + // and whether the iteration is reversed. For now, we skip this calculation + // by treating as if the flag is unset at all times. + // https://drafts.csswg.org/css-easing/#step-timing-function-algo + self.timing_function + .calculate_output(progress, BeforeFlag::Unset, epsilon) + } + + /// Update the given animation at a given point of progress. + fn calculate_value(&self, progress: f64) -> Result<AnimationValue, ()> { + let procedure = Procedure::Interpolate { + progress: self.timing_function_output(progress), + }; + self.from.animate(&self.to, procedure) + } +} + +/// This structure represents the state of an animation. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub enum AnimationState { + /// The animation has been created, but is not running yet. This state + /// is also used when an animation is still in the first delay phase. + Pending, + /// This animation is currently running. + Running, + /// This animation is paused. The inner field is the percentage of progress + /// when it was paused, from 0 to 1. + Paused(f64), + /// This animation has finished. + Finished, + /// This animation has been canceled. + Canceled, +} + +impl AnimationState { + /// Whether or not this state requires its owning animation to be ticked. + fn needs_to_be_ticked(&self) -> bool { + *self == AnimationState::Running || *self == AnimationState::Pending + } +} + +/// This structure represents a keyframes animation current iteration state. +/// +/// If the iteration count is infinite, there's no other state, otherwise we +/// have to keep track the current iteration and the max iteration count. +#[derive(Clone, Debug, MallocSizeOf)] +pub enum KeyframesIterationState { + /// Infinite iterations with the current iteration count. + Infinite(f64), + /// Current and max iterations. + Finite(f64, f64), +} + +/// A temporary data structure used when calculating ComputedKeyframes for an +/// animation. This data structure is used to collapse information for steps +/// which may be spread across multiple keyframe declarations into a single +/// instance per `start_percentage`. +struct IntermediateComputedKeyframe { + declarations: PropertyDeclarationBlock, + timing_function: Option<TimingFunction>, + start_percentage: f32, +} + +impl IntermediateComputedKeyframe { + fn new(start_percentage: f32) -> Self { + IntermediateComputedKeyframe { + declarations: PropertyDeclarationBlock::new(), + timing_function: None, + start_percentage, + } + } + + /// Walk through all keyframe declarations and combine all declarations with the + /// same `start_percentage` into individual `IntermediateComputedKeyframe`s. + fn generate_for_keyframes( + animation: &KeyframesAnimation, + context: &SharedStyleContext, + base_style: &ComputedValues, + ) -> Vec<Self> { + let mut intermediate_steps: Vec<Self> = Vec::with_capacity(animation.steps.len()); + let mut current_step = IntermediateComputedKeyframe::new(0.); + for step in animation.steps.iter() { + let start_percentage = step.start_percentage.0; + if start_percentage != current_step.start_percentage { + let new_step = IntermediateComputedKeyframe::new(start_percentage); + intermediate_steps.push(std::mem::replace(&mut current_step, new_step)); + } + + current_step.update_from_step(step, context, base_style); + } + intermediate_steps.push(current_step); + + // We should always have a first and a last step, even if these are just + // generated by KeyframesStepValue::ComputedValues. + debug_assert!(intermediate_steps.first().unwrap().start_percentage == 0.); + debug_assert!(intermediate_steps.last().unwrap().start_percentage == 1.); + + intermediate_steps + } + + fn update_from_step( + &mut self, + step: &KeyframesStep, + context: &SharedStyleContext, + base_style: &ComputedValues, + ) { + // Each keyframe declaration may optionally specify a timing function, falling + // back to the one defined global for the animation. + let guard = &context.guards.author; + if let Some(timing_function) = step.get_animation_timing_function(&guard) { + self.timing_function = Some(timing_function.to_computed_value_without_context()); + } + + let block = match step.value { + KeyframesStepValue::ComputedValues => return, + KeyframesStepValue::Declarations { ref block } => block, + }; + + // Filter out !important, non-animatable properties, and the + // 'display' property (which is only animatable from SMIL). + let guard = block.read_with(&guard); + for declaration in guard.normal_declaration_iter() { + if let PropertyDeclarationId::Longhand(id) = declaration.id() { + if id == LonghandId::Display { + continue; + } + + if !id.is_animatable() { + continue; + } + } + + self.declarations.push( + declaration.to_physical(base_style.writing_mode), + Importance::Normal, + ); + } + } + + fn resolve_style<E>( + self, + element: E, + context: &SharedStyleContext, + base_style: &Arc<ComputedValues>, + resolver: &mut StyleResolverForElement<E>, + ) -> Arc<ComputedValues> + where + E: TElement, + { + if !self.declarations.any_normal() { + return base_style.clone(); + } + + let document = element.as_node().owner_doc(); + let locked_block = Arc::new(document.shared_lock().wrap(self.declarations)); + let mut important_rules_changed = false; + let rule_node = base_style.rules().clone(); + let new_node = context.stylist.rule_tree().update_rule_at_level( + CascadeLevel::Animations, + LayerOrder::root(), + Some(locked_block.borrow_arc()), + &rule_node, + &context.guards, + &mut important_rules_changed, + ); + + if new_node.is_none() { + return base_style.clone(); + } + + let inputs = CascadeInputs { + rules: new_node, + visited_rules: base_style.visited_rules().cloned(), + flags: base_style.flags.for_cascade_inputs(), + }; + resolver + .cascade_style_and_visited_with_default_parents(inputs) + .0 + } +} + +/// A single computed keyframe for a CSS Animation. +#[derive(Clone, MallocSizeOf)] +struct ComputedKeyframe { + /// The timing function to use for transitions between this step + /// and the next one. + timing_function: TimingFunction, + + /// The starting percentage (a number between 0 and 1) which represents + /// at what point in an animation iteration this step is. + start_percentage: f32, + + /// The animation values to transition to and from when processing this + /// keyframe animation step. + values: Vec<AnimationValue>, +} + +impl ComputedKeyframe { + fn generate_for_keyframes<E>( + element: E, + animation: &KeyframesAnimation, + context: &SharedStyleContext, + base_style: &Arc<ComputedValues>, + default_timing_function: TimingFunction, + resolver: &mut StyleResolverForElement<E>, + ) -> Vec<Self> + where + E: TElement, + { + let mut animating_properties = PropertyDeclarationIdSet::default(); + for property in animation.properties_changed.iter() { + debug_assert!(property.is_animatable()); + animating_properties.insert(property.to_physical(base_style.writing_mode)); + } + + let animation_values_from_style: Vec<AnimationValue> = animating_properties + .iter() + .map(|property| { + AnimationValue::from_computed_values(property, &**base_style) + .expect("Unexpected non-animatable property.") + }) + .collect(); + + let intermediate_steps = + IntermediateComputedKeyframe::generate_for_keyframes(animation, context, base_style); + + let mut computed_steps: Vec<Self> = Vec::with_capacity(intermediate_steps.len()); + for (step_index, step) in intermediate_steps.into_iter().enumerate() { + let start_percentage = step.start_percentage; + let properties_changed_in_step = step.declarations.property_ids().clone(); + let step_timing_function = step.timing_function.clone(); + let step_style = step.resolve_style(element, context, base_style, resolver); + let timing_function = + step_timing_function.unwrap_or_else(|| default_timing_function.clone()); + + let values = { + // If a value is not set in a property declaration we use the value from + // the style for the first and last keyframe. For intermediate ones, we + // use the value from the previous keyframe. + // + // TODO(mrobinson): According to the spec, we should use an interpolated + // value for properties missing from keyframe declarations. + let default_values = if start_percentage == 0. || start_percentage == 1.0 { + &animation_values_from_style + } else { + debug_assert!(step_index != 0); + &computed_steps[step_index - 1].values + }; + + // For each property that is animating, pull the value from the resolved + // style for this step if it's in one of the declarations. Otherwise, we + // use the default value from the set we calculated above. + animating_properties + .iter() + .zip(default_values.iter()) + .map(|(property_declaration, default_value)| { + if properties_changed_in_step.contains(property_declaration) { + AnimationValue::from_computed_values(property_declaration, &step_style) + .unwrap_or_else(|| default_value.clone()) + } else { + default_value.clone() + } + }) + .collect() + }; + + computed_steps.push(ComputedKeyframe { + timing_function, + start_percentage, + values, + }); + } + computed_steps + } +} + +/// A CSS Animation +#[derive(Clone, MallocSizeOf)] +pub struct Animation { + /// The name of this animation as defined by the style. + pub name: Atom, + + /// The properties that change in this animation. + properties_changed: PropertyDeclarationIdSet, + + /// The computed style for each keyframe of this animation. + computed_steps: Vec<ComputedKeyframe>, + + /// The time this animation started at, which is the current value of the animation + /// timeline when this animation was created plus any animation delay. + pub started_at: f64, + + /// The duration of this animation. + pub duration: f64, + + /// The delay of the animation. + pub delay: f64, + + /// The `animation-fill-mode` property of this animation. + pub fill_mode: AnimationFillMode, + + /// The current iteration state for the animation. + pub iteration_state: KeyframesIterationState, + + /// Whether this animation is paused. + pub state: AnimationState, + + /// The declared animation direction of this animation. + pub direction: AnimationDirection, + + /// The current animation direction. This can only be `normal` or `reverse`. + pub current_direction: AnimationDirection, + + /// The original cascade style, needed to compute the generated keyframes of + /// the animation. + #[ignore_malloc_size_of = "ComputedValues"] + pub cascade_style: Arc<ComputedValues>, + + /// Whether or not this animation is new and or has already been tracked + /// by the script thread. + pub is_new: bool, +} + +impl Animation { + /// Whether or not this animation is cancelled by changes from a new style. + fn is_cancelled_in_new_style(&self, new_style: &Arc<ComputedValues>) -> bool { + let new_ui = new_style.get_ui(); + let index = new_ui + .animation_name_iter() + .position(|animation_name| Some(&self.name) == animation_name.as_atom()); + let index = match index { + Some(index) => index, + None => return true, + }; + + new_ui.animation_duration_mod(index).seconds() == 0. + } + + /// Given the current time, advances this animation to the next iteration, + /// updates times, and then toggles the direction if appropriate. Otherwise + /// does nothing. Returns true if this animation has iterated. + pub fn iterate_if_necessary(&mut self, time: f64) -> bool { + if !self.iteration_over(time) { + return false; + } + + // Only iterate animations that are currently running. + if self.state != AnimationState::Running { + return false; + } + + if self.on_last_iteration() { + return false; + } + + self.iterate(); + true + } + + fn iterate(&mut self) { + debug_assert!(!self.on_last_iteration()); + + if let KeyframesIterationState::Finite(ref mut current, max) = self.iteration_state { + *current = (*current + 1.).min(max); + } + + if let AnimationState::Paused(ref mut progress) = self.state { + debug_assert!(*progress > 1.); + *progress -= 1.; + } + + // Update the next iteration direction if applicable. + self.started_at += self.duration; + match self.direction { + AnimationDirection::Alternate | AnimationDirection::AlternateReverse => { + self.current_direction = match self.current_direction { + AnimationDirection::Normal => AnimationDirection::Reverse, + AnimationDirection::Reverse => AnimationDirection::Normal, + _ => unreachable!(), + }; + }, + _ => {}, + } + } + + /// A number (> 0 and <= 1) which represents the fraction of a full iteration + /// that the current iteration of the animation lasts. This will be less than 1 + /// if the current iteration is the fractional remainder of a non-integral + /// iteration count. + pub fn current_iteration_end_progress(&self) -> f64 { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => (max - current).min(1.), + KeyframesIterationState::Infinite(_) => 1., + } + } + + /// The duration of the current iteration of this animation which may be less + /// than the animation duration if it has a non-integral iteration count. + pub fn current_iteration_duration(&self) -> f64 { + self.current_iteration_end_progress() * self.duration + } + + /// Whether or not the current iteration is over. Note that this method assumes that + /// the animation is still running. + fn iteration_over(&self, time: f64) -> bool { + time > (self.started_at + self.current_iteration_duration()) + } + + /// Assuming this animation is running, whether or not it is on the last iteration. + fn on_last_iteration(&self) -> bool { + match self.iteration_state { + KeyframesIterationState::Finite(current, max) => current >= (max - 1.), + KeyframesIterationState::Infinite(_) => false, + } + } + + /// Whether or not this animation has finished at the provided time. This does + /// not take into account canceling i.e. when an animation or transition is + /// canceled due to changes in the style. + pub fn has_ended(&self, time: f64) -> bool { + if !self.on_last_iteration() { + return false; + } + + let progress = match self.state { + AnimationState::Finished => return true, + AnimationState::Paused(progress) => progress, + AnimationState::Running => (time - self.started_at) / self.duration, + AnimationState::Pending | AnimationState::Canceled => return false, + }; + + progress >= self.current_iteration_end_progress() + } + + /// Updates the appropiate state from other animation. + /// + /// This happens when an animation is re-submitted to layout, presumably + /// because of an state change. + /// + /// There are some bits of state we can't just replace, over all taking in + /// account times, so here's that logic. + pub fn update_from_other(&mut self, other: &Self, now: f64) { + use self::AnimationState::*; + + debug!( + "KeyframesAnimationState::update_from_other({:?}, {:?})", + self, other + ); + + // NB: We shall not touch the started_at field, since we don't want to + // restart the animation. + let old_started_at = self.started_at; + let old_duration = self.duration; + let old_direction = self.current_direction; + let old_state = self.state.clone(); + let old_iteration_state = self.iteration_state.clone(); + + *self = other.clone(); + + self.started_at = old_started_at; + self.current_direction = old_direction; + + // Don't update the iteration count, just the iteration limit. + // TODO: see how changing the limit affects rendering in other browsers. + // We might need to keep the iteration count even when it's infinite. + match (&mut self.iteration_state, old_iteration_state) { + ( + &mut KeyframesIterationState::Finite(ref mut iters, _), + KeyframesIterationState::Finite(old_iters, _), + ) => *iters = old_iters, + _ => {}, + } + + // Don't pause or restart animations that should remain finished. + // We call mem::replace because `has_ended(...)` looks at `Animation::state`. + let new_state = std::mem::replace(&mut self.state, Running); + if old_state == Finished && self.has_ended(now) { + self.state = Finished; + } else { + self.state = new_state; + } + + // If we're unpausing the animation, fake the start time so we seem to + // restore it. + // + // If the animation keeps paused, keep the old value. + // + // If we're pausing the animation, compute the progress value. + match (&mut self.state, &old_state) { + (&mut Pending, &Paused(progress)) => { + self.started_at = now - (self.duration * progress); + }, + (&mut Paused(ref mut new), &Paused(old)) => *new = old, + (&mut Paused(ref mut progress), &Running) => { + *progress = (now - old_started_at) / old_duration + }, + _ => {}, + } + + // Try to detect when we should skip straight to the running phase to + // avoid sending multiple animationstart events. + if self.state == Pending && self.started_at <= now && old_state != Pending { + self.state = Running; + } + } + + /// Fill in an `AnimationValueMap` with values calculated from this animation at + /// the given time value. + fn get_property_declaration_at_time(&self, now: f64, map: &mut AnimationValueMap) { + debug_assert!(!self.computed_steps.is_empty()); + + let total_progress = match self.state { + AnimationState::Running | AnimationState::Pending | AnimationState::Finished => { + (now - self.started_at) / self.duration + }, + AnimationState::Paused(progress) => progress, + AnimationState::Canceled => return, + }; + + if total_progress < 0. && + self.fill_mode != AnimationFillMode::Backwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + if self.has_ended(now) && + self.fill_mode != AnimationFillMode::Forwards && + self.fill_mode != AnimationFillMode::Both + { + return; + } + let total_progress = total_progress + .min(self.current_iteration_end_progress()) + .max(0.0); + + // Get the indices of the previous (from) keyframe and the next (to) keyframe. + let next_keyframe_index; + let prev_keyframe_index; + let num_steps = self.computed_steps.len(); + match self.current_direction { + AnimationDirection::Normal => { + next_keyframe_index = self + .computed_steps + .iter() + .position(|step| total_progress as f32 <= step.start_percentage); + prev_keyframe_index = next_keyframe_index + .and_then(|pos| if pos != 0 { Some(pos - 1) } else { None }) + .unwrap_or(0); + }, + AnimationDirection::Reverse => { + next_keyframe_index = self + .computed_steps + .iter() + .rev() + .position(|step| total_progress as f32 <= 1. - step.start_percentage) + .map(|pos| num_steps - pos - 1); + prev_keyframe_index = next_keyframe_index + .and_then(|pos| { + if pos != num_steps - 1 { + Some(pos + 1) + } else { + None + } + }) + .unwrap_or(num_steps - 1) + }, + _ => unreachable!(), + } + + debug!( + "Animation::get_property_declaration_at_time: keyframe from {:?} to {:?}", + prev_keyframe_index, next_keyframe_index + ); + + let prev_keyframe = &self.computed_steps[prev_keyframe_index]; + let next_keyframe = match next_keyframe_index { + Some(index) => &self.computed_steps[index], + None => return, + }; + + // If we only need to take into account one keyframe, then exit early + // in order to avoid doing more work. + let mut add_declarations_to_map = |keyframe: &ComputedKeyframe| { + for value in keyframe.values.iter() { + map.insert(value.id().to_owned(), value.clone()); + } + }; + if total_progress <= 0.0 { + add_declarations_to_map(&prev_keyframe); + return; + } + if total_progress >= 1.0 { + add_declarations_to_map(&next_keyframe); + return; + } + + let percentage_between_keyframes = + (next_keyframe.start_percentage - prev_keyframe.start_percentage).abs() as f64; + let duration_between_keyframes = percentage_between_keyframes * self.duration; + let direction_aware_prev_keyframe_start_percentage = match self.current_direction { + AnimationDirection::Normal => prev_keyframe.start_percentage as f64, + AnimationDirection::Reverse => 1. - prev_keyframe.start_percentage as f64, + _ => unreachable!(), + }; + let progress_between_keyframes = (total_progress - + direction_aware_prev_keyframe_start_percentage) / + percentage_between_keyframes; + + for (from, to) in prev_keyframe.values.iter().zip(next_keyframe.values.iter()) { + let animation = PropertyAnimation { + from: from.clone(), + to: to.clone(), + timing_function: prev_keyframe.timing_function.clone(), + duration: duration_between_keyframes as f64, + }; + + if let Ok(value) = animation.calculate_value(progress_between_keyframes) { + map.insert(value.id().to_owned(), value); + } + } + } +} + +impl fmt::Debug for Animation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Animation") + .field("name", &self.name) + .field("started_at", &self.started_at) + .field("duration", &self.duration) + .field("delay", &self.delay) + .field("iteration_state", &self.iteration_state) + .field("state", &self.state) + .field("direction", &self.direction) + .field("current_direction", &self.current_direction) + .field("cascade_style", &()) + .finish() + } +} + +/// A CSS Transition +#[derive(Clone, Debug, MallocSizeOf)] +pub struct Transition { + /// The start time of this transition, which is the current value of the animation + /// timeline when this transition was created plus any animation delay. + pub start_time: f64, + + /// The delay used for this transition. + pub delay: f64, + + /// The internal style `PropertyAnimation` for this transition. + pub property_animation: PropertyAnimation, + + /// The state of this transition. + pub state: AnimationState, + + /// Whether or not this transition is new and or has already been tracked + /// by the script thread. + pub is_new: bool, + + /// If this `Transition` has been replaced by a new one this field is + /// used to help produce better reversed transitions. + pub reversing_adjusted_start_value: AnimationValue, + + /// If this `Transition` has been replaced by a new one this field is + /// used to help produce better reversed transitions. + pub reversing_shortening_factor: f64, +} + +impl Transition { + fn update_for_possibly_reversed_transition( + &mut self, + replaced_transition: &Transition, + delay: f64, + now: f64, + ) { + // If we reach here, we need to calculate a reversed transition according to + // https://drafts.csswg.org/css-transitions/#starting + // + // "...if the reversing-adjusted start value of the running transition + // is the same as the value of the property in the after-change style (see + // the section on reversing of transitions for why these case exists), + // implementations must cancel the running transition and start + // a new transition..." + if replaced_transition.reversing_adjusted_start_value != self.property_animation.to { + return; + } + + // "* reversing-adjusted start value is the end value of the running transition" + let replaced_animation = &replaced_transition.property_animation; + self.reversing_adjusted_start_value = replaced_animation.to.clone(); + + // "* reversing shortening factor is the absolute value, clamped to the + // range [0, 1], of the sum of: + // 1. the output of the timing function of the old transition at the + // time of the style change event, times the reversing shortening + // factor of the old transition + // 2. 1 minus the reversing shortening factor of the old transition." + let transition_progress = ((now - replaced_transition.start_time) / + (replaced_transition.property_animation.duration)) + .min(1.0) + .max(0.0); + let timing_function_output = replaced_animation.timing_function_output(transition_progress); + let old_reversing_shortening_factor = replaced_transition.reversing_shortening_factor; + self.reversing_shortening_factor = ((timing_function_output * + old_reversing_shortening_factor) + + (1.0 - old_reversing_shortening_factor)) + .abs() + .min(1.0) + .max(0.0); + + // "* start time is the time of the style change event plus: + // 1. if the matching transition delay is nonnegative, the matching + // transition delay, or. + // 2. if the matching transition delay is negative, the product of the new + // transition’s reversing shortening factor and the matching transition delay," + self.start_time = if delay >= 0. { + now + delay + } else { + now + (self.reversing_shortening_factor * delay) + }; + + // "* end time is the start time plus the product of the matching transition + // duration and the new transition’s reversing shortening factor," + self.property_animation.duration *= self.reversing_shortening_factor; + + // "* start value is the current value of the property in the running transition, + // * end value is the value of the property in the after-change style," + let procedure = Procedure::Interpolate { + progress: timing_function_output, + }; + match replaced_animation + .from + .animate(&replaced_animation.to, procedure) + { + Ok(new_start) => self.property_animation.from = new_start, + Err(..) => {}, + } + } + + /// Whether or not this animation has ended at the provided time. This does + /// not take into account canceling i.e. when an animation or transition is + /// canceled due to changes in the style. + pub fn has_ended(&self, time: f64) -> bool { + time >= self.start_time + (self.property_animation.duration) + } + + /// Update the given animation at a given point of progress. + pub fn calculate_value(&self, time: f64) -> Option<AnimationValue> { + let progress = (time - self.start_time) / (self.property_animation.duration); + if progress < 0.0 { + return None; + } + + self.property_animation + .calculate_value(progress.min(1.0)) + .ok() + } +} + +/// Holds the animation state for a particular element. +#[derive(Debug, Default, MallocSizeOf)] +pub struct ElementAnimationSet { + /// The animations for this element. + pub animations: Vec<Animation>, + + /// The transitions for this element. + pub transitions: Vec<Transition>, + + /// Whether or not this ElementAnimationSet has had animations or transitions + /// which have been added, removed, or had their state changed. + pub dirty: bool, +} + +impl ElementAnimationSet { + /// Cancel all animations in this `ElementAnimationSet`. This is typically called + /// when the element has been removed from the DOM. + pub fn cancel_all_animations(&mut self) { + self.dirty = !self.animations.is_empty(); + for animation in self.animations.iter_mut() { + animation.state = AnimationState::Canceled; + } + self.cancel_active_transitions(); + } + + fn cancel_active_transitions(&mut self) { + for transition in self.transitions.iter_mut() { + if transition.state != AnimationState::Finished { + self.dirty = true; + transition.state = AnimationState::Canceled; + } + } + } + + /// Apply all active animations. + pub fn apply_active_animations( + &self, + context: &SharedStyleContext, + style: &mut Arc<ComputedValues>, + ) { + let now = context.current_time_for_animations; + let mutable_style = Arc::make_mut(style); + if let Some(map) = self.get_value_map_for_active_animations(now) { + for value in map.values() { + value.set_in_style_for_servo(mutable_style); + } + } + + if let Some(map) = self.get_value_map_for_active_transitions(now) { + for value in map.values() { + value.set_in_style_for_servo(mutable_style); + } + } + } + + /// Clear all canceled animations and transitions from this `ElementAnimationSet`. + pub fn clear_canceled_animations(&mut self) { + self.animations + .retain(|animation| animation.state != AnimationState::Canceled); + self.transitions + .retain(|animation| animation.state != AnimationState::Canceled); + } + + /// Whether this `ElementAnimationSet` is empty, which means it doesn't + /// hold any animations in any state. + pub fn is_empty(&self) -> bool { + self.animations.is_empty() && self.transitions.is_empty() + } + + /// Whether or not this state needs animation ticks for its transitions + /// or animations. + pub fn needs_animation_ticks(&self) -> bool { + self.animations + .iter() + .any(|animation| animation.state.needs_to_be_ticked()) || + self.transitions + .iter() + .any(|transition| transition.state.needs_to_be_ticked()) + } + + /// The number of running animations and transitions for this `ElementAnimationSet`. + pub fn running_animation_and_transition_count(&self) -> usize { + self.animations + .iter() + .filter(|animation| animation.state.needs_to_be_ticked()) + .count() + + self.transitions + .iter() + .filter(|transition| transition.state.needs_to_be_ticked()) + .count() + } + + /// If this `ElementAnimationSet` has any any active animations. + pub fn has_active_animation(&self) -> bool { + self.animations + .iter() + .any(|animation| animation.state != AnimationState::Canceled) + } + + /// If this `ElementAnimationSet` has any any active transitions. + pub fn has_active_transition(&self) -> bool { + self.transitions + .iter() + .any(|transition| transition.state != AnimationState::Canceled) + } + + /// Update our animations given a new style, canceling or starting new animations + /// when appropriate. + pub fn update_animations_for_new_style<E>( + &mut self, + element: E, + context: &SharedStyleContext, + new_style: &Arc<ComputedValues>, + resolver: &mut StyleResolverForElement<E>, + ) where + E: TElement, + { + for animation in self.animations.iter_mut() { + if animation.is_cancelled_in_new_style(new_style) { + animation.state = AnimationState::Canceled; + } + } + + maybe_start_animations(element, &context, &new_style, self, resolver); + } + + /// Update our transitions given a new style, canceling or starting new animations + /// when appropriate. + pub fn update_transitions_for_new_style( + &mut self, + might_need_transitions_update: bool, + context: &SharedStyleContext, + old_style: Option<&Arc<ComputedValues>>, + after_change_style: &Arc<ComputedValues>, + ) { + // If this is the first style, we don't trigger any transitions and we assume + // there were no previously triggered transitions. + let mut before_change_style = match old_style { + Some(old_style) => Arc::clone(old_style), + None => return, + }; + + // If the style of this element is display:none, then cancel all active transitions. + if after_change_style.get_box().clone_display().is_none() { + self.cancel_active_transitions(); + return; + } + + if !might_need_transitions_update { + return; + } + + // We convert old values into `before-change-style` here. + if self.has_active_transition() || self.has_active_animation() { + self.apply_active_animations(context, &mut before_change_style); + } + + let transitioning_properties = start_transitions_if_applicable( + context, + &before_change_style, + after_change_style, + self, + ); + + // Cancel any non-finished transitions that have properties which no longer transition. + for transition in self.transitions.iter_mut() { + if transition.state == AnimationState::Finished { + continue; + } + if transitioning_properties.contains(transition.property_animation.property_id()) { + continue; + } + transition.state = AnimationState::Canceled; + self.dirty = true; + } + } + + fn start_transition_if_applicable( + &mut self, + context: &SharedStyleContext, + property_declaration_id: &PropertyDeclarationId, + index: usize, + old_style: &ComputedValues, + new_style: &Arc<ComputedValues>, + ) { + let style = new_style.get_ui(); + let timing_function = style.transition_timing_function_mod(index); + let duration = style.transition_duration_mod(index); + let delay = style.transition_delay_mod(index).seconds() as f64; + let now = context.current_time_for_animations; + + // Only start a new transition if the style actually changes between + // the old style and the new style. + let property_animation = match PropertyAnimation::from_property_declaration( + property_declaration_id, + timing_function, + duration, + old_style, + new_style, + ) { + Some(property_animation) => property_animation, + None => return, + }; + + // Per [1], don't trigger a new transition if the end state for that + // transition is the same as that of a transition that's running or + // completed. We don't take into account any canceled animations. + // [1]: https://drafts.csswg.org/css-transitions/#starting + if self + .transitions + .iter() + .filter(|transition| transition.state != AnimationState::Canceled) + .any(|transition| transition.property_animation.to == property_animation.to) + { + return; + } + + // We are going to start a new transition, but we might have to update + // it if we are replacing a reversed transition. + let reversing_adjusted_start_value = property_animation.from.clone(); + let mut new_transition = Transition { + start_time: now + delay, + delay, + property_animation, + state: AnimationState::Pending, + is_new: true, + reversing_adjusted_start_value, + reversing_shortening_factor: 1.0, + }; + + if let Some(old_transition) = self + .transitions + .iter_mut() + .filter(|transition| transition.state == AnimationState::Running) + .find(|transition| { + transition.property_animation.property_id() == *property_declaration_id + }) + { + // We always cancel any running transitions for the same property. + old_transition.state = AnimationState::Canceled; + new_transition.update_for_possibly_reversed_transition(old_transition, delay, now); + } + + self.transitions.push(new_transition); + self.dirty = true; + } + + /// Generate a `AnimationValueMap` for this `ElementAnimationSet`'s + /// active transitions at the given time value. + pub fn get_value_map_for_active_transitions(&self, now: f64) -> Option<AnimationValueMap> { + if !self.has_active_transition() { + return None; + } + + let mut map = + AnimationValueMap::with_capacity_and_hasher(self.transitions.len(), Default::default()); + for transition in &self.transitions { + if transition.state == AnimationState::Canceled { + continue; + } + let value = match transition.calculate_value(now) { + Some(value) => value, + None => continue, + }; + map.insert(value.id().to_owned(), value); + } + + Some(map) + } + + /// Generate a `AnimationValueMap` for this `ElementAnimationSet`'s + /// active animations at the given time value. + pub fn get_value_map_for_active_animations(&self, now: f64) -> Option<AnimationValueMap> { + if !self.has_active_animation() { + return None; + } + + let mut map = Default::default(); + for animation in &self.animations { + animation.get_property_declaration_at_time(now, &mut map); + } + + Some(map) + } +} + +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +/// A key that is used to identify nodes in the `DocumentAnimationSet`. +pub struct AnimationSetKey { + /// The node for this `AnimationSetKey`. + pub node: OpaqueNode, + /// The pseudo element for this `AnimationSetKey`. If `None` this key will + /// refer to the main content for its node. + pub pseudo_element: Option<PseudoElement>, +} + +impl AnimationSetKey { + /// Create a new key given a node and optional pseudo element. + pub fn new(node: OpaqueNode, pseudo_element: Option<PseudoElement>) -> Self { + AnimationSetKey { + node, + pseudo_element, + } + } + + /// Create a new key for the main content of this node. + pub fn new_for_non_pseudo(node: OpaqueNode) -> Self { + AnimationSetKey { + node, + pseudo_element: None, + } + } + + /// Create a new key for given node and pseudo element. + pub fn new_for_pseudo(node: OpaqueNode, pseudo_element: PseudoElement) -> Self { + AnimationSetKey { + node, + pseudo_element: Some(pseudo_element), + } + } +} + +#[derive(Clone, Debug, Default, MallocSizeOf)] +/// A set of animations for a document. +pub struct DocumentAnimationSet { + /// The `ElementAnimationSet`s that this set contains. + #[ignore_malloc_size_of = "Arc is hard"] + pub sets: Arc<RwLock<FxHashMap<AnimationSetKey, ElementAnimationSet>>>, +} + +impl DocumentAnimationSet { + /// Return whether or not the provided node has active CSS animations. + pub fn has_active_animations(&self, key: &AnimationSetKey) -> bool { + self.sets + .read() + .get(key) + .map_or(false, |set| set.has_active_animation()) + } + + /// Return whether or not the provided node has active CSS transitions. + pub fn has_active_transitions(&self, key: &AnimationSetKey) -> bool { + self.sets + .read() + .get(key) + .map_or(false, |set| set.has_active_transition()) + } + + /// Return a locked PropertyDeclarationBlock with animation values for the given + /// key and time. + pub fn get_animation_declarations( + &self, + key: &AnimationSetKey, + time: f64, + shared_lock: &SharedRwLock, + ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> { + self.sets + .read() + .get(key) + .and_then(|set| set.get_value_map_for_active_animations(time)) + .map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }) + } + + /// Return a locked PropertyDeclarationBlock with transition values for the given + /// key and time. + pub fn get_transition_declarations( + &self, + key: &AnimationSetKey, + time: f64, + shared_lock: &SharedRwLock, + ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> { + self.sets + .read() + .get(key) + .and_then(|set| set.get_value_map_for_active_transitions(time)) + .map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }) + } + + /// Get all the animation declarations for the given key, returning an empty + /// `AnimationDeclarations` if there are no animations. + pub fn get_all_declarations( + &self, + key: &AnimationSetKey, + time: f64, + shared_lock: &SharedRwLock, + ) -> AnimationDeclarations { + let sets = self.sets.read(); + let set = match sets.get(key) { + Some(set) => set, + None => return Default::default(), + }; + + let animations = set.get_value_map_for_active_animations(time).map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }); + let transitions = set.get_value_map_for_active_transitions(time).map(|map| { + let block = PropertyDeclarationBlock::from_animation_value_map(&map); + Arc::new(shared_lock.wrap(block)) + }); + AnimationDeclarations { + animations, + transitions, + } + } + + /// Cancel all animations for set at the given key. + pub fn cancel_all_animations_for_key(&self, key: &AnimationSetKey) { + if let Some(set) = self.sets.write().get_mut(key) { + set.cancel_all_animations(); + } + } +} + +/// Kick off any new transitions for this node and return all of the properties that are +/// transitioning. This is at the end of calculating style for a single node. +pub fn start_transitions_if_applicable( + context: &SharedStyleContext, + old_style: &ComputedValues, + new_style: &Arc<ComputedValues>, + animation_state: &mut ElementAnimationSet, +) -> PropertyDeclarationIdSet { + let mut properties_that_transition = PropertyDeclarationIdSet::default(); + for transition in new_style.transition_properties() { + let physical_property = PropertyDeclarationId::Longhand( + transition.longhand_id.to_physical(new_style.writing_mode), + ); + if properties_that_transition.contains(physical_property) { + continue; + } + + properties_that_transition.insert(physical_property); + animation_state.start_transition_if_applicable( + context, + &physical_property, + transition.index, + old_style, + new_style, + ); + } + + properties_that_transition +} + +/// Triggers animations for a given node looking at the animation property +/// values. +pub fn maybe_start_animations<E>( + element: E, + context: &SharedStyleContext, + new_style: &Arc<ComputedValues>, + animation_state: &mut ElementAnimationSet, + resolver: &mut StyleResolverForElement<E>, +) where + E: TElement, +{ + let style = new_style.get_ui(); + for (i, name) in style.animation_name_iter().enumerate() { + let name = match name.as_atom() { + Some(atom) => atom, + None => continue, + }; + + debug!("maybe_start_animations: name={}", name); + let duration = style.animation_duration_mod(i).seconds() as f64; + if duration == 0. { + continue; + } + + let keyframe_animation = match context.stylist.get_animation(name, element) { + Some(animation) => animation, + None => continue, + }; + + debug!("maybe_start_animations: animation {} found", name); + + // If this animation doesn't have any keyframe, we can just continue + // without submitting it to the compositor, since both the first and + // the second keyframes would be synthetised from the computed + // values. + if keyframe_animation.steps.is_empty() { + continue; + } + + // NB: This delay may be negative, meaning that the animation may be created + // in a state where we have advanced one or more iterations or even that the + // animation begins in a finished state. + let delay = style.animation_delay_mod(i).seconds(); + + let iteration_count = style.animation_iteration_count_mod(i); + let iteration_state = if iteration_count.0.is_infinite() { + KeyframesIterationState::Infinite(0.0) + } else { + KeyframesIterationState::Finite(0.0, iteration_count.0 as f64) + }; + + let animation_direction = style.animation_direction_mod(i); + + let initial_direction = match animation_direction { + AnimationDirection::Normal | AnimationDirection::Alternate => { + AnimationDirection::Normal + }, + AnimationDirection::Reverse | AnimationDirection::AlternateReverse => { + AnimationDirection::Reverse + }, + }; + + let now = context.current_time_for_animations; + let started_at = now + delay as f64; + let mut starting_progress = (now - started_at) / duration; + let state = match style.animation_play_state_mod(i) { + AnimationPlayState::Paused => AnimationState::Paused(starting_progress), + AnimationPlayState::Running => AnimationState::Pending, + }; + + let computed_steps = ComputedKeyframe::generate_for_keyframes( + element, + &keyframe_animation, + context, + new_style, + style.animation_timing_function_mod(i), + resolver, + ); + + let mut new_animation = Animation { + name: name.clone(), + properties_changed: keyframe_animation.properties_changed.clone(), + computed_steps, + started_at, + duration, + fill_mode: style.animation_fill_mode_mod(i), + delay: delay as f64, + iteration_state, + state, + direction: animation_direction, + current_direction: initial_direction, + cascade_style: new_style.clone(), + is_new: true, + }; + + // If we started with a negative delay, make sure we iterate the animation if + // the delay moves us past the first iteration. + while starting_progress > 1. && !new_animation.on_last_iteration() { + new_animation.iterate(); + starting_progress -= 1.; + } + + animation_state.dirty = true; + + // If the animation was already present in the list for the node, just update its state. + for existing_animation in animation_state.animations.iter_mut() { + if existing_animation.state == AnimationState::Canceled { + continue; + } + + if new_animation.name == existing_animation.name { + existing_animation + .update_from_other(&new_animation, context.current_time_for_animations); + return; + } + } + + animation_state.animations.push(new_animation); + } +} diff --git a/servo/components/style/applicable_declarations.rs b/servo/components/style/applicable_declarations.rs new file mode 100644 index 0000000000..96049b76e3 --- /dev/null +++ b/servo/components/style/applicable_declarations.rs @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Applicable declarations management. + +use crate::properties::PropertyDeclarationBlock; +use crate::rule_tree::{CascadeLevel, StyleSource}; +use crate::shared_lock::Locked; +use crate::stylesheets::layer_rule::LayerOrder; +use servo_arc::Arc; +use smallvec::SmallVec; + +/// List of applicable declarations. This is a transient structure that shuttles +/// declarations between selector matching and inserting into the rule tree, and +/// therefore we want to avoid heap-allocation where possible. +/// +/// In measurements on wikipedia, we pretty much never have more than 8 applicable +/// declarations, so we could consider making this 8 entries instead of 16. +/// However, it may depend a lot on workload, and stack space is cheap. +pub type ApplicableDeclarationList = SmallVec<[ApplicableDeclarationBlock; 16]>; + +/// Blink uses 18 bits to store source order, and does not check overflow [1]. +/// That's a limit that could be reached in realistic webpages, so we use +/// 24 bits and enforce defined behavior in the overflow case. +/// +/// Note that right now this restriction could be lifted if wanted (because we +/// no longer stash the cascade level in the remaining bits), but we keep it in +/// place in case we come up with a use-case for them, lacking reports of the +/// current limit being too small. +/// +/// [1] https://cs.chromium.org/chromium/src/third_party/WebKit/Source/core/css/ +/// RuleSet.h?l=128&rcl=90140ab80b84d0f889abc253410f44ed54ae04f3 +const SOURCE_ORDER_BITS: usize = 24; +const SOURCE_ORDER_MAX: u32 = (1 << SOURCE_ORDER_BITS) - 1; +const SOURCE_ORDER_MASK: u32 = SOURCE_ORDER_MAX; + +/// The cascade-level+layer order of this declaration. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct CascadePriority { + cascade_level: CascadeLevel, + layer_order: LayerOrder, +} + +const_assert_eq!( + std::mem::size_of::<CascadePriority>(), + std::mem::size_of::<u32>() +); + +impl PartialOrd for CascadePriority { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + Some(self.cmp(other)) + } +} + +impl Ord for CascadePriority { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.cascade_level.cmp(&other.cascade_level).then_with(|| { + let ordering = self.layer_order.cmp(&other.layer_order); + if ordering == std::cmp::Ordering::Equal { + return ordering; + } + // https://drafts.csswg.org/css-cascade-5/#cascade-layering + // + // Cascade layers (like declarations) are ordered by order + // of appearance. When comparing declarations that belong to + // different layers, then for normal rules the declaration + // whose cascade layer is last wins, and for important rules + // the declaration whose cascade layer is first wins. + // + // But the style attribute layer for some reason is special. + if self.cascade_level.is_important() && + !self.layer_order.is_style_attribute_layer() && + !other.layer_order.is_style_attribute_layer() + { + ordering.reverse() + } else { + ordering + } + }) + } +} + +impl CascadePriority { + /// Construct a new CascadePriority for a given (level, order) pair. + pub fn new(cascade_level: CascadeLevel, layer_order: LayerOrder) -> Self { + Self { + cascade_level, + layer_order, + } + } + + /// Returns the layer order. + #[inline] + pub fn layer_order(&self) -> LayerOrder { + self.layer_order + } + + /// Returns the cascade level. + #[inline] + pub fn cascade_level(&self) -> CascadeLevel { + self.cascade_level + } + + /// Whether this declaration should be allowed if `revert` or `revert-layer` + /// have been specified on a given origin. + /// + /// `self` is the priority at which the `revert` or `revert-layer` keyword + /// have been specified. + pub fn allows_when_reverted(&self, other: &Self, origin_revert: bool) -> bool { + if origin_revert { + other.cascade_level.origin() < self.cascade_level.origin() + } else { + other.unimportant() < self.unimportant() + } + } + + /// Convert this priority from "important" to "non-important", if needed. + pub fn unimportant(&self) -> Self { + Self::new(self.cascade_level().unimportant(), self.layer_order()) + } + + /// Convert this priority from "non-important" to "important", if needed. + pub fn important(&self) -> Self { + Self::new(self.cascade_level().important(), self.layer_order()) + } + + /// The same tree, in author origin, at the root layer. + pub fn same_tree_author_normal_at_root_layer() -> Self { + Self::new(CascadeLevel::same_tree_author_normal(), LayerOrder::root()) + } +} + +/// A property declaration together with its precedence among rules of equal +/// specificity so that we can sort them. +/// +/// This represents the declarations in a given declaration block for a given +/// importance. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct ApplicableDeclarationBlock { + /// The style source, either a style rule, or a property declaration block. + #[ignore_malloc_size_of = "Arc"] + pub source: StyleSource, + /// The bits containing the source order, cascade level, and shadow cascade + /// order. + source_order: u32, + /// The specificity of the selector. + pub specificity: u32, + /// The cascade priority of the rule. + pub cascade_priority: CascadePriority, +} + +impl ApplicableDeclarationBlock { + /// Constructs an applicable declaration block from a given property + /// declaration block and importance. + #[inline] + pub fn from_declarations( + declarations: Arc<Locked<PropertyDeclarationBlock>>, + level: CascadeLevel, + layer_order: LayerOrder, + ) -> Self { + ApplicableDeclarationBlock { + source: StyleSource::from_declarations(declarations), + source_order: 0, + specificity: 0, + cascade_priority: CascadePriority::new(level, layer_order), + } + } + + /// Constructs an applicable declaration block from the given components. + #[inline] + pub fn new( + source: StyleSource, + source_order: u32, + level: CascadeLevel, + specificity: u32, + layer_order: LayerOrder, + ) -> Self { + ApplicableDeclarationBlock { + source, + source_order: source_order & SOURCE_ORDER_MASK, + specificity, + cascade_priority: CascadePriority::new(level, layer_order), + } + } + + /// Returns the source order of the block. + #[inline] + pub fn source_order(&self) -> u32 { + self.source_order + } + + /// Returns the cascade level of the block. + #[inline] + pub fn level(&self) -> CascadeLevel { + self.cascade_priority.cascade_level() + } + + /// Returns the cascade level of the block. + #[inline] + pub fn layer_order(&self) -> LayerOrder { + self.cascade_priority.layer_order() + } + + /// Convenience method to consume self and return the right thing for the + /// rule tree to iterate over. + #[inline] + pub fn for_rule_tree(self) -> (StyleSource, CascadePriority) { + (self.source, self.cascade_priority) + } +} + +// Size of this struct determines sorting and selector-matching performance. +size_of_test!(ApplicableDeclarationBlock, 24); diff --git a/servo/components/style/attr.rs b/servo/components/style/attr.rs new file mode 100644 index 0000000000..05833fa08d --- /dev/null +++ b/servo/components/style/attr.rs @@ -0,0 +1,599 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsed representations of [DOM attributes][attr]. +//! +//! [attr]: https://dom.spec.whatwg.org/#interface-attr + +use crate::properties::PropertyDeclarationBlock; +use crate::shared_lock::Locked; +use crate::str::str_join; +use crate::str::{read_exponent, read_fraction, HTML_SPACE_CHARACTERS}; +use crate::str::{read_numbers, split_commas, split_html_space_chars}; +use crate::values::specified::Length; +use crate::values::AtomString; +use crate::{Atom, LocalName, Namespace, Prefix}; +use app_units::Au; +use cssparser::{self, Color, RGBA}; +use euclid::num::Zero; +use num_traits::ToPrimitive; +use selectors::attr::AttrSelectorOperation; +use servo_arc::Arc; +use servo_url::ServoUrl; +use std::str::FromStr; + +// Duplicated from script::dom::values. +const UNSIGNED_LONG_MAX: u32 = 2147483647; + +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub enum LengthOrPercentageOrAuto { + Auto, + Percentage(f32), + Length(Au), +} + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub enum AttrValue { + String(String), + TokenList(String, Vec<Atom>), + UInt(String, u32), + Int(String, i32), + Double(String, f64), + Atom(Atom), + Length(String, Option<Length>), + Color(String, Option<RGBA>), + Dimension(String, LengthOrPercentageOrAuto), + + /// Stores a URL, computed from the input string and a document's base URL. + /// + /// The URL is resolved at setting-time, so this kind of attribute value is + /// not actually suitable for most URL-reflecting IDL attributes. + ResolvedUrl(String, Option<ServoUrl>), + + /// Note that this variant is only used transitively as a fast path to set + /// the property declaration block relevant to the style of an element when + /// set from the inline declaration of that element (that is, + /// `element.style`). + /// + /// This can, as of this writing, only correspond to the value of the + /// `style` element, and is set from its relevant CSSInlineStyleDeclaration, + /// and then converted to a string in Element::attribute_mutated. + /// + /// Note that we don't necessarily need to do that (we could just clone the + /// declaration block), but that avoids keeping a refcounted + /// declarationblock for longer than needed. + Declaration( + String, + #[ignore_malloc_size_of = "Arc"] Arc<Locked<PropertyDeclarationBlock>>, + ), +} + +/// Shared implementation to parse an integer according to +/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-integers> or +/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-negative-integers> +fn do_parse_integer<T: Iterator<Item = char>>(input: T) -> Result<i64, ()> { + let mut input = input + .skip_while(|c| HTML_SPACE_CHARACTERS.iter().any(|s| s == c)) + .peekable(); + + let sign = match input.peek() { + None => return Err(()), + Some(&'-') => { + input.next(); + -1 + }, + Some(&'+') => { + input.next(); + 1 + }, + Some(_) => 1, + }; + + let (value, _) = read_numbers(input); + + value.and_then(|value| value.checked_mul(sign)).ok_or(()) +} + +/// Parse an integer according to +/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-integers>. +pub fn parse_integer<T: Iterator<Item = char>>(input: T) -> Result<i32, ()> { + do_parse_integer(input).and_then(|result| result.to_i32().ok_or(())) +} + +/// Parse an integer according to +/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-negative-integers> +pub fn parse_unsigned_integer<T: Iterator<Item = char>>(input: T) -> Result<u32, ()> { + do_parse_integer(input).and_then(|result| result.to_u32().ok_or(())) +} + +/// Parse a floating-point number according to +/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-floating-point-number-values> +pub fn parse_double(string: &str) -> Result<f64, ()> { + let trimmed = string.trim_matches(HTML_SPACE_CHARACTERS); + let mut input = trimmed.chars().peekable(); + + let (value, divisor, chars_skipped) = match input.peek() { + None => return Err(()), + Some(&'-') => { + input.next(); + (-1f64, -1f64, 1) + }, + Some(&'+') => { + input.next(); + (1f64, 1f64, 1) + }, + _ => (1f64, 1f64, 0), + }; + + let (value, value_digits) = if let Some(&'.') = input.peek() { + (0f64, 0) + } else { + let (read_val, read_digits) = read_numbers(input); + ( + value * read_val.and_then(|result| result.to_f64()).unwrap_or(1f64), + read_digits, + ) + }; + + let input = trimmed + .chars() + .skip(value_digits + chars_skipped) + .peekable(); + + let (mut value, fraction_digits) = read_fraction(input, divisor, value); + + let input = trimmed + .chars() + .skip(value_digits + chars_skipped + fraction_digits) + .peekable(); + + if let Some(exp) = read_exponent(input) { + value *= 10f64.powi(exp) + }; + + Ok(value) +} + +impl AttrValue { + pub fn from_serialized_tokenlist(tokens: String) -> AttrValue { + let atoms = + split_html_space_chars(&tokens) + .map(Atom::from) + .fold(vec![], |mut acc, atom| { + if !acc.contains(&atom) { + acc.push(atom) + } + acc + }); + AttrValue::TokenList(tokens, atoms) + } + + pub fn from_comma_separated_tokenlist(tokens: String) -> AttrValue { + let atoms = split_commas(&tokens) + .map(Atom::from) + .fold(vec![], |mut acc, atom| { + if !acc.contains(&atom) { + acc.push(atom) + } + acc + }); + AttrValue::TokenList(tokens, atoms) + } + + pub fn from_atomic_tokens(atoms: Vec<Atom>) -> AttrValue { + // TODO(ajeffrey): effecient conversion of Vec<Atom> to String + let tokens = String::from(str_join(&atoms, "\x20")); + AttrValue::TokenList(tokens, atoms) + } + + // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-unsigned-long + pub fn from_u32(string: String, default: u32) -> AttrValue { + let result = parse_unsigned_integer(string.chars()).unwrap_or(default); + let result = if result > UNSIGNED_LONG_MAX { + default + } else { + result + }; + AttrValue::UInt(string, result) + } + + pub fn from_i32(string: String, default: i32) -> AttrValue { + let result = parse_integer(string.chars()).unwrap_or(default); + AttrValue::Int(string, result) + } + + // https://html.spec.whatwg.org/multipage/#reflecting-content-attributes-in-idl-attributes:idl-double + pub fn from_double(string: String, default: f64) -> AttrValue { + let result = parse_double(&string).unwrap_or(default); + + if result.is_normal() { + AttrValue::Double(string, result) + } else { + AttrValue::Double(string, default) + } + } + + // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers + pub fn from_limited_i32(string: String, default: i32) -> AttrValue { + let result = parse_integer(string.chars()).unwrap_or(default); + + if result < 0 { + AttrValue::Int(string, default) + } else { + AttrValue::Int(string, result) + } + } + + // https://html.spec.whatwg.org/multipage/#limited-to-only-non-negative-numbers-greater-than-zero + pub fn from_limited_u32(string: String, default: u32) -> AttrValue { + let result = parse_unsigned_integer(string.chars()).unwrap_or(default); + let result = if result == 0 || result > UNSIGNED_LONG_MAX { + default + } else { + result + }; + AttrValue::UInt(string, result) + } + + pub fn from_atomic(string: String) -> AttrValue { + let value = Atom::from(string); + AttrValue::Atom(value) + } + + pub fn from_resolved_url(base: &ServoUrl, url: String) -> AttrValue { + let joined = base.join(&url).ok(); + AttrValue::ResolvedUrl(url, joined) + } + + pub fn from_legacy_color(string: String) -> AttrValue { + let parsed = parse_legacy_color(&string).ok(); + AttrValue::Color(string, parsed) + } + + pub fn from_dimension(string: String) -> AttrValue { + let parsed = parse_length(&string); + AttrValue::Dimension(string, parsed) + } + + pub fn from_nonzero_dimension(string: String) -> AttrValue { + let parsed = parse_nonzero_length(&string); + AttrValue::Dimension(string, parsed) + } + + /// Assumes the `AttrValue` is a `TokenList` and returns its tokens + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `TokenList` + pub fn as_tokens(&self) -> &[Atom] { + match *self { + AttrValue::TokenList(_, ref tokens) => tokens, + _ => panic!("Tokens not found"), + } + } + + /// Assumes the `AttrValue` is an `Atom` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not an `Atom` + pub fn as_atom(&self) -> &Atom { + match *self { + AttrValue::Atom(ref value) => value, + _ => panic!("Atom not found"), + } + } + + /// Assumes the `AttrValue` is a `Color` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `Color` + pub fn as_color(&self) -> Option<&RGBA> { + match *self { + AttrValue::Color(_, ref color) => color.as_ref(), + _ => panic!("Color not found"), + } + } + + /// Assumes the `AttrValue` is a `Dimension` and returns its value + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `Dimension` + pub fn as_dimension(&self) -> &LengthOrPercentageOrAuto { + match *self { + AttrValue::Dimension(_, ref l) => l, + _ => panic!("Dimension not found"), + } + } + + /// Assumes the `AttrValue` is a `ResolvedUrl` and returns its value. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `ResolvedUrl` + pub fn as_resolved_url(&self) -> Option<&ServoUrl> { + match *self { + AttrValue::ResolvedUrl(_, ref url) => url.as_ref(), + _ => panic!("Url not found"), + } + } + + /// Return the AttrValue as its integer representation, if any. + /// This corresponds to attribute values returned as `AttrValue::UInt(_)` + /// by `VirtualMethods::parse_plain_attribute()`. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `UInt` + pub fn as_uint(&self) -> u32 { + if let AttrValue::UInt(_, value) = *self { + value + } else { + panic!("Uint not found"); + } + } + + /// Return the AttrValue as a dimension computed from its integer + /// representation, assuming that integer representation specifies pixels. + /// + /// This corresponds to attribute values returned as `AttrValue::UInt(_)` + /// by `VirtualMethods::parse_plain_attribute()`. + /// + /// ## Panics + /// + /// Panics if the `AttrValue` is not a `UInt` + pub fn as_uint_px_dimension(&self) -> LengthOrPercentageOrAuto { + if let AttrValue::UInt(_, value) = *self { + LengthOrPercentageOrAuto::Length(Au::from_px(value as i32)) + } else { + panic!("Uint not found"); + } + } + + pub fn eval_selector(&self, selector: &AttrSelectorOperation<&AtomString>) -> bool { + // FIXME(SimonSapin) this can be more efficient by matching on `(self, selector)` variants + // and doing Atom comparisons instead of string comparisons where possible, + // with SelectorImpl::AttrValue changed to Atom. + selector.eval_str(self) + } +} + +impl ::std::ops::Deref for AttrValue { + type Target = str; + + fn deref(&self) -> &str { + match *self { + AttrValue::String(ref value) | + AttrValue::TokenList(ref value, _) | + AttrValue::UInt(ref value, _) | + AttrValue::Double(ref value, _) | + AttrValue::Length(ref value, _) | + AttrValue::Color(ref value, _) | + AttrValue::Int(ref value, _) | + AttrValue::ResolvedUrl(ref value, _) | + AttrValue::Declaration(ref value, _) | + AttrValue::Dimension(ref value, _) => &value, + AttrValue::Atom(ref value) => &value, + } + } +} + +impl PartialEq<Atom> for AttrValue { + fn eq(&self, other: &Atom) -> bool { + match *self { + AttrValue::Atom(ref value) => value == other, + _ => other == &**self, + } + } +} + +/// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-non-zero-dimension-values> +pub fn parse_nonzero_length(value: &str) -> LengthOrPercentageOrAuto { + match parse_length(value) { + LengthOrPercentageOrAuto::Length(x) if x == Au::zero() => LengthOrPercentageOrAuto::Auto, + LengthOrPercentageOrAuto::Percentage(x) if x == 0. => LengthOrPercentageOrAuto::Auto, + x => x, + } +} + +/// Parses a [legacy color][color]. If unparseable, `Err` is returned. +/// +/// [color]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-colour-value +pub fn parse_legacy_color(mut input: &str) -> Result<RGBA, ()> { + // Steps 1 and 2. + if input.is_empty() { + return Err(()); + } + + // Step 3. + input = input.trim_matches(HTML_SPACE_CHARACTERS); + + // Step 4. + if input.eq_ignore_ascii_case("transparent") { + return Err(()); + } + + // Step 5. + if let Ok(Color::RGBA(rgba)) = cssparser::parse_color_keyword(input) { + return Ok(rgba); + } + + // Step 6. + if input.len() == 4 { + if let (b'#', Ok(r), Ok(g), Ok(b)) = ( + input.as_bytes()[0], + hex(input.as_bytes()[1] as char), + hex(input.as_bytes()[2] as char), + hex(input.as_bytes()[3] as char), + ) { + return Ok(RGBA::new(r * 17, g * 17, b * 17, 255)); + } + } + + // Step 7. + let mut new_input = String::new(); + for ch in input.chars() { + if ch as u32 > 0xffff { + new_input.push_str("00") + } else { + new_input.push(ch) + } + } + let mut input = &*new_input; + + // Step 8. + for (char_count, (index, _)) in input.char_indices().enumerate() { + if char_count == 128 { + input = &input[..index]; + break; + } + } + + // Step 9. + if input.as_bytes()[0] == b'#' { + input = &input[1..] + } + + // Step 10. + let mut new_input = Vec::new(); + for ch in input.chars() { + if hex(ch).is_ok() { + new_input.push(ch as u8) + } else { + new_input.push(b'0') + } + } + let mut input = new_input; + + // Step 11. + while input.is_empty() || (input.len() % 3) != 0 { + input.push(b'0') + } + + // Step 12. + let mut length = input.len() / 3; + let (mut red, mut green, mut blue) = ( + &input[..length], + &input[length..length * 2], + &input[length * 2..], + ); + + // Step 13. + if length > 8 { + red = &red[length - 8..]; + green = &green[length - 8..]; + blue = &blue[length - 8..]; + length = 8 + } + + // Step 14. + while length > 2 && red[0] == b'0' && green[0] == b'0' && blue[0] == b'0' { + red = &red[1..]; + green = &green[1..]; + blue = &blue[1..]; + length -= 1 + } + + // Steps 15-20. + return Ok(RGBA::new( + hex_string(red).unwrap(), + hex_string(green).unwrap(), + hex_string(blue).unwrap(), + 255, + )); + + fn hex(ch: char) -> Result<u8, ()> { + match ch { + '0'..='9' => Ok((ch as u8) - b'0'), + 'a'..='f' => Ok((ch as u8) - b'a' + 10), + 'A'..='F' => Ok((ch as u8) - b'A' + 10), + _ => Err(()), + } + } + + fn hex_string(string: &[u8]) -> Result<u8, ()> { + match string.len() { + 0 => Err(()), + 1 => hex(string[0] as char), + _ => { + let upper = hex(string[0] as char)?; + let lower = hex(string[1] as char)?; + Ok((upper << 4) | lower) + }, + } + } +} + +/// Parses a [dimension value][dim]. If unparseable, `Auto` is returned. +/// +/// [dim]: https://html.spec.whatwg.org/multipage/#rules-for-parsing-dimension-values +// TODO: this function can be rewritten to return Result<LengthPercentage, _> +pub fn parse_length(mut value: &str) -> LengthOrPercentageOrAuto { + // Steps 1 & 2 are not relevant + + // Step 3 + value = value.trim_start_matches(HTML_SPACE_CHARACTERS); + + // Step 4 + match value.chars().nth(0) { + Some('0'..='9') => {}, + _ => return LengthOrPercentageOrAuto::Auto, + } + + // Steps 5 to 8 + // We trim the string length to the minimum of: + // 1. the end of the string + // 2. the first occurence of a '%' (U+0025 PERCENT SIGN) + // 3. the second occurrence of a '.' (U+002E FULL STOP) + // 4. the occurrence of a character that is neither a digit nor '%' nor '.' + // Note: Step 7.4 is directly subsumed by FromStr::from_str + let mut end_index = value.len(); + let (mut found_full_stop, mut found_percent) = (false, false); + for (i, ch) in value.chars().enumerate() { + match ch { + '0'..='9' => continue, + '%' => { + found_percent = true; + end_index = i; + break; + }, + '.' if !found_full_stop => { + found_full_stop = true; + continue; + }, + _ => { + end_index = i; + break; + }, + } + } + value = &value[..end_index]; + + if found_percent { + let result: Result<f32, _> = FromStr::from_str(value); + match result { + Ok(number) => return LengthOrPercentageOrAuto::Percentage((number as f32) / 100.0), + Err(_) => return LengthOrPercentageOrAuto::Auto, + } + } + + match FromStr::from_str(value) { + Ok(number) => LengthOrPercentageOrAuto::Length(Au::from_f64_px(number)), + Err(_) => LengthOrPercentageOrAuto::Auto, + } +} + +/// A struct that uniquely identifies an element's attribute. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct AttrIdentifier { + pub local_name: LocalName, + pub name: LocalName, + pub namespace: Namespace, + pub prefix: Option<Prefix>, +} diff --git a/servo/components/style/author_styles.rs b/servo/components/style/author_styles.rs new file mode 100644 index 0000000000..a0223dcecc --- /dev/null +++ b/servo/components/style/author_styles.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A set of author stylesheets and their computed representation, such as the +//! ones used for ShadowRoot. + +use crate::dom::TElement; +use crate::invalidation::media_queries::ToMediaListKey; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheet_set::AuthorStylesheetSet; +use crate::stylesheets::StylesheetInDocument; +use crate::stylist::CascadeData; +use crate::stylist::Stylist; +use servo_arc::Arc; + +/// A set of author stylesheets and their computed representation, such as the +/// ones used for ShadowRoot. +#[derive(MallocSizeOf)] +pub struct GenericAuthorStyles<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The sheet collection, which holds the sheet pointers, the invalidations, + /// and all that stuff. + pub stylesheets: AuthorStylesheetSet<S>, + /// The actual cascade data computed from the stylesheets. + #[ignore_malloc_size_of = "Measured as part of the stylist"] + pub data: Arc<CascadeData>, +} + +pub use self::GenericAuthorStyles as AuthorStyles; + +lazy_static! { + static ref EMPTY_CASCADE_DATA: Arc<CascadeData> = Arc::new_leaked(CascadeData::new()); +} + +impl<S> GenericAuthorStyles<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Create an empty AuthorStyles. + #[inline] + pub fn new() -> Self { + Self { + stylesheets: AuthorStylesheetSet::new(), + data: EMPTY_CASCADE_DATA.clone(), + } + } + + /// Flush the pending sheet changes, updating `data` as appropriate. + /// + /// TODO(emilio): Need a host element and a snapshot map to do invalidation + /// properly. + #[inline] + pub fn flush<E>(&mut self, stylist: &mut Stylist, guard: &SharedRwLockReadGuard) + where + E: TElement, + S: ToMediaListKey, + { + let flusher = self + .stylesheets + .flush::<E>(/* host = */ None, /* snapshot_map = */ None); + + let result = stylist.rebuild_author_data(&self.data, flusher.sheets, guard); + if let Ok(Some(new_data)) = result { + self.data = new_data; + } + } +} diff --git a/servo/components/style/bezier.rs b/servo/components/style/bezier.rs new file mode 100644 index 0000000000..dd520ac0ed --- /dev/null +++ b/servo/components/style/bezier.rs @@ -0,0 +1,176 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parametric Bézier curves. +//! +//! This is based on `WebCore/platform/graphics/UnitBezier.h` in WebKit. + +#![deny(missing_docs)] + +use crate::values::CSSFloat; + +const NEWTON_METHOD_ITERATIONS: u8 = 8; + +/// A unit cubic Bézier curve, used for timing functions in CSS transitions and animations. +pub struct Bezier { + ax: f64, + bx: f64, + cx: f64, + ay: f64, + by: f64, + cy: f64, +} + +impl Bezier { + /// Calculate the output of a unit cubic Bézier curve from the two middle control points. + /// + /// X coordinate is time, Y coordinate is function advancement. + /// The nominal range for both is 0 to 1. + /// + /// The start and end points are always (0, 0) and (1, 1) so that a transition or animation + /// starts at 0% and ends at 100%. + pub fn calculate_bezier_output( + progress: f64, + epsilon: f64, + x1: f32, + y1: f32, + x2: f32, + y2: f32, + ) -> f64 { + // Check for a linear curve. + if x1 == y1 && x2 == y2 { + return progress; + } + + // Ensure that we return 0 or 1 on both edges. + if progress == 0.0 { + return 0.0; + } + if progress == 1.0 { + return 1.0; + } + + // For negative values, try to extrapolate with tangent (p1 - p0) or, + // if p1 is coincident with p0, with (p2 - p0). + if progress < 0.0 { + if x1 > 0.0 { + return progress * y1 as f64 / x1 as f64; + } + if y1 == 0.0 && x2 > 0.0 { + return progress * y2 as f64 / x2 as f64; + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 0.0; + } + + // For values greater than 1, try to extrapolate with tangent (p2 - p3) or, + // if p2 is coincident with p3, with (p1 - p3). + if progress > 1.0 { + if x2 < 1.0 { + return 1.0 + (progress - 1.0) * (y2 as f64 - 1.0) / (x2 as f64 - 1.0); + } + if y2 == 1.0 && x1 < 1.0 { + return 1.0 + (progress - 1.0) * (y1 as f64 - 1.0) / (x1 as f64 - 1.0); + } + // If we can't calculate a sensible tangent, don't extrapolate at all. + return 1.0; + } + + Bezier::new(x1, y1, x2, y2).solve(progress, epsilon) + } + + #[inline] + fn new(x1: CSSFloat, y1: CSSFloat, x2: CSSFloat, y2: CSSFloat) -> Bezier { + let cx = 3. * x1 as f64; + let bx = 3. * (x2 as f64 - x1 as f64) - cx; + + let cy = 3. * y1 as f64; + let by = 3. * (y2 as f64 - y1 as f64) - cy; + + Bezier { + ax: 1.0 - cx - bx, + bx: bx, + cx: cx, + ay: 1.0 - cy - by, + by: by, + cy: cy, + } + } + + #[inline] + fn sample_curve_x(&self, t: f64) -> f64 { + // ax * t^3 + bx * t^2 + cx * t + ((self.ax * t + self.bx) * t + self.cx) * t + } + + #[inline] + fn sample_curve_y(&self, t: f64) -> f64 { + ((self.ay * t + self.by) * t + self.cy) * t + } + + #[inline] + fn sample_curve_derivative_x(&self, t: f64) -> f64 { + (3.0 * self.ax * t + 2.0 * self.bx) * t + self.cx + } + + #[inline] + fn solve_curve_x(&self, x: f64, epsilon: f64) -> f64 { + // Fast path: Use Newton's method. + let mut t = x; + for _ in 0..NEWTON_METHOD_ITERATIONS { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t; + } + let dx = self.sample_curve_derivative_x(t); + if dx.approx_eq(0.0, 1e-6) { + break; + } + t -= (x2 - x) / dx; + } + + // Slow path: Use bisection. + let (mut lo, mut hi, mut t) = (0.0, 1.0, x); + + if t < lo { + return lo; + } + if t > hi { + return hi; + } + + while lo < hi { + let x2 = self.sample_curve_x(t); + if x2.approx_eq(x, epsilon) { + return t; + } + if x > x2 { + lo = t + } else { + hi = t + } + t = (hi - lo) / 2.0 + lo + } + + t + } + + /// Solve the bezier curve for a given `x` and an `epsilon`, that should be + /// between zero and one. + #[inline] + fn solve(&self, x: f64, epsilon: f64) -> f64 { + self.sample_curve_y(self.solve_curve_x(x, epsilon)) + } +} + +trait ApproxEq { + fn approx_eq(self, value: Self, epsilon: Self) -> bool; +} + +impl ApproxEq for f64 { + #[inline] + fn approx_eq(self, value: f64, epsilon: f64) -> bool { + (self - value).abs() < epsilon + } +} diff --git a/servo/components/style/bloom.rs b/servo/components/style/bloom.rs new file mode 100644 index 0000000000..824acb7114 --- /dev/null +++ b/servo/components/style/bloom.rs @@ -0,0 +1,401 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The style bloom filter is used as an optimization when matching deep +//! descendant selectors. + +#![deny(missing_docs)] + +use crate::dom::{SendElement, TElement}; +use atomic_refcell::{AtomicRefCell, AtomicRefMut}; +use owning_ref::OwningHandle; +use selectors::bloom::BloomFilter; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::mem::ManuallyDrop; + +thread_local! { + /// Bloom filters are large allocations, so we store them in thread-local storage + /// such that they can be reused across style traversals. StyleBloom is responsible + /// for ensuring that the bloom filter is zeroed when it is dropped. + /// + /// We intentionally leak this from TLS because we don't have the guarantee + /// of TLS destructors to run in worker threads. + /// + /// We could change this once https://github.com/rayon-rs/rayon/issues/688 + /// is fixed, hopefully. + static BLOOM_KEY: ManuallyDrop<Arc<AtomicRefCell<BloomFilter>>> = + ManuallyDrop::new(Arc::new_leaked(Default::default())); +} + +/// A struct that allows us to fast-reject deep descendant selectors avoiding +/// selector-matching. +/// +/// This is implemented using a counting bloom filter, and it's a standard +/// optimization. See Gecko's `AncestorFilter`, and Blink's and WebKit's +/// `SelectorFilter`. +/// +/// The constraints for Servo's style system are a bit different compared to +/// traditional style systems given Servo does a parallel breadth-first +/// traversal instead of a sequential depth-first traversal. +/// +/// This implies that we need to track a bit more state than other browsers to +/// ensure we're doing the correct thing during the traversal, and being able to +/// apply this optimization effectively. +/// +/// Concretely, we have a bloom filter instance per worker thread, and we track +/// the current DOM depth in order to find a common ancestor when it doesn't +/// match the previous element we've styled. +/// +/// This is usually a pretty fast operation (we use to be one level deeper than +/// the previous one), but in the case of work-stealing, we may needed to push +/// and pop multiple elements. +/// +/// See the `insert_parents_recovering`, where most of the magic happens. +/// +/// Regarding thread-safety, this struct is safe because: +/// +/// * We clear this after a restyle. +/// * The DOM shape and attributes (and every other thing we access here) are +/// immutable during a restyle. +/// +pub struct StyleBloom<E: TElement> { + /// A handle to the bloom filter from the thread upon which this StyleBloom + /// was created. We use AtomicRefCell so that this is all |Send|, which allows + /// StyleBloom to live in ThreadLocalStyleContext, which is dropped from the + /// parent thread. + filter: OwningHandle<Arc<AtomicRefCell<BloomFilter>>, AtomicRefMut<'static, BloomFilter>>, + + /// The stack of elements that this bloom filter contains, along with the + /// number of hashes pushed for each element. + elements: SmallVec<[PushedElement<E>; 16]>, + + /// Stack of hashes that have been pushed onto this filter. + pushed_hashes: SmallVec<[u32; 64]>, +} + +/// The very rough benchmarks in the selectors crate show clear() +/// costing about 25 times more than remove_hash(). We use this to implement +/// clear() more efficiently when only a small number of hashes have been +/// pushed. +/// +/// One subtly to note is that remove_hash() will not touch the value +/// if the filter overflowed. However, overflow can only occur if we +/// get 255 collisions on the same hash value, and 25 < 255. +const MEMSET_CLEAR_THRESHOLD: usize = 25; + +struct PushedElement<E: TElement> { + /// The element that was pushed. + element: SendElement<E>, + + /// The number of hashes pushed for the element. + num_hashes: usize, +} + +impl<E: TElement> PushedElement<E> { + fn new(el: E, num_hashes: usize) -> Self { + PushedElement { + element: unsafe { SendElement::new(el) }, + num_hashes, + } + } +} + +/// Returns whether the attribute name is excluded from the bloom filter. +/// +/// We do this for attributes that are very common but not commonly used in +/// selectors. +#[inline] +pub fn is_attr_name_excluded_from_filter(atom: &crate::Atom) -> bool { + *atom == atom!("class") || *atom == atom!("id") || *atom == atom!("style") +} + +/// Gather all relevant hash for fast-reject filters from an element. +pub fn each_relevant_element_hash<E, F>(element: E, mut f: F) +where + E: TElement, + F: FnMut(u32), +{ + f(element.local_name().get_hash()); + f(element.namespace().get_hash()); + + if let Some(id) = element.id() { + f(id.get_hash()); + } + + element.each_class(|class| f(class.get_hash())); + + element.each_attr_name(|name| { + if !is_attr_name_excluded_from_filter(name) { + f(name.get_hash()) + } + }); +} + +impl<E: TElement> Drop for StyleBloom<E> { + fn drop(&mut self) { + // Leave the reusable bloom filter in a zeroed state. + self.clear(); + } +} + +impl<E: TElement> StyleBloom<E> { + /// Create an empty `StyleBloom`. Because StyleBloom acquires the thread- + /// local filter buffer, creating multiple live StyleBloom instances at + /// the same time on the same thread will panic. + + // Forced out of line to limit stack frame sizes after extra inlining from + // https://github.com/rust-lang/rust/pull/43931 + // + // See https://github.com/servo/servo/pull/18420#issuecomment-328769322 + #[inline(never)] + pub fn new() -> Self { + let bloom_arc = BLOOM_KEY.with(|b| Arc::clone(&*b)); + let filter = + OwningHandle::new_with_fn(bloom_arc, |x| unsafe { x.as_ref() }.unwrap().borrow_mut()); + debug_assert!( + filter.is_zeroed(), + "Forgot to zero the bloom filter last time" + ); + StyleBloom { + filter, + elements: Default::default(), + pushed_hashes: Default::default(), + } + } + + /// Return the bloom filter used properly by the `selectors` crate. + pub fn filter(&self) -> &BloomFilter { + &*self.filter + } + + /// Push an element to the bloom filter, knowing that it's a child of the + /// last element parent. + pub fn push(&mut self, element: E) { + if cfg!(debug_assertions) { + if self.elements.is_empty() { + assert!(element.traversal_parent().is_none()); + } + } + self.push_internal(element); + } + + /// Same as `push`, but without asserting, in order to use it from + /// `rebuild`. + fn push_internal(&mut self, element: E) { + let mut count = 0; + each_relevant_element_hash(element, |hash| { + count += 1; + self.filter.insert_hash(hash); + self.pushed_hashes.push(hash); + }); + self.elements.push(PushedElement::new(element, count)); + } + + /// Pop the last element in the bloom filter and return it. + #[inline] + fn pop(&mut self) -> Option<E> { + let PushedElement { + element, + num_hashes, + } = self.elements.pop()?; + let popped_element = *element; + + // Verify that the pushed hashes match the ones we'd get from the element. + let mut expected_hashes = vec![]; + if cfg!(debug_assertions) { + each_relevant_element_hash(popped_element, |hash| expected_hashes.push(hash)); + } + + for _ in 0..num_hashes { + let hash = self.pushed_hashes.pop().unwrap(); + debug_assert_eq!(expected_hashes.pop().unwrap(), hash); + self.filter.remove_hash(hash); + } + + Some(popped_element) + } + + /// Returns the DOM depth of elements that can be correctly + /// matched against the bloom filter (that is, the number of + /// elements in our list). + pub fn matching_depth(&self) -> usize { + self.elements.len() + } + + /// Clears the bloom filter. + pub fn clear(&mut self) { + self.elements.clear(); + + if self.pushed_hashes.len() > MEMSET_CLEAR_THRESHOLD { + self.filter.clear(); + self.pushed_hashes.clear(); + } else { + for hash in self.pushed_hashes.drain(..) { + self.filter.remove_hash(hash); + } + debug_assert!(self.filter.is_zeroed()); + } + } + + /// Rebuilds the bloom filter up to the parent of the given element. + pub fn rebuild(&mut self, mut element: E) { + self.clear(); + + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + while let Some(parent) = element.traversal_parent() { + parents_to_insert.push(parent); + element = parent; + } + + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + } + + /// In debug builds, asserts that all the parents of `element` are in the + /// bloom filter. + /// + /// Goes away in release builds. + pub fn assert_complete(&self, mut element: E) { + if cfg!(debug_assertions) { + let mut checked = 0; + while let Some(parent) = element.traversal_parent() { + assert_eq!( + parent, + *(self.elements[self.elements.len() - 1 - checked].element) + ); + element = parent; + checked += 1; + } + assert_eq!(checked, self.elements.len()); + } + } + + /// Get the element that represents the chain of things inserted + /// into the filter right now. That chain is the given element + /// (if any) and its ancestors. + #[inline] + pub fn current_parent(&self) -> Option<E> { + self.elements.last().map(|ref el| *el.element) + } + + /// Insert the parents of an element in the bloom filter, trying to recover + /// the filter if the last element inserted doesn't match. + /// + /// Gets the element depth in the dom, to make it efficient, or if not + /// provided always rebuilds the filter from scratch. + /// + /// Returns the new bloom filter depth, that the traversal code is + /// responsible to keep around if it wants to get an effective filter. + pub fn insert_parents_recovering(&mut self, element: E, element_depth: usize) { + // Easy case, we're in a different restyle, or we're empty. + if self.elements.is_empty() { + self.rebuild(element); + return; + } + + let traversal_parent = match element.traversal_parent() { + Some(parent) => parent, + None => { + // Yay, another easy case. + self.clear(); + return; + }, + }; + + if self.current_parent() == Some(traversal_parent) { + // Ta da, cache hit, we're all done. + return; + } + + if element_depth == 0 { + self.clear(); + return; + } + + // We should've early exited above. + debug_assert!( + element_depth != 0, + "We should have already cleared the bloom filter" + ); + debug_assert!(!self.elements.is_empty(), "How! We should've just rebuilt!"); + + // Now the fun begins: We have the depth of the dom and the depth of the + // last element inserted in the filter, let's try to find a common + // parent. + // + // The current depth, that is, the depth of the last element inserted in + // the bloom filter, is the number of elements _minus one_, that is: if + // there's one element, it must be the root -> depth zero. + let mut current_depth = self.elements.len() - 1; + + // If the filter represents an element too deep in the dom, we need to + // pop ancestors. + while current_depth > element_depth - 1 { + self.pop().expect("Emilio is bad at math"); + current_depth -= 1; + } + + // Now let's try to find a common parent in the bloom filter chain, + // starting with traversal_parent. + let mut common_parent = traversal_parent; + let mut common_parent_depth = element_depth - 1; + + // Let's collect the parents we are going to need to insert once we've + // found the common one. + let mut parents_to_insert = SmallVec::<[E; 16]>::new(); + + // If the bloom filter still doesn't have enough elements, the common + // parent is up in the dom. + while common_parent_depth > current_depth { + // TODO(emilio): Seems like we could insert parents here, then + // reverse the slice. + parents_to_insert.push(common_parent); + common_parent = common_parent.traversal_parent().expect("We were lied to"); + common_parent_depth -= 1; + } + + // Now the two depths are the same. + debug_assert_eq!(common_parent_depth, current_depth); + + // Happy case: The parents match, we only need to push the ancestors + // we've collected and we'll never enter in this loop. + // + // Not-so-happy case: Parent's don't match, so we need to keep going up + // until we find a common ancestor. + // + // Gecko currently models native anonymous content that conceptually + // hangs off the document (such as scrollbars) as a separate subtree + // from the document root. + // + // Thus it's possible with Gecko that we do not find any common + // ancestor. + while *(self.elements.last().unwrap().element) != common_parent { + parents_to_insert.push(common_parent); + self.pop().unwrap(); + common_parent = match common_parent.traversal_parent() { + Some(parent) => parent, + None => { + debug_assert!(self.elements.is_empty()); + if cfg!(feature = "gecko") { + break; + } else { + panic!("should have found a common ancestor"); + } + }, + } + } + + // Now the parents match, so insert the stack of elements we have been + // collecting so far. + for parent in parents_to_insert.drain(..).rev() { + self.push(parent); + } + + debug_assert_eq!(self.elements.len(), element_depth); + + // We're done! Easy. + } +} diff --git a/servo/components/style/build.rs b/servo/components/style/build.rs new file mode 100644 index 0000000000..2247e87618 --- /dev/null +++ b/servo/components/style/build.rs @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#[macro_use] +extern crate lazy_static; + +use std::env; +use std::path::Path; +use std::process::{exit, Command}; +use walkdir::WalkDir; + +#[cfg(feature = "gecko")] +mod build_gecko; + +#[cfg(not(feature = "gecko"))] +mod build_gecko { + pub fn generate() {} +} + +lazy_static! { + pub static ref PYTHON: String = env::var("PYTHON3").ok().unwrap_or_else(|| { + let candidates = if cfg!(windows) { + ["python3.exe"] + } else { + ["python3"] + }; + for &name in &candidates { + if Command::new(name) + .arg("--version") + .output() + .ok() + .map_or(false, |out| out.status.success()) + { + return name.to_owned(); + } + } + panic!( + "Can't find python (tried {})! Try fixing PATH or setting the PYTHON3 env var", + candidates.join(", ") + ) + }); +} + +fn generate_properties(engine: &str) { + for entry in WalkDir::new("properties") { + let entry = entry.unwrap(); + match entry.path().extension().and_then(|e| e.to_str()) { + Some("mako") | Some("rs") | Some("py") | Some("zip") => { + println!("cargo:rerun-if-changed={}", entry.path().display()); + }, + _ => {}, + } + } + + let script = Path::new(&env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("properties") + .join("build.py"); + + let status = Command::new(&*PYTHON) + .arg(&script) + .arg(engine) + .arg("style-crate") + .status() + .unwrap(); + if !status.success() { + exit(1) + } +} + +fn main() { + let gecko = cfg!(feature = "gecko"); + let servo = cfg!(feature = "servo"); + let l2013 = cfg!(feature = "servo-layout-2013"); + let l2020 = cfg!(feature = "servo-layout-2020"); + let engine = match (gecko, servo, l2013, l2020) { + (true, false, false, false) => "gecko", + (false, true, true, false) => "servo-2013", + (false, true, false, true) => "servo-2020", + _ => panic!( + "\n\n\ + The style crate requires enabling one of its 'servo' or 'gecko' feature flags \ + and, in the 'servo' case, one of 'servo-layout-2013' or 'servo-layout-2020'.\ + \n\n" + ), + }; + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:out_dir={}", env::var("OUT_DIR").unwrap()); + generate_properties(engine); + build_gecko::generate(); +} diff --git a/servo/components/style/build_gecko.rs b/servo/components/style/build_gecko.rs new file mode 100644 index 0000000000..a83c5dbc6d --- /dev/null +++ b/servo/components/style/build_gecko.rs @@ -0,0 +1,400 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use super::PYTHON; +use bindgen::{Builder, CodegenConfig}; +use regex::Regex; +use std::cmp; +use std::collections::HashSet; +use std::env; +use std::fs::{self, File}; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{exit, Command}; +use std::slice; +use std::sync::Mutex; +use std::time::SystemTime; +use toml; +use toml::value::Table; + +lazy_static! { + static ref OUTDIR_PATH: PathBuf = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("gecko"); +} + +const STRUCTS_FILE: &'static str = "structs.rs"; + +fn read_config(path: &PathBuf) -> Table { + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + update_last_modified(&path); + + let mut contents = String::new(); + File::open(path) + .expect("Failed to open config file") + .read_to_string(&mut contents) + .expect("Failed to read config file"); + match toml::from_str::<Table>(&contents) { + Ok(result) => result, + Err(e) => panic!("Failed to parse config file: {}", e), + } +} + +lazy_static! { + static ref CONFIG: Table = { + // Load Gecko's binding generator config from the source tree. + let path = mozbuild::TOPSRCDIR.join("layout/style/ServoBindings.toml"); + read_config(&path) + }; + static ref BINDGEN_FLAGS: Vec<String> = { + // Load build-specific config overrides. + let path = mozbuild::TOPOBJDIR.join("layout/style/extra-bindgen-flags"); + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + fs::read_to_string(path).expect("Failed to read extra-bindgen-flags file") + .split_whitespace() + .map(std::borrow::ToOwned::to_owned) + .collect() + }; + static ref INCLUDE_RE: Regex = Regex::new(r#"#include\s*"(.+?)""#).unwrap(); + static ref DISTDIR_PATH: PathBuf = mozbuild::TOPOBJDIR.join("dist"); + static ref SEARCH_PATHS: Vec<PathBuf> = vec![ + DISTDIR_PATH.join("include"), + DISTDIR_PATH.join("include/nspr"), + ]; + static ref ADDED_PATHS: Mutex<HashSet<PathBuf>> = Mutex::new(HashSet::new()); + static ref LAST_MODIFIED: Mutex<SystemTime> = + Mutex::new(get_modified_time(&env::current_exe().unwrap()) + .expect("Failed to get modified time of executable")); +} + +fn get_modified_time(file: &Path) -> Option<SystemTime> { + file.metadata().and_then(|m| m.modified()).ok() +} + +fn update_last_modified(file: &Path) { + let modified = get_modified_time(file).expect("Couldn't get file modification time"); + let mut last_modified = LAST_MODIFIED.lock().unwrap(); + *last_modified = cmp::max(modified, *last_modified); +} + +fn search_include(name: &str) -> Option<PathBuf> { + for path in SEARCH_PATHS.iter() { + let file = path.join(name); + if file.is_file() { + update_last_modified(&file); + return Some(file); + } + } + None +} + +fn add_headers_recursively(path: PathBuf, added_paths: &mut HashSet<PathBuf>) { + if added_paths.contains(&path) { + return; + } + let mut file = File::open(&path).unwrap(); + let mut content = String::new(); + file.read_to_string(&mut content).unwrap(); + added_paths.insert(path); + // Find all includes and add them recursively + for cap in INCLUDE_RE.captures_iter(&content) { + if let Some(path) = search_include(cap.get(1).unwrap().as_str()) { + add_headers_recursively(path, added_paths); + } + } +} + +fn add_include(name: &str) -> String { + let mut added_paths = ADDED_PATHS.lock().unwrap(); + let file = match search_include(name) { + Some(file) => file, + None => panic!("Include not found: {}", name), + }; + let result = String::from(file.to_str().unwrap()); + add_headers_recursively(file, &mut *added_paths); + result +} + +trait BuilderExt { + fn get_initial_builder() -> Builder; + fn include<T: Into<String>>(self, file: T) -> Builder; +} + +impl BuilderExt for Builder { + fn get_initial_builder() -> Builder { + // Disable rust unions, because we replace some types inside of + // them. + let mut builder = Builder::default() + .size_t_is_usize(true) + .disable_untagged_union(); + + let rustfmt_path = env::var_os("RUSTFMT") + // This can be replaced with + // > .filter(|p| !p.is_empty()).map(PathBuf::from) + // once we can use 1.27+. + .and_then(|p| { + if p.is_empty() { + None + } else { + Some(PathBuf::from(p)) + } + }); + if let Some(path) = rustfmt_path { + builder = builder.with_rustfmt(path); + } + + for dir in SEARCH_PATHS.iter() { + builder = builder.clang_arg("-I").clang_arg(dir.to_str().unwrap()); + } + + builder = builder.include(add_include("mozilla-config.h")); + + if env::var("CARGO_FEATURE_GECKO_DEBUG").is_ok() { + builder = builder.clang_arg("-DDEBUG=1").clang_arg("-DJS_DEBUG=1"); + } + + for item in &*BINDGEN_FLAGS { + builder = builder.clang_arg(item); + } + + builder + } + fn include<T: Into<String>>(self, file: T) -> Builder { + self.clang_arg("-include").clang_arg(file) + } +} + +struct Fixup { + pat: String, + rep: String, +} + +fn write_binding_file(builder: Builder, file: &str, fixups: &[Fixup]) { + let out_file = OUTDIR_PATH.join(file); + if let Some(modified) = get_modified_time(&out_file) { + // Don't generate the file if nothing it depends on was modified. + let last_modified = LAST_MODIFIED.lock().unwrap(); + if *last_modified <= modified { + return; + } + } + let command_line_opts = builder.command_line_flags(); + let result = builder.generate(); + let mut result = match result { + Ok(bindings) => bindings.to_string(), + Err(_) => { + panic!( + "Failed to generate bindings, flags: {:?}", + command_line_opts + ); + }, + }; + + for fixup in fixups.iter() { + result = Regex::new(&fixup.pat) + .unwrap() + .replace_all(&result, &*fixup.rep) + .into_owned() + .into(); + } + let bytes = result.into_bytes(); + File::create(&out_file) + .unwrap() + .write_all(&bytes) + .expect("Unable to write output"); +} + +struct BuilderWithConfig<'a> { + builder: Builder, + config: &'a Table, + used_keys: HashSet<&'static str>, +} +impl<'a> BuilderWithConfig<'a> { + fn new(builder: Builder, config: &'a Table) -> Self { + BuilderWithConfig { + builder, + config, + used_keys: HashSet::new(), + } + } + + fn handle_list<F>(self, key: &'static str, func: F) -> BuilderWithConfig<'a> + where + F: FnOnce(Builder, slice::Iter<'a, toml::Value>) -> Builder, + { + let mut builder = self.builder; + let config = self.config; + let mut used_keys = self.used_keys; + if let Some(list) = config.get(key) { + used_keys.insert(key); + builder = func(builder, list.as_array().unwrap().as_slice().iter()); + } + BuilderWithConfig { + builder, + config, + used_keys, + } + } + fn handle_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a toml::Value) -> Builder, + { + self.handle_list(key, |b, iter| iter.fold(b, |b, item| func(b, item))) + } + fn handle_str_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a str) -> Builder, + { + self.handle_items(key, |b, item| func(b, item.as_str().unwrap())) + } + fn handle_table_items<F>(self, key: &'static str, mut func: F) -> BuilderWithConfig<'a> + where + F: FnMut(Builder, &'a Table) -> Builder, + { + self.handle_items(key, |b, item| func(b, item.as_table().unwrap())) + } + fn handle_common(self, fixups: &mut Vec<Fixup>) -> BuilderWithConfig<'a> { + self.handle_str_items("headers", |b, item| b.header(add_include(item))) + .handle_str_items("raw-lines", |b, item| b.raw_line(item)) + .handle_str_items("hide-types", |b, item| b.blocklist_type(item)) + .handle_table_items("fixups", |builder, item| { + fixups.push(Fixup { + pat: item["pat"].as_str().unwrap().into(), + rep: item["rep"].as_str().unwrap().into(), + }); + builder + }) + } + + fn get_builder(self) -> Builder { + for key in self.config.keys() { + if !self.used_keys.contains(key.as_str()) { + panic!("Unknown key: {}", key); + } + } + self.builder + } +} + +fn generate_structs() { + let builder = Builder::get_initial_builder() + .enable_cxx_namespaces() + .with_codegen_config(CodegenConfig::TYPES | CodegenConfig::VARS | CodegenConfig::FUNCTIONS); + let mut fixups = vec![]; + let builder = BuilderWithConfig::new(builder, CONFIG["structs"].as_table().unwrap()) + .handle_common(&mut fixups) + .handle_str_items("allowlist-functions", |b, item| b.allowlist_function(item)) + .handle_str_items("bitfield-enums", |b, item| b.bitfield_enum(item)) + .handle_str_items("rusty-enums", |b, item| b.rustified_enum(item)) + .handle_str_items("allowlist-vars", |b, item| b.allowlist_var(item)) + .handle_str_items("allowlist-types", |b, item| b.allowlist_type(item)) + .handle_str_items("opaque-types", |b, item| b.opaque_type(item)) + .handle_table_items("cbindgen-types", |b, item| { + let gecko = item["gecko"].as_str().unwrap(); + let servo = item["servo"].as_str().unwrap(); + b.blocklist_type(format!("mozilla::{}", gecko)) + .module_raw_line("root::mozilla", format!("pub use {} as {};", servo, gecko)) + }) + .handle_table_items("mapped-generic-types", |builder, item| { + let generic = item["generic"].as_bool().unwrap(); + let gecko = item["gecko"].as_str().unwrap(); + let servo = item["servo"].as_str().unwrap(); + let gecko_name = gecko.rsplit("::").next().unwrap(); + let gecko = gecko + .split("::") + .map(|s| format!("\\s*{}\\s*", s)) + .collect::<Vec<_>>() + .join("::"); + + fixups.push(Fixup { + pat: format!("\\broot\\s*::\\s*{}\\b", gecko), + rep: format!("crate::gecko_bindings::structs::{}", gecko_name), + }); + builder.blocklist_type(gecko).raw_line(format!( + "pub type {0}{2} = {1}{2};", + gecko_name, + servo, + if generic { "<T>" } else { "" } + )) + }) + .get_builder(); + write_binding_file(builder, STRUCTS_FILE, &fixups); +} + +fn setup_logging() -> bool { + struct BuildLogger { + file: Option<Mutex<fs::File>>, + filter: String, + } + + impl log::Log for BuildLogger { + fn enabled(&self, meta: &log::Metadata) -> bool { + self.file.is_some() && meta.target().contains(&self.filter) + } + + fn log(&self, record: &log::Record) { + if !self.enabled(record.metadata()) { + return; + } + + let mut file = self.file.as_ref().unwrap().lock().unwrap(); + let _ = writeln!( + file, + "{} - {} - {} @ {}:{}", + record.level(), + record.target(), + record.args(), + record.file().unwrap_or("<unknown>"), + record.line().unwrap_or(0) + ); + } + + fn flush(&self) { + if let Some(ref file) = self.file { + file.lock().unwrap().flush().unwrap(); + } + } + } + + if let Some(path) = env::var_os("STYLO_BUILD_LOG") { + log::set_max_level(log::LevelFilter::Debug); + log::set_boxed_logger(Box::new(BuildLogger { + file: fs::File::create(path).ok().map(Mutex::new), + filter: env::var("STYLO_BUILD_FILTER") + .ok() + .unwrap_or_else(|| "bindgen".to_owned()), + })) + .expect("Failed to set logger."); + + true + } else { + false + } +} + +fn generate_atoms() { + let script = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("gecko") + .join("regen_atoms.py"); + println!("cargo:rerun-if-changed={}", script.display()); + let status = Command::new(&*PYTHON) + .arg(&script) + .arg(DISTDIR_PATH.as_os_str()) + .arg(OUTDIR_PATH.as_os_str()) + .status() + .unwrap(); + if !status.success() { + exit(1); + } +} + +pub fn generate() { + println!("cargo:rerun-if-changed=build_gecko.rs"); + fs::create_dir_all(&*OUTDIR_PATH).unwrap(); + setup_logging(); + generate_structs(); + generate_atoms(); + + for path in ADDED_PATHS.lock().unwrap().iter() { + println!("cargo:rerun-if-changed={}", path.to_str().unwrap()); + } +} diff --git a/servo/components/style/color/convert.rs b/servo/components/style/color/convert.rs new file mode 100644 index 0000000000..a6274db39a --- /dev/null +++ b/servo/components/style/color/convert.rs @@ -0,0 +1,902 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color conversion algorithms. +//! +//! Algorithms, matrices and constants are from the [color-4] specification, +//! unless otherwise specified: +//! +//! https://drafts.csswg.org/css-color-4/#color-conversion-code +//! +//! NOTE: Matrices has to be transposed from the examples in the spec for use +//! with the `euclid` library. + +use crate::color::ColorComponents; +use crate::values::normalize; + +type Transform = euclid::default::Transform3D<f32>; +type Vector = euclid::default::Vector3D<f32>; + +/// Normalize hue into [0, 360). +#[inline] +pub fn normalize_hue(hue: f32) -> f32 { + hue - 360. * (hue / 360.).floor() +} + +/// Calculate the hue from RGB components and return it along with the min and +/// max RGB values. +#[inline] +fn rgb_to_hue_min_max(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { + let max = red.max(green).max(blue); + let min = red.min(green).min(blue); + + let delta = max - min; + + let hue = if delta != 0.0 { + 60.0 * if max == red { + (green - blue) / delta + if green < blue { 6.0 } else { 0.0 } + } else if max == green { + (blue - red) / delta + 2.0 + } else { + (red - green) / delta + 4.0 + } + } else { + f32::NAN + }; + + (hue, min, max) +} + +/// Convert from HSL notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hsl-to-rgb +#[inline] +pub fn hsl_to_rgb(from: &ColorComponents) -> ColorComponents { + fn hue_to_rgb(t1: f32, t2: f32, hue: f32) -> f32 { + let hue = normalize_hue(hue); + + if hue * 6.0 < 360.0 { + t1 + (t2 - t1) * hue / 60.0 + } else if hue * 2.0 < 360.0 { + t2 + } else if hue * 3.0 < 720.0 { + t1 + (t2 - t1) * (240.0 - hue) / 60.0 + } else { + t1 + } + } + + // Convert missing components to 0.0. + let ColorComponents(hue, saturation, lightness) = from.map(normalize); + let saturation = saturation / 100.0; + let lightness = lightness / 100.0; + + let t2 = if lightness <= 0.5 { + lightness * (saturation + 1.0) + } else { + lightness + saturation - lightness * saturation + }; + let t1 = lightness * 2.0 - t2; + + ColorComponents( + hue_to_rgb(t1, t2, hue + 120.0), + hue_to_rgb(t1, t2, hue), + hue_to_rgb(t1, t2, hue - 120.0), + ) +} + +/// Convert from RGB notation to HSL notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hsl +pub fn rgb_to_hsl(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let lightness = (min + max) / 2.0; + let delta = max - min; + + let saturation = if delta != 0.0 { + if lightness == 0.0 || lightness == 1.0 { + 0.0 + } else { + (max - lightness) / lightness.min(1.0 - lightness) + } + } else { + 0.0 + }; + + ColorComponents(hue, saturation * 100.0, lightness * 100.0) +} + +/// Convert from HWB notation to RGB notation. +/// https://drafts.csswg.org/css-color-4/#hwb-to-rgb +#[inline] +pub fn hwb_to_rgb(from: &ColorComponents) -> ColorComponents { + // Convert missing components to 0.0. + let ColorComponents(hue, whiteness, blackness) = from.map(normalize); + + let whiteness = whiteness / 100.0; + let blackness = blackness / 100.0; + + if whiteness + blackness >= 1.0 { + let gray = whiteness / (whiteness + blackness); + return ColorComponents(gray, gray, gray); + } + + let x = 1.0 - whiteness - blackness; + hsl_to_rgb(&ColorComponents(hue, 100.0, 50.0)).map(|v| v * x + whiteness) +} + +/// Convert from RGB notation to HWB notation. +/// https://drafts.csswg.org/css-color-4/#rgb-to-hwb +#[inline] +pub fn rgb_to_hwb(from: &ColorComponents) -> ColorComponents { + let ColorComponents(red, green, blue) = *from; + + let (hue, min, max) = rgb_to_hue_min_max(red, green, blue); + + let whiteness = min; + let blackness = 1.0 - max; + + ColorComponents(hue, whiteness * 100.0, blackness * 100.0) +} + +/// Convert from the rectangular orthogonal to the cylindrical polar coordinate +/// system. This is used to convert (ok)lab to (ok)lch. +/// <https://drafts.csswg.org/css-color-4/#lab-to-lch> +#[inline] +pub fn orthogonal_to_polar(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let chroma = (a * a + b * b).sqrt(); + + // Very small chroma values make the hue component powerless. + let hue = if chroma.abs() < 1.0e-6 { + f32::NAN + } else { + normalize_hue(b.atan2(a).to_degrees()) + }; + + ColorComponents(lightness, chroma, hue) +} + +/// Convert from the cylindrical polar to the rectangular orthogonal coordinate +/// system. This is used to convert (ok)lch to (ok)lab. +/// <https://drafts.csswg.org/css-color-4/#lch-to-lab> +#[inline] +pub fn polar_to_orthogonal(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, chroma, hue) = *from; + + // A missing hue component results in an achromatic color. + if hue.is_nan() { + return ColorComponents(lightness, 0.0, 0.0); + } + + let hue = hue.to_radians(); + let a = chroma * hue.cos(); + let b = chroma * hue.sin(); + + ColorComponents(lightness, a, b) +} + +#[inline] +fn transform(from: &ColorComponents, mat: &Transform) -> ColorComponents { + let result = mat.transform_vector3d(Vector::new(from.0, from.1, from.2)); + ColorComponents(result.x, result.y, result.z) +} + +fn xyz_d65_to_xyz_d50(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 1.0479298208405488, 0.029627815688159344, -0.009243058152591178, 0.0, + 0.022946793341019088, 0.990434484573249, 0.015055144896577895, 0.0, + -0.05019222954313557, -0.01707382502938514, 0.7518742899580008, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +fn xyz_d50_to_xyz_d65(from: &ColorComponents) -> ColorComponents { + #[rustfmt::skip] + const MAT: Transform = Transform::new( + 0.9554734527042182, -0.028369706963208136, 0.012314001688319899, 0.0, + -0.023098536874261423, 1.0099954580058226, -0.020507696433477912, 0.0, + 0.0632593086610217, 0.021041398966943008, 1.3303659366080753, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + transform(from, &MAT) +} + +/// A reference white that is used during color conversion. +pub enum WhitePoint { + /// D50 white reference. + D50, + /// D65 white reference. + D65, +} + +impl WhitePoint { + const fn values(&self) -> ColorComponents { + // <https://drafts.csswg.org/css-color-4/#color-conversion-code> + match self { + // [0.3457 / 0.3585, 1.00000, (1.0 - 0.3457 - 0.3585) / 0.3585] + WhitePoint::D50 => ColorComponents(0.9642956764295677, 1.0, 0.8251046025104602), + // [0.3127 / 0.3290, 1.00000, (1.0 - 0.3127 - 0.3290) / 0.3290] + WhitePoint::D65 => ColorComponents(0.9504559270516716, 1.0, 1.0890577507598784), + } + } +} + +fn convert_white_point(from: WhitePoint, to: WhitePoint, components: &mut ColorComponents) { + match (from, to) { + (WhitePoint::D50, WhitePoint::D65) => *components = xyz_d50_to_xyz_d65(components), + (WhitePoint::D65, WhitePoint::D50) => *components = xyz_d65_to_xyz_d50(components), + _ => {}, + } +} + +/// A trait that allows conversion of color spaces to and from XYZ coordinate +/// space with a specified white point. +/// +/// Allows following the specified method of converting between color spaces: +/// - Convert to values to sRGB linear light. +/// - Convert to XYZ coordinate space. +/// - Adjust white point to target white point. +/// - Convert to sRGB linear light in target color space. +/// - Convert to sRGB gamma encoded in target color space. +/// +/// https://drafts.csswg.org/css-color-4/#color-conversion +pub trait ColorSpaceConversion { + /// The white point that the implementer is represented in. + const WHITE_POINT: WhitePoint; + + /// Convert the components from sRGB gamma encoded values to sRGB linear + /// light values. + fn to_linear_light(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to XYZ coordinate + /// space. + fn to_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from XYZ coordinate space to sRGB linear light + /// values. + fn from_xyz(from: &ColorComponents) -> ColorComponents; + + /// Convert the components from sRGB linear light values to sRGB gamma + /// encoded values. + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents; +} + +/// Convert the color components from the specified color space to XYZ and +/// return the components and the white point they are in. +pub fn to_xyz<From: ColorSpaceConversion>(from: &ColorComponents) -> (ColorComponents, WhitePoint) { + // Convert the color components where in-gamut values are in the range + // [0 - 1] to linear light (un-companded) form. + let result = From::to_linear_light(from); + + // Convert the color components from the source color space to XYZ. + (From::to_xyz(&result), From::WHITE_POINT) +} + +/// Convert the color components from XYZ at the given white point to the +/// specified color space. +pub fn from_xyz<To: ColorSpaceConversion>( + from: &ColorComponents, + white_point: WhitePoint, +) -> ColorComponents { + let mut xyz = from.clone(); + + // Convert the white point if needed. + convert_white_point(white_point, To::WHITE_POINT, &mut xyz); + + // Convert the color from XYZ to the target color space. + let result = To::from_xyz(&xyz); + + // Convert the color components of linear-light values in the range + // [0 - 1] to a gamma corrected form. + To::to_gamma_encoded(&result) +} + +/// The sRGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB +pub struct Srgb; + +impl Srgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.4123907992659595, 0.21263900587151036, 0.01933081871559185, 0.0, + 0.35758433938387796, 0.7151686787677559, 0.11919477979462599, 0.0, + 0.1804807884018343, 0.07219231536073371, 0.9505321522496606, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 3.2409699419045213, -0.9692436362808798, 0.05563007969699361, 0.0, + -1.5373831775700935, 1.8759675015077206, -0.20397695888897657, 0.0, + -0.4986107602930033, 0.04155505740717561, 1.0569715142428786, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Srgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < 0.04045 { + value / 12.92 + } else { + value.signum() * ((abs + 0.055) / 1.055).powf(2.4) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs > 0.0031308 { + value.signum() * (1.055 * abs.powf(1.0 / 2.4) - 0.055) + } else { + 12.92 * value + } + }) + } +} + +/// Color specified with hue, saturation and lightness components. +pub struct Hsl; + +impl ColorSpaceConversion for Hsl { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hsl_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hsl(&Srgb::to_gamma_encoded(from)) + } +} + +/// Color specified with hue, whiteness and blackness components. +pub struct Hwb; + +impl ColorSpaceConversion for Hwb { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(&hwb_to_rgb(from)) + } + + #[inline] + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + #[inline] + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + rgb_to_hwb(&Srgb::to_gamma_encoded(from)) + } +} + +/// The same as sRGB color space, except the transfer function is linear light. +/// https://drafts.csswg.org/css-color-4/#predefined-sRGB-linear +pub struct SrgbLinear; + +impl ColorSpaceConversion for SrgbLinear { + const WHITE_POINT: WhitePoint = Srgb::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // Already in linear light form. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::to_xyz(from) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + Srgb::from_xyz(from) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // Stay in linear light form. + from.clone() + } +} + +/// The Display-P3 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-display-p3 +pub struct DisplayP3; + +impl DisplayP3 { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.48657094864821626, 0.22897456406974884, 0.0, 0.0, + 0.26566769316909294, 0.6917385218365062, 0.045113381858902575, 0.0, + 0.1982172852343625, 0.079286914093745, 1.0439443689009757, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.4934969119414245, -0.829488969561575, 0.035845830243784335, 0.0, + -0.9313836179191236, 1.7626640603183468, -0.07617238926804171, 0.0, + -0.40271078445071684, 0.02362468584194359, 0.9568845240076873, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for DisplayP3 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + Srgb::to_linear_light(from) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + Srgb::to_gamma_encoded(from) + } +} + +/// The a98-rgb color space. +/// https://drafts.csswg.org/css-color-4/#predefined-a98-rgb +pub struct A98Rgb; + +impl A98Rgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.5766690429101308, 0.29734497525053616, 0.027031361386412378, 0.0, + 0.18555823790654627, 0.627363566255466, 0.07068885253582714, 0.0, + 0.18822864623499472, 0.07529145849399789, 0.9913375368376389, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 2.041587903810746, -0.9692436362808798, 0.013444280632031024, 0.0, + -0.5650069742788596, 1.8759675015077206, -0.11836239223101824, 0.0, + -0.3447313507783295, 0.04155505740717561, 1.0151749943912054, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for A98Rgb { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| v.signum() * v.abs().powf(2.19921875)) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + .map(|v| v.signum() * v.abs().powf(0.4547069271758437)) + } +} + +/// The ProPhoto RGB color space. +/// https://drafts.csswg.org/css-color-4/#predefined-prophoto-rgb +pub struct ProphotoRgb; + +impl ProphotoRgb { + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.7977604896723027, 0.2880711282292934, 0.0, 0.0, + 0.13518583717574031, 0.7118432178101014, 0.0, 0.0, + 0.0313493495815248, 0.00008565396060525902, 0.8251046025104601, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.3457989731028281, -0.5446224939028347, 0.0, 0.0, + -0.25558010007997534, 1.5082327413132781, 0.0, 0.0, + -0.05110628506753401, 0.02053603239147973, 1.2119675456389454, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for ProphotoRgb { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + const ET2: f32 = 16.0 / 512.0; + + let abs = value.abs(); + + if abs <= ET2 { + value / 16.0 + } else { + value.signum() * abs.powf(1.8) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + const ET: f32 = 1.0 / 512.0; + + from.clone().map(|v| { + let abs = v.abs(); + if abs >= ET { + v.signum() * abs.powf(1.0 / 1.8) + } else { + 16.0 * v + } + }) + } +} + +/// The Rec.2020 color space. +/// https://drafts.csswg.org/css-color-4/#predefined-rec2020 +pub struct Rec2020; + +impl Rec2020 { + const ALPHA: f32 = 1.09929682680944; + const BETA: f32 = 0.018053968510807; + + #[rustfmt::skip] + const TO_XYZ: Transform = Transform::new( + 0.6369580483012913, 0.26270021201126703, 0.0, 0.0, + 0.14461690358620838, 0.677998071518871, 0.028072693049087508, 0.0, + 0.16888097516417205, 0.059301716469861945, 1.0609850577107909, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const FROM_XYZ: Transform = Transform::new( + 1.7166511879712676, -0.666684351832489, 0.017639857445310915, 0.0, + -0.3556707837763924, 1.616481236634939, -0.042770613257808655, 0.0, + -0.2533662813736598, 0.01576854581391113, 0.942103121235474, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Rec2020 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone().map(|value| { + let abs = value.abs(); + + if abs < Self::BETA * 4.5 { + value / 4.5 + } else { + value.signum() * ((abs + Self::ALPHA - 1.0) / Self::ALPHA).powf(1.0 / 0.45) + } + }) + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + transform(from, &Self::FROM_XYZ) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone().map(|v| { + let abs = v.abs(); + + if abs > Self::BETA { + v.signum() * (Self::ALPHA * abs.powf(0.45) - (Self::ALPHA - 1.0)) + } else { + 4.5 * v + } + }) + } +} + +/// A color in the XYZ coordinate space with a D50 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD50; + +impl ColorSpaceConversion for XyzD50 { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// A color in the XYZ coordinate space with a D65 white reference. +/// https://drafts.csswg.org/css-color-4/#predefined-xyz +pub struct XyzD65; + +impl ColorSpaceConversion for XyzD65 { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + from.clone() + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + from.clone() + } +} + +/// The Lab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lab; + +impl Lab { + const KAPPA: f32 = 24389.0 / 27.0; + const EPSILON: f32 = 216.0 / 24389.0; +} + +impl ColorSpaceConversion for Lab { + const WHITE_POINT: WhitePoint = WhitePoint::D50; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + /// Convert a CIELAB color to XYZ as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#lab-to-predefined + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let ColorComponents(lightness, a, b) = *from; + + let f1 = (lightness + 16.0) / 116.0; + let f0 = f1 + a / 500.0; + let f2 = f1 - b / 200.0; + + let f0_cubed = f0 * f0 * f0; + let x = if f0_cubed > Self::EPSILON { + f0_cubed + } else { + (116.0 * f0 - 16.0) / Self::KAPPA + }; + + let y = if lightness > Self::KAPPA * Self::EPSILON { + let v = (lightness + 16.0) / 116.0; + v * v * v + } else { + lightness / Self::KAPPA + }; + + let f2_cubed = f2 * f2 * f2; + let z = if f2_cubed > Self::EPSILON { + f2_cubed + } else { + (116.0 * f2 - 16.0) / Self::KAPPA + }; + + ColorComponents(x, y, z) * Self::WHITE_POINT.values() + } + + /// Convert an XYZ color to LAB as specified in [1] and [2]. + /// + /// [1]: https://drafts.csswg.org/css-color/#rgb-to-lab + /// [2]: https://drafts.csswg.org/css-color/#color-conversion-code + fn from_xyz(from: &ColorComponents) -> ColorComponents { + let adapted = *from / Self::WHITE_POINT.values(); + + // 4. Convert D50-adapted XYZ to Lab. + let ColorComponents(f0, f1, f2) = adapted.map(|v| { + if v > Self::EPSILON { + v.cbrt() + } else { + (Self::KAPPA * v + 16.0) / 116.0 + } + }); + + let lightness = 116.0 * f1 - 16.0; + let a = 500.0 * (f0 - f1); + let b = 200.0 * (f1 - f2); + + ColorComponents(lightness, a, b) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Lch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-lab-lch +pub struct Lch; + +impl ColorSpaceConversion for Lch { + const WHITE_POINT: WhitePoint = Lab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // Convert LCH to Lab first. + let lab = polar_to_orthogonal(from); + + // Then convert the Lab to XYZ. + Lab::to_xyz(&lab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert the XYZ to LAB. + let lab = Lab::from_xyz(&from); + + // Then convert the Lab to LCH. + orthogonal_to_polar(&lab) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklab color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklab; + +impl Oklab { + #[rustfmt::skip] + const XYZ_TO_LMS: Transform = Transform::new( + 0.8190224432164319, 0.0329836671980271, 0.048177199566046255, 0.0, + 0.3619062562801221, 0.9292868468965546, 0.26423952494422764, 0.0, + -0.12887378261216414, 0.03614466816999844, 0.6335478258136937, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_OKLAB: Transform = Transform::new( + 0.2104542553, 1.9779984951, 0.0259040371, 0.0, + 0.7936177850, -2.4285922050, 0.7827717662, 0.0, + -0.0040720468, 0.4505937099, -0.8086757660, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const LMS_TO_XYZ: Transform = Transform::new( + 1.2268798733741557, -0.04057576262431372, -0.07637294974672142, 0.0, + -0.5578149965554813, 1.1122868293970594, -0.4214933239627914, 0.0, + 0.28139105017721583, -0.07171106666151701, 1.5869240244272418, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); + + #[rustfmt::skip] + const OKLAB_TO_LMS: Transform = Transform::new( + 0.99999999845051981432, 1.0000000088817607767, 1.0000000546724109177, 0.0, + 0.39633779217376785678, -0.1055613423236563494, -0.089484182094965759684, 0.0, + 0.21580375806075880339, -0.063854174771705903402, -1.2914855378640917399, 0.0, + 0.0, 0.0, 0.0, 1.0, + ); +} + +impl ColorSpaceConversion for Oklab { + const WHITE_POINT: WhitePoint = WhitePoint::D65; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::OKLAB_TO_LMS); + let lms = lms.map(|v| v * v * v); + transform(&lms, &Self::LMS_TO_XYZ) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + let lms = transform(&from, &Self::XYZ_TO_LMS); + let lms = lms.map(|v| v.cbrt()); + transform(&lms, &Self::LMS_TO_OKLAB) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} + +/// The Oklch color space. +/// https://drafts.csswg.org/css-color-4/#specifying-oklab-oklch +pub struct Oklch; + +impl ColorSpaceConversion for Oklch { + const WHITE_POINT: WhitePoint = Oklab::WHITE_POINT; + + fn to_linear_light(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } + + fn to_xyz(from: &ColorComponents) -> ColorComponents { + // First convert OkLCH to Oklab. + let oklab = polar_to_orthogonal(from); + + // Then convert Oklab to XYZ. + Oklab::to_xyz(&oklab) + } + + fn from_xyz(from: &ColorComponents) -> ColorComponents { + // First convert XYZ to Oklab. + let lab = Oklab::from_xyz(&from); + + // Then convert Oklab to OkLCH. + orthogonal_to_polar(&lab) + } + + fn to_gamma_encoded(from: &ColorComponents) -> ColorComponents { + // No need for conversion. + from.clone() + } +} diff --git a/servo/components/style/color/mix.rs b/servo/components/style/color/mix.rs new file mode 100644 index 0000000000..bcc4628575 --- /dev/null +++ b/servo/components/style/color/mix.rs @@ -0,0 +1,558 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color mixing/interpolation. + +use super::{AbsoluteColor, ColorFlags, ColorSpace}; +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::color::ColorMixFlags; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A hue-interpolation-method as defined in [1]. +/// +/// [1]: https://drafts.csswg.org/css-color-4/#typedef-hue-interpolation-method +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum HueInterpolationMethod { + /// https://drafts.csswg.org/css-color-4/#shorter + Shorter, + /// https://drafts.csswg.org/css-color-4/#longer + Longer, + /// https://drafts.csswg.org/css-color-4/#increasing + Increasing, + /// https://drafts.csswg.org/css-color-4/#decreasing + Decreasing, + /// https://drafts.csswg.org/css-color-4/#specified + Specified, +} + +/// https://drafts.csswg.org/css-color-4/#color-interpolation-method +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToShmem, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, +)] +#[repr(C)] +pub struct ColorInterpolationMethod { + /// The color-space the interpolation should be done in. + pub space: ColorSpace, + /// The hue interpolation method. + pub hue: HueInterpolationMethod, +} + +impl ColorInterpolationMethod { + /// Returns the srgb interpolation method. + pub const fn srgb() -> Self { + Self { + space: ColorSpace::Srgb, + hue: HueInterpolationMethod::Shorter, + } + } + + /// Return the oklab interpolation method used for default color + /// interpolcation. + pub const fn oklab() -> Self { + Self { + space: ColorSpace::Oklab, + hue: HueInterpolationMethod::Shorter, + } + } + + /// Decides the best method for interpolating between the given colors. + /// https://drafts.csswg.org/css-color-4/#interpolation-space + pub fn best_interpolation_between(left: &AbsoluteColor, right: &AbsoluteColor) -> Self { + // The preferred color space to use for interpolating colors is Oklab. + // However, if either of the colors are in legacy rgb(), hsl() or hwb(), + // then interpolation is done in sRGB. + if !left.is_legacy_syntax() || !right.is_legacy_syntax() { + Self::oklab() + } else { + Self::srgb() + } + } +} + +impl Parse for ColorInterpolationMethod { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_ident_matching("in")?; + let space = ColorSpace::parse(input)?; + // https://drafts.csswg.org/css-color-4/#hue-interpolation + // Unless otherwise specified, if no specific hue interpolation + // algorithm is selected by the host syntax, the default is shorter. + let hue = if space.is_polar() { + input + .try_parse(|input| -> Result<_, ParseError<'i>> { + let hue = HueInterpolationMethod::parse(input)?; + input.expect_ident_matching("hue")?; + Ok(hue) + }) + .unwrap_or(HueInterpolationMethod::Shorter) + } else { + HueInterpolationMethod::Shorter + }; + Ok(Self { space, hue }) + } +} + +impl ToCss for ColorInterpolationMethod { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("in ")?; + self.space.to_css(dest)?; + if self.hue != HueInterpolationMethod::Shorter { + dest.write_char(' ')?; + self.hue.to_css(dest)?; + dest.write_str(" hue")?; + } + Ok(()) + } +} + +/// Mix two colors into one. +pub fn mix( + interpolation: ColorInterpolationMethod, + left_color: &AbsoluteColor, + mut left_weight: f32, + right_color: &AbsoluteColor, + mut right_weight: f32, + flags: ColorMixFlags, +) -> AbsoluteColor { + // https://drafts.csswg.org/css-color-5/#color-mix-percent-norm + let mut alpha_multiplier = 1.0; + if flags.contains(ColorMixFlags::NORMALIZE_WEIGHTS) { + let sum = left_weight + right_weight; + if sum != 1.0 { + let scale = 1.0 / sum; + left_weight *= scale; + right_weight *= scale; + if sum < 1.0 { + alpha_multiplier = sum; + } + } + } + + let result = mix_in( + interpolation.space, + left_color, + left_weight, + right_color, + right_weight, + interpolation.hue, + alpha_multiplier, + ); + + if flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) { + // If the result *MUST* be in modern syntax, then make sure it is in a + // color space that allows the modern syntax. So hsl and hwb will be + // converted to srgb. + if result.is_legacy_syntax() { + result.to_color_space(ColorSpace::Srgb) + } else { + result + } + } else if left_color.is_legacy_syntax() && right_color.is_legacy_syntax() { + // If both sides of the mix is legacy then convert the result back into + // legacy. + result.into_srgb_legacy() + } else { + result + } +} + +/// What the outcome of each component should be in a mix result. +#[derive(Clone, Copy)] +#[repr(u8)] +enum ComponentMixOutcome { + /// Mix the left and right sides to give the result. + Mix, + /// Carry the left side forward to the result. + UseLeft, + /// Carry the right side forward to the result. + UseRight, + /// The resulting component should also be none. + None, +} + +impl ComponentMixOutcome { + fn from_colors( + left: &AbsoluteColor, + right: &AbsoluteColor, + flags_to_check: ColorFlags, + ) -> Self { + match ( + left.flags.contains(flags_to_check), + right.flags.contains(flags_to_check), + ) { + (true, true) => Self::None, + (true, false) => Self::UseRight, + (false, true) => Self::UseLeft, + (false, false) => Self::Mix, + } + } +} + +/// Calculate the flags that should be carried forward a color before converting +/// it to the interpolation color space according to: +/// <https://drafts.csswg.org/css-color-4/#interpolation-missing> +fn carry_forward_analogous_missing_components( + from: ColorSpace, + to: ColorSpace, + flags: ColorFlags, +) -> ColorFlags { + use ColorFlags as F; + use ColorSpace as S; + + if from == to { + return flags; + } + + // Reds r, x + // Greens g, y + // Blues b, z + if from.is_rgb_or_xyz_like() && to.is_rgb_or_xyz_like() { + return flags; + } + + let mut result = flags; + + // Lightness L + if matches!(from, S::Lab | S::Lch | S::Oklab | S::Oklch) { + if matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) { + result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE)); + } else if matches!(to, S::Hsl) { + result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE)); + } + } else if matches!(from, S::Hsl) && matches!(to, S::Lab | S::Lch | S::Oklab | S::Oklch) { + result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE)); + } + + // Colorfulness C, S + if matches!(from, S::Hsl | S::Lch | S::Oklch) && matches!(to, S::Hsl | S::Lch | S::Oklch) { + result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE)); + } + + // Hue H + if matches!(from, S::Hsl | S::Hwb) { + if matches!(to, S::Hsl | S::Hwb) { + result.set(F::C0_IS_NONE, flags.contains(F::C0_IS_NONE)); + } else if matches!(to, S::Lch | S::Oklch) { + result.set(F::C2_IS_NONE, flags.contains(F::C0_IS_NONE)); + } + } else if matches!(from, S::Lch | S::Oklch) { + if matches!(to, S::Hsl | S::Hwb) { + result.set(F::C0_IS_NONE, flags.contains(F::C2_IS_NONE)); + } else if matches!(to, S::Lch | S::Oklch) { + result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE)); + } + } + + // Opponent a, a + // Opponent b, b + if matches!(from, S::Lab | S::Oklab) && matches!(to, S::Lab | S::Oklab) { + result.set(F::C1_IS_NONE, flags.contains(F::C1_IS_NONE)); + result.set(F::C2_IS_NONE, flags.contains(F::C2_IS_NONE)); + } + + result +} + +fn mix_in( + color_space: ColorSpace, + left_color: &AbsoluteColor, + left_weight: f32, + right_color: &AbsoluteColor, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, + alpha_multiplier: f32, +) -> AbsoluteColor { + // Convert both colors into the interpolation color space. + let mut left = left_color.to_color_space(color_space); + left.flags = + carry_forward_analogous_missing_components(left_color.color_space, color_space, left.flags); + let mut right = right_color.to_color_space(color_space); + right.flags = carry_forward_analogous_missing_components( + right_color.color_space, + color_space, + right.flags, + ); + + let outcomes = [ + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C0_IS_NONE), + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C1_IS_NONE), + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::C2_IS_NONE), + ComponentMixOutcome::from_colors(&left, &right, ColorFlags::ALPHA_IS_NONE), + ]; + + // Convert both sides into just components. + let left = left.raw_components(); + let right = right.raw_components(); + + let (result, result_flags) = interpolate_premultiplied( + &left, + left_weight, + &right, + right_weight, + color_space.hue_index(), + hue_interpolation, + &outcomes, + ); + + let alpha = if alpha_multiplier != 1.0 { + result[3] * alpha_multiplier + } else { + result[3] + }; + + // FIXME: In rare cases we end up with 0.999995 in the alpha channel, + // so we reduce the precision to avoid serializing to + // rgba(?, ?, ?, 1). This is not ideal, so we should look into + // ways to avoid it. Maybe pre-multiply all color components and + // then divide after calculations? + let alpha = (alpha * 1000.0).round() / 1000.0; + + let mut result = AbsoluteColor::new(color_space, result[0], result[1], result[2], alpha); + + result.flags = result_flags; + + result +} + +fn interpolate_premultiplied_component( + left: f32, + left_weight: f32, + left_alpha: f32, + right: f32, + right_weight: f32, + right_alpha: f32, +) -> f32 { + left * left_weight * left_alpha + right * right_weight * right_alpha +} + +// Normalize hue into [0, 360) +#[inline] +fn normalize_hue(v: f32) -> f32 { + v - 360. * (v / 360.).floor() +} + +fn adjust_hue(left: &mut f32, right: &mut f32, hue_interpolation: HueInterpolationMethod) { + // Adjust the hue angle as per + // https://drafts.csswg.org/css-color/#hue-interpolation. + // + // If both hue angles are NAN, they should be set to 0. Otherwise, if a + // single hue angle is NAN, it should use the other hue angle. + if left.is_nan() { + if right.is_nan() { + *left = 0.; + *right = 0.; + } else { + *left = *right; + } + } else if right.is_nan() { + *right = *left; + } + + if hue_interpolation == HueInterpolationMethod::Specified { + // Angles are not adjusted. They are interpolated like any other + // component. + return; + } + + *left = normalize_hue(*left); + *right = normalize_hue(*right); + + match hue_interpolation { + // https://drafts.csswg.org/css-color/#shorter + HueInterpolationMethod::Shorter => { + let delta = *right - *left; + + if delta > 180. { + *left += 360.; + } else if delta < -180. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#longer + HueInterpolationMethod::Longer => { + let delta = *right - *left; + if 0. < delta && delta < 180. { + *left += 360.; + } else if -180. < delta && delta <= 0. { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#increasing + HueInterpolationMethod::Increasing => { + if *right < *left { + *right += 360.; + } + }, + // https://drafts.csswg.org/css-color/#decreasing + HueInterpolationMethod::Decreasing => { + if *left < *right { + *left += 360.; + } + }, + HueInterpolationMethod::Specified => unreachable!("Handled above"), + } +} + +fn interpolate_hue( + mut left: f32, + left_weight: f32, + mut right: f32, + right_weight: f32, + hue_interpolation: HueInterpolationMethod, +) -> f32 { + adjust_hue(&mut left, &mut right, hue_interpolation); + left * left_weight + right * right_weight +} + +struct InterpolatedAlpha { + /// The adjusted left alpha value. + left: f32, + /// The adjusted right alpha value. + right: f32, + /// The interpolated alpha value. + interpolated: f32, + /// Whether the alpha component should be `none`. + is_none: bool, +} + +fn interpolate_alpha( + left: f32, + left_weight: f32, + right: f32, + right_weight: f32, + outcome: ComponentMixOutcome, +) -> InterpolatedAlpha { + // <https://drafts.csswg.org/css-color-4/#interpolation-missing> + let mut result = match outcome { + ComponentMixOutcome::Mix => { + let interpolated = left * left_weight + right * right_weight; + InterpolatedAlpha { + left, + right, + interpolated, + is_none: false, + } + }, + ComponentMixOutcome::UseLeft => InterpolatedAlpha { + left, + right: left, + interpolated: left, + is_none: false, + }, + ComponentMixOutcome::UseRight => InterpolatedAlpha { + left: right, + right, + interpolated: right, + is_none: false, + }, + ComponentMixOutcome::None => InterpolatedAlpha { + left: 1.0, + right: 1.0, + interpolated: 0.0, + is_none: true, + }, + }; + + // Clip all alpha values to [0.0..1.0]. + result.left = result.left.clamp(0.0, 1.0); + result.right = result.right.clamp(0.0, 1.0); + result.interpolated = result.interpolated.clamp(0.0, 1.0); + + result +} + +fn interpolate_premultiplied( + left: &[f32; 4], + left_weight: f32, + right: &[f32; 4], + right_weight: f32, + hue_index: Option<usize>, + hue_interpolation: HueInterpolationMethod, + outcomes: &[ComponentMixOutcome; 4], +) -> ([f32; 4], ColorFlags) { + let alpha = interpolate_alpha(left[3], left_weight, right[3], right_weight, outcomes[3]); + let mut flags = if alpha.is_none { + ColorFlags::ALPHA_IS_NONE + } else { + ColorFlags::empty() + }; + + let mut result = [0.; 4]; + + for i in 0..3 { + match outcomes[i] { + ComponentMixOutcome::Mix => { + let is_hue = hue_index == Some(i); + result[i] = if is_hue { + normalize_hue(interpolate_hue( + left[i], + left_weight, + right[i], + right_weight, + hue_interpolation, + )) + } else { + let interpolated = interpolate_premultiplied_component( + left[i], + left_weight, + alpha.left, + right[i], + right_weight, + alpha.right, + ); + + if alpha.interpolated == 0.0 { + interpolated + } else { + interpolated / alpha.interpolated + } + }; + }, + ComponentMixOutcome::UseLeft => result[i] = left[i], + ComponentMixOutcome::UseRight => result[i] = right[i], + ComponentMixOutcome::None => { + result[i] = 0.0; + match i { + 0 => flags.insert(ColorFlags::C0_IS_NONE), + 1 => flags.insert(ColorFlags::C1_IS_NONE), + 2 => flags.insert(ColorFlags::C2_IS_NONE), + _ => unreachable!(), + } + }, + } + } + result[3] = alpha.interpolated; + + (result, flags) +} diff --git a/servo/components/style/color/mod.rs b/servo/components/style/color/mod.rs new file mode 100644 index 0000000000..797a1cb00f --- /dev/null +++ b/servo/components/style/color/mod.rs @@ -0,0 +1,613 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Color support functions. + +/// cbindgen:ignore +pub mod convert; +pub mod mix; +pub mod parsing; + +use cssparser::color::PredefinedColorSpace; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// The 3 components that make up a color. (Does not include the alpha component) +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct ColorComponents(pub f32, pub f32, pub f32); + +impl ColorComponents { + /// Apply a function to each of the 3 components of the color. + #[must_use] + pub fn map(self, f: impl Fn(f32) -> f32) -> Self { + Self(f(self.0), f(self.1), f(self.2)) + } +} + +impl std::ops::Mul for ColorComponents { + type Output = Self; + + fn mul(self, rhs: Self) -> Self::Output { + Self(self.0 * rhs.0, self.1 * rhs.1, self.2 * rhs.2) + } +} + +impl std::ops::Div for ColorComponents { + type Output = Self; + + fn div(self, rhs: Self) -> Self::Output { + Self(self.0 / rhs.0, self.1 / rhs.1, self.2 / rhs.2) + } +} + +/// A color space representation in the CSS specification. +/// +/// https://drafts.csswg.org/css-color-4/#typedef-color-space +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ColorSpace { + /// A color specified in the sRGB color space with either the rgb/rgba(..) + /// functions or the newer color(srgb ..) function. If the color(..) + /// function is used, the AS_COLOR_FUNCTION flag will be set. Examples: + /// "color(srgb 0.691 0.139 0.259)", "rgb(176, 35, 66)" + Srgb = 0, + /// A color specified in the Hsl notation in the sRGB color space, e.g. + /// "hsl(289.18 93.136% 65.531%)" + /// https://drafts.csswg.org/css-color-4/#the-hsl-notation + Hsl, + /// A color specified in the Hwb notation in the sRGB color space, e.g. + /// "hwb(740deg 20% 30%)" + /// https://drafts.csswg.org/css-color-4/#the-hwb-notation + Hwb, + /// A color specified in the Lab color format, e.g. + /// "lab(29.2345% 39.3825 20.0664)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors + Lab, + /// A color specified in the Lch color format, e.g. + /// "lch(29.2345% 44.2 27)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors + Lch, + /// A color specified in the Oklab color format, e.g. + /// "oklab(40.101% 0.1147 0.0453)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lab-colors + Oklab, + /// A color specified in the Oklch color format, e.g. + /// "oklch(40.101% 0.12332 21.555)". + /// https://w3c.github.io/csswg-drafts/css-color-4/#lch-colors + Oklch, + /// A color specified with the color(..) function and the "srgb-linear" + /// color space, e.g. "color(srgb-linear 0.435 0.017 0.055)". + SrgbLinear, + /// A color specified with the color(..) function and the "display-p3" + /// color space, e.g. "color(display-p3 0.84 0.19 0.72)". + DisplayP3, + /// A color specified with the color(..) function and the "a98-rgb" color + /// space, e.g. "color(a98-rgb 0.44091 0.49971 0.37408)". + A98Rgb, + /// A color specified with the color(..) function and the "prophoto-rgb" + /// color space, e.g. "color(prophoto-rgb 0.36589 0.41717 0.31333)". + ProphotoRgb, + /// A color specified with the color(..) function and the "rec2020" color + /// space, e.g. "color(rec2020 0.42210 0.47580 0.35605)". + Rec2020, + /// A color specified with the color(..) function and the "xyz-d50" color + /// space, e.g. "color(xyz-d50 0.2005 0.14089 0.4472)". + XyzD50, + /// A color specified with the color(..) function and the "xyz-d65" or "xyz" + /// color space, e.g. "color(xyz-d65 0.21661 0.14602 0.59452)". + /// NOTE: https://drafts.csswg.org/css-color-4/#resolving-color-function-values + /// specifies that `xyz` is an alias for the `xyz-d65` color space. + #[parse(aliases = "xyz")] + XyzD65, +} + +impl ColorSpace { + /// Returns whether this is a `<rectangular-color-space>`. + #[inline] + pub fn is_rectangular(&self) -> bool { + !self.is_polar() + } + + /// Returns whether this is a `<polar-color-space>`. + #[inline] + pub fn is_polar(&self) -> bool { + matches!(self, Self::Hsl | Self::Hwb | Self::Lch | Self::Oklch) + } + + /// Returns true if the color has RGB or XYZ components. + #[inline] + pub fn is_rgb_or_xyz_like(&self) -> bool { + match self { + Self::Srgb | + Self::SrgbLinear | + Self::DisplayP3 | + Self::A98Rgb | + Self::ProphotoRgb | + Self::Rec2020 | + Self::XyzD50 | + Self::XyzD65 => true, + _ => false, + } + } + + /// Returns an index of the hue component in the color space, otherwise + /// `None`. + #[inline] + pub fn hue_index(&self) -> Option<usize> { + match self { + Self::Hsl | Self::Hwb => Some(0), + Self::Lch | Self::Oklch => Some(2), + + _ => { + debug_assert!(!self.is_polar()); + None + }, + } + } +} + +/// Flags used when serializing colors. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct ColorFlags(u8); +bitflags! { + impl ColorFlags : u8 { + /// Whether the 1st color component is `none`. + const C0_IS_NONE = 1 << 0; + /// Whether the 2nd color component is `none`. + const C1_IS_NONE = 1 << 1; + /// Whether the 3rd color component is `none`. + const C2_IS_NONE = 1 << 2; + /// Whether the alpha component is `none`. + const ALPHA_IS_NONE = 1 << 3; + /// Marks that this color is in the legacy color format. This flag is + /// only valid for the `Srgb` color space. + const IS_LEGACY_SRGB = 1 << 4; + } +} + +/// An absolutely specified color, using either rgb(), rgba(), lab(), lch(), +/// oklab(), oklch() or color(). +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct AbsoluteColor { + /// The 3 components that make up colors in any color space. + pub components: ColorComponents, + /// The alpha component of the color. + pub alpha: f32, + /// The current color space that the components represent. + pub color_space: ColorSpace, + /// Extra flags used durring serialization of this color. + pub flags: ColorFlags, +} + +/// Given an [`AbsoluteColor`], return the 4 float components as the type given, +/// e.g.: +/// +/// ```rust +/// let srgb = AbsoluteColor::new(ColorSpace::Srgb, 1.0, 0.0, 0.0, 0.0); +/// let floats = color_components_as!(&srgb, [f32; 4]); // [1.0, 0.0, 0.0, 0.0] +/// ``` +macro_rules! color_components_as { + ($c:expr, $t:ty) => {{ + // This macro is not an inline function, because we can't use the + // generic type ($t) in a constant expression as per: + // https://github.com/rust-lang/rust/issues/76560 + const_assert_eq!(std::mem::size_of::<$t>(), std::mem::size_of::<[f32; 4]>()); + const_assert_eq!(std::mem::align_of::<$t>(), std::mem::align_of::<[f32; 4]>()); + const_assert!(std::mem::size_of::<AbsoluteColor>() >= std::mem::size_of::<$t>()); + const_assert_eq!( + std::mem::align_of::<AbsoluteColor>(), + std::mem::align_of::<$t>() + ); + + std::mem::transmute::<&ColorComponents, &$t>(&$c.components) + }}; +} + +/// Holds details about each component passed into creating a new [`AbsoluteColor`]. +pub struct ComponentDetails { + value: f32, + is_none: bool, +} + +impl From<f32> for ComponentDetails { + fn from(value: f32) -> Self { + Self { + value, + is_none: false, + } + } +} + +impl From<u8> for ComponentDetails { + fn from(value: u8) -> Self { + Self { + value: value as f32 / 255.0, + is_none: false, + } + } +} + +impl From<Option<f32>> for ComponentDetails { + fn from(value: Option<f32>) -> Self { + if let Some(value) = value { + Self { + value, + is_none: false, + } + } else { + Self { + value: 0.0, + is_none: true, + } + } + } +} + +impl AbsoluteColor { + /// A fully transparent color in the legacy syntax. + pub const TRANSPARENT_BLACK: Self = Self { + components: ColorComponents(0.0, 0.0, 0.0), + alpha: 0.0, + color_space: ColorSpace::Srgb, + flags: ColorFlags::IS_LEGACY_SRGB, + }; + + /// An opaque black color in the legacy syntax. + pub const BLACK: Self = Self { + components: ColorComponents(0.0, 0.0, 0.0), + alpha: 1.0, + color_space: ColorSpace::Srgb, + flags: ColorFlags::IS_LEGACY_SRGB, + }; + + /// An opaque white color in the legacy syntax. + pub const WHITE: Self = Self { + components: ColorComponents(1.0, 1.0, 1.0), + alpha: 1.0, + color_space: ColorSpace::Srgb, + flags: ColorFlags::IS_LEGACY_SRGB, + }; + + /// Create a new [`AbsoluteColor`] with the given [`ColorSpace`] and + /// components. + pub fn new( + color_space: ColorSpace, + c1: impl Into<ComponentDetails>, + c2: impl Into<ComponentDetails>, + c3: impl Into<ComponentDetails>, + alpha: impl Into<ComponentDetails>, + ) -> Self { + let mut flags = ColorFlags::empty(); + + macro_rules! cd { + ($c:expr,$flag:expr) => {{ + let component_details = $c.into(); + if component_details.is_none { + flags |= $flag; + } + component_details.value + }}; + } + + let mut components = ColorComponents( + cd!(c1, ColorFlags::C0_IS_NONE), + cd!(c2, ColorFlags::C1_IS_NONE), + cd!(c3, ColorFlags::C2_IS_NONE), + ); + + let alpha = cd!(alpha, ColorFlags::ALPHA_IS_NONE); + + // Lightness for Lab and Lch is clamped to [0..100]. + if matches!(color_space, ColorSpace::Lab | ColorSpace::Lch) { + components.0 = components.0.clamp(0.0, 100.0); + } + + // Lightness for Oklab and Oklch is clamped to [0..1]. + if matches!(color_space, ColorSpace::Oklab | ColorSpace::Oklch) { + components.0 = components.0.clamp(0.0, 1.0); + } + + // Chroma must not be less than 0. + if matches!(color_space, ColorSpace::Lch | ColorSpace::Oklch) { + components.1 = components.1.max(0.0); + } + + // Alpha is always clamped to [0..1]. + let alpha = alpha.clamp(0.0, 1.0); + + Self { + components, + alpha, + color_space, + flags, + } + } + + /// Convert this color into the sRGB color space and set it to the legacy + /// syntax. + #[inline] + #[must_use] + pub fn into_srgb_legacy(self) -> Self { + let mut result = if !matches!(self.color_space, ColorSpace::Srgb) { + self.to_color_space(ColorSpace::Srgb) + } else { + self + }; + + // Explicitly set the flags to IS_LEGACY_SRGB only to clear out the + // *_IS_NONE flags, because the legacy syntax doesn't allow "none". + result.flags = ColorFlags::IS_LEGACY_SRGB; + + result + } + + /// Create a new [`AbsoluteColor`] from rgba legacy syntax values in the sRGB color space. + pub fn srgb_legacy(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + let mut result = Self::new(ColorSpace::Srgb, red, green, blue, alpha); + result.flags = ColorFlags::IS_LEGACY_SRGB; + result + } + + /// Return all the components of the color in an array. (Includes alpha) + #[inline] + pub fn raw_components(&self) -> &[f32; 4] { + unsafe { color_components_as!(self, [f32; 4]) } + } + + /// Returns true if this color is in the legacy color syntax. + #[inline] + pub fn is_legacy_syntax(&self) -> bool { + // rgb(), rgba(), hsl(), hsla(), hwb(), hwba() + match self.color_space { + ColorSpace::Srgb => self.flags.contains(ColorFlags::IS_LEGACY_SRGB), + ColorSpace::Hsl | ColorSpace::Hwb => true, + _ => false, + } + } + + /// Returns true if this color is fully transparent. + #[inline] + pub fn is_transparent(&self) -> bool { + self.flags.contains(ColorFlags::ALPHA_IS_NONE) || self.alpha == 0.0 + } + + /// Return an optional first component. + #[inline] + pub fn c0(&self) -> Option<f32> { + if self.flags.contains(ColorFlags::C0_IS_NONE) { + None + } else { + Some(self.components.0) + } + } + + /// Return an optional second component. + #[inline] + pub fn c1(&self) -> Option<f32> { + if self.flags.contains(ColorFlags::C1_IS_NONE) { + None + } else { + Some(self.components.1) + } + } + + /// Return an optional second component. + #[inline] + pub fn c2(&self) -> Option<f32> { + if self.flags.contains(ColorFlags::C2_IS_NONE) { + None + } else { + Some(self.components.2) + } + } + + /// Return an optional alpha component. + #[inline] + pub fn alpha(&self) -> Option<f32> { + if self.flags.contains(ColorFlags::ALPHA_IS_NONE) { + None + } else { + Some(self.alpha) + } + } + + /// Convert this color to the specified color space. + pub fn to_color_space(&self, color_space: ColorSpace) -> Self { + use ColorSpace::*; + + if self.color_space == color_space { + return self.clone(); + } + + // Conversion functions doesn't handle NAN component values, so they are + // converted to 0.0. They do however need to know if a component is + // missing, so we use NAN as the marker for that. + macro_rules! missing_to_nan { + ($c:expr) => {{ + if let Some(v) = $c { + crate::values::normalize(v) + } else { + f32::NAN + } + }}; + } + + let components = ColorComponents( + missing_to_nan!(self.c0()), + missing_to_nan!(self.c1()), + missing_to_nan!(self.c2()), + ); + + let result = match (self.color_space, color_space) { + // We have simplified conversions that do not need to convert to XYZ + // first. This improves performance, because it skips at least 2 + // matrix multiplications and reduces float rounding errors. + (Srgb, Hsl) => convert::rgb_to_hsl(&components), + (Srgb, Hwb) => convert::rgb_to_hwb(&components), + (Hsl, Srgb) => convert::hsl_to_rgb(&components), + (Hwb, Srgb) => convert::hwb_to_rgb(&components), + (Lab, Lch) | (Oklab, Oklch) => convert::orthogonal_to_polar(&components), + (Lch, Lab) | (Oklch, Oklab) => convert::polar_to_orthogonal(&components), + + // All other conversions need to convert to XYZ first. + _ => { + let (xyz, white_point) = match self.color_space { + Lab => convert::to_xyz::<convert::Lab>(&components), + Lch => convert::to_xyz::<convert::Lch>(&components), + Oklab => convert::to_xyz::<convert::Oklab>(&components), + Oklch => convert::to_xyz::<convert::Oklch>(&components), + Srgb => convert::to_xyz::<convert::Srgb>(&components), + Hsl => convert::to_xyz::<convert::Hsl>(&components), + Hwb => convert::to_xyz::<convert::Hwb>(&components), + SrgbLinear => convert::to_xyz::<convert::SrgbLinear>(&components), + DisplayP3 => convert::to_xyz::<convert::DisplayP3>(&components), + A98Rgb => convert::to_xyz::<convert::A98Rgb>(&components), + ProphotoRgb => convert::to_xyz::<convert::ProphotoRgb>(&components), + Rec2020 => convert::to_xyz::<convert::Rec2020>(&components), + XyzD50 => convert::to_xyz::<convert::XyzD50>(&components), + XyzD65 => convert::to_xyz::<convert::XyzD65>(&components), + }; + + match color_space { + Lab => convert::from_xyz::<convert::Lab>(&xyz, white_point), + Lch => convert::from_xyz::<convert::Lch>(&xyz, white_point), + Oklab => convert::from_xyz::<convert::Oklab>(&xyz, white_point), + Oklch => convert::from_xyz::<convert::Oklch>(&xyz, white_point), + Srgb => convert::from_xyz::<convert::Srgb>(&xyz, white_point), + Hsl => convert::from_xyz::<convert::Hsl>(&xyz, white_point), + Hwb => convert::from_xyz::<convert::Hwb>(&xyz, white_point), + SrgbLinear => convert::from_xyz::<convert::SrgbLinear>(&xyz, white_point), + DisplayP3 => convert::from_xyz::<convert::DisplayP3>(&xyz, white_point), + A98Rgb => convert::from_xyz::<convert::A98Rgb>(&xyz, white_point), + ProphotoRgb => convert::from_xyz::<convert::ProphotoRgb>(&xyz, white_point), + Rec2020 => convert::from_xyz::<convert::Rec2020>(&xyz, white_point), + XyzD50 => convert::from_xyz::<convert::XyzD50>(&xyz, white_point), + XyzD65 => convert::from_xyz::<convert::XyzD65>(&xyz, white_point), + } + }, + }; + + // A NAN value coming from a conversion function means the the component + // is missing, so we convert it to None. + macro_rules! nan_to_missing { + ($v:expr) => {{ + if $v.is_nan() { + None + } else { + Some($v) + } + }}; + } + + Self::new( + color_space, + nan_to_missing!(result.0), + nan_to_missing!(result.1), + nan_to_missing!(result.2), + self.alpha(), + ) + } +} + +impl From<PredefinedColorSpace> for ColorSpace { + fn from(value: PredefinedColorSpace) -> Self { + match value { + PredefinedColorSpace::Srgb => ColorSpace::Srgb, + PredefinedColorSpace::SrgbLinear => ColorSpace::SrgbLinear, + PredefinedColorSpace::DisplayP3 => ColorSpace::DisplayP3, + PredefinedColorSpace::A98Rgb => ColorSpace::A98Rgb, + PredefinedColorSpace::ProphotoRgb => ColorSpace::ProphotoRgb, + PredefinedColorSpace::Rec2020 => ColorSpace::Rec2020, + PredefinedColorSpace::XyzD50 => ColorSpace::XyzD50, + PredefinedColorSpace::XyzD65 => ColorSpace::XyzD65, + } + } +} + +impl ToCss for AbsoluteColor { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match self.color_space { + ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => { + // The "none" keyword is not supported in the rgb/rgba legacy syntax. + cssparser::ToCss::to_css( + &parsing::RgbaLegacy::from_floats( + self.components.0, + self.components.1, + self.components.2, + self.alpha, + ), + dest, + ) + }, + ColorSpace::Hsl | ColorSpace::Hwb => self.into_srgb_legacy().to_css(dest), + ColorSpace::Lab => cssparser::ToCss::to_css( + &parsing::Lab::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + ColorSpace::Lch => cssparser::ToCss::to_css( + &parsing::Lch::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + ColorSpace::Oklab => cssparser::ToCss::to_css( + &parsing::Oklab::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + ColorSpace::Oklch => cssparser::ToCss::to_css( + &parsing::Oklch::new(self.c0(), self.c1(), self.c2(), self.alpha()), + dest, + ), + _ => { + let color_space = match self.color_space { + ColorSpace::Srgb => { + debug_assert!( + !self.flags.contains(ColorFlags::IS_LEGACY_SRGB), + "legacy srgb is not a color function" + ); + PredefinedColorSpace::Srgb + }, + ColorSpace::SrgbLinear => PredefinedColorSpace::SrgbLinear, + ColorSpace::DisplayP3 => PredefinedColorSpace::DisplayP3, + ColorSpace::A98Rgb => PredefinedColorSpace::A98Rgb, + ColorSpace::ProphotoRgb => PredefinedColorSpace::ProphotoRgb, + ColorSpace::Rec2020 => PredefinedColorSpace::Rec2020, + ColorSpace::XyzD50 => PredefinedColorSpace::XyzD50, + ColorSpace::XyzD65 => PredefinedColorSpace::XyzD65, + + _ => { + unreachable!("other color spaces do not support color() syntax") + }, + }; + + let color_function = parsing::ColorFunction { + color_space, + c1: self.c0(), + c2: self.c1(), + c3: self.c2(), + alpha: self.alpha(), + }; + let color = parsing::Color::ColorFunction(color_function); + cssparser::ToCss::to_css(&color, dest) + }, + } + } +} diff --git a/servo/components/style/color/parsing.rs b/servo/components/style/color/parsing.rs new file mode 100644 index 0000000000..f60b44c5b6 --- /dev/null +++ b/servo/components/style/color/parsing.rs @@ -0,0 +1,1246 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#![deny(missing_docs)] + +//! Fairly complete css-color implementation. +//! Relative colors, color-mix, system colors, and other such things require better calc() support +//! and integration. + +use super::{ + convert::{hsl_to_rgb, hwb_to_rgb, normalize_hue}, + ColorComponents, +}; +use crate::values::normalize; +use cssparser::color::{ + clamp_floor_256_f32, clamp_unit_f32, parse_hash_color, serialize_color_alpha, + PredefinedColorSpace, OPAQUE, +}; +use cssparser::{match_ignore_ascii_case, CowRcStr, ParseError, Parser, ToCss, Token}; +#[cfg(feature = "serde")] +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::f32::consts::PI; +use std::fmt; +use std::str::FromStr; + +/// Return the named color with the given name. +/// +/// Matching is case-insensitive in the ASCII range. +/// CSS escaping (if relevant) should be resolved before calling this function. +/// (For example, the value of an `Ident` token is fine.) +#[inline] +pub fn parse_color_keyword<Output>(ident: &str) -> Result<Output, ()> +where + Output: FromParsedColor, +{ + Ok(match_ignore_ascii_case! { ident , + "transparent" => Output::from_rgba(0, 0, 0, 0.0), + "currentcolor" => Output::from_current_color(), + _ => { + let (r, g, b) = cssparser::color::parse_named_color(ident)?; + Output::from_rgba(r, g, b, OPAQUE) + } + }) +} + +/// Parse a CSS color using the specified [`ColorParser`] and return a new color +/// value on success. +pub fn parse_color_with<'i, 't, P>( + color_parser: &P, + input: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Hash(ref value) | Token::IDHash(ref value) => { + parse_hash_color(value.as_bytes()).map(|(r, g, b, a)| P::Output::from_rgba(r, g, b, a)) + }, + Token::Ident(ref value) => parse_color_keyword(value), + Token::Function(ref name) => { + let name = name.clone(); + return input.parse_nested_block(|arguments| { + parse_color_function(color_parser, name, arguments) + }); + }, + _ => Err(()), + } + .map_err(|()| location.new_unexpected_token_error(token.clone())) +} + +/// Parse one of the color functions: rgba(), lab(), color(), etc. +#[inline] +fn parse_color_function<'i, 't, P>( + color_parser: &P, + name: CowRcStr<'i>, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let color = match_ignore_ascii_case! { &name, + "rgb" | "rgba" => parse_rgb(color_parser, arguments), + + "hsl" | "hsla" => parse_hsl(color_parser, arguments), + + "hwb" => parse_hwb(color_parser, arguments), + + // for L: 0% = 0.0, 100% = 100.0 + // for a and b: -100% = -125, 100% = 125 + "lab" => parse_lab_like(color_parser, arguments, 100.0, 125.0, P::Output::from_lab), + + // for L: 0% = 0.0, 100% = 100.0 + // for C: 0% = 0, 100% = 150 + "lch" => parse_lch_like(color_parser, arguments, 100.0, 150.0, P::Output::from_lch), + + // for L: 0% = 0.0, 100% = 1.0 + // for a and b: -100% = -0.4, 100% = 0.4 + "oklab" => parse_lab_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklab), + + // for L: 0% = 0.0, 100% = 1.0 + // for C: 0% = 0.0 100% = 0.4 + "oklch" => parse_lch_like(color_parser, arguments, 1.0, 0.4, P::Output::from_oklch), + + "color" => parse_color_with_color_space(color_parser, arguments), + + _ => return Err(arguments.new_unexpected_token_error(Token::Ident(name))), + }?; + + arguments.expect_exhausted()?; + + Ok(color) +} + +/// Parse the alpha component by itself from either number or percentage, +/// clipping the result to [0.0..1.0]. +#[inline] +fn parse_alpha_component<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<f32, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + // Percent reference range for alpha: 0% = 0.0, 100% = 1.0 + let alpha = color_parser + .parse_number_or_percentage(arguments)? + .to_number(1.0); + Ok(normalize(alpha).clamp(0.0, OPAQUE)) +} + +fn parse_legacy_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<f32, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + Ok(if !arguments.is_exhausted() { + arguments.expect_comma()?; + parse_alpha_component(color_parser, arguments)? + } else { + OPAQUE + }) +} + +fn parse_modern_alpha<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<Option<f32>, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + if !arguments.is_exhausted() { + arguments.expect_delim('/')?; + parse_none_or(arguments, |p| parse_alpha_component(color_parser, p)) + } else { + Ok(Some(OPAQUE)) + } +} + +#[inline] +fn parse_rgb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let maybe_red = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))?; + + // If the first component is not "none" and is followed by a comma, then we + // are parsing the legacy syntax. + let is_legacy_syntax = maybe_red.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let (red, green, blue, alpha) = if is_legacy_syntax { + let (red, green, blue) = match maybe_red.unwrap() { + NumberOrPercentage::Number { value } => { + let red = clamp_floor_256_f32(value); + let green = clamp_floor_256_f32(color_parser.parse_number(arguments)?); + arguments.expect_comma()?; + let blue = clamp_floor_256_f32(color_parser.parse_number(arguments)?); + (red, green, blue) + }, + NumberOrPercentage::Percentage { unit_value } => { + let red = clamp_unit_f32(unit_value); + let green = clamp_unit_f32(color_parser.parse_percentage(arguments)?); + arguments.expect_comma()?; + let blue = clamp_unit_f32(color_parser.parse_percentage(arguments)?); + (red, green, blue) + }, + }; + + let alpha = parse_legacy_alpha(color_parser, arguments)?; + + (red, green, blue, alpha) + } else { + #[inline] + fn get_component_value(c: Option<NumberOrPercentage>) -> u8 { + c.map(|c| match c { + NumberOrPercentage::Number { value } => clamp_floor_256_f32(value), + NumberOrPercentage::Percentage { unit_value } => clamp_unit_f32(unit_value), + }) + .unwrap_or(0) + } + + let red = get_component_value(maybe_red); + + let green = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + let blue = get_component_value(parse_none_or(arguments, |p| { + color_parser.parse_number_or_percentage(p) + })?); + + let alpha = parse_modern_alpha(color_parser, arguments)?.unwrap_or(0.0); + + (red, green, blue, alpha) + }; + + Ok(P::Output::from_rgba(red, green, blue, alpha)) +} + +/// Parses hsl syntax. +/// +/// <https://drafts.csswg.org/css-color/#the-hsl-notation> +#[inline] +fn parse_hsl<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + // Percent reference range for S and L: 0% = 0.0, 100% = 100.0 + const LIGHTNESS_RANGE: f32 = 100.0; + const SATURATION_RANGE: f32 = 100.0; + + let maybe_hue = parse_none_or(arguments, |p| color_parser.parse_angle_or_number(p))?; + + // If the hue is not "none" and is followed by a comma, then we are parsing + // the legacy syntax. + let is_legacy_syntax = maybe_hue.is_some() && arguments.try_parse(|p| p.expect_comma()).is_ok(); + + let saturation: Option<f32>; + let lightness: Option<f32>; + + let alpha = if is_legacy_syntax { + saturation = Some(color_parser.parse_percentage(arguments)? * SATURATION_RANGE); + arguments.expect_comma()?; + lightness = Some(color_parser.parse_percentage(arguments)? * LIGHTNESS_RANGE); + Some(parse_legacy_alpha(color_parser, arguments)?) + } else { + saturation = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))? + .map(|v| v.to_number(SATURATION_RANGE)); + lightness = parse_none_or(arguments, |p| color_parser.parse_number_or_percentage(p))? + .map(|v| v.to_number(LIGHTNESS_RANGE)); + parse_modern_alpha(color_parser, arguments)? + }; + + let hue = maybe_hue.map(|h| normalize_hue(h.degrees())); + let saturation = saturation.map(|s| s.clamp(0.0, SATURATION_RANGE)); + let lightness = lightness.map(|l| l.clamp(0.0, LIGHTNESS_RANGE)); + + Ok(P::Output::from_hsl(hue, saturation, lightness, alpha)) +} + +/// Parses hwb syntax. +/// +/// <https://drafts.csswg.org/css-color/#the-hbw-notation> +#[inline] +fn parse_hwb<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + // Percent reference range for W and B: 0% = 0.0, 100% = 100.0 + const WHITENESS_RANGE: f32 = 100.0; + const BLACKNESS_RANGE: f32 = 100.0; + + let (hue, whiteness, blackness, alpha) = parse_components( + color_parser, + arguments, + P::parse_angle_or_number, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let hue = hue.map(|h| normalize_hue(h.degrees())); + let whiteness = whiteness.map(|w| w.to_number(WHITENESS_RANGE).clamp(0.0, WHITENESS_RANGE)); + let blackness = blackness.map(|b| b.to_number(BLACKNESS_RANGE).clamp(0.0, BLACKNESS_RANGE)); + + Ok(P::Output::from_hwb(hue, whiteness, blackness, alpha)) +} + +type IntoColorFn<Output> = + fn(l: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) -> Output; + +#[inline] +fn parse_lab_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + a_b_range: f32, + into_color: IntoColorFn<P::Output>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let (lightness, a, b, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let lightness = lightness.map(|l| l.to_number(lightness_range)); + let a = a.map(|a| a.to_number(a_b_range)); + let b = b.map(|b| b.to_number(a_b_range)); + + Ok(into_color(lightness, a, b, alpha)) +} + +#[inline] +fn parse_lch_like<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, + lightness_range: f32, + chroma_range: f32, + into_color: IntoColorFn<P::Output>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let (lightness, chroma, hue, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_angle_or_number, + )?; + + let lightness = lightness.map(|l| l.to_number(lightness_range)); + let chroma = chroma.map(|c| c.to_number(chroma_range)); + let hue = hue.map(|h| normalize_hue(h.degrees())); + + Ok(into_color(lightness, chroma, hue, alpha)) +} + +/// Parse the color() function. +#[inline] +fn parse_color_with_color_space<'i, 't, P>( + color_parser: &P, + arguments: &mut Parser<'i, 't>, +) -> Result<P::Output, ParseError<'i, P::Error>> +where + P: ColorParser<'i>, +{ + let color_space = { + let location = arguments.current_source_location(); + + let ident = arguments.expect_ident()?; + PredefinedColorSpace::from_str(ident) + .map_err(|_| location.new_unexpected_token_error(Token::Ident(ident.clone())))? + }; + + let (c1, c2, c3, alpha) = parse_components( + color_parser, + arguments, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + P::parse_number_or_percentage, + )?; + + let c1 = c1.map(|c| c.to_number(1.0)); + let c2 = c2.map(|c| c.to_number(1.0)); + let c3 = c3.map(|c| c.to_number(1.0)); + + Ok(P::Output::from_color_function( + color_space, + c1, + c2, + c3, + alpha, + )) +} + +type ComponentParseResult<'i, R1, R2, R3, Error> = + Result<(Option<R1>, Option<R2>, Option<R3>, Option<f32>), ParseError<'i, Error>>; + +/// Parse the color components and alpha with the modern [color-4] syntax. +pub fn parse_components<'i, 't, P, F1, F2, F3, R1, R2, R3>( + color_parser: &P, + input: &mut Parser<'i, 't>, + f1: F1, + f2: F2, + f3: F3, +) -> ComponentParseResult<'i, R1, R2, R3, P::Error> +where + P: ColorParser<'i>, + F1: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R1, ParseError<'i, P::Error>>, + F2: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R2, ParseError<'i, P::Error>>, + F3: FnOnce(&P, &mut Parser<'i, 't>) -> Result<R3, ParseError<'i, P::Error>>, +{ + let r1 = parse_none_or(input, |p| f1(color_parser, p))?; + let r2 = parse_none_or(input, |p| f2(color_parser, p))?; + let r3 = parse_none_or(input, |p| f3(color_parser, p))?; + + let alpha = parse_modern_alpha(color_parser, input)?; + + Ok((r1, r2, r3, alpha)) +} + +fn parse_none_or<'i, 't, F, T, E>(input: &mut Parser<'i, 't>, thing: F) -> Result<Option<T>, E> +where + F: FnOnce(&mut Parser<'i, 't>) -> Result<T, E>, +{ + match input.try_parse(|p| p.expect_ident_matching("none")) { + Ok(_) => Ok(None), + Err(_) => Ok(Some(thing(input)?)), + } +} + +/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and +/// floating point values. +struct ModernComponent<'a>(&'a Option<f32>); + +impl<'a> ToCss for ModernComponent<'a> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + if let Some(value) = self.0 { + if value.is_finite() { + value.to_css(dest) + } else if value.is_nan() { + dest.write_str("calc(NaN)") + } else { + debug_assert!(value.is_infinite()); + if value.is_sign_negative() { + dest.write_str("calc(-infinity)") + } else { + dest.write_str("calc(infinity)") + } + } + } else { + dest.write_str("none") + } + } +} + +/// A color with red, green, blue, and alpha components, in a byte each. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct RgbaLegacy { + /// The red component. + pub red: u8, + /// The green component. + pub green: u8, + /// The blue component. + pub blue: u8, + /// The alpha component. + pub alpha: f32, +} + +impl RgbaLegacy { + /// Constructs a new RGBA value from float components. It expects the red, + /// green, blue and alpha channels in that order, and all values will be + /// clamped to the 0.0 ... 1.0 range. + #[inline] + pub fn from_floats(red: f32, green: f32, blue: f32, alpha: f32) -> Self { + Self::new( + clamp_unit_f32(red), + clamp_unit_f32(green), + clamp_unit_f32(blue), + alpha.clamp(0.0, OPAQUE), + ) + } + + /// Same thing, but with `u8` values instead of floats in the 0 to 1 range. + #[inline] + pub const fn new(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + Self { + red, + green, + blue, + alpha, + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for RgbaLegacy { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.red, self.green, self.blue, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for RgbaLegacy { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (r, g, b, a) = Deserialize::deserialize(deserializer)?; + Ok(RgbaLegacy::new(r, g, b, a)) + } +} + +impl ToCss for RgbaLegacy { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + let has_alpha = self.alpha != OPAQUE; + + dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?; + self.red.to_css(dest)?; + dest.write_str(", ")?; + self.green.to_css(dest)?; + dest.write_str(", ")?; + self.blue.to_css(dest)?; + + // Legacy syntax does not allow none components. + serialize_color_alpha(dest, Some(self.alpha), true)?; + + dest.write_char(')') + } +} + +/// Color specified by hue, saturation and lightness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hsl { + /// The hue component. + pub hue: Option<f32>, + /// The saturation component. + pub saturation: Option<f32>, + /// The lightness component. + pub lightness: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +impl Hsl { + /// Construct a new HSL color from it's components. + pub fn new( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + hue, + saturation, + lightness, + alpha, + } + } +} + +impl ToCss for Hsl { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HSL serializes to RGB, so we have to convert it. + let ColorComponents(red, green, blue) = hsl_to_rgb(&ColorComponents( + self.hue.unwrap_or(0.0) / 360.0, + self.saturation.unwrap_or(0.0), + self.lightness.unwrap_or(0.0), + )); + + RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hsl { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.hue, self.saturation, self.lightness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hsl { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } +} + +/// Color specified by hue, whiteness and blackness components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Hwb { + /// The hue component. + pub hue: Option<f32>, + /// The whiteness component. + pub whiteness: Option<f32>, + /// The blackness component. + pub blackness: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +impl Hwb { + /// Construct a new HWB color from it's components. + pub fn new( + hue: Option<f32>, + whiteness: Option<f32>, + blackness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + hue, + whiteness, + blackness, + alpha, + } + } +} + +impl ToCss for Hwb { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + // HWB serializes to RGB, so we have to convert it. + let ColorComponents(red, green, blue) = hwb_to_rgb(&ColorComponents( + self.hue.unwrap_or(0.0) / 360.0, + self.whiteness.unwrap_or(0.0), + self.blackness.unwrap_or(0.0), + )); + + RgbaLegacy::from_floats(red, green, blue, self.alpha.unwrap_or(OPAQUE)).to_css(dest) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Hwb { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.hue, self.whiteness, self.blackness, self.alpha).serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de> Deserialize<'de> for Hwb { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, whiteness, blackness, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, whiteness, blackness, alpha)) + } +} + +// NOTE: LAB and OKLAB is not declared inside the [impl_lab_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lab { + /// The lightness component. + pub lightness: Option<f32>, + /// The a-axis component. + pub a: Option<f32>, + /// The b-axis component. + pub b: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +/// Color specified by lightness, a- and b-axis components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklab { + /// The lightness component. + pub lightness: Option<f32>, + /// The a-axis component. + pub a: Option<f32>, + /// The b-axis component. + pub b: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +macro_rules! impl_lab_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new Lab color format with lightness, a, b and alpha components. + pub fn new( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + lightness, + a, + b, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.lightness, self.a, self.b, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, a, b, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, a, b, alpha)) + } + } + + impl ToCss for $cls { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + ModernComponent(&self.lightness).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.a).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.b).to_css(dest)?; + serialize_color_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lab_like!(Lab, "lab"); +impl_lab_like!(Oklab, "oklab"); + +// NOTE: LCH and OKLCH is not declared inside the [impl_lch_like] macro, +// because it causes cbindgen to ignore them. + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Lch { + /// The lightness component. + pub lightness: Option<f32>, + /// The chroma component. + pub chroma: Option<f32>, + /// The hue component. + pub hue: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +/// Color specified by lightness, chroma and hue components. +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct Oklch { + /// The lightness component. + pub lightness: Option<f32>, + /// The chroma component. + pub chroma: Option<f32>, + /// The hue component. + pub hue: Option<f32>, + /// The alpha component. + pub alpha: Option<f32>, +} + +macro_rules! impl_lch_like { + ($cls:ident, $fname:literal) => { + impl $cls { + /// Construct a new color with lightness, chroma and hue components. + pub fn new( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + lightness, + chroma, + hue, + alpha, + } + } + } + + #[cfg(feature = "serde")] + impl Serialize for $cls { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + (self.lightness, self.chroma, self.hue, self.alpha).serialize(serializer) + } + } + + #[cfg(feature = "serde")] + impl<'de> Deserialize<'de> for $cls { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let (lightness, chroma, hue, alpha) = Deserialize::deserialize(deserializer)?; + Ok(Self::new(lightness, chroma, hue, alpha)) + } + } + + impl ToCss for $cls { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str($fname)?; + dest.write_str("(")?; + ModernComponent(&self.lightness).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.chroma).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.hue).to_css(dest)?; + serialize_color_alpha(dest, self.alpha, false)?; + dest.write_char(')') + } + } + }; +} + +impl_lch_like!(Lch, "lch"); +impl_lch_like!(Oklch, "oklch"); + +/// A color specified by the color() function. +/// <https://drafts.csswg.org/css-color-4/#color-function> +#[derive(Clone, Copy, PartialEq, Debug)] +pub struct ColorFunction { + /// The color space for this color. + pub color_space: PredefinedColorSpace, + /// The first component of the color. Either red or x. + pub c1: Option<f32>, + /// The second component of the color. Either green or y. + pub c2: Option<f32>, + /// The third component of the color. Either blue or z. + pub c3: Option<f32>, + /// The alpha component of the color. + pub alpha: Option<f32>, +} + +impl ColorFunction { + /// Construct a new color function definition with the given color space and + /// color components. + pub fn new( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Self { + color_space, + c1, + c2, + c3, + alpha, + } + } +} + +impl ToCss for ColorFunction { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str("color(")?; + self.color_space.to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c1).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c2).to_css(dest)?; + dest.write_char(' ')?; + ModernComponent(&self.c3).to_css(dest)?; + + serialize_color_alpha(dest, self.alpha, false)?; + + dest.write_char(')') + } +} + +/// Describes one of the value <color> values according to the CSS +/// specification. +/// +/// Most components are `Option<_>`, so when the value is `None`, that component +/// serializes to the "none" keyword. +/// +/// <https://drafts.csswg.org/css-color-4/#color-type> +#[derive(Clone, Copy, PartialEq, Debug)] +pub enum Color { + /// The 'currentcolor' keyword. + CurrentColor, + /// Specify sRGB colors directly by their red/green/blue/alpha chanels. + Rgba(RgbaLegacy), + /// Specifies a color in sRGB using hue, saturation and lightness components. + Hsl(Hsl), + /// Specifies a color in sRGB using hue, whiteness and blackness components. + Hwb(Hwb), + /// Specifies a CIELAB color by CIE Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the CIE LAB + /// rectangular coordinate model. + Lab(Lab), + /// Specifies a CIELAB color by CIE Lightness, Chroma, and hue using the + /// CIE LCH cylindrical coordinate model. + Lch(Lch), + /// Specifies an Oklab color by Oklab Lightness and its a- and b-axis hue + /// coordinates (red/green-ness, and yellow/blue-ness) using the Oklab + /// rectangular coordinate model. + Oklab(Oklab), + /// Specifies an Oklab color by Oklab Lightness, Chroma, and hue using + /// the OKLCH cylindrical coordinate model. + Oklch(Oklch), + /// Specifies a color in a predefined color space. + ColorFunction(ColorFunction), +} + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Color::CurrentColor => dest.write_str("currentcolor"), + Color::Rgba(rgba) => rgba.to_css(dest), + Color::Hsl(hsl) => hsl.to_css(dest), + Color::Hwb(hwb) => hwb.to_css(dest), + Color::Lab(lab) => lab.to_css(dest), + Color::Lch(lch) => lch.to_css(dest), + Color::Oklab(lab) => lab.to_css(dest), + Color::Oklch(lch) => lch.to_css(dest), + Color::ColorFunction(color_function) => color_function.to_css(dest), + } + } +} + +/// Either a number or a percentage. +pub enum NumberOrPercentage { + /// `<number>`. + Number { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `<percentage>` + Percentage { + /// The value as a float, divided by 100 so that the nominal range is + /// 0.0 to 1.0. + unit_value: f32, + }, +} + +impl NumberOrPercentage { + /// Return the value as a number. Percentages will be adjusted to the range + /// [0..percent_basis]. + pub fn to_number(&self, percentage_basis: f32) -> f32 { + match *self { + Self::Number { value } => value, + Self::Percentage { unit_value } => unit_value * percentage_basis, + } + } +} + +/// Either an angle or a number. +pub enum AngleOrNumber { + /// `<number>`. + Number { + /// The numeric value parsed, as a float. + value: f32, + }, + /// `<angle>` + Angle { + /// The value as a number of degrees. + degrees: f32, + }, +} + +impl AngleOrNumber { + /// Return the angle in degrees. `AngleOrNumber::Number` is returned as + /// degrees, because it is the canonical unit. + pub fn degrees(&self) -> f32 { + match *self { + AngleOrNumber::Number { value } => value, + AngleOrNumber::Angle { degrees } => degrees, + } + } +} + +/// A trait that can be used to hook into how `cssparser` parses color +/// components, with the intention of implementing more complicated behavior. +/// +/// For example, this is used by Servo to support calc() in color. +pub trait ColorParser<'i> { + /// The type that the parser will construct on a successful parse. + type Output: FromParsedColor; + + /// A custom error type that can be returned from the parsing functions. + type Error: 'i; + + /// Parse an `<angle>` or `<number>`. + /// + /// Returns the result in degrees. + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<AngleOrNumber, ParseError<'i, Self::Error>> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => AngleOrNumber::Number { value }, + Token::Dimension { + value: v, ref unit, .. + } => { + let degrees = match_ignore_ascii_case! { unit, + "deg" => v, + "grad" => v * 360. / 400., + "rad" => v * 360. / (2. * PI), + "turn" => v * 360., + _ => { + return Err(location.new_unexpected_token_error(Token::Ident(unit.clone()))) + } + }; + + AngleOrNumber::Angle { degrees } + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } + + /// Parse a `<percentage>` value. + /// + /// Returns the result in a number from 0.0 to 1.0. + fn parse_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<f32, ParseError<'i, Self::Error>> { + input.expect_percentage().map_err(From::from) + } + + /// Parse a `<number>` value. + fn parse_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<f32, ParseError<'i, Self::Error>> { + input.expect_number().map_err(From::from) + } + + /// Parse a `<number>` value or a `<percentage>` value. + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<NumberOrPercentage, ParseError<'i, Self::Error>> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Number { value, .. } => NumberOrPercentage::Number { value }, + Token::Percentage { unit_value, .. } => NumberOrPercentage::Percentage { unit_value }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } +} + +/// Default implementation of a [`ColorParser`] +pub struct DefaultColorParser; + +impl<'i> ColorParser<'i> for DefaultColorParser { + type Output = Color; + type Error = (); +} + +impl Color { + /// Parse a <color> value, per CSS Color Module Level 3. + /// + /// FIXME(#2) Deprecated CSS2 System Colors are not supported yet. + pub fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Color, ParseError<'i, ()>> { + parse_color_with(&DefaultColorParser, input) + } +} + +/// This trait is used by the [`ColorParser`] to construct colors of any type. +pub trait FromParsedColor { + /// Construct a new color from the CSS `currentcolor` keyword. + fn from_current_color() -> Self; + + /// Construct a new color from red, green, blue and alpha components. + fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self; + + /// Construct a new color from hue, saturation, lightness and alpha components. + fn from_hsl( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from hue, blackness, whiteness and alpha components. + fn from_hwb( + hue: Option<f32>, + whiteness: Option<f32>, + blackness: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from the `lab` notation. + fn from_lab(lightness: Option<f32>, a: Option<f32>, b: Option<f32>, alpha: Option<f32>) + -> Self; + + /// Construct a new color from the `lch` notation. + fn from_lch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from the `oklab` notation. + fn from_oklab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color from the `oklch` notation. + fn from_oklch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self; + + /// Construct a new color with a predefined color space. + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self; +} + +impl FromParsedColor for Color { + #[inline] + fn from_current_color() -> Self { + Color::CurrentColor + } + + #[inline] + fn from_rgba(red: u8, green: u8, blue: u8, alpha: f32) -> Self { + Color::Rgba(RgbaLegacy::new(red, green, blue, alpha)) + } + + fn from_hsl( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Hsl(Hsl::new(hue, saturation, lightness, alpha)) + } + + fn from_hwb( + hue: Option<f32>, + blackness: Option<f32>, + whiteness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Hwb(Hwb::new(hue, blackness, whiteness, alpha)) + } + + #[inline] + fn from_lab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Lab(Lab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_lch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Lch(Lch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_oklab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Oklab(Oklab::new(lightness, a, b, alpha)) + } + + #[inline] + fn from_oklch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::Oklch(Oklch::new(lightness, chroma, hue, alpha)) + } + + #[inline] + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self { + Color::ColorFunction(ColorFunction::new(color_space, c1, c2, c3, alpha)) + } +} diff --git a/servo/components/style/context.rs b/servo/components/style/context.rs new file mode 100644 index 0000000000..a2c020475b --- /dev/null +++ b/servo/components/style/context.rs @@ -0,0 +1,698 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The context within which style is calculated. + +#[cfg(feature = "servo")] +use crate::animation::DocumentAnimationSet; +use crate::bloom::StyleBloom; +use crate::computed_value_flags::ComputedValueFlags; +use crate::data::{EagerPseudoStyles, ElementData}; +use crate::dom::{SendElement, TElement}; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs; +use crate::parallel::{STACK_SAFETY_MARGIN_KB, STYLE_THREAD_STACK_SIZE_KB}; +use crate::properties::ComputedValues; +#[cfg(feature = "servo")] +use crate::properties::PropertyId; +use crate::rule_cache::RuleCache; +use crate::rule_tree::StrongRuleNode; +use crate::selector_parser::{SnapshotMap, EAGER_PSEUDO_COUNT}; +use crate::shared_lock::StylesheetGuards; +use crate::sharing::StyleSharingCache; +use crate::stylist::Stylist; +use crate::thread_state::{self, ThreadState}; +use crate::traversal::DomTraversal; +use crate::traversal_flags::TraversalFlags; +use app_units::Au; +use euclid::default::Size2D; +use euclid::Scale; +#[cfg(feature = "servo")] +use fxhash::FxHashMap; +use selectors::context::SelectorCaches; +#[cfg(feature = "gecko")] +use servo_arc::Arc; +#[cfg(feature = "servo")] +use servo_atoms::Atom; +use std::fmt; +use std::ops; +use style_traits::CSSPixel; +use style_traits::DevicePixel; +#[cfg(feature = "servo")] +use style_traits::SpeculativePainter; +use time; + +pub use selectors::matching::QuirksMode; + +/// A global options structure for the style system. We use this instead of +/// opts to abstract across Gecko and Servo. +#[derive(Clone)] +pub struct StyleSystemOptions { + /// Whether the style sharing cache is disabled. + pub disable_style_sharing_cache: bool, + /// Whether we should dump statistics about the style system. + pub dump_style_statistics: bool, + /// The minimum number of elements that must be traversed to trigger a dump + /// of style statistics. + pub style_statistics_threshold: usize, +} + +#[cfg(feature = "gecko")] +fn get_env_bool(name: &str) -> bool { + use std::env; + match env::var(name) { + Ok(s) => !s.is_empty(), + Err(_) => false, + } +} + +const DEFAULT_STATISTICS_THRESHOLD: usize = 50; + +#[cfg(feature = "gecko")] +fn get_env_usize(name: &str) -> Option<usize> { + use std::env; + env::var(name).ok().map(|s| { + s.parse::<usize>() + .expect("Couldn't parse environmental variable as usize") + }) +} + +/// A global variable holding the state of +/// `StyleSystemOptions::default().disable_style_sharing_cache`. +/// See [#22854](https://github.com/servo/servo/issues/22854). +#[cfg(feature = "servo")] +pub static DEFAULT_DISABLE_STYLE_SHARING_CACHE: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +/// A global variable holding the state of +/// `StyleSystemOptions::default().dump_style_statistics`. +/// See [#22854](https://github.com/servo/servo/issues/22854). +#[cfg(feature = "servo")] +pub static DEFAULT_DUMP_STYLE_STATISTICS: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +impl Default for StyleSystemOptions { + #[cfg(feature = "servo")] + fn default() -> Self { + use std::sync::atomic::Ordering; + + StyleSystemOptions { + disable_style_sharing_cache: DEFAULT_DISABLE_STYLE_SHARING_CACHE + .load(Ordering::Relaxed), + dump_style_statistics: DEFAULT_DUMP_STYLE_STATISTICS.load(Ordering::Relaxed), + style_statistics_threshold: DEFAULT_STATISTICS_THRESHOLD, + } + } + + #[cfg(feature = "gecko")] + fn default() -> Self { + StyleSystemOptions { + disable_style_sharing_cache: get_env_bool("DISABLE_STYLE_SHARING_CACHE"), + dump_style_statistics: get_env_bool("DUMP_STYLE_STATISTICS"), + style_statistics_threshold: get_env_usize("STYLE_STATISTICS_THRESHOLD") + .unwrap_or(DEFAULT_STATISTICS_THRESHOLD), + } + } +} + +/// A shared style context. +/// +/// There's exactly one of these during a given restyle traversal, and it's +/// shared among the worker threads. +pub struct SharedStyleContext<'a> { + /// The CSS selector stylist. + pub stylist: &'a Stylist, + + /// Whether visited styles are enabled. + /// + /// They may be disabled when Gecko's pref layout.css.visited_links_enabled + /// is false, or when in private browsing mode. + pub visited_styles_enabled: bool, + + /// Configuration options. + pub options: StyleSystemOptions, + + /// Guards for pre-acquired locks + pub guards: StylesheetGuards<'a>, + + /// The current time for transitions and animations. This is needed to ensure + /// a consistent sampling time and also to adjust the time for testing. + pub current_time_for_animations: f64, + + /// Flags controlling how we traverse the tree. + pub traversal_flags: TraversalFlags, + + /// A map with our snapshots in order to handle restyle hints. + pub snapshot_map: &'a SnapshotMap, + + /// The state of all animations for our styled elements. + #[cfg(feature = "servo")] + pub animations: DocumentAnimationSet, + + /// Paint worklets + #[cfg(feature = "servo")] + pub registered_speculative_painters: &'a dyn RegisteredSpeculativePainters, +} + +impl<'a> SharedStyleContext<'a> { + /// Return a suitable viewport size in order to be used for viewport units. + pub fn viewport_size(&self) -> Size2D<Au> { + self.stylist.device().au_viewport_size() + } + + /// The device pixel ratio + pub fn device_pixel_ratio(&self) -> Scale<f32, CSSPixel, DevicePixel> { + self.stylist.device().device_pixel_ratio() + } + + /// The quirks mode of the document. + pub fn quirks_mode(&self) -> QuirksMode { + self.stylist.quirks_mode() + } +} + +/// The structure holds various intermediate inputs that are eventually used by +/// by the cascade. +/// +/// The matching and cascading process stores them in this format temporarily +/// within the `CurrentElementInfo`. At the end of the cascade, they are folded +/// down into the main `ComputedValues` to reduce memory usage per element while +/// still remaining accessible. +#[derive(Clone, Debug, Default)] +pub struct CascadeInputs { + /// The rule node representing the ordered list of rules matched for this + /// node. + pub rules: Option<StrongRuleNode>, + + /// The rule node representing the ordered list of rules matched for this + /// node if visited, only computed if there's a relevant link for this + /// element. A element's "relevant link" is the element being matched if it + /// is a link or the nearest ancestor link. + pub visited_rules: Option<StrongRuleNode>, + + /// The set of flags from container queries that we need for invalidation. + pub flags: ComputedValueFlags, +} + +impl CascadeInputs { + /// Construct inputs from previous cascade results, if any. + pub fn new_from_style(style: &ComputedValues) -> Self { + Self { + rules: style.rules.clone(), + visited_rules: style.visited_style().and_then(|v| v.rules.clone()), + flags: style.flags.for_cascade_inputs(), + } + } +} + +/// A list of cascade inputs for eagerly-cascaded pseudo-elements. +/// The list is stored inline. +#[derive(Debug)] +pub struct EagerPseudoCascadeInputs(Option<[Option<CascadeInputs>; EAGER_PSEUDO_COUNT]>); + +// Manually implement `Clone` here because the derived impl of `Clone` for +// array types assumes the value inside is `Copy`. +impl Clone for EagerPseudoCascadeInputs { + fn clone(&self) -> Self { + if self.0.is_none() { + return EagerPseudoCascadeInputs(None); + } + let self_inputs = self.0.as_ref().unwrap(); + let mut inputs: [Option<CascadeInputs>; EAGER_PSEUDO_COUNT] = Default::default(); + for i in 0..EAGER_PSEUDO_COUNT { + inputs[i] = self_inputs[i].clone(); + } + EagerPseudoCascadeInputs(Some(inputs)) + } +} + +impl EagerPseudoCascadeInputs { + /// Construct inputs from previous cascade results, if any. + fn new_from_style(styles: &EagerPseudoStyles) -> Self { + EagerPseudoCascadeInputs(styles.as_optional_array().map(|styles| { + let mut inputs: [Option<CascadeInputs>; EAGER_PSEUDO_COUNT] = Default::default(); + for i in 0..EAGER_PSEUDO_COUNT { + inputs[i] = styles[i].as_ref().map(|s| CascadeInputs::new_from_style(s)); + } + inputs + })) + } + + /// Returns the list of rules, if they exist. + pub fn into_array(self) -> Option<[Option<CascadeInputs>; EAGER_PSEUDO_COUNT]> { + self.0 + } +} + +/// The cascade inputs associated with a node, including those for any +/// pseudo-elements. +/// +/// The matching and cascading process stores them in this format temporarily +/// within the `CurrentElementInfo`. At the end of the cascade, they are folded +/// down into the main `ComputedValues` to reduce memory usage per element while +/// still remaining accessible. +#[derive(Clone, Debug)] +pub struct ElementCascadeInputs { + /// The element's cascade inputs. + pub primary: CascadeInputs, + /// A list of the inputs for the element's eagerly-cascaded pseudo-elements. + pub pseudos: EagerPseudoCascadeInputs, +} + +impl ElementCascadeInputs { + /// Construct inputs from previous cascade results, if any. + #[inline] + pub fn new_from_element_data(data: &ElementData) -> Self { + debug_assert!(data.has_styles()); + ElementCascadeInputs { + primary: CascadeInputs::new_from_style(data.styles.primary()), + pseudos: EagerPseudoCascadeInputs::new_from_style(&data.styles.pseudos), + } + } +} + +/// Statistics gathered during the traversal. We gather statistics on each +/// thread and then combine them after the threads join via the Add +/// implementation below. +#[derive(AddAssign, Clone, Default)] +pub struct PerThreadTraversalStatistics { + /// The total number of elements traversed. + pub elements_traversed: u32, + /// The number of elements where has_styles() went from false to true. + pub elements_styled: u32, + /// The number of elements for which we performed selector matching. + pub elements_matched: u32, + /// The number of cache hits from the StyleSharingCache. + pub styles_shared: u32, + /// The number of styles reused via rule node comparison from the + /// StyleSharingCache. + pub styles_reused: u32, +} + +/// Statistics gathered during the traversal plus some information from +/// other sources including stylist. +#[derive(Default)] +pub struct TraversalStatistics { + /// Aggregated statistics gathered during the traversal. + pub aggregated: PerThreadTraversalStatistics, + /// The number of selectors in the stylist. + pub selectors: u32, + /// The number of revalidation selectors. + pub revalidation_selectors: u32, + /// The number of state/attr dependencies in the dependency set. + pub dependency_selectors: u32, + /// The number of declarations in the stylist. + pub declarations: u32, + /// The number of times the stylist was rebuilt. + pub stylist_rebuilds: u32, + /// Time spent in the traversal, in milliseconds. + pub traversal_time_ms: f64, + /// Whether this was a parallel traversal. + pub is_parallel: bool, + /// Whether this is a "large" traversal. + pub is_large: bool, +} + +/// Format the statistics in a way that the performance test harness understands. +/// See https://bugzilla.mozilla.org/show_bug.cgi?id=1331856#c2 +impl fmt::Display for TraversalStatistics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + debug_assert!( + self.traversal_time_ms != 0.0, + "should have set traversal time" + ); + writeln!(f, "[PERF] perf block start")?; + writeln!( + f, + "[PERF],traversal,{}", + if self.is_parallel { + "parallel" + } else { + "sequential" + } + )?; + writeln!( + f, + "[PERF],elements_traversed,{}", + self.aggregated.elements_traversed + )?; + writeln!( + f, + "[PERF],elements_styled,{}", + self.aggregated.elements_styled + )?; + writeln!( + f, + "[PERF],elements_matched,{}", + self.aggregated.elements_matched + )?; + writeln!(f, "[PERF],styles_shared,{}", self.aggregated.styles_shared)?; + writeln!(f, "[PERF],styles_reused,{}", self.aggregated.styles_reused)?; + writeln!(f, "[PERF],selectors,{}", self.selectors)?; + writeln!( + f, + "[PERF],revalidation_selectors,{}", + self.revalidation_selectors + )?; + writeln!( + f, + "[PERF],dependency_selectors,{}", + self.dependency_selectors + )?; + writeln!(f, "[PERF],declarations,{}", self.declarations)?; + writeln!(f, "[PERF],stylist_rebuilds,{}", self.stylist_rebuilds)?; + writeln!(f, "[PERF],traversal_time_ms,{}", self.traversal_time_ms)?; + writeln!(f, "[PERF] perf block end") + } +} + +impl TraversalStatistics { + /// Generate complete traversal statistics. + /// + /// The traversal time is computed given the start time in seconds. + pub fn new<E, D>( + aggregated: PerThreadTraversalStatistics, + traversal: &D, + parallel: bool, + start: f64, + ) -> TraversalStatistics + where + E: TElement, + D: DomTraversal<E>, + { + let threshold = traversal + .shared_context() + .options + .style_statistics_threshold; + let stylist = traversal.shared_context().stylist; + let is_large = aggregated.elements_traversed as usize >= threshold; + TraversalStatistics { + aggregated, + selectors: stylist.num_selectors() as u32, + revalidation_selectors: stylist.num_revalidation_selectors() as u32, + dependency_selectors: stylist.num_invalidations() as u32, + declarations: stylist.num_declarations() as u32, + stylist_rebuilds: stylist.num_rebuilds() as u32, + traversal_time_ms: (time::precise_time_s() - start) * 1000.0, + is_parallel: parallel, + is_large, + } + } +} + +#[cfg(feature = "gecko")] +bitflags! { + /// Represents which tasks are performed in a SequentialTask of + /// UpdateAnimations which is a result of normal restyle. + pub struct UpdateAnimationsTasks: u8 { + /// Update CSS Animations. + const CSS_ANIMATIONS = structs::UpdateAnimationsTasks_CSSAnimations; + /// Update CSS Transitions. + const CSS_TRANSITIONS = structs::UpdateAnimationsTasks_CSSTransitions; + /// Update effect properties. + const EFFECT_PROPERTIES = structs::UpdateAnimationsTasks_EffectProperties; + /// Update animation cacade results for animations running on the compositor. + const CASCADE_RESULTS = structs::UpdateAnimationsTasks_CascadeResults; + /// Display property was changed from none. + /// Script animations keep alive on display:none elements, so we need to trigger + /// the second animation restyles for the script animations in the case where + /// the display property was changed from 'none' to others. + const DISPLAY_CHANGED_FROM_NONE = structs::UpdateAnimationsTasks_DisplayChangedFromNone; + /// Update CSS named scroll progress timelines. + const SCROLL_TIMELINES = structs::UpdateAnimationsTasks_ScrollTimelines; + /// Update CSS named view progress timelines. + const VIEW_TIMELINES = structs::UpdateAnimationsTasks_ViewTimelines; + } +} + +#[cfg(feature = "gecko")] +bitflags! { + /// Represents which tasks are performed in a SequentialTask as a result of + /// animation-only restyle. + pub struct PostAnimationTasks: u8 { + /// Display property was changed from none in animation-only restyle so + /// that we need to resolve styles for descendants in a subsequent + /// normal restyle. + const DISPLAY_CHANGED_FROM_NONE_FOR_SMIL = 0x01; + } +} + +/// A task to be run in sequential mode on the parent (non-worker) thread. This +/// is used by the style system to queue up work which is not safe to do during +/// the parallel traversal. +pub enum SequentialTask<E: TElement> { + /// Entry to avoid an unused type parameter error on servo. + Unused(SendElement<E>), + + /// Performs one of a number of possible tasks related to updating + /// animations based on the |tasks| field. These include updating CSS + /// animations/transitions that changed as part of the non-animation style + /// traversal, and updating the computed effect properties. + #[cfg(feature = "gecko")] + UpdateAnimations { + /// The target element or pseudo-element. + el: SendElement<E>, + /// The before-change style for transitions. We use before-change style + /// as the initial value of its Keyframe. Required if |tasks| includes + /// CSSTransitions. + before_change_style: Option<Arc<ComputedValues>>, + /// The tasks which are performed in this SequentialTask. + tasks: UpdateAnimationsTasks, + }, + + /// Performs one of a number of possible tasks as a result of animation-only + /// restyle. + /// + /// Currently we do only process for resolving descendant elements that were + /// display:none subtree for SMIL animation. + #[cfg(feature = "gecko")] + PostAnimation { + /// The target element. + el: SendElement<E>, + /// The tasks which are performed in this SequentialTask. + tasks: PostAnimationTasks, + }, +} + +impl<E: TElement> SequentialTask<E> { + /// Executes this task. + pub fn execute(self) { + use self::SequentialTask::*; + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + match self { + Unused(_) => unreachable!(), + #[cfg(feature = "gecko")] + UpdateAnimations { + el, + before_change_style, + tasks, + } => { + el.update_animations(before_change_style, tasks); + }, + #[cfg(feature = "gecko")] + PostAnimation { el, tasks } => { + el.process_post_animation(tasks); + }, + } + } + + /// Creates a task to update various animation-related state on a given + /// (pseudo-)element. + #[cfg(feature = "gecko")] + pub fn update_animations( + el: E, + before_change_style: Option<Arc<ComputedValues>>, + tasks: UpdateAnimationsTasks, + ) -> Self { + use self::SequentialTask::*; + UpdateAnimations { + el: unsafe { SendElement::new(el) }, + before_change_style, + tasks, + } + } + + /// Creates a task to do post-process for a given element as a result of + /// animation-only restyle. + #[cfg(feature = "gecko")] + pub fn process_post_animation(el: E, tasks: PostAnimationTasks) -> Self { + use self::SequentialTask::*; + PostAnimation { + el: unsafe { SendElement::new(el) }, + tasks, + } + } +} + +/// A list of SequentialTasks that get executed on Drop. +pub struct SequentialTaskList<E>(Vec<SequentialTask<E>>) +where + E: TElement; + +impl<E> ops::Deref for SequentialTaskList<E> +where + E: TElement, +{ + type Target = Vec<SequentialTask<E>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<E> ops::DerefMut for SequentialTaskList<E> +where + E: TElement, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl<E> Drop for SequentialTaskList<E> +where + E: TElement, +{ + fn drop(&mut self) { + debug_assert_eq!(thread_state::get(), ThreadState::LAYOUT); + for task in self.0.drain(..) { + task.execute() + } + } +} + +/// A helper type for stack limit checking. This assumes that stacks grow +/// down, which is true for all non-ancient CPU architectures. +pub struct StackLimitChecker { + lower_limit: usize, +} + +impl StackLimitChecker { + /// Create a new limit checker, for this thread, allowing further use + /// of up to |stack_size| bytes beyond (below) the current stack pointer. + #[inline(never)] + pub fn new(stack_size_limit: usize) -> Self { + StackLimitChecker { + lower_limit: StackLimitChecker::get_sp() - stack_size_limit, + } + } + + /// Checks whether the previously stored stack limit has now been exceeded. + #[inline(never)] + pub fn limit_exceeded(&self) -> bool { + let curr_sp = StackLimitChecker::get_sp(); + + // Do some sanity-checking to ensure that our invariants hold, even in + // the case where we've exceeded the soft limit. + // + // The correctness of depends on the assumption that no stack wraps + // around the end of the address space. + if cfg!(debug_assertions) { + // Compute the actual bottom of the stack by subtracting our safety + // margin from our soft limit. Note that this will be slightly below + // the actual bottom of the stack, because there are a few initial + // frames on the stack before we do the measurement that computes + // the limit. + let stack_bottom = self.lower_limit - STACK_SAFETY_MARGIN_KB * 1024; + + // The bottom of the stack should be below the current sp. If it + // isn't, that means we've either waited too long to check the limit + // and burned through our safety margin (in which case we probably + // would have segfaulted by now), or we're using a limit computed for + // a different thread. + debug_assert!(stack_bottom < curr_sp); + + // Compute the distance between the current sp and the bottom of + // the stack, and compare it against the current stack. It should be + // no further from us than the total stack size. We allow some slop + // to handle the fact that stack_bottom is a bit further than the + // bottom of the stack, as discussed above. + let distance_to_stack_bottom = curr_sp - stack_bottom; + let max_allowable_distance = (STYLE_THREAD_STACK_SIZE_KB + 10) * 1024; + debug_assert!(distance_to_stack_bottom <= max_allowable_distance); + } + + // The actual bounds check. + curr_sp <= self.lower_limit + } + + // Technically, rustc can optimize this away, but shouldn't for now. + // We should fix this once black_box is stable. + #[inline(always)] + fn get_sp() -> usize { + let mut foo: usize = 42; + (&mut foo as *mut usize) as usize + } +} + +/// A thread-local style context. +/// +/// This context contains data that needs to be used during restyling, but is +/// not required to be unique among worker threads, so we create one per worker +/// thread in order to be able to mutate it without locking. +pub struct ThreadLocalStyleContext<E: TElement> { + /// A cache to share style among siblings. + pub sharing_cache: StyleSharingCache<E>, + /// A cache from matched properties to elements that match those. + pub rule_cache: RuleCache, + /// The bloom filter used to fast-reject selector-matching. + pub bloom_filter: StyleBloom<E>, + /// A set of tasks to be run (on the parent thread) in sequential mode after + /// the rest of the styling is complete. This is useful for + /// infrequently-needed non-threadsafe operations. + /// + /// It's important that goes after the style sharing cache and the bloom + /// filter, to ensure they're dropped before we execute the tasks, which + /// could create another ThreadLocalStyleContext for style computation. + pub tasks: SequentialTaskList<E>, + /// Statistics about the traversal. + pub statistics: PerThreadTraversalStatistics, + /// A checker used to ensure that parallel.rs does not recurse indefinitely + /// even on arbitrarily deep trees. See Gecko bug 1376883. + pub stack_limit_checker: StackLimitChecker, + /// Collection of caches (And cache-likes) for speeding up expensive selector matches. + pub selector_caches: SelectorCaches, +} + +impl<E: TElement> ThreadLocalStyleContext<E> { + /// Creates a new `ThreadLocalStyleContext` + pub fn new() -> Self { + ThreadLocalStyleContext { + sharing_cache: StyleSharingCache::new(), + rule_cache: RuleCache::new(), + bloom_filter: StyleBloom::new(), + tasks: SequentialTaskList(Vec::new()), + statistics: PerThreadTraversalStatistics::default(), + stack_limit_checker: StackLimitChecker::new( + (STYLE_THREAD_STACK_SIZE_KB - STACK_SAFETY_MARGIN_KB) * 1024, + ), + selector_caches: SelectorCaches::default(), + } + } +} + +/// A `StyleContext` is just a simple container for a immutable reference to a +/// shared style context, and a mutable reference to a local one. +pub struct StyleContext<'a, E: TElement + 'a> { + /// The shared style context reference. + pub shared: &'a SharedStyleContext<'a>, + /// The thread-local style context (mutable) reference. + pub thread_local: &'a mut ThreadLocalStyleContext<E>, +} + +/// A registered painter +#[cfg(feature = "servo")] +pub trait RegisteredSpeculativePainter: SpeculativePainter { + /// The name it was registered with + fn name(&self) -> Atom; + /// The properties it was registered with + fn properties(&self) -> &FxHashMap<Atom, PropertyId>; +} + +/// A set of registered painters +#[cfg(feature = "servo")] +pub trait RegisteredSpeculativePainters: Sync { + /// Look up a speculative painter + fn get(&self, name: &Atom) -> Option<&dyn RegisteredSpeculativePainter>; +} diff --git a/servo/components/style/counter_style/mod.rs b/servo/components/style/counter_style/mod.rs new file mode 100644 index 0000000000..fc7a0fb447 --- /dev/null +++ b/servo/components/style/counter_style/mod.rs @@ -0,0 +1,695 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@counter-style`][counter-style] at-rule. +//! +//! [counter-style]: https://drafts.csswg.org/css-counter-styles/ + +use crate::error_reporting::ContextualParseError; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::specified::Integer; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::{ + AtRuleParser, DeclarationParser, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, +}; +use cssparser::{CowRcStr, Parser, SourceLocation, Token}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use std::mem; +use std::num::Wrapping; +use style_traits::{Comma, CssWriter, OneOrMoreSeparated, ParseError}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// Parse a counter style name reference. +/// +/// This allows the reserved counter style names "decimal" and "disc". +pub fn parse_counter_style_name<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<CustomIdent, ParseError<'i>> { + macro_rules! predefined { + ($($name: tt,)+) => {{ + ascii_case_insensitive_phf_map! { + predefined -> Atom = { + $( + $name => atom!($name), + )+ + } + } + + let location = input.current_source_location(); + let ident = input.expect_ident()?; + // This effectively performs case normalization only on predefined names. + if let Some(lower_case) = predefined::get(&ident) { + Ok(CustomIdent(lower_case.clone())) + } else { + // none is always an invalid <counter-style> value. + CustomIdent::from_ident(location, ident, &["none"]) + } + }} + } + include!("predefined.rs") +} + +fn is_valid_name_definition(ident: &CustomIdent) -> bool { + ident.0 != atom!("decimal") && + ident.0 != atom!("disc") && + ident.0 != atom!("circle") && + ident.0 != atom!("square") && + ident.0 != atom!("disclosure-closed") && + ident.0 != atom!("disclosure-open") +} + +/// Parse the prelude of an @counter-style rule +pub fn parse_counter_style_name_definition<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<CustomIdent, ParseError<'i>> { + parse_counter_style_name(input).and_then(|ident| { + if !is_valid_name_definition(&ident) { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(ident) + } + }) +} + +/// Parse the body (inside `{}`) of an @counter-style rule +pub fn parse_counter_style_body<'i, 't>( + name: CustomIdent, + context: &ParserContext, + input: &mut Parser<'i, 't>, + location: SourceLocation, +) -> Result<CounterStyleRuleData, ParseError<'i>> { + let start = input.current_source_location(); + let mut rule = CounterStyleRuleData::empty(name, location); + { + let mut parser = CounterStyleRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration( + slice, error, + ); + context.log_css_error(location, error) + } + } + } + let error = match *rule.resolved_system() { + ref system @ System::Cyclic | + ref system @ System::Fixed { .. } | + ref system @ System::Symbolic | + ref system @ System::Alphabetic | + ref system @ System::Numeric + if rule.symbols.is_none() => + { + let system = system.to_css_string(); + Some(ContextualParseError::InvalidCounterStyleWithoutSymbols( + system, + )) + }, + ref system @ System::Alphabetic | ref system @ System::Numeric + if rule.symbols().unwrap().0.len() < 2 => + { + let system = system.to_css_string(); + Some(ContextualParseError::InvalidCounterStyleNotEnoughSymbols( + system, + )) + }, + System::Additive if rule.additive_symbols.is_none() => { + Some(ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols) + }, + System::Extends(_) if rule.symbols.is_some() => { + Some(ContextualParseError::InvalidCounterStyleExtendsWithSymbols) + }, + System::Extends(_) if rule.additive_symbols.is_some() => { + Some(ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols) + }, + _ => None, + }; + if let Some(error) = error { + context.log_css_error(start, error); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(rule) + } +} + +struct CounterStyleRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + rule: &'a mut CounterStyleRuleData, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for CounterStyleRuleParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} + +macro_rules! checker { + ($self:ident._($value:ident)) => {}; + ($self:ident. $checker:ident($value:ident)) => { + if !$self.$checker(&$value) { + return false; + } + }; +} + +macro_rules! counter_style_descriptors { + ( + $( #[$doc: meta] $name: tt $ident: ident / $setter: ident [$checker: tt]: $ty: ty, )+ + ) => { + /// An @counter-style rule + #[derive(Clone, Debug, ToShmem)] + pub struct CounterStyleRuleData { + name: CustomIdent, + generation: Wrapping<u32>, + $( + #[$doc] + $ident: Option<$ty>, + )+ + /// Line and column of the @counter-style rule source code. + pub source_location: SourceLocation, + } + + impl CounterStyleRuleData { + fn empty(name: CustomIdent, source_location: SourceLocation) -> Self { + CounterStyleRuleData { + name: name, + generation: Wrapping(0), + $( + $ident: None, + )+ + source_location, + } + } + + $( + #[$doc] + pub fn $ident(&self) -> Option<&$ty> { + self.$ident.as_ref() + } + )+ + + $( + #[$doc] + pub fn $setter(&mut self, value: $ty) -> bool { + checker!(self.$checker(value)); + self.$ident = Some(value); + self.generation += Wrapping(1); + true + } + )+ + } + + impl<'a, 'b, 'i> DeclarationParser<'i> for CounterStyleRuleParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name => { + // DeclarationParser also calls parse_entirely so we’d normally not + // need to, but in this case we do because we set the value as a side + // effect rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.rule.$ident = Some(value) + }, + )* + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } + } + + impl ToCssWithGuard for CounterStyleRuleData { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@counter-style ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + $( + if let Some(ref value) = self.$ident { + dest.write_str(concat!($name, ": "))?; + ToCss::to_css(value, &mut CssWriter::new(dest))?; + dest.write_str("; ")?; + } + )+ + dest.write_char('}') + } + } + } +} + +counter_style_descriptors! { + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-system> + "system" system / set_system [check_system]: System, + + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-negative> + "negative" negative / set_negative [_]: Negative, + + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-prefix> + "prefix" prefix / set_prefix [_]: Symbol, + + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-suffix> + "suffix" suffix / set_suffix [_]: Symbol, + + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-range> + "range" range / set_range [_]: CounterRanges, + + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-pad> + "pad" pad / set_pad [_]: Pad, + + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-fallback> + "fallback" fallback / set_fallback [_]: Fallback, + + /// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-symbols> + "symbols" symbols / set_symbols [check_symbols]: Symbols, + + /// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-additive-symbols> + "additive-symbols" additive_symbols / + set_additive_symbols [check_additive_symbols]: AdditiveSymbols, + + /// <https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as> + "speak-as" speak_as / set_speak_as [_]: SpeakAs, +} + +// Implements the special checkers for some setters. +// See <https://drafts.csswg.org/css-counter-styles/#the-csscounterstylerule-interface> +impl CounterStyleRuleData { + /// Check that the system is effectively not changed. Only params + /// of system descriptor is changeable. + fn check_system(&self, value: &System) -> bool { + mem::discriminant(self.resolved_system()) == mem::discriminant(value) + } + + fn check_symbols(&self, value: &Symbols) -> bool { + match *self.resolved_system() { + // These two systems require at least two symbols. + System::Numeric | System::Alphabetic => value.0.len() >= 2, + // No symbols should be set for extends system. + System::Extends(_) => false, + _ => true, + } + } + + fn check_additive_symbols(&self, _value: &AdditiveSymbols) -> bool { + match *self.resolved_system() { + // No additive symbols should be set for extends system. + System::Extends(_) => false, + _ => true, + } + } +} + +impl CounterStyleRuleData { + /// Get the name of the counter style rule. + pub fn name(&self) -> &CustomIdent { + &self.name + } + + /// Set the name of the counter style rule. Caller must ensure that + /// the name is valid. + pub fn set_name(&mut self, name: CustomIdent) { + debug_assert!(is_valid_name_definition(&name)); + self.name = name; + } + + /// Get the current generation of the counter style rule. + pub fn generation(&self) -> u32 { + self.generation.0 + } + + /// Get the system of this counter style rule, default to + /// `symbolic` if not specified. + pub fn resolved_system(&self) -> &System { + match self.system { + Some(ref system) => system, + None => &System::Symbolic, + } + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#counter-style-system> +#[derive(Clone, Debug, ToShmem)] +pub enum System { + /// 'cyclic' + Cyclic, + /// 'numeric' + Numeric, + /// 'alphabetic' + Alphabetic, + /// 'symbolic' + Symbolic, + /// 'additive' + Additive, + /// 'fixed <integer>?' + Fixed { + /// '<integer>?' + first_symbol_value: Option<Integer>, + }, + /// 'extends <counter-style-name>' + Extends(CustomIdent), +} + +impl Parse for System { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + try_match_ident_ignore_ascii_case! { input, + "cyclic" => Ok(System::Cyclic), + "numeric" => Ok(System::Numeric), + "alphabetic" => Ok(System::Alphabetic), + "symbolic" => Ok(System::Symbolic), + "additive" => Ok(System::Additive), + "fixed" => { + let first_symbol_value = input.try_parse(|i| Integer::parse(context, i)).ok(); + Ok(System::Fixed { first_symbol_value }) + }, + "extends" => { + let other = parse_counter_style_name(input)?; + Ok(System::Extends(other)) + }, + } + } +} + +impl ToCss for System { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + System::Cyclic => dest.write_str("cyclic"), + System::Numeric => dest.write_str("numeric"), + System::Alphabetic => dest.write_str("alphabetic"), + System::Symbolic => dest.write_str("symbolic"), + System::Additive => dest.write_str("additive"), + System::Fixed { first_symbol_value } => { + if let Some(value) = first_symbol_value { + dest.write_str("fixed ")?; + value.to_css(dest) + } else { + dest.write_str("fixed") + } + }, + System::Extends(ref other) => { + dest.write_str("extends ")?; + other.to_css(dest) + }, + } + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#typedef-symbol> +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum Symbol { + /// <string> + String(crate::OwnedStr), + /// <custom-ident> + Ident(CustomIdent), + // Not implemented: + // /// <image> + // Image(Image), +} + +impl Parse for Symbol { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::QuotedString(ref s) => Ok(Symbol::String(s.as_ref().to_owned().into())), + Token::Ident(ref s) => Ok(Symbol::Ident(CustomIdent::from_ident(location, s, &[])?)), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Symbol { + /// Returns whether this symbol is allowed in symbols() function. + pub fn is_allowed_in_symbols(&self) -> bool { + match self { + // Identifier is not allowed. + &Symbol::Ident(_) => false, + _ => true, + } + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#counter-style-negative> +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Negative(pub Symbol, pub Option<Symbol>); + +impl Parse for Negative { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Negative( + Symbol::parse(context, input)?, + input.try_parse(|input| Symbol::parse(context, input)).ok(), + )) + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#counter-style-range> +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct CounterRange { + /// The start of the range. + pub start: CounterBound, + /// The end of the range. + pub end: CounterBound, +} + +/// <https://drafts.csswg.org/css-counter-styles/#counter-style-range> +/// +/// Empty represents 'auto' +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct CounterRanges(#[css(iterable, if_empty = "auto")] pub crate::OwnedSlice<CounterRange>); + +/// A bound found in `CounterRanges`. +#[derive(Clone, Copy, Debug, ToCss, ToShmem)] +pub enum CounterBound { + /// An integer bound. + Integer(Integer), + /// The infinite bound. + Infinite, +} + +impl Parse for CounterRanges { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(CounterRanges(Default::default())); + } + + let ranges = input.parse_comma_separated(|input| { + let start = parse_bound(context, input)?; + let end = parse_bound(context, input)?; + if let (CounterBound::Integer(start), CounterBound::Integer(end)) = (start, end) { + if start > end { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + Ok(CounterRange { start, end }) + })?; + + Ok(CounterRanges(ranges.into())) + } +} + +fn parse_bound<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<CounterBound, ParseError<'i>> { + if let Ok(integer) = input.try_parse(|input| Integer::parse(context, input)) { + return Ok(CounterBound::Integer(integer)); + } + input.expect_ident_matching("infinite")?; + Ok(CounterBound::Infinite) +} + +/// <https://drafts.csswg.org/css-counter-styles/#counter-style-pad> +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Pad(pub Integer, pub Symbol); + +impl Parse for Pad { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let pad_with = input.try_parse(|input| Symbol::parse(context, input)); + let min_length = Integer::parse_non_negative(context, input)?; + let pad_with = pad_with.or_else(|_| Symbol::parse(context, input))?; + Ok(Pad(min_length, pad_with)) + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#counter-style-fallback> +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct Fallback(pub CustomIdent); + +impl Parse for Fallback { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Fallback(parse_counter_style_name(input)?)) + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-symbols> +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToCss, ToShmem, +)] +#[repr(C)] +pub struct Symbols(#[css(iterable)] pub crate::OwnedSlice<Symbol>); + +impl Parse for Symbols { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut symbols = Vec::new(); + while let Ok(s) = input.try_parse(|input| Symbol::parse(context, input)) { + symbols.push(s); + } + if symbols.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(Symbols(symbols.into())) + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#descdef-counter-style-additive-symbols> +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct AdditiveSymbols(#[css(iterable)] pub crate::OwnedSlice<AdditiveTuple>); + +impl Parse for AdditiveSymbols { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tuples = Vec::<AdditiveTuple>::parse(context, input)?; + // FIXME maybe? https://github.com/w3c/csswg-drafts/issues/1220 + if tuples + .windows(2) + .any(|window| window[0].weight <= window[1].weight) + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(AdditiveSymbols(tuples.into())) + } +} + +/// <integer> && <symbol> +#[derive(Clone, Debug, ToCss, ToShmem)] +pub struct AdditiveTuple { + /// <integer> + pub weight: Integer, + /// <symbol> + pub symbol: Symbol, +} + +impl OneOrMoreSeparated for AdditiveTuple { + type S = Comma; +} + +impl Parse for AdditiveTuple { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let symbol = input.try_parse(|input| Symbol::parse(context, input)); + let weight = Integer::parse_non_negative(context, input)?; + let symbol = symbol.or_else(|_| Symbol::parse(context, input))?; + Ok(Self { weight, symbol }) + } +} + +/// <https://drafts.csswg.org/css-counter-styles/#counter-style-speak-as> +#[derive(Clone, Debug, ToCss, ToShmem)] +pub enum SpeakAs { + /// auto + Auto, + /// bullets + Bullets, + /// numbers + Numbers, + /// words + Words, + // /// spell-out, not supported, see bug 1024178 + // SpellOut, + /// <counter-style-name> + Other(CustomIdent), +} + +impl Parse for SpeakAs { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut is_spell_out = false; + let result = input.try_parse(|input| { + let ident = input.expect_ident().map_err(|_| ())?; + match_ignore_ascii_case! { &*ident, + "auto" => Ok(SpeakAs::Auto), + "bullets" => Ok(SpeakAs::Bullets), + "numbers" => Ok(SpeakAs::Numbers), + "words" => Ok(SpeakAs::Words), + "spell-out" => { + is_spell_out = true; + Err(()) + }, + _ => Err(()), + } + }); + if is_spell_out { + // spell-out is not supported, but don’t parse it as a <counter-style-name>. + // See bug 1024178. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + result.or_else(|_| Ok(SpeakAs::Other(parse_counter_style_name(input)?))) + } +} diff --git a/servo/components/style/counter_style/predefined.rs b/servo/components/style/counter_style/predefined.rs new file mode 100644 index 0000000000..7243e3b3f3 --- /dev/null +++ b/servo/components/style/counter_style/predefined.rs @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +predefined! { + "decimal", + "decimal-leading-zero", + "arabic-indic", + "armenian", + "upper-armenian", + "lower-armenian", + "bengali", + "cambodian", + "khmer", + "cjk-decimal", + "devanagari", + "georgian", + "gujarati", + "gurmukhi", + "hebrew", + "kannada", + "lao", + "malayalam", + "mongolian", + "myanmar", + "oriya", + "persian", + "lower-roman", + "upper-roman", + "tamil", + "telugu", + "thai", + "tibetan", + "lower-alpha", + "lower-latin", + "upper-alpha", + "upper-latin", + "cjk-earthly-branch", + "cjk-heavenly-stem", + "lower-greek", + "hiragana", + "hiragana-iroha", + "katakana", + "katakana-iroha", + "disc", + "circle", + "square", + "disclosure-open", + "disclosure-closed", + "japanese-informal", + "japanese-formal", + "korean-hangul-formal", + "korean-hanja-informal", + "korean-hanja-formal", + "simp-chinese-informal", + "simp-chinese-formal", + "trad-chinese-informal", + "trad-chinese-formal", + "cjk-ideographic", + "ethiopic-numeric", +} diff --git a/servo/components/style/counter_style/update_predefined.py b/servo/components/style/counter_style/update_predefined.py new file mode 100755 index 0000000000..1523958ff3 --- /dev/null +++ b/servo/components/style/counter_style/update_predefined.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import os.path +import re +import urllib + + +def main(filename): + names = [ + re.search('>([^>]+)(</dfn>|<a class="self-link")', line).group(1) + for line in urllib.urlopen("https://drafts.csswg.org/css-counter-styles/") + if 'data-dfn-for="<counter-style-name>"' in line + or 'data-dfn-for="<counter-style>"' in line + ] + with open(filename, "wb") as f: + f.write( + """\ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +predefined! { +""" + ) + for name in names: + f.write(' "%s",\n' % name) + f.write("}\n") + + +if __name__ == "__main__": + main(os.path.join(os.path.dirname(__file__), "predefined.rs")) diff --git a/servo/components/style/custom_properties.rs b/servo/components/style/custom_properties.rs new file mode 100644 index 0000000000..cb3b9685ae --- /dev/null +++ b/servo/components/style/custom_properties.rs @@ -0,0 +1,1959 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Support for [custom properties for cascading variables][custom]. +//! +//! [custom]: https://drafts.csswg.org/css-variables/ + +use crate::applicable_declarations::CascadePriority; +use crate::custom_properties_map::CustomPropertiesMap; +use crate::media_queries::Device; +use crate::properties::{ + CSSWideKeyword, CustomDeclaration, CustomDeclarationValue, LonghandId, LonghandIdSet, + VariableDeclaration, +}; +use crate::properties_and_values::{ + registry::PropertyRegistrationData, + value::{AllowComputationallyDependent, SpecifiedValue as SpecifiedRegisteredValue}, +}; +use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet}; +use crate::stylesheets::UrlExtraData; +use crate::stylist::Stylist; +use crate::values::computed; +use crate::values::specified::FontRelativeLength; +use crate::Atom; +use cssparser::{ + CowRcStr, Delimiter, Parser, ParserInput, SourcePosition, Token, TokenSerializationType, +}; +use selectors::parser::SelectorParseErrorKind; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::borrow::Cow; +use std::collections::hash_map::Entry; +use std::fmt::{self, Write}; +use std::ops::{Index, IndexMut}; +use std::{cmp, num}; +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. +#[derive(Debug, MallocSizeOf)] +pub struct CssEnvironment; + +type EnvironmentEvaluator = fn(device: &Device, url_data: &UrlExtraData) -> VariableValue; + +struct EnvironmentVariable { + name: Atom, + evaluator: EnvironmentEvaluator, +} + +macro_rules! make_variable { + ($name:expr, $evaluator:expr) => {{ + EnvironmentVariable { + name: $name, + evaluator: $evaluator, + } + }}; +} + +fn get_safearea_inset_top(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().top, url_data) +} + +fn get_safearea_inset_bottom(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().bottom, url_data) +} + +fn get_safearea_inset_left(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().left, url_data) +} + +fn get_safearea_inset_right(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.safe_area_insets().right, url_data) +} + +fn get_content_preferred_color_scheme(device: &Device, url_data: &UrlExtraData) -> VariableValue { + use crate::gecko::media_features::PrefersColorScheme; + let prefers_color_scheme = unsafe { + crate::gecko_bindings::bindings::Gecko_MediaFeatures_PrefersColorScheme( + device.document(), + /* use_content = */ true, + ) + }; + VariableValue::ident( + match prefers_color_scheme { + PrefersColorScheme::Light => "light", + PrefersColorScheme::Dark => "dark", + }, + url_data, + ) +} + +fn get_scrollbar_inline_size(device: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::pixels(device.scrollbar_inline_size().px(), url_data) +} + +static ENVIRONMENT_VARIABLES: [EnvironmentVariable; 4] = [ + make_variable!(atom!("safe-area-inset-top"), get_safearea_inset_top), + make_variable!(atom!("safe-area-inset-bottom"), get_safearea_inset_bottom), + make_variable!(atom!("safe-area-inset-left"), get_safearea_inset_left), + make_variable!(atom!("safe-area-inset-right"), get_safearea_inset_right), +]; + +macro_rules! lnf_int { + ($id:ident) => { + unsafe { + crate::gecko_bindings::bindings::Gecko_GetLookAndFeelInt( + crate::gecko_bindings::bindings::LookAndFeel_IntID::$id as i32, + ) + } + }; +} + +macro_rules! lnf_int_variable { + ($atom:expr, $id:ident, $ctor:ident) => {{ + fn __eval(_: &Device, url_data: &UrlExtraData) -> VariableValue { + VariableValue::$ctor(lnf_int!($id), url_data) + } + make_variable!($atom, __eval) + }}; +} + +static CHROME_ENVIRONMENT_VARIABLES: [EnvironmentVariable; 7] = [ + lnf_int_variable!( + atom!("-moz-gtk-csd-titlebar-radius"), + TitlebarRadius, + int_pixels + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-close-button-position"), + GTKCSDCloseButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-minimize-button-position"), + GTKCSDMinimizeButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-gtk-csd-maximize-button-position"), + GTKCSDMaximizeButtonPosition, + integer + ), + lnf_int_variable!( + atom!("-moz-overlay-scrollbar-fade-duration"), + ScrollbarFadeDuration, + int_ms + ), + make_variable!( + atom!("-moz-content-preferred-color-scheme"), + get_content_preferred_color_scheme + ), + make_variable!(atom!("scrollbar-inline-size"), get_scrollbar_inline_size), +]; + +impl CssEnvironment { + #[inline] + fn get(&self, name: &Atom, device: &Device, url_data: &UrlExtraData) -> Option<VariableValue> { + if let Some(var) = ENVIRONMENT_VARIABLES.iter().find(|var| var.name == *name) { + return Some((var.evaluator)(device, url_data)); + } + if !url_data.chrome_rules_enabled() { + return None; + } + let var = CHROME_ENVIRONMENT_VARIABLES + .iter() + .find(|var| var.name == *name)?; + Some((var.evaluator)(device, url_data)) + } +} + +/// A custom property name is just an `Atom`. +/// +/// Note that this does not include the `--` prefix +pub type Name = Atom; + +/// Parse a custom property name. +/// +/// <https://drafts.csswg.org/css-variables/#typedef-custom-property-name> +pub fn parse_name(s: &str) -> Result<&str, ()> { + if s.starts_with("--") && s.len() > 2 { + Ok(&s[2..]) + } else { + Err(()) + } +} + +/// A value for a custom property is just a set of tokens. +/// +/// We preserve the original CSS for serialization, and also the variable +/// references to other custom property names. +#[derive(Clone, Debug, MallocSizeOf, ToShmem)] +pub struct VariableValue { + /// The raw CSS string. + pub css: String, + + /// The url data of the stylesheet where this value came from. + pub url_data: UrlExtraData, + + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + + /// var(), env(), or non-custom property (e.g. through `em`) references. + references: References, +} + +trivial_to_computed_value!(VariableValue); + +// For all purposes, we want values to be considered equal if their css text is equal. +impl PartialEq for VariableValue { + fn eq(&self, other: &Self) -> bool { + self.css == other.css + } +} + +impl Eq for VariableValue {} + +impl ToCss for SpecifiedValue { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.css) + } +} + +/// A pair of separate CustomPropertiesMaps, split between custom properties +/// that have the inherit flag set and those with the flag unset. +#[repr(C)] +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ComputedCustomProperties { + /// Map for custom properties with inherit flag set, including non-registered + /// ones. + pub inherited: CustomPropertiesMap, + /// Map for custom properties with inherit flag unset. + pub non_inherited: CustomPropertiesMap, +} + +impl ComputedCustomProperties { + /// Return whether the inherited and non_inherited maps are none. + pub fn is_empty(&self) -> bool { + self.inherited.is_empty() && self.non_inherited.is_empty() + } + + /// Return the name and value of the property at specified index, if any. + pub fn property_at(&self, index: usize) -> Option<(&Name, &Option<Arc<VariableValue>>)> { + // Just expose the custom property items from custom_properties.inherited, followed + // by custom property items from custom_properties.non_inherited. + self.inherited + .get_index(index) + .or_else(|| self.non_inherited.get_index(index - self.inherited.len())) + } + + /// Insert a custom property in the corresponding inherited/non_inherited + /// map, depending on whether the inherit flag is set or unset. + fn insert( + &mut self, + registration: &PropertyRegistrationData, + name: &Name, + value: Arc<VariableValue>, + ) { + self.map_mut(registration).insert(name, value) + } + + /// Remove a custom property from the corresponding inherited/non_inherited + /// map, depending on whether the inherit flag is set or unset. + fn remove(&mut self, registration: &PropertyRegistrationData, name: &Name) { + self.map_mut(registration).remove(name); + } + + /// Shrink the capacity of the inherited maps as much as possible. + fn shrink_to_fit(&mut self) { + self.inherited.shrink_to_fit(); + self.non_inherited.shrink_to_fit(); + } + + fn map_mut(&mut self, registration: &PropertyRegistrationData) -> &mut CustomPropertiesMap { + if registration.inherits() { + &mut self.inherited + } else { + &mut self.non_inherited + } + } + + fn get( + &self, + registration: &PropertyRegistrationData, + name: &Name, + ) -> Option<&Arc<VariableValue>> { + if registration.inherits() { + self.inherited.get(name) + } else { + self.non_inherited.get(name) + } + } +} + +/// Both specified and computed values are VariableValues, the difference is +/// whether var() functions are expanded. +pub type SpecifiedValue = VariableValue; +/// Both specified and computed values are VariableValues, the difference is +/// whether var() functions are expanded. +pub type ComputedValue = VariableValue; + +/// Set of flags to non-custom references this custom property makes. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, MallocSizeOf, ToShmem)] +struct NonCustomReferences(u8); + +bitflags! { + impl NonCustomReferences: u8 { + /// At least one custom property depends on font-relative units. + const FONT_UNITS = 1 << 0; + /// At least one custom property depends on root element's font-relative units. + const ROOT_FONT_UNITS = 1 << 1; + /// At least one custom property depends on line height units. + const LH_UNITS = 1 << 2; + /// At least one custom property depends on root element's line height units. + const ROOT_LH_UNITS = 1 << 3; + /// All dependencies not depending on the root element. + const NON_ROOT_DEPENDENCIES = Self::FONT_UNITS.bits() | Self::LH_UNITS.bits(); + /// All dependencies depending on the root element. + const ROOT_DEPENDENCIES = Self::ROOT_FONT_UNITS.bits() | Self::ROOT_LH_UNITS.bits(); + } +} + +impl NonCustomReferences { + fn for_each<F>(&self, mut f: F) + where + F: FnMut(SingleNonCustomReference), + { + for (_, r) in self.iter_names() { + let single = match r { + Self::FONT_UNITS => SingleNonCustomReference::FontUnits, + Self::ROOT_FONT_UNITS => SingleNonCustomReference::RootFontUnits, + Self::LH_UNITS => SingleNonCustomReference::LhUnits, + Self::ROOT_LH_UNITS => SingleNonCustomReference::RootLhUnits, + _ => unreachable!("Unexpected single bit value"), + }; + f(single); + } + } + + fn from_unit(value: &CowRcStr) -> Self { + // For registered properties, any reference to font-relative dimensions + // make it dependent on font-related properties. + // TODO(dshin): When we unit algebra gets implemented and handled - + // Is it valid to say that `calc(1em / 2em * 3px)` triggers this? + if value.eq_ignore_ascii_case(FontRelativeLength::LH) { + return Self::FONT_UNITS | Self::LH_UNITS; + } + if value.eq_ignore_ascii_case(FontRelativeLength::EM) || + value.eq_ignore_ascii_case(FontRelativeLength::EX) || + value.eq_ignore_ascii_case(FontRelativeLength::CAP) || + value.eq_ignore_ascii_case(FontRelativeLength::CH) || + value.eq_ignore_ascii_case(FontRelativeLength::IC) + { + return Self::FONT_UNITS; + } + if value.eq_ignore_ascii_case(FontRelativeLength::RLH) { + return Self::ROOT_FONT_UNITS | Self::ROOT_LH_UNITS; + } + if value.eq_ignore_ascii_case(FontRelativeLength::REM) { + return Self::ROOT_FONT_UNITS; + } + Self::empty() + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum SingleNonCustomReference { + FontUnits = 0, + RootFontUnits, + LhUnits, + RootLhUnits, +} + +struct NonCustomReferenceMap<T>([Option<T>; 4]); + +impl<T> Default for NonCustomReferenceMap<T> { + fn default() -> Self { + NonCustomReferenceMap(Default::default()) + } +} + +impl<T> Index<SingleNonCustomReference> for NonCustomReferenceMap<T> { + type Output = Option<T>; + + fn index(&self, reference: SingleNonCustomReference) -> &Self::Output { + &self.0[reference as usize] + } +} + +impl<T> IndexMut<SingleNonCustomReference> for NonCustomReferenceMap<T> { + fn index_mut(&mut self, reference: SingleNonCustomReference) -> &mut Self::Output { + &mut self.0[reference as usize] + } +} + +/// Whether to defer resolving custom properties referencing font relative units. +#[derive(Clone, Copy, PartialEq, Eq)] +#[allow(missing_docs)] +pub enum DeferFontRelativeCustomPropertyResolution { + Yes, + No, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +struct VariableFallback { + start: num::NonZeroUsize, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +struct VarOrEnvReference { + name: Name, + start: usize, + end: usize, + fallback: Option<VariableFallback>, + prev_token_type: TokenSerializationType, + next_token_type: TokenSerializationType, + is_var: bool, +} + +/// A struct holding information about the external references to that a custom +/// property value may have. +#[derive(Clone, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +struct References { + refs: Vec<VarOrEnvReference>, + non_custom_references: NonCustomReferences, + any_env: bool, + any_var: bool, +} + +impl References { + fn has_references(&self) -> bool { + !self.refs.is_empty() + } + + fn get_non_custom_dependencies(&self, is_root_element: bool) -> NonCustomReferences { + let mask = NonCustomReferences::NON_ROOT_DEPENDENCIES; + let mask = if is_root_element { + mask | NonCustomReferences::ROOT_DEPENDENCIES + } else { + mask + }; + + self.non_custom_references & mask + } +} + +impl VariableValue { + fn empty(url_data: &UrlExtraData) -> Self { + Self { + css: String::new(), + last_token_type: Default::default(), + first_token_type: Default::default(), + url_data: url_data.clone(), + references: Default::default(), + } + } + + /// Create a new custom property without parsing if the CSS is known to be valid and contain no + /// references. + pub fn new( + css: String, + url_data: &UrlExtraData, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + ) -> Self { + Self { + css, + url_data: url_data.clone(), + first_token_type, + last_token_type, + references: Default::default(), + } + } + + fn push<'i>( + &mut self, + css: &str, + css_first_token_type: TokenSerializationType, + css_last_token_type: TokenSerializationType, + ) -> Result<(), ()> { + /// Prevent values from getting terribly big since you can use custom + /// properties exponentially. + /// + /// This number (2MB) is somewhat arbitrary, but silly enough that no + /// reasonable page should hit it. We could limit by number of total + /// substitutions, but that was very easy to work around in practice + /// (just choose a larger initial value and boom). + const MAX_VALUE_LENGTH_IN_BYTES: usize = 2 * 1024 * 1024; + + if self.css.len() + css.len() > MAX_VALUE_LENGTH_IN_BYTES { + return Err(()); + } + + // This happens e.g. between two subsequent var() functions: + // `var(--a)var(--b)`. + // + // In that case, css_*_token_type is nonsensical. + if css.is_empty() { + return Ok(()); + } + + self.first_token_type.set_if_nothing(css_first_token_type); + // If self.first_token_type was nothing, + // self.last_token_type is also nothing and this will be false: + if self + .last_token_type + .needs_separator_when_before(css_first_token_type) + { + self.css.push_str("/**/") + } + self.css.push_str(css); + self.last_token_type = css_last_token_type; + Ok(()) + } + + /// Parse a custom property value. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + url_data: &UrlExtraData, + ) -> Result<Self, ParseError<'i>> { + input.skip_whitespace(); + + let mut references = References::default(); + let mut missing_closing_characters = String::new(); + let start_position = input.position(); + let (first_token_type, last_token_type) = parse_declaration_value( + input, + start_position, + &mut references, + &mut missing_closing_characters, + )?; + let mut css = input.slice_from(start_position).to_owned(); + if !missing_closing_characters.is_empty() { + // Unescaped backslash at EOF in a quoted string is ignored. + if css.ends_with("\\") && + matches!(missing_closing_characters.as_bytes()[0], b'"' | b'\'') + { + css.pop(); + } + css.push_str(&missing_closing_characters); + } + + css.shrink_to_fit(); + references.refs.shrink_to_fit(); + + Ok(Self { + css, + url_data: url_data.clone(), + first_token_type, + last_token_type, + references, + }) + } + + /// Create VariableValue from an int. + fn integer(number: i32, url_data: &UrlExtraData) -> Self { + Self::from_token( + Token::Number { + has_sign: false, + value: number as f32, + int_value: Some(number), + }, + url_data, + ) + } + + /// Create VariableValue from an int. + fn ident(ident: &'static str, url_data: &UrlExtraData) -> Self { + Self::from_token(Token::Ident(ident.into()), url_data) + } + + /// Create VariableValue from a float amount of CSS pixels. + fn pixels(number: f32, url_data: &UrlExtraData) -> Self { + // FIXME (https://github.com/servo/rust-cssparser/issues/266): + // No way to get TokenSerializationType::Dimension without creating + // Token object. + Self::from_token( + Token::Dimension { + has_sign: false, + value: number, + int_value: None, + unit: CowRcStr::from("px"), + }, + url_data, + ) + } + + /// Create VariableValue from an integer amount of milliseconds. + fn int_ms(number: i32, url_data: &UrlExtraData) -> Self { + Self::from_token( + Token::Dimension { + has_sign: false, + value: number as f32, + int_value: Some(number), + unit: CowRcStr::from("ms"), + }, + url_data, + ) + } + + /// Create VariableValue from an integer amount of CSS pixels. + fn int_pixels(number: i32, url_data: &UrlExtraData) -> Self { + Self::from_token( + Token::Dimension { + has_sign: false, + value: number as f32, + int_value: Some(number), + unit: CowRcStr::from("px"), + }, + url_data, + ) + } + + fn from_token(token: Token, url_data: &UrlExtraData) -> Self { + let token_type = token.serialization_type(); + let mut css = token.to_css_string(); + css.shrink_to_fit(); + + VariableValue { + css, + url_data: url_data.clone(), + first_token_type: token_type, + last_token_type: token_type, + references: Default::default(), + } + } + + /// Returns the raw CSS text from this VariableValue + pub fn css_text(&self) -> &str { + &self.css + } + + /// Returns whether this variable value has any reference to the environment or other + /// variables. + pub fn has_references(&self) -> bool { + self.references.has_references() + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#typedef-declaration-value> +fn parse_declaration_value<'i, 't>( + input: &mut Parser<'i, 't>, + input_start: SourcePosition, + references: &mut References, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + input.parse_until_before(Delimiter::Bang | Delimiter::Semicolon, |input| { + parse_declaration_value_block(input, input_start, references, missing_closing_characters) + }) +} + +/// Like parse_declaration_value, but accept `!` and `;` since they are only invalid at the top level. +fn parse_declaration_value_block<'i, 't>( + input: &mut Parser<'i, 't>, + input_start: SourcePosition, + references: &mut References, + missing_closing_characters: &mut String, +) -> Result<(TokenSerializationType, TokenSerializationType), ParseError<'i>> { + let mut is_first = true; + let mut first_token_type = TokenSerializationType::Nothing; + let mut last_token_type = TokenSerializationType::Nothing; + let mut prev_reference_index: Option<usize> = None; + loop { + let token_start = input.position(); + let Ok(token) = input.next_including_whitespace_and_comments() else { break }; + + let prev_token_type = last_token_type; + let serialization_type = token.serialization_type(); + last_token_type = serialization_type; + if is_first { + first_token_type = last_token_type; + is_first = false; + } + + macro_rules! nested { + () => { + input.parse_nested_block(|input| { + parse_declaration_value_block( + input, + input_start, + references, + missing_closing_characters, + ) + })? + }; + } + macro_rules! check_closed { + ($closing:expr) => { + if !input.slice_from(token_start).ends_with($closing) { + missing_closing_characters.push_str($closing) + } + }; + } + if let Some(index) = prev_reference_index.take() { + references.refs[index].next_token_type = serialization_type; + } + match *token { + Token::Comment(_) => { + let token_slice = input.slice_from(token_start); + if !token_slice.ends_with("*/") { + missing_closing_characters.push_str(if token_slice.ends_with('*') { + "/" + } else { + "*/" + }) + } + }, + Token::BadUrl(ref u) => { + let e = StyleParseErrorKind::BadUrlInDeclarationValueBlock(u.clone()); + return Err(input.new_custom_error(e)); + }, + Token::BadString(ref s) => { + let e = StyleParseErrorKind::BadStringInDeclarationValueBlock(s.clone()); + return Err(input.new_custom_error(e)); + }, + Token::CloseParenthesis => { + let e = StyleParseErrorKind::UnbalancedCloseParenthesisInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::CloseSquareBracket => { + let e = StyleParseErrorKind::UnbalancedCloseSquareBracketInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::CloseCurlyBracket => { + let e = StyleParseErrorKind::UnbalancedCloseCurlyBracketInDeclarationValueBlock; + return Err(input.new_custom_error(e)); + }, + Token::Function(ref name) => { + let is_var = name.eq_ignore_ascii_case("var"); + if is_var || name.eq_ignore_ascii_case("env") { + let our_ref_index = references.refs.len(); + let fallback = input.parse_nested_block(|input| { + // TODO(emilio): For env() this should be <custom-ident> per spec, but no other browser does + // that, see https://github.com/w3c/csswg-drafts/issues/3262. + let name = input.expect_ident()?; + let name = Atom::from(if is_var { + match parse_name(name.as_ref()) { + Ok(name) => name, + Err(()) => { + let name = name.clone(); + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(name), + )); + }, + } + } else { + name.as_ref() + }); + + // We want the order of the references to match source order. So we need to reserve our slot + // now, _before_ parsing our fallback. Note that we don't care if parsing fails after all, since + // if this fails we discard the whole result anyways. + let start = token_start.byte_index() - input_start.byte_index(); + references.refs.push(VarOrEnvReference { + name, + start, + // To be fixed up after parsing fallback and auto-closing via our_ref_index. + end: start, + prev_token_type, + // To be fixed up (if needed) on the next loop iteration via prev_reference_index. + next_token_type: TokenSerializationType::Nothing, + // To be fixed up after parsing fallback. + fallback: None, + is_var, + }); + + let mut fallback = None; + if input.try_parse(|input| input.expect_comma()).is_ok() { + input.skip_whitespace(); + let fallback_start = num::NonZeroUsize::new( + input.position().byte_index() - input_start.byte_index(), + ) + .unwrap(); + // NOTE(emilio): Intentionally using parse_declaration_value rather than + // parse_declaration_value_block, since that's what parse_fallback used to do. + let (first, last) = parse_declaration_value( + input, + input_start, + references, + missing_closing_characters, + )?; + fallback = Some(VariableFallback { + start: fallback_start, + first_token_type: first, + last_token_type: last, + }); + } else { + let state = input.state(); + // We still need to consume the rest of the potentially-unclosed + // tokens, but make sure to not consume tokens that would otherwise be + // invalid, by calling reset(). + parse_declaration_value_block( + input, + input_start, + references, + missing_closing_characters, + )?; + input.reset(&state); + } + Ok(fallback) + })?; + check_closed!(")"); + prev_reference_index = Some(our_ref_index); + let reference = &mut references.refs[our_ref_index]; + reference.end = input.position().byte_index() - input_start.byte_index() + missing_closing_characters.len(); + reference.fallback = fallback; + if is_var { + references.any_var = true; + } else { + references.any_env = true; + } + } else { + nested!(); + check_closed!(")"); + } + }, + Token::ParenthesisBlock => { + nested!(); + check_closed!(")"); + }, + Token::CurlyBracketBlock => { + nested!(); + check_closed!("}"); + }, + Token::SquareBracketBlock => { + nested!(); + check_closed!("]"); + }, + Token::QuotedString(_) => { + let token_slice = input.slice_from(token_start); + let quote = &token_slice[..1]; + debug_assert!(matches!(quote, "\"" | "'")); + if !(token_slice.ends_with(quote) && token_slice.len() > 1) { + missing_closing_characters.push_str(quote) + } + }, + Token::Ident(ref value) | + Token::AtKeyword(ref value) | + Token::Hash(ref value) | + Token::IDHash(ref value) | + Token::UnquotedUrl(ref value) | + Token::Dimension { + unit: ref value, .. + } => { + references + .non_custom_references + .insert(NonCustomReferences::from_unit(value)); + let is_unquoted_url = matches!(token, Token::UnquotedUrl(_)); + if value.ends_with("�") && input.slice_from(token_start).ends_with("\\") { + // Unescaped backslash at EOF in these contexts is interpreted as U+FFFD + // Check the value in case the final backslash was itself escaped. + // Serialize as escaped U+FFFD, which is also interpreted as U+FFFD. + // (Unescaped U+FFFD would also work, but removing the backslash is annoying.) + missing_closing_characters.push_str("�") + } + if is_unquoted_url { + check_closed!(")"); + } + }, + _ => {}, + }; + } + Ok((first_token_type, last_token_type)) +} + +/// A struct that takes care of encapsulating the cascade process for custom properties. +pub struct CustomPropertiesBuilder<'a, 'b: 'a> { + seen: PrecomputedHashSet<&'a Name>, + may_have_cycles: bool, + custom_properties: ComputedCustomProperties, + reverted: PrecomputedHashMap<&'a Name, (CascadePriority, bool)>, + stylist: &'a Stylist, + computed_context: &'a mut computed::Context<'b>, + references_from_non_custom_properties: NonCustomReferenceMap<Vec<Name>>, +} + +impl<'a, 'b: 'a> CustomPropertiesBuilder<'a, 'b> { + /// Create a new builder, inheriting from a given custom properties map. + /// + /// We expose this publicly mostly for @keyframe blocks. + pub fn new_with_properties(stylist: &'a Stylist, custom_properties: ComputedCustomProperties, computed_context: &'a mut computed::Context<'b>) -> Self { + Self { + seen: PrecomputedHashSet::default(), + reverted: Default::default(), + may_have_cycles: false, + custom_properties, + stylist, + computed_context, + references_from_non_custom_properties: NonCustomReferenceMap::default(), + } + } + + /// Create a new builder, inheriting from the right style given context. + pub fn new(stylist: &'a Stylist, context: &'a mut computed::Context<'b>) -> Self { + let is_root_element = context.is_root_element(); + + let inherited = context.inherited_custom_properties(); + let initial_values = stylist.get_custom_property_initial_values(); + let properties = ComputedCustomProperties { + inherited: if is_root_element { + debug_assert!(inherited.is_empty()); + initial_values.inherited.clone() + } else { + inherited.inherited.clone() + }, + non_inherited: initial_values.non_inherited.clone(), + }; + + // Reuse flags from computing registered custom properties initial values, such as + // whether they depend on viewport units. + context.style().add_flags(stylist.get_custom_property_initial_values_flags()); + Self::new_with_properties(stylist, properties, context) + } + + /// Cascade a given custom property declaration. + pub fn cascade(&mut self, declaration: &'a CustomDeclaration, priority: CascadePriority) { + let CustomDeclaration { + ref name, + ref value, + } = *declaration; + + if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&name) { + if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) { + return; + } + } + + let was_already_present = !self.seen.insert(name); + if was_already_present { + return; + } + + if !self.value_may_affect_style(name, value) { + return; + } + + let map = &mut self.custom_properties; + let registration = self.stylist.get_custom_property_registration(&name); + match *value { + CustomDeclarationValue::Value(ref unparsed_value) => { + let has_custom_property_references = unparsed_value.references.any_var; + let registered_length_property = + registration.syntax.may_reference_font_relative_length(); + // Non-custom dependency is really relevant for registered custom properties + // that require computed value of such dependencies. + let has_non_custom_dependencies = registered_length_property && + !unparsed_value + .references + .get_non_custom_dependencies(self.computed_context.is_root_element()) + .is_empty(); + self.may_have_cycles |= + has_custom_property_references || has_non_custom_dependencies; + + // If the variable value has no references to other properties, perform + // substitution here instead of forcing a full traversal in `substitute_all` + // afterwards. + if !has_custom_property_references && !has_non_custom_dependencies { + return substitute_references_if_needed_and_apply( + name, + unparsed_value, + map, + self.stylist, + self.computed_context, + ); + } + map.insert(registration, name, Arc::clone(unparsed_value)); + }, + CustomDeclarationValue::CSSWideKeyword(keyword) => match keyword { + CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => { + let origin_revert = keyword == CSSWideKeyword::Revert; + self.seen.remove(name); + self.reverted.insert(name, (priority, origin_revert)); + }, + CSSWideKeyword::Initial => { + // For non-inherited custom properties, 'initial' was handled in value_may_affect_style. + debug_assert!(registration.inherits(), "Should've been handled earlier"); + map.remove(registration, name); + if let Some(ref initial_value) = registration.initial_value { + map.insert(registration, name, initial_value.clone()); + } + }, + CSSWideKeyword::Inherit => { + // For inherited custom properties, 'inherit' was handled in value_may_affect_style. + debug_assert!(!registration.inherits(), "Should've been handled earlier"); + if let Some(inherited_value) = self + .computed_context + .inherited_custom_properties() + .non_inherited + .get(name) + { + map.insert(registration, name, inherited_value.clone()); + } + }, + // handled in value_may_affect_style + CSSWideKeyword::Unset => unreachable!(), + }, + } + } + + /// Note a non-custom property with variable reference that may in turn depend on that property. + /// e.g. `font-size` depending on a custom property that may be a registered property using `em`. + pub fn note_potentially_cyclic_non_custom_dependency(&mut self, id: LonghandId, decl: &VariableDeclaration) { + // With unit algebra in `calc()`, references aren't limited to `font-size`. + // For example, `--foo: 100ex; font-weight: calc(var(--foo) / 1ex);`, + // or `--foo: 1em; zoom: calc(var(--foo) * 30px / 2em);` + let references = match id { + LonghandId::FontSize => { + if self.computed_context.is_root_element() { + NonCustomReferences::ROOT_FONT_UNITS + } else { + NonCustomReferences::FONT_UNITS + } + }, + LonghandId::LineHeight => { + if self.computed_context.is_root_element() { + NonCustomReferences::ROOT_LH_UNITS | + NonCustomReferences::ROOT_FONT_UNITS + } else { + NonCustomReferences::LH_UNITS | NonCustomReferences::FONT_UNITS + } + }, + _ => return, + }; + let refs = &decl.value.variable_value.references; + if !refs.any_var { + return; + } + + let variables: Vec<Atom> = refs.refs.iter().filter_map(|reference| { + if !reference.is_var { + return None; + } + if !self.stylist.get_custom_property_registration(&reference.name).syntax.may_compute_length() { + return None; + } + Some(reference.name.clone()) + }).collect(); + references.for_each(|idx| { + let entry = &mut self.references_from_non_custom_properties[idx]; + let was_none = entry.is_none(); + let v = entry.get_or_insert_with(|| variables.clone()); + if was_none { + return; + } + v.extend(variables.clone().into_iter()); + }); + } + + fn value_may_affect_style(&self, name: &Name, value: &CustomDeclarationValue) -> bool { + let registration = self.stylist.get_custom_property_registration(&name); + match *value { + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit) => { + // For inherited custom properties, explicit 'inherit' means we + // can just use any existing value in the inherited + // CustomPropertiesMap. + if registration.inherits() { + return false; + } + }, + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial) => { + // For non-inherited custom properties, explicit 'initial' means + // we can just use any initial value in the registration. + if !registration.inherits() { + return false; + } + }, + CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Unset) => { + // Explicit 'unset' means we can either just use any existing + // value in the inherited CustomPropertiesMap or the initial + // value in the registration. + return false; + }, + _ => {}, + } + + let existing_value = self.custom_properties.get(registration, &name); + match (existing_value, value) { + (None, &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial)) => { + debug_assert!(registration.inherits(), "Should've been handled earlier"); + // The initial value of a custom property without a + // guaranteed-invalid initial value is the same as it + // not existing in the map. + if registration.initial_value.is_none() { + return false; + } + }, + ( + Some(existing_value), + &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Initial), + ) => { + debug_assert!(registration.inherits(), "Should've been handled earlier"); + // Don't bother overwriting an existing value with the initial value specified in + // the registration. + if Some(existing_value) == registration.initial_value.as_ref() { + return false; + } + }, + (Some(_), &CustomDeclarationValue::CSSWideKeyword(CSSWideKeyword::Inherit)) => { + debug_assert!(!registration.inherits(), "Should've been handled earlier"); + // existing_value is the registered initial value. + // Don't bother adding it to self.custom_properties.non_inherited + // if the key is also absent from self.inherited.non_inherited. + if self + .computed_context + .inherited_custom_properties() + .non_inherited + .get(name) + .is_none() + { + return false; + } + }, + (Some(existing_value), &CustomDeclarationValue::Value(ref value)) => { + // Don't bother overwriting an existing value with the same + // specified value. + if existing_value == value { + return false; + } + }, + _ => {}, + } + + true + } + + /// Computes the map of applicable custom properties, as well as + /// longhand properties that are now considered invalid-at-compute time. + /// The result is saved into the computed context. + /// + /// If there was any specified property or non-inherited custom property + /// with an initial value, we've created a new map and now we + /// need to remove any potential cycles (And marking non-custom + /// properties), and wrap it in an arc. + /// + /// Some registered custom properties may require font-related properties + /// be resolved to resolve. If these properties are not resolved at this time, + /// `defer` should be set to `Yes`, which will leave such custom properties, + /// and other properties referencing them, untouched. These properties are + /// returned separately, to be resolved by `build_deferred` to fully resolve + /// all custom properties after all necessary non-custom properties are resolved. + pub fn build( + mut self, + defer: DeferFontRelativeCustomPropertyResolution, + ) -> Option<ComputedCustomProperties> { + let mut deferred_custom_properties = None; + if self.may_have_cycles { + if defer == DeferFontRelativeCustomPropertyResolution::Yes { + deferred_custom_properties = Some(ComputedCustomProperties::default()); + } + let mut invalid_non_custom_properties = LonghandIdSet::default(); + substitute_all( + &mut self.custom_properties, + deferred_custom_properties.as_mut(), + &mut invalid_non_custom_properties, + &self.seen, + &self.references_from_non_custom_properties, + self.stylist, + self.computed_context, + ); + self.computed_context.builder.invalid_non_custom_properties = invalid_non_custom_properties; + } + + self.custom_properties.shrink_to_fit(); + + // Some pages apply a lot of redundant custom properties, see e.g. + // bug 1758974 comment 5. Try to detect the case where the values + // haven't really changed, and save some memory by reusing the inherited + // map in that case. + let initial_values = self.stylist.get_custom_property_initial_values(); + self.computed_context.builder.custom_properties = ComputedCustomProperties { + inherited: if self + .computed_context + .inherited_custom_properties() + .inherited == self.custom_properties.inherited + { + self.computed_context + .inherited_custom_properties() + .inherited + .clone() + } else { + self.custom_properties.inherited + }, + non_inherited: if initial_values.non_inherited == self.custom_properties.non_inherited { + initial_values.non_inherited.clone() + } else { + self.custom_properties.non_inherited + }, + }; + + deferred_custom_properties + } + + /// Fully resolve all deferred custom properties, assuming that the incoming context + /// has necessary properties resolved. + pub fn build_deferred( + deferred: ComputedCustomProperties, + stylist: &Stylist, + computed_context: &mut computed::Context, + ) { + if deferred.is_empty() { + return; + } + // Guaranteed to not have cycles at this point. + let substitute = + |deferred: &CustomPropertiesMap, + stylist: &Stylist, + context: &computed::Context, + custom_properties: &mut ComputedCustomProperties| { + // Since `CustomPropertiesMap` preserves insertion order, we shouldn't + // have to worry about resolving in a wrong order. + for (k, v) in deferred.iter() { + let Some(v) = v else { continue }; + substitute_references_if_needed_and_apply( + k, + v, + custom_properties, + stylist, + context, + ); + } + }; + let mut custom_properties = std::mem::take(&mut computed_context.builder.custom_properties); + substitute( + &deferred.inherited, + stylist, + computed_context, + &mut custom_properties, + ); + substitute( + &deferred.non_inherited, + stylist, + computed_context, + &mut custom_properties, + ); + computed_context.builder.custom_properties = custom_properties; + } +} + +/// Resolve all custom properties to either substituted, invalid, or unset +/// (meaning we should use the inherited value). +/// +/// It does cycle dependencies removal at the same time as substitution. +fn substitute_all( + custom_properties_map: &mut ComputedCustomProperties, + mut deferred_properties_map: Option<&mut ComputedCustomProperties>, + invalid_non_custom_properties: &mut LonghandIdSet, + seen: &PrecomputedHashSet<&Name>, + references_from_non_custom_properties: &NonCustomReferenceMap<Vec<Name>>, + stylist: &Stylist, + computed_context: &computed::Context, +) { + // The cycle dependencies removal in this function is a variant + // of Tarjan's algorithm. It is mostly based on the pseudo-code + // listed in + // https://en.wikipedia.org/w/index.php? + // title=Tarjan%27s_strongly_connected_components_algorithm&oldid=801728495 + + #[derive(Clone, Eq, PartialEq, Debug)] + enum VarType { + Custom(Name), + NonCustom(SingleNonCustomReference), + } + + /// Struct recording necessary information for each variable. + #[derive(Debug)] + struct VarInfo { + /// The name of the variable. It will be taken to save addref + /// when the corresponding variable is popped from the stack. + /// This also serves as a mark for whether the variable is + /// currently in the stack below. + var: Option<VarType>, + /// If the variable is in a dependency cycle, lowlink represents + /// a smaller index which corresponds to a variable in the same + /// strong connected component, which is known to be accessible + /// from this variable. It is not necessarily the root, though. + lowlink: usize, + } + /// Context struct for traversing the variable graph, so that we can + /// avoid referencing all the fields multiple times. + struct Context<'a, 'b: 'a> { + /// Number of variables visited. This is used as the order index + /// when we visit a new unresolved variable. + count: usize, + /// The map from custom property name to its order index. + index_map: PrecomputedHashMap<Name, usize>, + /// Mapping from a non-custom dependency to its order index. + non_custom_index_map: NonCustomReferenceMap<usize>, + /// Information of each variable indexed by the order index. + var_info: SmallVec<[VarInfo; 5]>, + /// The stack of order index of visited variables. It contains + /// all unfinished strong connected components. + stack: SmallVec<[usize; 5]>, + /// References to non-custom properties in this strongly connected component. + non_custom_references: NonCustomReferences, + map: &'a mut ComputedCustomProperties, + /// The stylist is used to get registered properties, and to resolve the environment to + /// substitute `env()` variables. + stylist: &'a Stylist, + /// The computed context is used to get inherited custom + /// properties and compute registered custom properties. + computed_context: &'a computed::Context<'b>, + /// Longhand IDs that became invalid due to dependency cycle(s). + invalid_non_custom_properties: &'a mut LonghandIdSet, + /// Properties that cannot yet be substituted. + deferred_properties: Option<&'a mut ComputedCustomProperties>, + } + + /// This function combines the traversal for cycle removal and value + /// substitution. It returns either a signal None if this variable + /// has been fully resolved (to either having no reference or being + /// marked invalid), or the order index for the given name. + /// + /// When it returns, the variable corresponds to the name would be + /// in one of the following states: + /// * It is still in context.stack, which means it is part of an + /// potentially incomplete dependency circle. + /// * It has been removed from the map. It can be either that the + /// substitution failed, or it is inside a dependency circle. + /// When this function removes a variable from the map because + /// of dependency circle, it would put all variables in the same + /// strong connected component to the set together. + /// * It doesn't have any reference, because either this variable + /// doesn't have reference at all in specified value, or it has + /// been completely resolved. + /// * There is no such variable at all. + fn traverse<'a, 'b>( + var: VarType, + non_custom_references: &NonCustomReferenceMap<Vec<Name>>, + context: &mut Context<'a, 'b>, + ) -> Option<usize> { + // Some shortcut checks. + let (value, should_substitute) = match var { + VarType::Custom(ref name) => { + let registration = context.stylist.get_custom_property_registration(name); + let value = context.map.get(registration, name)?; + + let non_custom_references = value + .references + .get_non_custom_dependencies(context.computed_context.is_root_element()); + let has_custom_property_reference = value.references.any_var; + // Nothing to resolve. + if !has_custom_property_reference && non_custom_references.is_empty() { + debug_assert!(!value.references.any_env, "Should've been handled earlier"); + return None; + } + + // Has this variable been visited? + match context.index_map.entry(name.clone()) { + Entry::Occupied(entry) => { + return Some(*entry.get()); + }, + Entry::Vacant(entry) => { + entry.insert(context.count); + }, + } + context.non_custom_references |= value.as_ref().references.non_custom_references; + + // Hold a strong reference to the value so that we don't + // need to keep reference to context.map. + (Some(value.clone()), has_custom_property_reference) + }, + VarType::NonCustom(ref non_custom) => { + let entry = &mut context.non_custom_index_map[*non_custom]; + if let Some(v) = entry { + return Some(*v); + } + *entry = Some(context.count); + (None, false) + }, + }; + + // Add new entry to the information table. + let index = context.count; + context.count += 1; + debug_assert_eq!(index, context.var_info.len()); + context.var_info.push(VarInfo { + var: Some(var.clone()), + lowlink: index, + }); + context.stack.push(index); + + let mut self_ref = false; + let mut lowlink = index; + let visit_link = + |var: VarType, context: &mut Context, lowlink: &mut usize, self_ref: &mut bool| { + let next_index = match traverse(var, non_custom_references, context) { + Some(index) => index, + // There is nothing to do if the next variable has been + // fully resolved at this point. + None => { + return; + }, + }; + let next_info = &context.var_info[next_index]; + if next_index > index { + // The next variable has a larger index than us, so it + // must be inserted in the recursive call above. We want + // to get its lowlink. + *lowlink = cmp::min(*lowlink, next_info.lowlink); + } else if next_index == index { + *self_ref = true; + } else if next_info.var.is_some() { + // The next variable has a smaller order index and it is + // in the stack, so we are at the same component. + *lowlink = cmp::min(*lowlink, next_index); + } + }; + if let Some(ref v) = value.as_ref() { + debug_assert!( + matches!(var, VarType::Custom(_)), + "Non-custom property has references?" + ); + + // Visit other custom properties... + // FIXME: Maybe avoid visiting the same var twice if not needed? + for next in &v.references.refs { + if !next.is_var { + continue; + } + visit_link( + VarType::Custom(next.name.clone()), + context, + &mut lowlink, + &mut self_ref, + ); + } + + // ... Then non-custom properties. + v.references.non_custom_references.for_each(|r| { + visit_link(VarType::NonCustom(r), context, &mut lowlink, &mut self_ref); + }); + } else if let VarType::NonCustom(non_custom) = var { + let entry = &non_custom_references[non_custom]; + if let Some(deps) = entry.as_ref() { + for d in deps { + // Visit any reference from this non-custom property to custom properties. + visit_link( + VarType::Custom(d.clone()), + context, + &mut lowlink, + &mut self_ref, + ); + } + } + } + + context.var_info[index].lowlink = lowlink; + if lowlink != index { + // This variable is in a loop, but it is not the root of + // this strong connected component. We simply return for + // now, and the root would remove it from the map. + // + // This cannot be removed from the map here, because + // otherwise the shortcut check at the beginning of this + // function would return the wrong value. + return Some(index); + } + + // This is the root of a strong-connected component. + let mut in_loop = self_ref; + let name; + + let handle_variable_in_loop = |name: &Name, context: &mut Context<'a, 'b>| { + if context + .non_custom_references + .intersects(NonCustomReferences::FONT_UNITS | NonCustomReferences::ROOT_FONT_UNITS) + { + context + .invalid_non_custom_properties + .insert(LonghandId::FontSize); + } + if context.non_custom_references.intersects( + NonCustomReferences::LH_UNITS | + NonCustomReferences::ROOT_LH_UNITS, + ) { + context + .invalid_non_custom_properties + .insert(LonghandId::LineHeight); + } + // This variable is in loop. Resolve to invalid. + handle_invalid_at_computed_value_time( + name, + context.map, + context.computed_context.inherited_custom_properties(), + context.stylist, + context.computed_context.is_root_element(), + ); + }; + loop { + let var_index = context + .stack + .pop() + .expect("The current variable should still be in stack"); + let var_info = &mut context.var_info[var_index]; + // We should never visit the variable again, so it's safe + // to take the name away, so that we don't do additional + // reference count. + let var_name = var_info + .var + .take() + .expect("Variable should not be poped from stack twice"); + if var_index == index { + name = match var_name { + VarType::Custom(name) => name, + // At the root of this component, and it's a non-custom + // reference - we have nothing to substitute, so + // it's effectively resolved. + VarType::NonCustom(..) => return None, + }; + break; + } + if let VarType::Custom(name) = var_name { + // Anything here is in a loop which can traverse to the + // variable we are handling, so it's invalid at + // computed-value time. + handle_variable_in_loop(&name, context); + } + in_loop = true; + } + // We've gotten to the root of this strongly connected component, so clear + // whether or not it involved non-custom references. + // It's fine to track it like this, because non-custom properties currently + // being tracked can only participate in any loop only once. + if in_loop { + handle_variable_in_loop(&name, context); + context.non_custom_references = NonCustomReferences::default(); + return None; + } + + if let Some(ref v) = value.as_ref() { + let registration = context.stylist.get_custom_property_registration(&name); + let registered_length_property = + registration.syntax.may_reference_font_relative_length(); + let mut defer = false; + if !context.non_custom_references.is_empty() && registered_length_property { + if let Some(deferred) = &mut context.deferred_properties { + // This property directly depends on a non-custom property, defer resolving it. + deferred.insert(registration, &name, (*v).clone()); + context.map.remove(registration, &name); + defer = true; + } + } + if should_substitute && !defer { + for reference in v.references.refs.iter() { + if !reference.is_var { + continue; + } + if let Some(deferred) = &mut context.deferred_properties { + let registration = + context.stylist.get_custom_property_registration(&reference.name); + if deferred.get(registration, &reference.name).is_some() { + // This property depends on a custom property that depends on a non-custom property, defer. + deferred.insert(registration, &name, Arc::clone(v)); + context.map.remove(registration, &name); + defer = true; + break; + } + } + } + if !defer { + substitute_references_if_needed_and_apply( + &name, + v, + &mut context.map, + context.stylist, + context.computed_context, + ); + } + } + } + context.non_custom_references = NonCustomReferences::default(); + + // All resolved, so return the signal value. + None + } + + // Note that `seen` doesn't contain names inherited from our parent, but + // those can't have variable references (since we inherit the computed + // variables) so we don't want to spend cycles traversing them anyway. + for name in seen { + let mut context = Context { + count: 0, + index_map: PrecomputedHashMap::default(), + non_custom_index_map: NonCustomReferenceMap::default(), + stack: SmallVec::new(), + var_info: SmallVec::new(), + map: custom_properties_map, + non_custom_references: NonCustomReferences::default(), + stylist, + computed_context, + invalid_non_custom_properties, + deferred_properties: deferred_properties_map.as_deref_mut(), + }; + traverse( + VarType::Custom((*name).clone()), + references_from_non_custom_properties, + &mut context, + ); + } +} + +// See https://drafts.csswg.org/css-variables-2/#invalid-at-computed-value-time +fn handle_invalid_at_computed_value_time( + name: &Name, + custom_properties: &mut ComputedCustomProperties, + inherited: &ComputedCustomProperties, + stylist: &Stylist, + is_root_element: bool, +) { + let registration = stylist.get_custom_property_registration(&name); + if !registration.syntax.is_universal() { + // For the root element, inherited maps are empty. We should just + // use the initial value if any, rather than removing the name. + if registration.inherits() && !is_root_element { + if let Some(value) = inherited.get(registration, name) { + custom_properties.insert(registration, name, Arc::clone(value)); + return; + } + } else { + if let Some(ref initial_value) = registration.initial_value { + custom_properties.insert(registration, name, Arc::clone(initial_value)); + return; + } + } + } + custom_properties.remove(registration, name); +} + +/// Replace `var()` and `env()` functions in a pre-existing variable value. +fn substitute_references_if_needed_and_apply( + name: &Name, + value: &Arc<VariableValue>, + custom_properties: &mut ComputedCustomProperties, + stylist: &Stylist, + computed_context: &computed::Context, +) { + let registration = stylist.get_custom_property_registration(&name); + if !value.has_references() && registration.syntax.is_universal() { + // Trivial path: no references and no need to compute the value, just apply it directly. + custom_properties.insert(registration, name, Arc::clone(value)); + return; + } + + let inherited = computed_context.inherited_custom_properties(); + let value = match substitute_internal(value, custom_properties, stylist, registration, computed_context) { + Ok(v) => v, + Err(..) => { + handle_invalid_at_computed_value_time( + name, + custom_properties, + inherited, + stylist, + computed_context.is_root_element(), + ); + return; + }, + }.into_value(&value.url_data); + + // If variable fallback results in a wide keyword, deal with it now. + { + let mut input = ParserInput::new(&value.css); + let mut input = Parser::new(&mut input); + + if let Ok(kw) = input.try_parse(CSSWideKeyword::parse) { + // TODO: It's unclear what this should do for revert / revert-layer, see + // https://github.com/w3c/csswg-drafts/issues/9131. For now treating as unset + // seems fine? + match (kw, registration.inherits(), computed_context.is_root_element()) { + (CSSWideKeyword::Initial, _, _) | + (CSSWideKeyword::Revert, false, _) | + (CSSWideKeyword::RevertLayer, false, _) | + (CSSWideKeyword::Unset, false, _) | + (CSSWideKeyword::Revert, true, true) | + (CSSWideKeyword::RevertLayer, true, true) | + (CSSWideKeyword::Unset, true, true) | + (CSSWideKeyword::Inherit, _, true) => { + custom_properties.remove(registration, name); + if let Some(ref initial_value) = registration.initial_value { + custom_properties.insert(registration, name, Arc::clone(initial_value)); + } + }, + (CSSWideKeyword::Revert, true, false) | + (CSSWideKeyword::RevertLayer, true, false) | + (CSSWideKeyword::Inherit, _, false) | + (CSSWideKeyword::Unset, true, false) => { + match inherited.get(registration, name) { + Some(value) => { + custom_properties.insert(registration, name, Arc::clone(value)); + }, + None => { + custom_properties.remove(registration, name); + }, + }; + }, + } + return; + } + } + + custom_properties.insert(registration, name, Arc::new(value)); +} + +#[derive(Default)] +struct Substitution<'a> { + css: Cow<'a, str>, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, +} + +impl<'a> Substitution<'a> { + fn new( + css: &'a str, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + ) -> Self { + Self { + css: Cow::Borrowed(css), + first_token_type, + last_token_type, + } + } + + fn from_value(v: VariableValue) -> Substitution<'static> { + debug_assert!(!v.has_references(), "Computed values shouldn't have references"); + Substitution { + css: Cow::from(v.css), + first_token_type: v.first_token_type, + last_token_type: v.last_token_type, + } + } + + fn into_value(self, url_data: &UrlExtraData) -> VariableValue { + VariableValue { + css: self.css.into_owned(), + first_token_type: self.first_token_type, + last_token_type: self.last_token_type, + url_data: url_data.clone(), + references: Default::default(), + } + } +} + +fn compute_value( + css: &str, + url_data: &UrlExtraData, + registration: &PropertyRegistrationData, + computed_context: &computed::Context, +) -> Result<Substitution<'static>, ()> { + debug_assert!(!registration.syntax.is_universal()); + + let mut input = ParserInput::new(&css); + let mut input = Parser::new(&mut input); + + let value = SpecifiedRegisteredValue::compute( + &mut input, + registration, + url_data, + computed_context, + AllowComputationallyDependent::Yes, + )?; + Ok(Substitution::from_value(value)) +} + +fn do_substitute_chunk<'a>( + css: &'a str, + start: usize, + end: usize, + first_token_type: TokenSerializationType, + last_token_type: TokenSerializationType, + url_data: &UrlExtraData, + custom_properties: &'a ComputedCustomProperties, + registration: &PropertyRegistrationData, + stylist: &Stylist, + computed_context: &computed::Context, + references: &mut std::iter::Peekable<std::slice::Iter<VarOrEnvReference>>, +) -> Result<Substitution<'a>, ()> { + if start == end { + // Empty string. Easy. + return Ok(Substitution::default()); + } + // Easy case: no references involved. + if references + .peek() + .map_or(true, |reference| reference.end > end) + { + let result = &css[start..end]; + if !registration.syntax.is_universal() { + return compute_value(result, url_data, registration, computed_context); + } + return Ok(Substitution::new(result, first_token_type, last_token_type)); + } + + let mut substituted = ComputedValue::empty(url_data); + let mut next_token_type = first_token_type; + let mut cur_pos = start; + while let Some(reference) = references.next_if(|reference| reference.end <= end) { + if reference.start != cur_pos { + substituted.push( + &css[cur_pos..reference.start], + next_token_type, + reference.prev_token_type, + )?; + } + + let substitution = substitute_one_reference( + css, + url_data, + custom_properties, + reference, + stylist, + computed_context, + references, + )?; + + // Optimize the property: var(--...) case to avoid allocating at all. + if reference.start == start && reference.end == end && registration.syntax.is_universal() { + return Ok(substitution); + } + + substituted.push( + &substitution.css, + substitution.first_token_type, + substitution.last_token_type, + )?; + next_token_type = reference.next_token_type; + cur_pos = reference.end; + } + // Push the rest of the value if needed. + if cur_pos != end { + substituted.push(&css[cur_pos..end], next_token_type, last_token_type)?; + } + if !registration.syntax.is_universal() { + return compute_value(&substituted.css, url_data, registration, computed_context); + } + Ok(Substitution::from_value(substituted)) +} + +fn substitute_one_reference<'a>( + css: &'a str, + url_data: &UrlExtraData, + custom_properties: &'a ComputedCustomProperties, + reference: &VarOrEnvReference, + stylist: &Stylist, + computed_context: &computed::Context, + references: &mut std::iter::Peekable<std::slice::Iter<VarOrEnvReference>>, +) -> Result<Substitution<'a>, ()> { + let registration; + if reference.is_var { + registration = stylist.get_custom_property_registration(&reference.name); + if let Some(v) = custom_properties.get(registration, &reference.name) { + debug_assert!(!v.has_references(), "Should be already computed"); + if registration.syntax.is_universal() { + // Skip references that are inside the outer variable (in fallback for example). + while references + .next_if(|next_ref| next_ref.end <= reference.end) + .is_some() + {} + } else { + // We need to validate the fallback if any, since invalid fallback should + // invalidate the whole variable. + if let Some(ref fallback) = reference.fallback { + let _ = do_substitute_chunk( + css, + fallback.start.get(), + reference.end - 1, // Don't include the closing parenthesis. + fallback.first_token_type, + fallback.last_token_type, + url_data, + custom_properties, + registration, + stylist, + computed_context, + references, + )?; + } + } + return Ok(Substitution { + css: Cow::from(&v.css), + first_token_type: v.first_token_type, + last_token_type: v.last_token_type, + }); + } + } else { + registration = PropertyRegistrationData::unregistered(); + let device = stylist.device(); + if let Some(v) = device.environment().get(&reference.name, device, url_data) { + while references + .next_if(|next_ref| next_ref.end <= reference.end) + .is_some() + {} + return Ok(Substitution::from_value(v)); + } + } + + let Some(ref fallback) = reference.fallback else { return Err(()) }; + + do_substitute_chunk( + css, + fallback.start.get(), + reference.end - 1, // Skip the closing parenthesis of the reference value. + fallback.first_token_type, + fallback.last_token_type, + url_data, + custom_properties, + registration, + stylist, + computed_context, + references, + ) +} + +/// Replace `var()` and `env()` functions. Return `Err(..)` for invalid at computed time. +fn substitute_internal<'a>( + variable_value: &'a VariableValue, + custom_properties: &'a ComputedCustomProperties, + stylist: &Stylist, + registration: &PropertyRegistrationData, + computed_context: &computed::Context, +) -> Result<Substitution<'a>, ()> { + let mut refs = variable_value.references.refs.iter().peekable(); + do_substitute_chunk( + &variable_value.css, + /* start = */ 0, + /* end = */ variable_value.css.len(), + variable_value.first_token_type, + variable_value.last_token_type, + &variable_value.url_data, + custom_properties, + registration, + stylist, + computed_context, + &mut refs, + ) +} + +/// Replace var() and env() functions, returning the resulting CSS string. +pub fn substitute<'a>( + variable_value: &'a VariableValue, + custom_properties: &'a ComputedCustomProperties, + stylist: &Stylist, + computed_context: &computed::Context, +) -> Result<Cow<'a, str>, ()> { + debug_assert!(variable_value.has_references()); + let v = substitute_internal( + variable_value, + custom_properties, + stylist, + PropertyRegistrationData::unregistered(), + computed_context, + )?; + Ok(v.css) +} diff --git a/servo/components/style/custom_properties_map.rs b/servo/components/style/custom_properties_map.rs new file mode 100644 index 0000000000..04ca8e1b3d --- /dev/null +++ b/servo/components/style/custom_properties_map.rs @@ -0,0 +1,237 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The structure that contains the custom properties of a given element. + +use crate::custom_properties::{Name, VariableValue}; +use crate::selector_map::PrecomputedHasher; +use indexmap::IndexMap; +use servo_arc::Arc; +use std::hash::BuildHasherDefault; + +/// A map for a set of custom properties, which implements copy-on-write behavior on insertion with +/// cheap copying. +#[derive(Clone, Debug, PartialEq)] +pub struct CustomPropertiesMap(Arc<Inner>); + +impl Default for CustomPropertiesMap { + fn default() -> Self { + Self(EMPTY.clone()) + } +} + +/// We use None in the value to represent a removed entry. +type OwnMap = IndexMap<Name, Option<Arc<VariableValue>>, BuildHasherDefault<PrecomputedHasher>>; + +// IndexMap equality doesn't consider ordering, which we want to account for. Also, for the same +// reason, IndexMap equality comparisons are slower than needed. +// +// See https://github.com/bluss/indexmap/issues/153. +// TODO: use as_slice when updating to indexmap 2.0. +fn maps_equal(l: &OwnMap, r: &OwnMap) -> bool { + if std::ptr::eq(l, r) { + return true; + } + if l.len() != r.len() { + return false; + } + l.iter() + .zip(r.iter()) + .all(|((k1, v1), (k2, v2))| k1 == k2 && v1 == v2) +} + +lazy_static! { + static ref EMPTY: Arc<Inner> = { + Arc::new_leaked(Inner { + own_properties: Default::default(), + parent: None, + len: 0, + ancestor_count: 0, + }) + }; +} + +#[derive(Debug, Clone)] +struct Inner { + own_properties: OwnMap, + parent: Option<Arc<Inner>>, + /// The number of custom properties we store. Note that this is different from the sum of our + /// own and our parent's length, since we might store duplicate entries. + len: usize, + /// The number of ancestors we have. + ancestor_count: u8, +} + +/// A not-too-large, not too small ancestor limit, to prevent creating too-big chains. +const ANCESTOR_COUNT_LIMIT: usize = 4; + +/// An iterator over the custom properties. +pub struct Iter<'a> { + current: &'a Inner, + current_iter: indexmap::map::Iter<'a, Name, Option<Arc<VariableValue>>>, + descendants: smallvec::SmallVec<[&'a Inner; ANCESTOR_COUNT_LIMIT]>, +} + +impl<'a> Iterator for Iter<'a> { + type Item = (&'a Name, &'a Option<Arc<VariableValue>>); + + fn next(&mut self) -> Option<Self::Item> { + loop { + let (name, value) = match self.current_iter.next() { + Some(v) => v, + None => { + let parent = self.current.parent.as_deref()?; + self.descendants.push(self.current); + self.current = parent; + self.current_iter = parent.own_properties.iter(); + continue; + }, + }; + // If the property is overridden by a descendant we've already visited it. + for descendant in &self.descendants { + if descendant.own_properties.contains_key(name) { + continue; + } + } + return Some((name, value)); + } + } +} + +impl PartialEq for Inner { + fn eq(&self, other: &Self) -> bool { + if self.len != other.len { + return false; + } + if self.parent_ptr_eq(other) { + return maps_equal(&self.own_properties, &other.own_properties); + } + for (name, value) in self.iter() { + if other.get(name) != value.as_ref() { + return false; + } + } + return true; + } +} + +impl Inner { + fn parent_ptr_eq(&self, other: &Self) -> bool { + match (&self.parent, &other.parent) { + (Some(p1), Some(p2)) => Arc::ptr_eq(p1, p2), + (None, None) => true, + _ => false, + } + } + + fn iter(&self) -> Iter { + Iter { + current: self, + current_iter: self.own_properties.iter(), + descendants: Default::default(), + } + } + + fn is_empty(&self) -> bool { + self.len == 0 + } + + fn len(&self) -> usize { + self.len + } + + fn get(&self, name: &Name) -> Option<&Arc<VariableValue>> { + if let Some(p) = self.own_properties.get(name) { + return p.as_ref(); + } + self.parent.as_ref()?.get(name) + } + + fn insert(&mut self, name: &Name, value: Option<Arc<VariableValue>>) { + let new = self.own_properties.insert(name.clone(), value).is_none(); + if new && self.parent.as_ref().map_or(true, |p| p.get(name).is_none()) { + self.len += 1; + } + } + + /// Whether we should expand the chain, or just copy-on-write. + fn should_expand_chain(&self) -> bool { + const SMALL_THRESHOLD: usize = 8; + if self.own_properties.len() <= SMALL_THRESHOLD { + return false; // Just copy, to avoid very long chains. + } + self.ancestor_count < ANCESTOR_COUNT_LIMIT as u8 + } +} + +impl CustomPropertiesMap { + /// Returns whether the map has no properties in it. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Returns the amount of different properties in the map. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Returns the property name and value at a given index. + pub fn get_index(&self, index: usize) -> Option<(&Name, &Option<Arc<VariableValue>>)> { + if index >= self.len() { + return None; + } + // FIXME: This is O(n) which is a bit unfortunate. + self.0.iter().nth(index) + } + + /// Returns a given property value by name. + pub fn get(&self, name: &Name) -> Option<&Arc<VariableValue>> { + self.0.get(name) + } + + fn do_insert(&mut self, name: &Name, value: Option<Arc<VariableValue>>) { + if let Some(inner) = Arc::get_mut(&mut self.0) { + return inner.insert(name, value); + } + if self.get(name) == value.as_ref() { + return; + } + if !self.0.should_expand_chain() { + return Arc::make_mut(&mut self.0).insert(name, value); + } + let len = self.0.len; + let ancestor_count = self.0.ancestor_count + 1; + let mut new_inner = Inner { + own_properties: Default::default(), + // FIXME: Would be nice to avoid this clone. + parent: Some(self.0.clone()), + len, + ancestor_count, + }; + new_inner.insert(name, value); + self.0 = Arc::new(new_inner); + } + + /// Inserts an element in the map. + pub fn insert(&mut self, name: &Name, value: Arc<VariableValue>) { + self.do_insert(name, Some(value)) + } + + /// Removes an element from the map. + pub fn remove(&mut self, name: &Name) { + self.do_insert(name, None) + } + + /// Shrinks the map as much as possible. + pub fn shrink_to_fit(&mut self) { + if let Some(inner) = Arc::get_mut(&mut self.0) { + inner.own_properties.shrink_to_fit() + } + } + + /// Return iterator to go through all properties. + pub fn iter(&self) -> Iter { + self.0.iter() + } +} diff --git a/servo/components/style/data.rs b/servo/components/style/data.rs new file mode 100644 index 0000000000..ceddc5bd20 --- /dev/null +++ b/servo/components/style/data.rs @@ -0,0 +1,545 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Per-node data used in style calculation. + +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{SharedStyleContext, StackLimitChecker}; +use crate::dom::TElement; +use crate::invalidation::element::invalidator::InvalidationResult; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::properties::ComputedValues; +use crate::selector_parser::{PseudoElement, RestyleDamage, EAGER_PSEUDO_COUNT}; +use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles, ResolvedStyle}; +#[cfg(feature = "gecko")] +use malloc_size_of::MallocSizeOfOps; +use selectors::matching::SelectorCaches; +use servo_arc::Arc; +use std::fmt; +use std::mem; +use std::ops::{Deref, DerefMut}; + +bitflags! { + /// Various flags stored on ElementData. + #[derive(Debug, Default)] + pub struct ElementDataFlags: u8 { + /// Whether the styles changed for this restyle. + const WAS_RESTYLED = 1 << 0; + /// Whether the last traversal of this element did not do + /// any style computation. This is not true during the initial + /// styling pass, nor is it true when we restyle (in which case + /// WAS_RESTYLED is set). + /// + /// This bit always corresponds to the last time the element was + /// traversed, so each traversal simply updates it with the appropriate + /// value. + const TRAVERSED_WITHOUT_STYLING = 1 << 1; + + /// Whether the primary style of this element data was reused from + /// another element via a rule node comparison. This allows us to + /// differentiate between elements that shared styles because they met + /// all the criteria of the style sharing cache, compared to elements + /// that reused style structs via rule node identity. + /// + /// The former gives us stronger transitive guarantees that allows us to + /// apply the style sharing cache to cousins. + const PRIMARY_STYLE_REUSED_VIA_RULE_NODE = 1 << 2; + } +} + +/// A lazily-allocated list of styles for eagerly-cascaded pseudo-elements. +/// +/// We use an Arc so that sharing these styles via the style sharing cache does +/// not require duplicate allocations. We leverage the copy-on-write semantics of +/// Arc::make_mut(), which is free (i.e. does not require atomic RMU operations) +/// in servo_arc. +#[derive(Clone, Debug, Default)] +pub struct EagerPseudoStyles(Option<Arc<EagerPseudoArray>>); + +#[derive(Default)] +struct EagerPseudoArray(EagerPseudoArrayInner); +type EagerPseudoArrayInner = [Option<Arc<ComputedValues>>; EAGER_PSEUDO_COUNT]; + +impl Deref for EagerPseudoArray { + type Target = EagerPseudoArrayInner; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for EagerPseudoArray { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// Manually implement `Clone` here because the derived impl of `Clone` for +// array types assumes the value inside is `Copy`. +impl Clone for EagerPseudoArray { + fn clone(&self) -> Self { + let mut clone = Self::default(); + for i in 0..EAGER_PSEUDO_COUNT { + clone[i] = self.0[i].clone(); + } + clone + } +} + +// Override Debug to print which pseudos we have, and substitute the rule node +// for the much-more-verbose ComputedValues stringification. +impl fmt::Debug for EagerPseudoArray { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "EagerPseudoArray {{ ")?; + for i in 0..EAGER_PSEUDO_COUNT { + if let Some(ref values) = self[i] { + write!( + f, + "{:?}: {:?}, ", + PseudoElement::from_eager_index(i), + &values.rules + )?; + } + } + write!(f, "}}") + } +} + +// Can't use [None; EAGER_PSEUDO_COUNT] here because it complains +// about Copy not being implemented for our Arc type. +#[cfg(feature = "gecko")] +const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None, None]; +#[cfg(feature = "servo")] +const EMPTY_PSEUDO_ARRAY: &'static EagerPseudoArrayInner = &[None, None, None]; + +impl EagerPseudoStyles { + /// Returns whether there are any pseudo styles. + pub fn is_empty(&self) -> bool { + self.0.is_none() + } + + /// Grabs a reference to the list of styles, if they exist. + pub fn as_optional_array(&self) -> Option<&EagerPseudoArrayInner> { + match self.0 { + None => None, + Some(ref x) => Some(&x.0), + } + } + + /// Grabs a reference to the list of styles or a list of None if + /// there are no styles to be had. + pub fn as_array(&self) -> &EagerPseudoArrayInner { + self.as_optional_array().unwrap_or(EMPTY_PSEUDO_ARRAY) + } + + /// Returns a reference to the style for a given eager pseudo, if it exists. + pub fn get(&self, pseudo: &PseudoElement) -> Option<&Arc<ComputedValues>> { + debug_assert!(pseudo.is_eager()); + self.0 + .as_ref() + .and_then(|p| p[pseudo.eager_index()].as_ref()) + } + + /// Sets the style for the eager pseudo. + pub fn set(&mut self, pseudo: &PseudoElement, value: Arc<ComputedValues>) { + if self.0.is_none() { + self.0 = Some(Arc::new(Default::default())); + } + let arr = Arc::make_mut(self.0.as_mut().unwrap()); + arr[pseudo.eager_index()] = Some(value); + } +} + +/// The styles associated with a node, including the styles for any +/// pseudo-elements. +#[derive(Clone, Default)] +pub struct ElementStyles { + /// The element's style. + pub primary: Option<Arc<ComputedValues>>, + /// A list of the styles for the element's eagerly-cascaded pseudo-elements. + pub pseudos: EagerPseudoStyles, +} + +// There's one of these per rendered elements so it better be small. +size_of_test!(ElementStyles, 16); + +/// Information on how this element uses viewport units. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum ViewportUnitUsage { + /// No viewport units are used. + None = 0, + /// There are viewport units used from regular style rules (which means we + /// should re-cascade). + FromDeclaration, + /// There are viewport units used from container queries (which means we + /// need to re-selector-match). + FromQuery, +} + +impl ElementStyles { + /// Returns the primary style. + pub fn get_primary(&self) -> Option<&Arc<ComputedValues>> { + self.primary.as_ref() + } + + /// Returns the primary style. Panic if no style available. + pub fn primary(&self) -> &Arc<ComputedValues> { + self.primary.as_ref().unwrap() + } + + /// Whether this element `display` value is `none`. + pub fn is_display_none(&self) -> bool { + self.primary().get_box().clone_display().is_none() + } + + /// Whether this element uses viewport units. + pub fn viewport_unit_usage(&self) -> ViewportUnitUsage { + fn usage_from_flags(flags: ComputedValueFlags) -> ViewportUnitUsage { + if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES) { + return ViewportUnitUsage::FromQuery; + } + if flags.intersects(ComputedValueFlags::USES_VIEWPORT_UNITS) { + return ViewportUnitUsage::FromDeclaration; + } + ViewportUnitUsage::None + } + + let mut usage = usage_from_flags(self.primary().flags); + for pseudo_style in self.pseudos.as_array() { + if let Some(ref pseudo_style) = pseudo_style { + usage = std::cmp::max(usage, usage_from_flags(pseudo_style.flags)); + } + } + + usage + } + + #[cfg(feature = "gecko")] + fn size_of_excluding_cvs(&self, _ops: &mut MallocSizeOfOps) -> usize { + // As the method name suggests, we don't measures the ComputedValues + // here, because they are measured on the C++ side. + + // XXX: measure the EagerPseudoArray itself, but not the ComputedValues + // within it. + + 0 + } +} + +// We manually implement Debug for ElementStyles so that we can avoid the +// verbose stringification of every property in the ComputedValues. We +// substitute the rule node instead. +impl fmt::Debug for ElementStyles { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "ElementStyles {{ primary: {:?}, pseudos: {:?} }}", + self.primary.as_ref().map(|x| &x.rules), + self.pseudos + ) + } +} + +/// Style system data associated with an Element. +/// +/// In Gecko, this hangs directly off the Element. Servo, this is embedded +/// inside of layout data, which itself hangs directly off the Element. In +/// both cases, it is wrapped inside an AtomicRefCell to ensure thread safety. +#[derive(Debug, Default)] +pub struct ElementData { + /// The styles for the element and its pseudo-elements. + pub styles: ElementStyles, + + /// The restyle damage, indicating what kind of layout changes are required + /// afte restyling. + pub damage: RestyleDamage, + + /// The restyle hint, which indicates whether selectors need to be rematched + /// for this element, its children, and its descendants. + pub hint: RestyleHint, + + /// Flags. + pub flags: ElementDataFlags, +} + +// There's one of these per rendered elements so it better be small. +size_of_test!(ElementData, 24); + +/// The kind of restyle that a single element should do. +#[derive(Debug)] +pub enum RestyleKind { + /// We need to run selector matching plus re-cascade, that is, a full + /// restyle. + MatchAndCascade, + /// We need to recascade with some replacement rule, such as the style + /// attribute, or animation rules. + CascadeWithReplacements(RestyleHint), + /// We only need to recascade, for example, because only inherited + /// properties in the parent changed. + CascadeOnly, +} + +impl ElementData { + /// Invalidates style for this element, its descendants, and later siblings, + /// based on the snapshot of the element that we took when attributes or + /// state changed. + pub fn invalidate_style_if_needed<'a, E: TElement>( + &mut self, + element: E, + shared_context: &SharedStyleContext, + stack_limit_checker: Option<&StackLimitChecker>, + selector_caches: &'a mut SelectorCaches, + ) -> InvalidationResult { + // In animation-only restyle we shouldn't touch snapshot at all. + if shared_context.traversal_flags.for_animation_only() { + return InvalidationResult::empty(); + } + + use crate::invalidation::element::invalidator::TreeStyleInvalidator; + use crate::invalidation::element::state_and_attributes::StateAndAttrInvalidationProcessor; + + debug!( + "invalidate_style_if_needed: {:?}, flags: {:?}, has_snapshot: {}, \ + handled_snapshot: {}, pseudo: {:?}", + element, + shared_context.traversal_flags, + element.has_snapshot(), + element.handled_snapshot(), + element.implemented_pseudo_element() + ); + + if !element.has_snapshot() || element.handled_snapshot() { + return InvalidationResult::empty(); + } + + let mut processor = + StateAndAttrInvalidationProcessor::new(shared_context, element, self, selector_caches); + + let invalidator = TreeStyleInvalidator::new(element, stack_limit_checker, &mut processor); + + let result = invalidator.invalidate(); + + unsafe { element.set_handled_snapshot() } + debug_assert!(element.handled_snapshot()); + + result + } + + /// Returns true if this element has styles. + #[inline] + pub fn has_styles(&self) -> bool { + self.styles.primary.is_some() + } + + /// Returns this element's styles as resolved styles to use for sharing. + pub fn share_styles(&self) -> ResolvedElementStyles { + ResolvedElementStyles { + primary: self.share_primary_style(), + pseudos: self.styles.pseudos.clone(), + } + } + + /// Returns this element's primary style as a resolved style to use for sharing. + pub fn share_primary_style(&self) -> PrimaryStyle { + let reused_via_rule_node = self + .flags + .contains(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + + PrimaryStyle { + style: ResolvedStyle(self.styles.primary().clone()), + reused_via_rule_node, + } + } + + /// Sets a new set of styles, returning the old ones. + pub fn set_styles(&mut self, new_styles: ResolvedElementStyles) -> ElementStyles { + if new_styles.primary.reused_via_rule_node { + self.flags + .insert(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } else { + self.flags + .remove(ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE); + } + mem::replace(&mut self.styles, new_styles.into()) + } + + /// Returns the kind of restyling that we're going to need to do on this + /// element, based of the stored restyle hint. + pub fn restyle_kind(&self, shared_context: &SharedStyleContext) -> Option<RestyleKind> { + if shared_context.traversal_flags.for_animation_only() { + return self.restyle_kind_for_animation(shared_context); + } + + let style = match self.styles.primary { + Some(ref s) => s, + None => return Some(RestyleKind::MatchAndCascade), + }; + + let hint = self.hint; + if hint.is_empty() { + return None; + } + + let needs_to_match_self = hint.intersects(RestyleHint::RESTYLE_SELF) || + (hint.intersects(RestyleHint::RESTYLE_SELF_IF_PSEUDO) && style.is_pseudo_style()); + if needs_to_match_self { + return Some(RestyleKind::MatchAndCascade); + } + + if hint.has_replacements() { + debug_assert!( + !hint.has_animation_hint(), + "Animation only restyle hint should have already processed" + ); + return Some(RestyleKind::CascadeWithReplacements( + hint & RestyleHint::replacements(), + )); + } + + let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || + (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && + style + .flags + .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); + if needs_to_recascade_self { + return Some(RestyleKind::CascadeOnly); + } + + None + } + + /// Returns the kind of restyling for animation-only restyle. + fn restyle_kind_for_animation( + &self, + shared_context: &SharedStyleContext, + ) -> Option<RestyleKind> { + debug_assert!(shared_context.traversal_flags.for_animation_only()); + debug_assert!( + self.has_styles(), + "animation traversal doesn't care about unstyled elements" + ); + + // FIXME: We should ideally restyle here, but it is a hack to work around our weird + // animation-only traversal stuff: If we're display: none and the rules we could + // match could change, we consider our style up-to-date. This is because re-cascading with + // and old style doesn't guarantee returning the correct animation style (that's + // bug 1393323). So if our display changed, and it changed from display: none, we would + // incorrectly forget about it and wouldn't be able to correctly style our descendants + // later. + // XXX Figure out if this still makes sense. + let hint = self.hint; + if self.styles.is_display_none() && hint.intersects(RestyleHint::RESTYLE_SELF) { + return None; + } + + let style = self.styles.primary(); + // Return either CascadeWithReplacements or CascadeOnly in case of + // animation-only restyle. I.e. animation-only restyle never does + // selector matching. + if hint.has_animation_hint() { + return Some(RestyleKind::CascadeWithReplacements( + hint & RestyleHint::for_animations(), + )); + } + + let needs_to_recascade_self = hint.intersects(RestyleHint::RECASCADE_SELF) || + (hint.intersects(RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE) && + style + .flags + .contains(ComputedValueFlags::INHERITS_RESET_STYLE)); + if needs_to_recascade_self { + return Some(RestyleKind::CascadeOnly); + } + return None; + } + + /// Drops any restyle state from the element. + /// + /// FIXME(bholley): The only caller of this should probably just assert that + /// the hint is empty and call clear_flags_and_damage(). + #[inline] + pub fn clear_restyle_state(&mut self) { + self.hint = RestyleHint::empty(); + self.clear_restyle_flags_and_damage(); + } + + /// Drops restyle flags and damage from the element. + #[inline] + pub fn clear_restyle_flags_and_damage(&mut self) { + self.damage = RestyleDamage::empty(); + self.flags.remove(ElementDataFlags::WAS_RESTYLED); + } + + /// Mark this element as restyled, which is useful to know whether we need + /// to do a post-traversal. + pub fn set_restyled(&mut self) { + self.flags.insert(ElementDataFlags::WAS_RESTYLED); + self.flags + .remove(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); + } + + /// Returns true if this element was restyled. + #[inline] + pub fn is_restyle(&self) -> bool { + self.flags.contains(ElementDataFlags::WAS_RESTYLED) + } + + /// Mark that we traversed this element without computing any style for it. + pub fn set_traversed_without_styling(&mut self) { + self.flags + .insert(ElementDataFlags::TRAVERSED_WITHOUT_STYLING); + } + + /// Returns whether this element has been part of a restyle. + #[inline] + pub fn contains_restyle_data(&self) -> bool { + self.is_restyle() || !self.hint.is_empty() || !self.damage.is_empty() + } + + /// Returns whether it is safe to perform cousin sharing based on the ComputedValues + /// identity of the primary style in this ElementData. There are a few subtle things + /// to check. + /// + /// First, if a parent element was already styled and we traversed past it without + /// restyling it, that may be because our clever invalidation logic was able to prove + /// that the styles of that element would remain unchanged despite changes to the id + /// or class attributes. However, style sharing relies on the strong guarantee that all + /// the classes and ids up the respective parent chains are identical. As such, if we + /// skipped styling for one (or both) of the parents on this traversal, we can't share + /// styles across cousins. Note that this is a somewhat conservative check. We could + /// tighten it by having the invalidation logic explicitly flag elements for which it + /// ellided styling. + /// + /// Second, we want to only consider elements whose ComputedValues match due to a hit + /// in the style sharing cache, rather than due to the rule-node-based reuse that + /// happens later in the styling pipeline. The former gives us the stronger guarantees + /// we need for style sharing, the latter does not. + pub fn safe_for_cousin_sharing(&self) -> bool { + if self.flags.intersects( + ElementDataFlags::TRAVERSED_WITHOUT_STYLING | + ElementDataFlags::PRIMARY_STYLE_REUSED_VIA_RULE_NODE, + ) { + return false; + } + if !self + .styles + .primary() + .get_box() + .clone_container_type() + .is_normal() + { + return false; + } + true + } + + /// Measures memory usage. + #[cfg(feature = "gecko")] + pub fn size_of_excluding_cvs(&self, ops: &mut MallocSizeOfOps) -> usize { + let n = self.styles.size_of_excluding_cvs(ops); + + // We may measure more fields in the future if DMD says it's worth it. + + n + } +} diff --git a/servo/components/style/dom.rs b/servo/components/style/dom.rs new file mode 100644 index 0000000000..554d79fdb3 --- /dev/null +++ b/servo/components/style/dom.rs @@ -0,0 +1,951 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Types and traits used to access the DOM from style calculation. + +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use crate::applicable_declarations::ApplicableDeclarationBlock; +use crate::context::SharedStyleContext; +#[cfg(feature = "gecko")] +use crate::context::{PostAnimationTasks, UpdateAnimationsTasks}; +use crate::data::ElementData; +use crate::media_queries::Device; +use crate::properties::{AnimationDeclarations, ComputedValues, PropertyDeclarationBlock}; +use crate::selector_parser::{AttrValue, CustomState, Lang, PseudoElement, SelectorImpl}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::stylist::CascadeData; +use crate::values::computed::Display; +use crate::values::AtomIdent; +use crate::WeakAtom; +use atomic_refcell::{AtomicRef, AtomicRefMut}; +use dom::ElementState; +use selectors::matching::{ElementSelectorFlags, QuirksMode, VisitedHandlingMode}; +use selectors::sink::Push; +use selectors::Element as SelectorsElement; +use servo_arc::{Arc, ArcBorrow}; +use std::fmt; +use std::fmt::Debug; +use std::hash::Hash; +use std::ops::Deref; + +pub use style_traits::dom::OpaqueNode; + +/// Simple trait to provide basic information about the type of an element. +/// +/// We avoid exposing the full type id, since computing it in the general case +/// would be difficult for Gecko nodes. +pub trait NodeInfo { + /// Whether this node is an element. + fn is_element(&self) -> bool; + /// Whether this node is a text node. + fn is_text_node(&self) -> bool; +} + +/// A node iterator that only returns node that don't need layout. +pub struct LayoutIterator<T>(pub T); + +impl<T, N> Iterator for LayoutIterator<T> +where + T: Iterator<Item = N>, + N: NodeInfo, +{ + type Item = N; + + fn next(&mut self) -> Option<N> { + loop { + let n = self.0.next()?; + // Filter out nodes that layout should ignore. + if n.is_text_node() || n.is_element() { + return Some(n); + } + } + } +} + +/// An iterator over the DOM children of a node. +pub struct DomChildren<N>(Option<N>); +impl<N> Iterator for DomChildren<N> +where + N: TNode, +{ + type Item = N; + + fn next(&mut self) -> Option<N> { + let n = self.0.take()?; + self.0 = n.next_sibling(); + Some(n) + } +} + +/// An iterator over the DOM descendants of a node in pre-order. +pub struct DomDescendants<N> { + previous: Option<N>, + scope: N, +} + +impl<N> Iterator for DomDescendants<N> +where + N: TNode, +{ + type Item = N; + + #[inline] + fn next(&mut self) -> Option<N> { + let prev = self.previous.take()?; + self.previous = prev.next_in_preorder(self.scope); + self.previous + } +} + +/// The `TDocument` trait, to represent a document node. +pub trait TDocument: Sized + Copy + Clone { + /// The concrete `TNode` type. + type ConcreteNode: TNode<ConcreteDocument = Self>; + + /// Get this document as a `TNode`. + fn as_node(&self) -> Self::ConcreteNode; + + /// Returns whether this document is an HTML document. + fn is_html_document(&self) -> bool; + + /// Returns the quirks mode of this document. + fn quirks_mode(&self) -> QuirksMode; + + /// Get a list of elements with a given ID in this document, sorted by + /// tree position. + /// + /// Can return an error to signal that this list is not available, or also + /// return an empty slice. + fn elements_with_id<'a>( + &self, + _id: &AtomIdent, + ) -> Result<&'a [<Self::ConcreteNode as TNode>::ConcreteElement], ()> + where + Self: 'a, + { + Err(()) + } + + /// This document's shared lock. + fn shared_lock(&self) -> &SharedRwLock; +} + +/// The `TNode` trait. This is the main generic trait over which the style +/// system can be implemented. +pub trait TNode: Sized + Copy + Clone + Debug + NodeInfo + PartialEq { + /// The concrete `TElement` type. + type ConcreteElement: TElement<ConcreteNode = Self>; + + /// The concrete `TDocument` type. + type ConcreteDocument: TDocument<ConcreteNode = Self>; + + /// The concrete `TShadowRoot` type. + type ConcreteShadowRoot: TShadowRoot<ConcreteNode = Self>; + + /// Get this node's parent node. + fn parent_node(&self) -> Option<Self>; + + /// Get this node's first child. + fn first_child(&self) -> Option<Self>; + + /// Get this node's last child. + fn last_child(&self) -> Option<Self>; + + /// Get this node's previous sibling. + fn prev_sibling(&self) -> Option<Self>; + + /// Get this node's next sibling. + fn next_sibling(&self) -> Option<Self>; + + /// Get the owner document of this node. + fn owner_doc(&self) -> Self::ConcreteDocument; + + /// Iterate over the DOM children of a node. + #[inline(always)] + fn dom_children(&self) -> DomChildren<Self> { + DomChildren(self.first_child()) + } + + /// Returns whether the node is attached to a document. + fn is_in_document(&self) -> bool; + + /// Iterate over the DOM children of a node, in preorder. + #[inline(always)] + fn dom_descendants(&self) -> DomDescendants<Self> { + DomDescendants { + previous: Some(*self), + scope: *self, + } + } + + /// Returns the next node after this one, in a pre-order tree-traversal of + /// the subtree rooted at scoped_to. + #[inline] + fn next_in_preorder(&self, scoped_to: Self) -> Option<Self> { + if let Some(c) = self.first_child() { + return Some(c); + } + + let mut current = *self; + loop { + if current == scoped_to { + return None; + } + + if let Some(s) = current.next_sibling() { + return Some(s); + } + + debug_assert!( + current.parent_node().is_some(), + "Not a descendant of the scope?" + ); + current = current.parent_node()?; + } + } + + /// Get this node's parent element from the perspective of a restyle + /// traversal. + fn traversal_parent(&self) -> Option<Self::ConcreteElement>; + + /// Get this node's parent element if present. + fn parent_element(&self) -> Option<Self::ConcreteElement> { + self.parent_node().and_then(|n| n.as_element()) + } + + /// Get this node's parent element, or shadow host if it's a shadow root. + fn parent_element_or_host(&self) -> Option<Self::ConcreteElement> { + let parent = self.parent_node()?; + if let Some(e) = parent.as_element() { + return Some(e); + } + if let Some(root) = parent.as_shadow_root() { + return Some(root.host()); + } + None + } + + /// Converts self into an `OpaqueNode`. + fn opaque(&self) -> OpaqueNode; + + /// A debug id, only useful, mm... for debugging. + fn debug_id(self) -> usize; + + /// Get this node as an element, if it's one. + fn as_element(&self) -> Option<Self::ConcreteElement>; + + /// Get this node as a document, if it's one. + fn as_document(&self) -> Option<Self::ConcreteDocument>; + + /// Get this node as a ShadowRoot, if it's one. + fn as_shadow_root(&self) -> Option<Self::ConcreteShadowRoot>; +} + +/// Wrapper to output the subtree rather than the single node when formatting +/// for Debug. +pub struct ShowSubtree<N: TNode>(pub N); +impl<N: TNode> Debug for ShowSubtree<N> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| write!(f, "{:?}", n), self.0, 1) + } +} + +/// Wrapper to output the subtree along with the ElementData when formatting +/// for Debug. +pub struct ShowSubtreeData<N: TNode>(pub N); +impl<N: TNode> Debug for ShowSubtreeData<N> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| fmt_with_data(f, n), self.0, 1) + } +} + +/// Wrapper to output the subtree along with the ElementData and primary +/// ComputedValues when formatting for Debug. This is extremely verbose. +#[cfg(feature = "servo")] +pub struct ShowSubtreeDataAndPrimaryValues<N: TNode>(pub N); +#[cfg(feature = "servo")] +impl<N: TNode> Debug for ShowSubtreeDataAndPrimaryValues<N> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f, "DOM Subtree:")?; + fmt_subtree(f, &|f, n| fmt_with_data_and_primary_values(f, n), self.0, 1) + } +} + +fn fmt_with_data<N: TNode>(f: &mut fmt::Formatter, n: N) -> fmt::Result { + if let Some(el) = n.as_element() { + write!( + f, + "{:?} dd={} aodd={} data={:?}", + el, + el.has_dirty_descendants(), + el.has_animation_only_dirty_descendants(), + el.borrow_data(), + ) + } else { + write!(f, "{:?}", n) + } +} + +#[cfg(feature = "servo")] +fn fmt_with_data_and_primary_values<N: TNode>(f: &mut fmt::Formatter, n: N) -> fmt::Result { + if let Some(el) = n.as_element() { + let dd = el.has_dirty_descendants(); + let aodd = el.has_animation_only_dirty_descendants(); + let data = el.borrow_data(); + let values = data.as_ref().and_then(|d| d.styles.get_primary()); + write!( + f, + "{:?} dd={} aodd={} data={:?} values={:?}", + el, dd, aodd, &data, values + ) + } else { + write!(f, "{:?}", n) + } +} + +fn fmt_subtree<F, N: TNode>(f: &mut fmt::Formatter, stringify: &F, n: N, indent: u32) -> fmt::Result +where + F: Fn(&mut fmt::Formatter, N) -> fmt::Result, +{ + for _ in 0..indent { + write!(f, " ")?; + } + stringify(f, n)?; + if let Some(e) = n.as_element() { + for kid in e.traversal_children() { + writeln!(f, "")?; + fmt_subtree(f, stringify, kid, indent + 1)?; + } + } + + Ok(()) +} + +/// The ShadowRoot trait. +pub trait TShadowRoot: Sized + Copy + Clone + Debug + PartialEq { + /// The concrete node type. + type ConcreteNode: TNode<ConcreteShadowRoot = Self>; + + /// Get this ShadowRoot as a node. + fn as_node(&self) -> Self::ConcreteNode; + + /// Get the shadow host that hosts this ShadowRoot. + fn host(&self) -> <Self::ConcreteNode as TNode>::ConcreteElement; + + /// Get the style data for this ShadowRoot. + fn style_data<'a>(&self) -> Option<&'a CascadeData> + where + Self: 'a; + + /// Get the list of shadow parts for this shadow root. + fn parts<'a>(&self) -> &[<Self::ConcreteNode as TNode>::ConcreteElement] + where + Self: 'a, + { + &[] + } + + /// Get a list of elements with a given ID in this shadow root, sorted by + /// tree position. + /// + /// Can return an error to signal that this list is not available, or also + /// return an empty slice. + fn elements_with_id<'a>( + &self, + _id: &AtomIdent, + ) -> Result<&'a [<Self::ConcreteNode as TNode>::ConcreteElement], ()> + where + Self: 'a, + { + Err(()) + } +} + +/// The element trait, the main abstraction the style crate acts over. +pub trait TElement: + Eq + PartialEq + Debug + Hash + Sized + Copy + Clone + SelectorsElement<Impl = SelectorImpl> +{ + /// The concrete node type. + type ConcreteNode: TNode<ConcreteElement = Self>; + + /// A concrete children iterator type in order to iterate over the `Node`s. + /// + /// TODO(emilio): We should eventually replace this with the `impl Trait` + /// syntax. + type TraversalChildrenIterator: Iterator<Item = Self::ConcreteNode>; + + /// Get this element as a node. + fn as_node(&self) -> Self::ConcreteNode; + + /// A debug-only check that the device's owner doc matches the actual doc + /// we're the root of. + /// + /// Otherwise we may set document-level state incorrectly, like the root + /// font-size used for rem units. + fn owner_doc_matches_for_testing(&self, _: &Device) -> bool { + true + } + + /// Whether this element should match user and content rules. + /// + /// We use this for Native Anonymous Content in Gecko. + fn matches_user_and_content_rules(&self) -> bool { + true + } + + /// Returns the depth of this element in the DOM. + fn depth(&self) -> usize { + let mut depth = 0; + let mut curr = *self; + while let Some(parent) = curr.traversal_parent() { + depth += 1; + curr = parent; + } + + depth + } + + /// Get this node's parent element from the perspective of a restyle + /// traversal. + fn traversal_parent(&self) -> Option<Self> { + self.as_node().traversal_parent() + } + + /// Get this node's children from the perspective of a restyle traversal. + fn traversal_children(&self) -> LayoutIterator<Self::TraversalChildrenIterator>; + + /// Returns the parent element we should inherit from. + /// + /// This is pretty much always the parent element itself, except in the case + /// of Gecko's Native Anonymous Content, which uses the traversal parent + /// (i.e. the flattened tree parent) and which also may need to find the + /// closest non-NAC ancestor. + fn inheritance_parent(&self) -> Option<Self> { + self.parent_element() + } + + /// The ::before pseudo-element of this element, if it exists. + fn before_pseudo_element(&self) -> Option<Self> { + None + } + + /// The ::after pseudo-element of this element, if it exists. + fn after_pseudo_element(&self) -> Option<Self> { + None + } + + /// The ::marker pseudo-element of this element, if it exists. + fn marker_pseudo_element(&self) -> Option<Self> { + None + } + + /// Execute `f` for each anonymous content child (apart from ::before and + /// ::after) whose originating element is `self`. + fn each_anonymous_content_child<F>(&self, _f: F) + where + F: FnMut(Self), + { + } + + /// Return whether this element is an element in the HTML namespace. + fn is_html_element(&self) -> bool; + + /// Return whether this element is an element in the MathML namespace. + fn is_mathml_element(&self) -> bool; + + /// Return whether this element is an element in the SVG namespace. + fn is_svg_element(&self) -> bool; + + /// Return whether this element is an element in the XUL namespace. + fn is_xul_element(&self) -> bool { + false + } + + /// Return the list of slotted nodes of this node. + fn slotted_nodes(&self) -> &[Self::ConcreteNode] { + &[] + } + + /// Get this element's style attribute. + fn style_attribute(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>; + + /// Unset the style attribute's dirty bit. + /// Servo doesn't need to manage ditry bit for style attribute. + fn unset_dirty_style_attribute(&self) {} + + /// Get this element's SMIL override declarations. + fn smil_override(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> { + None + } + + /// Get the combined animation and transition rules. + /// + /// FIXME(emilio): Is this really useful? + fn animation_declarations(&self, context: &SharedStyleContext) -> AnimationDeclarations { + if !self.may_have_animations() { + return Default::default(); + } + + AnimationDeclarations { + animations: self.animation_rule(context), + transitions: self.transition_rule(context), + } + } + + /// Get this element's animation rule. + fn animation_rule( + &self, + _: &SharedStyleContext, + ) -> Option<Arc<Locked<PropertyDeclarationBlock>>>; + + /// Get this element's transition rule. + fn transition_rule( + &self, + context: &SharedStyleContext, + ) -> Option<Arc<Locked<PropertyDeclarationBlock>>>; + + /// Get this element's state, for non-tree-structural pseudos. + fn state(&self) -> ElementState; + + /// Returns whether this element's CustomStateSet contains a given state. + fn has_custom_state(&self, _state: &CustomState) -> bool { + false + } + + /// Returns whether this element has a `part` attribute. + fn has_part_attr(&self) -> bool; + + /// Returns whether this element exports any part from its shadow tree. + fn exports_any_part(&self) -> bool; + + /// The ID for this element. + fn id(&self) -> Option<&WeakAtom>; + + /// Internal iterator for the classes of this element. + fn each_class<F>(&self, callback: F) + where + F: FnMut(&AtomIdent); + + /// Internal iterator for the part names of this element. + fn each_part<F>(&self, _callback: F) + where + F: FnMut(&AtomIdent), + { + } + + /// Internal iterator for the attribute names of this element. + fn each_attr_name<F>(&self, callback: F) + where + F: FnMut(&AtomIdent); + + /// Internal iterator for the part names that this element exports for a + /// given part name. + fn each_exported_part<F>(&self, _name: &AtomIdent, _callback: F) + where + F: FnMut(&AtomIdent), + { + } + + /// Whether a given element may generate a pseudo-element. + /// + /// This is useful to avoid computing, for example, pseudo styles for + /// `::-first-line` or `::-first-letter`, when we know it won't affect us. + /// + /// TODO(emilio, bz): actually implement the logic for it. + fn may_generate_pseudo(&self, pseudo: &PseudoElement, _primary_style: &ComputedValues) -> bool { + // ::before/::after are always supported for now, though we could try to + // optimize out leaf elements. + + // ::first-letter and ::first-line are only supported for block-inside + // things, and only in Gecko, not Servo. Unfortunately, Gecko has + // block-inside things that might have any computed display value due to + // things like fieldsets, legends, etc. Need to figure out how this + // should work. + debug_assert!( + pseudo.is_eager(), + "Someone called may_generate_pseudo with a non-eager pseudo." + ); + true + } + + /// Returns true if this element may have a descendant needing style processing. + /// + /// Note that we cannot guarantee the existence of such an element, because + /// it may have been removed from the DOM between marking it for restyle and + /// the actual restyle traversal. + fn has_dirty_descendants(&self) -> bool; + + /// Returns whether state or attributes that may change style have changed + /// on the element, and thus whether the element has been snapshotted to do + /// restyle hint computation. + fn has_snapshot(&self) -> bool; + + /// Returns whether the current snapshot if present has been handled. + fn handled_snapshot(&self) -> bool; + + /// Flags this element as having handled already its snapshot. + unsafe fn set_handled_snapshot(&self); + + /// Returns whether the element's styles are up-to-date after traversal + /// (i.e. in post traversal). + fn has_current_styles(&self, data: &ElementData) -> bool { + if self.has_snapshot() && !self.handled_snapshot() { + return false; + } + + data.has_styles() && + // TODO(hiro): When an animating element moved into subtree of + // contenteditable element, there remains animation restyle hints in + // post traversal. It's generally harmless since the hints will be + // processed in a next styling but ideally it should be processed soon. + // + // Without this, we get failures in: + // layout/style/crashtests/1383319.html + // layout/style/crashtests/1383001.html + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=1389675 tracks fixing + // this. + !data.hint.has_non_animation_invalidations() + } + + /// Flag that this element has a descendant for style processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn set_dirty_descendants(&self); + + /// Flag that this element has no descendant for style processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn unset_dirty_descendants(&self); + + /// Similar to the dirty_descendants but for representing a descendant of + /// the element needs to be updated in animation-only traversal. + fn has_animation_only_dirty_descendants(&self) -> bool { + false + } + + /// Flag that this element has a descendant for animation-only restyle + /// processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn set_animation_only_dirty_descendants(&self) {} + + /// Flag that this element has no descendant for animation-only restyle processing. + /// + /// Only safe to call with exclusive access to the element. + unsafe fn unset_animation_only_dirty_descendants(&self) {} + + /// Clear all bits related describing the dirtiness of descendants. + /// + /// In Gecko, this corresponds to the regular dirty descendants bit, the + /// animation-only dirty descendants bit, and the lazy frame construction + /// descendants bit. + unsafe fn clear_descendant_bits(&self) { + self.unset_dirty_descendants(); + } + + /// Returns true if this element is a visited link. + /// + /// Servo doesn't support visited styles yet. + fn is_visited_link(&self) -> bool { + false + } + + /// Returns the pseudo-element implemented by this element, if any. + /// + /// Gecko traverses pseudo-elements during the style traversal, and we need + /// to know this so we can properly grab the pseudo-element style from the + /// parent element. + /// + /// Note that we still need to compute the pseudo-elements before-hand, + /// given otherwise we don't know if we need to create an element or not. + /// + /// Servo doesn't have to deal with this. + fn implemented_pseudo_element(&self) -> Option<PseudoElement> { + None + } + + /// Atomically stores the number of children of this node that we will + /// need to process during bottom-up traversal. + fn store_children_to_process(&self, n: isize); + + /// Atomically notes that a child has been processed during bottom-up + /// traversal. Returns the number of children left to process. + fn did_process_child(&self) -> isize; + + /// Gets a reference to the ElementData container, or creates one. + /// + /// Unsafe because it can race to allocate and leak if not used with + /// exclusive access to the element. + unsafe fn ensure_data(&self) -> AtomicRefMut<ElementData>; + + /// Clears the element data reference, if any. + /// + /// Unsafe following the same reasoning as ensure_data. + unsafe fn clear_data(&self); + + /// Whether there is an ElementData container. + fn has_data(&self) -> bool; + + /// Immutably borrows the ElementData. + fn borrow_data(&self) -> Option<AtomicRef<ElementData>>; + + /// Mutably borrows the ElementData. + fn mutate_data(&self) -> Option<AtomicRefMut<ElementData>>; + + /// Whether we should skip any root- or item-based display property + /// blockification on this element. (This function exists so that Gecko + /// native anonymous content can opt out of this style fixup.) + fn skip_item_display_fixup(&self) -> bool; + + /// In Gecko, element has a flag that represents the element may have + /// any type of animations or not to bail out animation stuff early. + /// Whereas Servo doesn't have such flag. + fn may_have_animations(&self) -> bool; + + /// Creates a task to update various animation state on a given (pseudo-)element. + #[cfg(feature = "gecko")] + fn update_animations( + &self, + before_change_style: Option<Arc<ComputedValues>>, + tasks: UpdateAnimationsTasks, + ); + + /// Creates a task to process post animation on a given element. + #[cfg(feature = "gecko")] + fn process_post_animation(&self, tasks: PostAnimationTasks); + + /// Returns true if the element has relevant animations. Relevant + /// animations are those animations that are affecting the element's style + /// or are scheduled to do so in the future. + fn has_animations(&self, context: &SharedStyleContext) -> bool; + + /// Returns true if the element has a CSS animation. The `context` and `pseudo_element` + /// arguments are only used by Servo, since it stores animations globally and pseudo-elements + /// are not in the DOM. + fn has_css_animations( + &self, + context: &SharedStyleContext, + pseudo_element: Option<PseudoElement>, + ) -> bool; + + /// Returns true if the element has a CSS transition (including running transitions and + /// completed transitions). The `context` and `pseudo_element` arguments are only used + /// by Servo, since it stores animations globally and pseudo-elements are not in the DOM. + fn has_css_transitions( + &self, + context: &SharedStyleContext, + pseudo_element: Option<PseudoElement>, + ) -> bool; + + /// Returns true if the element has animation restyle hints. + fn has_animation_restyle_hints(&self) -> bool { + let data = match self.borrow_data() { + Some(d) => d, + None => return false, + }; + return data.hint.has_animation_hint(); + } + + /// The shadow root this element is a host of. + fn shadow_root(&self) -> Option<<Self::ConcreteNode as TNode>::ConcreteShadowRoot>; + + /// The shadow root which roots the subtree this element is contained in. + fn containing_shadow(&self) -> Option<<Self::ConcreteNode as TNode>::ConcreteShadowRoot>; + + /// Return the element which we can use to look up rules in the selector + /// maps. + /// + /// This is always the element itself, except in the case where we are an + /// element-backed pseudo-element, in which case we return the originating + /// element. + fn rule_hash_target(&self) -> Self { + if self.is_pseudo_element() { + self.pseudo_element_originating_element() + .expect("Trying to collect rules for a detached pseudo-element") + } else { + *self + } + } + + /// Executes the callback for each applicable style rule data which isn't + /// the main document's data (which stores UA / author rules). + /// + /// The element passed to the callback is the containing shadow host for the + /// data if it comes from Shadow DOM. + /// + /// Returns whether normal document author rules should apply. + /// + /// TODO(emilio): We could separate the invalidation data for elements + /// matching in other scopes to avoid over-invalidation. + fn each_applicable_non_document_style_rule_data<'a, F>(&self, mut f: F) -> bool + where + Self: 'a, + F: FnMut(&'a CascadeData, Self), + { + use crate::rule_collector::containing_shadow_ignoring_svg_use; + + let target = self.rule_hash_target(); + let matches_user_and_content_rules = target.matches_user_and_content_rules(); + let mut doc_rules_apply = matches_user_and_content_rules; + + // Use the same rules to look for the containing host as we do for rule + // collection. + if let Some(shadow) = containing_shadow_ignoring_svg_use(target) { + doc_rules_apply = false; + if let Some(data) = shadow.style_data() { + f(data, shadow.host()); + } + } + + if let Some(shadow) = target.shadow_root() { + if let Some(data) = shadow.style_data() { + f(data, shadow.host()); + } + } + + let mut current = target.assigned_slot(); + while let Some(slot) = current { + // Slots can only have assigned nodes when in a shadow tree. + let shadow = slot.containing_shadow().unwrap(); + if let Some(data) = shadow.style_data() { + if data.any_slotted_rule() { + f(data, shadow.host()); + } + } + current = slot.assigned_slot(); + } + + if target.has_part_attr() { + if let Some(mut inner_shadow) = target.containing_shadow() { + loop { + let inner_shadow_host = inner_shadow.host(); + match inner_shadow_host.containing_shadow() { + Some(shadow) => { + if let Some(data) = shadow.style_data() { + if data.any_part_rule() { + f(data, shadow.host()) + } + } + // TODO: Could be more granular. + if !inner_shadow_host.exports_any_part() { + break; + } + inner_shadow = shadow; + }, + None => { + // TODO(emilio): Should probably distinguish with + // MatchesDocumentRules::{No,Yes,IfPart} or something so that we could + // skip some work. + doc_rules_apply = matches_user_and_content_rules; + break; + }, + } + } + } + } + + doc_rules_apply + } + + /// Returns true if one of the transitions needs to be updated on this element. We check all + /// the transition properties to make sure that updating transitions is necessary. + /// This method should only be called if might_needs_transitions_update returns true when + /// passed the same parameters. + #[cfg(feature = "gecko")] + fn needs_transitions_update( + &self, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + ) -> bool; + + /// Returns the value of the `xml:lang=""` attribute (or, if appropriate, + /// the `lang=""` attribute) on this element. + fn lang_attr(&self) -> Option<AttrValue>; + + /// Returns whether this element's language matches the language tag + /// `value`. If `override_lang` is not `None`, it specifies the value + /// of the `xml:lang=""` or `lang=""` attribute to use in place of + /// looking at the element and its ancestors. (This argument is used + /// to implement matching of `:lang()` against snapshots.) + fn match_element_lang(&self, override_lang: Option<Option<AttrValue>>, value: &Lang) -> bool; + + /// Returns whether this element is the main body element of the HTML + /// document it is on. + fn is_html_document_body_element(&self) -> bool; + + /// Generate the proper applicable declarations due to presentational hints, + /// and insert them into `hints`. + fn synthesize_presentational_hints_for_legacy_attributes<V>( + &self, + visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push<ApplicableDeclarationBlock>; + + /// Returns element's local name. + fn local_name(&self) -> &<SelectorImpl as selectors::parser::SelectorImpl>::BorrowedLocalName; + + /// Returns element's namespace. + fn namespace(&self) + -> &<SelectorImpl as selectors::parser::SelectorImpl>::BorrowedNamespaceUrl; + + /// Returns the size of the element to be used in container size queries. + /// This will usually be the size of the content area of the primary box, + /// but can be None if there is no box or if some axis lacks size containment. + fn query_container_size( + &self, + display: &Display, + ) -> euclid::default::Size2D<Option<app_units::Au>>; + + /// Returns true if the element has all of specified selector flags. + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool; + + /// Returns the search direction for relative selector invalidation, if it is on the search path. + fn relative_selector_search_direction(&self) -> Option<ElementSelectorFlags>; +} + +/// TNode and TElement aren't Send because we want to be careful and explicit +/// about our parallel traversal. However, there are certain situations +/// (including but not limited to the traversal) where we need to send DOM +/// objects to other threads. +/// +/// That's the reason why `SendNode` exists. +#[derive(Clone, Debug, PartialEq)] +pub struct SendNode<N: TNode>(N); +unsafe impl<N: TNode> Send for SendNode<N> {} +impl<N: TNode> SendNode<N> { + /// Unsafely construct a SendNode. + pub unsafe fn new(node: N) -> Self { + SendNode(node) + } +} +impl<N: TNode> Deref for SendNode<N> { + type Target = N; + fn deref(&self) -> &N { + &self.0 + } +} + +/// Same reason as for the existence of SendNode, SendElement does the proper +/// things for a given `TElement`. +#[derive(Debug, Eq, Hash, PartialEq)] +pub struct SendElement<E: TElement>(E); +unsafe impl<E: TElement> Send for SendElement<E> {} +impl<E: TElement> SendElement<E> { + /// Unsafely construct a SendElement. + pub unsafe fn new(el: E) -> Self { + SendElement(el) + } +} +impl<E: TElement> Deref for SendElement<E> { + type Target = E; + fn deref(&self) -> &E { + &self.0 + } +} diff --git a/servo/components/style/dom_apis.rs b/servo/components/style/dom_apis.rs new file mode 100644 index 0000000000..cdc106e1ad --- /dev/null +++ b/servo/components/style/dom_apis.rs @@ -0,0 +1,814 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic implementations of some DOM APIs so they can be shared between Servo +//! and Gecko. + +use crate::context::QuirksMode; +use crate::dom::{TDocument, TElement, TNode, TShadowRoot}; +use crate::invalidation::element::invalidation_map::Dependency; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, Invalidation, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{InvalidationProcessor, InvalidationVector}; +use crate::selector_parser::SelectorImpl; +use crate::values::AtomIdent; +use selectors::attr::CaseSensitivity; +use selectors::attr::{AttrSelectorOperation, NamespaceConstraint}; +use selectors::matching::{ + self, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, + SelectorCaches, +}; +use selectors::parser::{Combinator, Component, LocalName}; +use selectors::{Element, SelectorList}; +use smallvec::SmallVec; + +/// <https://dom.spec.whatwg.org/#dom-element-matches> +pub fn element_matches<E>( + element: &E, + selector_list: &SelectorList<E::Impl>, + quirks_mode: QuirksMode, +) -> bool +where + E: Element, +{ + let mut selector_caches = SelectorCaches::default(); + + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut selector_caches, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + context.scope_element = Some(element.opaque()); + context.current_host = element.containing_shadow_host().map(|e| e.opaque()); + matching::matches_selector_list(selector_list, element, &mut context) +} + +/// <https://dom.spec.whatwg.org/#dom-element-closest> +pub fn element_closest<E>( + element: E, + selector_list: &SelectorList<E::Impl>, + quirks_mode: QuirksMode, +) -> Option<E> +where + E: Element, +{ + let mut selector_caches = SelectorCaches::default(); + + let mut context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut selector_caches, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + context.scope_element = Some(element.opaque()); + context.current_host = element.containing_shadow_host().map(|e| e.opaque()); + + let mut current = Some(element); + while let Some(element) = current.take() { + if matching::matches_selector_list(selector_list, &element, &mut context) { + return Some(element); + } + current = element.parent_element(); + } + + return None; +} + +/// A selector query abstraction, in order to be generic over QuerySelector and +/// QuerySelectorAll. +pub trait SelectorQuery<E: TElement> { + /// The output of the query. + type Output; + + /// Whether the query should stop after the first element has been matched. + fn should_stop_after_first_match() -> bool; + + /// Append an element matching after the first query. + fn append_element(output: &mut Self::Output, element: E); + + /// Returns true if the output is empty. + fn is_empty(output: &Self::Output) -> bool; +} + +/// The result of a querySelectorAll call. +pub type QuerySelectorAllResult<E> = SmallVec<[E; 128]>; + +/// A query for all the elements in a subtree. +pub struct QueryAll; + +impl<E: TElement> SelectorQuery<E> for QueryAll { + type Output = QuerySelectorAllResult<E>; + + fn should_stop_after_first_match() -> bool { + false + } + + fn append_element(output: &mut Self::Output, element: E) { + output.push(element); + } + + fn is_empty(output: &Self::Output) -> bool { + output.is_empty() + } +} + +/// A query for the first in-tree match of all the elements in a subtree. +pub struct QueryFirst; + +impl<E: TElement> SelectorQuery<E> for QueryFirst { + type Output = Option<E>; + + fn should_stop_after_first_match() -> bool { + true + } + + fn append_element(output: &mut Self::Output, element: E) { + if output.is_none() { + *output = Some(element) + } + } + + fn is_empty(output: &Self::Output) -> bool { + output.is_none() + } +} + +struct QuerySelectorProcessor<'a, 'b, E, Q> +where + E: TElement + 'a, + Q: SelectorQuery<E>, + Q::Output: 'a, +{ + results: &'a mut Q::Output, + matching_context: MatchingContext<'b, E::Impl>, + traversal_map: SiblingTraversalMap<E>, + dependencies: &'a [Dependency], +} + +impl<'a, 'b, E, Q> InvalidationProcessor<'a, 'b, E> for QuerySelectorProcessor<'a, 'b, E, Q> +where + E: TElement + 'a, + Q: SelectorQuery<E>, + Q::Output: 'a, +{ + fn light_tree_only(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, _: &Dependency, _: E) -> bool { + debug_assert!( + false, + "How? We should only have parent-less dependencies here!" + ); + true + } + + fn collect_invalidations( + &mut self, + element: E, + self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + _sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + // TODO(emilio): If the element is not a root element, and + // selector_list has any descendant combinator, we need to do extra work + // in order to handle properly things like: + // + // <div id="a"> + // <div id="b"> + // <div id="c"></div> + // </div> + // </div> + // + // b.querySelector('#a div'); // Should return "c". + // + // For now, assert it's a root element. + debug_assert!(element.parent_element().is_none()); + + let target_vector = if self.matching_context.scope_element.is_some() { + &mut descendant_invalidations.dom_descendants + } else { + self_invalidations + }; + + for dependency in self.dependencies.iter() { + target_vector.push(Invalidation::new( + dependency, + self.matching_context.current_host.clone(), + )) + } + + false + } + + fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + &self.traversal_map + } + + fn should_process_descendants(&mut self, _: E) -> bool { + if Q::should_stop_after_first_match() { + return Q::is_empty(&self.results); + } + + true + } + + fn invalidated_self(&mut self, e: E) { + Q::append_element(self.results, e); + } + + fn invalidated_sibling(&mut self, e: E, _of: E) { + Q::append_element(self.results, e); + } + + fn recursion_limit_exceeded(&mut self, _e: E) {} + fn invalidated_descendants(&mut self, _e: E, _child: E) {} +} + +fn collect_all_elements<E, Q, F>(root: E::ConcreteNode, results: &mut Q::Output, mut filter: F) +where + E: TElement, + Q: SelectorQuery<E>, + F: FnMut(E) -> bool, +{ + for node in root.dom_descendants() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if !filter(element) { + continue; + } + + Q::append_element(results, element); + if Q::should_stop_after_first_match() { + return; + } + } +} + +/// Returns whether a given element connected to `root` is descendant of `root`. +/// +/// NOTE(emilio): if root == element, this returns false. +fn connected_element_is_descendant_of<E>(element: E, root: E::ConcreteNode) -> bool +where + E: TElement, +{ + // Optimize for when the root is a document or a shadow root and the element + // is connected to that root. + if root.as_document().is_some() { + debug_assert!(element.as_node().is_in_document(), "Not connected?"); + debug_assert_eq!( + root, + root.owner_doc().as_node(), + "Where did this element come from?", + ); + return true; + } + + if root.as_shadow_root().is_some() { + debug_assert_eq!( + element.containing_shadow().unwrap().as_node(), + root, + "Not connected?" + ); + return true; + } + + let mut current = element.as_node().parent_node(); + while let Some(n) = current.take() { + if n == root { + return true; + } + + current = n.parent_node(); + } + false +} + +/// Fast path for iterating over every element with a given id in the document +/// or shadow root that `root` is connected to. +fn fast_connected_elements_with_id<'a, N>( + root: N, + id: &AtomIdent, + case_sensitivity: CaseSensitivity, +) -> Result<&'a [N::ConcreteElement], ()> +where + N: TNode + 'a, +{ + if case_sensitivity != CaseSensitivity::CaseSensitive { + return Err(()); + } + + if root.is_in_document() { + return root.owner_doc().elements_with_id(id); + } + + if let Some(shadow) = root.as_shadow_root() { + return shadow.elements_with_id(id); + } + + if let Some(shadow) = root.as_element().and_then(|e| e.containing_shadow()) { + return shadow.elements_with_id(id); + } + + Err(()) +} + +/// Collects elements with a given id under `root`, that pass `filter`. +fn collect_elements_with_id<E, Q, F>( + root: E::ConcreteNode, + id: &AtomIdent, + results: &mut Q::Output, + class_and_id_case_sensitivity: CaseSensitivity, + mut filter: F, +) where + E: TElement, + Q: SelectorQuery<E>, + F: FnMut(E) -> bool, +{ + let elements = match fast_connected_elements_with_id(root, id, class_and_id_case_sensitivity) { + Ok(elements) => elements, + Err(()) => { + collect_all_elements::<E, Q, _>(root, results, |e| { + e.has_id(id, class_and_id_case_sensitivity) && filter(e) + }); + + return; + }, + }; + + for element in elements { + // If the element is not an actual descendant of the root, even though + // it's connected, we don't really care about it. + if !connected_element_is_descendant_of(*element, root) { + continue; + } + + if !filter(*element) { + continue; + } + + Q::append_element(results, *element); + if Q::should_stop_after_first_match() { + break; + } + } +} + +fn has_attr<E>(element: E, local_name: &AtomIdent) -> bool +where + E: TElement, +{ + let mut found = false; + element.each_attr_name(|name| found |= name == local_name); + found +} + +#[inline(always)] +fn local_name_matches<E>(element: E, local_name: &LocalName<E::Impl>) -> bool +where + E: TElement, +{ + let LocalName { + ref name, + ref lower_name, + } = *local_name; + + let chosen_name = if name == lower_name || element.is_html_element_in_html_document() { + lower_name + } else { + name + }; + + element.local_name() == &**chosen_name +} + +fn get_attr_name(component: &Component<SelectorImpl>) -> Option<&AtomIdent> { + let (name, name_lower) = match component { + Component::AttributeInNoNamespace { ref local_name, .. } => return Some(local_name), + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + .. + } => (local_name, local_name_lower), + Component::AttributeOther(ref attr) => (&attr.local_name, &attr.local_name_lower), + _ => return None, + }; + if name != name_lower { + return None; // TODO: Maybe optimize this? + } + Some(name) +} + +fn get_id(component: &Component<SelectorImpl>) -> Option<&AtomIdent> { + use selectors::attr::AttrSelectorOperator; + Some(match component { + Component::ID(ref id) => id, + Component::AttributeInNoNamespace { + ref operator, + ref local_name, + ref value, + .. + } => { + if *local_name != local_name!("id") { + return None; + } + if *operator != AttrSelectorOperator::Equal { + return None; + } + AtomIdent::cast(&value.0) + }, + _ => return None, + }) +} + +/// Fast paths for querySelector with a single simple selector. +fn query_selector_single_query<E, Q>( + root: E::ConcreteNode, + component: &Component<E::Impl>, + results: &mut Q::Output, + class_and_id_case_sensitivity: CaseSensitivity, +) -> Result<(), ()> +where + E: TElement, + Q: SelectorQuery<E>, +{ + match *component { + Component::ExplicitUniversalType => { + collect_all_elements::<E, Q, _>(root, results, |_| true) + }, + Component::Class(ref class) => collect_all_elements::<E, Q, _>(root, results, |element| { + element.has_class(class, class_and_id_case_sensitivity) + }), + Component::LocalName(ref local_name) => { + collect_all_elements::<E, Q, _>(root, results, |element| { + local_name_matches(element, local_name) + }) + }, + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + } => collect_all_elements::<E, Q, _>(root, results, |element| { + element.has_attr_in_no_namespace(matching::select_name( + &element, + local_name, + local_name_lower, + )) + }), + Component::AttributeInNoNamespace { + ref local_name, + ref value, + operator, + case_sensitivity, + } => { + let empty_namespace = selectors::parser::namespace_empty_string::<E::Impl>(); + let namespace_constraint = NamespaceConstraint::Specific(&empty_namespace); + collect_all_elements::<E, Q, _>(root, results, |element| { + element.attr_matches( + &namespace_constraint, + local_name, + &AttrSelectorOperation::WithValue { + operator, + case_sensitivity: matching::to_unconditional_case_sensitivity( + case_sensitivity, + &element, + ), + value, + }, + ) + }) + }, + ref other => { + let id = match get_id(other) { + Some(id) => id, + // TODO(emilio): More fast paths? + None => return Err(()), + }; + collect_elements_with_id::<E, Q, _>( + root, + id, + results, + class_and_id_case_sensitivity, + |_| true, + ); + }, + } + + Ok(()) +} + +enum SimpleFilter<'a> { + Class(&'a AtomIdent), + Attr(&'a AtomIdent), + LocalName(&'a LocalName<SelectorImpl>), +} + +/// Fast paths for a given selector query. +/// +/// When there's only one component, we go directly to +/// `query_selector_single_query`, otherwise, we try to optimize by looking just +/// at the subtrees rooted at ids in the selector, and otherwise we try to look +/// up by class name or local name in the rightmost compound. +/// +/// FIXME(emilio, nbp): This may very well be a good candidate for code to be +/// replaced by HolyJit :) +fn query_selector_fast<E, Q>( + root: E::ConcreteNode, + selector_list: &SelectorList<E::Impl>, + results: &mut Q::Output, + matching_context: &mut MatchingContext<E::Impl>, +) -> Result<(), ()> +where + E: TElement, + Q: SelectorQuery<E>, +{ + // We need to return elements in document order, and reordering them + // afterwards is kinda silly. + if selector_list.len() > 1 { + return Err(()); + } + + let selector = &selector_list.slice()[0]; + let class_and_id_case_sensitivity = matching_context.classes_and_ids_case_sensitivity(); + // Let's just care about the easy cases for now. + if selector.len() == 1 { + if query_selector_single_query::<E, Q>( + root, + selector.iter().next().unwrap(), + results, + class_and_id_case_sensitivity, + ) + .is_ok() + { + return Ok(()); + } + } + + let mut iter = selector.iter(); + let mut combinator: Option<Combinator> = None; + + // We want to optimize some cases where there's no id involved whatsoever, + // like `.foo .bar`, but we don't want to make `#foo .bar` slower because of + // that. + let mut simple_filter = None; + + 'selector_loop: loop { + debug_assert!(combinator.map_or(true, |c| !c.is_sibling())); + + 'component_loop: for component in &mut iter { + match *component { + Component::Class(ref class) => { + if combinator.is_none() { + simple_filter = Some(SimpleFilter::Class(class)); + } + }, + Component::LocalName(ref local_name) => { + if combinator.is_none() { + // Prefer to look at class rather than local-name if + // both are present. + if let Some(SimpleFilter::Class(..)) = simple_filter { + continue; + } + simple_filter = Some(SimpleFilter::LocalName(local_name)); + } + }, + ref other => { + if let Some(id) = get_id(other) { + if combinator.is_none() { + // In the rightmost compound, just find descendants of root that match + // the selector list with that id. + collect_elements_with_id::<E, Q, _>( + root, + id, + results, + class_and_id_case_sensitivity, + |e| { + matching::matches_selector_list( + selector_list, + &e, + matching_context, + ) + }, + ); + return Ok(()); + } + + let elements = fast_connected_elements_with_id( + root, + id, + class_and_id_case_sensitivity, + )?; + if elements.is_empty() { + return Ok(()); + } + + // Results need to be in document order. Let's not bother + // reordering or deduplicating nodes, which we would need to + // do if one element with the given id were a descendant of + // another element with that given id. + if !Q::should_stop_after_first_match() && elements.len() > 1 { + continue; + } + + for element in elements { + // If the element is not a descendant of the root, then + // it may have descendants that match our selector that + // _are_ descendants of the root, and other descendants + // that match our selector that are _not_. + // + // So we can't just walk over the element's descendants + // and match the selector against all of them, nor can + // we skip looking at this element's descendants. + // + // Give up on trying to optimize based on this id and + // keep walking our selector. + if !connected_element_is_descendant_of(*element, root) { + continue 'component_loop; + } + + query_selector_slow::<E, Q>( + element.as_node(), + selector_list, + results, + matching_context, + ); + + if Q::should_stop_after_first_match() && !Q::is_empty(&results) { + break; + } + } + + return Ok(()); + } + if combinator.is_none() && simple_filter.is_none() { + if let Some(attr_name) = get_attr_name(other) { + simple_filter = Some(SimpleFilter::Attr(attr_name)); + } + } + }, + } + } + + loop { + let next_combinator = match iter.next_sequence() { + None => break 'selector_loop, + Some(c) => c, + }; + + // We don't want to scan stuff affected by sibling combinators, + // given we scan the subtree of elements with a given id (and we + // don't want to care about scanning the siblings' subtrees). + if next_combinator.is_sibling() { + // Advance to the next combinator. + for _ in &mut iter {} + continue; + } + + combinator = Some(next_combinator); + break; + } + } + + // We got here without finding any ID or such that we could handle. Try to + // use one of the simple filters. + let simple_filter = match simple_filter { + Some(f) => f, + None => return Err(()), + }; + + match simple_filter { + SimpleFilter::Class(ref class) => { + collect_all_elements::<E, Q, _>(root, results, |element| { + element.has_class(class, class_and_id_case_sensitivity) && + matching::matches_selector_list(selector_list, &element, matching_context) + }); + }, + SimpleFilter::LocalName(ref local_name) => { + collect_all_elements::<E, Q, _>(root, results, |element| { + local_name_matches(element, local_name) && + matching::matches_selector_list(selector_list, &element, matching_context) + }); + }, + SimpleFilter::Attr(ref local_name) => { + collect_all_elements::<E, Q, _>(root, results, |element| { + has_attr(element, local_name) && + matching::matches_selector_list(selector_list, &element, matching_context) + }); + }, + } + + Ok(()) +} + +// Slow path for a given selector query. +fn query_selector_slow<E, Q>( + root: E::ConcreteNode, + selector_list: &SelectorList<E::Impl>, + results: &mut Q::Output, + matching_context: &mut MatchingContext<E::Impl>, +) where + E: TElement, + Q: SelectorQuery<E>, +{ + collect_all_elements::<E, Q, _>(root, results, |element| { + matching::matches_selector_list(selector_list, &element, matching_context) + }); +} + +/// Whether the invalidation machinery should be used for this query. +#[derive(PartialEq)] +pub enum MayUseInvalidation { + /// We may use it if we deem it useful. + Yes, + /// Don't use it. + No, +} + +/// <https://dom.spec.whatwg.org/#dom-parentnode-queryselector> +pub fn query_selector<E, Q>( + root: E::ConcreteNode, + selector_list: &SelectorList<E::Impl>, + results: &mut Q::Output, + may_use_invalidation: MayUseInvalidation, +) where + E: TElement, + Q: SelectorQuery<E>, +{ + use crate::invalidation::element::invalidator::TreeStyleInvalidator; + + let mut selector_caches = SelectorCaches::default(); + let quirks_mode = root.owner_doc().quirks_mode(); + + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut selector_caches, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + let root_element = root.as_element(); + matching_context.scope_element = root_element.map(|e| e.opaque()); + matching_context.current_host = match root_element { + Some(root) => root.containing_shadow_host().map(|host| host.opaque()), + None => root.as_shadow_root().map(|root| root.host().opaque()), + }; + + let fast_result = + query_selector_fast::<E, Q>(root, selector_list, results, &mut matching_context); + + if fast_result.is_ok() { + return; + } + + // Slow path: Use the invalidation machinery if we're a root, and tree + // traversal otherwise. + // + // See the comment in collect_invalidations to see why only if we're a root. + // + // The invalidation mechanism is only useful in presence of combinators. + // + // We could do that check properly here, though checking the length of the + // selectors is a good heuristic. + // + // A selector with a combinator needs to have a length of at least 3: A + // simple selector, a combinator, and another simple selector. + let invalidation_may_be_useful = may_use_invalidation == MayUseInvalidation::Yes && + selector_list.slice().iter().any(|s| s.len() > 2); + + if root_element.is_some() || !invalidation_may_be_useful { + query_selector_slow::<E, Q>(root, selector_list, results, &mut matching_context); + } else { + let dependencies = selector_list + .slice() + .iter() + .map(|selector| Dependency::for_full_selector_invalidation(selector.clone())) + .collect::<SmallVec<[_; 5]>>(); + let mut processor = QuerySelectorProcessor::<E, Q> { + results, + matching_context, + traversal_map: SiblingTraversalMap::default(), + dependencies: &dependencies, + }; + + for node in root.dom_children() { + if let Some(e) = node.as_element() { + TreeStyleInvalidator::new(e, /* stack_limit_checker = */ None, &mut processor) + .invalidate(); + } + } + } +} diff --git a/servo/components/style/driver.rs b/servo/components/style/driver.rs new file mode 100644 index 0000000000..95447ce08e --- /dev/null +++ b/servo/components/style/driver.rs @@ -0,0 +1,164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Implements traversal over the DOM tree. The traversal starts in sequential +//! mode, and optionally parallelizes as it discovers work. + +#![deny(missing_docs)] + +use crate::context::{PerThreadTraversalStatistics, StyleContext}; +use crate::context::{ThreadLocalStyleContext, TraversalStatistics}; +use crate::dom::{SendNode, TElement, TNode}; +use crate::parallel; +use crate::scoped_tls::ScopedTLS; +use crate::traversal::{DomTraversal, PerLevelTraversalData, PreTraverseToken}; +use rayon; +use std::collections::VecDeque; +use time; + +#[cfg(feature = "servo")] +fn should_report_statistics() -> bool { + false +} + +#[cfg(feature = "gecko")] +fn should_report_statistics() -> bool { + unsafe { crate::gecko_bindings::structs::ServoTraversalStatistics_sActive } +} + +#[cfg(feature = "servo")] +fn report_statistics(_stats: &PerThreadTraversalStatistics) { + unreachable!("Servo never report stats"); +} + +#[cfg(feature = "gecko")] +fn report_statistics(stats: &PerThreadTraversalStatistics) { + // This should only be called in the main thread, or it may be racy + // to update the statistics in a global variable. + debug_assert!(unsafe { crate::gecko_bindings::bindings::Gecko_IsMainThread() }); + let gecko_stats = + unsafe { &mut crate::gecko_bindings::structs::ServoTraversalStatistics_sSingleton }; + gecko_stats.mElementsTraversed += stats.elements_traversed; + gecko_stats.mElementsStyled += stats.elements_styled; + gecko_stats.mElementsMatched += stats.elements_matched; + gecko_stats.mStylesShared += stats.styles_shared; + gecko_stats.mStylesReused += stats.styles_reused; +} + +fn with_pool_in_place_scope<'scope, R>( + work_unit_max: usize, + pool: Option<&rayon::ThreadPool>, + closure: impl FnOnce(Option<&rayon::ScopeFifo<'scope>>) -> R, +) -> R { + if work_unit_max == 0 || pool.is_none() { + closure(None) + } else { + pool.unwrap() + .in_place_scope_fifo(|scope| closure(Some(scope))) + } +} + +/// See documentation of the pref for performance characteristics. +fn work_unit_max() -> usize { + static_prefs::pref!("layout.css.stylo-work-unit-size") as usize +} + +/// Do a DOM traversal for top-down and (optionally) bottom-up processing, generic over `D`. +/// +/// We use an adaptive traversal strategy. We start out with simple sequential processing, until we +/// arrive at a wide enough level in the DOM that the parallel traversal would parallelize it. +/// If a thread pool is provided, we then transfer control over to the parallel traversal. +/// +/// Returns true if the traversal was parallel, and also returns the statistics object containing +/// information on nodes traversed (on nightly only). Not all of its fields will be initialized +/// since we don't call finish(). +pub fn traverse_dom<E, D>( + traversal: &D, + token: PreTraverseToken<E>, + pool: Option<&rayon::ThreadPool>, +) -> E +where + E: TElement, + D: DomTraversal<E>, +{ + let root = token + .traversal_root() + .expect("Should've ensured we needed to traverse"); + + let report_stats = should_report_statistics(); + let dump_stats = traversal.shared_context().options.dump_style_statistics; + let start_time = if dump_stats { + Some(time::precise_time_s()) + } else { + None + }; + + // Declare the main-thread context, as well as the worker-thread contexts, + // which we may or may not instantiate. It's important to declare the worker- + // thread contexts first, so that they get dropped second. This matters because: + // * ThreadLocalContexts borrow AtomicRefCells in TLS. + // * Dropping a ThreadLocalContext can run SequentialTasks. + // * Sequential tasks may call into functions like + // Servo_StyleSet_GetBaseComputedValuesForElement, which instantiate a + // ThreadLocalStyleContext on the main thread. If the main thread + // ThreadLocalStyleContext has not released its TLS borrow by that point, + // we'll panic on double-borrow. + let mut scoped_tls = ScopedTLS::<ThreadLocalStyleContext<E>>::new(pool); + // Process the nodes breadth-first. This helps keep similar traversal characteristics for the + // style sharing cache. + let work_unit_max = work_unit_max(); + with_pool_in_place_scope(work_unit_max, pool, |maybe_scope| { + let mut tlc = scoped_tls.ensure(parallel::create_thread_local_context); + let mut context = StyleContext { + shared: traversal.shared_context(), + thread_local: &mut tlc, + }; + + debug_assert_eq!( + scoped_tls.current_thread_index(), + 0, + "Main thread should be the first thread" + ); + + let mut discovered = VecDeque::with_capacity(work_unit_max * 2); + discovered.push_back(unsafe { SendNode::new(root.as_node()) }); + parallel::style_trees( + &mut context, + discovered, + root.as_node().opaque(), + work_unit_max, + PerLevelTraversalData { + current_dom_depth: root.depth(), + }, + maybe_scope, + traversal, + &scoped_tls, + ); + }); + + // Collect statistics from thread-locals if requested. + if dump_stats || report_stats { + let mut aggregate = PerThreadTraversalStatistics::default(); + for slot in scoped_tls.slots() { + if let Some(cx) = slot.get_mut() { + aggregate += cx.statistics.clone(); + } + } + + if report_stats { + report_statistics(&aggregate); + } + // dump statistics to stdout if requested + if dump_stats { + let parallel = pool.is_some(); + let stats = + TraversalStatistics::new(aggregate, traversal, parallel, start_time.unwrap()); + if stats.is_large { + println!("{}", stats); + } + } + } + + root +} diff --git a/servo/components/style/encoding_support.rs b/servo/components/style/encoding_support.rs new file mode 100644 index 0000000000..c144ad0b3b --- /dev/null +++ b/servo/components/style/encoding_support.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsing stylesheets from bytes (not `&str`). + +use crate::context::QuirksMode; +use crate::error_reporting::ParseErrorReporter; +use crate::media_queries::MediaList; +use crate::shared_lock::SharedRwLock; +use crate::stylesheets::{AllowImportRules, Origin, Stylesheet, StylesheetLoader, UrlExtraData}; +use cssparser::{stylesheet_encoding, EncodingSupport}; +use servo_arc::Arc; +use std::borrow::Cow; +use std::str; + +struct EncodingRs; + +impl EncodingSupport for EncodingRs { + type Encoding = &'static encoding_rs::Encoding; + + fn utf8() -> Self::Encoding { + encoding_rs::UTF_8 + } + + fn is_utf16_be_or_le(encoding: &Self::Encoding) -> bool { + *encoding == encoding_rs::UTF_16LE || *encoding == encoding_rs::UTF_16BE + } + + fn from_label(ascii_label: &[u8]) -> Option<Self::Encoding> { + encoding_rs::Encoding::for_label(ascii_label) + } +} + +fn decode_stylesheet_bytes<'a>( + css: &'a [u8], + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, +) -> Cow<'a, str> { + let fallback_encoding = stylesheet_encoding::<EncodingRs>( + css, + protocol_encoding_label.map(str::as_bytes), + environment_encoding, + ); + let (result, _used_encoding, _) = fallback_encoding.decode(&css); + // FIXME record used encoding for environment encoding of @import + result +} + +impl Stylesheet { + /// Parse a stylesheet from a set of bytes, potentially received over the + /// network. + /// + /// Takes care of decoding the network bytes and forwards the resulting + /// string to `Stylesheet::from_str`. + pub fn from_bytes( + bytes: &[u8], + url_data: UrlExtraData, + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, + origin: Origin, + media: MediaList, + shared_lock: SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + ) -> Stylesheet { + let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding); + Stylesheet::from_str( + &string, + url_data, + origin, + Arc::new(shared_lock.wrap(media)), + shared_lock, + stylesheet_loader, + error_reporter, + quirks_mode, + 0, + AllowImportRules::Yes, + ) + } + + /// Updates an empty stylesheet with a set of bytes that reached over the + /// network. + pub fn update_from_bytes( + existing: &Stylesheet, + bytes: &[u8], + protocol_encoding_label: Option<&str>, + environment_encoding: Option<&'static encoding_rs::Encoding>, + url_data: UrlExtraData, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + ) { + let string = decode_stylesheet_bytes(bytes, protocol_encoding_label, environment_encoding); + Self::update_from_str( + existing, + &string, + url_data, + stylesheet_loader, + error_reporter, + 0, + AllowImportRules::Yes, + ) + } +} diff --git a/servo/components/style/error_reporting.rs b/servo/components/style/error_reporting.rs new file mode 100644 index 0000000000..6db4c18e27 --- /dev/null +++ b/servo/components/style/error_reporting.rs @@ -0,0 +1,454 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Types used to report parsing errors. + +#![deny(missing_docs)] + +use crate::selector_parser::SelectorImpl; +use crate::stylesheets::UrlExtraData; +use cssparser::{BasicParseErrorKind, ParseErrorKind, SourceLocation, Token}; +use selectors::parser::{Component, RelativeSelector, Selector}; +use selectors::visitor::{SelectorListKind, SelectorVisitor}; +use selectors::SelectorList; +use std::fmt; +use style_traits::ParseError; + +/// Errors that can be encountered while parsing CSS. +#[derive(Debug)] +pub enum ContextualParseError<'a> { + /// A property declaration was not recognized. + UnsupportedPropertyDeclaration(&'a str, ParseError<'a>, &'a [SelectorList<SelectorImpl>]), + /// A property descriptor was not recognized. + UnsupportedPropertyDescriptor(&'a str, ParseError<'a>), + /// A font face descriptor was not recognized. + UnsupportedFontFaceDescriptor(&'a str, ParseError<'a>), + /// A font feature values descriptor was not recognized. + UnsupportedFontFeatureValuesDescriptor(&'a str, ParseError<'a>), + /// A font palette values descriptor was not recognized. + UnsupportedFontPaletteValuesDescriptor(&'a str, ParseError<'a>), + /// A keyframe rule was not valid. + InvalidKeyframeRule(&'a str, ParseError<'a>), + /// A font feature values rule was not valid. + InvalidFontFeatureValuesRule(&'a str, ParseError<'a>), + /// A keyframe property declaration was not recognized. + UnsupportedKeyframePropertyDeclaration(&'a str, ParseError<'a>), + /// A rule was invalid for some reason. + InvalidRule(&'a str, ParseError<'a>), + /// A rule was not recognized. + UnsupportedRule(&'a str, ParseError<'a>), + /// A viewport descriptor declaration was not recognized. + UnsupportedViewportDescriptorDeclaration(&'a str, ParseError<'a>), + /// A counter style descriptor declaration was not recognized. + UnsupportedCounterStyleDescriptorDeclaration(&'a str, ParseError<'a>), + /// A counter style rule had no symbols. + InvalidCounterStyleWithoutSymbols(String), + /// A counter style rule had less than two symbols. + InvalidCounterStyleNotEnoughSymbols(String), + /// A counter style rule did not have additive-symbols. + InvalidCounterStyleWithoutAdditiveSymbols, + /// A counter style rule had extends with symbols. + InvalidCounterStyleExtendsWithSymbols, + /// A counter style rule had extends with additive-symbols. + InvalidCounterStyleExtendsWithAdditiveSymbols, + /// A media rule was invalid for some reason. + InvalidMediaRule(&'a str, ParseError<'a>), + /// A value was not recognized. + UnsupportedValue(&'a str, ParseError<'a>), + /// A never-matching `:host` selector was found. + NeverMatchingHostSelector(String), +} + +impl<'a> fmt::Display for ContextualParseError<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn token_to_str(t: &Token, f: &mut fmt::Formatter) -> fmt::Result { + match *t { + Token::Ident(ref i) => write!(f, "identifier {}", i), + Token::AtKeyword(ref kw) => write!(f, "keyword @{}", kw), + Token::Hash(ref h) => write!(f, "hash #{}", h), + Token::IDHash(ref h) => write!(f, "id selector #{}", h), + Token::QuotedString(ref s) => write!(f, "quoted string \"{}\"", s), + Token::UnquotedUrl(ref u) => write!(f, "url {}", u), + Token::Delim(ref d) => write!(f, "delimiter {}", d), + Token::Number { + int_value: Some(i), .. + } => write!(f, "number {}", i), + Token::Number { value, .. } => write!(f, "number {}", value), + Token::Percentage { + int_value: Some(i), .. + } => write!(f, "percentage {}", i), + Token::Percentage { unit_value, .. } => { + write!(f, "percentage {}", unit_value * 100.) + }, + Token::Dimension { + value, ref unit, .. + } => write!(f, "dimension {}{}", value, unit), + Token::WhiteSpace(_) => write!(f, "whitespace"), + Token::Comment(_) => write!(f, "comment"), + Token::Colon => write!(f, "colon (:)"), + Token::Semicolon => write!(f, "semicolon (;)"), + Token::Comma => write!(f, "comma (,)"), + Token::IncludeMatch => write!(f, "include match (~=)"), + Token::DashMatch => write!(f, "dash match (|=)"), + Token::PrefixMatch => write!(f, "prefix match (^=)"), + Token::SuffixMatch => write!(f, "suffix match ($=)"), + Token::SubstringMatch => write!(f, "substring match (*=)"), + Token::CDO => write!(f, "CDO (<!--)"), + Token::CDC => write!(f, "CDC (-->)"), + Token::Function(ref name) => write!(f, "function {}", name), + Token::ParenthesisBlock => write!(f, "parenthesis ("), + Token::SquareBracketBlock => write!(f, "square bracket ["), + Token::CurlyBracketBlock => write!(f, "curly bracket {{"), + Token::BadUrl(ref _u) => write!(f, "bad url parse error"), + Token::BadString(ref _s) => write!(f, "bad string parse error"), + Token::CloseParenthesis => write!(f, "unmatched close parenthesis"), + Token::CloseSquareBracket => write!(f, "unmatched close square bracket"), + Token::CloseCurlyBracket => write!(f, "unmatched close curly bracket"), + } + } + + fn parse_error_to_str(err: &ParseError, f: &mut fmt::Formatter) -> fmt::Result { + match err.kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(ref t)) => { + write!(f, "found unexpected ")?; + token_to_str(t, f) + }, + ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput) => { + write!(f, "unexpected end of input") + }, + ParseErrorKind::Basic(BasicParseErrorKind::AtRuleInvalid(ref i)) => { + write!(f, "@ rule invalid: {}", i) + }, + ParseErrorKind::Basic(BasicParseErrorKind::AtRuleBodyInvalid) => { + write!(f, "@ rule invalid") + }, + ParseErrorKind::Basic(BasicParseErrorKind::QualifiedRuleInvalid) => { + write!(f, "qualified rule invalid") + }, + ParseErrorKind::Custom(ref err) => write!(f, "{:?}", err), + } + } + + match *self { + ContextualParseError::UnsupportedPropertyDeclaration(decl, ref err, _selectors) => { + write!(f, "Unsupported property declaration: '{}', ", decl)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedPropertyDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @property descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontFaceDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-face descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontFeatureValuesDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-feature-values descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedFontPaletteValuesDescriptor(decl, ref err) => { + write!( + f, + "Unsupported @font-palette-values descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidKeyframeRule(rule, ref err) => { + write!(f, "Invalid keyframe rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidFontFeatureValuesRule(rule, ref err) => { + write!(f, "Invalid font feature value rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedKeyframePropertyDeclaration(decl, ref err) => { + write!(f, "Unsupported keyframe property declaration: '{}', ", decl)?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidRule(rule, ref err) => { + write!(f, "Invalid rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedRule(rule, ref err) => { + write!(f, "Unsupported rule: '{}', ", rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedViewportDescriptorDeclaration(decl, ref err) => { + write!( + f, + "Unsupported @viewport descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedCounterStyleDescriptorDeclaration(decl, ref err) => { + write!( + f, + "Unsupported @counter-style descriptor declaration: '{}', ", + decl + )?; + parse_error_to_str(err, f) + }, + ContextualParseError::InvalidCounterStyleWithoutSymbols(ref system) => write!( + f, + "Invalid @counter-style rule: 'system: {}' without 'symbols'", + system + ), + ContextualParseError::InvalidCounterStyleNotEnoughSymbols(ref system) => write!( + f, + "Invalid @counter-style rule: 'system: {}' less than two 'symbols'", + system + ), + ContextualParseError::InvalidCounterStyleWithoutAdditiveSymbols => write!( + f, + "Invalid @counter-style rule: 'system: additive' without 'additive-symbols'" + ), + ContextualParseError::InvalidCounterStyleExtendsWithSymbols => write!( + f, + "Invalid @counter-style rule: 'system: extends …' with 'symbols'" + ), + ContextualParseError::InvalidCounterStyleExtendsWithAdditiveSymbols => write!( + f, + "Invalid @counter-style rule: 'system: extends …' with 'additive-symbols'" + ), + ContextualParseError::InvalidMediaRule(media_rule, ref err) => { + write!(f, "Invalid media rule: {}, ", media_rule)?; + parse_error_to_str(err, f) + }, + ContextualParseError::UnsupportedValue(_value, ref err) => parse_error_to_str(err, f), + ContextualParseError::NeverMatchingHostSelector(ref selector) => { + write!(f, ":host selector is not featureless: {}", selector) + }, + } + } +} + +/// A generic trait for an error reporter. +pub trait ParseErrorReporter { + /// Called when the style engine detects an error. + /// + /// Returns the current input being parsed, the source location it was + /// reported from, and a message. + fn report_error( + &self, + url: &UrlExtraData, + location: SourceLocation, + error: ContextualParseError, + ); +} + +/// An error reporter that uses [the `log` crate](https://github.com/rust-lang-nursery/log) +/// at `info` level. +/// +/// This logging is silent by default, and can be enabled with a `RUST_LOG=style=info` +/// environment variable. +/// (See [`env_logger`](https://rust-lang-nursery.github.io/log/env_logger/).) +#[cfg(feature = "servo")] +pub struct RustLogReporter; + +#[cfg(feature = "servo")] +impl ParseErrorReporter for RustLogReporter { + fn report_error( + &self, + url: &UrlExtraData, + location: SourceLocation, + error: ContextualParseError, + ) { + if log_enabled!(log::Level::Info) { + info!( + "Url:\t{}\n{}:{} {}", + url.as_str(), + location.line, + location.column, + error + ) + } + } +} + +/// Any warning a selector may generate. +/// TODO(dshin): Bug 1860634 - Merge with never matching host selector warning, which is part of the rule parser. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum SelectorWarningKind { + /// Relative Selector with not enough constraint, either outside or inside the selector. e.g. `*:has(.a)`, `.a:has(*)`. + /// May cause expensive invalidations for every element inserted and/or removed. + UnconstraintedRelativeSelector, +} + +impl SelectorWarningKind { + /// Get all warnings for this selector. + pub fn from_selector(selector: &Selector<SelectorImpl>) -> Vec<Self> { + let mut result = vec![]; + if UnconstrainedRelativeSelectorVisitor::has_warning(selector, 0, false) { + result.push(SelectorWarningKind::UnconstraintedRelativeSelector); + } + result + } +} + +/// Per-compound state for finding unconstrained relative selectors. +struct PerCompoundState { + /// Is there a relative selector in this compound? + relative_selector_found: bool, + /// Is this compound constrained in any way? + constrained: bool, + /// Nested below, or inside relative selector? + in_relative_selector: bool, +} + +impl PerCompoundState { + fn new(in_relative_selector: bool) -> Self { + Self { + relative_selector_found: false, + constrained: false, + in_relative_selector, + } + } +} + +/// Visitor to check if there's any unconstrained relative selector. +struct UnconstrainedRelativeSelectorVisitor { + compound_state: PerCompoundState, +} + +impl UnconstrainedRelativeSelectorVisitor { + fn new(in_relative_selector: bool) -> Self { + Self { + compound_state: PerCompoundState::new(in_relative_selector), + } + } + + fn has_warning( + selector: &Selector<SelectorImpl>, + offset: usize, + in_relative_selector: bool, + ) -> bool { + let relative_selector = matches!( + selector.iter_raw_parse_order_from(0).next().unwrap(), + Component::RelativeSelectorAnchor + ); + debug_assert!( + !relative_selector || offset == 0, + "Checking relative selector from non-rightmost?" + ); + let mut visitor = Self::new(in_relative_selector); + let mut iter = if relative_selector { + selector.iter_skip_relative_selector_anchor() + } else { + selector.iter_from(offset) + }; + loop { + visitor.compound_state = PerCompoundState::new(in_relative_selector); + + for s in &mut iter { + s.visit(&mut visitor); + } + + if (visitor.compound_state.relative_selector_found || + visitor.compound_state.in_relative_selector) && + !visitor.compound_state.constrained + { + return true; + } + + if iter.next_sequence().is_none() { + break; + } + } + false + } +} + +impl SelectorVisitor for UnconstrainedRelativeSelectorVisitor { + type Impl = SelectorImpl; + + fn visit_simple_selector(&mut self, c: &Component<Self::Impl>) -> bool { + match c { + // Deferred to visit_selector_list + Component::Is(..) | + Component::Where(..) | + Component::Negation(..) | + Component::Has(..) => (), + Component::ExplicitUniversalType => (), + _ => self.compound_state.constrained |= true, + }; + true + } + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<Self::Impl>], + ) -> bool { + let mut all_constrained = true; + for s in list { + let mut offset = 0; + // First, check the rightmost compound for constraint at this level. + if !self.compound_state.in_relative_selector { + let mut nested = Self::new(false); + let mut iter = s.iter(); + loop { + for c in &mut iter { + c.visit(&mut nested); + offset += 1; + } + + let c = iter.next_sequence(); + offset += 1; + if c.map_or(true, |c| !c.is_pseudo_element()) { + break; + } + } + // Every single selector in the list must be constrained. + all_constrained &= nested.compound_state.constrained; + } + + if offset >= s.len() { + continue; + } + + // Then, recurse in to check at the deeper level. + if Self::has_warning(s, offset, self.compound_state.in_relative_selector) { + self.compound_state.constrained = false; + if !self.compound_state.in_relative_selector { + self.compound_state.relative_selector_found = true; + } + return false; + } + } + self.compound_state.constrained |= all_constrained; + true + } + + fn visit_relative_selector_list(&mut self, list: &[RelativeSelector<Self::Impl>]) -> bool { + debug_assert!( + !self.compound_state.in_relative_selector, + "Nested relative selector" + ); + self.compound_state.relative_selector_found = true; + + for rs in list { + // If the inside is unconstrained, we are unconstrained no matter what. + if Self::has_warning(&rs.selector, 0, true) { + self.compound_state.constrained = false; + return false; + } + } + true + } +} diff --git a/servo/components/style/font_face.rs b/servo/components/style/font_face.rs new file mode 100644 index 0000000000..5f1efdd266 --- /dev/null +++ b/servo/components/style/font_face.rs @@ -0,0 +1,807 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@font-face`][ff] at-rule. +//! +//! [ff]: https://drafts.csswg.org/css-fonts/#at-font-face-rule + +use crate::error_reporting::ContextualParseError; +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::properties::longhands::font_language_override; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::computed::font::{FamilyName, FontStretch}; +use crate::values::generics::font::FontStyle as GenericFontStyle; +use crate::values::specified::font::SpecifiedFontStyle; +use crate::values::specified::font::{ + AbsoluteFontWeight, FontStretch as SpecifiedFontStretch, MetricsOverride, +}; +#[cfg(feature = "gecko")] +use crate::values::specified::font::{FontFeatureSettings, FontVariationSettings}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, NonNegativePercentage}; +#[cfg(feature = "gecko")] +use cssparser::UnicodeRange; +use cssparser::{ + AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, RuleBodyItemParser, + RuleBodyParser, SourceLocation, +}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// A source for a font-face rule. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +pub enum Source { + /// A `url()` source. + Url(UrlSource), + /// A `local()` source. + #[css(function)] + Local(FamilyName), +} + +/// A list of sources for the font-face src descriptor. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct SourceList(#[css(iterable)] pub Vec<Source>); + +// We can't just use OneOrMoreSeparated to derive Parse for the Source list, +// because we want to filter out components that parsed as None, then fail if no +// valid components remain. So we provide our own implementation here. +impl Parse for SourceList { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Parse the comma-separated list, then let filter_map discard any None items. + let list = input + .parse_comma_separated(|input| { + let s = input.parse_entirely(|input| Source::parse(context, input)); + while input.next().is_ok() {} + Ok(s.ok()) + })? + .into_iter() + .filter_map(|s| s) + .collect::<Vec<Source>>(); + if list.is_empty() { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(SourceList(list)) + } + } +} + +/// Keywords for the font-face src descriptor's format() function. +/// ('None' and 'Unknown' are for internal use in gfx, not exposed to CSS.) +#[derive(Clone, Copy, Debug, Eq, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceFormatKeyword { + #[css(skip)] + None, + Collection, + EmbeddedOpentype, + Opentype, + Svg, + Truetype, + Woff, + Woff2, + #[css(skip)] + Unknown, +} + +/// Flags for the @font-face tech() function, indicating font technologies +/// required by the resource. +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +#[repr(C)] +pub struct FontFaceSourceTechFlags(u16); +bitflags! { + impl FontFaceSourceTechFlags: u16 { + /// Font requires OpenType feature support. + const FEATURES_OPENTYPE = 1 << 0; + /// Font requires Apple Advanced Typography support. + const FEATURES_AAT = 1 << 1; + /// Font requires Graphite shaping support. + const FEATURES_GRAPHITE = 1 << 2; + /// Font requires COLRv0 rendering support (simple list of colored layers). + const COLOR_COLRV0 = 1 << 3; + /// Font requires COLRv1 rendering support (graph of paint operations). + const COLOR_COLRV1 = 1 << 4; + /// Font requires SVG glyph rendering support. + const COLOR_SVG = 1 << 5; + /// Font has bitmap glyphs in 'sbix' format. + const COLOR_SBIX = 1 << 6; + /// Font has bitmap glyphs in 'CBDT' format. + const COLOR_CBDT = 1 << 7; + /// Font requires OpenType Variations support. + const VARIATIONS = 1 << 8; + /// Font requires CPAL palette selection support. + const PALETTES = 1 << 9; + /// Font requires support for incremental downloading. + const INCREMENTAL = 1 << 10; + } +} + +impl FontFaceSourceTechFlags { + /// Parse a single font-technology keyword and return its flag. + pub fn parse_one<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "features-opentype" => Self::FEATURES_OPENTYPE, + "features-aat" => Self::FEATURES_AAT, + "features-graphite" => Self::FEATURES_GRAPHITE, + "color-colrv0" => Self::COLOR_COLRV0, + "color-colrv1" => Self::COLOR_COLRV1, + "color-svg" => Self::COLOR_SVG, + "color-sbix" => Self::COLOR_SBIX, + "color-cbdt" => Self::COLOR_CBDT, + "variations" => Self::VARIATIONS, + "palettes" => Self::PALETTES, + "incremental" => Self::INCREMENTAL, + }) + } +} + +impl Parse for FontFaceSourceTechFlags { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + // We don't actually care about the return value of parse_comma_separated, + // because we insert the flags into result as we go. + let mut result = Self::empty(); + input.parse_comma_separated(|input| { + let flag = Self::parse_one(input)?; + result.insert(flag); + Ok(()) + })?; + if !result.is_empty() { + Ok(result) + } else { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +#[allow(unused_assignments)] +impl ToCss for FontFaceSourceTechFlags { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + let mut first = true; + + macro_rules! write_if_flag { + ($s:expr => $f:ident) => { + if self.contains(Self::$f) { + if first { + first = false; + } else { + dest.write_str(", ")?; + } + dest.write_str($s)?; + } + }; + } + + write_if_flag!("features-opentype" => FEATURES_OPENTYPE); + write_if_flag!("features-aat" => FEATURES_AAT); + write_if_flag!("features-graphite" => FEATURES_GRAPHITE); + write_if_flag!("color-colrv0" => COLOR_COLRV0); + write_if_flag!("color-colrv1" => COLOR_COLRV1); + write_if_flag!("color-svg" => COLOR_SVG); + write_if_flag!("color-sbix" => COLOR_SBIX); + write_if_flag!("color-cbdt" => COLOR_CBDT); + write_if_flag!("variations" => VARIATIONS); + write_if_flag!("palettes" => PALETTES); + write_if_flag!("incremental" => INCREMENTAL); + + Ok(()) + } +} + +/// A POD representation for Gecko. All pointers here are non-owned and as such +/// can't outlive the rule they came from, but we can't enforce that via C++. +/// +/// All the strings are of course utf8. +#[cfg(feature = "gecko")] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceListComponent { + Url(*const crate::gecko::url::CssUrl), + Local(*mut crate::gecko_bindings::structs::nsAtom), + FormatHintKeyword(FontFaceSourceFormatKeyword), + FormatHintString { + length: usize, + utf8_bytes: *const u8, + }, + TechFlags(FontFaceSourceTechFlags), +} + +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum FontFaceSourceFormat { + Keyword(FontFaceSourceFormatKeyword), + String(String), +} + +/// A `UrlSource` represents a font-face source that has been specified with a +/// `url()` function. +/// +/// <https://drafts.csswg.org/css-fonts/#src-desc> +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Debug, Eq, PartialEq, ToShmem)] +pub struct UrlSource { + /// The specified url. + pub url: SpecifiedUrl, + /// The format hint specified with the `format()` function, if present. + pub format_hint: Option<FontFaceSourceFormat>, + /// The font technology flags specified with the `tech()` function, if any. + pub tech_flags: FontFaceSourceTechFlags, +} + +impl ToCss for UrlSource { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.url.to_css(dest)?; + if let Some(hint) = &self.format_hint { + dest.write_str(" format(")?; + hint.to_css(dest)?; + dest.write_char(')')?; + } + if !self.tech_flags.is_empty() { + dest.write_str(" tech(")?; + self.tech_flags.to_css(dest)?; + dest.write_char(')')?; + } + Ok(()) + } +} + +/// A font-display value for a @font-face rule. +/// The font-display descriptor determines how a font face is displayed based +/// on whether and when it is downloaded and ready to use. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToComputedValue, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum FontDisplay { + Auto, + Block, + Swap, + Fallback, + Optional, +} + +macro_rules! impl_range { + ($range:ident, $component:ident) => { + impl Parse for $range { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let first = $component::parse(context, input)?; + let second = input + .try_parse(|input| $component::parse(context, input)) + .unwrap_or_else(|_| first.clone()); + Ok($range(first, second)) + } + } + impl ToCss for $range { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.0.to_css(dest)?; + if self.0 != self.1 { + dest.write_char(' ')?; + self.1.to_css(dest)?; + } + Ok(()) + } + } + }; +} + +/// The font-weight descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-weight +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontWeightRange(pub AbsoluteFontWeight, pub AbsoluteFontWeight); +impl_range!(FontWeightRange, AbsoluteFontWeight); + +/// The computed representation of the above so Gecko can read them easily. +/// +/// This one is needed because cbindgen doesn't know how to generate +/// specified::Number. +#[repr(C)] +#[allow(missing_docs)] +pub struct ComputedFontWeightRange(f32, f32); + +#[inline] +fn sort_range<T: PartialOrd>(a: T, b: T) -> (T, T) { + if a > b { + (b, a) + } else { + (a, b) + } +} + +impl FontWeightRange { + /// Returns a computed font-stretch range. + pub fn compute(&self) -> ComputedFontWeightRange { + let (min, max) = sort_range(self.0.compute().value(), self.1.compute().value()); + ComputedFontWeightRange(min, max) + } +} + +/// The font-stretch descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-stretch +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontStretchRange(pub SpecifiedFontStretch, pub SpecifiedFontStretch); +impl_range!(FontStretchRange, SpecifiedFontStretch); + +/// The computed representation of the above, so that Gecko can read them +/// easily. +#[repr(C)] +#[allow(missing_docs)] +pub struct ComputedFontStretchRange(FontStretch, FontStretch); + +impl FontStretchRange { + /// Returns a computed font-stretch range. + pub fn compute(&self) -> ComputedFontStretchRange { + fn compute_stretch(s: &SpecifiedFontStretch) -> FontStretch { + match *s { + SpecifiedFontStretch::Keyword(ref kw) => kw.compute(), + SpecifiedFontStretch::Stretch(ref p) => FontStretch::from_percentage(p.0.get()), + SpecifiedFontStretch::System(..) => unreachable!(), + } + } + + let (min, max) = sort_range(compute_stretch(&self.0), compute_stretch(&self.1)); + ComputedFontStretchRange(min, max) + } +} + +/// The font-style descriptor: +/// +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-style +#[derive(Clone, Debug, PartialEq, ToShmem)] +#[allow(missing_docs)] +pub enum FontStyle { + Normal, + Italic, + Oblique(Angle, Angle), +} + +/// The computed representation of the above, with angles in degrees, so that +/// Gecko can read them easily. +#[repr(u8)] +#[allow(missing_docs)] +pub enum ComputedFontStyleDescriptor { + Normal, + Italic, + Oblique(f32, f32), +} + +impl Parse for FontStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let style = SpecifiedFontStyle::parse(context, input)?; + Ok(match style { + GenericFontStyle::Normal => FontStyle::Normal, + GenericFontStyle::Italic => FontStyle::Italic, + GenericFontStyle::Oblique(angle) => { + let second_angle = input + .try_parse(|input| SpecifiedFontStyle::parse_angle(context, input)) + .unwrap_or_else(|_| angle.clone()); + + FontStyle::Oblique(angle, second_angle) + }, + }) + } +} + +impl ToCss for FontStyle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + match *self { + FontStyle::Normal => dest.write_str("normal"), + FontStyle::Italic => dest.write_str("italic"), + FontStyle::Oblique(ref first, ref second) => { + dest.write_str("oblique")?; + if *first != SpecifiedFontStyle::default_angle() || first != second { + dest.write_char(' ')?; + first.to_css(dest)?; + } + if first != second { + dest.write_char(' ')?; + second.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl FontStyle { + /// Returns a computed font-style descriptor. + pub fn compute(&self) -> ComputedFontStyleDescriptor { + match *self { + FontStyle::Normal => ComputedFontStyleDescriptor::Normal, + FontStyle::Italic => ComputedFontStyleDescriptor::Italic, + FontStyle::Oblique(ref first, ref second) => { + let (min, max) = sort_range( + SpecifiedFontStyle::compute_angle_degrees(first), + SpecifiedFontStyle::compute_angle_degrees(second), + ); + ComputedFontStyleDescriptor::Oblique(min, max) + }, + } + } +} + +/// Parse the block inside a `@font-face` rule. +/// +/// Note that the prelude parsing code lives in the `stylesheets` module. +pub fn parse_font_face_block( + context: &ParserContext, + input: &mut Parser, + location: SourceLocation, +) -> FontFaceRuleData { + let mut rule = FontFaceRuleData::empty(location); + { + let mut parser = FontFaceRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedFontFaceDescriptor(slice, error); + context.log_css_error(location, error) + } + } + } + rule +} + +/// A @font-face rule that is known to have font-family and src declarations. +#[cfg(feature = "servo")] +pub struct FontFace<'a>(&'a FontFaceRuleData); + +/// A list of effective sources that we send over through IPC to the font cache. +#[cfg(feature = "servo")] +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct EffectiveSources(SourceList); + +#[cfg(feature = "servo")] +impl<'a> FontFace<'a> { + /// Returns the list of effective sources for that font-face, that is the + /// sources which don't list any format hint, or the ones which list at + /// least "truetype" or "opentype". + pub fn effective_sources(&self) -> EffectiveSources { + EffectiveSources( + self.sources() + .iter() + .rev() + .filter(|source| { + if let Source::Url(ref url_source) = **source { + // We support only opentype fonts and truetype is an alias for + // that format. Sources without format hints need to be + // downloaded in case we support them. + url_source.format_hint.as_ref().map_or(true, |hint| { + hint == "truetype" || hint == "opentype" || hint == "woff" + }) + } else { + true + } + }) + .cloned() + .collect(), + ) + } +} + +#[cfg(feature = "servo")] +impl Iterator for EffectiveSources { + type Item = Source; + fn next(&mut self) -> Option<Source> { + self.0.pop() + } + + fn size_hint(&self) -> (usize, Option<usize>) { + (self.0.len(), Some(self.0.len())) + } +} + +struct FontFaceRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + rule: &'a mut FontFaceRuleData, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for FontFaceRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for FontFaceRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for FontFaceRuleParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} + +impl Parse for Source { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Source, ParseError<'i>> { + if input + .try_parse(|input| input.expect_function_matching("local")) + .is_ok() + { + return input + .parse_nested_block(|input| FamilyName::parse(context, input)) + .map(Source::Local); + } + + let url = SpecifiedUrl::parse(context, input)?; + + // Parsing optional format() + let format_hint = if input + .try_parse(|input| input.expect_function_matching("format")) + .is_ok() + { + input.parse_nested_block(|input| { + if let Ok(kw) = input.try_parse(FontFaceSourceFormatKeyword::parse) { + Ok(Some(FontFaceSourceFormat::Keyword(kw))) + } else { + let s = input.expect_string()?.as_ref().to_owned(); + Ok(Some(FontFaceSourceFormat::String(s))) + } + })? + } else { + None + }; + + // Parse optional tech() + let tech_flags = if static_prefs::pref!("layout.css.font-tech.enabled") && + input + .try_parse(|input| input.expect_function_matching("tech")) + .is_ok() + { + input.parse_nested_block(|input| FontFaceSourceTechFlags::parse(context, input))? + } else { + FontFaceSourceTechFlags::empty() + }; + + Ok(Source::Url(UrlSource { + url, + format_hint, + tech_flags, + })) + } +} + +macro_rules! is_descriptor_enabled { + ("font-variation-settings") => { + static_prefs::pref!("layout.css.font-variations.enabled") + }; + ("size-adjust") => { + static_prefs::pref!("layout.css.size-adjust.enabled") + }; + ($name:tt) => { + true + }; +} + +macro_rules! font_face_descriptors_common { + ( + $( #[$doc: meta] $name: tt $ident: ident / $gecko_ident: ident: $ty: ty, )* + ) => { + /// Data inside a `@font-face` rule. + /// + /// <https://drafts.csswg.org/css-fonts/#font-face-rule> + #[derive(Clone, Debug, PartialEq, ToShmem)] + pub struct FontFaceRuleData { + $( + #[$doc] + pub $ident: Option<$ty>, + )* + /// Line and column of the @font-face rule source code. + pub source_location: SourceLocation, + } + + impl FontFaceRuleData { + /// Create an empty font-face rule + pub fn empty(location: SourceLocation) -> Self { + FontFaceRuleData { + $( + $ident: None, + )* + source_location: location, + } + } + + /// Serialization of declarations in the FontFaceRule + pub fn decl_to_css(&self, dest: &mut CssStringWriter) -> fmt::Result { + $( + if let Some(ref value) = self.$ident { + dest.write_str(concat!($name, ": "))?; + value.to_css(&mut CssWriter::new(dest))?; + dest.write_str("; ")?; + } + )* + Ok(()) + } + } + + impl<'a, 'b, 'i> DeclarationParser<'i> for FontFaceRuleParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name if is_descriptor_enabled!($name) => { + // DeclarationParser also calls parse_entirely + // so we’d normally not need to, + // but in this case we do because we set the value as a side effect + // rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.rule.$ident = Some(value) + }, + )* + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } + } + } +} + +impl ToCssWithGuard for FontFaceRuleData { + // Serialization of FontFaceRule is not specced. + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@font-face { ")?; + self.decl_to_css(dest)?; + dest.write_char('}') + } +} + +macro_rules! font_face_descriptors { + ( + mandatory descriptors = [ + $( #[$m_doc: meta] $m_name: tt $m_ident: ident / $m_gecko_ident: ident: $m_ty: ty, )* + ] + optional descriptors = [ + $( #[$o_doc: meta] $o_name: tt $o_ident: ident / $o_gecko_ident: ident: $o_ty: ty, )* + ] + ) => { + font_face_descriptors_common! { + $( #[$m_doc] $m_name $m_ident / $m_gecko_ident: $m_ty, )* + $( #[$o_doc] $o_name $o_ident / $o_gecko_ident: $o_ty, )* + } + + impl FontFaceRuleData { + /// Per https://github.com/w3c/csswg-drafts/issues/1133 an @font-face rule + /// is valid as far as the CSS parser is concerned even if it doesn’t have + /// a font-family or src declaration. + /// + /// However both are required for the rule to represent an actual font face. + #[cfg(feature = "servo")] + pub fn font_face(&self) -> Option<FontFace> { + if $( self.$m_ident.is_some() )&&* { + Some(FontFace(self)) + } else { + None + } + } + } + + #[cfg(feature = "servo")] + impl<'a> FontFace<'a> { + $( + #[$m_doc] + pub fn $m_ident(&self) -> &$m_ty { + self.0 .$m_ident.as_ref().unwrap() + } + )* + } + } +} + +#[cfg(feature = "gecko")] +font_face_descriptors! { + mandatory descriptors = [ + /// The name of this font face + "font-family" family / mFamily: FamilyName, + + /// The alternative sources for this font face. + "src" sources / mSrc: SourceList, + ] + optional descriptors = [ + /// The style of this font face. + "font-style" style / mStyle: FontStyle, + + /// The weight of this font face. + "font-weight" weight / mWeight: FontWeightRange, + + /// The stretch of this font face. + "font-stretch" stretch / mStretch: FontStretchRange, + + /// The display of this font face. + "font-display" display / mDisplay: FontDisplay, + + /// The ranges of code points outside of which this font face should not be used. + "unicode-range" unicode_range / mUnicodeRange: Vec<UnicodeRange>, + + /// The feature settings of this font face. + "font-feature-settings" feature_settings / mFontFeatureSettings: FontFeatureSettings, + + /// The variation settings of this font face. + "font-variation-settings" variation_settings / mFontVariationSettings: FontVariationSettings, + + /// The language override of this font face. + "font-language-override" language_override / mFontLanguageOverride: font_language_override::SpecifiedValue, + + /// The ascent override for this font face. + "ascent-override" ascent_override / mAscentOverride: MetricsOverride, + + /// The descent override for this font face. + "descent-override" descent_override / mDescentOverride: MetricsOverride, + + /// The line-gap override for this font face. + "line-gap-override" line_gap_override / mLineGapOverride: MetricsOverride, + + /// The size adjustment for this font face. + "size-adjust" size_adjust / mSizeAdjust: NonNegativePercentage, + ] +} + +#[cfg(feature = "servo")] +font_face_descriptors! { + mandatory descriptors = [ + /// The name of this font face + "font-family" family / mFamily: FamilyName, + + /// The alternative sources for this font face. + "src" sources / mSrc: SourceList, + ] + optional descriptors = [ + ] +} diff --git a/servo/components/style/font_metrics.rs b/servo/components/style/font_metrics.rs new file mode 100644 index 0000000000..391d3653ee --- /dev/null +++ b/servo/components/style/font_metrics.rs @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Access to font metrics from the style system. + +#![deny(missing_docs)] + +use crate::values::computed::Length; + +/// Represents the font metrics that style needs from a font to compute the +/// value of certain CSS units like `ex`. +#[derive(Clone, Debug, PartialEq)] +pub struct FontMetrics { + /// The x-height of the font. + pub x_height: Option<Length>, + /// The zero advance. This is usually writing mode dependent + pub zero_advance_measure: Option<Length>, + /// The cap-height of the font. + pub cap_height: Option<Length>, + /// The ideographic-width of the font. + pub ic_width: Option<Length>, + /// The ascent of the font (a value is always available for this). + pub ascent: Length, + /// Script scale down factor for math-depth 1. + /// https://w3c.github.io/mathml-core/#dfn-scriptpercentscaledown + pub script_percent_scale_down: Option<f32>, + /// Script scale down factor for math-depth 2. + /// https://w3c.github.io/mathml-core/#dfn-scriptscriptpercentscaledown + pub script_script_percent_scale_down: Option<f32>, +} + +impl Default for FontMetrics { + fn default() -> Self { + FontMetrics { + x_height: None, + zero_advance_measure: None, + cap_height: None, + ic_width: None, + ascent: Length::new(0.0), + script_percent_scale_down: None, + script_script_percent_scale_down: None, + } + } +} + +/// Type of font metrics to retrieve. +#[derive(Clone, Debug, PartialEq)] +pub enum FontMetricsOrientation { + /// Get metrics for horizontal or vertical according to the Context's + /// writing mode, using horizontal metrics for vertical/mixed + MatchContextPreferHorizontal, + /// Get metrics for horizontal or vertical according to the Context's + /// writing mode, using vertical metrics for vertical/mixed + MatchContextPreferVertical, + /// Force getting horizontal metrics. + Horizontal, +} diff --git a/servo/components/style/gecko/arc_types.rs b/servo/components/style/gecko/arc_types.rs new file mode 100644 index 0000000000..24bf22d69a --- /dev/null +++ b/servo/components/style/gecko/arc_types.rs @@ -0,0 +1,171 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This file lists all arc FFI types and defines corresponding addref and release functions. This +//! list loosely corresponds to ServoLockedArcTypeList.h file in Gecko. + +#![allow(non_snake_case, missing_docs)] + +use crate::gecko::url::CssUrlData; +use crate::media_queries::MediaList; +use crate::properties::animated_properties::AnimationValue; +use crate::properties::{ComputedValues, PropertyDeclarationBlock}; +use crate::shared_lock::Locked; +use crate::stylesheets::keyframes_rule::Keyframe; +use crate::stylesheets::{ + ContainerRule, CounterStyleRule, CssRules, DocumentRule, FontFaceRule, FontFeatureValuesRule, + FontPaletteValuesRule, ImportRule, KeyframesRule, LayerBlockRule, LayerStatementRule, + MediaRule, NamespaceRule, PageRule, PropertyRule, StyleRule, StylesheetContents, SupportsRule, +}; +use servo_arc::Arc; + +macro_rules! impl_simple_arc_ffi { + ($ty:ty, $addref:ident, $release:ident) => { + #[no_mangle] + pub unsafe extern "C" fn $addref(obj: *const $ty) { + std::mem::forget(Arc::from_raw_addrefed(obj)); + } + + #[no_mangle] + pub unsafe extern "C" fn $release(obj: *const $ty) { + let _ = Arc::from_raw(obj); + } + }; +} + +macro_rules! impl_locked_arc_ffi { + ($servo_type:ty, $alias:ident, $addref:ident, $release:ident) => { + /// A simple alias for a locked type. + pub type $alias = Locked<$servo_type>; + impl_simple_arc_ffi!($alias, $addref, $release); + }; +} + +impl_locked_arc_ffi!( + CssRules, + LockedCssRules, + Servo_CssRules_AddRef, + Servo_CssRules_Release +); +impl_locked_arc_ffi!( + PropertyDeclarationBlock, + LockedDeclarationBlock, + Servo_DeclarationBlock_AddRef, + Servo_DeclarationBlock_Release +); +impl_locked_arc_ffi!( + StyleRule, + LockedStyleRule, + Servo_StyleRule_AddRef, + Servo_StyleRule_Release +); +impl_locked_arc_ffi!( + ImportRule, + LockedImportRule, + Servo_ImportRule_AddRef, + Servo_ImportRule_Release +); +impl_locked_arc_ffi!( + Keyframe, + LockedKeyframe, + Servo_Keyframe_AddRef, + Servo_Keyframe_Release +); +impl_locked_arc_ffi!( + KeyframesRule, + LockedKeyframesRule, + Servo_KeyframesRule_AddRef, + Servo_KeyframesRule_Release +); +impl_simple_arc_ffi!( + LayerBlockRule, + Servo_LayerBlockRule_AddRef, + Servo_LayerBlockRule_Release +); +impl_simple_arc_ffi!( + LayerStatementRule, + Servo_LayerStatementRule_AddRef, + Servo_LayerStatementRule_Release +); +impl_locked_arc_ffi!( + MediaList, + LockedMediaList, + Servo_MediaList_AddRef, + Servo_MediaList_Release +); +impl_simple_arc_ffi!(MediaRule, Servo_MediaRule_AddRef, Servo_MediaRule_Release); +impl_simple_arc_ffi!( + NamespaceRule, + Servo_NamespaceRule_AddRef, + Servo_NamespaceRule_Release +); +impl_locked_arc_ffi!( + PageRule, + LockedPageRule, + Servo_PageRule_AddRef, + Servo_PageRule_Release +); +impl_simple_arc_ffi!( + PropertyRule, + Servo_PropertyRule_AddRef, + Servo_PropertyRule_Release +); +impl_simple_arc_ffi!( + SupportsRule, + Servo_SupportsRule_AddRef, + Servo_SupportsRule_Release +); +impl_simple_arc_ffi!( + ContainerRule, + Servo_ContainerRule_AddRef, + Servo_ContainerRule_Release +); +impl_simple_arc_ffi!( + DocumentRule, + Servo_DocumentRule_AddRef, + Servo_DocumentRule_Release +); +impl_simple_arc_ffi!( + FontFeatureValuesRule, + Servo_FontFeatureValuesRule_AddRef, + Servo_FontFeatureValuesRule_Release +); +impl_simple_arc_ffi!( + FontPaletteValuesRule, + Servo_FontPaletteValuesRule_AddRef, + Servo_FontPaletteValuesRule_Release +); +impl_locked_arc_ffi!( + FontFaceRule, + LockedFontFaceRule, + Servo_FontFaceRule_AddRef, + Servo_FontFaceRule_Release +); +impl_locked_arc_ffi!( + CounterStyleRule, + LockedCounterStyleRule, + Servo_CounterStyleRule_AddRef, + Servo_CounterStyleRule_Release +); + +impl_simple_arc_ffi!( + StylesheetContents, + Servo_StyleSheetContents_AddRef, + Servo_StyleSheetContents_Release +); +impl_simple_arc_ffi!( + CssUrlData, + Servo_CssUrlData_AddRef, + Servo_CssUrlData_Release +); +impl_simple_arc_ffi!( + ComputedValues, + Servo_ComputedStyle_AddRef, + Servo_ComputedStyle_Release +); +impl_simple_arc_ffi!( + AnimationValue, + Servo_AnimationValue_AddRef, + Servo_AnimationValue_Release +); diff --git a/servo/components/style/gecko/conversions.rs b/servo/components/style/gecko/conversions.rs new file mode 100644 index 0000000000..ea3700a323 --- /dev/null +++ b/servo/components/style/gecko/conversions.rs @@ -0,0 +1,59 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This module contains conversion helpers between Servo and Gecko types +//! Ideally, it would be in geckolib itself, but coherence +//! forces us to keep the traits and implementations here +//! +//! FIXME(emilio): This file should generally just die. + +#![allow(unsafe_code)] + +use crate::gecko_bindings::structs::{nsresult, Matrix4x4Components}; +use crate::stylesheets::RulesMutateError; +use crate::values::computed::transform::Matrix3D; + +impl From<RulesMutateError> for nsresult { + fn from(other: RulesMutateError) -> Self { + match other { + RulesMutateError::Syntax => nsresult::NS_ERROR_DOM_SYNTAX_ERR, + RulesMutateError::IndexSize => nsresult::NS_ERROR_DOM_INDEX_SIZE_ERR, + RulesMutateError::HierarchyRequest => nsresult::NS_ERROR_DOM_HIERARCHY_REQUEST_ERR, + RulesMutateError::InvalidState => nsresult::NS_ERROR_DOM_INVALID_STATE_ERR, + } + } +} + +impl<'a> From<&'a Matrix4x4Components> for Matrix3D { + fn from(m: &'a Matrix4x4Components) -> Matrix3D { + Matrix3D { + m11: m[0], + m12: m[1], + m13: m[2], + m14: m[3], + m21: m[4], + m22: m[5], + m23: m[6], + m24: m[7], + m31: m[8], + m32: m[9], + m33: m[10], + m34: m[11], + m41: m[12], + m42: m[13], + m43: m[14], + m44: m[15], + } + } +} + +impl From<Matrix3D> for Matrix4x4Components { + fn from(matrix: Matrix3D) -> Self { + [ + matrix.m11, matrix.m12, matrix.m13, matrix.m14, matrix.m21, matrix.m22, matrix.m23, + matrix.m24, matrix.m31, matrix.m32, matrix.m33, matrix.m34, matrix.m41, matrix.m42, + matrix.m43, matrix.m44, + ] + } +} diff --git a/servo/components/style/gecko/data.rs b/servo/components/style/gecko/data.rs new file mode 100644 index 0000000000..c4a5554c5e --- /dev/null +++ b/servo/components/style/gecko/data.rs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Data needed to style a Gecko document. + +use crate::dom::TElement; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{ + self, ServoStyleSetSizes, StyleSheet as DomStyleSheet, StyleSheetInfo, +}; +use crate::invalidation::media_queries::{MediaListKey, ToMediaListKey}; +use crate::media_queries::{Device, MediaList}; +use crate::properties::ComputedValues; +use crate::selector_parser::SnapshotMap; +use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards}; +use crate::stylesheets::{StylesheetContents, StylesheetInDocument}; +use crate::stylist::Stylist; +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use malloc_size_of::MallocSizeOfOps; +use servo_arc::Arc; +use std::fmt; + +/// Little wrapper to a Gecko style sheet. +#[derive(Eq, PartialEq)] +pub struct GeckoStyleSheet(*const DomStyleSheet); + +// NOTE(emilio): These are kind of a lie. We allow to make these Send + Sync so that other data +// structures can also be Send and Sync, but Gecko's stylesheets are main-thread-reference-counted. +// +// We assert that we reference-count in the right thread (in the Addref/Release implementations). +// Sending these to a different thread can't really happen (it could theoretically really happen if +// we allowed @import rules inside a nested style rule, but that can't happen per spec and would be +// a parser bug, caught by the asserts). +unsafe impl Send for GeckoStyleSheet {} +unsafe impl Sync for GeckoStyleSheet {} + +impl fmt::Debug for GeckoStyleSheet { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let contents = self.contents(); + formatter + .debug_struct("GeckoStyleSheet") + .field("origin", &contents.origin) + .field("url_data", &*contents.url_data.read()) + .finish() + } +} + +impl ToMediaListKey for crate::gecko::data::GeckoStyleSheet { + fn to_media_list_key(&self) -> MediaListKey { + use std::mem; + unsafe { MediaListKey::from_raw(mem::transmute(self.0)) } + } +} + +impl GeckoStyleSheet { + /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer. + #[inline] + pub unsafe fn new(s: *const DomStyleSheet) -> Self { + debug_assert!(!s.is_null()); + bindings::Gecko_StyleSheet_AddRef(s); + Self::from_addrefed(s) + } + + /// Create a `GeckoStyleSheet` from a raw `DomStyleSheet` pointer that + /// already holds a strong reference. + #[inline] + pub unsafe fn from_addrefed(s: *const DomStyleSheet) -> Self { + assert!(!s.is_null()); + GeckoStyleSheet(s) + } + + /// HACK(emilio): This is so that we can avoid crashing release due to + /// bug 1719963 and can hopefully get a useful report from fuzzers. + #[inline] + pub fn hack_is_null(&self) -> bool { + self.0.is_null() + } + + /// Get the raw `StyleSheet` that we're wrapping. + pub fn raw(&self) -> &DomStyleSheet { + unsafe { &*self.0 } + } + + fn inner(&self) -> &StyleSheetInfo { + unsafe { &*(self.raw().mInner as *const StyleSheetInfo) } + } +} + +impl Drop for GeckoStyleSheet { + fn drop(&mut self) { + unsafe { bindings::Gecko_StyleSheet_Release(self.0) }; + } +} + +impl Clone for GeckoStyleSheet { + fn clone(&self) -> Self { + unsafe { bindings::Gecko_StyleSheet_AddRef(self.0) }; + GeckoStyleSheet(self.0) + } +} + +impl StylesheetInDocument for GeckoStyleSheet { + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + use crate::gecko_bindings::structs::mozilla::dom::MediaList as DomMediaList; + unsafe { + let dom_media_list = self.raw().mMedia.mRawPtr as *const DomMediaList; + if dom_media_list.is_null() { + return None; + } + let list = &*(*dom_media_list).mRawList.mRawPtr; + Some(list.read_with(guard)) + } + } + + // All the stylesheets Servo knows about are enabled, because that state is + // handled externally by Gecko. + #[inline] + fn enabled(&self) -> bool { + true + } + + #[inline] + fn contents(&self) -> &StylesheetContents { + debug_assert!(!self.inner().mContents.mRawPtr.is_null()); + unsafe { &*self.inner().mContents.mRawPtr } + } +} + +/// The container for data that a Servo-backed Gecko document needs to style +/// itself. +pub struct PerDocumentStyleDataImpl { + /// Rule processor. + pub stylist: Stylist, + + /// A cache from element to resolved style. + pub undisplayed_style_cache: crate::traversal::UndisplayedStyleCache, + + /// The generation for which our cache is valid. + pub undisplayed_style_cache_generation: u64, +} + +/// The data itself is an `AtomicRefCell`, which guarantees the proper semantics +/// and unexpected races while trying to mutate it. +pub struct PerDocumentStyleData(AtomicRefCell<PerDocumentStyleDataImpl>); + +impl PerDocumentStyleData { + /// Create a `PerDocumentStyleData`. + pub fn new(document: *const structs::Document) -> Self { + let device = Device::new(document); + let quirks_mode = device.document().mCompatMode; + + PerDocumentStyleData(AtomicRefCell::new(PerDocumentStyleDataImpl { + stylist: Stylist::new(device, quirks_mode.into()), + undisplayed_style_cache: Default::default(), + undisplayed_style_cache_generation: 0, + })) + } + + /// Get an immutable reference to this style data. + pub fn borrow(&self) -> AtomicRef<PerDocumentStyleDataImpl> { + self.0.borrow() + } + + /// Get an mutable reference to this style data. + pub fn borrow_mut(&self) -> AtomicRefMut<PerDocumentStyleDataImpl> { + self.0.borrow_mut() + } +} + +impl PerDocumentStyleDataImpl { + /// Recreate the style data if the stylesheets have changed. + pub fn flush_stylesheets<E>( + &mut self, + guard: &SharedRwLockReadGuard, + document_element: Option<E>, + snapshots: Option<&SnapshotMap>, + ) -> bool + where + E: TElement, + { + self.stylist + .flush(&StylesheetGuards::same(guard), document_element, snapshots) + } + + /// Get the default computed values for this document. + pub fn default_computed_values(&self) -> &Arc<ComputedValues> { + self.stylist.device().default_computed_values_arc() + } + + /// Measure heap usage. + pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.stylist.add_size_of(ops, sizes); + } +} + +/// The gecko-specific AuthorStyles instantiation. +pub type AuthorStyles = crate::author_styles::AuthorStyles<GeckoStyleSheet>; diff --git a/servo/components/style/gecko/media_features.rs b/servo/components/style/gecko/media_features.rs new file mode 100644 index 0000000000..c9ad30b28b --- /dev/null +++ b/servo/components/style/gecko/media_features.rs @@ -0,0 +1,1003 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's media feature list and evaluator. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::ScreenColorGamut; +use crate::media_queries::{Device, MediaType}; +use crate::queries::condition::KleeneValue; +use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; +use crate::queries::values::Orientation; +use crate::values::computed::{CSSPixelLength, Context, Ratio, Resolution}; +use crate::values::AtomString; +use app_units::Au; +use euclid::default::Size2D; + +fn device_size(device: &Device) -> Size2D<Au> { + let mut width = 0; + let mut height = 0; + unsafe { + bindings::Gecko_MediaFeatures_GetDeviceSize(device.document(), &mut width, &mut height); + } + Size2D::new(Au(width), Au(height)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#width +fn eval_width(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().width.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-width +fn eval_device_width(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(device_size(context.device()).width.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#height +fn eval_height(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(context.device().au_viewport_size().height.to_f32_px()) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-height +fn eval_device_height(context: &Context) -> CSSPixelLength { + CSSPixelLength::new(device_size(context.device()).height.to_f32_px()) +} + +fn eval_aspect_ratio_for<F>(context: &Context, get_size: F) -> Ratio +where + F: FnOnce(&Device) -> Size2D<Au>, +{ + let size = get_size(context.device()); + Ratio::new(size.width.0 as f32, size.height.0 as f32) +} + +/// https://drafts.csswg.org/mediaqueries-4/#aspect-ratio +fn eval_aspect_ratio(context: &Context) -> Ratio { + eval_aspect_ratio_for(context, Device::au_viewport_size) +} + +/// https://drafts.csswg.org/mediaqueries-4/#device-aspect-ratio +fn eval_device_aspect_ratio(context: &Context) -> Ratio { + eval_aspect_ratio_for(context, device_size) +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-device-pixel-ratio +fn eval_device_pixel_ratio(context: &Context) -> f32 { + eval_resolution(context).dppx() +} + +/// https://drafts.csswg.org/mediaqueries-4/#orientation +fn eval_orientation(context: &Context, value: Option<Orientation>) -> bool { + Orientation::eval(context.device().au_viewport_size(), value) +} + +/// FIXME: There's no spec for `-moz-device-orientation`. +fn eval_device_orientation(context: &Context, value: Option<Orientation>) -> bool { + Orientation::eval(device_size(context.device()), value) +} + +/// Values for the display-mode media feature. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum DisplayMode { + Browser = 0, + MinimalUi, + Standalone, + Fullscreen, +} + +/// https://w3c.github.io/manifest/#the-display-mode-media-feature +fn eval_display_mode(context: &Context, query_value: Option<DisplayMode>) -> bool { + match query_value { + Some(v) => { + v == unsafe { + bindings::Gecko_MediaFeatures_GetDisplayMode(context.device().document()) + } + }, + None => true, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#grid +fn eval_grid(_: &Context) -> bool { + // Gecko doesn't support grid devices (e.g., ttys), so the 'grid' feature + // is always 0. + false +} + +/// https://compat.spec.whatwg.org/#css-media-queries-webkit-transform-3d +fn eval_transform_3d(_: &Context) -> bool { + true +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Scan { + Progressive, + Interlace, +} + +/// https://drafts.csswg.org/mediaqueries-4/#scan +fn eval_scan(_: &Context, _: Option<Scan>) -> bool { + // Since Gecko doesn't support the 'tv' media type, the 'scan' feature never + // matches. + false +} + +/// https://drafts.csswg.org/mediaqueries-4/#color +fn eval_color(context: &Context) -> i32 { + unsafe { bindings::Gecko_MediaFeatures_GetColorDepth(context.device().document()) } +} + +/// https://drafts.csswg.org/mediaqueries-4/#color-index +fn eval_color_index(_: &Context) -> i32 { + // We should return zero if the device does not use a color lookup table. + 0 +} + +/// https://drafts.csswg.org/mediaqueries-4/#monochrome +fn eval_monochrome(context: &Context) -> i32 { + // For color devices we should return 0. + unsafe { bindings::Gecko_MediaFeatures_GetMonochromeBitsPerPixel(context.device().document()) } +} + +/// Values for the color-gamut media feature. +/// This implements PartialOrd so that lower values will correctly match +/// higher capabilities. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] +#[repr(u8)] +enum ColorGamut { + /// The sRGB gamut. + Srgb, + /// The gamut specified by the Display P3 Color Space. + P3, + /// The gamut specified by the ITU-R Recommendation BT.2020 Color Space. + Rec2020, +} + +/// https://drafts.csswg.org/mediaqueries-4/#color-gamut +fn eval_color_gamut(context: &Context, query_value: Option<ColorGamut>) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + let color_gamut = + unsafe { bindings::Gecko_MediaFeatures_ColorGamut(context.device().document()) }; + // Match if our color gamut is at least as wide as the query value + query_value <= + match color_gamut { + // EndGuard_ is not a valid color gamut, so the default color-gamut is used. + ScreenColorGamut::Srgb | ScreenColorGamut::EndGuard_ => ColorGamut::Srgb, + ScreenColorGamut::P3 => ColorGamut::P3, + ScreenColorGamut::Rec2020 => ColorGamut::Rec2020, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#resolution +fn eval_resolution(context: &Context) -> Resolution { + let resolution_dppx = + unsafe { bindings::Gecko_MediaFeatures_GetResolution(context.device().document()) }; + Resolution::from_dppx(resolution_dppx) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum PrefersReducedMotion { + NoPreference, + Reduce, +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum PrefersReducedTransparency { + NoPreference, + Reduce, +} + +/// Values for the prefers-color-scheme media feature. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum PrefersColorScheme { + Light, + Dark, +} + +/// Values for the dynamic-range and video-dynamic-range media features. +/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range +/// This implements PartialOrd so that lower values will correctly match +/// higher capabilities. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, PartialOrd, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum DynamicRange { + Standard, + High, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-motion +fn eval_prefers_reduced_motion( + context: &Context, + query_value: Option<PrefersReducedMotion>, +) -> bool { + let prefers_reduced = + unsafe { bindings::Gecko_MediaFeatures_PrefersReducedMotion(context.device().document()) }; + let query_value = match query_value { + Some(v) => v, + None => return prefers_reduced, + }; + + match query_value { + PrefersReducedMotion::NoPreference => !prefers_reduced, + PrefersReducedMotion::Reduce => prefers_reduced, + } +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-reduced-transparency +fn eval_prefers_reduced_transparency( + context: &Context, + query_value: Option<PrefersReducedTransparency>, +) -> bool { + let prefers_reduced = unsafe { + bindings::Gecko_MediaFeatures_PrefersReducedTransparency(context.device().document()) + }; + let query_value = match query_value { + Some(v) => v, + None => return prefers_reduced, + }; + + match query_value { + PrefersReducedTransparency::NoPreference => !prefers_reduced, + PrefersReducedTransparency::Reduce => prefers_reduced, + } +} + +/// Possible values for prefers-contrast media query. +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum PrefersContrast { + /// More contrast is preferred. + More, + /// Low contrast is preferred. + Less, + /// Custom (not more, not less). + Custom, + /// The default value if neither high or low contrast is enabled. + NoPreference, +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-contrast +fn eval_prefers_contrast(context: &Context, query_value: Option<PrefersContrast>) -> bool { + let prefers_contrast = + unsafe { bindings::Gecko_MediaFeatures_PrefersContrast(context.device().document()) }; + match query_value { + Some(v) => v == prefers_contrast, + None => prefers_contrast != PrefersContrast::NoPreference, + } +} + +/// Possible values for the forced-colors media query. +/// https://drafts.csswg.org/mediaqueries-5/#forced-colors +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum ForcedColors { + /// Page colors are not being forced. + None, + /// Page colors are being forced. + Active, +} + +/// https://drafts.csswg.org/mediaqueries-5/#forced-colors +fn eval_forced_colors(context: &Context, query_value: Option<ForcedColors>) -> bool { + let forced = !context.device().use_document_colors(); + match query_value { + Some(query_value) => forced == (query_value == ForcedColors::Active), + None => forced, + } +} + +/// Possible values for the inverted-colors media query. +/// https://drafts.csswg.org/mediaqueries-5/#inverted +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum InvertedColors { + /// Colors are displayed normally. + None, + /// All pixels within the displayed area have been inverted. + Inverted, +} + +/// https://drafts.csswg.org/mediaqueries-5/#inverted +fn eval_inverted_colors(context: &Context, query_value: Option<InvertedColors>) -> bool { + let inverted_colors = + unsafe { bindings::Gecko_MediaFeatures_InvertedColors(context.device().document()) }; + let query_value = match query_value { + Some(v) => v, + None => return inverted_colors, + }; + + match query_value { + InvertedColors::None => !inverted_colors, + InvertedColors::Inverted => inverted_colors, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum OverflowBlock { + None, + Scroll, + Paged, +} + +/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-block +fn eval_overflow_block(context: &Context, query_value: Option<OverflowBlock>) -> bool { + // For the time being, assume that printing (including previews) + // is the only time when we paginate, and we are otherwise always + // scrolling. This is true at the moment in Firefox, but may need + // updating in the future (e.g., ebook readers built with Stylo, a + // billboard mode that doesn't support overflow at all). + // + // If this ever changes, don't forget to change eval_overflow_inline too. + let scrolling = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return true, + }; + + match query_value { + OverflowBlock::None => false, + OverflowBlock::Scroll => scrolling, + OverflowBlock::Paged => !scrolling, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum OverflowInline { + None, + Scroll, +} + +/// https://drafts.csswg.org/mediaqueries-4/#mf-overflow-inline +fn eval_overflow_inline(context: &Context, query_value: Option<OverflowInline>) -> bool { + // See the note in eval_overflow_block. + let scrolling = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return scrolling, + }; + + match query_value { + OverflowInline::None => !scrolling, + OverflowInline::Scroll => scrolling, + } +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Update { + None, + Slow, + Fast, +} + +/// https://drafts.csswg.org/mediaqueries-4/#update +fn eval_update(context: &Context, query_value: Option<Update>) -> bool { + // This has similar caveats to those described in eval_overflow_block. + // For now, we report that print (incl. print media simulation, + // which can in fact update but is limited to the developer tools) + // is `update: none` and that all other contexts are `update: fast`, + // which may not be true for future platforms, like e-ink devices. + let can_update = context.device().media_type() != MediaType::print(); + let query_value = match query_value { + Some(v) => v, + None => return can_update, + }; + + match query_value { + Update::None => !can_update, + Update::Slow => false, + Update::Fast => can_update, + } +} + +fn do_eval_prefers_color_scheme( + context: &Context, + use_content: bool, + query_value: Option<PrefersColorScheme>, +) -> bool { + let prefers_color_scheme = unsafe { + bindings::Gecko_MediaFeatures_PrefersColorScheme(context.device().document(), use_content) + }; + match query_value { + Some(v) => prefers_color_scheme == v, + None => true, + } +} + +/// https://drafts.csswg.org/mediaqueries-5/#prefers-color-scheme +fn eval_prefers_color_scheme(context: &Context, query_value: Option<PrefersColorScheme>) -> bool { + do_eval_prefers_color_scheme(context, /* use_content = */ false, query_value) +} + +fn eval_content_prefers_color_scheme( + context: &Context, + query_value: Option<PrefersColorScheme>, +) -> bool { + do_eval_prefers_color_scheme(context, /* use_content = */ true, query_value) +} + +/// https://drafts.csswg.org/mediaqueries-5/#dynamic-range +fn eval_dynamic_range(context: &Context, query_value: Option<DynamicRange>) -> bool { + let dynamic_range = + unsafe { bindings::Gecko_MediaFeatures_DynamicRange(context.device().document()) }; + match query_value { + Some(v) => dynamic_range >= v, + None => false, + } +} +/// https://drafts.csswg.org/mediaqueries-5/#video-dynamic-range +fn eval_video_dynamic_range(context: &Context, query_value: Option<DynamicRange>) -> bool { + let dynamic_range = + unsafe { bindings::Gecko_MediaFeatures_VideoDynamicRange(context.device().document()) }; + match query_value { + Some(v) => dynamic_range >= v, + None => false, + } +} + +bitflags! { + /// https://drafts.csswg.org/mediaqueries-4/#mf-interaction + struct PointerCapabilities: u8 { + const COARSE = structs::PointerCapabilities_Coarse; + const FINE = structs::PointerCapabilities_Fine; + const HOVER = structs::PointerCapabilities_Hover; + } +} + +fn primary_pointer_capabilities(context: &Context) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_PrimaryPointerCapabilities(context.device().document()) + }) +} + +fn all_pointer_capabilities(context: &Context) -> PointerCapabilities { + PointerCapabilities::from_bits_truncate(unsafe { + bindings::Gecko_MediaFeatures_AllPointerCapabilities(context.device().document()) + }) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Pointer { + None, + Coarse, + Fine, +} + +fn eval_pointer_capabilities( + query_value: Option<Pointer>, + pointer_capabilities: PointerCapabilities, +) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return !pointer_capabilities.is_empty(), + }; + + match query_value { + Pointer::None => pointer_capabilities.is_empty(), + Pointer::Coarse => pointer_capabilities.intersects(PointerCapabilities::COARSE), + Pointer::Fine => pointer_capabilities.intersects(PointerCapabilities::FINE), + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#pointer +fn eval_pointer(context: &Context, query_value: Option<Pointer>) -> bool { + eval_pointer_capabilities(query_value, primary_pointer_capabilities(context)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-pointer +fn eval_any_pointer(context: &Context, query_value: Option<Pointer>) -> bool { + eval_pointer_capabilities(query_value, all_pointer_capabilities(context)) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Hover { + None, + Hover, +} + +fn eval_hover_capabilities( + query_value: Option<Hover>, + pointer_capabilities: PointerCapabilities, +) -> bool { + let can_hover = pointer_capabilities.intersects(PointerCapabilities::HOVER); + let query_value = match query_value { + Some(v) => v, + None => return can_hover, + }; + + match query_value { + Hover::None => !can_hover, + Hover::Hover => can_hover, + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#hover +fn eval_hover(context: &Context, query_value: Option<Hover>) -> bool { + eval_hover_capabilities(query_value, primary_pointer_capabilities(context)) +} + +/// https://drafts.csswg.org/mediaqueries-4/#descdef-media-any-hover +fn eval_any_hover(context: &Context, query_value: Option<Hover>) -> bool { + eval_hover_capabilities(query_value, all_pointer_capabilities(context)) +} + +fn eval_moz_is_glyph(context: &Context) -> bool { + context.device().document().mIsSVGGlyphsDocument() +} + +fn eval_moz_print_preview(context: &Context) -> bool { + let is_print_preview = context.device().is_print_preview(); + if is_print_preview { + debug_assert_eq!(context.device().media_type(), MediaType::print()); + } + is_print_preview +} + +fn eval_moz_is_resource_document(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_IsResourceDocument(context.device().document()) } +} + +/// Allows front-end CSS to discern platform via media queries. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +pub enum Platform { + /// Matches any Android version. + Android, + /// For our purposes here, "linux" is just "gtk" (so unix-but-not-mac). + /// There's no need for our front-end code to differentiate between those + /// platforms and they already use the "linux" string elsewhere (e.g., + /// toolkit/themes/linux). + Linux, + /// Matches any macOS version. + Macos, + /// Matches any Windows version. + Windows, +} + +fn eval_moz_platform(_: &Context, query_value: Option<Platform>) -> bool { + let query_value = match query_value { + Some(v) => v, + None => return false, + }; + + unsafe { bindings::Gecko_MediaFeatures_MatchesPlatform(query_value) } +} + +/// Allows front-end CSS to discern gtk theme via media queries. +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum GtkThemeFamily { + /// Unknown theme family. + Unknown = 0, + /// Adwaita, the default GTK theme. + Adwaita, + /// Breeze, the default KDE theme. + Breeze, + /// Yaru, the default Ubuntu theme. + Yaru, +} + +fn eval_gtk_theme_family(_: &Context, query_value: Option<GtkThemeFamily>) -> bool { + let family = unsafe { bindings::Gecko_MediaFeatures_GtkThemeFamily() }; + match query_value { + Some(v) => v == family, + None => return family != GtkThemeFamily::Unknown, + } +} + +/// Values for the scripting media feature. +/// https://drafts.csswg.org/mediaqueries-5/#scripting +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, PartialEq, ToCss)] +#[repr(u8)] +pub enum Scripting { + /// Scripting is not supported or not enabled + None, + /// Scripting is supported and enabled, but only for initial page load + /// We will never match this value as it is intended for non-browser user agents, + /// but it is part of the spec so we should still parse it. + /// See: https://github.com/w3c/csswg-drafts/issues/8621 + InitialOnly, + /// Scripting is supported and enabled + Enabled, +} + +/// https://drafts.csswg.org/mediaqueries-5/#scripting +fn eval_scripting(context: &Context, query_value: Option<Scripting>) -> bool { + let scripting = unsafe { bindings::Gecko_MediaFeatures_Scripting(context.device().document()) }; + match query_value { + Some(v) => v == scripting, + None => scripting != Scripting::None, + } +} + +fn eval_moz_overlay_scrollbars(context: &Context) -> bool { + unsafe { bindings::Gecko_MediaFeatures_UseOverlayScrollbars(context.device().document()) } +} + +fn eval_moz_bool_pref(_: &Context, pref: Option<&AtomString>) -> KleeneValue { + let Some(pref) = pref else { + return KleeneValue::False; + }; + KleeneValue::from(unsafe { bindings::Gecko_ComputeBoolPrefMediaQuery(pref.as_ptr()) }) +} + +fn get_lnf_int(int_id: i32) -> i32 { + unsafe { bindings::Gecko_GetLookAndFeelInt(int_id) } +} + +fn get_lnf_int_as_bool(int_id: i32) -> bool { + get_lnf_int(int_id) != 0 +} + +fn get_scrollbar_start_backward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartBackward as i32) != 0 +} + +fn get_scrollbar_start_forward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_StartForward as i32) != 0 +} + +fn get_scrollbar_end_backward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndBackward as i32) != 0 +} + +fn get_scrollbar_end_forward(int_id: i32) -> bool { + (get_lnf_int(int_id) & bindings::LookAndFeel_eScrollArrow_EndForward as i32) != 0 +} + +macro_rules! lnf_int_feature { + ($feature_name:expr, $int_id:ident, $get_value:ident) => {{ + fn __eval(_: &Context) -> bool { + $get_value(bindings::LookAndFeel_IntID::$int_id as i32) + } + + feature!( + $feature_name, + AllowsRanges::No, + Evaluator::BoolInteger(__eval), + FeatureFlags::CHROME_AND_UA_ONLY, + ) + }}; + ($feature_name:expr, $int_id:ident) => {{ + lnf_int_feature!($feature_name, $int_id, get_lnf_int_as_bool) + }}; +} + +/// Adding new media features requires (1) adding the new feature to this +/// array, with appropriate entries (and potentially any new code needed +/// to support new types in these entries and (2) ensuring that either +/// nsPresContext::MediaFeatureValuesChanged is called when the value that +/// would be returned by the evaluator function could change. +pub static MEDIA_FEATURES: [QueryFeatureDescription; 59] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::Length(eval_width), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::Length(eval_height), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_aspect_ratio), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + FeatureFlags::VIEWPORT_DEPENDENT, + ), + feature!( + atom!("device-width"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_width), + FeatureFlags::empty(), + ), + feature!( + atom!("device-height"), + AllowsRanges::Yes, + Evaluator::Length(eval_device_height), + FeatureFlags::empty(), + ), + feature!( + atom!("device-aspect-ratio"), + AllowsRanges::Yes, + Evaluator::NumberRatio(eval_device_aspect_ratio), + FeatureFlags::empty(), + ), + feature!( + atom!("-moz-device-orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_device_orientation, Orientation), + FeatureFlags::empty(), + ), + // Webkit extensions that we support for de-facto web compatibility. + // -webkit-{min|max}-device-pixel-ratio (controlled with its own pref): + feature!( + atom!("device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::WEBKIT_PREFIX, + ), + // -webkit-transform-3d. + feature!( + atom!("transform-3d"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_transform_3d), + FeatureFlags::WEBKIT_PREFIX, + ), + feature!( + atom!("-moz-device-pixel-ratio"), + AllowsRanges::Yes, + Evaluator::Float(eval_device_pixel_ratio), + FeatureFlags::empty(), + ), + feature!( + atom!("resolution"), + AllowsRanges::Yes, + Evaluator::Resolution(eval_resolution), + FeatureFlags::empty(), + ), + feature!( + atom!("display-mode"), + AllowsRanges::No, + keyword_evaluator!(eval_display_mode, DisplayMode), + FeatureFlags::empty(), + ), + feature!( + atom!("grid"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_grid), + FeatureFlags::empty(), + ), + feature!( + atom!("scan"), + AllowsRanges::No, + keyword_evaluator!(eval_scan, Scan), + FeatureFlags::empty(), + ), + feature!( + atom!("color"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color), + FeatureFlags::empty(), + ), + feature!( + atom!("color-index"), + AllowsRanges::Yes, + Evaluator::Integer(eval_color_index), + FeatureFlags::empty(), + ), + feature!( + atom!("monochrome"), + AllowsRanges::Yes, + Evaluator::Integer(eval_monochrome), + FeatureFlags::empty(), + ), + feature!( + atom!("color-gamut"), + AllowsRanges::No, + keyword_evaluator!(eval_color_gamut, ColorGamut), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-reduced-motion"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_reduced_motion, PrefersReducedMotion), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-reduced-transparency"), + AllowsRanges::No, + keyword_evaluator!( + eval_prefers_reduced_transparency, + PrefersReducedTransparency + ), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-contrast"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_contrast, PrefersContrast), + // Note: by default this is only enabled in browser chrome and + // ua. It can be enabled on the web via the + // layout.css.prefers-contrast.enabled preference. See + // disabed_by_pref in media_feature_expression.rs for how that + // is done. + FeatureFlags::empty(), + ), + feature!( + atom!("forced-colors"), + AllowsRanges::No, + keyword_evaluator!(eval_forced_colors, ForcedColors), + FeatureFlags::empty(), + ), + feature!( + atom!("inverted-colors"), + AllowsRanges::No, + keyword_evaluator!(eval_inverted_colors, InvertedColors), + FeatureFlags::empty(), + ), + feature!( + atom!("overflow-block"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_block, OverflowBlock), + FeatureFlags::empty(), + ), + feature!( + atom!("overflow-inline"), + AllowsRanges::No, + keyword_evaluator!(eval_overflow_inline, OverflowInline), + FeatureFlags::empty(), + ), + feature!( + atom!("update"), + AllowsRanges::No, + keyword_evaluator!(eval_update, Update), + FeatureFlags::empty(), + ), + feature!( + atom!("prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_prefers_color_scheme, PrefersColorScheme), + FeatureFlags::empty(), + ), + feature!( + atom!("dynamic-range"), + AllowsRanges::No, + keyword_evaluator!(eval_dynamic_range, DynamicRange), + FeatureFlags::empty(), + ), + feature!( + atom!("video-dynamic-range"), + AllowsRanges::No, + keyword_evaluator!(eval_video_dynamic_range, DynamicRange), + FeatureFlags::empty(), + ), + feature!( + atom!("scripting"), + AllowsRanges::No, + keyword_evaluator!(eval_scripting, Scripting), + FeatureFlags::empty(), + ), + // Evaluates to the preferred color scheme for content. Only useful in + // chrome context, where the chrome color-scheme and the content + // color-scheme might differ. + feature!( + atom!("-moz-content-prefers-color-scheme"), + AllowsRanges::No, + keyword_evaluator!(eval_content_prefers_color_scheme, PrefersColorScheme), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("any-pointer"), + AllowsRanges::No, + keyword_evaluator!(eval_any_pointer, Pointer), + FeatureFlags::empty(), + ), + feature!( + atom!("hover"), + AllowsRanges::No, + keyword_evaluator!(eval_hover, Hover), + FeatureFlags::empty(), + ), + feature!( + atom!("any-hover"), + AllowsRanges::No, + keyword_evaluator!(eval_any_hover, Hover), + FeatureFlags::empty(), + ), + // Internal -moz-is-glyph media feature: applies only inside SVG glyphs. + // Internal because it is really only useful in the user agent anyway + // and therefore not worth standardizing. + feature!( + atom!("-moz-is-glyph"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_glyph), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-is-resource-document"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_is_resource_document), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-platform"), + AllowsRanges::No, + keyword_evaluator!(eval_moz_platform, Platform), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-gtk-theme-family"), + AllowsRanges::No, + keyword_evaluator!(eval_gtk_theme_family, GtkThemeFamily), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-print-preview"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_print_preview), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-overlay-scrollbars"), + AllowsRanges::No, + Evaluator::BoolInteger(eval_moz_overlay_scrollbars), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + feature!( + atom!("-moz-bool-pref"), + AllowsRanges::No, + Evaluator::String(eval_moz_bool_pref), + FeatureFlags::CHROME_AND_UA_ONLY, + ), + lnf_int_feature!( + atom!("-moz-scrollbar-start-backward"), + ScrollArrowStyle, + get_scrollbar_start_backward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-start-forward"), + ScrollArrowStyle, + get_scrollbar_start_forward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-end-backward"), + ScrollArrowStyle, + get_scrollbar_end_backward + ), + lnf_int_feature!( + atom!("-moz-scrollbar-end-forward"), + ScrollArrowStyle, + get_scrollbar_end_forward + ), + lnf_int_feature!(atom!("-moz-menubar-drag"), MenuBarDrag), + lnf_int_feature!(atom!("-moz-mac-big-sur-theme"), MacBigSurTheme), + lnf_int_feature!(atom!("-moz-mac-rtl"), MacRTL), + lnf_int_feature!( + atom!("-moz-windows-accent-color-in-titlebar"), + WindowsAccentColorInTitlebar + ), + lnf_int_feature!(atom!("-moz-swipe-animation-enabled"), SwipeAnimationEnabled), + lnf_int_feature!(atom!("-moz-gtk-csd-available"), GTKCSDAvailable), + lnf_int_feature!(atom!("-moz-gtk-csd-minimize-button"), GTKCSDMinimizeButton), + lnf_int_feature!(atom!("-moz-gtk-csd-maximize-button"), GTKCSDMaximizeButton), + lnf_int_feature!(atom!("-moz-gtk-csd-close-button"), GTKCSDCloseButton), + lnf_int_feature!( + atom!("-moz-gtk-csd-reversed-placement"), + GTKCSDReversedPlacement + ), + lnf_int_feature!(atom!("-moz-system-dark-theme"), SystemUsesDarkTheme), + lnf_int_feature!(atom!("-moz-panel-animations"), PanelAnimations), +]; diff --git a/servo/components/style/gecko/media_queries.rs b/servo/components/style/gecko/media_queries.rs new file mode 100644 index 0000000000..ef156ab380 --- /dev/null +++ b/servo/components/style/gecko/media_queries.rs @@ -0,0 +1,593 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's media-query device and expression representation. + +use crate::color::AbsoluteColor; +use crate::context::QuirksMode; +use crate::custom_properties::CssEnvironment; +use crate::font_metrics::FontMetrics; +use crate::gecko::values::{convert_absolute_color_to_nscolor, convert_nscolor_to_absolute_color}; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::logical_geometry::WritingMode; +use crate::media_queries::MediaType; +use crate::properties::ComputedValues; +use crate::string_cache::Atom; +use crate::values::computed::font::GenericFontFamily; +use crate::values::computed::{ColorScheme, Length, NonNegativeLength}; +use crate::values::specified::color::SystemColor; +use crate::values::specified::font::{FONT_MEDIUM_LINE_HEIGHT_PX, FONT_MEDIUM_PX}; +use crate::values::specified::ViewportVariant; +use crate::values::{CustomIdent, KeyframesName}; +use app_units::{Au, AU_PER_PX}; +use euclid::default::Size2D; +use euclid::{Scale, SideOffsets2D}; +use servo_arc::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; +use std::{cmp, fmt}; +use style_traits::{CSSPixel, DevicePixel}; + +/// The `Device` in Gecko wraps a pres context, has a default values computed, +/// and contains all the viewport rule state. +pub struct Device { + /// NB: The document owns the styleset, who owns the stylist, and thus the + /// `Device`, so having a raw document pointer here is fine. + document: *const structs::Document, + default_values: Arc<ComputedValues>, + /// The font size of the root element. + /// + /// This is set when computing the style of the root element, and used for + /// rem units in other elements. + /// + /// When computing the style of the root element, there can't be any other + /// style being computed at the same time, given we need the style of the + /// parent to compute everything else. So it is correct to just use a + /// relaxed atomic here. + root_font_size: AtomicU32, + /// Line height of the root element, used for rlh units in other elements. + root_line_height: AtomicU32, + /// The body text color, stored as an `nscolor`, used for the "tables + /// inherit from body" quirk. + /// + /// <https://quirks.spec.whatwg.org/#the-tables-inherit-color-from-body-quirk> + body_text_color: AtomicUsize, + /// Whether any styles computed in the document relied on the root font-size + /// by using rem units. + used_root_font_size: AtomicBool, + /// Whether any styles computed in the document relied on the root line-height + /// by using rlh units. + used_root_line_height: AtomicBool, + /// Whether any styles computed in the document relied on font metrics. + used_font_metrics: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using vw/vh/vmin/vmax units. + used_viewport_size: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size + /// by using dvw/dvh/dvmin/dvmax units. + used_dynamic_viewport_size: AtomicBool, + /// The CssEnvironment object responsible of getting CSS environment + /// variables. + environment: CssEnvironment, +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + + let mut doc_uri = nsCString::new(); + unsafe { + bindings::Gecko_nsIURI_Debug((*self.document()).mDocumentURI.raw(), &mut doc_uri) + }; + + f.debug_struct("Device") + .field("document_url", &doc_uri) + .finish() + } +} + +unsafe impl Sync for Device {} +unsafe impl Send for Device {} + +impl Device { + /// Trivially constructs a new `Device`. + pub fn new(document: *const structs::Document) -> Self { + assert!(!document.is_null()); + let doc = unsafe { &*document }; + let prefs = unsafe { &*bindings::Gecko_GetPrefSheetPrefs(doc) }; + Device { + document, + default_values: ComputedValues::default_values(doc), + root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), + root_line_height: AtomicU32::new(FONT_MEDIUM_LINE_HEIGHT_PX.to_bits()), + // This gets updated when we see the <body>, so it doesn't really + // matter which color-scheme we look at here. + body_text_color: AtomicUsize::new(prefs.mLightColors.mDefault as usize), + used_root_font_size: AtomicBool::new(false), + used_root_line_height: AtomicBool::new(false), + used_font_metrics: AtomicBool::new(false), + used_viewport_size: AtomicBool::new(false), + used_dynamic_viewport_size: AtomicBool::new(false), + environment: CssEnvironment, + } + } + + /// Get the relevant environment to resolve `env()` functions. + #[inline] + pub fn environment(&self) -> &CssEnvironment { + &self.environment + } + + /// 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. + pub fn calc_line_height( + &self, + font: &crate::properties::style_structs::Font, + writing_mode: WritingMode, + element: Option<super::wrapper::GeckoElement>, + ) -> NonNegativeLength { + let pres_context = self.pres_context(); + let line_height = font.clone_line_height(); + let au = Au(unsafe { + bindings::Gecko_CalcLineHeight( + &line_height, + pres_context.map_or(std::ptr::null(), |pc| pc), + writing_mode.is_text_vertical(), + &**font, + element.map_or(std::ptr::null(), |e| e.0), + ) + }); + NonNegativeLength::new(au.to_f32_px()) + } + + /// Whether any animation name may be referenced from the style of any + /// element. + pub fn animation_name_may_be_referenced(&self, name: &KeyframesName) -> bool { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return false, + }; + + unsafe { + bindings::Gecko_AnimationNameMayBeReferencedFromStyle(pc, name.as_atom().as_ptr()) + } + } + + /// Returns the default computed values as a reference, in order to match + /// Servo. + pub fn default_computed_values(&self) -> &ComputedValues { + &self.default_values + } + + /// Returns the default computed values as an `Arc`. + pub fn default_computed_values_arc(&self) -> &Arc<ComputedValues> { + &self.default_values + } + + /// Get the font size of the root element (for rem) + pub fn root_font_size(&self) -> Length { + self.used_root_font_size.store(true, Ordering::Relaxed); + Length::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed))) + } + + /// Set the font size of the root element (for rem) + pub fn set_root_font_size(&self, size: Length) { + self.root_font_size + .store(size.px().to_bits(), Ordering::Relaxed) + } + + /// Get the line height of the root element (for rlh) + pub fn root_line_height(&self) -> Length { + self.used_root_line_height.store(true, Ordering::Relaxed); + Length::new(f32::from_bits( + self.root_line_height.load(Ordering::Relaxed), + )) + } + + /// Set the line height of the root element (for rlh) + pub fn set_root_line_height(&self, size: Length) { + self.root_line_height + .store(size.px().to_bits(), Ordering::Relaxed); + } + + /// The quirks mode of the document. + pub fn quirks_mode(&self) -> QuirksMode { + self.document().mCompatMode.into() + } + + /// Sets the body text color for the "inherit color from body" quirk. + /// + /// <https://quirks.spec.whatwg.org/#the-tables-inherit-color-from-body-quirk> + pub fn set_body_text_color(&self, color: AbsoluteColor) { + self.body_text_color.store( + convert_absolute_color_to_nscolor(&color) as usize, + Ordering::Relaxed, + ) + } + + /// Gets the base size given a generic font family and a language. + pub fn base_size_for_generic(&self, language: &Atom, generic: GenericFontFamily) -> Length { + unsafe { bindings::Gecko_GetBaseSize(self.document(), language.as_ptr(), generic) } + } + + /// Gets the size of the scrollbar in CSS pixels. + pub fn scrollbar_inline_size(&self) -> Length { + let pc = match self.pres_context() { + Some(pc) => pc, + // XXX: we could have a more reasonable default perhaps. + None => return Length::new(0.0), + }; + Length::new(unsafe { bindings::Gecko_GetScrollbarInlineSize(pc) }) + } + + /// Queries font metrics + pub fn query_font_metrics( + &self, + vertical: bool, + font: &crate::properties::style_structs::Font, + base_size: Length, + in_media_query: bool, + retrieve_math_scales: bool, + ) -> FontMetrics { + self.used_font_metrics.store(true, Ordering::Relaxed); + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Default::default(), + }; + let gecko_metrics = unsafe { + bindings::Gecko_GetFontMetrics( + pc, + vertical, + &**font, + base_size, + // we don't use the user font set in a media query + !in_media_query, + retrieve_math_scales, + ) + }; + FontMetrics { + x_height: Some(gecko_metrics.mXSize), + zero_advance_measure: if gecko_metrics.mChSize.px() >= 0. { + Some(gecko_metrics.mChSize) + } else { + None + }, + cap_height: if gecko_metrics.mCapHeight.px() >= 0. { + Some(gecko_metrics.mCapHeight) + } else { + None + }, + ic_width: if gecko_metrics.mIcWidth.px() >= 0. { + Some(gecko_metrics.mIcWidth) + } else { + None + }, + ascent: gecko_metrics.mAscent, + script_percent_scale_down: if gecko_metrics.mScriptPercentScaleDown > 0. { + Some(gecko_metrics.mScriptPercentScaleDown) + } else { + None + }, + script_script_percent_scale_down: if gecko_metrics.mScriptScriptPercentScaleDown > 0. { + Some(gecko_metrics.mScriptScriptPercentScaleDown) + } else { + None + }, + } + } + + /// Returns the body text color. + pub fn body_text_color(&self) -> AbsoluteColor { + convert_nscolor_to_absolute_color(self.body_text_color.load(Ordering::Relaxed) as u32) + } + + /// Gets the document pointer. + #[inline] + pub fn document(&self) -> &structs::Document { + unsafe { &*self.document } + } + + /// Gets the pres context associated with this document. + #[inline] + pub fn pres_context(&self) -> Option<&structs::nsPresContext> { + unsafe { + self.document() + .mPresShell + .as_ref()? + .mPresContext + .mRawPtr + .as_ref() + } + } + + /// Gets the preference stylesheet prefs for our document. + #[inline] + pub fn pref_sheet_prefs(&self) -> &structs::PreferenceSheet_Prefs { + unsafe { &*bindings::Gecko_GetPrefSheetPrefs(self.document()) } + } + + /// Recreates the default computed values. + pub fn reset_computed_values(&mut self) { + self.default_values = ComputedValues::default_values(self.document()); + } + + /// Rebuild all the cached data. + pub fn rebuild_cached_data(&mut self) { + self.reset_computed_values(); + self.used_root_font_size.store(false, Ordering::Relaxed); + self.used_root_line_height.store(false, Ordering::Relaxed); + self.used_font_metrics.store(false, Ordering::Relaxed); + self.used_viewport_size.store(false, Ordering::Relaxed); + self.used_dynamic_viewport_size + .store(false, Ordering::Relaxed); + } + + /// Returns whether we ever looked up the root font size of the device. + pub fn used_root_font_size(&self) -> bool { + self.used_root_font_size.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the root line-height of the device. + pub fn used_root_line_height(&self) -> bool { + self.used_root_line_height.load(Ordering::Relaxed) + } + + /// Recreates all the temporary state that the `Device` stores. + /// + /// This includes the viewport override from `@viewport` rules, and also the + /// default computed values. + pub fn reset(&mut self) { + self.reset_computed_values(); + } + + /// Returns whether this document is in print preview. + pub fn is_print_preview(&self) -> bool { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return false, + }; + pc.mType == structs::nsPresContext_nsPresContextType_eContext_PrintPreview + } + + /// Returns the current media type of the device. + pub fn media_type(&self) -> MediaType { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return MediaType::screen(), + }; + + // Gecko allows emulating random media with mMediaEmulationData.mMedium. + let medium_to_use = if !pc.mMediaEmulationData.mMedium.mRawPtr.is_null() { + pc.mMediaEmulationData.mMedium.mRawPtr + } else { + pc.mMedium as *const structs::nsAtom as *mut _ + }; + + MediaType(CustomIdent(unsafe { Atom::from_raw(medium_to_use) })) + } + + // It may make sense to account for @page rule margins here somehow, however + // it's not clear how that'd work, see: + // https://github.com/w3c/csswg-drafts/issues/5437 + fn page_size_minus_default_margin(&self, pc: &structs::nsPresContext) -> Size2D<Au> { + debug_assert!(pc.mIsRootPaginatedDocument() != 0); + let area = &pc.mPageSize; + let margin = &pc.mDefaultPageMargin; + let width = area.width - margin.left - margin.right; + let height = area.height - margin.top - margin.bottom; + Size2D::new(Au(cmp::max(width, 0)), Au(cmp::max(height, 0))) + } + + /// Returns the current viewport size in app units. + pub fn au_viewport_size(&self) -> Size2D<Au> { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Size2D::new(Au(0), Au(0)), + }; + + if pc.mIsRootPaginatedDocument() != 0 { + return self.page_size_minus_default_margin(pc); + } + + let area = &pc.mVisibleArea; + Size2D::new(Au(area.width), Au(area.height)) + } + + /// Returns the current viewport size in app units, recording that it's been + /// used for viewport unit resolution. + pub fn au_viewport_size_for_viewport_unit_resolution( + &self, + variant: ViewportVariant, + ) -> Size2D<Au> { + self.used_viewport_size.store(true, Ordering::Relaxed); + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Size2D::new(Au(0), Au(0)), + }; + + if pc.mIsRootPaginatedDocument() != 0 { + return self.page_size_minus_default_margin(pc); + } + + match variant { + ViewportVariant::UADefault => { + let size = &pc.mSizeForViewportUnits; + Size2D::new(Au(size.width), Au(size.height)) + }, + ViewportVariant::Small => { + let size = &pc.mVisibleArea; + Size2D::new(Au(size.width), Au(size.height)) + }, + ViewportVariant::Large => { + let size = &pc.mVisibleArea; + // Looks like IntCoordTyped is treated as if it's u32 in Rust. + debug_assert!( + /* pc.mDynamicToolbarMaxHeight >=0 && */ + pc.mDynamicToolbarMaxHeight < i32::MAX as u32 + ); + Size2D::new( + Au(size.width), + Au(size.height + + pc.mDynamicToolbarMaxHeight as i32 * pc.mCurAppUnitsPerDevPixel), + ) + }, + ViewportVariant::Dynamic => { + self.used_dynamic_viewport_size + .store(true, Ordering::Relaxed); + let size = &pc.mVisibleArea; + // Looks like IntCoordTyped is treated as if it's u32 in Rust. + debug_assert!( + /* pc.mDynamicToolbarHeight >=0 && */ + pc.mDynamicToolbarHeight < i32::MAX as u32 + ); + Size2D::new( + Au(size.width), + Au(size.height + + (pc.mDynamicToolbarMaxHeight - pc.mDynamicToolbarHeight) as i32 * + pc.mCurAppUnitsPerDevPixel), + ) + }, + } + } + + /// Returns whether we ever looked up the viewport size of the Device. + pub fn used_viewport_size(&self) -> bool { + self.used_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether we ever looked up the dynamic viewport size of the Device. + pub fn used_dynamic_viewport_size(&self) -> bool { + self.used_dynamic_viewport_size.load(Ordering::Relaxed) + } + + /// Returns whether font metrics have been queried. + pub fn used_font_metrics(&self) -> bool { + self.used_font_metrics.load(Ordering::Relaxed) + } + + /// Returns whether visited styles are enabled. + pub fn visited_styles_enabled(&self) -> bool { + unsafe { bindings::Gecko_VisitedStylesEnabled(self.document()) } + } + + /// Returns the number of app units per device pixel we're using currently. + pub fn app_units_per_device_pixel(&self) -> i32 { + match self.pres_context() { + Some(pc) => pc.mCurAppUnitsPerDevPixel, + None => AU_PER_PX, + } + } + + /// Returns the device pixel ratio. + pub fn device_pixel_ratio(&self) -> Scale<f32, CSSPixel, DevicePixel> { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return Scale::new(1.), + }; + + if pc.mMediaEmulationData.mDPPX > 0.0 { + return Scale::new(pc.mMediaEmulationData.mDPPX); + } + + let au_per_dpx = pc.mCurAppUnitsPerDevPixel as f32; + let au_per_px = AU_PER_PX as f32; + Scale::new(au_per_px / au_per_dpx) + } + + /// Returns whether document colors are enabled. + #[inline] + pub fn use_document_colors(&self) -> bool { + let doc = self.document(); + if doc.mIsBeingUsedAsImage() { + return true; + } + self.pref_sheet_prefs().mUseDocumentColors + } + + /// Computes a system color and returns it as an nscolor. + pub(crate) fn system_nscolor( + &self, + system_color: SystemColor, + color_scheme: &ColorScheme, + ) -> u32 { + unsafe { bindings::Gecko_ComputeSystemColor(system_color, self.document(), color_scheme) } + } + + /// Returns whether the used color-scheme for `color-scheme` should be dark. + pub(crate) fn is_dark_color_scheme(&self, color_scheme: &ColorScheme) -> bool { + unsafe { bindings::Gecko_IsDarkColorScheme(self.document(), color_scheme) } + } + + /// Returns the default background color. + /// + /// This is only for forced-colors/high-contrast, so looking at light colors + /// is ok. + pub fn default_background_color(&self) -> AbsoluteColor { + let normal = ColorScheme::normal(); + convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvas, &normal)) + } + + /// Returns the default foreground color. + /// + /// See above for looking at light colors only. + pub fn default_color(&self) -> AbsoluteColor { + let normal = ColorScheme::normal(); + convert_nscolor_to_absolute_color(self.system_nscolor(SystemColor::Canvastext, &normal)) + } + + /// Returns the current effective text zoom. + #[inline] + fn text_zoom(&self) -> f32 { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return 1., + }; + pc.mTextZoom + } + + /// Applies text zoom to a font-size or line-height value (see nsStyleFont::ZoomText). + #[inline] + pub fn zoom_text(&self, size: Length) -> Length { + size.scale_by(self.text_zoom()) + } + + /// Un-apply text zoom. + #[inline] + pub fn unzoom_text(&self, size: Length) -> Length { + size.scale_by(1. / self.text_zoom()) + } + + /// Returns safe area insets + pub fn safe_area_insets(&self) -> SideOffsets2D<f32, CSSPixel> { + let pc = match self.pres_context() { + Some(pc) => pc, + None => return SideOffsets2D::zero(), + }; + let mut top = 0.0; + let mut right = 0.0; + let mut bottom = 0.0; + let mut left = 0.0; + unsafe { + bindings::Gecko_GetSafeAreaInsets(pc, &mut top, &mut right, &mut bottom, &mut left) + }; + SideOffsets2D::new(top, right, bottom, left) + } + + /// Returns true if the given MIME type is supported + pub fn is_supported_mime_type(&self, mime_type: &str) -> bool { + unsafe { + bindings::Gecko_IsSupportedImageMimeType(mime_type.as_ptr(), mime_type.len() as u32) + } + } + + /// Return whether the document is a chrome document. + /// + /// This check is consistent with how we enable chrome rules for chrome:// and resource:// + /// stylesheets (and thus chrome:// documents). + #[inline] + pub fn chrome_rules_enabled_for_document(&self) -> bool { + self.document().mChromeRulesEnabled() + } +} diff --git a/servo/components/style/gecko/mod.rs b/servo/components/style/gecko/mod.rs new file mode 100644 index 0000000000..c32ded14f3 --- /dev/null +++ b/servo/components/style/gecko/mod.rs @@ -0,0 +1,23 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific style-system bits. + +#[macro_use] +mod non_ts_pseudo_class_list; + +pub mod arc_types; +pub mod conversions; +pub mod data; +pub mod media_features; +pub mod media_queries; +pub mod pseudo_element; +pub mod restyle_damage; +pub mod selector_parser; +pub mod snapshot; +pub mod snapshot_helpers; +pub mod traversal; +pub mod url; +pub mod values; +pub mod wrapper; diff --git a/servo/components/style/gecko/non_ts_pseudo_class_list.rs b/servo/components/style/gecko/non_ts_pseudo_class_list.rs new file mode 100644 index 0000000000..cc7495dd9c --- /dev/null +++ b/servo/components/style/gecko/non_ts_pseudo_class_list.rs @@ -0,0 +1,106 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/* + * This file contains a helper macro includes all supported non-tree-structural + * pseudo-classes. + * + * FIXME: Find a way to autogenerate this file. + * + * Expected usage is as follows: + * ``` + * macro_rules! pseudo_class_macro{ + * ([$(($css:expr, $name:ident, $gecko_type:tt, $state:tt, $flags:tt),)*]) => { + * // do stuff + * } + * } + * apply_non_ts_list!(pseudo_class_macro) + * ``` + * + * $gecko_type can be either "_" or an ident in Gecko's CSSPseudoClassType. + * $state can be either "_" or an expression of type ElementState. If present, + * the semantics are that the pseudo-class matches if any of the bits in + * $state are set on the element. + * $flags can be either "_" or an expression of type NonTSPseudoClassFlag, + * see selector_parser.rs for more details. + */ + +macro_rules! apply_non_ts_list { + ($apply_macro:ident) => { + $apply_macro! { + [ + ("-moz-table-border-nonzero", MozTableBorderNonzero, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-select-list-box", MozSelectListBox, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("link", Link, UNVISITED, _), + ("any-link", AnyLink, VISITED_OR_UNVISITED, _), + ("visited", Visited, VISITED, _), + ("active", Active, ACTIVE, _), + ("autofill", Autofill, AUTOFILL, _), + ("checked", Checked, CHECKED, _), + ("defined", Defined, DEFINED, _), + ("disabled", Disabled, DISABLED, _), + ("enabled", Enabled, ENABLED, _), + ("focus", Focus, FOCUS, _), + ("focus-within", FocusWithin, FOCUS_WITHIN, _), + ("focus-visible", FocusVisible, FOCUSRING, _), + ("hover", Hover, HOVER, _), + ("-moz-drag-over", MozDragOver, DRAGOVER, _), + ("target", Target, URLTARGET, _), + ("indeterminate", Indeterminate, INDETERMINATE, _), + ("-moz-inert", MozInert, INERT, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-devtools-highlighted", MozDevtoolsHighlighted, DEVTOOLS_HIGHLIGHTED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-styleeditor-transitioning", MozStyleeditorTransitioning, STYLEEDITOR_TRANSITIONING, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("fullscreen", Fullscreen, FULLSCREEN, _), + ("modal", Modal, MODAL, _), + ("-moz-topmost-modal", MozTopmostModal, TOPMOST_MODAL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-broken", MozBroken, BROKEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-has-dir-attr", MozHasDirAttr, HAS_DIR_ATTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-ltr", MozDirAttrLTR, HAS_DIR_ATTR_LTR, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-rtl", MozDirAttrRTL, HAS_DIR_ATTR_RTL, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-dir-attr-like-auto", MozDirAttrLikeAuto, HAS_DIR_ATTR_LIKE_AUTO, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + + ("-moz-autofill-preview", MozAutofillPreview, AUTOFILL_PREVIEW, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-value-empty", MozValueEmpty, VALUE_EMPTY, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-revealed", MozRevealed, REVEALED, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + + ("-moz-math-increment-script-level", MozMathIncrementScriptLevel, INCREMENT_SCRIPT_LEVEL, _), + + ("required", Required, REQUIRED, _), + ("popover-open", PopoverOpen, POPOVER_OPEN, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("optional", Optional, OPTIONAL_, _), + ("valid", Valid, VALID, _), + ("invalid", Invalid, INVALID, _), + ("in-range", InRange, INRANGE, _), + ("out-of-range", OutOfRange, OUTOFRANGE, _), + ("default", Default, DEFAULT, _), + ("placeholder-shown", PlaceholderShown, PLACEHOLDER_SHOWN, _), + ("read-only", ReadOnly, READONLY, _), + ("read-write", ReadWrite, READWRITE, _), + ("user-valid", UserValid, USER_VALID, _), + ("user-invalid", UserInvalid, USER_INVALID, _), + ("-moz-meter-optimum", MozMeterOptimum, OPTIMUM, _), + ("-moz-meter-sub-optimum", MozMeterSubOptimum, SUB_OPTIMUM, _), + ("-moz-meter-sub-sub-optimum", MozMeterSubSubOptimum, SUB_SUB_OPTIMUM, _), + + ("-moz-first-node", MozFirstNode, _, _), + ("-moz-last-node", MozLastNode, _, _), + ("-moz-only-whitespace", MozOnlyWhitespace, _, _), + ("-moz-native-anonymous", MozNativeAnonymous, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-placeholder", MozPlaceholder, _, _), + + // NOTE(emilio): Pseudo-classes below only depend on document state, and thus + // conceptually they should probably be media queries instead. + // + // However that has a set of trade-offs that might not be worth making. In + // particular, such media queries would prevent documents that match them from + // sharing user-agent stylesheets with documents that don't. Also, changes between + // media query results are more expensive than document state changes. So for now + // making them pseudo-classes is probably the right trade-off. + ("-moz-is-html", MozIsHTML, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS), + ("-moz-lwtheme", MozLWTheme, _, PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + ("-moz-window-inactive", MozWindowInactive, _, _), + ] + } + } +} diff --git a/servo/components/style/gecko/pseudo_element.rs b/servo/components/style/gecko/pseudo_element.rs new file mode 100644 index 0000000000..3bcd873455 --- /dev/null +++ b/servo/components/style/gecko/pseudo_element.rs @@ -0,0 +1,233 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's definition of a pseudo-element. +//! +//! Note that a few autogenerated bits of this live in +//! `pseudo_element_definition.mako.rs`. If you touch that file, you probably +//! need to update the checked-in files for Servo. + +use crate::gecko_bindings::structs::{self, PseudoStyleType}; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::{ComputedValues, PropertyFlags}; +use crate::selector_parser::{PseudoElementCascadeType, SelectorImpl}; +use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; +use crate::string_cache::Atom; +use crate::values::serialize_atom_identifier; +use crate::values::AtomIdent; +use cssparser::ToCss; +use static_prefs::pref; +use std::fmt; + +include!(concat!( + env!("OUT_DIR"), + "/gecko/pseudo_element_definition.rs" +)); + +impl ::selectors::parser::PseudoElement for PseudoElement { + type Impl = SelectorImpl; + + // ::slotted() should support all tree-abiding pseudo-elements, see + // https://drafts.csswg.org/css-scoping/#slotted-pseudo + // https://drafts.csswg.org/css-pseudo-4/#treelike + #[inline] + fn valid_after_slotted(&self) -> bool { + matches!( + *self, + Self::Before | + Self::After | + Self::Marker | + Self::Placeholder | + Self::FileSelectorButton + ) + } + + #[inline] + fn accepts_state_pseudo_classes(&self) -> bool { + self.supports_user_action_state() + } +} + +impl PseudoElement { + /// Returns the kind of cascade type that a given pseudo is going to use. + /// + /// In Gecko we only compute ::before and ::after eagerly. We save the rules + /// for anonymous boxes separately, so we resolve them as precomputed + /// pseudos. + /// + /// We resolve the others lazily, see `Servo_ResolvePseudoStyle`. + pub fn cascade_type(&self) -> PseudoElementCascadeType { + if self.is_eager() { + debug_assert!(!self.is_anon_box()); + return PseudoElementCascadeType::Eager; + } + + if self.is_precomputed() { + return PseudoElementCascadeType::Precomputed; + } + + PseudoElementCascadeType::Lazy + } + + /// Gets the canonical index of this eagerly-cascaded pseudo-element. + #[inline] + pub fn eager_index(&self) -> usize { + EAGER_PSEUDOS + .iter() + .position(|p| p == self) + .expect("Not an eager pseudo") + } + + /// Creates a pseudo-element from an eager index. + #[inline] + pub fn from_eager_index(i: usize) -> Self { + EAGER_PSEUDOS[i].clone() + } + + /// Whether animations for the current pseudo element are stored in the + /// parent element. + #[inline] + pub fn animations_stored_in_parent(&self) -> bool { + matches!(*self, Self::Before | Self::After | Self::Marker) + } + + /// Whether the current pseudo element is ::before or ::after. + #[inline] + pub fn is_before_or_after(&self) -> bool { + matches!(*self, Self::Before | Self::After) + } + + /// Whether this pseudo-element is the ::before pseudo. + #[inline] + pub fn is_before(&self) -> bool { + *self == PseudoElement::Before + } + + /// Whether this pseudo-element is the ::after pseudo. + #[inline] + pub fn is_after(&self) -> bool { + *self == PseudoElement::After + } + + /// Whether this pseudo-element is the ::marker pseudo. + #[inline] + pub fn is_marker(&self) -> bool { + *self == PseudoElement::Marker + } + + /// Whether this pseudo-element is the ::selection pseudo. + #[inline] + pub fn is_selection(&self) -> bool { + *self == PseudoElement::Selection + } + + /// Whether this pseudo-element is ::first-letter. + #[inline] + pub fn is_first_letter(&self) -> bool { + *self == PseudoElement::FirstLetter + } + + /// Whether this pseudo-element is ::first-line. + #[inline] + pub fn is_first_line(&self) -> bool { + *self == PseudoElement::FirstLine + } + + /// Whether this pseudo-element is the ::-moz-color-swatch pseudo. + #[inline] + pub fn is_color_swatch(&self) -> bool { + *self == PseudoElement::MozColorSwatch + } + + /// Whether this pseudo-element is lazily-cascaded. + #[inline] + pub fn is_lazy(&self) -> bool { + !self.is_eager() && !self.is_precomputed() + } + + /// The identifier of the highlight this pseudo-element represents. + pub fn highlight_name(&self) -> Option<&AtomIdent> { + match *self { + Self::Highlight(ref name) => Some(name), + _ => None, + } + } + + /// Whether this pseudo-element is the ::highlight pseudo. + pub fn is_highlight(&self) -> bool { + matches!(*self, Self::Highlight(_)) + } + + /// Whether this pseudo-element supports user action selectors. + pub fn supports_user_action_state(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE) != 0 + } + + /// Whether this pseudo-element is enabled for all content. + pub fn enabled_in_content(&self) -> bool { + match *self { + Self::Highlight(..) => pref!("dom.customHighlightAPI.enabled"), + Self::SliderFill | Self::SliderTrack | Self::SliderThumb => { + pref!("layout.css.modern-range-pseudos.enabled") + }, + // If it's not explicitly enabled in UA sheets or chrome, then we're enabled for + // content. + _ => (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME) == 0, + } + } + + /// Whether this pseudo is enabled explicitly in UA sheets. + pub fn enabled_in_ua_sheets(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS) != 0 + } + + /// Whether this pseudo is enabled explicitly in chrome sheets. + pub fn enabled_in_chrome(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_CHROME) != 0 + } + + /// Whether this pseudo-element skips flex/grid container display-based + /// fixup. + #[inline] + pub fn skip_item_display_fixup(&self) -> bool { + (self.flags() & structs::CSS_PSEUDO_ELEMENT_IS_FLEX_OR_GRID_ITEM) == 0 + } + + /// Whether this pseudo-element is precomputed. + #[inline] + pub fn is_precomputed(&self) -> bool { + self.is_anon_box() && !self.is_tree_pseudo_element() + } + + /// Property flag that properties must have to apply to this pseudo-element. + #[inline] + pub fn property_restriction(&self) -> Option<PropertyFlags> { + Some(match *self { + PseudoElement::FirstLetter => PropertyFlags::APPLIES_TO_FIRST_LETTER, + PseudoElement::FirstLine => PropertyFlags::APPLIES_TO_FIRST_LINE, + PseudoElement::Placeholder => PropertyFlags::APPLIES_TO_PLACEHOLDER, + PseudoElement::Cue => PropertyFlags::APPLIES_TO_CUE, + PseudoElement::Marker if static_prefs::pref!("layout.css.marker.restricted") => { + PropertyFlags::APPLIES_TO_MARKER + }, + _ => return None, + }) + } + + /// Whether this pseudo-element should actually exist if it has + /// the given styles. + pub fn should_exist(&self, style: &ComputedValues) -> bool { + debug_assert!(self.is_eager()); + + if style.get_box().clone_display() == Display::None { + return false; + } + + if self.is_before_or_after() && style.ineffective_content_property() { + return false; + } + + true + } +} diff --git a/servo/components/style/gecko/pseudo_element_definition.mako.rs b/servo/components/style/gecko/pseudo_element_definition.mako.rs new file mode 100644 index 0000000000..48f0618502 --- /dev/null +++ b/servo/components/style/gecko/pseudo_element_definition.mako.rs @@ -0,0 +1,278 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/// Gecko's pseudo-element definition. +/// +/// We intentionally double-box legacy ::-moz-tree pseudo-elements to keep the +/// size of PseudoElement (and thus selector components) small. +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] +pub enum PseudoElement { + % for pseudo in PSEUDOS: + /// ${pseudo.value} + % if pseudo.is_tree_pseudo_element(): + ${pseudo.capitalized_pseudo()}(thin_vec::ThinVec<Atom>), + % elif pseudo.pseudo_ident == "highlight": + ${pseudo.capitalized_pseudo()}(AtomIdent), + % else: + ${pseudo.capitalized_pseudo()}, + % endif + % endfor + /// ::-webkit-* that we don't recognize + /// https://github.com/whatwg/compat/issues/103 + UnknownWebkit(Atom), +} + +/// Important: If you change this, you should also update Gecko's +/// nsCSSPseudoElements::IsEagerlyCascadedInServo. +<% EAGER_PSEUDOS = ["Before", "After", "FirstLine", "FirstLetter"] %> +<% TREE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_tree_pseudo_element()] %> +<% SIMPLE_PSEUDOS = [pseudo for pseudo in PSEUDOS if pseudo.is_simple_pseudo_element()] %> + +/// The number of eager pseudo-elements. +pub const EAGER_PSEUDO_COUNT: usize = ${len(EAGER_PSEUDOS)}; + +/// The number of non-functional pseudo-elements. +pub const SIMPLE_PSEUDO_COUNT: usize = ${len(SIMPLE_PSEUDOS)}; + +/// The number of tree pseudo-elements. +pub const TREE_PSEUDO_COUNT: usize = ${len(TREE_PSEUDOS)}; + +/// The number of all pseudo-elements. +pub const PSEUDO_COUNT: usize = ${len(PSEUDOS)}; + +/// The list of eager pseudos. +pub const EAGER_PSEUDOS: [PseudoElement; EAGER_PSEUDO_COUNT] = [ + % for eager_pseudo_name in EAGER_PSEUDOS: + PseudoElement::${eager_pseudo_name}, + % endfor +]; + +<%def name="pseudo_element_variant(pseudo, tree_arg='..')">\ +PseudoElement::${pseudo.capitalized_pseudo()}${"({})".format(tree_arg) if not pseudo.is_simple_pseudo_element() else ""}\ +</%def> + +impl PseudoElement { + /// Returns an index of the pseudo-element. + #[inline] + pub fn index(&self) -> usize { + match *self { + % for i, pseudo in enumerate(PSEUDOS): + ${pseudo_element_variant(pseudo)} => ${i}, + % endfor + PseudoElement::UnknownWebkit(..) => unreachable!(), + } + } + + /// Returns an array of `None` values. + /// + /// FIXME(emilio): Integer generics can't come soon enough. + pub fn pseudo_none_array<T>() -> [Option<T>; PSEUDO_COUNT] { + [ + ${",\n ".join(["None" for pseudo in PSEUDOS])} + ] + } + + /// Whether this pseudo-element is an anonymous box. + #[inline] + pub fn is_anon_box(&self) -> bool { + match *self { + % for pseudo in PSEUDOS: + % if pseudo.is_anon_box(): + ${pseudo_element_variant(pseudo)} => true, + % endif + % endfor + _ => false, + } + } + + /// Whether this pseudo-element is eagerly-cascaded. + #[inline] + pub fn is_eager(&self) -> bool { + matches!(*self, + ${" | ".join(map(lambda name: "PseudoElement::{}".format(name), EAGER_PSEUDOS))}) + } + + /// Whether this pseudo-element is tree pseudo-element. + #[inline] + pub fn is_tree_pseudo_element(&self) -> bool { + match *self { + % for pseudo in TREE_PSEUDOS: + ${pseudo_element_variant(pseudo)} => true, + % endfor + _ => false, + } + } + + /// Whether this pseudo-element is an unknown Webkit-prefixed pseudo-element. + #[inline] + pub fn is_unknown_webkit_pseudo_element(&self) -> bool { + matches!(*self, PseudoElement::UnknownWebkit(..)) + } + + /// Gets the flags associated to this pseudo-element, or 0 if it's an + /// anonymous box. + pub fn flags(&self) -> u32 { + match *self { + % for pseudo in PSEUDOS: + ${pseudo_element_variant(pseudo)} => + % if pseudo.is_tree_pseudo_element(): + structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS_AND_CHROME, + % elif pseudo.is_anon_box(): + structs::CSS_PSEUDO_ELEMENT_ENABLED_IN_UA_SHEETS, + % else: + structs::SERVO_CSS_PSEUDO_ELEMENT_FLAGS_${pseudo.pseudo_ident}, + % endif + % endfor + PseudoElement::UnknownWebkit(..) => 0, + } + } + + /// Construct a pseudo-element from a `PseudoStyleType`. + #[inline] + pub fn from_pseudo_type(type_: PseudoStyleType, functional_pseudo_parameter: Option<AtomIdent>) -> Option<Self> { + match type_ { + % for pseudo in PSEUDOS: + % if pseudo.is_simple_pseudo_element(): + PseudoStyleType::${pseudo.pseudo_ident} => { + debug_assert!(functional_pseudo_parameter.is_none()); + Some(${pseudo_element_variant(pseudo)}) + }, + % endif + % endfor + PseudoStyleType::highlight => { + match functional_pseudo_parameter { + Some(p) => Some(PseudoElement::Highlight(p)), + None => None + } + } + _ => None, + } + } + + /// Construct a `PseudoStyleType` from a pseudo-element + #[inline] + pub fn pseudo_type(&self) -> PseudoStyleType { + match *self { + % for pseudo in PSEUDOS: + % if pseudo.is_tree_pseudo_element(): + PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::XULTree, + % elif pseudo.pseudo_ident == "highlight": + PseudoElement::${pseudo.capitalized_pseudo()}(..) => PseudoStyleType::${pseudo.pseudo_ident}, + % else: + PseudoElement::${pseudo.capitalized_pseudo()} => PseudoStyleType::${pseudo.pseudo_ident}, + % endif + % endfor + PseudoElement::UnknownWebkit(..) => unreachable!(), + } + } + + /// Get the argument list of a tree pseudo-element. + #[inline] + pub fn tree_pseudo_args(&self) -> Option<<&[Atom]> { + match *self { + % for pseudo in TREE_PSEUDOS: + PseudoElement::${pseudo.capitalized_pseudo()}(ref args) => Some(args), + % endfor + _ => None, + } + } + + /// Construct a tree pseudo-element from atom and args. + #[inline] + pub fn from_tree_pseudo_atom(atom: &Atom, args: Box<[Atom]>) -> Option<Self> { + % for pseudo in PSEUDOS: + % if pseudo.is_tree_pseudo_element(): + if atom == &atom!("${pseudo.value}") { + return Some(PseudoElement::${pseudo.capitalized_pseudo()}(args.into())); + } + % endif + % endfor + None + } + + /// Constructs a pseudo-element from a string of text. + /// + /// Returns `None` if the pseudo-element is not recognised. + #[inline] + pub fn from_slice(name: &str, allow_unkown_webkit: bool) -> Option<Self> { + // We don't need to support tree pseudos because functional + // pseudo-elements needs arguments, and thus should be created + // via other methods. + ascii_case_insensitive_phf_map! { + pseudo -> PseudoElement = { + % for pseudo in SIMPLE_PSEUDOS: + "${pseudo.value[1:]}" => ${pseudo_element_variant(pseudo)}, + % endfor + // Alias some legacy prefixed pseudos to their standardized name at parse time: + "-moz-selection" => PseudoElement::Selection, + "-moz-placeholder" => PseudoElement::Placeholder, + "-moz-list-bullet" => PseudoElement::Marker, + "-moz-list-number" => PseudoElement::Marker, + } + } + if let Some(p) = pseudo::get(name) { + return Some(p.clone()); + } + if starts_with_ignore_ascii_case(name, "-moz-tree-") { + return PseudoElement::tree_pseudo_element(name, Default::default()) + } + const WEBKIT_PREFIX: &str = "-webkit-"; + if allow_unkown_webkit && starts_with_ignore_ascii_case(name, WEBKIT_PREFIX) { + let part = string_as_ascii_lowercase(&name[WEBKIT_PREFIX.len()..]); + return Some(PseudoElement::UnknownWebkit(part.into())); + } + None + } + + /// Constructs a tree pseudo-element from the given name and arguments. + /// "name" must start with "-moz-tree-". + /// + /// Returns `None` if the pseudo-element is not recognized. + #[inline] + pub fn tree_pseudo_element(name: &str, args: thin_vec::ThinVec<Atom>) -> Option<Self> { + debug_assert!(starts_with_ignore_ascii_case(name, "-moz-tree-")); + let tree_part = &name[10..]; + % for pseudo in TREE_PSEUDOS: + if tree_part.eq_ignore_ascii_case("${pseudo.value[11:]}") { + return Some(${pseudo_element_variant(pseudo, "args")}); + } + % endfor + None + } +} + +impl ToCss for PseudoElement { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result where W: fmt::Write { + dest.write_char(':')?; + match *self { + % for pseudo in (p for p in PSEUDOS if p.pseudo_ident != "highlight"): + ${pseudo_element_variant(pseudo)} => dest.write_str("${pseudo.value}")?, + % endfor + PseudoElement::Highlight(ref name) => { + dest.write_str(":highlight(")?; + serialize_atom_identifier(name, dest)?; + dest.write_char(')')?; + } + PseudoElement::UnknownWebkit(ref atom) => { + dest.write_str(":-webkit-")?; + serialize_atom_identifier(atom, dest)?; + } + } + if let Some(args) = self.tree_pseudo_args() { + if !args.is_empty() { + dest.write_char('(')?; + let mut iter = args.iter(); + if let Some(first) = iter.next() { + serialize_atom_identifier(&first, dest)?; + for item in iter { + dest.write_str(", ")?; + serialize_atom_identifier(item, dest)?; + } + } + dest.write_char(')')?; + } + } + Ok(()) + } +} diff --git a/servo/components/style/gecko/regen_atoms.py b/servo/components/style/gecko/regen_atoms.py new file mode 100755 index 0000000000..61f2fc4c63 --- /dev/null +++ b/servo/components/style/gecko/regen_atoms.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python + +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import re +import os +import sys + +from io import BytesIO + +GECKO_DIR = os.path.dirname(__file__.replace("\\", "/")) +sys.path.insert(0, os.path.join(os.path.dirname(GECKO_DIR), "properties")) + +import build + + +# Matches lines like `GK_ATOM(foo, "foo", 0x12345678, true, nsStaticAtom, PseudoElementAtom)`. +PATTERN = re.compile( + '^GK_ATOM\(([^,]*),[^"]*"([^"]*)",\s*(0x[0-9a-f]+),\s*[^,]*,\s*([^,]*),\s*([^)]*)\)', + re.MULTILINE, +) +FILE = "include/nsGkAtomList.h" + + +def map_atom(ident): + if ident in { + "box", + "loop", + "match", + "mod", + "ref", + "self", + "type", + "use", + "where", + "in", + }: + return ident + "_" + return ident + + +class Atom: + def __init__(self, ident, value, hash, ty, atom_type): + self.ident = "nsGkAtoms_{}".format(ident) + self.original_ident = ident + self.value = value + self.hash = hash + # The Gecko type: "nsStaticAtom", "nsCSSPseudoElementStaticAtom", or + # "nsAnonBoxPseudoStaticAtom". + self.ty = ty + # The type of atom: "Atom", "PseudoElement", "NonInheritingAnonBox", + # or "InheritingAnonBox". + self.atom_type = atom_type + + if ( + self.is_pseudo_element() + or self.is_anon_box() + or self.is_tree_pseudo_element() + ): + self.pseudo_ident = (ident.split("_", 1))[1] + + if self.is_anon_box(): + assert self.is_inheriting_anon_box() or self.is_non_inheriting_anon_box() + + def type(self): + return self.ty + + def capitalized_pseudo(self): + return self.pseudo_ident[0].upper() + self.pseudo_ident[1:] + + def is_pseudo_element(self): + return self.atom_type == "PseudoElementAtom" + + def is_anon_box(self): + if self.is_tree_pseudo_element(): + return False + return self.is_non_inheriting_anon_box() or self.is_inheriting_anon_box() + + def is_non_inheriting_anon_box(self): + assert not self.is_tree_pseudo_element() + return self.atom_type == "NonInheritingAnonBoxAtom" + + def is_inheriting_anon_box(self): + if self.is_tree_pseudo_element(): + return False + return self.atom_type == "InheritingAnonBoxAtom" + + def is_tree_pseudo_element(self): + return self.value.startswith(":-moz-tree-") + + def is_simple_pseudo_element(self) -> bool: + return not (self.is_tree_pseudo_element() or self.pseudo_ident == "highlight") + + +def collect_atoms(objdir): + atoms = [] + path = os.path.abspath(os.path.join(objdir, FILE)) + print("cargo:rerun-if-changed={}".format(path)) + with open(path) as f: + content = f.read() + for result in PATTERN.finditer(content): + atoms.append( + Atom( + result.group(1), + result.group(2), + result.group(3), + result.group(4), + result.group(5), + ) + ) + return atoms + + +class FileAvoidWrite(BytesIO): + """File-like object that buffers output and only writes if content changed.""" + + def __init__(self, filename): + BytesIO.__init__(self) + self.name = filename + + def write(self, buf): + if isinstance(buf, str): + buf = buf.encode("utf-8") + BytesIO.write(self, buf) + + def close(self): + buf = self.getvalue() + BytesIO.close(self) + try: + with open(self.name, "rb") as f: + old_content = f.read() + if old_content == buf: + print("{} is not changed, skip".format(self.name)) + return + except IOError: + pass + with open(self.name, "wb") as f: + f.write(buf) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if not self.closed: + self.close() + + +PRELUDE = """ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// Autogenerated file created by components/style/gecko/regen_atoms.py. +// DO NOT EDIT DIRECTLY +"""[ + 1: +] + +RULE_TEMPLATE = """ + ("{atom}") => {{{{ + #[allow(unsafe_code)] #[allow(unused_unsafe)] + unsafe {{ $crate::string_cache::Atom::from_index_unchecked({index}) }} + }}}}; +"""[ + 1: +] + +MACRO_TEMPLATE = """ +/// Returns a static atom by passing the literal string it represents. +#[macro_export] +macro_rules! atom {{ +{body}\ +}} +""" + + +def write_atom_macro(atoms, file_name): + with FileAvoidWrite(file_name) as f: + f.write(PRELUDE) + macro_rules = [ + RULE_TEMPLATE.format(atom=atom.value, name=atom.ident, index=i) + for (i, atom) in enumerate(atoms) + ] + f.write(MACRO_TEMPLATE.format(body="".join(macro_rules))) + + +def write_pseudo_elements(atoms, target_filename): + pseudos = [] + for atom in atoms: + if ( + atom.type() == "nsCSSPseudoElementStaticAtom" + or atom.type() == "nsCSSAnonBoxPseudoStaticAtom" + ): + pseudos.append(atom) + + pseudo_definition_template = os.path.join( + GECKO_DIR, "pseudo_element_definition.mako.rs" + ) + print("cargo:rerun-if-changed={}".format(pseudo_definition_template)) + contents = build.render(pseudo_definition_template, PSEUDOS=pseudos) + + with FileAvoidWrite(target_filename) as f: + f.write(contents) + + +def generate_atoms(dist, out): + atoms = collect_atoms(dist) + write_atom_macro(atoms, os.path.join(out, "atom_macro.rs")) + write_pseudo_elements(atoms, os.path.join(out, "pseudo_element_definition.rs")) + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: {} dist out".format(sys.argv[0])) + exit(2) + generate_atoms(sys.argv[1], sys.argv[2]) diff --git a/servo/components/style/gecko/restyle_damage.rs b/servo/components/style/gecko/restyle_damage.rs new file mode 100644 index 0000000000..4749daea18 --- /dev/null +++ b/servo/components/style/gecko/restyle_damage.rs @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's restyle damage computation (aka change hints, aka `nsChangeHint`). + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::nsChangeHint; +use crate::matching::{StyleChange, StyleDifference}; +use crate::properties::ComputedValues; +use std::ops::{BitAnd, BitOr, BitOrAssign, Not}; + +/// The representation of Gecko's restyle damage is just a wrapper over +/// `nsChangeHint`. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct GeckoRestyleDamage(nsChangeHint); + +impl GeckoRestyleDamage { + /// Trivially construct a new `GeckoRestyleDamage`. + #[inline] + pub fn new(raw: nsChangeHint) -> Self { + GeckoRestyleDamage(raw) + } + + /// Get the inner change hint for this damage. + #[inline] + pub fn as_change_hint(&self) -> nsChangeHint { + self.0 + } + + /// Get an empty change hint, that is (`nsChangeHint(0)`). + #[inline] + pub fn empty() -> Self { + GeckoRestyleDamage(nsChangeHint(0)) + } + + /// Returns whether this restyle damage represents the empty damage. + #[inline] + pub fn is_empty(&self) -> bool { + self.0 == nsChangeHint(0) + } + + /// Computes the `StyleDifference` (including the appropriate change hint) + /// given an old and a new style. + pub fn compute_style_difference( + old_style: &ComputedValues, + new_style: &ComputedValues, + ) -> StyleDifference { + let mut any_style_changed = false; + let mut reset_only = false; + let hint = unsafe { + bindings::Gecko_CalcStyleDifference( + old_style.as_gecko_computed_style(), + new_style.as_gecko_computed_style(), + &mut any_style_changed, + &mut reset_only, + ) + }; + if reset_only && !old_style.custom_properties_equal(new_style) { + // The Gecko_CalcStyleDifference call only checks the non-custom + // property structs, so we check the custom properties here. Since + // they generate no damage themselves, we can skip this check if we + // already know we had some inherited (regular) property + // differences. + any_style_changed = true; + reset_only = false; + } + let change = if any_style_changed { + StyleChange::Changed { reset_only } + } else { + StyleChange::Unchanged + }; + let damage = GeckoRestyleDamage(nsChangeHint(hint)); + StyleDifference { damage, change } + } + + /// Returns true if this restyle damage contains all the damage of |other|. + pub fn contains(self, other: Self) -> bool { + self & other == other + } + + /// Gets restyle damage to reconstruct the entire frame, subsuming all + /// other damage. + pub fn reconstruct() -> Self { + GeckoRestyleDamage(structs::nsChangeHint::nsChangeHint_ReconstructFrame) + } +} + +impl Default for GeckoRestyleDamage { + fn default() -> Self { + Self::empty() + } +} + +impl BitOr for GeckoRestyleDamage { + type Output = Self; + fn bitor(self, other: Self) -> Self { + GeckoRestyleDamage(self.0 | other.0) + } +} + +impl BitOrAssign for GeckoRestyleDamage { + fn bitor_assign(&mut self, other: Self) { + *self = *self | other; + } +} + +impl BitAnd for GeckoRestyleDamage { + type Output = Self; + fn bitand(self, other: Self) -> Self { + GeckoRestyleDamage(nsChangeHint((self.0).0 & (other.0).0)) + } +} + +impl Not for GeckoRestyleDamage { + type Output = Self; + fn not(self) -> Self { + GeckoRestyleDamage(nsChangeHint(!(self.0).0)) + } +} diff --git a/servo/components/style/gecko/selector_parser.rs b/servo/components/style/gecko/selector_parser.rs new file mode 100644 index 0000000000..203e6a3609 --- /dev/null +++ b/servo/components/style/gecko/selector_parser.rs @@ -0,0 +1,519 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific bits for selector-parsing. + +use crate::computed_value_flags::ComputedValueFlags; +use crate::invalidation::element::document_state::InvalidationMatchingData; +use crate::properties::ComputedValues; +use crate::selector_parser::{Direction, HorizontalDirection, SelectorParser}; +use crate::str::starts_with_ignore_ascii_case; +use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; +use crate::values::{AtomIdent, AtomString}; +use cssparser::{BasicParseError, BasicParseErrorKind, Parser}; +use cssparser::{CowRcStr, SourceLocation, ToCss, Token}; +use dom::{DocumentState, ElementState}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss as ToCss_}; +use thin_vec::ThinVec; + +pub use crate::gecko::pseudo_element::{ + PseudoElement, EAGER_PSEUDOS, EAGER_PSEUDO_COUNT, PSEUDO_COUNT, +}; +pub use crate::gecko::snapshot::SnapshotMap; + +bitflags! { + // See NonTSPseudoClass::is_enabled_in() + #[derive(Copy, Clone)] + struct NonTSPseudoClassFlag: u8 { + const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS = 1 << 0; + const PSEUDO_CLASS_ENABLED_IN_CHROME = 1 << 1; + const PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME = + NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS.bits() | + NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME.bits(); + } +} + +/// The type used to store the language argument to the `:lang` pseudo-class. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct Lang(#[css(iterable)] pub ThinVec<AtomIdent>); + +/// The type used to store the state argument to the `:state` pseudo-class. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub struct CustomState(pub AtomIdent); + +macro_rules! pseudo_class_name { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + /// Our representation of a non tree-structural pseudo-class. + #[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] + pub enum NonTSPseudoClass { + $( + #[doc = $css] + $name, + )* + /// The `:lang` pseudo-class. + Lang(Lang), + /// The `:dir` pseudo-class. + Dir(Direction), + /// The :state` pseudo-class. + CustomState(CustomState), + /// The non-standard `:-moz-locale-dir` pseudo-class. + MozLocaleDir(Direction), + } + } +} +apply_non_ts_list!(pseudo_class_name); + +impl ToCss for NonTSPseudoClass { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + macro_rules! pseudo_class_serialize { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => concat!(":", $css),)* + NonTSPseudoClass::Lang(ref lang) => { + dest.write_str(":lang(")?; + lang.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')'); + }, + NonTSPseudoClass::CustomState(ref state) => { + dest.write_str(":state(")?; + state.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')'); + }, + NonTSPseudoClass::MozLocaleDir(ref dir) => { + dest.write_str(":-moz-locale-dir(")?; + dir.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')') + }, + NonTSPseudoClass::Dir(ref dir) => { + dest.write_str(":dir(")?; + dir.to_css(&mut CssWriter::new(dest))?; + return dest.write_char(')') + }, + } + } + } + let ser = apply_non_ts_list!(pseudo_class_serialize); + dest.write_str(ser) + } +} + +impl NonTSPseudoClass { + /// Parses the name and returns a non-ts-pseudo-class if succeeds. + /// None otherwise. It doesn't check whether the pseudo-class is enabled + /// in a particular state. + pub fn parse_non_functional(name: &str) -> Option<Self> { + macro_rules! pseudo_class_parse { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match_ignore_ascii_case! { &name, + $($css => Some(NonTSPseudoClass::$name),)* + "-moz-full-screen" => Some(NonTSPseudoClass::Fullscreen), + "-moz-read-only" => Some(NonTSPseudoClass::ReadOnly), + "-moz-read-write" => Some(NonTSPseudoClass::ReadWrite), + "-moz-focusring" => Some(NonTSPseudoClass::FocusVisible), + "-moz-ui-valid" => Some(NonTSPseudoClass::UserValid), + "-moz-ui-invalid" => Some(NonTSPseudoClass::UserInvalid), + "-webkit-autofill" => Some(NonTSPseudoClass::Autofill), + _ => None, + } + } + } + apply_non_ts_list!(pseudo_class_parse) + } + + /// Returns true if this pseudo-class has any of the given flags set. + fn has_any_flag(&self, flags: NonTSPseudoClassFlag) -> bool { + macro_rules! check_flag { + (_) => { + false + }; + ($flags:ident) => { + NonTSPseudoClassFlag::$flags.intersects(flags) + }; + } + macro_rules! pseudo_class_check_is_enabled_in { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => check_flag!($flags),)* + NonTSPseudoClass::MozLocaleDir(_) => check_flag!(PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME), + NonTSPseudoClass::CustomState(_) | + NonTSPseudoClass::Lang(_) | + NonTSPseudoClass::Dir(_) => false, + } + } + } + apply_non_ts_list!(pseudo_class_check_is_enabled_in) + } + + /// Returns whether the pseudo-class is enabled in content sheets. + #[inline] + fn is_enabled_in_content(&self) -> bool { + if matches!(*self, Self::PopoverOpen) { + return static_prefs::pref!("dom.element.popover.enabled"); + } + if matches!(*self, Self::CustomState(_)) { + return static_prefs::pref!("dom.element.customstateset.enabled"); + } + !self.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS_AND_CHROME) + } + + /// Get the state flag associated with a pseudo-class, if any. + pub fn state_flag(&self) -> ElementState { + macro_rules! flag { + (_) => { + ElementState::empty() + }; + ($state:ident) => { + ElementState::$state + }; + } + macro_rules! pseudo_class_state { + ([$(($css:expr, $name:ident, $state:tt, $flags:tt),)*]) => { + match *self { + $(NonTSPseudoClass::$name => flag!($state),)* + NonTSPseudoClass::Dir(ref dir) => dir.element_state(), + NonTSPseudoClass::MozLocaleDir(..) | + NonTSPseudoClass::CustomState(..) | + NonTSPseudoClass::Lang(..) => ElementState::empty(), + } + } + } + apply_non_ts_list!(pseudo_class_state) + } + + /// Get the document state flag associated with a pseudo-class, if any. + pub fn document_state_flag(&self) -> DocumentState { + match *self { + NonTSPseudoClass::MozLocaleDir(ref dir) => match dir.as_horizontal_direction() { + Some(HorizontalDirection::Ltr) => DocumentState::LTR_LOCALE, + Some(HorizontalDirection::Rtl) => DocumentState::RTL_LOCALE, + None => DocumentState::empty(), + }, + NonTSPseudoClass::MozWindowInactive => DocumentState::WINDOW_INACTIVE, + NonTSPseudoClass::MozLWTheme => DocumentState::LWTHEME, + _ => DocumentState::empty(), + } + } + + /// Returns true if the given pseudoclass should trigger style sharing cache + /// revalidation. + pub fn needs_cache_revalidation(&self) -> bool { + self.state_flag().is_empty() && + !matches!( + *self, + // :dir() depends on state only, but may have an empty state_flag for invalid + // arguments. + NonTSPseudoClass::Dir(_) | + // We prevent style sharing for NAC. + NonTSPseudoClass::MozNativeAnonymous | + // :-moz-placeholder is parsed but never matches. + NonTSPseudoClass::MozPlaceholder | + // :-moz-is-html, :-moz-lwtheme, :-moz-locale-dir and :-moz-window-inactive + // depend only on the state of the document, which is invariant across all + // elements involved in a given style cache. + NonTSPseudoClass::MozIsHTML | + NonTSPseudoClass::MozLWTheme | + NonTSPseudoClass::MozLocaleDir(_) | + NonTSPseudoClass::MozWindowInactive + ) + } +} + +impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass { + type Impl = SelectorImpl; + + #[inline] + fn is_active_or_hover(&self) -> bool { + matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover) + } + + /// We intentionally skip the link-related ones. + #[inline] + fn is_user_action_state(&self) -> bool { + matches!( + *self, + NonTSPseudoClass::Hover | NonTSPseudoClass::Active | NonTSPseudoClass::Focus + ) + } +} + +/// The dummy struct we use to implement our selector parsing. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SelectorImpl; + +/// A set of extra data to carry along with the matching context, either for +/// selector-matching or invalidation. +#[derive(Default)] +pub struct ExtraMatchingData<'a> { + /// The invalidation data to invalidate doc-state pseudo-classes correctly. + pub invalidation_data: InvalidationMatchingData, + + /// The invalidation bits from matching container queries. These are here + /// just for convenience mostly. + pub cascade_input_flags: ComputedValueFlags, + + /// The style of the originating element in order to evaluate @container + /// size queries affecting pseudo-elements. + pub originating_element_style: Option<&'a ComputedValues>, +} + +impl ::selectors::SelectorImpl for SelectorImpl { + type ExtraMatchingData<'a> = ExtraMatchingData<'a>; + type AttrValue = AtomString; + type Identifier = AtomIdent; + type LocalName = AtomIdent; + type NamespacePrefix = AtomIdent; + type NamespaceUrl = Namespace; + type BorrowedNamespaceUrl = WeakNamespace; + type BorrowedLocalName = WeakAtom; + + type PseudoElement = PseudoElement; + type NonTSPseudoClass = NonTSPseudoClass; + + fn should_collect_attr_hash(name: &AtomIdent) -> bool { + !crate::bloom::is_attr_name_excluded_from_filter(name) + } +} + +impl<'a> SelectorParser<'a> { + fn is_pseudo_class_enabled(&self, pseudo_class: &NonTSPseudoClass) -> bool { + if pseudo_class.is_enabled_in_content() { + return true; + } + + if self.in_user_agent_stylesheet() && + pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_UA_SHEETS) + { + return true; + } + + if self.chrome_rules_enabled() && + pseudo_class.has_any_flag(NonTSPseudoClassFlag::PSEUDO_CLASS_ENABLED_IN_CHROME) + { + return true; + } + + if matches!(*pseudo_class, NonTSPseudoClass::MozBroken) { + return static_prefs::pref!("layout.css.moz-broken.content.enabled"); + } + + return false; + } + + fn is_pseudo_element_enabled(&self, pseudo_element: &PseudoElement) -> bool { + if pseudo_element.enabled_in_content() { + return true; + } + + if self.in_user_agent_stylesheet() && pseudo_element.enabled_in_ua_sheets() { + return true; + } + + if self.chrome_rules_enabled() && pseudo_element.enabled_in_chrome() { + return true; + } + + return false; + } +} + +impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { + type Impl = SelectorImpl; + type Error = StyleParseErrorKind<'i>; + + #[inline] + fn parse_parent_selector(&self) -> bool { + true + } + + #[inline] + fn parse_slotted(&self) -> bool { + true + } + + #[inline] + fn parse_host(&self) -> bool { + true + } + + #[inline] + fn parse_nth_child_of(&self) -> bool { + true + } + + #[inline] + fn parse_is_and_where(&self) -> bool { + true + } + + #[inline] + fn parse_has(&self) -> bool { + static_prefs::pref!("layout.css.has-selector.enabled") + } + + #[inline] + fn parse_part(&self) -> bool { + true + } + + #[inline] + fn is_is_alias(&self, function: &str) -> bool { + function.eq_ignore_ascii_case("-moz-any") + } + + #[inline] + fn allow_forgiving_selectors(&self) -> bool { + !self.for_supports_rule + } + + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<NonTSPseudoClass, ParseError<'i>> { + if let Some(pseudo_class) = NonTSPseudoClass::parse_non_functional(&name) { + if self.is_pseudo_class_enabled(&pseudo_class) { + return Ok(pseudo_class); + } + } + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut Parser<'i, 't>, + after_part: bool, + ) -> Result<NonTSPseudoClass, ParseError<'i>> { + let pseudo_class = match_ignore_ascii_case! { &name, + "lang" if !after_part => { + let result = parser.parse_comma_separated(|input| { + Ok(AtomIdent::from(input.expect_ident_or_string()?.as_ref())) + })?; + if result.is_empty() { + return Err(parser.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + NonTSPseudoClass::Lang(Lang(result.into())) + }, + "state" => { + let result = AtomIdent::from(parser.expect_ident()?.as_ref()); + NonTSPseudoClass::CustomState(CustomState(result)) + }, + "-moz-locale-dir" if !after_part => { + NonTSPseudoClass::MozLocaleDir(Direction::parse(parser)?) + }, + "dir" if !after_part => { + NonTSPseudoClass::Dir(Direction::parse(parser)?) + }, + _ => return Err(parser.new_custom_error( + SelectorParseErrorKind::UnsupportedPseudoClassOrElement(name.clone()) + )) + }; + if self.is_pseudo_class_enabled(&pseudo_class) { + Ok(pseudo_class) + } else { + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<PseudoElement, ParseError<'i>> { + let allow_unkown_webkit = !self.for_supports_rule; + if let Some(pseudo) = PseudoElement::from_slice(&name, allow_unkown_webkit) { + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + + Err( + location.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn parse_functional_pseudo_element<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut Parser<'i, 't>, + ) -> Result<PseudoElement, ParseError<'i>> { + if starts_with_ignore_ascii_case(&name, "-moz-tree-") { + // Tree pseudo-elements can have zero or more arguments, separated + // by either comma or space. + let mut args = ThinVec::new(); + loop { + let location = parser.current_source_location(); + match parser.next() { + Ok(&Token::Ident(ref ident)) => args.push(Atom::from(ident.as_ref())), + Ok(&Token::Comma) => {}, + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(BasicParseError { + kind: BasicParseErrorKind::EndOfInput, + .. + }) => break, + _ => unreachable!("Parser::next() shouldn't return any other error"), + } + } + if let Some(pseudo) = PseudoElement::tree_pseudo_element(&name, args) { + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + } else if name.eq_ignore_ascii_case("highlight") { + let pseudo = PseudoElement::Highlight(AtomIdent::from(parser.expect_ident()?.as_ref())); + if self.is_pseudo_element_enabled(&pseudo) { + return Ok(pseudo); + } + } + Err( + parser.new_custom_error(SelectorParseErrorKind::UnsupportedPseudoClassOrElement( + name, + )), + ) + } + + fn default_namespace(&self) -> Option<Namespace> { + self.namespaces.default.clone() + } + + fn namespace_for_prefix(&self, prefix: &AtomIdent) -> Option<Namespace> { + self.namespaces.prefixes.get(prefix).cloned() + } +} + +impl SelectorImpl { + /// A helper to traverse each eagerly cascaded pseudo-element, executing + /// `fun` on it. + #[inline] + pub fn each_eagerly_cascaded_pseudo_element<F>(mut fun: F) + where + F: FnMut(PseudoElement), + { + for pseudo in &EAGER_PSEUDOS { + fun(pseudo.clone()) + } + } +} + +// Selector and component sizes are important for matching performance. +size_of_test!(selectors::parser::Selector<SelectorImpl>, 8); +size_of_test!(selectors::parser::Component<SelectorImpl>, 24); +size_of_test!(PseudoElement, 16); +size_of_test!(NonTSPseudoClass, 16); diff --git a/servo/components/style/gecko/snapshot.rs b/servo/components/style/gecko/snapshot.rs new file mode 100644 index 0000000000..2ff04406ac --- /dev/null +++ b/servo/components/style/gecko/snapshot.rs @@ -0,0 +1,174 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A gecko snapshot, that stores the element attributes and state before they +//! change in order to properly calculate restyle hints. + +use crate::dom::TElement; +use crate::gecko::snapshot_helpers; +use crate::gecko::wrapper::GeckoElement; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::ServoElementSnapshot; +use crate::gecko_bindings::structs::ServoElementSnapshotFlags as Flags; +use crate::gecko_bindings::structs::ServoElementSnapshotTable; +use crate::invalidation::element::element_wrapper::ElementSnapshot; +use crate::selector_parser::AttrValue; +use crate::string_cache::{Atom, Namespace}; +use crate::values::{AtomIdent, AtomString}; +use crate::LocalName; +use crate::WeakAtom; +use dom::ElementState; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; + +/// A snapshot of a Gecko element. +pub type GeckoElementSnapshot = ServoElementSnapshot; + +/// A map from elements to snapshots for Gecko's style back-end. +pub type SnapshotMap = ServoElementSnapshotTable; + +impl SnapshotMap { + /// Gets the snapshot for this element, if any. + /// + /// FIXME(emilio): The transmute() business we do here is kind of nasty, but + /// it's a consequence of the map being a OpaqueNode -> Snapshot table in + /// Servo and an Element -> Snapshot table in Gecko. + /// + /// We should be able to make this a more type-safe with type annotations by + /// making SnapshotMap a trait and moving the implementations outside, but + /// that's a pain because it implies parameterizing SharedStyleContext. + pub fn get<E: TElement>(&self, element: &E) -> Option<&GeckoElementSnapshot> { + debug_assert!(element.has_snapshot()); + + unsafe { + let element = ::std::mem::transmute::<&E, &GeckoElement>(element); + bindings::Gecko_GetElementSnapshot(self, element.0).as_ref() + } + } +} + +impl GeckoElementSnapshot { + #[inline] + fn has_any(&self, flags: Flags) -> bool { + (self.mContains as u8 & flags as u8) != 0 + } + + /// Returns true if the snapshot has stored state for pseudo-classes + /// that depend on things other than `ElementState`. + #[inline] + pub fn has_other_pseudo_class_state(&self) -> bool { + self.has_any(Flags::OtherPseudoClassState) + } + + /// Returns true if the snapshot recorded an id change. + #[inline] + pub fn id_changed(&self) -> bool { + self.mIdAttributeChanged() + } + + /// Returns true if the snapshot recorded a class attribute change. + #[inline] + pub fn class_changed(&self) -> bool { + self.mClassAttributeChanged() + } + + /// Executes the callback once for each attribute that changed. + #[inline] + pub fn each_attr_changed<F>(&self, mut callback: F) + where + F: FnMut(&AtomIdent), + { + for attr in self.mChangedAttrNames.iter() { + unsafe { AtomIdent::with(attr.mRawPtr, &mut callback) } + } + } + + /// selectors::Element::attr_matches + pub fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + snapshot_helpers::attr_matches(&self.mAttrs, ns, local_name, operation) + } +} + +impl ElementSnapshot for GeckoElementSnapshot { + fn debug_list_attributes(&self) -> String { + use nsstring::nsCString; + let mut string = nsCString::new(); + unsafe { + bindings::Gecko_Snapshot_DebugListAttributes(self, &mut string); + } + String::from_utf8_lossy(&*string).into_owned() + } + + fn state(&self) -> Option<ElementState> { + if self.has_any(Flags::State) { + Some(ElementState::from_bits_retain(self.mState)) + } else { + None + } + } + + #[inline] + fn has_attrs(&self) -> bool { + self.has_any(Flags::Attributes) + } + + #[inline] + fn id_attr(&self) -> Option<&WeakAtom> { + if !self.has_any(Flags::Id) { + return None; + } + + snapshot_helpers::get_id(&*self.mAttrs) + } + + #[inline] + fn is_part(&self, name: &AtomIdent) -> bool { + let attr = match snapshot_helpers::find_attr(&*self.mAttrs, &atom!("part")) { + Some(attr) => attr, + None => return false, + }; + + snapshot_helpers::has_class_or_part(name, CaseSensitivity::CaseSensitive, attr) + } + + #[inline] + fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> { + snapshot_helpers::imported_part(&*self.mAttrs, name) + } + + #[inline] + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + if !self.has_any(Flags::MaybeClass) { + return false; + } + + snapshot_helpers::has_class_or_part(name, case_sensitivity, &self.mClass) + } + + #[inline] + fn each_class<F>(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + if !self.has_any(Flags::MaybeClass) { + return; + } + + snapshot_helpers::each_class_or_part(&self.mClass, callback) + } + + #[inline] + fn lang_attr(&self) -> Option<AtomString> { + let ptr = unsafe { bindings::Gecko_SnapshotLangValue(self) }; + if ptr.is_null() { + None + } else { + Some(AtomString(unsafe { Atom::from_addrefed(ptr) })) + } + } +} diff --git a/servo/components/style/gecko/snapshot_helpers.rs b/servo/components/style/gecko/snapshot_helpers.rs new file mode 100644 index 0000000000..ab2d08eaf8 --- /dev/null +++ b/servo/components/style/gecko/snapshot_helpers.rs @@ -0,0 +1,316 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Element an snapshot common logic. + +use crate::dom::TElement; +use crate::gecko::wrapper::namespace_id_to_atom; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{self, nsAtom}; +use crate::invalidation::element::element_wrapper::ElementSnapshot; +use crate::selector_parser::{AttrValue, SnapshotMap}; +use crate::string_cache::WeakAtom; +use crate::values::AtomIdent; +use crate::{Atom, CaseSensitivityExt, LocalName, Namespace}; +use selectors::attr::{ + AttrSelectorOperation, AttrSelectorOperator, CaseSensitivity, NamespaceConstraint, +}; +use smallvec::SmallVec; + +/// A function that, given an element of type `T`, allows you to get a single +/// class or a class list. +enum Class<'a> { + None, + One(*const nsAtom), + More(&'a [structs::RefPtr<nsAtom>]), +} + +#[inline(always)] +fn base_type(attr: &structs::nsAttrValue) -> structs::nsAttrValue_ValueBaseType { + (attr.mBits & structs::NS_ATTRVALUE_BASETYPE_MASK) as structs::nsAttrValue_ValueBaseType +} + +#[inline(always)] +unsafe fn ptr<T>(attr: &structs::nsAttrValue) -> *const T { + (attr.mBits & !structs::NS_ATTRVALUE_BASETYPE_MASK) as *const T +} + +#[inline(always)] +unsafe fn get_class_or_part_from_attr(attr: &structs::nsAttrValue) -> Class { + debug_assert!(bindings::Gecko_AssertClassAttrValueIsSane(attr)); + let base_type = base_type(attr); + if base_type == structs::nsAttrValue_ValueBaseType_eAtomBase { + return Class::One(ptr::<nsAtom>(attr)); + } + if base_type == structs::nsAttrValue_ValueBaseType_eOtherBase { + let container = ptr::<structs::MiscContainer>(attr); + debug_assert_eq!( + (*container).mType, + structs::nsAttrValue_ValueType_eAtomArray + ); + // NOTE: Bindgen doesn't deal with AutoTArray, so cast it below. + let attr_array: *const _ = *(*container) + .__bindgen_anon_1 + .mValue + .as_ref() + .__bindgen_anon_1 + .mAtomArray + .as_ref(); + let array = + (*attr_array).mArray.as_ptr() as *const structs::nsTArray<structs::RefPtr<nsAtom>>; + return Class::More(&**array); + } + debug_assert_eq!(base_type, structs::nsAttrValue_ValueBaseType_eStringBase); + Class::None +} + +#[inline(always)] +unsafe fn get_id_from_attr(attr: &structs::nsAttrValue) -> &WeakAtom { + debug_assert_eq!( + base_type(attr), + structs::nsAttrValue_ValueBaseType_eAtomBase + ); + WeakAtom::new(ptr::<nsAtom>(attr)) +} + +impl structs::nsAttrName { + #[inline] + fn is_nodeinfo(&self) -> bool { + self.mBits & 1 != 0 + } + + #[inline] + unsafe fn as_nodeinfo(&self) -> &structs::NodeInfo { + debug_assert!(self.is_nodeinfo()); + &*((self.mBits & !1) as *const structs::NodeInfo) + } + + #[inline] + fn namespace_id(&self) -> i32 { + if !self.is_nodeinfo() { + return structs::kNameSpaceID_None; + } + unsafe { self.as_nodeinfo() }.mInner.mNamespaceID + } + + /// Returns the attribute name as an atom pointer. + #[inline] + pub fn name(&self) -> *const nsAtom { + if self.is_nodeinfo() { + unsafe { self.as_nodeinfo() }.mInner.mName + } else { + self.mBits as *const nsAtom + } + } +} + +/// Find an attribute value with a given name and no namespace. +#[inline(always)] +pub fn find_attr<'a>( + attrs: &'a [structs::AttrArray_InternalAttr], + name: &Atom, +) -> Option<&'a structs::nsAttrValue> { + attrs + .iter() + .find(|attr| attr.mName.mBits == name.as_ptr() as usize) + .map(|attr| &attr.mValue) +} + +/// Finds the id attribute from a list of attributes. +#[inline(always)] +pub fn get_id(attrs: &[structs::AttrArray_InternalAttr]) -> Option<&WeakAtom> { + Some(unsafe { get_id_from_attr(find_attr(attrs, &atom!("id"))?) }) +} + +#[inline(always)] +pub(super) fn each_exported_part( + attrs: &[structs::AttrArray_InternalAttr], + name: &AtomIdent, + mut callback: impl FnMut(&AtomIdent), +) { + let attr = match find_attr(attrs, &atom!("exportparts")) { + Some(attr) => attr, + None => return, + }; + let mut length = 0; + let atoms = unsafe { bindings::Gecko_Element_ExportedParts(attr, name.as_ptr(), &mut length) }; + if atoms.is_null() { + return; + } + + unsafe { + for atom in std::slice::from_raw_parts(atoms, length) { + AtomIdent::with(*atom, &mut callback) + } + } +} + +#[inline(always)] +pub(super) fn imported_part( + attrs: &[structs::AttrArray_InternalAttr], + name: &AtomIdent, +) -> Option<AtomIdent> { + let attr = find_attr(attrs, &atom!("exportparts"))?; + let atom = unsafe { bindings::Gecko_Element_ImportedPart(attr, name.as_ptr()) }; + if atom.is_null() { + return None; + } + Some(AtomIdent(unsafe { Atom::from_raw(atom) })) +} + +/// Given a class or part name, a case sensitivity, and an array of attributes, +/// returns whether the attribute has that name. +#[inline(always)] +pub fn has_class_or_part( + name: &AtomIdent, + case_sensitivity: CaseSensitivity, + attr: &structs::nsAttrValue, +) -> bool { + match unsafe { get_class_or_part_from_attr(attr) } { + Class::None => false, + Class::One(atom) => unsafe { case_sensitivity.eq_atom(name, WeakAtom::new(atom)) }, + Class::More(atoms) => match case_sensitivity { + CaseSensitivity::CaseSensitive => { + let name_ptr = name.as_ptr(); + atoms.iter().any(|atom| atom.mRawPtr == name_ptr) + }, + CaseSensitivity::AsciiCaseInsensitive => unsafe { + atoms + .iter() + .any(|atom| WeakAtom::new(atom.mRawPtr).eq_ignore_ascii_case(name)) + }, + }, + } +} + +/// Given an item, a callback, and a getter, execute `callback` for each class +/// or part name this `item` has. +#[inline(always)] +pub fn each_class_or_part<F>(attr: &structs::nsAttrValue, mut callback: F) +where + F: FnMut(&AtomIdent), +{ + unsafe { + match get_class_or_part_from_attr(attr) { + Class::None => {}, + Class::One(atom) => AtomIdent::with(atom, callback), + Class::More(atoms) => { + for atom in atoms { + AtomIdent::with(atom.mRawPtr, &mut callback) + } + }, + } + } +} + +/// Returns a list of classes that were either added to or removed from the +/// element since the snapshot. +pub fn classes_changed<E: TElement>(element: &E, snapshots: &SnapshotMap) -> SmallVec<[Atom; 8]> { + debug_assert!(element.has_snapshot(), "Why bothering?"); + let snapshot = snapshots.get(element).expect("has_snapshot lied"); + if !snapshot.class_changed() { + return SmallVec::new(); + } + + let mut classes_changed = SmallVec::<[Atom; 8]>::new(); + snapshot.each_class(|c| { + if !element.has_class(c, CaseSensitivity::CaseSensitive) { + classes_changed.push(c.0.clone()); + } + }); + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_changed.push(c.0.clone()); + } + }); + + classes_changed +} + +/// Returns whether a given attribute selector matches given the internal attrs. +#[inline(always)] +pub(crate) fn attr_matches( + attrs: &[structs::AttrArray_InternalAttr], + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, +) -> bool { + let name_ptr = local_name.as_ptr(); + for attr in attrs { + if attr.mName.name() != name_ptr { + continue; + } + + if attr_matches_checked_name(attr, ns, operation) { + return true; + } + + // The name matched but the value or namespace didn't. The only reason to check the other + // attributes now would be to find one with the same name but a different namespace. + if *ns != NamespaceConstraint::Any { + // We don't want to look for other namespaces, so we're done. + return false; + } + } + false +} + +/// Returns whether a given attribute selector matches given a single attribute, +/// for the case where the caller has already found an attribute with the right name. +fn attr_matches_checked_name( + attr: &structs::AttrArray_InternalAttr, + ns: &NamespaceConstraint<&Namespace>, + operation: &AttrSelectorOperation<&AttrValue>, +) -> bool { + let ns_matches = match *ns { + NamespaceConstraint::Any => true, + NamespaceConstraint::Specific(ns) => { + if *ns == ns!() { + !attr.mName.is_nodeinfo() + } else { + ns.as_ptr() == unsafe { namespace_id_to_atom(attr.mName.namespace_id()) } + } + }, + }; + + if !ns_matches { + return false; + } + + let (operator, case_sensitivity, value) = match *operation { + AttrSelectorOperation::Exists => return true, + AttrSelectorOperation::WithValue { + operator, + case_sensitivity, + value, + } => (operator, case_sensitivity, value), + }; + let ignore_case = match case_sensitivity { + CaseSensitivity::CaseSensitive => false, + CaseSensitivity::AsciiCaseInsensitive => true, + }; + let value = value.as_ptr(); + unsafe { + match operator { + AttrSelectorOperator::Equal => { + bindings::Gecko_AttrEquals(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Includes => { + bindings::Gecko_AttrIncludes(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::DashMatch => { + bindings::Gecko_AttrDashEquals(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Prefix => { + bindings::Gecko_AttrHasPrefix(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Suffix => { + bindings::Gecko_AttrHasSuffix(&attr.mValue, value, ignore_case) + }, + AttrSelectorOperator::Substring => { + bindings::Gecko_AttrHasSubstring(&attr.mValue, value, ignore_case) + }, + } + } +} diff --git a/servo/components/style/gecko/traversal.rs b/servo/components/style/gecko/traversal.rs new file mode 100644 index 0000000000..71d1a2f949 --- /dev/null +++ b/servo/components/style/gecko/traversal.rs @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko-specific bits for the styling DOM traversal. + +use crate::context::{SharedStyleContext, StyleContext}; +use crate::dom::{TElement, TNode}; +use crate::gecko::wrapper::{GeckoElement, GeckoNode}; +use crate::traversal::{recalc_style_at, DomTraversal, PerLevelTraversalData}; + +/// This is the simple struct that Gecko uses to encapsulate a DOM traversal for +/// styling. +pub struct RecalcStyleOnly<'a> { + shared: SharedStyleContext<'a>, +} + +impl<'a> RecalcStyleOnly<'a> { + /// Create a `RecalcStyleOnly` traversal from a `SharedStyleContext`. + pub fn new(shared: SharedStyleContext<'a>) -> Self { + RecalcStyleOnly { shared: shared } + } +} + +impl<'recalc, 'le> DomTraversal<GeckoElement<'le>> for RecalcStyleOnly<'recalc> { + fn process_preorder<F>( + &self, + traversal_data: &PerLevelTraversalData, + context: &mut StyleContext<GeckoElement<'le>>, + node: GeckoNode<'le>, + note_child: F, + ) where + F: FnMut(GeckoNode<'le>), + { + if let Some(el) = node.as_element() { + let mut data = unsafe { el.ensure_data() }; + recalc_style_at(self, traversal_data, context, el, &mut data, note_child); + } + } + + fn process_postorder(&self, _: &mut StyleContext<GeckoElement<'le>>, _: GeckoNode<'le>) { + unreachable!(); + } + + /// We don't use the post-order traversal for anything. + fn needs_postorder_traversal() -> bool { + false + } + + fn shared_context(&self) -> &SharedStyleContext { + &self.shared + } +} diff --git a/servo/components/style/gecko/url.rs b/servo/components/style/gecko/url.rs new file mode 100644 index 0000000000..7fe32acc20 --- /dev/null +++ b/servo/components/style/gecko/url.rs @@ -0,0 +1,384 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common handling for the specified value CSS url() values. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs; +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::{CorsMode, UrlExtraData}; +use crate::values::computed::{Context, ToComputedValue}; +use cssparser::Parser; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use nsstring::nsCString; +use servo_arc::Arc; +use std::collections::HashMap; +use std::fmt::{self, Write}; +use std::mem::ManuallyDrop; +use std::sync::RwLock; +use style_traits::{CssWriter, ParseError, ToCss}; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +/// A CSS url() value for gecko. +#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[css(function = "url")] +#[repr(C)] +pub struct CssUrl(pub Arc<CssUrlData>); + +/// Data shared between CssUrls. +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[derive(Debug, SpecifiedValueInfo, ToCss, ToShmem)] +#[repr(C)] +pub struct CssUrlData { + /// The URL in unresolved string form. + serialization: crate::OwnedStr, + + /// The URL extra data. + #[css(skip)] + pub extra_data: UrlExtraData, + + /// The CORS mode that will be used for the load. + #[css(skip)] + cors_mode: CorsMode, + + /// Data to trigger a load from Gecko. This is mutable in C++. + /// + /// TODO(emilio): Maybe we can eagerly resolve URLs and make this immutable? + #[css(skip)] + load_data: LoadDataSource, +} + +impl PartialEq for CssUrlData { + fn eq(&self, other: &Self) -> bool { + self.serialization == other.serialization && + self.extra_data == other.extra_data && + self.cors_mode == other.cors_mode + } +} + +impl CssUrl { + fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + let url = input.expect_url()?; + Ok(Self::parse_from_string( + url.as_ref().to_owned(), + context, + cors_mode, + )) + } + + /// Parse a URL from a string value that is a valid CSS token for a URL. + pub fn parse_from_string(url: String, context: &ParserContext, cors_mode: CorsMode) -> Self { + CssUrl(Arc::new(CssUrlData { + serialization: url.into(), + extra_data: context.url_data.clone(), + cors_mode, + load_data: LoadDataSource::Owned(LoadData::default()), + })) + } + + /// Returns true if the URL is definitely invalid. We don't eagerly resolve + /// URLs in gecko, so we just return false here. + /// use its |resolved| status. + pub fn is_invalid(&self) -> bool { + false + } + + /// Returns true if this URL looks like a fragment. + /// See https://drafts.csswg.org/css-values/#local-urls + #[inline] + pub fn is_fragment(&self) -> bool { + self.0.is_fragment() + } + + /// Return the unresolved url as string, or the empty string if it's + /// invalid. + #[inline] + pub fn as_str(&self) -> &str { + self.0.as_str() + } +} + +impl CssUrlData { + /// Returns true if this URL looks like a fragment. + /// See https://drafts.csswg.org/css-values/#local-urls + pub fn is_fragment(&self) -> bool { + self.as_str() + .as_bytes() + .iter() + .next() + .map_or(false, |b| *b == b'#') + } + + /// Return the unresolved url as string, or the empty string if it's + /// invalid. + pub fn as_str(&self) -> &str { + &*self.serialization + } +} + +impl Parse for CssUrl { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_cors_mode(context, input, CorsMode::None) + } +} + +impl Eq for CssUrl {} + +impl MallocSizeOf for CssUrl { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + // XXX: measure `serialization` once bug 1397971 lands + + // We ignore `extra_data`, because RefPtr is tricky, and there aren't + // many of them in practise (sharing is common). + + 0 + } +} + +/// A key type for LOAD_DATA_TABLE. +#[derive(Eq, Hash, PartialEq)] +struct LoadDataKey(*const LoadDataSource); + +unsafe impl Sync for LoadDataKey {} +unsafe impl Send for LoadDataKey {} + +bitflags! { + /// Various bits of mutable state that are kept for image loads. + #[derive(Debug)] + #[repr(C)] + pub struct LoadDataFlags: u8 { + /// Whether we tried to resolve the uri at least once. + const TRIED_TO_RESOLVE_URI = 1 << 0; + /// Whether we tried to resolve the image at least once. + const TRIED_TO_RESOLVE_IMAGE = 1 << 1; + } +} + +/// This is usable and movable from multiple threads just fine, as long as it's +/// not cloned (it is not clonable), and the methods that mutate it run only on +/// the main thread (when all the other threads we care about are paused). +unsafe impl Sync for LoadData {} +unsafe impl Send for LoadData {} + +/// The load data for a given URL. This is mutable from C++, and shouldn't be +/// accessed from rust for anything. +#[repr(C)] +#[derive(Debug)] +pub struct LoadData { + /// A strong reference to the imgRequestProxy, if any, that should be + /// released on drop. + /// + /// These are raw pointers because they are not safe to reference-count off + /// the main thread. + resolved_image: *mut structs::imgRequestProxy, + /// A strong reference to the resolved URI of this image. + resolved_uri: *mut structs::nsIURI, + /// A few flags that are set when resolving the image or such. + flags: LoadDataFlags, +} + +impl Drop for LoadData { + fn drop(&mut self) { + unsafe { bindings::Gecko_LoadData_Drop(self) } + } +} + +impl Default for LoadData { + fn default() -> Self { + Self { + resolved_image: std::ptr::null_mut(), + resolved_uri: std::ptr::null_mut(), + flags: LoadDataFlags::empty(), + } + } +} + +/// The data for a load, or a lazy-loaded, static member that will be stored in +/// LOAD_DATA_TABLE, keyed by the memory location of this object, which is +/// always in the heap because it's inside the CssUrlData object. +/// +/// This type is meant not to be used from C++ so we don't derive helper +/// methods. +/// +/// cbindgen:derive-helper-methods=false +#[derive(Debug)] +#[repr(u8, C)] +pub enum LoadDataSource { + /// An owned copy of the load data. + Owned(LoadData), + /// A lazily-resolved copy of it. + Lazy, +} + +impl LoadDataSource { + /// Gets the load data associated with the source. + /// + /// This relies on the source on being in a stable location if lazy. + #[inline] + pub unsafe fn get(&self) -> *const LoadData { + match *self { + LoadDataSource::Owned(ref d) => return d, + LoadDataSource::Lazy => {}, + } + + let key = LoadDataKey(self); + + { + let guard = LOAD_DATA_TABLE.read().unwrap(); + if let Some(r) = guard.get(&key) { + return &**r; + } + } + let mut guard = LOAD_DATA_TABLE.write().unwrap(); + let r = guard.entry(key).or_insert_with(Default::default); + &**r + } +} + +impl ToShmem for LoadDataSource { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + Ok(ManuallyDrop::new(match self { + LoadDataSource::Owned(..) => LoadDataSource::Lazy, + LoadDataSource::Lazy => LoadDataSource::Lazy, + })) + } +} + +/// A specified non-image `url()` value. +pub type SpecifiedUrl = CssUrl; + +/// Clears LOAD_DATA_TABLE. Entries in this table, which are for specified URL +/// values that come from shared memory style sheets, would otherwise persist +/// until the end of the process and be reported as leaks. +pub fn shutdown() { + LOAD_DATA_TABLE.write().unwrap().clear(); +} + +impl ToComputedValue for SpecifiedUrl { + type ComputedValue = ComputedUrl; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + ComputedUrl(self.clone()) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + computed.0.clone() + } +} + +/// A specified image `url()` value. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct SpecifiedImageUrl(pub SpecifiedUrl); + +impl SpecifiedImageUrl { + /// Parse a URL from a string value that is a valid CSS token for a URL. + pub fn parse_from_string(url: String, context: &ParserContext, cors_mode: CorsMode) -> Self { + SpecifiedImageUrl(SpecifiedUrl::parse_from_string(url, context, cors_mode)) + } + + /// Provides an alternate method for parsing that associates the URL + /// with anonymous CORS headers. + pub fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + Ok(SpecifiedImageUrl(SpecifiedUrl::parse_with_cors_mode( + context, input, cors_mode, + )?)) + } +} + +impl Parse for SpecifiedImageUrl { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + SpecifiedUrl::parse(context, input).map(SpecifiedImageUrl) + } +} + +impl ToComputedValue for SpecifiedImageUrl { + type ComputedValue = ComputedImageUrl; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedImageUrl(self.0.to_computed_value(context)) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + SpecifiedImageUrl(ToComputedValue::from_computed_value(&computed.0)) + } +} + +/// The computed value of a CSS non-image `url()`. +/// +/// The only difference between specified and computed URLs is the +/// serialization. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq)] +#[repr(C)] +pub struct ComputedUrl(pub SpecifiedUrl); + +impl ComputedUrl { + fn serialize_with<W>( + &self, + function: unsafe extern "C" fn(*const Self, *mut nsCString), + dest: &mut CssWriter<W>, + ) -> fmt::Result + where + W: Write, + { + dest.write_str("url(")?; + unsafe { + let mut string = nsCString::new(); + function(self, &mut string); + string.as_str_unchecked().to_css(dest)?; + } + dest.write_char(')') + } +} + +impl ToCss for ComputedUrl { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.serialize_with(bindings::Gecko_GetComputedURLSpec, dest) + } +} + +/// The computed value of a CSS image `url()`. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq)] +#[repr(transparent)] +pub struct ComputedImageUrl(pub ComputedUrl); + +impl ToCss for ComputedImageUrl { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0 + .serialize_with(bindings::Gecko_GetComputedImageURLSpec, dest) + } +} + +lazy_static! { + /// A table mapping CssUrlData objects to their lazily created LoadData + /// objects. + static ref LOAD_DATA_TABLE: RwLock<HashMap<LoadDataKey, Box<LoadData>>> = { + Default::default() + }; +} diff --git a/servo/components/style/gecko/values.rs b/servo/components/style/gecko/values.rs new file mode 100644 index 0000000000..d04c73c70f --- /dev/null +++ b/servo/components/style/gecko/values.rs @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! Different kind of helpers to interact with Gecko values. + +use crate::color::{AbsoluteColor, ColorSpace}; +use crate::counter_style::{Symbol, Symbols}; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::CounterStylePtr; +use crate::values::generics::CounterStyle; +use crate::values::Either; +use crate::Atom; + +/// Convert a color value to `nscolor`. +pub fn convert_absolute_color_to_nscolor(color: &AbsoluteColor) -> u32 { + let srgb = color.to_color_space(ColorSpace::Srgb); + u32::from_le_bytes([ + (srgb.components.0 * 255.0).round() as u8, + (srgb.components.1 * 255.0).round() as u8, + (srgb.components.2 * 255.0).round() as u8, + (srgb.alpha * 255.0).round() as u8, + ]) +} + +/// Convert a given `nscolor` to a Servo AbsoluteColor value. +pub fn convert_nscolor_to_absolute_color(color: u32) -> AbsoluteColor { + let [r, g, b, a] = color.to_le_bytes(); + AbsoluteColor::srgb_legacy(r, g, b, a as f32 / 255.0) +} + +#[test] +fn convert_ns_color_to_absolute_color_should_be_in_legacy_syntax() { + use crate::color::ColorFlags; + + let result = convert_nscolor_to_absolute_color(0x336699CC); + assert!(result.flags.contains(ColorFlags::IS_LEGACY_SRGB)); + + assert!(result.is_legacy_syntax()); +} + +impl CounterStyle { + /// Convert this counter style to a Gecko CounterStylePtr. + #[inline] + pub fn to_gecko_value(&self, gecko_value: &mut CounterStylePtr) { + unsafe { bindings::Gecko_CounterStyle_ToPtr(self, gecko_value) } + } + + /// Convert Gecko CounterStylePtr to CounterStyle or String. + pub fn from_gecko_value(gecko_value: &CounterStylePtr) -> Either<Self, String> { + use crate::values::CustomIdent; + + let name = unsafe { bindings::Gecko_CounterStyle_GetName(gecko_value) }; + if !name.is_null() { + let name = unsafe { Atom::from_raw(name) }; + debug_assert_ne!(name, atom!("none")); + Either::First(CounterStyle::Name(CustomIdent(name))) + } else { + let anonymous = + unsafe { bindings::Gecko_CounterStyle_GetAnonymous(gecko_value).as_ref() }.unwrap(); + let symbols = &anonymous.mSymbols; + if anonymous.mSingleString { + debug_assert_eq!(symbols.len(), 1); + Either::Second(symbols[0].to_string()) + } else { + let symbol_type = anonymous.mSymbolsType; + let symbols = symbols + .iter() + .map(|gecko_symbol| Symbol::String(gecko_symbol.to_string().into())) + .collect(); + Either::First(CounterStyle::Symbols(symbol_type, Symbols(symbols))) + } + } + } +} diff --git a/servo/components/style/gecko/wrapper.rs b/servo/components/style/gecko/wrapper.rs new file mode 100644 index 0000000000..61352ef9c0 --- /dev/null +++ b/servo/components/style/gecko/wrapper.rs @@ -0,0 +1,2211 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! Wrapper definitions on top of Gecko types in order to be used in the style +//! system. +//! +//! This really follows the Servo pattern in +//! `components/script/layout_wrapper.rs`. +//! +//! This theoretically should live in its own crate, but now it lives in the +//! style system it's kind of pointless in the Stylo case, and only Servo forces +//! the separation between the style system implementation and everything else. + +use crate::applicable_declarations::ApplicableDeclarationBlock; +use crate::bloom::each_relevant_element_hash; +use crate::context::{PostAnimationTasks, QuirksMode, SharedStyleContext, UpdateAnimationsTasks}; +use crate::data::ElementData; +use crate::dom::{LayoutIterator, NodeInfo, OpaqueNode, TDocument, TElement, TNode, TShadowRoot}; +use crate::gecko::selector_parser::{CustomState, NonTSPseudoClass, PseudoElement, SelectorImpl}; +use crate::gecko::snapshot_helpers; +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::bindings::Gecko_ElementHasAnimations; +use crate::gecko_bindings::bindings::Gecko_ElementHasCSSAnimations; +use crate::gecko_bindings::bindings::Gecko_ElementHasCSSTransitions; +use crate::gecko_bindings::bindings::Gecko_ElementState; +use crate::gecko_bindings::bindings::Gecko_GetActiveLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetAnimationEffectCount; +use crate::gecko_bindings::bindings::Gecko_GetAnimationRule; +use crate::gecko_bindings::bindings::Gecko_GetExtraContentStyleDeclarations; +use crate::gecko_bindings::bindings::Gecko_GetHTMLPresentationAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetStyleAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetUnvisitedLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_GetVisitedLinkAttrDeclarationBlock; +use crate::gecko_bindings::bindings::Gecko_IsSignificantChild; +use crate::gecko_bindings::bindings::Gecko_MatchLang; +use crate::gecko_bindings::bindings::Gecko_UnsetDirtyStyleAttr; +use crate::gecko_bindings::bindings::Gecko_UpdateAnimations; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::nsChangeHint; +use crate::gecko_bindings::structs::EffectCompositor_CascadeLevel as CascadeLevel; +use crate::gecko_bindings::structs::ELEMENT_HANDLED_SNAPSHOT; +use crate::gecko_bindings::structs::ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO; +use crate::gecko_bindings::structs::ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO; +use crate::gecko_bindings::structs::ELEMENT_HAS_SNAPSHOT; +use crate::gecko_bindings::structs::NODE_DESCENDANTS_NEED_FRAMES; +use crate::gecko_bindings::structs::NODE_NEEDS_FRAME; +use crate::gecko_bindings::structs::{nsAtom, nsIContent, nsINode_BooleanFlag}; +use crate::gecko_bindings::structs::{nsINode as RawGeckoNode, Element as RawGeckoElement}; +use crate::global_style_data::GLOBAL_STYLE_DATA; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::media_queries::Device; +use crate::properties::{ + animated_properties::{AnimationValue, AnimationValueMap}, + ComputedValues, Importance, OwnedPropertyDeclarationId, PropertyDeclaration, + PropertyDeclarationBlock, PropertyDeclarationId, PropertyDeclarationIdSet, +}; +use crate::rule_tree::CascadeLevel as ServoCascadeLevel; +use crate::selector_parser::{AttrValue, Lang}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::string_cache::{Atom, Namespace, WeakAtom, WeakNamespace}; +use crate::stylist::CascadeData; +use crate::values::computed::Display; +use crate::values::{AtomIdent, AtomString}; +use crate::CaseSensitivityExt; +use crate::LocalName; +use app_units::Au; +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +use dom::{DocumentState, ElementState}; +use euclid::default::Size2D; +use fxhash::FxHashMap; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::bloom::{BloomFilter, BLOOM_HASH_MASK}; +use selectors::matching::VisitedHandlingMode; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::sink::Push; +use selectors::{Element, OpaqueElement}; +use servo_arc::{Arc, ArcBorrow}; +use std::cell::Cell; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::mem; +use std::ptr; +use std::sync::atomic::{AtomicU32, Ordering}; + +#[inline] +fn elements_with_id<'a, 'le>( + array: *const structs::nsTArray<*mut RawGeckoElement>, +) -> &'a [GeckoElement<'le>] { + unsafe { + if array.is_null() { + return &[]; + } + + let elements: &[*mut RawGeckoElement] = &**array; + + // NOTE(emilio): We rely on the in-memory representation of + // GeckoElement<'ld> and *mut RawGeckoElement being the same. + #[allow(dead_code)] + unsafe fn static_assert() { + mem::transmute::<*mut RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *mut _); + } + + mem::transmute(elements) + } +} + +/// A simple wrapper over `Document`. +#[derive(Clone, Copy)] +pub struct GeckoDocument<'ld>(pub &'ld structs::Document); + +impl<'ld> TDocument for GeckoDocument<'ld> { + type ConcreteNode = GeckoNode<'ld>; + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + GeckoNode(&self.0._base) + } + + #[inline] + fn is_html_document(&self) -> bool { + self.0.mType == structs::Document_Type::eHTML + } + + #[inline] + fn quirks_mode(&self) -> QuirksMode { + self.0.mCompatMode.into() + } + + #[inline] + fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'ld>], ()> + where + Self: 'a, + { + Ok(elements_with_id(unsafe { + bindings::Gecko_Document_GetElementsWithId(self.0, id.as_ptr()) + })) + } + + fn shared_lock(&self) -> &SharedRwLock { + &GLOBAL_STYLE_DATA.shared_lock + } +} + +/// A simple wrapper over `ShadowRoot`. +#[derive(Clone, Copy)] +pub struct GeckoShadowRoot<'lr>(pub &'lr structs::ShadowRoot); + +impl<'ln> fmt::Debug for GeckoShadowRoot<'ln> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO(emilio): Maybe print the host or something? + write!(f, "<shadow-root> ({:#x})", self.as_node().opaque().0) + } +} + +impl<'lr> PartialEq for GeckoShadowRoot<'lr> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'lr> TShadowRoot for GeckoShadowRoot<'lr> { + type ConcreteNode = GeckoNode<'lr>; + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + GeckoNode(&self.0._base._base._base._base) + } + + #[inline] + fn host(&self) -> GeckoElement<'lr> { + GeckoElement(unsafe { &*self.0._base.mHost.mRawPtr }) + } + + #[inline] + fn style_data<'a>(&self) -> Option<&'a CascadeData> + where + Self: 'a, + { + let author_styles = unsafe { self.0.mServoStyles.mPtr.as_ref()? }; + Some(&author_styles.data) + } + + #[inline] + fn elements_with_id<'a>(&self, id: &AtomIdent) -> Result<&'a [GeckoElement<'lr>], ()> + where + Self: 'a, + { + Ok(elements_with_id(unsafe { + bindings::Gecko_ShadowRoot_GetElementsWithId(self.0, id.as_ptr()) + })) + } + + #[inline] + fn parts<'a>(&self) -> &[<Self::ConcreteNode as TNode>::ConcreteElement] + where + Self: 'a, + { + let slice: &[*const RawGeckoElement] = &*self.0.mParts; + + #[allow(dead_code)] + unsafe fn static_assert() { + mem::transmute::<*const RawGeckoElement, GeckoElement<'static>>(0xbadc0de as *const _); + } + + unsafe { mem::transmute(slice) } + } +} + +/// A simple wrapper over a non-null Gecko node (`nsINode`) pointer. +/// +/// Important: We don't currently refcount the DOM, because the wrapper lifetime +/// magic guarantees that our LayoutFoo references won't outlive the root, and +/// we don't mutate any of the references on the Gecko side during restyle. +/// +/// We could implement refcounting if need be (at a potentially non-trivial +/// performance cost) by implementing Drop and making LayoutFoo non-Copy. +#[derive(Clone, Copy)] +pub struct GeckoNode<'ln>(pub &'ln RawGeckoNode); + +impl<'ln> PartialEq for GeckoNode<'ln> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'ln> fmt::Debug for GeckoNode<'ln> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + if let Some(el) = self.as_element() { + return el.fmt(f); + } + + if self.is_text_node() { + return write!(f, "<text node> ({:#x})", self.opaque().0); + } + + if self.is_document() { + return write!(f, "<document> ({:#x})", self.opaque().0); + } + + if let Some(sr) = self.as_shadow_root() { + return sr.fmt(f); + } + + write!(f, "<non-text node> ({:#x})", self.opaque().0) + } +} + +impl<'ln> GeckoNode<'ln> { + #[inline] + fn is_document(&self) -> bool { + // This is a DOM constant that isn't going to change. + const DOCUMENT_NODE: u16 = 9; + self.node_info().mInner.mNodeType == DOCUMENT_NODE + } + + #[inline] + fn is_shadow_root(&self) -> bool { + self.is_in_shadow_tree() && self.parent_node().is_none() + } + + #[inline] + fn from_content(content: &'ln nsIContent) -> Self { + GeckoNode(&content._base) + } + + #[inline] + fn set_flags(&self, flags: u32) { + self.flags_atomic().fetch_or(flags, Ordering::Relaxed); + } + + fn flags_atomic_for(flags: &Cell<u32>) -> &AtomicU32 { + const_assert!(std::mem::size_of::<Cell<u32>>() == std::mem::size_of::<AtomicU32>()); + const_assert!(std::mem::align_of::<Cell<u32>>() == std::mem::align_of::<AtomicU32>()); + + // Rust doesn't provide standalone atomic functions like GCC/clang do + // (via the atomic intrinsics) or via std::atomic_ref, but it guarantees + // that the memory representation of u32 and AtomicU32 matches: + // https://doc.rust-lang.org/std/sync/atomic/struct.AtomicU32.html + unsafe { std::mem::transmute::<&Cell<u32>, &AtomicU32>(flags) } + } + + #[inline] + fn flags_atomic(&self) -> &AtomicU32 { + Self::flags_atomic_for(&self.0._base._base_1.mFlags) + } + + #[inline] + fn flags(&self) -> u32 { + self.flags_atomic().load(Ordering::Relaxed) + } + + #[inline] + fn selector_flags_atomic(&self) -> &AtomicU32 { + Self::flags_atomic_for(&self.0.mSelectorFlags) + } + + #[inline] + fn selector_flags(&self) -> u32 { + self.selector_flags_atomic().load(Ordering::Relaxed) + } + + #[inline] + fn set_selector_flags(&self, flags: u32) { + self.selector_flags_atomic() + .fetch_or(flags, Ordering::Relaxed); + } + + #[inline] + fn node_info(&self) -> &structs::NodeInfo { + debug_assert!(!self.0.mNodeInfo.mRawPtr.is_null()); + unsafe { &*self.0.mNodeInfo.mRawPtr } + } + + // These live in different locations depending on processor architecture. + #[cfg(target_pointer_width = "64")] + #[inline] + fn bool_flags(&self) -> u32 { + (self.0)._base._base_1.mBoolFlags + } + + #[cfg(target_pointer_width = "32")] + #[inline] + fn bool_flags(&self) -> u32 { + (self.0).mBoolFlags + } + + #[inline] + fn get_bool_flag(&self, flag: nsINode_BooleanFlag) -> bool { + self.bool_flags() & (1u32 << flag as u32) != 0 + } + + /// This logic is duplicate in Gecko's nsINode::IsInShadowTree(). + #[inline] + fn is_in_shadow_tree(&self) -> bool { + use crate::gecko_bindings::structs::NODE_IS_IN_SHADOW_TREE; + self.flags() & NODE_IS_IN_SHADOW_TREE != 0 + } + + /// Returns true if we know for sure that `flattened_tree_parent` and `parent_node` return the + /// same thing. + /// + /// TODO(emilio): Measure and consider not doing this fast-path, it's only a function call and + /// from profiles it seems that keeping this fast path makes the compiler not inline + /// `flattened_tree_parent` as a whole, so we're not gaining much either. + #[inline] + fn flattened_tree_parent_is_parent(&self) -> bool { + use crate::gecko_bindings::structs::*; + let flags = self.flags(); + + let parent = match self.parent_node() { + Some(p) => p, + None => return true, + }; + + if parent.is_shadow_root() { + return false; + } + + if let Some(parent) = parent.as_element() { + if flags & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0 && parent.is_root() { + return false; + } + if parent.shadow_root().is_some() || parent.is_html_slot_element() { + return false; + } + } + + true + } + + #[inline] + fn flattened_tree_parent(&self) -> Option<Self> { + if self.flattened_tree_parent_is_parent() { + debug_assert_eq!( + unsafe { + bindings::Gecko_GetFlattenedTreeParentNode(self.0) + .as_ref() + .map(GeckoNode) + }, + self.parent_node(), + "Fast path stopped holding!" + ); + return self.parent_node(); + } + + // NOTE(emilio): If this call is too expensive, we could manually inline more aggressively. + unsafe { + bindings::Gecko_GetFlattenedTreeParentNode(self.0) + .as_ref() + .map(GeckoNode) + } + } + + #[inline] + fn contains_non_whitespace_content(&self) -> bool { + unsafe { Gecko_IsSignificantChild(self.0, false) } + } + + /// Returns the previous sibling of this node that is an element. + #[inline] + pub fn prev_sibling_element(&self) -> Option<GeckoElement> { + let mut prev = self.prev_sibling(); + while let Some(p) = prev { + if let Some(e) = p.as_element() { + return Some(e); + } + prev = p.prev_sibling(); + } + None + } + + /// Returns the next sibling of this node that is an element. + #[inline] + pub fn next_sibling_element(&self) -> Option<GeckoElement> { + let mut next = self.next_sibling(); + while let Some(n) = next { + if let Some(e) = n.as_element() { + return Some(e); + } + next = n.next_sibling(); + } + None + } + + /// Returns last child sibling of this node that is an element. + #[inline] + pub fn last_child_element(&self) -> Option<GeckoElement<'ln>> { + let last = match self.last_child() { + Some(n) => n, + None => return None, + }; + if let Some(e) = last.as_element() { + return Some(e); + } + None + } +} + +impl<'ln> NodeInfo for GeckoNode<'ln> { + #[inline] + fn is_element(&self) -> bool { + self.get_bool_flag(nsINode_BooleanFlag::NodeIsElement) + } + + fn is_text_node(&self) -> bool { + // This is a DOM constant that isn't going to change. + const TEXT_NODE: u16 = 3; + self.node_info().mInner.mNodeType == TEXT_NODE + } +} + +impl<'ln> TNode for GeckoNode<'ln> { + type ConcreteDocument = GeckoDocument<'ln>; + type ConcreteShadowRoot = GeckoShadowRoot<'ln>; + type ConcreteElement = GeckoElement<'ln>; + + #[inline] + fn parent_node(&self) -> Option<Self> { + unsafe { self.0.mParent.as_ref().map(GeckoNode) } + } + + #[inline] + fn first_child(&self) -> Option<Self> { + unsafe { + self.0 + .mFirstChild + .raw() + .as_ref() + .map(GeckoNode::from_content) + } + } + + #[inline] + fn last_child(&self) -> Option<Self> { + unsafe { bindings::Gecko_GetLastChild(self.0).as_ref().map(GeckoNode) } + } + + #[inline] + fn prev_sibling(&self) -> Option<Self> { + unsafe { + let prev_or_last = GeckoNode::from_content(self.0.mPreviousOrLastSibling.as_ref()?); + if prev_or_last.0.mNextSibling.raw().is_null() { + return None; + } + Some(prev_or_last) + } + } + + #[inline] + fn next_sibling(&self) -> Option<Self> { + unsafe { + self.0 + .mNextSibling + .raw() + .as_ref() + .map(GeckoNode::from_content) + } + } + + #[inline] + fn owner_doc(&self) -> Self::ConcreteDocument { + debug_assert!(!self.node_info().mDocument.is_null()); + GeckoDocument(unsafe { &*self.node_info().mDocument }) + } + + #[inline] + fn is_in_document(&self) -> bool { + self.get_bool_flag(nsINode_BooleanFlag::IsInDocument) + } + + fn traversal_parent(&self) -> Option<GeckoElement<'ln>> { + self.flattened_tree_parent().and_then(|n| n.as_element()) + } + + #[inline] + fn opaque(&self) -> OpaqueNode { + let ptr: usize = self.0 as *const _ as usize; + OpaqueNode(ptr) + } + + fn debug_id(self) -> usize { + unimplemented!() + } + + #[inline] + fn as_element(&self) -> Option<GeckoElement<'ln>> { + if !self.is_element() { + return None; + } + + Some(GeckoElement(unsafe { + &*(self.0 as *const _ as *const RawGeckoElement) + })) + } + + #[inline] + fn as_document(&self) -> Option<Self::ConcreteDocument> { + if !self.is_document() { + return None; + } + + debug_assert_eq!(self.owner_doc().as_node(), *self, "How?"); + Some(self.owner_doc()) + } + + #[inline] + fn as_shadow_root(&self) -> Option<Self::ConcreteShadowRoot> { + if !self.is_shadow_root() { + return None; + } + + Some(GeckoShadowRoot(unsafe { + &*(self.0 as *const _ as *const structs::ShadowRoot) + })) + } +} + +/// A wrapper on top of two kind of iterators, depending on the parent being +/// iterated. +/// +/// We generally iterate children by traversing the light-tree siblings of the +/// first child like Servo does. +/// +/// However, for nodes with anonymous children, we use a custom (heavier-weight) +/// Gecko-implemented iterator. +/// +/// FIXME(emilio): If we take into account shadow DOM, we're going to need the +/// flat tree pretty much always. We can try to optimize the case where there's +/// no shadow root sibling, probably. +pub enum GeckoChildrenIterator<'a> { + /// A simple iterator that tracks the current node being iterated and + /// replaces it with the next sibling when requested. + Current(Option<GeckoNode<'a>>), + /// A Gecko-implemented iterator we need to drop appropriately. + GeckoIterator(structs::StyleChildrenIterator), +} + +impl<'a> Drop for GeckoChildrenIterator<'a> { + fn drop(&mut self) { + if let GeckoChildrenIterator::GeckoIterator(ref mut it) = *self { + unsafe { + bindings::Gecko_DestroyStyleChildrenIterator(it); + } + } + } +} + +impl<'a> Iterator for GeckoChildrenIterator<'a> { + type Item = GeckoNode<'a>; + fn next(&mut self) -> Option<GeckoNode<'a>> { + match *self { + GeckoChildrenIterator::Current(curr) => { + let next = curr.and_then(|node| node.next_sibling()); + *self = GeckoChildrenIterator::Current(next); + curr + }, + GeckoChildrenIterator::GeckoIterator(ref mut it) => unsafe { + // We do this unsafe lengthening of the lifetime here because + // structs::StyleChildrenIterator is actually StyleChildrenIterator<'a>, + // however we can't express this easily with bindgen, and it would + // introduce functions with two input lifetimes into bindgen, + // which would be out of scope for elision. + bindings::Gecko_GetNextStyleChild(&mut *(it as *mut _)) + .as_ref() + .map(GeckoNode) + }, + } + } +} + +/// A simple wrapper over a non-null Gecko `Element` pointer. +#[derive(Clone, Copy)] +pub struct GeckoElement<'le>(pub &'le RawGeckoElement); + +impl<'le> fmt::Debug for GeckoElement<'le> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + + write!(f, "<{}", self.local_name())?; + + let mut attrs = nsCString::new(); + unsafe { + bindings::Gecko_Element_DebugListAttributes(self.0, &mut attrs); + } + write!(f, "{}", attrs)?; + write!(f, "> ({:#x})", self.as_node().opaque().0) + } +} + +impl<'le> GeckoElement<'le> { + /// Gets the raw `ElementData` refcell for the element. + #[inline(always)] + pub fn get_data(&self) -> Option<&AtomicRefCell<ElementData>> { + unsafe { self.0.mServoData.get().as_ref() } + } + + /// Returns whether any animation applies to this element. + #[inline] + pub fn has_any_animation(&self) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasAnimations(self.0) } + } + + #[inline(always)] + fn attrs(&self) -> &[structs::AttrArray_InternalAttr] { + unsafe { + match self.0.mAttrs.mImpl.mPtr.as_ref() { + Some(attrs) => attrs.mBuffer.as_slice(attrs.mAttrCount as usize), + None => return &[], + } + } + } + + #[inline(always)] + fn get_part_attr(&self) -> Option<&structs::nsAttrValue> { + if !self.has_part_attr() { + return None; + } + snapshot_helpers::find_attr(self.attrs(), &atom!("part")) + } + + #[inline(always)] + fn get_class_attr(&self) -> Option<&structs::nsAttrValue> { + if !self.may_have_class() { + return None; + } + + if self.is_svg_element() { + let svg_class = unsafe { bindings::Gecko_GetSVGAnimatedClass(self.0).as_ref() }; + if let Some(c) = svg_class { + return Some(c); + } + } + + snapshot_helpers::find_attr(self.attrs(), &atom!("class")) + } + + #[inline] + fn may_have_anonymous_children(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveAnonymousChildren) + } + + #[inline] + fn flags(&self) -> u32 { + self.as_node().flags() + } + + #[inline] + fn set_flags(&self, flags: u32) { + self.as_node().set_flags(flags); + } + + #[inline] + unsafe fn unset_flags(&self, flags: u32) { + self.as_node() + .flags_atomic() + .fetch_and(!flags, Ordering::Relaxed); + } + + /// Returns true if this element has descendants for lazy frame construction. + #[inline] + pub fn descendants_need_frames(&self) -> bool { + self.flags() & NODE_DESCENDANTS_NEED_FRAMES != 0 + } + + /// Returns true if this element needs lazy frame construction. + #[inline] + pub fn needs_frame(&self) -> bool { + self.flags() & NODE_NEEDS_FRAME != 0 + } + + /// Returns a reference to the DOM slots for this Element, if they exist. + #[inline] + fn dom_slots(&self) -> Option<&structs::FragmentOrElement_nsDOMSlots> { + let slots = self.as_node().0.mSlots as *const structs::FragmentOrElement_nsDOMSlots; + unsafe { slots.as_ref() } + } + + /// Returns a reference to the extended DOM slots for this Element. + #[inline] + fn extended_slots(&self) -> Option<&structs::FragmentOrElement_nsExtendedDOMSlots> { + self.dom_slots().and_then(|s| unsafe { + // For the bit usage, see nsContentSlots::GetExtendedSlots. + let e_slots = s._base.mExtendedSlots & + !structs::nsIContent_nsContentSlots_sNonOwningExtendedSlotsFlag; + (e_slots as *const structs::FragmentOrElement_nsExtendedDOMSlots).as_ref() + }) + } + + #[inline] + fn namespace_id(&self) -> i32 { + self.as_node().node_info().mInner.mNamespaceID + } + + #[inline] + fn has_id(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasID) + } + + #[inline] + fn state_internal(&self) -> u64 { + if !self + .as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasLockedStyleStates) + { + return self.0.mState.bits; + } + unsafe { Gecko_ElementState(self.0) } + } + + #[inline] + fn document_state(&self) -> DocumentState { + DocumentState::from_bits_retain(self.as_node().owner_doc().0.mState.bits) + } + + #[inline] + fn may_have_class(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveClass) + } + + #[inline] + fn has_properties(&self) -> bool { + use crate::gecko_bindings::structs::NODE_HAS_PROPERTIES; + + self.flags() & NODE_HAS_PROPERTIES != 0 + } + + #[inline] + fn before_or_after_pseudo(&self, is_before: bool) -> Option<Self> { + if !self.has_properties() { + return None; + } + + unsafe { + bindings::Gecko_GetBeforeOrAfterPseudo(self.0, is_before) + .as_ref() + .map(GeckoElement) + } + } + + #[inline] + fn may_have_style_attribute(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementMayHaveStyle) + } + + /// Only safe to call on the main thread, with exclusive access to the + /// element and its ancestors. + /// + /// This function is also called after display property changed for SMIL + /// animation. + /// + /// Also this function schedules style flush. + pub unsafe fn note_explicit_hints(&self, restyle_hint: RestyleHint, change_hint: nsChangeHint) { + use crate::gecko::restyle_damage::GeckoRestyleDamage; + + let damage = GeckoRestyleDamage::new(change_hint); + debug!( + "note_explicit_hints: {:?}, restyle_hint={:?}, change_hint={:?}", + self, restyle_hint, change_hint + ); + + debug_assert!( + !(restyle_hint.has_animation_hint() && restyle_hint.has_non_animation_hint()), + "Animation restyle hints should not appear with non-animation restyle hints" + ); + + let mut data = match self.mutate_data() { + Some(d) => d, + None => { + debug!("(Element not styled, discarding hints)"); + return; + }, + }; + + debug_assert!(data.has_styles(), "how?"); + + // Propagate the bit up the chain. + if restyle_hint.has_animation_hint() { + bindings::Gecko_NoteAnimationOnlyDirtyElement(self.0); + } else { + bindings::Gecko_NoteDirtyElement(self.0); + } + + data.hint.insert(restyle_hint); + data.damage |= damage; + } + + /// This logic is duplicated in Gecko's nsIContent::IsRootOfNativeAnonymousSubtree. + #[inline] + fn is_root_of_native_anonymous_subtree(&self) -> bool { + use crate::gecko_bindings::structs::NODE_IS_NATIVE_ANONYMOUS_ROOT; + return self.flags() & NODE_IS_NATIVE_ANONYMOUS_ROOT != 0; + } + + fn css_transitions_info(&self) -> FxHashMap<OwnedPropertyDeclarationId, Arc<AnimationValue>> { + use crate::gecko_bindings::bindings::Gecko_ElementTransitions_EndValueAt; + use crate::gecko_bindings::bindings::Gecko_ElementTransitions_Length; + + let collection_length = unsafe { Gecko_ElementTransitions_Length(self.0) } as usize; + let mut map = FxHashMap::with_capacity_and_hasher(collection_length, Default::default()); + + for i in 0..collection_length { + let end_value = + unsafe { Arc::from_raw_addrefed(Gecko_ElementTransitions_EndValueAt(self.0, i)) }; + let property = end_value.id(); + debug_assert!(!property.is_logical()); + map.insert(property.to_owned(), end_value); + } + map + } + + fn needs_transitions_update_per_property( + &self, + property_declaration_id: PropertyDeclarationId, + combined_duration_seconds: f32, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + existing_transitions: &FxHashMap<OwnedPropertyDeclarationId, Arc<AnimationValue>>, + ) -> bool { + use crate::values::animated::{Animate, Procedure}; + debug_assert!(!property_declaration_id.is_logical()); + + // If there is an existing transition, update only if the end value + // differs. + // + // If the end value has not changed, we should leave the currently + // running transition as-is since we don't want to interrupt its timing + // function. + if let Some(ref existing) = existing_transitions.get(&property_declaration_id.to_owned()) { + let after_value = + AnimationValue::from_computed_values(property_declaration_id, after_change_style) + .unwrap(); + + return ***existing != after_value; + } + + let from = + AnimationValue::from_computed_values(property_declaration_id, before_change_style); + let to = AnimationValue::from_computed_values(property_declaration_id, after_change_style); + + debug_assert_eq!(to.is_some(), from.is_some()); + + combined_duration_seconds > 0.0f32 && + from != to && + from.unwrap() + .animate( + to.as_ref().unwrap(), + Procedure::Interpolate { progress: 0.5 }, + ) + .is_ok() + } + + /// Get slow selector flags required for nth-of invalidation. + pub fn slow_selector_flags(&self) -> ElementSelectorFlags { + slow_selector_flags_from_node_selector_flags(self.as_node().selector_flags()) + } +} + +/// Convert slow selector flags from the raw `NodeSelectorFlags`. +pub fn slow_selector_flags_from_node_selector_flags(flags: u32) -> ElementSelectorFlags { + use crate::gecko_bindings::structs::NodeSelectorFlags; + let mut result = ElementSelectorFlags::empty(); + if flags & NodeSelectorFlags::HasSlowSelector.0 != 0 { + result.insert(ElementSelectorFlags::HAS_SLOW_SELECTOR); + } + if flags & NodeSelectorFlags::HasSlowSelectorLaterSiblings.0 != 0 { + result.insert(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS); + } + result +} + +/// Converts flags from the layout used by rust-selectors to the layout used +/// by Gecko. We could align these and then do this without conditionals, but +/// it's probably not worth the trouble. +fn selector_flags_to_node_flags(flags: ElementSelectorFlags) -> u32 { + use crate::gecko_bindings::structs::NodeSelectorFlags; + let mut gecko_flags = 0u32; + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR) { + gecko_flags |= NodeSelectorFlags::HasSlowSelector.0; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_LATER_SIBLINGS) { + gecko_flags |= NodeSelectorFlags::HasSlowSelectorLaterSiblings.0; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH) { + gecko_flags |= NodeSelectorFlags::HasSlowSelectorNth.0; + } + if flags.contains(ElementSelectorFlags::HAS_SLOW_SELECTOR_NTH_OF) { + gecko_flags |= NodeSelectorFlags::HasSlowSelectorNthOf.0; + } + if flags.contains(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR) { + gecko_flags |= NodeSelectorFlags::HasEdgeChildSelector.0; + } + if flags.contains(ElementSelectorFlags::HAS_EMPTY_SELECTOR) { + gecko_flags |= NodeSelectorFlags::HasEmptySelector.0; + } + if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorAnchor.0; + } + if flags.contains(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorAnchorNonSubject.0; + } + if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorSearchDirectionAncestor.0; + } + if flags.contains(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING) { + gecko_flags |= NodeSelectorFlags::RelativeSelectorSearchDirectionSibling.0; + } + + gecko_flags +} + +fn get_animation_rule( + element: &GeckoElement, + cascade_level: CascadeLevel, +) -> Option<Arc<Locked<PropertyDeclarationBlock>>> { + // There's a very rough correlation between the number of effects + // (animations) on an element and the number of properties it is likely to + // animate, so we use that as an initial guess for the size of the + // AnimationValueMap in order to reduce the number of re-allocations needed. + let effect_count = unsafe { Gecko_GetAnimationEffectCount(element.0) }; + // Also, we should try to reuse the PDB, to avoid creating extra rule nodes. + let mut animation_values = AnimationValueMap::with_capacity_and_hasher( + effect_count.min(crate::properties::property_counts::ANIMATABLE), + Default::default(), + ); + if unsafe { Gecko_GetAnimationRule(element.0, cascade_level, &mut animation_values) } { + let shared_lock = &GLOBAL_STYLE_DATA.shared_lock; + Some(Arc::new(shared_lock.wrap( + PropertyDeclarationBlock::from_animation_value_map(&animation_values), + ))) + } else { + None + } +} + +/// Turns a gecko namespace id into an atom. Might panic if you pass any random thing that isn't a +/// namespace id. +#[inline(always)] +pub unsafe fn namespace_id_to_atom(id: i32) -> *mut nsAtom { + unsafe { + let namespace_manager = structs::nsNameSpaceManager_sInstance.mRawPtr; + (*namespace_manager).mURIArray[id as usize].mRawPtr + } +} + +impl<'le> TElement for GeckoElement<'le> { + type ConcreteNode = GeckoNode<'le>; + type TraversalChildrenIterator = GeckoChildrenIterator<'le>; + + fn inheritance_parent(&self) -> Option<Self> { + if self.is_pseudo_element() { + return self.pseudo_element_originating_element(); + } + + self.as_node() + .flattened_tree_parent() + .and_then(|n| n.as_element()) + } + + fn traversal_children(&self) -> LayoutIterator<GeckoChildrenIterator<'le>> { + // This condition is similar to the check that + // StyleChildrenIterator::IsNeeded does, except that it might return + // true if we used to (but no longer) have anonymous content from + // ::before/::after, or nsIAnonymousContentCreators. + if self.is_html_slot_element() || + self.shadow_root().is_some() || + self.may_have_anonymous_children() + { + unsafe { + let mut iter: structs::StyleChildrenIterator = ::std::mem::zeroed(); + bindings::Gecko_ConstructStyleChildrenIterator(self.0, &mut iter); + return LayoutIterator(GeckoChildrenIterator::GeckoIterator(iter)); + } + } + + LayoutIterator(GeckoChildrenIterator::Current(self.as_node().first_child())) + } + + fn before_pseudo_element(&self) -> Option<Self> { + self.before_or_after_pseudo(/* is_before = */ true) + } + + fn after_pseudo_element(&self) -> Option<Self> { + self.before_or_after_pseudo(/* is_before = */ false) + } + + fn marker_pseudo_element(&self) -> Option<Self> { + if !self.has_properties() { + return None; + } + + unsafe { + bindings::Gecko_GetMarkerPseudo(self.0) + .as_ref() + .map(GeckoElement) + } + } + + #[inline] + fn is_html_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_XHTML as i32 + } + + #[inline] + fn is_mathml_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_MathML as i32 + } + + #[inline] + fn is_svg_element(&self) -> bool { + self.namespace_id() == structs::kNameSpaceID_SVG as i32 + } + + #[inline] + fn is_xul_element(&self) -> bool { + self.namespace_id() == structs::root::kNameSpaceID_XUL as i32 + } + + #[inline] + fn local_name(&self) -> &WeakAtom { + unsafe { WeakAtom::new(self.as_node().node_info().mInner.mName) } + } + + #[inline] + fn namespace(&self) -> &WeakNamespace { + unsafe { WeakNamespace::new(namespace_id_to_atom(self.namespace_id())) } + } + + #[inline] + fn query_container_size(&self, display: &Display) -> Size2D<Option<Au>> { + // If an element gets 'display: contents' and its nsIFrame has not been removed yet, + // Gecko_GetQueryContainerSize will not notice that it can't have size containment. + // Other cases like 'display: inline' will be handled once the new nsIFrame is created. + if display.is_contents() { + return Size2D::new(None, None); + } + + let mut width = -1; + let mut height = -1; + unsafe { + bindings::Gecko_GetQueryContainerSize(self.0, &mut width, &mut height); + } + Size2D::new( + if width >= 0 { Some(Au(width)) } else { None }, + if height >= 0 { Some(Au(height)) } else { None }, + ) + } + + /// Return the list of slotted nodes of this node. + #[inline] + fn slotted_nodes(&self) -> &[Self::ConcreteNode] { + if !self.is_html_slot_element() || !self.as_node().is_in_shadow_tree() { + return &[]; + } + + let slot: &structs::HTMLSlotElement = unsafe { mem::transmute(self.0) }; + + if cfg!(debug_assertions) { + let base: &RawGeckoElement = &slot._base._base._base; + assert_eq!(base as *const _, self.0 as *const _, "Bad cast"); + } + + // FIXME(emilio): Workaround a bindgen bug on Android that causes + // mAssignedNodes to be at the wrong offset. See bug 1466406. + // + // Bug 1466580 tracks running the Android layout tests on automation. + // + // The actual bindgen bug still needs reduction. + let assigned_nodes: &[structs::RefPtr<structs::nsINode>] = if !cfg!(target_os = "android") { + debug_assert_eq!( + unsafe { bindings::Gecko_GetAssignedNodes(self.0) }, + &slot.mAssignedNodes as *const _, + ); + + &*slot.mAssignedNodes + } else { + unsafe { &**bindings::Gecko_GetAssignedNodes(self.0) } + }; + + debug_assert_eq!( + mem::size_of::<structs::RefPtr<structs::nsINode>>(), + mem::size_of::<Self::ConcreteNode>(), + "Bad cast!" + ); + + unsafe { mem::transmute(assigned_nodes) } + } + + #[inline] + fn shadow_root(&self) -> Option<GeckoShadowRoot<'le>> { + let slots = self.extended_slots()?; + unsafe { slots.mShadowRoot.mRawPtr.as_ref().map(GeckoShadowRoot) } + } + + #[inline] + fn containing_shadow(&self) -> Option<GeckoShadowRoot<'le>> { + let slots = self.extended_slots()?; + unsafe { + slots + ._base + .mContainingShadow + .mRawPtr + .as_ref() + .map(GeckoShadowRoot) + } + } + + fn each_anonymous_content_child<F>(&self, mut f: F) + where + F: FnMut(Self), + { + if !self.may_have_anonymous_children() { + return; + } + + let array: *mut structs::nsTArray<*mut nsIContent> = + unsafe { bindings::Gecko_GetAnonymousContentForElement(self.0) }; + + if array.is_null() { + return; + } + + for content in unsafe { &**array } { + let node = GeckoNode::from_content(unsafe { &**content }); + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + f(element); + } + + unsafe { bindings::Gecko_DestroyAnonymousContentList(array) }; + } + + #[inline] + fn as_node(&self) -> Self::ConcreteNode { + unsafe { GeckoNode(&*(self.0 as *const _ as *const RawGeckoNode)) } + } + + fn owner_doc_matches_for_testing(&self, device: &Device) -> bool { + self.as_node().owner_doc().0 as *const structs::Document == device.document() as *const _ + } + + fn style_attribute(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> { + if !self.may_have_style_attribute() { + return None; + } + + unsafe { + let declarations = Gecko_GetStyleAttrDeclarationBlock(self.0).as_ref()?; + Some(ArcBorrow::from_ref(declarations)) + } + } + + fn unset_dirty_style_attribute(&self) { + if !self.may_have_style_attribute() { + return; + } + + unsafe { Gecko_UnsetDirtyStyleAttr(self.0) }; + } + + fn smil_override(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> { + unsafe { + let slots = self.extended_slots()?; + + let declaration: &structs::DeclarationBlock = + slots.mSMILOverrideStyleDeclaration.mRawPtr.as_ref()?; + + let raw: &structs::StyleLockedDeclarationBlock = declaration.mRaw.mRawPtr.as_ref()?; + Some(ArcBorrow::from_ref(raw)) + } + } + + fn animation_rule( + &self, + _: &SharedStyleContext, + ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> { + get_animation_rule(self, CascadeLevel::Animations) + } + + fn transition_rule( + &self, + _: &SharedStyleContext, + ) -> Option<Arc<Locked<PropertyDeclarationBlock>>> { + get_animation_rule(self, CascadeLevel::Transitions) + } + + #[inline] + fn state(&self) -> ElementState { + ElementState::from_bits_retain(self.state_internal()) + } + + #[inline] + fn has_custom_state(&self, state: &CustomState) -> bool { + if !self.is_html_element() { + return false; + } + let check_state_ptr: *const nsAtom = state.0.as_ptr(); + self.extended_slots().map_or(false, |slot| { + (&slot.mCustomStates).iter().any(|setstate| { + let setstate_ptr: *const nsAtom = setstate.mRawPtr; + setstate_ptr == check_state_ptr + }) + }) + } + + #[inline] + fn has_part_attr(&self) -> bool { + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasPart) + } + + #[inline] + fn exports_any_part(&self) -> bool { + snapshot_helpers::find_attr(self.attrs(), &atom!("exportparts")).is_some() + } + + // FIXME(emilio): we should probably just return a reference to the Atom. + #[inline] + fn id(&self) -> Option<&WeakAtom> { + if !self.has_id() { + return None; + } + snapshot_helpers::get_id(self.attrs()) + } + + fn each_attr_name<F>(&self, mut callback: F) + where + F: FnMut(&AtomIdent), + { + for attr in self.attrs() { + unsafe { AtomIdent::with(attr.mName.name(), |a| callback(a)) } + } + } + + fn each_class<F>(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + let attr = match self.get_class_attr() { + Some(c) => c, + None => return, + }; + + snapshot_helpers::each_class_or_part(attr, callback) + } + + #[inline] + fn each_exported_part<F>(&self, name: &AtomIdent, callback: F) + where + F: FnMut(&AtomIdent), + { + snapshot_helpers::each_exported_part(self.attrs(), name, callback) + } + + fn each_part<F>(&self, callback: F) + where + F: FnMut(&AtomIdent), + { + let attr = match self.get_part_attr() { + Some(c) => c, + None => return, + }; + + snapshot_helpers::each_class_or_part(attr, callback) + } + + #[inline] + fn has_snapshot(&self) -> bool { + self.flags() & ELEMENT_HAS_SNAPSHOT != 0 + } + + #[inline] + fn handled_snapshot(&self) -> bool { + self.flags() & ELEMENT_HANDLED_SNAPSHOT != 0 + } + + unsafe fn set_handled_snapshot(&self) { + debug_assert!(self.has_data()); + self.set_flags(ELEMENT_HANDLED_SNAPSHOT) + } + + #[inline] + fn has_dirty_descendants(&self) -> bool { + self.flags() & ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO != 0 + } + + unsafe fn set_dirty_descendants(&self) { + debug_assert!(self.has_data()); + debug!("Setting dirty descendants: {:?}", self); + self.set_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn unset_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO) + } + + #[inline] + fn has_animation_only_dirty_descendants(&self) -> bool { + self.flags() & ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO != 0 + } + + unsafe fn set_animation_only_dirty_descendants(&self) { + self.set_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn unset_animation_only_dirty_descendants(&self) { + self.unset_flags(ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO) + } + + unsafe fn clear_descendant_bits(&self) { + self.unset_flags( + ELEMENT_HAS_DIRTY_DESCENDANTS_FOR_SERVO | + ELEMENT_HAS_ANIMATION_ONLY_DIRTY_DESCENDANTS_FOR_SERVO | + NODE_DESCENDANTS_NEED_FRAMES, + ) + } + + fn is_visited_link(&self) -> bool { + self.state().intersects(ElementState::VISITED) + } + + /// We want to match rules from the same tree in all cases, except for native anonymous content + /// that _isn't_ part directly of a UA widget (e.g., such generated by form controls, or + /// pseudo-elements). + #[inline] + fn matches_user_and_content_rules(&self) -> bool { + use crate::gecko_bindings::structs::{ + NODE_HAS_BEEN_IN_UA_WIDGET, NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE, + }; + let flags = self.flags(); + (flags & NODE_IS_IN_NATIVE_ANONYMOUS_SUBTREE) == 0 || + (flags & NODE_HAS_BEEN_IN_UA_WIDGET) != 0 + } + + #[inline] + fn implemented_pseudo_element(&self) -> Option<PseudoElement> { + if self.matches_user_and_content_rules() { + return None; + } + + if !self.has_properties() { + return None; + } + + PseudoElement::from_pseudo_type( + unsafe { bindings::Gecko_GetImplementedPseudo(self.0) }, + None, + ) + } + + #[inline] + fn store_children_to_process(&self, _: isize) { + // This is only used for bottom-up traversal, and is thus a no-op for Gecko. + } + + fn did_process_child(&self) -> isize { + panic!("Atomic child count not implemented in Gecko"); + } + + unsafe fn ensure_data(&self) -> AtomicRefMut<ElementData> { + if !self.has_data() { + debug!("Creating ElementData for {:?}", self); + let ptr = Box::into_raw(Box::new(AtomicRefCell::new(ElementData::default()))); + self.0.mServoData.set(ptr); + } + self.mutate_data().unwrap() + } + + unsafe fn clear_data(&self) { + let ptr = self.0.mServoData.get(); + self.unset_flags( + ELEMENT_HAS_SNAPSHOT | + ELEMENT_HANDLED_SNAPSHOT | + structs::Element_kAllServoDescendantBits | + NODE_NEEDS_FRAME, + ); + if !ptr.is_null() { + debug!("Dropping ElementData for {:?}", self); + let data = Box::from_raw(self.0.mServoData.get()); + self.0.mServoData.set(ptr::null_mut()); + + // Perform a mutable borrow of the data in debug builds. This + // serves as an assertion that there are no outstanding borrows + // when we destroy the data. + debug_assert!({ + let _ = data.borrow_mut(); + true + }); + } + } + + #[inline] + fn skip_item_display_fixup(&self) -> bool { + debug_assert!( + !self.is_pseudo_element(), + "Just don't call me if I'm a pseudo, you should know the answer already" + ); + self.is_root_of_native_anonymous_subtree() + } + + #[inline] + fn may_have_animations(&self) -> bool { + if let Some(pseudo) = self.implemented_pseudo_element() { + if pseudo.animations_stored_in_parent() { + // FIXME(emilio): When would the parent of a ::before / ::after + // pseudo-element be null? + return self.parent_element().map_or(false, |p| { + p.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations) + }); + } + } + self.as_node() + .get_bool_flag(nsINode_BooleanFlag::ElementHasAnimations) + } + + /// Process various tasks that are a result of animation-only restyle. + fn process_post_animation(&self, tasks: PostAnimationTasks) { + debug_assert!(!tasks.is_empty(), "Should be involved a task"); + + // If display style was changed from none to other, we need to resolve + // the descendants in the display:none subtree. Instead of resolving + // those styles in animation-only restyle, we defer it to a subsequent + // normal restyle. + if tasks.intersects(PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL) { + debug_assert!( + self.implemented_pseudo_element() + .map_or(true, |p| !p.is_before_or_after()), + "display property animation shouldn't run on pseudo elements \ + since it's only for SMIL" + ); + unsafe { + self.note_explicit_hints( + RestyleHint::restyle_subtree(), + nsChangeHint::nsChangeHint_Empty, + ); + } + } + } + + /// Update various animation-related state on a given (pseudo-)element as + /// results of normal restyle. + fn update_animations( + &self, + before_change_style: Option<Arc<ComputedValues>>, + tasks: UpdateAnimationsTasks, + ) { + // We have to update animations even if the element has no computed + // style since it means the element is in a display:none subtree, we + // should destroy all CSS animations in display:none subtree. + let computed_data = self.borrow_data(); + let computed_values = computed_data.as_ref().map(|d| d.styles.primary()); + let before_change_values = before_change_style + .as_ref() + .map_or(ptr::null(), |x| x.as_gecko_computed_style()); + let computed_values_opt = computed_values + .as_ref() + .map_or(ptr::null(), |x| x.as_gecko_computed_style()); + unsafe { + Gecko_UpdateAnimations( + self.0, + before_change_values, + computed_values_opt, + tasks.bits(), + ); + } + } + + #[inline] + fn has_animations(&self, _: &SharedStyleContext) -> bool { + self.has_any_animation() + } + + fn has_css_animations(&self, _: &SharedStyleContext, _: Option<PseudoElement>) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasCSSAnimations(self.0) } + } + + fn has_css_transitions(&self, _: &SharedStyleContext, _: Option<PseudoElement>) -> bool { + self.may_have_animations() && unsafe { Gecko_ElementHasCSSTransitions(self.0) } + } + + // Detect if there are any changes that require us to update transitions. + // + // This is used as a more thoroughgoing check than the cheaper + // might_need_transitions_update check. + // + // The following logic shadows the logic used on the Gecko side + // (nsTransitionManager::DoUpdateTransitions) where we actually perform the + // update. + // + // https://drafts.csswg.org/css-transitions/#starting + fn needs_transitions_update( + &self, + before_change_style: &ComputedValues, + after_change_style: &ComputedValues, + ) -> bool { + let after_change_ui_style = after_change_style.get_ui(); + let existing_transitions = self.css_transitions_info(); + + if after_change_style.get_box().clone_display().is_none() { + // We need to cancel existing transitions. + return !existing_transitions.is_empty(); + } + + let mut transitions_to_keep = PropertyDeclarationIdSet::default(); + for transition_property in after_change_style.transition_properties() { + let physical_longhand = PropertyDeclarationId::Longhand( + transition_property + .longhand_id + .to_physical(after_change_style.writing_mode), + ); + transitions_to_keep.insert(physical_longhand); + if self.needs_transitions_update_per_property( + physical_longhand, + after_change_ui_style + .transition_combined_duration_at(transition_property.index) + .seconds(), + before_change_style, + after_change_style, + &existing_transitions, + ) { + return true; + } + } + + // Check if we have to cancel the running transition because this is not + // a matching transition-property value. + existing_transitions + .keys() + .any(|property| !transitions_to_keep.contains(property.as_borrowed())) + } + + /// Whether there is an ElementData container. + #[inline] + fn has_data(&self) -> bool { + self.get_data().is_some() + } + + /// Immutably borrows the ElementData. + fn borrow_data(&self) -> Option<AtomicRef<ElementData>> { + self.get_data().map(|x| x.borrow()) + } + + /// Mutably borrows the ElementData. + fn mutate_data(&self) -> Option<AtomicRefMut<ElementData>> { + self.get_data().map(|x| x.borrow_mut()) + } + + #[inline] + fn lang_attr(&self) -> Option<AttrValue> { + let ptr = unsafe { bindings::Gecko_LangValue(self.0) }; + if ptr.is_null() { + None + } else { + Some(AtomString(unsafe { Atom::from_addrefed(ptr) })) + } + } + + fn match_element_lang(&self, override_lang: Option<Option<AttrValue>>, value: &Lang) -> bool { + // Gecko supports :lang() from CSS Selectors 4, which accepts a list + // of language tags, and does BCP47-style range matching. + let override_lang_ptr = match override_lang { + Some(Some(ref atom)) => atom.as_ptr(), + _ => ptr::null_mut(), + }; + value.0.iter().any(|lang| unsafe { + Gecko_MatchLang( + self.0, + override_lang_ptr, + override_lang.is_some(), + lang.as_slice().as_ptr(), + ) + }) + } + + fn is_html_document_body_element(&self) -> bool { + if self.local_name() != &**local_name!("body") { + return false; + } + + if !self.is_html_element() { + return false; + } + + unsafe { bindings::Gecko_IsDocumentBody(self.0) } + } + + fn synthesize_presentational_hints_for_legacy_attributes<V>( + &self, + visited_handling: VisitedHandlingMode, + hints: &mut V, + ) where + V: Push<ApplicableDeclarationBlock>, + { + use crate::properties::longhands::_x_lang::SpecifiedValue as SpecifiedLang; + use crate::properties::longhands::color::SpecifiedValue as SpecifiedColor; + use crate::stylesheets::layer_rule::LayerOrder; + use crate::values::specified::{color::Color, font::XTextScale}; + lazy_static! { + static ref TABLE_COLOR_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::Color(SpecifiedColor(Color::InheritFromBodyQuirk.into())), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + static ref MATHML_LANG_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XLang(SpecifiedLang(atom!("x-math"))), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + static ref SVG_TEXT_DISABLE_SCALE_RULE: ApplicableDeclarationBlock = { + let global_style_data = &*GLOBAL_STYLE_DATA; + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XTextScale(XTextScale::None), + Importance::Normal, + ); + let arc = Arc::new_leaked(global_style_data.shared_lock.wrap(pdb)); + ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + ) + }; + }; + + let ns = self.namespace_id(); + // <th> elements get a default MozCenterOrInherit which may get overridden + if ns == structs::kNameSpaceID_XHTML as i32 { + if self.local_name().as_ptr() == atom!("table").as_ptr() && + self.as_node().owner_doc().quirks_mode() == QuirksMode::Quirks + { + hints.push(TABLE_COLOR_RULE.clone()); + } + } + if ns == structs::kNameSpaceID_SVG as i32 { + if self.local_name().as_ptr() == atom!("text").as_ptr() { + hints.push(SVG_TEXT_DISABLE_SCALE_RULE.clone()); + } + } + let declarations = + unsafe { Gecko_GetHTMLPresentationAttrDeclarationBlock(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + let declarations = unsafe { Gecko_GetExtraContentStyleDeclarations(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + + // Support for link, vlink, and alink presentation hints on <body> + if self.is_link() { + // Unvisited vs. visited styles are computed up-front based on the + // visited mode (not the element's actual state). + let declarations = match visited_handling { + VisitedHandlingMode::AllLinksVisitedAndUnvisited => { + unreachable!( + "We should never try to selector match with \ + AllLinksVisitedAndUnvisited" + ); + }, + VisitedHandlingMode::AllLinksUnvisited => unsafe { + Gecko_GetUnvisitedLinkAttrDeclarationBlock(self.0).as_ref() + }, + VisitedHandlingMode::RelevantLinkVisited => unsafe { + Gecko_GetVisitedLinkAttrDeclarationBlock(self.0).as_ref() + }, + }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + + let active = self + .state() + .intersects(NonTSPseudoClass::Active.state_flag()); + if active { + let declarations = + unsafe { Gecko_GetActiveLinkAttrDeclarationBlock(self.0).as_ref() }; + if let Some(decl) = declarations { + hints.push(ApplicableDeclarationBlock::from_declarations( + unsafe { Arc::from_raw_addrefed(decl) }, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )); + } + } + } + + // xml:lang has precedence over lang, which can be + // set by Gecko_GetHTMLPresentationAttrDeclarationBlock + // + // http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#language + let ptr = unsafe { bindings::Gecko_GetXMLLangValue(self.0) }; + if !ptr.is_null() { + let global_style_data = &*GLOBAL_STYLE_DATA; + + let pdb = PropertyDeclarationBlock::with_one( + PropertyDeclaration::XLang(SpecifiedLang(unsafe { Atom::from_addrefed(ptr) })), + Importance::Normal, + ); + let arc = Arc::new(global_style_data.shared_lock.wrap(pdb)); + hints.push(ApplicableDeclarationBlock::from_declarations( + arc, + ServoCascadeLevel::PresHints, + LayerOrder::root(), + )) + } + // MathML's default lang has precedence over both `lang` and `xml:lang` + if ns == structs::kNameSpaceID_MathML as i32 { + if self.local_name().as_ptr() == atom!("math").as_ptr() { + hints.push(MATHML_LANG_RULE.clone()); + } + } + } + + fn has_selector_flags(&self, flags: ElementSelectorFlags) -> bool { + let node_flags = selector_flags_to_node_flags(flags); + self.as_node().selector_flags() & node_flags == node_flags + } + + fn relative_selector_search_direction(&self) -> Option<ElementSelectorFlags> { + use crate::gecko_bindings::structs::NodeSelectorFlags; + let flags = self.as_node().selector_flags(); + if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionAncestorSibling.0) != 0 { + Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR_SIBLING) + } else if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionAncestor.0) != 0 { + Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR) + } else if (flags & NodeSelectorFlags::RelativeSelectorSearchDirectionSibling.0) != 0 { + Some(ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING) + } else { + None + } + } +} + +impl<'le> PartialEq for GeckoElement<'le> { + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0 as *const _ == other.0 as *const _ + } +} + +impl<'le> Eq for GeckoElement<'le> {} + +impl<'le> Hash for GeckoElement<'le> { + #[inline] + fn hash<H: Hasher>(&self, state: &mut H) { + (self.0 as *const RawGeckoElement).hash(state); + } +} + +impl<'le> ::selectors::Element for GeckoElement<'le> { + type Impl = SelectorImpl; + + #[inline] + fn opaque(&self) -> OpaqueElement { + OpaqueElement::new(self.0) + } + + #[inline] + fn parent_element(&self) -> Option<Self> { + let parent_node = self.as_node().parent_node(); + parent_node.and_then(|n| n.as_element()) + } + + #[inline] + fn parent_node_is_shadow_root(&self) -> bool { + self.as_node() + .parent_node() + .map_or(false, |p| p.is_shadow_root()) + } + + #[inline] + fn containing_shadow_host(&self) -> Option<Self> { + let shadow = self.containing_shadow()?; + Some(shadow.host()) + } + + #[inline] + fn is_pseudo_element(&self) -> bool { + self.implemented_pseudo_element().is_some() + } + + #[inline] + fn pseudo_element_originating_element(&self) -> Option<Self> { + debug_assert!(self.is_pseudo_element()); + debug_assert!(!self.matches_user_and_content_rules()); + let mut current = *self; + loop { + if current.is_root_of_native_anonymous_subtree() { + return current.traversal_parent(); + } + + current = current.traversal_parent()?; + } + } + + #[inline] + fn assigned_slot(&self) -> Option<Self> { + let slot = self.extended_slots()?._base.mAssignedSlot.mRawPtr; + + unsafe { Some(GeckoElement(&slot.as_ref()?._base._base._base)) } + } + + #[inline] + fn prev_sibling_element(&self) -> Option<Self> { + let mut sibling = self.as_node().prev_sibling(); + while let Some(sibling_node) = sibling { + if let Some(el) = sibling_node.as_element() { + return Some(el); + } + sibling = sibling_node.prev_sibling(); + } + None + } + + #[inline] + fn next_sibling_element(&self) -> Option<Self> { + let mut sibling = self.as_node().next_sibling(); + while let Some(sibling_node) = sibling { + if let Some(el) = sibling_node.as_element() { + return Some(el); + } + sibling = sibling_node.next_sibling(); + } + None + } + + #[inline] + fn first_element_child(&self) -> Option<Self> { + let mut child = self.as_node().first_child(); + while let Some(child_node) = child { + if let Some(el) = child_node.as_element() { + return Some(el); + } + child = child_node.next_sibling(); + } + None + } + + fn apply_selector_flags(&self, flags: ElementSelectorFlags) { + // Handle flags that apply to the element. + let self_flags = flags.for_self(); + if !self_flags.is_empty() { + self.as_node() + .set_selector_flags(selector_flags_to_node_flags(flags)) + } + + // Handle flags that apply to the parent. + let parent_flags = flags.for_parent(); + if !parent_flags.is_empty() { + if let Some(p) = self.as_node().parent_node() { + if p.is_element() || p.is_shadow_root() { + p.set_selector_flags(selector_flags_to_node_flags(parent_flags)); + } + } + } + } + + fn has_attr_in_no_namespace(&self, local_name: &LocalName) -> bool { + for attr in self.attrs() { + if attr.mName.mBits == local_name.as_ptr() as usize { + return true; + } + } + false + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + snapshot_helpers::attr_matches(self.attrs(), ns, local_name, operation) + } + + #[inline] + fn is_root(&self) -> bool { + if self + .as_node() + .get_bool_flag(nsINode_BooleanFlag::ParentIsContent) + { + return false; + } + + if !self.as_node().is_in_document() { + return false; + } + + debug_assert!(self + .as_node() + .parent_node() + .map_or(false, |p| p.is_document())); + // XXX this should always return true at this point, shouldn't it? + unsafe { bindings::Gecko_IsRootElement(self.0) } + } + + fn is_empty(&self) -> bool { + !self + .as_node() + .dom_children() + .any(|child| unsafe { Gecko_IsSignificantChild(child.0, true) }) + } + + #[inline] + fn has_local_name(&self, name: &WeakAtom) -> bool { + self.local_name() == name + } + + #[inline] + fn has_namespace(&self, ns: &WeakNamespace) -> bool { + self.namespace() == ns + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.local_name() == other.local_name() && self.namespace() == other.namespace() + } + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &NonTSPseudoClass, + context: &mut MatchingContext<Self::Impl>, + ) -> bool { + use selectors::matching::*; + match *pseudo_class { + NonTSPseudoClass::Autofill | + NonTSPseudoClass::Defined | + NonTSPseudoClass::Focus | + NonTSPseudoClass::Enabled | + NonTSPseudoClass::Disabled | + NonTSPseudoClass::Checked | + NonTSPseudoClass::Fullscreen | + NonTSPseudoClass::Indeterminate | + NonTSPseudoClass::MozInert | + NonTSPseudoClass::PopoverOpen | + NonTSPseudoClass::PlaceholderShown | + NonTSPseudoClass::Target | + NonTSPseudoClass::Valid | + NonTSPseudoClass::Invalid | + NonTSPseudoClass::MozBroken | + NonTSPseudoClass::Required | + NonTSPseudoClass::Optional | + NonTSPseudoClass::ReadOnly | + NonTSPseudoClass::ReadWrite | + NonTSPseudoClass::FocusWithin | + NonTSPseudoClass::FocusVisible | + NonTSPseudoClass::MozDragOver | + NonTSPseudoClass::MozDevtoolsHighlighted | + NonTSPseudoClass::MozStyleeditorTransitioning | + NonTSPseudoClass::MozMathIncrementScriptLevel | + NonTSPseudoClass::InRange | + NonTSPseudoClass::OutOfRange | + NonTSPseudoClass::Default | + NonTSPseudoClass::UserValid | + NonTSPseudoClass::UserInvalid | + NonTSPseudoClass::MozMeterOptimum | + NonTSPseudoClass::MozMeterSubOptimum | + NonTSPseudoClass::MozMeterSubSubOptimum | + NonTSPseudoClass::MozHasDirAttr | + NonTSPseudoClass::MozDirAttrLTR | + NonTSPseudoClass::MozDirAttrRTL | + NonTSPseudoClass::MozDirAttrLikeAuto | + NonTSPseudoClass::Modal | + NonTSPseudoClass::MozTopmostModal | + NonTSPseudoClass::Active | + NonTSPseudoClass::Hover | + NonTSPseudoClass::MozAutofillPreview | + NonTSPseudoClass::MozRevealed | + NonTSPseudoClass::MozValueEmpty => self.state().intersects(pseudo_class.state_flag()), + // TODO: This applying only to HTML elements is weird. + NonTSPseudoClass::Dir(ref dir) => { + self.is_html_element() && self.state().intersects(dir.element_state()) + }, + NonTSPseudoClass::AnyLink => self.is_link(), + NonTSPseudoClass::Link => { + self.is_link() && context.visited_handling().matches_unvisited() + }, + NonTSPseudoClass::CustomState(ref state) => self.has_custom_state(state), + NonTSPseudoClass::Visited => { + self.is_link() && context.visited_handling().matches_visited() + }, + NonTSPseudoClass::MozFirstNode => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR); + } + let mut elem = self.as_node(); + while let Some(prev) = elem.prev_sibling() { + if prev.contains_non_whitespace_content() { + return false; + } + elem = prev; + } + true + }, + NonTSPseudoClass::MozLastNode => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EDGE_CHILD_SELECTOR); + } + let mut elem = self.as_node(); + while let Some(next) = elem.next_sibling() { + if next.contains_non_whitespace_content() { + return false; + } + elem = next; + } + true + }, + NonTSPseudoClass::MozOnlyWhitespace => { + if context.needs_selector_flags() { + self.apply_selector_flags(ElementSelectorFlags::HAS_EMPTY_SELECTOR); + } + if self + .as_node() + .dom_children() + .any(|c| c.contains_non_whitespace_content()) + { + return false; + } + true + }, + NonTSPseudoClass::MozNativeAnonymous => !self.matches_user_and_content_rules(), + NonTSPseudoClass::MozTableBorderNonzero => unsafe { + bindings::Gecko_IsTableBorderNonzero(self.0) + }, + NonTSPseudoClass::MozSelectListBox => unsafe { + bindings::Gecko_IsSelectListBox(self.0) + }, + NonTSPseudoClass::MozIsHTML => self.as_node().owner_doc().is_html_document(), + NonTSPseudoClass::MozLWTheme | + NonTSPseudoClass::MozLocaleDir(..) | + NonTSPseudoClass::MozWindowInactive => { + let state_bit = pseudo_class.document_state_flag(); + if state_bit.is_empty() { + debug_assert!( + matches!(pseudo_class, NonTSPseudoClass::MozLocaleDir(..)), + "Only moz-locale-dir should ever return an empty state" + ); + return false; + } + if context + .extra_data + .invalidation_data + .document_state + .intersects(state_bit) + { + return !context.in_negation(); + } + self.document_state().contains(state_bit) + }, + NonTSPseudoClass::MozPlaceholder => false, + NonTSPseudoClass::Lang(ref lang_arg) => self.match_element_lang(None, lang_arg), + } + } + + fn match_pseudo_element( + &self, + pseudo_element: &PseudoElement, + _context: &mut MatchingContext<Self::Impl>, + ) -> bool { + // TODO(emilio): I believe we could assert we are a pseudo-element and + // match the proper pseudo-element, given how we rulehash the stuff + // based on the pseudo. + match self.implemented_pseudo_element() { + Some(ref pseudo) => *pseudo == *pseudo_element, + None => false, + } + } + + #[inline] + fn is_link(&self) -> bool { + self.state().intersects(ElementState::VISITED_OR_UNVISITED) + } + + #[inline] + fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + if !self.has_id() { + return false; + } + + let element_id = match snapshot_helpers::get_id(self.attrs()) { + Some(id) => id, + None => return false, + }; + + case_sensitivity.eq_atom(element_id, id) + } + + #[inline] + fn is_part(&self, name: &AtomIdent) -> bool { + let attr = match self.get_part_attr() { + Some(c) => c, + None => return false, + }; + + snapshot_helpers::has_class_or_part(name, CaseSensitivity::CaseSensitive, attr) + } + + #[inline] + fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> { + snapshot_helpers::imported_part(self.attrs(), name) + } + + #[inline(always)] + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + let attr = match self.get_class_attr() { + Some(c) => c, + None => return false, + }; + + snapshot_helpers::has_class_or_part(name, case_sensitivity, attr) + } + + #[inline] + fn is_html_element_in_html_document(&self) -> bool { + self.is_html_element() && self.as_node().owner_doc().is_html_document() + } + + #[inline] + fn is_html_slot_element(&self) -> bool { + self.is_html_element() && self.local_name().as_ptr() == local_name!("slot").as_ptr() + } + + #[inline] + fn ignores_nth_child_selectors(&self) -> bool { + self.is_root_of_native_anonymous_subtree() + } + + fn add_element_unique_hashes(&self, filter: &mut BloomFilter) -> bool { + each_relevant_element_hash(*self, |hash| filter.insert_hash(hash & BLOOM_HASH_MASK)); + true + } +} diff --git a/servo/components/style/gecko_bindings/mod.rs b/servo/components/style/gecko_bindings/mod.rs new file mode 100644 index 0000000000..f0b0adc7ec --- /dev/null +++ b/servo/components/style/gecko_bindings/mod.rs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Gecko's C++ bindings, along with some rust helpers to ease its use. + +// FIXME: We allow `improper_ctypes` (for now), because the lint doesn't allow +// foreign structs to have `PhantomData`. We should remove this once the lint +// ignores this case. + +#[allow( + dead_code, + improper_ctypes, + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + missing_docs +)] +// TODO: Remove this when updating bindgen, see +// https://github.com/rust-lang/rust-bindgen/issues/1651 +#[cfg_attr(test, allow(deref_nullptr))] +pub mod structs { + include!(concat!(env!("OUT_DIR"), "/gecko/structs.rs")); +} + +pub use self::structs as bindings; + +pub mod sugar; diff --git a/servo/components/style/gecko_bindings/sugar/mod.rs b/servo/components/style/gecko_bindings/sugar/mod.rs new file mode 100644 index 0000000000..00faf63ba6 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/mod.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust sugar and convenience methods for Gecko types. + +mod ns_com_ptr; +mod ns_compatibility; +mod ns_style_auto_array; +mod ns_t_array; +pub mod origin_flags; +pub mod ownership; +pub mod refptr; diff --git a/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs new file mode 100644 index 0000000000..1c54541bd8 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_com_ptr.rs @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Little helpers for `nsCOMPtr`. + +use crate::gecko_bindings::structs::nsCOMPtr; + +impl<T> nsCOMPtr<T> { + /// Get this pointer as a raw pointer. + #[inline] + pub fn raw(&self) -> *mut T { + self.mRawPtr + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs new file mode 100644 index 0000000000..f4b81e9f79 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_compatibility.rs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Little helper for `nsCompatibility`. + +use crate::context::QuirksMode; +use crate::gecko_bindings::structs::nsCompatibility; + +impl From<nsCompatibility> for QuirksMode { + #[inline] + fn from(mode: nsCompatibility) -> QuirksMode { + match mode { + nsCompatibility::eCompatibility_FullStandards => QuirksMode::NoQuirks, + nsCompatibility::eCompatibility_AlmostStandards => QuirksMode::LimitedQuirks, + nsCompatibility::eCompatibility_NavQuirks => QuirksMode::Quirks, + } + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs new file mode 100644 index 0000000000..b5772a6c77 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_style_auto_array.rs @@ -0,0 +1,111 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust helpers for Gecko's `nsStyleAutoArray`. + +use crate::gecko_bindings::bindings::Gecko_EnsureStyleAnimationArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleScrollTimelineArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleTransitionArrayLength; +use crate::gecko_bindings::bindings::Gecko_EnsureStyleViewTimelineArrayLength; +use crate::gecko_bindings::structs::nsStyleAutoArray; +use crate::gecko_bindings::structs::{StyleAnimation, StyleTransition}; +use crate::gecko_bindings::structs::{StyleScrollTimeline, StyleViewTimeline}; +use std::iter::{once, Chain, IntoIterator, Once}; +use std::ops::{Index, IndexMut}; +use std::slice::{Iter, IterMut}; + +impl<T> Index<usize> for nsStyleAutoArray<T> { + type Output = T; + fn index(&self, index: usize) -> &T { + match index { + 0 => &self.mFirstElement, + _ => &self.mOtherElements[index - 1], + } + } +} + +impl<T> IndexMut<usize> for nsStyleAutoArray<T> { + fn index_mut(&mut self, index: usize) -> &mut T { + match index { + 0 => &mut self.mFirstElement, + _ => &mut self.mOtherElements[index - 1], + } + } +} + +impl<T> nsStyleAutoArray<T> { + /// Mutably iterate over the array elements. + pub fn iter_mut(&mut self) -> Chain<Once<&mut T>, IterMut<T>> { + once(&mut self.mFirstElement).chain(self.mOtherElements.iter_mut()) + } + + /// Iterate over the array elements. + pub fn iter(&self) -> Chain<Once<&T>, Iter<T>> { + once(&self.mFirstElement).chain(self.mOtherElements.iter()) + } + + /// Returns the length of the array. + /// + /// Note that often structs containing autoarrays will have additional + /// member fields that contain the length, which must be kept in sync. + pub fn len(&self) -> usize { + 1 + self.mOtherElements.len() + } +} + +impl nsStyleAutoArray<StyleAnimation> { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleAnimationArrayLength( + self as *mut nsStyleAutoArray<StyleAnimation> as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray<StyleTransition> { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleTransitionArrayLength( + self as *mut nsStyleAutoArray<StyleTransition> as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray<StyleViewTimeline> { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleViewTimelineArrayLength( + self as *mut nsStyleAutoArray<StyleViewTimeline> as *mut _, + len, + ); + } + } +} + +impl nsStyleAutoArray<StyleScrollTimeline> { + /// Ensures that the array has length at least the given length. + pub fn ensure_len(&mut self, len: usize) { + unsafe { + Gecko_EnsureStyleScrollTimelineArrayLength( + self as *mut nsStyleAutoArray<StyleScrollTimeline> as *mut _, + len, + ); + } + } +} + +impl<'a, T> IntoIterator for &'a mut nsStyleAutoArray<T> { + type Item = &'a mut T; + type IntoIter = Chain<Once<&'a mut T>, IterMut<'a, T>>; + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ns_t_array.rs b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs new file mode 100644 index 0000000000..d10ed420dd --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ns_t_array.rs @@ -0,0 +1,144 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Rust helpers for Gecko's nsTArray. + +use crate::gecko_bindings::bindings; +use crate::gecko_bindings::structs::{nsTArray, nsTArrayHeader, CopyableTArray}; +use std::mem; +use std::ops::{Deref, DerefMut}; +use std::slice; + +impl<T> Deref for nsTArray<T> { + type Target = [T]; + + #[inline] + fn deref<'a>(&'a self) -> &'a [T] { + unsafe { slice::from_raw_parts(self.slice_begin(), self.header().mLength as usize) } + } +} + +impl<T> DerefMut for nsTArray<T> { + fn deref_mut<'a>(&'a mut self) -> &'a mut [T] { + unsafe { slice::from_raw_parts_mut(self.slice_begin(), self.header().mLength as usize) } + } +} + +impl<T> nsTArray<T> { + #[inline] + fn header<'a>(&'a self) -> &'a nsTArrayHeader { + debug_assert!(!self.mBuffer.is_null()); + unsafe { mem::transmute(self.mBuffer) } + } + + // unsafe, since header may be in shared static or something + unsafe fn header_mut<'a>(&'a mut self) -> &'a mut nsTArrayHeader { + debug_assert!(!self.mBuffer.is_null()); + + mem::transmute(self.mBuffer) + } + + #[inline] + unsafe fn slice_begin(&self) -> *mut T { + debug_assert!(!self.mBuffer.is_null()); + (self.mBuffer as *const nsTArrayHeader).offset(1) as *mut _ + } + + /// Ensures the array has enough capacity at least to hold `cap` elements. + /// + /// NOTE: This doesn't call the constructor on the values! + pub fn ensure_capacity(&mut self, cap: usize) { + if cap >= self.len() { + unsafe { + bindings::Gecko_EnsureTArrayCapacity( + self as *mut nsTArray<T> as *mut _, + cap, + mem::size_of::<T>(), + ) + } + } + } + + /// Clears the array storage without calling the destructor on the values. + #[inline] + pub unsafe fn clear(&mut self) { + if self.len() != 0 { + bindings::Gecko_ClearPODTArray( + self as *mut nsTArray<T> as *mut _, + mem::size_of::<T>(), + mem::align_of::<T>(), + ); + } + } + + /// Clears a POD array. This is safe since copy types are memcopyable. + #[inline] + pub fn clear_pod(&mut self) + where + T: Copy, + { + unsafe { self.clear() } + } + + /// Resize and set the length of the array to `len`. + /// + /// unsafe because this may leave the array with uninitialized elements. + /// + /// This will not call constructors. If you need that, either manually add + /// bindings or run the typed `EnsureCapacity` call on the gecko side. + pub unsafe fn set_len(&mut self, len: u32) { + // this can leak + debug_assert!(len >= self.len() as u32); + if self.len() == len as usize { + return; + } + self.ensure_capacity(len as usize); + self.header_mut().mLength = len; + } + + /// Resizes an array containing only POD elements + /// + /// unsafe because this may leave the array with uninitialized elements. + /// + /// This will not leak since it only works on POD types (and thus doesn't assert) + pub unsafe fn set_len_pod(&mut self, len: u32) + where + T: Copy, + { + if self.len() == len as usize { + return; + } + self.ensure_capacity(len as usize); + let header = self.header_mut(); + header.mLength = len; + } + + /// Collects the given iterator into this array. + /// + /// Not unsafe because we won't leave uninitialized elements in the array. + pub fn assign_from_iter_pod<I>(&mut self, iter: I) + where + T: Copy, + I: ExactSizeIterator + Iterator<Item = T>, + { + debug_assert!(iter.len() <= 0xFFFFFFFF); + unsafe { + self.set_len_pod(iter.len() as u32); + } + self.iter_mut().zip(iter).for_each(|(r, v)| *r = v); + } +} + +impl<T> Deref for CopyableTArray<T> { + type Target = nsTArray<T>; + fn deref(&self) -> &Self::Target { + &self._base + } +} + +impl<T> DerefMut for CopyableTArray<T> { + fn deref_mut(&mut self) -> &mut nsTArray<T> { + &mut self._base + } +} diff --git a/servo/components/style/gecko_bindings/sugar/origin_flags.rs b/servo/components/style/gecko_bindings/sugar/origin_flags.rs new file mode 100644 index 0000000000..b27060405a --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/origin_flags.rs @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helper to iterate over `OriginFlags` bits. + +use crate::gecko_bindings::structs::OriginFlags; +use crate::stylesheets::OriginSet; + +/// Checks that the values for OriginFlags are the ones we expect. +pub fn assert_flags_match() { + use crate::stylesheets::origin::*; + debug_assert_eq!( + OriginFlags::UserAgent.0, + OriginSet::ORIGIN_USER_AGENT.bits() + ); + debug_assert_eq!(OriginFlags::Author.0, OriginSet::ORIGIN_AUTHOR.bits()); + debug_assert_eq!(OriginFlags::User.0, OriginSet::ORIGIN_USER.bits()); +} + +impl From<OriginFlags> for OriginSet { + fn from(flags: OriginFlags) -> Self { + Self::from_bits_retain(flags.0) + } +} + +impl From<OriginSet> for OriginFlags { + fn from(set: OriginSet) -> Self { + OriginFlags(set.bits()) + } +} diff --git a/servo/components/style/gecko_bindings/sugar/ownership.rs b/servo/components/style/gecko_bindings/sugar/ownership.rs new file mode 100644 index 0000000000..31b512cf1e --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/ownership.rs @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helpers for different FFI pointer kinds that Gecko's FFI layer uses. + +use crate::gecko_bindings::structs::root::mozilla::detail::CopyablePtr; +use servo_arc::Arc; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::ptr; + +/// Gecko-FFI-safe Arc (T is an ArcInner). +/// +/// This can be null. +/// +/// Leaks on drop. Please don't drop this. +#[repr(C)] +pub struct Strong<GeckoType> { + ptr: *const GeckoType, + _marker: PhantomData<GeckoType>, +} + +impl<T> From<Arc<T>> for Strong<T> { + fn from(arc: Arc<T>) -> Self { + Self { + ptr: Arc::into_raw(arc), + _marker: PhantomData, + } + } +} + +impl<GeckoType> Strong<GeckoType> { + #[inline] + /// Returns whether this reference is null. + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + #[inline] + /// Returns a null pointer + pub fn null() -> Self { + Self { + ptr: ptr::null(), + _marker: PhantomData, + } + } +} + +impl<T> Deref for CopyablePtr<T> { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.mPtr + } +} + +impl<T> DerefMut for CopyablePtr<T> { + fn deref_mut<'a>(&'a mut self) -> &'a mut T { + &mut self.mPtr + } +} diff --git a/servo/components/style/gecko_bindings/sugar/refptr.rs b/servo/components/style/gecko_bindings/sugar/refptr.rs new file mode 100644 index 0000000000..c4a0479a07 --- /dev/null +++ b/servo/components/style/gecko_bindings/sugar/refptr.rs @@ -0,0 +1,289 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A rust helper to ease the use of Gecko's refcounted types. + +use crate::gecko_bindings::{bindings, structs}; +use crate::Atom; +use servo_arc::Arc; +use std::fmt::Write; +use std::marker::PhantomData; +use std::ops::Deref; +use std::{fmt, mem, ptr}; + +/// Trait for all objects that have Addref() and Release +/// methods and can be placed inside RefPtr<T> +pub unsafe trait RefCounted { + /// Bump the reference count. + fn addref(&self); + /// Decrease the reference count. + unsafe fn release(&self); +} + +/// Trait for types which can be shared across threads in RefPtr. +pub unsafe trait ThreadSafeRefCounted: RefCounted {} + +/// A custom RefPtr implementation to take into account Drop semantics and +/// a bit less-painful memory management. +pub struct RefPtr<T: RefCounted> { + ptr: *mut T, + _marker: PhantomData<T>, +} + +impl<T: RefCounted> fmt::Debug for RefPtr<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("RefPtr { ")?; + self.ptr.fmt(f)?; + f.write_char('}') + } +} + +impl<T: RefCounted> RefPtr<T> { + /// Create a new RefPtr from an already addrefed pointer obtained from FFI. + /// + /// The pointer must be valid, non-null and have been addrefed. + pub unsafe fn from_addrefed(ptr: *mut T) -> Self { + debug_assert!(!ptr.is_null()); + RefPtr { + ptr, + _marker: PhantomData, + } + } + + /// Returns whether the current pointer is null. + pub fn is_null(&self) -> bool { + self.ptr.is_null() + } + + /// Returns a null pointer. + pub fn null() -> Self { + Self { + ptr: ptr::null_mut(), + _marker: PhantomData, + } + } + + /// Create a new RefPtr from a pointer obtained from FFI. + /// + /// This method calls addref() internally + pub unsafe fn new(ptr: *mut T) -> Self { + let ret = RefPtr { + ptr, + _marker: PhantomData, + }; + ret.addref(); + ret + } + + /// Produces an FFI-compatible RefPtr that can be stored in style structs. + /// + /// structs::RefPtr does not have a destructor, so this may leak + pub fn forget(self) -> structs::RefPtr<T> { + let ret = structs::RefPtr { + mRawPtr: self.ptr, + _phantom_0: PhantomData, + }; + mem::forget(self); + ret + } + + /// Returns the raw inner pointer to be fed back into FFI. + pub fn get(&self) -> *mut T { + self.ptr + } + + /// Addref the inner data, obviously leaky on its own. + pub fn addref(&self) { + if !self.ptr.is_null() { + unsafe { + (*self.ptr).addref(); + } + } + } + + /// Release the inner data. + /// + /// Call only when the data actually needs releasing. + pub unsafe fn release(&self) { + if !self.ptr.is_null() { + (*self.ptr).release(); + } + } +} + +impl<T: RefCounted> Deref for RefPtr<T> { + type Target = T; + fn deref(&self) -> &T { + debug_assert!(!self.ptr.is_null()); + unsafe { &*self.ptr } + } +} + +impl<T: RefCounted> structs::RefPtr<T> { + /// Produces a Rust-side RefPtr from an FFI RefPtr, bumping the refcount. + /// + /// Must be called on a valid, non-null structs::RefPtr<T>. + pub unsafe fn to_safe(&self) -> RefPtr<T> { + let r = RefPtr { + ptr: self.mRawPtr, + _marker: PhantomData, + }; + r.addref(); + r + } + /// Produces a Rust-side RefPtr, consuming the existing one (and not bumping + /// the refcount). + pub unsafe fn into_safe(self) -> RefPtr<T> { + debug_assert!(!self.mRawPtr.is_null()); + RefPtr { + ptr: self.mRawPtr, + _marker: PhantomData, + } + } + + /// Replace a structs::RefPtr<T> with a different one, appropriately + /// addref/releasing. + /// + /// Both `self` and `other` must be valid, but can be null. + /// + /// Safe when called on an aliased pointer because the refcount in that case + /// needs to be at least two. + pub unsafe fn set(&mut self, other: &Self) { + self.clear(); + if !other.mRawPtr.is_null() { + *self = other.to_safe().forget(); + } + } + + /// Clear an instance of the structs::RefPtr<T>, by releasing + /// it and setting its contents to null. + /// + /// `self` must be valid, but can be null. + pub unsafe fn clear(&mut self) { + if !self.mRawPtr.is_null() { + (*self.mRawPtr).release(); + self.mRawPtr = ptr::null_mut(); + } + } + + /// Replace a `structs::RefPtr<T>` with a `RefPtr<T>`, + /// consuming the `RefPtr<T>`, and releasing the old + /// value in `self` if necessary. + /// + /// `self` must be valid, possibly null. + pub fn set_move(&mut self, other: RefPtr<T>) { + if !self.mRawPtr.is_null() { + unsafe { + (*self.mRawPtr).release(); + } + } + *self = other.forget(); + } +} + +impl<T> structs::RefPtr<T> { + /// Returns a new, null refptr. + pub fn null() -> Self { + Self { + mRawPtr: ptr::null_mut(), + _phantom_0: PhantomData, + } + } + + /// Create a new RefPtr from an arc. + pub fn from_arc(s: Arc<T>) -> Self { + Self { + mRawPtr: Arc::into_raw(s) as *mut _, + _phantom_0: PhantomData, + } + } + + /// Sets the contents to an Arc<T>. + pub fn set_arc(&mut self, other: Arc<T>) { + unsafe { + if !self.mRawPtr.is_null() { + let _ = Arc::from_raw(self.mRawPtr); + } + self.mRawPtr = Arc::into_raw(other) as *mut _; + } + } +} + +impl<T: RefCounted> Drop for RefPtr<T> { + fn drop(&mut self) { + unsafe { self.release() } + } +} + +impl<T: RefCounted> Clone for RefPtr<T> { + fn clone(&self) -> Self { + self.addref(); + RefPtr { + ptr: self.ptr, + _marker: PhantomData, + } + } +} + +impl<T: RefCounted> PartialEq for RefPtr<T> { + fn eq(&self, other: &Self) -> bool { + self.ptr == other.ptr + } +} + +unsafe impl<T: ThreadSafeRefCounted> Send for RefPtr<T> {} +unsafe impl<T: ThreadSafeRefCounted> Sync for RefPtr<T> {} + +macro_rules! impl_refcount { + ($t:ty, $addref:path, $release:path) => { + unsafe impl RefCounted for $t { + #[inline] + fn addref(&self) { + unsafe { $addref(self as *const _ as *mut _) } + } + + #[inline] + unsafe fn release(&self) { + $release(self as *const _ as *mut _) + } + } + }; +} + +// Companion of NS_DECL_THREADSAFE_FFI_REFCOUNTING. +// +// Gets you a free RefCounted impl implemented via FFI. +macro_rules! impl_threadsafe_refcount { + ($t:ty, $addref:path, $release:path) => { + impl_refcount!($t, $addref, $release); + unsafe impl ThreadSafeRefCounted for $t {} + }; +} + +impl_threadsafe_refcount!( + structs::mozilla::URLExtraData, + bindings::Gecko_AddRefURLExtraDataArbitraryThread, + bindings::Gecko_ReleaseURLExtraDataArbitraryThread +); +impl_threadsafe_refcount!( + structs::nsIURI, + bindings::Gecko_AddRefnsIURIArbitraryThread, + bindings::Gecko_ReleasensIURIArbitraryThread +); +impl_threadsafe_refcount!( + structs::SheetLoadDataHolder, + bindings::Gecko_AddRefSheetLoadDataHolderArbitraryThread, + bindings::Gecko_ReleaseSheetLoadDataHolderArbitraryThread +); + +#[inline] +unsafe fn addref_atom(atom: *mut structs::nsAtom) { + mem::forget(Atom::from_raw(atom)); +} + +#[inline] +unsafe fn release_atom(atom: *mut structs::nsAtom) { + let _ = Atom::from_addrefed(atom); +} +impl_threadsafe_refcount!(structs::nsAtom, addref_atom, release_atom); diff --git a/servo/components/style/gecko_string_cache/mod.rs b/servo/components/style/gecko_string_cache/mod.rs new file mode 100644 index 0000000000..79a5d46525 --- /dev/null +++ b/servo/components/style/gecko_string_cache/mod.rs @@ -0,0 +1,497 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] +// This is needed for the constants in atom_macro.rs, because we have some +// atoms whose names differ only by case, e.g. datetime and dateTime. +#![allow(non_upper_case_globals)] + +//! A drop-in replacement for string_cache, but backed by Gecko `nsAtom`s. + +use crate::gecko_bindings::bindings::Gecko_AddRefAtom; +use crate::gecko_bindings::bindings::Gecko_Atomize; +use crate::gecko_bindings::bindings::Gecko_Atomize16; +use crate::gecko_bindings::bindings::Gecko_ReleaseAtom; +use crate::gecko_bindings::structs::root::mozilla::detail::gGkAtoms; +use crate::gecko_bindings::structs::root::mozilla::detail::GkAtoms_Atoms_AtomsCount; +use crate::gecko_bindings::structs::{nsAtom, nsDynamicAtom, nsStaticAtom}; +use nsstring::{nsAString, nsStr}; +use precomputed_hash::PrecomputedHash; +use std::borrow::{Borrow, Cow}; +use std::char::{self, DecodeUtf16}; +use std::fmt::{self, Write}; +use std::hash::{Hash, Hasher}; +use std::iter::Cloned; +use std::mem::{self, ManuallyDrop}; +use std::num::NonZeroUsize; +use std::ops::Deref; +use std::{slice, str}; +use style_traits::SpecifiedValueInfo; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +#[macro_use] +#[allow(improper_ctypes, non_camel_case_types, missing_docs)] +pub mod atom_macro { + include!(concat!(env!("OUT_DIR"), "/gecko/atom_macro.rs")); +} + +#[macro_use] +pub mod namespace; + +pub use self::namespace::{Namespace, WeakNamespace}; + +/// A handle to a Gecko atom. This is a type that can represent either: +/// +/// * A strong reference to a dynamic atom (an `nsAtom` pointer), in which case +/// the `usize` just holds the pointer value. +/// +/// * An index from `gGkAtoms` to the `nsStaticAtom` object (shifted to the left one bit, and with +/// the lower bit set to `1` to differentiate it from the above), so `(index << 1 | 1)`. +/// +#[derive(Eq, PartialEq)] +#[repr(C)] +pub struct Atom(NonZeroUsize); + +/// An atom *without* a strong reference. +/// +/// Only usable as `&'a WeakAtom`, +/// where `'a` is the lifetime of something that holds a strong reference to that atom. +pub struct WeakAtom(nsAtom); + +/// The number of static atoms we have. +const STATIC_ATOM_COUNT: usize = GkAtoms_Atoms_AtomsCount as usize; + +impl Deref for Atom { + type Target = WeakAtom; + + #[inline] + fn deref(&self) -> &WeakAtom { + unsafe { + let addr = if self.is_static() { + // This is really hot. + &gGkAtoms.mAtoms.get_unchecked(self.0.get() >> 1)._base as *const nsAtom + } else { + self.0.get() as *const nsAtom + }; + WeakAtom::new(addr as *const nsAtom) + } + } +} + +impl PrecomputedHash for Atom { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.get_hash() + } +} + +impl Borrow<WeakAtom> for Atom { + #[inline] + fn borrow(&self) -> &WeakAtom { + self + } +} + +impl ToShmem for Atom { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + if !self.is_static() { + return Err(format!( + "ToShmem failed for Atom: must be a static atom: {}", + self + )); + } + + Ok(ManuallyDrop::new(Atom(self.0))) + } +} + +impl Eq for WeakAtom {} +impl PartialEq for WeakAtom { + #[inline] + fn eq(&self, other: &Self) -> bool { + let weak: *const WeakAtom = self; + let other: *const WeakAtom = other; + weak == other + } +} + +impl PartialEq<Atom> for WeakAtom { + #[inline] + fn eq(&self, other: &Atom) -> bool { + self == &**other + } +} + +unsafe impl Send for Atom {} +unsafe impl Sync for Atom {} +unsafe impl Sync for WeakAtom {} + +impl WeakAtom { + /// Construct a `WeakAtom` from a raw `nsAtom`. + #[inline] + pub unsafe fn new<'a>(atom: *const nsAtom) -> &'a mut Self { + &mut *(atom as *mut WeakAtom) + } + + /// Clone this atom, bumping the refcount if the atom is not static. + #[inline] + pub fn clone(&self) -> Atom { + unsafe { Atom::from_raw(self.as_ptr()) } + } + + /// Get the atom hash. + #[inline] + pub fn get_hash(&self) -> u32 { + self.0.mHash + } + + /// Get the atom as a slice of utf-16 chars. + #[inline] + pub fn as_slice(&self) -> &[u16] { + let string = if self.is_static() { + let atom_ptr = self.as_ptr() as *const nsStaticAtom; + let string_offset = unsafe { (*atom_ptr).mStringOffset }; + let string_offset = -(string_offset as isize); + let u8_ptr = atom_ptr as *const u8; + // It is safe to use offset() here because both addresses are within + // the same struct, e.g. mozilla::detail::gGkAtoms. + unsafe { u8_ptr.offset(string_offset) as *const u16 } + } else { + let atom_ptr = self.as_ptr() as *const nsDynamicAtom; + let buffer_ptr = unsafe { (*atom_ptr).mStringBuffer.mRawPtr }; + // Dynamic atom chars are stored at the end of the string buffer. + unsafe { buffer_ptr.offset(1) as *const u16 } + }; + unsafe { slice::from_raw_parts(string, self.len() as usize) } + } + + // NOTE: don't expose this, since it's slow, and easy to be misused. + fn chars(&self) -> DecodeUtf16<Cloned<slice::Iter<u16>>> { + char::decode_utf16(self.as_slice().iter().cloned()) + } + + /// Execute `cb` with the string that this atom represents. + /// + /// Find alternatives to this function when possible, please, since it's + /// pretty slow. + pub fn with_str<F, Output>(&self, cb: F) -> Output + where + F: FnOnce(&str) -> Output, + { + let mut buffer = mem::MaybeUninit::<[u8; 64]>::uninit(); + let buffer = unsafe { &mut *buffer.as_mut_ptr() }; + + // The total string length in utf16 is going to be less than or equal + // the slice length (each utf16 character is going to take at least one + // and at most 2 items in the utf16 slice). + // + // Each of those characters will take at most four bytes in the utf8 + // one. Thus if the slice is less than 64 / 4 (16) we can guarantee that + // we'll decode it in place. + let owned_string; + let len = self.len(); + let utf8_slice = if len <= 16 { + let mut total_len = 0; + + for c in self.chars() { + let c = c.unwrap_or(char::REPLACEMENT_CHARACTER); + let utf8_len = c.encode_utf8(&mut buffer[total_len..]).len(); + total_len += utf8_len; + } + + let slice = unsafe { str::from_utf8_unchecked(&buffer[..total_len]) }; + debug_assert_eq!(slice, String::from_utf16_lossy(self.as_slice())); + slice + } else { + owned_string = String::from_utf16_lossy(self.as_slice()); + &*owned_string + }; + + cb(utf8_slice) + } + + /// Returns whether this atom is static. + #[inline] + pub fn is_static(&self) -> bool { + self.0.mIsStatic() != 0 + } + + /// Returns whether this atom is ascii lowercase. + #[inline] + fn is_ascii_lowercase(&self) -> bool { + self.0.mIsAsciiLowercase() != 0 + } + + /// Returns the length of the atom string. + #[inline] + pub fn len(&self) -> u32 { + self.0.mLength() + } + + /// Returns whether this atom is the empty string. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Returns the atom as a mutable pointer. + #[inline] + pub fn as_ptr(&self) -> *mut nsAtom { + let const_ptr: *const nsAtom = &self.0; + const_ptr as *mut nsAtom + } + + /// Convert this atom to ASCII lower-case + pub fn to_ascii_lowercase(&self) -> Atom { + if self.is_ascii_lowercase() { + return self.clone(); + } + + let slice = self.as_slice(); + let mut buffer = mem::MaybeUninit::<[u16; 64]>::uninit(); + let buffer = unsafe { &mut *buffer.as_mut_ptr() }; + let mut vec; + let mutable_slice = if let Some(buffer_prefix) = buffer.get_mut(..slice.len()) { + buffer_prefix.copy_from_slice(slice); + buffer_prefix + } else { + vec = slice.to_vec(); + &mut vec + }; + for char16 in &mut *mutable_slice { + if *char16 <= 0x7F { + *char16 = (*char16 as u8).to_ascii_lowercase() as u16 + } + } + Atom::from(&*mutable_slice) + } + + /// Return whether two atoms are ASCII-case-insensitive matches + #[inline] + pub fn eq_ignore_ascii_case(&self, other: &Self) -> bool { + if self == other { + return true; + } + + // If we know both atoms are ascii-lowercase, then we can stick with + // pointer equality. + if self.is_ascii_lowercase() && other.is_ascii_lowercase() { + debug_assert!(!self.eq_ignore_ascii_case_slow(other)); + return false; + } + + self.eq_ignore_ascii_case_slow(other) + } + + fn eq_ignore_ascii_case_slow(&self, other: &Self) -> bool { + let a = self.as_slice(); + let b = other.as_slice(); + + if a.len() != b.len() { + return false; + } + + a.iter().zip(b).all(|(&a16, &b16)| { + if a16 <= 0x7F && b16 <= 0x7F { + (a16 as u8).eq_ignore_ascii_case(&(b16 as u8)) + } else { + a16 == b16 + } + }) + } +} + +impl fmt::Debug for WeakAtom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!(w, "Gecko WeakAtom({:p}, {})", self, self) + } +} + +impl fmt::Display for WeakAtom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + for c in self.chars() { + w.write_char(c.unwrap_or(char::REPLACEMENT_CHARACTER))? + } + Ok(()) + } +} + +#[inline] +unsafe fn make_handle(ptr: *const nsAtom) -> NonZeroUsize { + debug_assert!(!ptr.is_null()); + if !WeakAtom::new(ptr).is_static() { + NonZeroUsize::new_unchecked(ptr as usize) + } else { + make_static_handle(ptr as *mut nsStaticAtom) + } +} + +#[inline] +unsafe fn make_static_handle(ptr: *const nsStaticAtom) -> NonZeroUsize { + let index = ptr.offset_from(&gGkAtoms.mAtoms[0] as *const _); + debug_assert!(index >= 0, "Should be a non-negative index"); + debug_assert!( + (index as usize) < STATIC_ATOM_COUNT, + "Should be a valid static atom index" + ); + NonZeroUsize::new_unchecked(((index as usize) << 1) | 1) +} + +impl Atom { + #[inline] + fn is_static(&self) -> bool { + self.0.get() & 1 == 1 + } + + /// Execute a callback with the atom represented by `ptr`. + pub unsafe fn with<F, R>(ptr: *const nsAtom, callback: F) -> R + where + F: FnOnce(&Atom) -> R, + { + let atom = Atom(make_handle(ptr as *mut nsAtom)); + let ret = callback(&atom); + mem::forget(atom); + ret + } + + /// Creates a static atom from its index in the static atom table, without + /// checking. + #[inline] + pub const unsafe fn from_index_unchecked(index: u16) -> Self { + debug_assert!((index as usize) < STATIC_ATOM_COUNT); + Atom(NonZeroUsize::new_unchecked(((index as usize) << 1) | 1)) + } + + /// Creates an atom from an atom pointer. + #[inline(always)] + pub unsafe fn from_raw(ptr: *mut nsAtom) -> Self { + let atom = Atom(make_handle(ptr)); + if !atom.is_static() { + Gecko_AddRefAtom(ptr); + } + atom + } + + /// Creates an atom from an atom pointer that has already had AddRef + /// called on it. This may be a static or dynamic atom. + #[inline] + pub unsafe fn from_addrefed(ptr: *mut nsAtom) -> Self { + assert!(!ptr.is_null()); + Atom(make_handle(ptr)) + } + + /// Convert this atom into an addrefed nsAtom pointer. + #[inline] + pub fn into_addrefed(self) -> *mut nsAtom { + let ptr = self.as_ptr(); + mem::forget(self); + ptr + } +} + +impl Hash for Atom { + fn hash<H>(&self, state: &mut H) + where + H: Hasher, + { + state.write_u32(self.get_hash()); + } +} + +impl Hash for WeakAtom { + fn hash<H>(&self, state: &mut H) + where + H: Hasher, + { + state.write_u32(self.get_hash()); + } +} + +impl Clone for Atom { + #[inline(always)] + fn clone(&self) -> Atom { + unsafe { + let atom = Atom(self.0); + if !atom.is_static() { + Gecko_AddRefAtom(atom.as_ptr()); + } + atom + } + } +} + +impl Drop for Atom { + #[inline] + fn drop(&mut self) { + if !self.is_static() { + unsafe { + Gecko_ReleaseAtom(self.as_ptr()); + } + } + } +} + +impl Default for Atom { + #[inline] + fn default() -> Self { + atom!("") + } +} + +impl fmt::Debug for Atom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + write!(w, "Atom(0x{:08x}, {})", self.0, self) + } +} + +impl fmt::Display for Atom { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + self.deref().fmt(w) + } +} + +impl<'a> From<&'a str> for Atom { + #[inline] + fn from(string: &str) -> Atom { + debug_assert!(string.len() <= u32::max_value() as usize); + unsafe { + Atom::from_addrefed(Gecko_Atomize( + string.as_ptr() as *const _, + string.len() as u32, + )) + } + } +} + +impl<'a> From<&'a [u16]> for Atom { + #[inline] + fn from(slice: &[u16]) -> Atom { + Atom::from(&*nsStr::from(slice)) + } +} + +impl<'a> From<&'a nsAString> for Atom { + #[inline] + fn from(string: &nsAString) -> Atom { + unsafe { Atom::from_addrefed(Gecko_Atomize16(string)) } + } +} + +impl<'a> From<Cow<'a, str>> for Atom { + #[inline] + fn from(string: Cow<'a, str>) -> Atom { + Atom::from(&*string) + } +} + +impl From<String> for Atom { + #[inline] + fn from(string: String) -> Atom { + Atom::from(&*string) + } +} + +malloc_size_of_is_0!(Atom); + +impl SpecifiedValueInfo for Atom {} diff --git a/servo/components/style/gecko_string_cache/namespace.rs b/servo/components/style/gecko_string_cache/namespace.rs new file mode 100644 index 0000000000..d9745b9e21 --- /dev/null +++ b/servo/components/style/gecko_string_cache/namespace.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A type to represent a namespace. + +use crate::gecko_bindings::structs::nsAtom; +use crate::string_cache::{Atom, WeakAtom}; +use precomputed_hash::PrecomputedHash; +use std::borrow::Borrow; +use std::fmt; +use std::ops::Deref; + +/// In Gecko namespaces are just regular atoms, so this is a simple macro to +/// forward one macro to the other. +#[macro_export] +macro_rules! ns { + () => { + $crate::string_cache::Namespace(atom!("")) + }; + ($s:tt) => { + $crate::string_cache::Namespace(atom!($s)) + }; +} + +/// A Gecko namespace is just a wrapped atom. +#[derive( + Clone, + Debug, + Default, + Eq, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct Namespace(pub Atom); + +impl PrecomputedHash for Namespace { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.0.precomputed_hash() + } +} + +/// A Gecko WeakNamespace is a wrapped WeakAtom. +#[derive(Deref, Hash)] +pub struct WeakNamespace(WeakAtom); + +impl Deref for Namespace { + type Target = WeakNamespace; + + #[inline] + fn deref(&self) -> &WeakNamespace { + let weak: *const WeakAtom = &*self.0; + unsafe { &*(weak as *const WeakNamespace) } + } +} + +impl<'a> From<&'a str> for Namespace { + fn from(s: &'a str) -> Self { + Namespace(Atom::from(s)) + } +} + +impl fmt::Display for Namespace { + fn fmt(&self, w: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(w) + } +} + +impl Borrow<WeakNamespace> for Namespace { + #[inline] + fn borrow(&self) -> &WeakNamespace { + self + } +} + +impl WeakNamespace { + /// Trivially construct a WeakNamespace. + #[inline] + pub unsafe fn new<'a>(atom: *mut nsAtom) -> &'a Self { + &*(atom as *const WeakNamespace) + } + + /// Clone this WeakNamespace to obtain a strong reference to the same + /// underlying namespace. + #[inline] + pub fn clone(&self) -> Namespace { + Namespace(self.0.clone()) + } +} + +impl Eq for WeakNamespace {} +impl PartialEq for WeakNamespace { + #[inline] + fn eq(&self, other: &Self) -> bool { + let weak: *const WeakNamespace = self; + let other: *const WeakNamespace = other; + weak == other + } +} diff --git a/servo/components/style/global_style_data.rs b/servo/components/style/global_style_data.rs new file mode 100644 index 0000000000..38d72b2c74 --- /dev/null +++ b/servo/components/style/global_style_data.rs @@ -0,0 +1,212 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Global style data + +use crate::context::StyleSystemOptions; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::bindings; +use crate::parallel::STYLE_THREAD_STACK_SIZE_KB; +use crate::shared_lock::SharedRwLock; +use crate::thread_state; +use gecko_profiler; +use parking_lot::{Mutex, RwLock, RwLockReadGuard}; +use rayon; +#[cfg(unix)] +use std::os::unix::thread::{JoinHandleExt, RawPthread}; +#[cfg(windows)] +use std::os::windows::{io::AsRawHandle, prelude::RawHandle}; +use std::{io, thread}; +use thin_vec::ThinVec; + +/// Platform-specific handle to a thread. +#[cfg(unix)] +pub type PlatformThreadHandle = RawPthread; +/// Platform-specific handle to a thread. +#[cfg(windows)] +pub type PlatformThreadHandle = RawHandle; + +/// Global style data +pub struct GlobalStyleData { + /// Shared RWLock for CSSOM objects + pub shared_lock: SharedRwLock, + + /// Global style system options determined by env vars. + pub options: StyleSystemOptions, +} + +/// Global thread pool. +pub struct StyleThreadPool { + /// How many threads parallel styling can use. If not using a thread pool, this is set to `None`. + pub num_threads: Option<usize>, + + /// The parallel styling thread pool. + /// + /// For leak-checking purposes, we want to terminate the thread-pool, which + /// waits for all the async jobs to complete. Thus the RwLock. + style_thread_pool: RwLock<Option<rayon::ThreadPool>>, +} + +fn thread_name(index: usize) -> String { + format!("StyleThread#{}", index) +} + +lazy_static! { + /// JoinHandles for spawned style threads. These will be joined during + /// StyleThreadPool::shutdown() after exiting the thread pool. + /// + /// This would be quite inefficient if rayon destroyed and re-created + /// threads regularly during threadpool operation in response to demand, + /// however rayon actually never destroys its threads until the entire + /// thread pool is shut-down, so the size of this list is bounded. + static ref STYLE_THREAD_JOIN_HANDLES: Mutex<Vec<thread::JoinHandle<()>>> = + Mutex::new(Vec::new()); +} + +fn thread_spawn(options: rayon::ThreadBuilder) -> io::Result<()> { + let mut b = thread::Builder::new(); + if let Some(name) = options.name() { + b = b.name(name.to_owned()); + } + if let Some(stack_size) = options.stack_size() { + b = b.stack_size(stack_size); + } + let join_handle = b.spawn(|| options.run())?; + STYLE_THREAD_JOIN_HANDLES.lock().push(join_handle); + Ok(()) +} + +fn thread_startup(_index: usize) { + thread_state::initialize_layout_worker_thread(); + #[cfg(feature = "gecko")] + unsafe { + bindings::Gecko_SetJemallocThreadLocalArena(true); + let name = thread_name(_index); + gecko_profiler::register_thread(&name); + } +} + +fn thread_shutdown(_: usize) { + #[cfg(feature = "gecko")] + unsafe { + gecko_profiler::unregister_thread(); + bindings::Gecko_SetJemallocThreadLocalArena(false); + } +} + +impl StyleThreadPool { + /// Shuts down the thread pool, waiting for all work to complete. + pub fn shutdown() { + if STYLE_THREAD_JOIN_HANDLES.lock().is_empty() { + return; + } + { + // Drop the pool. + let _ = STYLE_THREAD_POOL.style_thread_pool.write().take(); + } + + // Join spawned threads until all of the threads have been joined. This + // will usually be pretty fast, as on shutdown there should be basically + // no threads left running. + while let Some(join_handle) = STYLE_THREAD_JOIN_HANDLES.lock().pop() { + let _ = join_handle.join(); + } + } + + /// Returns a reference to the thread pool. + /// + /// We only really want to give read-only access to the pool, except + /// for shutdown(). + pub fn pool(&self) -> RwLockReadGuard<Option<rayon::ThreadPool>> { + self.style_thread_pool.read() + } + + /// Returns a list of the pool's platform-specific thread handles. + pub fn get_thread_handles(handles: &mut ThinVec<PlatformThreadHandle>) { + // Force the lazy initialization of STYLE_THREAD_POOL so that the threads get spawned and + // their join handles are added to STYLE_THREAD_JOIN_HANDLES. + lazy_static::initialize(&STYLE_THREAD_POOL); + + for join_handle in STYLE_THREAD_JOIN_HANDLES.lock().iter() { + #[cfg(unix)] + let handle = join_handle.as_pthread_t(); + #[cfg(windows)] + let handle = join_handle.as_raw_handle(); + + handles.push(handle); + } + } +} + +#[cfg(feature = "servo")] +fn stylo_threads_pref() -> i32 { + pref!(layout.threads) +} + +#[cfg(feature = "gecko")] +fn stylo_threads_pref() -> i32 { + static_prefs::pref!("layout.css.stylo-threads") +} + +/// The performance benefit of additional threads seems to level off at around six, so we cap it +/// there on many-core machines (see bug 1431285 comment 14). +pub(crate) const STYLO_MAX_THREADS: usize = 6; + +lazy_static! { + /// Global thread pool + pub static ref STYLE_THREAD_POOL: StyleThreadPool = { + use std::cmp; + // We always set this pref on startup, before layout or script have had a chance of + // accessing (and thus creating) the thread-pool. + let threads_pref: i32 = stylo_threads_pref(); + let num_threads = if threads_pref >= 0 { + threads_pref as usize + } else { + // Gecko may wish to override the default number of threads, for example on + // systems with heterogeneous CPUs. + #[cfg(feature = "gecko")] + let num_threads = unsafe { bindings::Gecko_GetNumStyleThreads() }; + #[cfg(not(feature = "gecko"))] + let num_threads = -1; + + if num_threads >= 0 { + num_threads as usize + } else { + use num_cpus; + // The default heuristic is num_virtual_cores * .75. This gives us three threads on a + // hyper-threaded dual core, and six threads on a hyper-threaded quad core. + cmp::max(num_cpus::get() * 3 / 4, 1) + } + }; + + let num_threads = cmp::min(num_threads, STYLO_MAX_THREADS); + // Since the main-thread is also part of the pool, having one thread or less doesn't make + // sense. + let (pool, num_threads) = if num_threads <= 1 { + (None, None) + } else { + let workers = rayon::ThreadPoolBuilder::new() + .spawn_handler(thread_spawn) + .use_current_thread() + .num_threads(num_threads) + .thread_name(thread_name) + .start_handler(thread_startup) + .exit_handler(thread_shutdown) + .stack_size(STYLE_THREAD_STACK_SIZE_KB * 1024) + .build(); + (workers.ok(), Some(num_threads)) + }; + + StyleThreadPool { + num_threads, + style_thread_pool: RwLock::new(pool), + } + }; + + /// Global style data + pub static ref GLOBAL_STYLE_DATA: GlobalStyleData = GlobalStyleData { + shared_lock: SharedRwLock::new_leaked(), + options: StyleSystemOptions::default(), + }; +} diff --git a/servo/components/style/invalidation/element/document_state.rs b/servo/components/style/invalidation/element/document_state.rs new file mode 100644 index 0000000000..0b846510d8 --- /dev/null +++ b/servo/components/style/invalidation/element/document_state.rs @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to document state changes. + +use crate::dom::TElement; +use crate::invalidation::element::invalidation_map::Dependency; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::state_and_attributes; +use crate::stylist::CascadeData; +use dom::DocumentState; +use selectors::matching::{ + MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, QuirksMode, + SelectorCaches, VisitedHandlingMode, +}; + +/// A struct holding the members necessary to invalidate document state +/// selectors. +#[derive(Debug)] +pub struct InvalidationMatchingData { + /// The document state that has changed, which makes it always match. + pub document_state: DocumentState, +} + +impl Default for InvalidationMatchingData { + #[inline(always)] + fn default() -> Self { + Self { + document_state: DocumentState::empty(), + } + } +} + +/// An invalidation processor for style changes due to state and attribute +/// changes. +pub struct DocumentStateInvalidationProcessor<'a, 'b, E: TElement, I> { + rules: I, + matching_context: MatchingContext<'a, E::Impl>, + traversal_map: SiblingTraversalMap<E>, + document_states_changed: DocumentState, + _marker: std::marker::PhantomData<&'b ()>, +} + +impl<'a, 'b, E: TElement, I> DocumentStateInvalidationProcessor<'a, 'b, E, I> { + /// Creates a new DocumentStateInvalidationProcessor. + #[inline] + pub fn new( + rules: I, + document_states_changed: DocumentState, + selector_caches: &'a mut SelectorCaches, + quirks_mode: QuirksMode, + ) -> Self { + let mut matching_context = MatchingContext::<'a, E::Impl>::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::No, + ); + + matching_context.extra_data.invalidation_data.document_state = document_states_changed; + + Self { + rules, + document_states_changed, + matching_context, + traversal_map: SiblingTraversalMap::default(), + _marker: std::marker::PhantomData, + } + } +} + +impl<'a, 'b, E, I> InvalidationProcessor<'b, 'a, E> + for DocumentStateInvalidationProcessor<'a, 'b, E, I> +where + E: TElement, + I: Iterator<Item = &'b CascadeData>, +{ + fn check_outer_dependency(&mut self, _: &Dependency, _: E) -> bool { + debug_assert!( + false, + "how, we should only have parent-less dependencies here!" + ); + true + } + + fn collect_invalidations( + &mut self, + _element: E, + self_invalidations: &mut InvalidationVector<'b>, + _descendant_invalidations: &mut DescendantInvalidationLists<'b>, + _sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + for cascade_data in &mut self.rules { + let map = cascade_data.invalidation_map(); + for dependency in &map.document_state_selectors { + if !dependency.state.intersects(self.document_states_changed) { + continue; + } + + // We pass `None` as a scope, as document state selectors aren't + // affected by the current scope. + // + // FIXME(emilio): We should really pass the relevant host for + // self.rules, so that we invalidate correctly if the selector + // happens to have something like :host(:-moz-window-inactive) + // for example. + self_invalidations.push(Invalidation::new( + &dependency.dependency, + /* scope = */ None, + )); + } + } + + false + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + &self.traversal_map + } + + fn recursion_limit_exceeded(&mut self, _: E) { + unreachable!("We don't run document state invalidation with stack limits") + } + + fn should_process_descendants(&mut self, element: E) -> bool { + match element.borrow_data() { + Some(d) => state_and_attributes::should_process_descendants(&d), + None => false, + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + state_and_attributes::invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + state_and_attributes::invalidated_self(element); + } + + fn invalidated_sibling(&mut self, sibling: E, of: E) { + state_and_attributes::invalidated_sibling(sibling, of); + } +} diff --git a/servo/components/style/invalidation/element/element_wrapper.rs b/servo/components/style/invalidation/element/element_wrapper.rs new file mode 100644 index 0000000000..e17afd7774 --- /dev/null +++ b/servo/components/style/invalidation/element/element_wrapper.rs @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A wrapper over an element and a snapshot, that allows us to selector-match +//! against a past state of the element. + +use crate::dom::TElement; +use crate::selector_parser::{AttrValue, NonTSPseudoClass, PseudoElement, SelectorImpl}; +use crate::selector_parser::{Snapshot, SnapshotMap}; +use crate::values::AtomIdent; +use crate::{CaseSensitivityExt, LocalName, Namespace, WeakAtom}; +use dom::ElementState; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::bloom::BloomFilter; +use selectors::matching::{ElementSelectorFlags, MatchingContext}; +use selectors::{Element, OpaqueElement}; +use std::cell::Cell; +use std::fmt; + +/// In order to compute restyle hints, we perform a selector match against a +/// list of partial selectors whose rightmost simple selector may be sensitive +/// to the thing being changed. We do this matching twice, once for the element +/// as it exists now and once for the element as it existed at the time of the +/// last restyle. If the results of the selector match differ, that means that +/// the given partial selector is sensitive to the change, and we compute a +/// restyle hint based on its combinator. +/// +/// In order to run selector matching against the old element state, we generate +/// a wrapper for the element which claims to have the old state. This is the +/// ElementWrapper logic below. +/// +/// Gecko does this differently for element states, and passes a mask called +/// mStateMask, which indicates the states that need to be ignored during +/// selector matching. This saves an ElementWrapper allocation and an additional +/// selector match call at the expense of additional complexity inside the +/// selector matching logic. This only works for boolean states though, so we +/// still need to take the ElementWrapper approach for attribute-dependent +/// style. So we do it the same both ways for now to reduce complexity, but it's +/// worth measuring the performance impact (if any) of the mStateMask approach. +pub trait ElementSnapshot: Sized { + /// The state of the snapshot, if any. + fn state(&self) -> Option<ElementState>; + + /// If this snapshot contains attribute information. + fn has_attrs(&self) -> bool; + + /// Gets the attribute information of the snapshot as a string. + /// + /// Only for debugging purposes. + fn debug_list_attributes(&self) -> String { + String::new() + } + + /// The ID attribute per this snapshot. Should only be called if + /// `has_attrs()` returns true. + fn id_attr(&self) -> Option<&WeakAtom>; + + /// Whether this snapshot contains the class `name`. Should only be called + /// if `has_attrs()` returns true. + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool; + + /// Whether this snapshot represents the part named `name`. Should only be + /// called if `has_attrs()` returns true. + fn is_part(&self, name: &AtomIdent) -> bool; + + /// See Element::imported_part. + fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent>; + + /// A callback that should be called for each class of the snapshot. Should + /// only be called if `has_attrs()` returns true. + fn each_class<F>(&self, _: F) + where + F: FnMut(&AtomIdent); + + /// The `xml:lang=""` or `lang=""` attribute value per this snapshot. + fn lang_attr(&self) -> Option<AttrValue>; +} + +/// A simple wrapper over an element and a snapshot, that allows us to +/// selector-match against a past state of the element. +#[derive(Clone)] +pub struct ElementWrapper<'a, E> +where + E: TElement, +{ + element: E, + cached_snapshot: Cell<Option<&'a Snapshot>>, + snapshot_map: &'a SnapshotMap, +} + +impl<'a, E> ElementWrapper<'a, E> +where + E: TElement, +{ + /// Trivially constructs an `ElementWrapper`. + pub fn new(el: E, snapshot_map: &'a SnapshotMap) -> Self { + ElementWrapper { + element: el, + cached_snapshot: Cell::new(None), + snapshot_map: snapshot_map, + } + } + + /// Gets the snapshot associated with this element, if any. + pub fn snapshot(&self) -> Option<&'a Snapshot> { + if !self.element.has_snapshot() { + return None; + } + + if let Some(s) = self.cached_snapshot.get() { + return Some(s); + } + + let snapshot = self.snapshot_map.get(&self.element); + debug_assert!(snapshot.is_some(), "has_snapshot lied!"); + + self.cached_snapshot.set(snapshot); + + snapshot + } + + /// Returns the states that have changed since the element was snapshotted. + pub fn state_changes(&self) -> ElementState { + let snapshot = match self.snapshot() { + Some(s) => s, + None => return ElementState::empty(), + }; + + match snapshot.state() { + Some(state) => state ^ self.element.state(), + None => ElementState::empty(), + } + } + + /// Returns the value of the `xml:lang=""` (or, if appropriate, `lang=""`) + /// attribute from this element's snapshot or the closest ancestor + /// element snapshot with the attribute specified. + fn get_lang(&self) -> Option<AttrValue> { + let mut current = self.clone(); + loop { + let lang = match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.lang_attr(), + _ => current.element.lang_attr(), + }; + if lang.is_some() { + return lang; + } + current = current.parent_element()?; + } + } +} + +impl<'a, E> fmt::Debug for ElementWrapper<'a, E> +where + E: TElement, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Ignore other fields for now, can change later if needed. + self.element.fmt(f) + } +} + +impl<'a, E> Element for ElementWrapper<'a, E> +where + E: TElement, +{ + type Impl = SelectorImpl; + + fn match_non_ts_pseudo_class( + &self, + pseudo_class: &NonTSPseudoClass, + context: &mut MatchingContext<Self::Impl>, + ) -> bool { + // Some pseudo-classes need special handling to evaluate them against + // the snapshot. + match *pseudo_class { + // For :link and :visited, we don't actually want to test the + // element state directly. + // + // Instead, we use the `visited_handling` to determine if they + // match. + NonTSPseudoClass::Link => { + return self.is_link() && context.visited_handling().matches_unvisited(); + }, + NonTSPseudoClass::Visited => { + return self.is_link() && context.visited_handling().matches_visited(); + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsTableBorderNonzero(); + } + } + }, + + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + if let Some(snapshot) = self.snapshot() { + if snapshot.has_other_pseudo_class_state() { + return snapshot.mIsSelectListBox(); + } + } + }, + + // :lang() needs to match using the closest ancestor xml:lang="" or + // lang="" attribtue from snapshots. + NonTSPseudoClass::Lang(ref lang_arg) => { + return self + .element + .match_element_lang(Some(self.get_lang()), lang_arg); + }, + + _ => {}, + } + + let flag = pseudo_class.state_flag(); + if flag.is_empty() { + return self + .element + .match_non_ts_pseudo_class(pseudo_class, context); + } + match self.snapshot().and_then(|s| s.state()) { + Some(snapshot_state) => snapshot_state.intersects(flag), + None => self + .element + .match_non_ts_pseudo_class(pseudo_class, context), + } + } + + fn apply_selector_flags(&self, _flags: ElementSelectorFlags) { + debug_assert!(false, "Shouldn't need selector flags for invalidation"); + } + + fn match_pseudo_element( + &self, + pseudo_element: &PseudoElement, + context: &mut MatchingContext<Self::Impl>, + ) -> bool { + self.element.match_pseudo_element(pseudo_element, context) + } + + fn is_link(&self) -> bool { + match self.snapshot().and_then(|s| s.state()) { + Some(state) => state.intersects(ElementState::VISITED_OR_UNVISITED), + None => self.element.is_link(), + } + } + + fn opaque(&self) -> OpaqueElement { + self.element.opaque() + } + + fn parent_element(&self) -> Option<Self> { + let parent = self.element.parent_element()?; + Some(Self::new(parent, self.snapshot_map)) + } + + fn parent_node_is_shadow_root(&self) -> bool { + self.element.parent_node_is_shadow_root() + } + + fn containing_shadow_host(&self) -> Option<Self> { + let host = self.element.containing_shadow_host()?; + Some(Self::new(host, self.snapshot_map)) + } + + fn prev_sibling_element(&self) -> Option<Self> { + let sibling = self.element.prev_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn next_sibling_element(&self) -> Option<Self> { + let sibling = self.element.next_sibling_element()?; + Some(Self::new(sibling, self.snapshot_map)) + } + + fn first_element_child(&self) -> Option<Self> { + let child = self.element.first_element_child()?; + Some(Self::new(child, self.snapshot_map)) + } + + #[inline] + fn is_html_element_in_html_document(&self) -> bool { + self.element.is_html_element_in_html_document() + } + + #[inline] + fn is_html_slot_element(&self) -> bool { + self.element.is_html_slot_element() + } + + #[inline] + fn has_local_name( + &self, + local_name: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedLocalName, + ) -> bool { + self.element.has_local_name(local_name) + } + + #[inline] + fn has_namespace( + &self, + ns: &<Self::Impl as ::selectors::SelectorImpl>::BorrowedNamespaceUrl, + ) -> bool { + self.element.has_namespace(ns) + } + + #[inline] + fn is_same_type(&self, other: &Self) -> bool { + self.element.is_same_type(&other.element) + } + + fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AttrValue>, + ) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => { + snapshot.attr_matches(ns, local_name, operation) + }, + _ => self.element.attr_matches(ns, local_name, operation), + } + } + + fn has_id(&self, id: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot + .id_attr() + .map_or(false, |atom| case_sensitivity.eq_atom(&atom, id)), + _ => self.element.has_id(id, case_sensitivity), + } + } + + fn is_part(&self, name: &AtomIdent) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.is_part(name), + _ => self.element.is_part(name), + } + } + + fn imported_part(&self, name: &AtomIdent) -> Option<AtomIdent> { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.imported_part(name), + _ => self.element.imported_part(name), + } + } + + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + match self.snapshot() { + Some(snapshot) if snapshot.has_attrs() => snapshot.has_class(name, case_sensitivity), + _ => self.element.has_class(name, case_sensitivity), + } + } + + fn is_empty(&self) -> bool { + self.element.is_empty() + } + + fn is_root(&self) -> bool { + self.element.is_root() + } + + fn is_pseudo_element(&self) -> bool { + self.element.is_pseudo_element() + } + + fn pseudo_element_originating_element(&self) -> Option<Self> { + self.element + .pseudo_element_originating_element() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } + + fn assigned_slot(&self) -> Option<Self> { + self.element + .assigned_slot() + .map(|e| ElementWrapper::new(e, self.snapshot_map)) + } + + fn add_element_unique_hashes(&self, _filter: &mut BloomFilter) -> bool { + // Should not be relevant in the context of checking past elements in invalidation. + false + } +} diff --git a/servo/components/style/invalidation/element/invalidation_map.rs b/servo/components/style/invalidation/element/invalidation_map.rs new file mode 100644 index 0000000000..cb03862740 --- /dev/null +++ b/servo/components/style/invalidation/element/invalidation_map.rs @@ -0,0 +1,1425 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code for invalidations due to state or attribute changes. + +use crate::context::QuirksMode; +use crate::selector_map::{ + MaybeCaseInsensitiveHashMap, PrecomputedHashMap, SelectorMap, SelectorMapEntry, +}; +use crate::selector_parser::{NonTSPseudoClass, SelectorImpl}; +use crate::AllocErr; +use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded}; +use dom::{DocumentState, ElementState}; +use selectors::attr::NamespaceConstraint; +use selectors::parser::{ + Combinator, Component, RelativeSelector, RelativeSelectorCombinatorCount, + RelativeSelectorMatchHint, +}; +use selectors::parser::{Selector, SelectorIter}; +use selectors::visitor::{SelectorListKind, SelectorVisitor}; +use servo_arc::Arc; +use smallvec::SmallVec; + +/// Mapping between (partial) CompoundSelectors (and the combinator to their +/// right) and the states and attributes they depend on. +/// +/// In general, for all selectors in all applicable stylesheets of the form: +/// +/// |a _ b _ c _ d _ e| +/// +/// Where: +/// * |b| and |d| are simple selectors that depend on state (like :hover) or +/// attributes (like [attr...], .foo, or #foo). +/// * |a|, |c|, and |e| are arbitrary simple selectors that do not depend on +/// state or attributes. +/// +/// We generate a Dependency for both |a _ b:X _| and |a _ b:X _ c _ d:Y _|, +/// even though those selectors may not appear on their own in any stylesheet. +/// This allows us to quickly scan through the dependency sites of all style +/// rules and determine the maximum effect that a given state or attribute +/// change may have on the style of elements in the document. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct Dependency { + /// The dependency selector. + #[ignore_malloc_size_of = "CssRules have primary refs, we measure there"] + pub selector: Selector<SelectorImpl>, + + /// The offset into the selector that we should match on. + pub selector_offset: usize, + + /// The parent dependency for an ancestor selector. For example, consider + /// the following: + /// + /// .foo .bar:where(.baz span) .qux + /// ^ ^ ^ + /// A B C + /// + /// We'd generate: + /// + /// * One dependency for .qux (offset: 0, parent: None) + /// * One dependency for .baz pointing to B with parent being a + /// dependency pointing to C. + /// * One dependency from .bar pointing to C (parent: None) + /// * One dependency from .foo pointing to A (parent: None) + /// + #[ignore_malloc_size_of = "Arc"] + pub parent: Option<Arc<Dependency>>, + + /// What kind of relative selector invalidation this generates. + /// None if this dependency is not within a relative selector. + relative_kind: Option<RelativeDependencyInvalidationKind>, +} + +impl SelectorMapEntry for Dependency { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.selector.iter_from(self.selector_offset) + } +} + +/// The kind of elements down the tree this dependency may affect. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum NormalDependencyInvalidationKind { + /// This dependency may affect the element that changed itself. + Element, + /// This dependency affects the style of the element itself, and also the + /// style of its descendants. + /// + /// TODO(emilio): Each time this feels more of a hack for eager pseudos... + ElementAndDescendants, + /// This dependency may affect descendants down the tree. + Descendants, + /// This dependency may affect siblings to the right of the element that + /// changed. + Siblings, + /// This dependency may affect slotted elements of the element that changed. + SlottedElements, + /// This dependency may affect parts of the element that changed. + Parts, +} + +/// The kind of elements up the tree this relative selector dependency may +/// affect. Because this travels upwards, it's not viable for parallel subtree +/// traversal, and is handled separately. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum RelativeDependencyInvalidationKind { + /// This dependency may affect relative selector anchors for ancestors. + Ancestors, + /// This dependency may affect a relative selector anchor for the parent. + Parent, + /// This dependency may affect a relative selector anchor for the previous sibling. + PrevSibling, + /// This dependency may affect relative selector anchors for ancestors' previous siblings. + AncestorPrevSibling, + /// This dependency may affect relative selector anchors for earlier siblings. + EarlierSibling, + /// This dependency may affect relative selector anchors for ancestors' earlier siblings. + AncestorEarlierSibling, +} + +/// Invalidation kind merging normal and relative dependencies. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, MallocSizeOf)] +pub enum DependencyInvalidationKind { + /// This dependency is a normal dependency. + Normal(NormalDependencyInvalidationKind), + /// This dependency is a relative dependency. + Relative(RelativeDependencyInvalidationKind), +} + +impl Dependency { + /// Creates a dummy dependency to invalidate the whole selector. + /// + /// This is necessary because document state invalidation wants to + /// invalidate all elements in the document. + /// + /// The offset is such as that Invalidation::new(self) returns a zero + /// offset. That is, it points to a virtual "combinator" outside of the + /// selector, so calling combinator() on such a dependency will panic. + pub fn for_full_selector_invalidation(selector: Selector<SelectorImpl>) -> Self { + Self { + selector_offset: selector.len() + 1, + selector, + parent: None, + relative_kind: None, + } + } + + /// Returns the combinator to the right of the partial selector this + /// dependency represents. + /// + /// TODO(emilio): Consider storing inline if it helps cache locality? + fn combinator(&self) -> Option<Combinator> { + if self.selector_offset == 0 { + return None; + } + + Some( + self.selector + .combinator_at_match_order(self.selector_offset - 1), + ) + } + + /// The kind of normal invalidation that this would generate. The dependency + /// in question must be a normal dependency. + pub fn normal_invalidation_kind(&self) -> NormalDependencyInvalidationKind { + debug_assert!( + self.relative_kind.is_none(), + "Querying normal invalidation kind on relative dependency." + ); + match self.combinator() { + None => NormalDependencyInvalidationKind::Element, + Some(Combinator::Child) | Some(Combinator::Descendant) => { + NormalDependencyInvalidationKind::Descendants + }, + Some(Combinator::LaterSibling) | Some(Combinator::NextSibling) => { + NormalDependencyInvalidationKind::Siblings + }, + // TODO(emilio): We could look at the selector itself to see if it's + // an eager pseudo, and return only Descendants here if not. + Some(Combinator::PseudoElement) => { + NormalDependencyInvalidationKind::ElementAndDescendants + }, + Some(Combinator::SlotAssignment) => NormalDependencyInvalidationKind::SlottedElements, + Some(Combinator::Part) => NormalDependencyInvalidationKind::Parts, + } + } + + /// The kind of invalidation that this would generate. + pub fn invalidation_kind(&self) -> DependencyInvalidationKind { + if let Some(kind) = self.relative_kind { + return DependencyInvalidationKind::Relative(kind); + } + DependencyInvalidationKind::Normal(self.normal_invalidation_kind()) + } + + /// Is the combinator to the right of this dependency's compound selector + /// the next sibling combinator? This matters for insertion/removal in between + /// two elements connected through next sibling, e.g. `.foo:has(> .a + .b)` + /// where an element gets inserted between `.a` and `.b`. + pub fn right_combinator_is_next_sibling(&self) -> bool { + if self.selector_offset == 0 { + return false; + } + matches!( + self.selector + .combinator_at_match_order(self.selector_offset - 1), + Combinator::NextSibling + ) + } + + /// Is this dependency's compound selector a single compound in `:has` + /// with the next sibling relative combinator i.e. `:has(> .foo)`? + /// This matters for insertion between an anchor and an element + /// connected through next sibling, e.g. `.a:has(> .b)`. + pub fn dependency_is_relative_with_single_next_sibling(&self) -> bool { + match self.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => false, + DependencyInvalidationKind::Relative(kind) => { + kind == RelativeDependencyInvalidationKind::PrevSibling + }, + } + } +} + +/// The same, but for state selectors, which can track more exactly what state +/// do they track. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct StateDependency { + /// The other dependency fields. + pub dep: Dependency, + /// The state this dependency is affected by. + pub state: ElementState, +} + +impl SelectorMapEntry for StateDependency { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.dep.selector() + } +} + +/// The same, but for document state selectors. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct DocumentStateDependency { + /// We track `Dependency` even though we don't need to track an offset, + /// since when it changes it changes for the whole document anyway. + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "CssRules have primary refs, we measure there" + )] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + pub dependency: Dependency, + /// The state this dependency is affected by. + pub state: DocumentState, +} + +/// Dependency mapping for classes or IDs. +pub type IdOrClassDependencyMap = MaybeCaseInsensitiveHashMap<Atom, SmallVec<[Dependency; 1]>>; +/// Dependency mapping for non-tree-strctural pseudo-class states. +pub type StateDependencyMap = SelectorMap<StateDependency>; +/// Dependency mapping for local names. +pub type LocalNameDependencyMap = PrecomputedHashMap<LocalName, SmallVec<[Dependency; 1]>>; + +/// A map where we store invalidations. +/// +/// This is slightly different to a SelectorMap, in the sense of that the same +/// selector may appear multiple times. +/// +/// In particular, we want to lookup as few things as possible to get the fewer +/// selectors the better, so this looks up by id, class, or looks at the list of +/// state/other attribute affecting selectors. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct InvalidationMap { + /// A map from a given class name to all the selectors with that class + /// selector. + pub class_to_selector: IdOrClassDependencyMap, + /// A map from a given id to all the selectors with that ID in the + /// stylesheets currently applying to the document. + pub id_to_selector: IdOrClassDependencyMap, + /// A map of all the state dependencies. + pub state_affecting_selectors: StateDependencyMap, + /// A list of document state dependencies in the rules we represent. + pub document_state_selectors: Vec<DocumentStateDependency>, + /// A map of other attribute affecting selectors. + pub other_attribute_affecting_selectors: LocalNameDependencyMap, +} + +/// Tree-structural pseudoclasses that we care about for (Relative selector) invalidation. +/// Specifically, we need to store information on ones that don't generate the inner selector. +#[derive(Clone, Copy, Debug, MallocSizeOf)] +pub struct TSStateForInvalidation(u8); + +bitflags! { + impl TSStateForInvalidation : u8 { + /// :empty + const EMPTY = 1 << 0; + /// :nth etc, without of. + const NTH = 1 << 1; + /// "Simple" edge child selectors, like :first-child, :last-child, etc. + /// Excludes :*-of-type. + const NTH_EDGE = 1 << 2; + } +} + +/// Dependency for tree-structural pseudo-classes. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct TSStateDependency { + /// The other dependency fields. + pub dep: Dependency, + /// The state this dependency is affected by. + pub state: TSStateForInvalidation, +} + +impl SelectorMapEntry for TSStateDependency { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.dep.selector() + } +} + +/// Dependency mapping for tree-structural pseudo-class states. +pub type TSStateDependencyMap = SelectorMap<TSStateDependency>; +/// Dependency mapping for * selectors. +pub type AnyDependencyMap = SmallVec<[Dependency; 1]>; + +/// A map to store all relative selector invalidations. +/// This keeps a lot more data than the usual map, because any change can generate +/// upward traversals that need to be handled separately. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct RelativeSelectorInvalidationMap { + /// Portion common to the normal invalidation map, except that this is for relative selectors and their inner selectors. + pub map: InvalidationMap, + /// A map for a given tree-structural pseudo-class to all the relative selector dependencies with that type. + pub ts_state_to_selector: TSStateDependencyMap, + /// A map from a given type name to all the relative selector dependencies with that type. + pub type_to_selector: LocalNameDependencyMap, + /// All relative selector dependencies that specify `*`. + pub any_to_selector: AnyDependencyMap, + /// Flag indicating if any relative selector is used. + pub used: bool, + /// Flag indicating if invalidating a relative selector requires ancestor traversal. + pub needs_ancestors_traversal: bool, +} + +impl RelativeSelectorInvalidationMap { + /// Creates an empty `InvalidationMap`. + pub fn new() -> Self { + Self { + map: InvalidationMap::new(), + ts_state_to_selector: TSStateDependencyMap::default(), + type_to_selector: LocalNameDependencyMap::default(), + any_to_selector: SmallVec::default(), + used: false, + needs_ancestors_traversal: false, + } + } + + /// Returns the number of dependencies stored in the invalidation map. + pub fn len(&self) -> usize { + self.map.len() + } + + /// Clears this map, leaving it empty. + pub fn clear(&mut self) { + self.map.clear(); + self.ts_state_to_selector.clear(); + self.type_to_selector.clear(); + self.any_to_selector.clear(); + } + + /// Shrink the capacity of hash maps if needed. + pub fn shrink_if_needed(&mut self) { + self.map.shrink_if_needed(); + self.ts_state_to_selector.shrink_if_needed(); + self.type_to_selector.shrink_if_needed(); + } +} + +impl InvalidationMap { + /// Creates an empty `InvalidationMap`. + pub fn new() -> Self { + Self { + class_to_selector: IdOrClassDependencyMap::new(), + id_to_selector: IdOrClassDependencyMap::new(), + state_affecting_selectors: StateDependencyMap::new(), + document_state_selectors: Vec::new(), + other_attribute_affecting_selectors: LocalNameDependencyMap::default(), + } + } + + /// Returns the number of dependencies stored in the invalidation map. + pub fn len(&self) -> usize { + self.state_affecting_selectors.len() + + self.document_state_selectors.len() + + self.other_attribute_affecting_selectors + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.id_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + + self.class_to_selector + .iter() + .fold(0, |accum, (_, ref v)| accum + v.len()) + } + + /// Clears this map, leaving it empty. + pub fn clear(&mut self) { + self.class_to_selector.clear(); + self.id_to_selector.clear(); + self.state_affecting_selectors.clear(); + self.document_state_selectors.clear(); + self.other_attribute_affecting_selectors.clear(); + } + + /// Shrink the capacity of hash maps if needed. + pub fn shrink_if_needed(&mut self) { + self.class_to_selector.shrink_if_needed(); + self.id_to_selector.shrink_if_needed(); + self.state_affecting_selectors.shrink_if_needed(); + self.other_attribute_affecting_selectors.shrink_if_needed(); + } +} + +/// Adds a selector to the given `InvalidationMap`. Returns Err(..) to signify OOM. +pub fn note_selector_for_invalidation( + selector: &Selector<SelectorImpl>, + quirks_mode: QuirksMode, + map: &mut InvalidationMap, + relative_selector_invalidation_map: &mut RelativeSelectorInvalidationMap, +) -> Result<(), AllocErr> { + debug!("note_selector_for_invalidation({:?})", selector); + + let mut document_state = DocumentState::empty(); + { + let mut parent_stack = ParentSelectors::new(); + let mut alloc_error = None; + let mut collector = SelectorDependencyCollector { + map, + relative_selector_invalidation_map, + document_state: &mut document_state, + selector, + parent_selectors: &mut parent_stack, + quirks_mode, + compound_state: PerCompoundState::new(0), + alloc_error: &mut alloc_error, + }; + + let visit_result = collector.visit_whole_selector(); + debug_assert_eq!(!visit_result, alloc_error.is_some()); + if let Some(alloc_error) = alloc_error { + return Err(alloc_error); + } + } + + if !document_state.is_empty() { + let dep = DocumentStateDependency { + state: document_state, + dependency: Dependency::for_full_selector_invalidation(selector.clone()), + }; + map.document_state_selectors.try_reserve(1)?; + map.document_state_selectors.push(dep); + } + Ok(()) +} +struct PerCompoundState { + /// The offset at which our compound starts. + offset: usize, + + /// The state this compound selector is affected by. + element_state: ElementState, +} + +impl PerCompoundState { + fn new(offset: usize) -> Self { + Self { + offset, + element_state: ElementState::empty(), + } + } +} + +struct ParentDependencyEntry { + selector: Selector<SelectorImpl>, + offset: usize, + cached_dependency: Option<Arc<Dependency>>, +} + +trait Collector { + fn dependency(&mut self) -> Dependency; + fn id_map(&mut self) -> &mut IdOrClassDependencyMap; + fn class_map(&mut self) -> &mut IdOrClassDependencyMap; + fn state_map(&mut self) -> &mut StateDependencyMap; + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap; + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState); + + // In normal invalidations, type-based dependencies don't need to be explicitly tracked; + // elements don't change their types, and mutations cause invalidations to go descendant + // (Where they are about to be styled anyway), and/or later-sibling direction (Where they + // siblings after inserted/removed elements get restyled anyway). + // However, for relative selectors, a DOM mutation can affect and arbitrary ancestor and/or + // earlier siblings, so we need to keep track of them. + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + unreachable!(); + } + + // Tree-structural pseudo-selectors generally invalidates in a well-defined way, which are + // handled by RestyleManager. However, for relative selectors, as with type invalidations, + // the direction of invalidation becomes arbitrary, so we need to keep track of them. + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + unreachable!(); + } + + // Same story as type invalidation maps. + fn any_vec(&mut self) -> &mut AnyDependencyMap { + unreachable!(); + } +} + +fn on_attribute<C: Collector>( + local_name: &LocalName, + local_name_lower: &LocalName, + collector: &mut C, +) -> Result<(), AllocErr> { + add_attr_dependency(local_name.clone(), collector)?; + + if local_name != local_name_lower { + add_attr_dependency(local_name_lower.clone(), collector)?; + } + Ok(()) +} + +fn on_id_or_class<C: Collector>( + s: &Component<SelectorImpl>, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + let dependency = collector.dependency(); + let (atom, map) = match *s { + Component::ID(ref atom) => (atom, collector.id_map()), + Component::Class(ref atom) => (atom, collector.class_map()), + _ => unreachable!(), + }; + let entry = map.try_entry(atom.0.clone(), quirks_mode)?; + let vec = entry.or_insert_with(SmallVec::new); + vec.try_reserve(1)?; + vec.push(dependency); + Ok(()) +} + +fn add_attr_dependency<C: Collector>(name: LocalName, collector: &mut C) -> Result<(), AllocErr> { + let dependency = collector.dependency(); + let map = collector.attribute_map(); + add_local_name(name, dependency, map) +} + +fn add_local_name( + name: LocalName, + dependency: Dependency, + map: &mut LocalNameDependencyMap, +) -> Result<(), AllocErr> { + map.try_reserve(1)?; + let vec = map.entry(name).or_default(); + vec.try_reserve(1)?; + vec.push(dependency); + Ok(()) +} + +fn on_pseudo_class<C: Collector>(pc: &NonTSPseudoClass, collector: &mut C) -> Result<(), AllocErr> { + collector.update_states(pc.state_flag(), pc.document_state_flag()); + + let attr_name = match *pc { + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozTableBorderNonzero => local_name!("border"), + #[cfg(feature = "gecko")] + NonTSPseudoClass::MozSelectListBox => { + // This depends on two attributes. + add_attr_dependency(local_name!("multiple"), collector)?; + return add_attr_dependency(local_name!("size"), collector); + }, + NonTSPseudoClass::Lang(..) => local_name!("lang"), + _ => return Ok(()), + }; + + add_attr_dependency(attr_name, collector) +} + +fn add_pseudo_class_dependency<C: Collector>( + element_state: ElementState, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + if element_state.is_empty() { + return Ok(()); + } + let dependency = collector.dependency(); + collector.state_map().insert( + StateDependency { + dep: dependency, + state: element_state, + }, + quirks_mode, + ) +} + +type ParentSelectors = SmallVec<[ParentDependencyEntry; 5]>; + +/// A struct that collects invalidations for a given compound selector. +struct SelectorDependencyCollector<'a> { + map: &'a mut InvalidationMap, + relative_selector_invalidation_map: &'a mut RelativeSelectorInvalidationMap, + + /// The document this _complex_ selector is affected by. + /// + /// We don't need to track state per compound selector, since it's global + /// state and it changes for everything. + document_state: &'a mut DocumentState, + + /// The current selector and offset we're iterating. + selector: &'a Selector<SelectorImpl>, + + /// The stack of parent selectors that we have, and at which offset of the + /// sequence. + /// + /// This starts empty. It grows when we find nested :is and :where selector + /// lists. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: PerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option<AllocErr>, +} + +fn parent_dependency( + parent_selectors: &mut ParentSelectors, + outer_parent: Option<&Arc<Dependency>>, +) -> Option<Arc<Dependency>> { + if parent_selectors.is_empty() { + return outer_parent.cloned(); + } + + fn dependencies_from( + entries: &mut [ParentDependencyEntry], + outer_parent: &Option<&Arc<Dependency>>, + ) -> Option<Arc<Dependency>> { + if entries.is_empty() { + return None; + } + + let last_index = entries.len() - 1; + let (previous, last) = entries.split_at_mut(last_index); + let last = &mut last[0]; + let selector = &last.selector; + let selector_offset = last.offset; + Some( + last.cached_dependency + .get_or_insert_with(|| { + Arc::new(Dependency { + selector: selector.clone(), + selector_offset, + parent: dependencies_from(previous, outer_parent), + relative_kind: None, + }) + }) + .clone(), + ) + } + + dependencies_from(parent_selectors, &outer_parent) +} + +impl<'a> Collector for SelectorDependencyCollector<'a> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, None); + Dependency { + selector: self.selector.clone(), + selector_offset: self.compound_state.offset, + parent, + relative_kind: None, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.element_state |= element_state; + *self.document_state |= document_state; + } +} + +impl<'a> SelectorDependencyCollector<'a> { + fn visit_whole_selector(&mut self) -> bool { + let iter = self.selector.iter(); + self.visit_whole_selector_from(iter, 0) + } + + fn visit_whole_selector_from( + &mut self, + mut iter: SelectorIter<SelectorImpl>, + mut index: usize, + ) -> bool { + loop { + // Reset the compound state. + self.compound_state = PerCompoundState::new(index); + + // Visit all the simple selectors in this sequence. + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; // Account for the simple selector. + } + + if let Err(err) = add_pseudo_class_dependency( + self.compound_state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a> SelectorVisitor for SelectorDependencyCollector<'a> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<SelectorImpl>], + ) -> bool { + for selector in list { + // Here we cheat a bit: We can visit the rightmost compound with + // the "outer" visitor, and it'd be fine. This reduces the amount of + // state and attribute invalidations, and we need to check the outer + // selector to the left anyway to avoid over-invalidation, so it + // avoids matching it twice uselessly. + let mut iter = selector.iter(); + let mut index = 0; + + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + continue; + } + + index += 1; // account for the combinator. + + self.parent_selectors.push(ParentDependencyEntry { + selector: self.selector.clone(), + offset: self.compound_state.offset, + cached_dependency: None, + }); + let mut nested = SelectorDependencyCollector { + map: &mut *self.map, + relative_selector_invalidation_map: &mut *self.relative_selector_invalidation_map, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: PerCompoundState::new(index), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector_from(iter, index) { + return false; + } + self.parent_selectors.pop(); + } + true + } + + fn visit_relative_selector_list( + &mut self, + list: &[selectors::parser::RelativeSelector<Self::Impl>], + ) -> bool { + self.relative_selector_invalidation_map.used = true; + for relative_selector in list { + // We can't cheat here like we do with other selector lists - the rightmost + // compound of a relative selector is not the subject of the invalidation. + self.parent_selectors.push(ParentDependencyEntry { + selector: self.selector.clone(), + offset: self.compound_state.offset, + cached_dependency: None, + }); + let mut nested = RelativeSelectorDependencyCollector { + map: &mut *self.relative_selector_invalidation_map, + document_state: &mut *self.document_state, + selector: &relative_selector, + combinator_count: RelativeSelectorCombinatorCount::new(relative_selector), + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + self.parent_selectors.pop(); + } + true + } + + fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} + +struct RelativeSelectorPerCompoundState { + state: PerCompoundState, + ts_state: TSStateForInvalidation, + added_entry: bool, +} + +impl RelativeSelectorPerCompoundState { + fn new(offset: usize) -> Self { + Self { + state: PerCompoundState::new(offset), + ts_state: TSStateForInvalidation::empty(), + added_entry: false, + } + } +} + +/// A struct that collects invalidations for a given compound selector. +struct RelativeSelectorDependencyCollector<'a> { + map: &'a mut RelativeSelectorInvalidationMap, + + /// The document this _complex_ selector is affected by. + /// + /// We don't need to track state per compound selector, since it's global + /// state and it changes for everything. + document_state: &'a mut DocumentState, + + /// The current inner relative selector and offset we're iterating. + selector: &'a RelativeSelector<SelectorImpl>, + /// Running combinator for this inner relative selector. + combinator_count: RelativeSelectorCombinatorCount, + + /// The stack of parent selectors that we have, and at which offset of the + /// sequence. + /// + /// This starts empty. It grows when we find nested :is and :where selector + /// lists. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: RelativeSelectorPerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option<AllocErr>, +} + +fn add_non_unique_info<C: Collector>( + selector: &Selector<SelectorImpl>, + offset: usize, + collector: &mut C, +) -> Result<(), AllocErr> { + // Go through this compound again. + for ss in selector.iter_from(offset) { + match ss { + Component::LocalName(ref name) => { + let dependency = collector.dependency(); + add_local_name(name.name.clone(), dependency, &mut collector.type_map())?; + if name.name != name.lower_name { + let dependency = collector.dependency(); + add_local_name( + name.lower_name.clone(), + dependency, + &mut collector.type_map(), + )?; + } + return Ok(()); + }, + _ => (), + }; + } + // Ouch. Add one for *. + collector.any_vec().try_reserve(1)?; + let dependency = collector.dependency(); + collector.any_vec().push(dependency); + Ok(()) +} + +fn add_ts_pseudo_class_dependency<C: Collector>( + state: TSStateForInvalidation, + quirks_mode: QuirksMode, + collector: &mut C, +) -> Result<(), AllocErr> { + if state.is_empty() { + return Ok(()); + } + let dependency = collector.dependency(); + collector.ts_state_map().insert( + TSStateDependency { + dep: dependency, + state, + }, + quirks_mode, + ) +} + +impl<'a> RelativeSelectorDependencyCollector<'a> { + fn visit_whole_selector(&mut self) -> bool { + let mut iter = self.selector.selector.iter_skip_relative_selector_anchor(); + let mut index = 0; + + self.map.needs_ancestors_traversal |= match self.selector.match_hint { + RelativeSelectorMatchHint::InNextSiblingSubtree | + RelativeSelectorMatchHint::InSiblingSubtree | + RelativeSelectorMatchHint::InSubtree => true, + _ => false, + }; + loop { + // Reset the compound state. + self.compound_state = RelativeSelectorPerCompoundState::new(index); + + // Visit all the simple selectors in this sequence. + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; // Account for the simple selector. + } + + if let Err(err) = add_pseudo_class_dependency( + self.compound_state.state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + if let Err(err) = + add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self) + { + *self.alloc_error = Some(err); + return false; + } + + if !self.compound_state.added_entry { + // Not great - we didn't add any uniquely identifiable information. + if let Err(err) = add_non_unique_info( + &self.selector.selector, + self.compound_state.state.offset, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + } + + let combinator = iter.next_sequence(); + if let Some(c) = combinator { + match c { + Combinator::Child | Combinator::Descendant => { + self.combinator_count.child_or_descendants -= 1 + }, + Combinator::NextSibling | Combinator::LaterSibling => { + self.combinator_count.adjacent_or_next_siblings -= 1 + }, + Combinator::Part | Combinator::PseudoElement | Combinator::SlotAssignment => (), + } + } else { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a> Collector for RelativeSelectorDependencyCollector<'a> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, None); + Dependency { + selector: self.selector.selector.clone(), + selector_offset: self.compound_state.state.offset, + relative_kind: Some(match self.combinator_count.get_match_hint() { + RelativeSelectorMatchHint::InChild => RelativeDependencyInvalidationKind::Parent, + RelativeSelectorMatchHint::InSubtree => { + RelativeDependencyInvalidationKind::Ancestors + }, + RelativeSelectorMatchHint::InNextSibling => { + RelativeDependencyInvalidationKind::PrevSibling + }, + RelativeSelectorMatchHint::InSibling => { + RelativeDependencyInvalidationKind::EarlierSibling + }, + RelativeSelectorMatchHint::InNextSiblingSubtree => { + RelativeDependencyInvalidationKind::AncestorPrevSibling + }, + RelativeSelectorMatchHint::InSiblingSubtree => { + RelativeDependencyInvalidationKind::AncestorEarlierSibling + }, + }), + parent, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.state.element_state |= element_state; + *self.document_state |= document_state; + } + + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.type_to_selector + } + + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + &mut self.map.ts_state_to_selector + } + + fn any_vec(&mut self) -> &mut AnyDependencyMap { + &mut self.map.any_to_selector + } +} + +impl<'a> SelectorVisitor for RelativeSelectorDependencyCollector<'a> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<SelectorImpl>], + ) -> bool { + let mut parent_stack = ParentSelectors::new(); + let parent_dependency = Arc::new(self.dependency()); + for selector in list { + // Subjects inside relative selectors aren't really subjects. + // This simplifies compound state tracking as well (Additional + // states we track for relative selector's inner selectors should + // not leak out of the relevant selector). + let mut nested = RelativeSelectorInnerDependencyCollector { + map: &mut *self.map, + parent_dependency: &parent_dependency, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut parent_stack, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + } + true + } + + fn visit_relative_selector_list( + &mut self, + _list: &[selectors::parser::RelativeSelector<Self::Impl>], + ) -> bool { + // Ignore nested relative selectors. These can happen as a result of nesting. + true + } + + fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + self.compound_state.added_entry = true; + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if !pc + .state_flag() + .intersects(ElementState::VISITED_OR_UNVISITED) + { + // Visited/Unvisited styling doesn't take the usual state invalidation path. + self.compound_state.added_entry = true; + } + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::Empty => { + self.compound_state + .ts_state + .insert(TSStateForInvalidation::EMPTY); + true + }, + Component::Nth(data) => { + let kind = if data.is_simple_edge() { + TSStateForInvalidation::NTH_EDGE + } else { + TSStateForInvalidation::NTH + }; + self.compound_state + .ts_state + .insert(kind); + true + }, + Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"), + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + self.compound_state.added_entry = true; + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} + +/// A struct that collects invalidations from a complex selector inside a relative selector. +/// TODO(dshin): All of this duplication is not great Perhaps should be merged to the normal +/// one, if possible? See bug 1855690. +struct RelativeSelectorInnerDependencyCollector<'a, 'b> { + map: &'a mut RelativeSelectorInvalidationMap, + + /// The document this _complex_ selector is affected by. + /// + /// We don't need to track state per compound selector, since it's global + /// state and it changes for everything. + document_state: &'a mut DocumentState, + + /// Parent relative selector dependency. + parent_dependency: &'b Arc<Dependency>, + + /// The current inner relative selector and offset we're iterating. + selector: &'a Selector<SelectorImpl>, + + /// The stack of parent selectors that we have, and at which offset of the + /// sequence. + /// + /// This starts empty. It grows when we find nested :is and :where selector + /// lists. The dependency field is cached and reference counted. + parent_selectors: &'a mut ParentSelectors, + + /// The quirks mode of the document where we're inserting dependencies. + quirks_mode: QuirksMode, + + /// State relevant to a given compound selector. + compound_state: RelativeSelectorPerCompoundState, + + /// The allocation error, if we OOM. + alloc_error: &'a mut Option<AllocErr>, +} + +impl<'a, 'b> Collector for RelativeSelectorInnerDependencyCollector<'a, 'b> { + fn dependency(&mut self) -> Dependency { + let parent = parent_dependency(self.parent_selectors, Some(self.parent_dependency)); + Dependency { + selector: self.selector.clone(), + selector_offset: self.compound_state.state.offset, + parent, + relative_kind: None, + } + } + + fn id_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.id_to_selector + } + + fn class_map(&mut self) -> &mut IdOrClassDependencyMap { + &mut self.map.map.class_to_selector + } + + fn state_map(&mut self) -> &mut StateDependencyMap { + &mut self.map.map.state_affecting_selectors + } + + fn attribute_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.map.other_attribute_affecting_selectors + } + + fn update_states(&mut self, element_state: ElementState, document_state: DocumentState) { + self.compound_state.state.element_state |= element_state; + *self.document_state |= document_state; + } + + fn type_map(&mut self) -> &mut LocalNameDependencyMap { + &mut self.map.type_to_selector + } + + fn ts_state_map(&mut self) -> &mut TSStateDependencyMap { + &mut self.map.ts_state_to_selector + } + + fn any_vec(&mut self) -> &mut AnyDependencyMap { + &mut self.map.any_to_selector + } +} + +impl<'a, 'b> RelativeSelectorInnerDependencyCollector<'a, 'b> { + fn visit_whole_selector(&mut self) -> bool { + let mut iter = self.selector.iter(); + let mut index = 0; + loop { + // Reset the compound state. + self.compound_state = RelativeSelectorPerCompoundState::new(index); + + // Visit all the simple selectors in this sequence. + for ss in &mut iter { + if !ss.visit(self) { + return false; + } + index += 1; // Account for the simple selector. + } + + if let Err(err) = add_pseudo_class_dependency( + self.compound_state.state.element_state, + self.quirks_mode, + self, + ) { + *self.alloc_error = Some(err); + return false; + } + + if let Err(err) = + add_ts_pseudo_class_dependency(self.compound_state.ts_state, self.quirks_mode, self) + { + *self.alloc_error = Some(err); + return false; + } + + if !self.compound_state.added_entry { + // Not great - we didn't add any uniquely identifiable information. + if let Err(err) = + add_non_unique_info(&self.selector, self.compound_state.state.offset, self) + { + *self.alloc_error = Some(err); + return false; + } + } + + let combinator = iter.next_sequence(); + if combinator.is_none() { + return true; + } + index += 1; // account for the combinator + } + } +} + +impl<'a, 'b> SelectorVisitor for RelativeSelectorInnerDependencyCollector<'a, 'b> { + type Impl = SelectorImpl; + + fn visit_selector_list( + &mut self, + _list_kind: SelectorListKind, + list: &[Selector<SelectorImpl>], + ) -> bool { + let parent_dependency = Arc::new(self.dependency()); + for selector in list { + // Subjects inside relative selectors aren't really subjects. + // This simplifies compound state tracking as well (Additional + // states we track for relative selector's inner selectors should + // not leak out of the relevant selector). + let mut nested = RelativeSelectorInnerDependencyCollector { + map: &mut *self.map, + parent_dependency: &parent_dependency, + document_state: &mut *self.document_state, + selector, + parent_selectors: &mut *self.parent_selectors, + quirks_mode: self.quirks_mode, + compound_state: RelativeSelectorPerCompoundState::new(0), + alloc_error: &mut *self.alloc_error, + }; + if !nested.visit_whole_selector() { + return false; + } + } + true + } + + fn visit_relative_selector_list( + &mut self, + _list: &[selectors::parser::RelativeSelector<Self::Impl>], + ) -> bool { + // Ignore nested relative selectors. These can happen as a result of nesting. + true + } + + fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool { + match *s { + Component::ID(..) | Component::Class(..) => { + self.compound_state.added_entry = true; + if let Err(err) = on_id_or_class(s, self.quirks_mode, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::NonTSPseudoClass(ref pc) => { + if !pc + .state_flag() + .intersects(ElementState::VISITED_OR_UNVISITED) + { + // Visited/Unvisited styling doesn't take the usual state invalidation path. + self.compound_state.added_entry = true; + } + if let Err(err) = on_pseudo_class(pc, self) { + *self.alloc_error = Some(err.into()); + return false; + } + true + }, + Component::Empty => { + self.compound_state + .ts_state + .insert(TSStateForInvalidation::EMPTY); + true + }, + Component::Nth(data) => { + let kind = if data.is_simple_edge() { + TSStateForInvalidation::NTH_EDGE + } else { + TSStateForInvalidation::NTH + }; + self.compound_state + .ts_state + .insert(kind); + true + }, + Component::RelativeSelectorAnchor => unreachable!("Should not visit this far"), + _ => true, + } + } + + fn visit_attribute_selector( + &mut self, + _: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + local_name_lower: &LocalName, + ) -> bool { + self.compound_state.added_entry = true; + if let Err(err) = on_attribute(local_name, local_name_lower, self) { + *self.alloc_error = Some(err); + return false; + } + true + } +} diff --git a/servo/components/style/invalidation/element/invalidator.rs b/servo/components/style/invalidation/element/invalidator.rs new file mode 100644 index 0000000000..5dee1f5dcf --- /dev/null +++ b/servo/components/style/invalidation/element/invalidator.rs @@ -0,0 +1,1130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The struct that takes care of encapsulating all the logic on where and how +//! element styles need to be invalidated. + +use crate::context::StackLimitChecker; +use crate::dom::{TElement, TNode, TShadowRoot}; +use crate::invalidation::element::invalidation_map::{ + Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind, + RelativeDependencyInvalidationKind, +}; +use selectors::matching::matches_compound_selector_from; +use selectors::matching::{CompoundSelectorMatchingResult, MatchingContext}; +use selectors::parser::{Combinator, Component}; +use selectors::OpaqueElement; +use smallvec::SmallVec; +use std::fmt; +use std::fmt::Write; + +struct SiblingInfo<E> +where + E: TElement, +{ + affected: E, + prev_sibling: Option<E>, + next_sibling: Option<E>, +} + +/// Traversal mapping for elements under consideration. It acts like a snapshot map, +/// though this only "maps" one element at most. +/// For general invalidations, this has no effect, especially since when +/// DOM mutates, the mutation's effect should not escape the subtree being mutated. +/// This is not the case for relative selectors, unfortunately, so we may end up +/// traversing a portion of the DOM tree that mutated. In case the mutation is removal, +/// its sibling relation is severed by the time the invalidation happens. This structure +/// recovers that relation. Note - it assumes that there is only one element under this +/// effect. +pub struct SiblingTraversalMap<E> +where + E: TElement, +{ + info: Option<SiblingInfo<E>>, +} + +impl<E> Default for SiblingTraversalMap<E> +where + E: TElement, +{ + fn default() -> Self { + Self { info: None } + } +} + +impl<E> SiblingTraversalMap<E> +where + E: TElement, +{ + /// Create a new traversal map with the affected element. + pub fn new(affected: E, prev_sibling: Option<E>, next_sibling: Option<E>) -> Self { + Self { + info: Some(SiblingInfo { + affected, + prev_sibling, + next_sibling, + }), + } + } + + /// Get the element's previous sibling element. + pub fn next_sibling_for(&self, element: &E) -> Option<E> { + if let Some(ref info) = self.info { + if *element == info.affected { + return info.next_sibling; + } + } + element.next_sibling_element() + } + + /// Get the element's previous sibling element. + pub fn prev_sibling_for(&self, element: &E) -> Option<E> { + if let Some(ref info) = self.info { + if *element == info.affected { + return info.prev_sibling; + } + } + element.prev_sibling_element() + } +} + +/// A trait to abstract the collection of invalidations for a given pass. +pub trait InvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// Whether an invalidation that contains only a pseudo-element selector + /// like ::before or ::after triggers invalidation of the element that would + /// originate it. + fn invalidates_on_pseudo_element(&self) -> bool { + false + } + + /// Whether the invalidation processor only cares about light-tree + /// descendants of a given element, that is, doesn't invalidate + /// pseudo-elements, NAC, shadow dom... + fn light_tree_only(&self) -> bool { + false + } + + /// When a dependency from a :where or :is selector matches, it may still be + /// the case that we don't need to invalidate the full style. Consider the + /// case of: + /// + /// div .foo:where(.bar *, .baz) .qux + /// + /// We can get to the `*` part after a .bar class change, but you only need + /// to restyle the element if it also matches .foo. + /// + /// Similarly, you only need to restyle .baz if the whole result of matching + /// the selector changes. + /// + /// This function is called to check the result of matching the "outer" + /// dependency that we generate for the parent of the `:where` selector, + /// that is, in the case above it should match + /// `div .foo:where(.bar *, .baz)`. + /// + /// Returning true unconditionally here is over-optimistic and may + /// over-invalidate. + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool; + + /// The matching context that should be used to process invalidations. + fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl>; + + /// The traversal map that should be used to process invalidations. + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E>; + + /// Collect invalidations for a given element's descendants and siblings. + /// + /// Returns whether the element itself was invalidated. + fn collect_invalidations( + &mut self, + element: E, + self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool; + + /// Returns whether the invalidation process should process the descendants + /// of the given element. + fn should_process_descendants(&mut self, element: E) -> bool; + + /// Executes an arbitrary action when the recursion limit is exceded (if + /// any). + fn recursion_limit_exceeded(&mut self, element: E); + + /// Executes an action when `Self` is invalidated. + fn invalidated_self(&mut self, element: E); + + /// Executes an action when `sibling` is invalidated as a sibling of + /// `of`. + fn invalidated_sibling(&mut self, sibling: E, of: E); + + /// Executes an action when any descendant of `Self` is invalidated. + fn invalidated_descendants(&mut self, element: E, child: E); + + /// Executes an action when an element in a relative selector is reached. + /// Lets the dependency to be borrowed for further processing out of the + /// invalidation traversal. + fn found_relative_selector_invalidation( + &mut self, + _element: E, + _kind: RelativeDependencyInvalidationKind, + _relative_dependency: &'a Dependency, + ) { + debug_assert!(false, "Reached relative selector dependency"); + } +} + +/// Different invalidation lists for descendants. +#[derive(Debug, Default)] +pub struct DescendantInvalidationLists<'a> { + /// Invalidations for normal DOM children and pseudo-elements. + /// + /// TODO(emilio): Having a list of invalidations just for pseudo-elements + /// may save some work here and there. + pub dom_descendants: InvalidationVector<'a>, + /// Invalidations for slotted children of an element. + pub slotted_descendants: InvalidationVector<'a>, + /// Invalidations for ::part()s of an element. + pub parts: InvalidationVector<'a>, +} + +impl<'a> DescendantInvalidationLists<'a> { + fn is_empty(&self) -> bool { + self.dom_descendants.is_empty() && + self.slotted_descendants.is_empty() && + self.parts.is_empty() + } +} + +/// The struct that takes care of encapsulating all the logic on where and how +/// element styles need to be invalidated. +pub struct TreeStyleInvalidator<'a, 'b, 'c, E, P: 'a> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, 'c, E>, +{ + element: E, + stack_limit_checker: Option<&'a StackLimitChecker>, + processor: &'a mut P, + _marker: std::marker::PhantomData<(&'b (), &'c ())>, +} + +/// A vector of invalidations, optimized for small invalidation sets. +pub type InvalidationVector<'a> = SmallVec<[Invalidation<'a>; 10]>; + +/// The kind of descendant invalidation we're processing. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DescendantInvalidationKind { + /// A DOM descendant invalidation. + Dom, + /// A ::slotted() descendant invalidation. + Slotted, + /// A ::part() descendant invalidation. + Part, +} + +/// The kind of invalidation we're processing. +/// +/// We can use this to avoid pushing invalidations of the same kind to our +/// descendants or siblings. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum InvalidationKind { + Descendant(DescendantInvalidationKind), + Sibling, +} + +/// An `Invalidation` is a complex selector that describes which elements, +/// relative to a current element we are processing, must be restyled. +#[derive(Clone)] +pub struct Invalidation<'a> { + /// The dependency that generated this invalidation. + /// + /// Note that the offset inside the dependency is not really useful after + /// construction. + dependency: &'a Dependency, + /// The right shadow host from where the rule came from, if any. + /// + /// This is needed to ensure that we match the selector with the right + /// state, as whether some selectors like :host and ::part() match depends + /// on it. + scope: Option<OpaqueElement>, + /// The offset of the selector pointing to a compound selector. + /// + /// This order is a "parse order" offset, that is, zero is the leftmost part + /// of the selector written as parsed / serialized. + /// + /// It is initialized from the offset from `dependency`. + offset: usize, + /// Whether the invalidation was already matched by any previous sibling or + /// ancestor. + /// + /// If this is the case, we can avoid pushing invalidations generated by + /// this one if the generated invalidation is effective for all the siblings + /// or descendants after us. + matched_by_any_previous: bool, +} + +impl<'a> Invalidation<'a> { + /// Create a new invalidation for matching a dependency. + pub fn new(dependency: &'a Dependency, scope: Option<OpaqueElement>) -> Self { + debug_assert!( + dependency.selector_offset == dependency.selector.len() + 1 || + dependency.normal_invalidation_kind() != + NormalDependencyInvalidationKind::Element, + "No point to this, if the dependency matched the element we should just invalidate it" + ); + Self { + dependency, + scope, + // + 1 to go past the combinator. + offset: dependency.selector.len() + 1 - dependency.selector_offset, + matched_by_any_previous: false, + } + } + + /// Whether this invalidation is effective for the next sibling or + /// descendant after us. + fn effective_for_next(&self) -> bool { + if self.offset == 0 { + return true; + } + + // TODO(emilio): For pseudo-elements this should be mostly false, except + // for the weird pseudos in <input type="number">. + // + // We should be able to do better here! + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Descendant | Combinator::LaterSibling | Combinator::PseudoElement => true, + Combinator::Part | + Combinator::SlotAssignment | + Combinator::NextSibling | + Combinator::Child => false, + } + } + + fn kind(&self) -> InvalidationKind { + if self.offset == 0 { + return InvalidationKind::Descendant(DescendantInvalidationKind::Dom); + } + + match self + .dependency + .selector + .combinator_at_parse_order(self.offset - 1) + { + Combinator::Child | Combinator::Descendant | Combinator::PseudoElement => { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) + }, + Combinator::Part => InvalidationKind::Descendant(DescendantInvalidationKind::Part), + Combinator::SlotAssignment => { + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) + }, + Combinator::NextSibling | Combinator::LaterSibling => InvalidationKind::Sibling, + } + } +} + +impl<'a> fmt::Debug for Invalidation<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use cssparser::ToCss; + + f.write_str("Invalidation(")?; + for component in self + .dependency + .selector + .iter_raw_parse_order_from(self.offset) + { + if matches!(*component, Component::Combinator(..)) { + break; + } + component.to_css(f)?; + } + f.write_char(')') + } +} + +/// The result of processing a single invalidation for a given element. +struct SingleInvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the invalidation matched, either invalidating the element or + /// generating another invalidation. + matched: bool, +} + +/// The result of a whole invalidation process for a given element. +pub struct InvalidationResult { + /// Whether the element itself was invalidated. + invalidated_self: bool, + /// Whether the element's descendants were invalidated. + invalidated_descendants: bool, + /// Whether the element's siblings were invalidated. + invalidated_siblings: bool, +} + +impl InvalidationResult { + /// Create an emtpy result. + pub fn empty() -> Self { + Self { + invalidated_self: false, + invalidated_descendants: false, + invalidated_siblings: false, + } + } + + /// Whether the invalidation has invalidate the element itself. + pub fn has_invalidated_self(&self) -> bool { + self.invalidated_self + } + + /// Whether the invalidation has invalidate desendants. + pub fn has_invalidated_descendants(&self) -> bool { + self.invalidated_descendants + } + + /// Whether the invalidation has invalidate siblings. + pub fn has_invalidated_siblings(&self) -> bool { + self.invalidated_siblings + } +} + +impl<'a, 'b, 'c, E, P: 'a> TreeStyleInvalidator<'a, 'b, 'c, E, P> +where + 'b: 'a, + E: TElement, + P: InvalidationProcessor<'b, 'c, E>, +{ + /// Trivially constructs a new `TreeStyleInvalidator`. + pub fn new( + element: E, + stack_limit_checker: Option<&'a StackLimitChecker>, + processor: &'a mut P, + ) -> Self { + Self { + element, + stack_limit_checker, + processor, + _marker: std::marker::PhantomData, + } + } + + /// Perform the invalidation pass. + pub fn invalidate(mut self) -> InvalidationResult { + debug!("StyleTreeInvalidator::invalidate({:?})", self.element); + + let mut self_invalidations = InvalidationVector::new(); + let mut descendant_invalidations = DescendantInvalidationLists::default(); + let mut sibling_invalidations = InvalidationVector::new(); + + let mut invalidated_self = self.processor.collect_invalidations( + self.element, + &mut self_invalidations, + &mut descendant_invalidations, + &mut sibling_invalidations, + ); + + debug!("Collected invalidations (self: {}): ", invalidated_self); + debug!( + " > self: {}, {:?}", + self_invalidations.len(), + self_invalidations + ); + debug!(" > descendants: {:?}", descendant_invalidations); + debug!( + " > siblings: {}, {:?}", + sibling_invalidations.len(), + sibling_invalidations + ); + + let invalidated_self_from_collection = invalidated_self; + + invalidated_self |= self.process_descendant_invalidations( + &self_invalidations, + &mut descendant_invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + + if invalidated_self && !invalidated_self_from_collection { + self.processor.invalidated_self(self.element); + } + + let invalidated_descendants = self.invalidate_descendants(&descendant_invalidations); + let invalidated_siblings = self.invalidate_siblings(&mut sibling_invalidations); + + InvalidationResult { + invalidated_self, + invalidated_descendants, + invalidated_siblings, + } + } + + /// Go through later DOM siblings, invalidating style as needed using the + /// `sibling_invalidations` list. + /// + /// Returns whether any sibling's style or any sibling descendant's style + /// was invalidated. + fn invalidate_siblings(&mut self, sibling_invalidations: &mut InvalidationVector<'b>) -> bool { + if sibling_invalidations.is_empty() { + return false; + } + + let mut current = self + .processor + .sibling_traversal_map() + .next_sibling_for(&self.element); + let mut any_invalidated = false; + + while let Some(sibling) = current { + let mut sibling_invalidator = + TreeStyleInvalidator::new(sibling, self.stack_limit_checker, self.processor); + + let mut invalidations_for_descendants = DescendantInvalidationLists::default(); + let invalidated_sibling = sibling_invalidator.process_sibling_invalidations( + &mut invalidations_for_descendants, + sibling_invalidations, + ); + + if invalidated_sibling { + sibling_invalidator + .processor + .invalidated_sibling(sibling, self.element); + } + + any_invalidated |= invalidated_sibling; + + any_invalidated |= + sibling_invalidator.invalidate_descendants(&invalidations_for_descendants); + + if sibling_invalidations.is_empty() { + break; + } + + current = self + .processor + .sibling_traversal_map() + .next_sibling_for(&sibling); + } + + any_invalidated + } + + fn invalidate_pseudo_element_or_nac( + &mut self, + child: E, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut sibling_invalidations = InvalidationVector::new(); + + let result = self.invalidate_child( + child, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + + // Roots of NAC subtrees can indeed generate sibling invalidations, but + // they can be just ignored, since they have no siblings. + // + // Note that we can end up testing selectors that wouldn't end up + // matching due to this being NAC, like those coming from document + // rules, but we overinvalidate instead of checking this. + + result + } + + /// Invalidate a child and recurse down invalidating its descendants if + /// needed. + fn invalidate_child( + &mut self, + child: E, + invalidations: &[Invalidation<'b>], + sibling_invalidations: &mut InvalidationVector<'b>, + descendant_invalidation_kind: DescendantInvalidationKind, + ) -> bool { + let mut invalidations_for_descendants = DescendantInvalidationLists::default(); + + let mut invalidated_child = false; + let invalidated_descendants = { + let mut child_invalidator = + TreeStyleInvalidator::new(child, self.stack_limit_checker, self.processor); + + invalidated_child |= child_invalidator.process_sibling_invalidations( + &mut invalidations_for_descendants, + sibling_invalidations, + ); + + invalidated_child |= child_invalidator.process_descendant_invalidations( + invalidations, + &mut invalidations_for_descendants, + sibling_invalidations, + descendant_invalidation_kind, + ); + + if invalidated_child { + child_invalidator.processor.invalidated_self(child); + } + + child_invalidator.invalidate_descendants(&invalidations_for_descendants) + }; + + // The child may not be a flattened tree child of the current element, + // but may be arbitrarily deep. + // + // Since we keep the traversal flags in terms of the flattened tree, + // we need to propagate it as appropriate. + if invalidated_child || invalidated_descendants { + self.processor.invalidated_descendants(self.element, child); + } + + invalidated_child || invalidated_descendants + } + + fn invalidate_nac(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + let mut any_nac_root = false; + + let element = self.element; + element.each_anonymous_content_child(|nac| { + any_nac_root |= self.invalidate_pseudo_element_or_nac(nac, invalidations); + }); + + any_nac_root + } + + // NB: It's important that this operates on DOM children, which is what + // selector-matching operates on. + fn invalidate_dom_descendants_of( + &mut self, + parent: E::ConcreteNode, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut any_descendant = false; + + let mut sibling_invalidations = InvalidationVector::new(); + for child in parent.dom_children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_descendant |= self.invalidate_child( + child, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Dom, + ); + } + + any_descendant + } + + fn invalidate_parts_in_shadow_tree( + &mut self, + shadow: <E::ConcreteNode as TNode>::ConcreteShadowRoot, + invalidations: &[Invalidation<'b>], + ) -> bool { + debug_assert!(!invalidations.is_empty()); + + let mut any = false; + let mut sibling_invalidations = InvalidationVector::new(); + + for node in shadow.as_node().dom_descendants() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if element.has_part_attr() { + any |= self.invalidate_child( + element, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Part, + ); + debug_assert!( + sibling_invalidations.is_empty(), + "::part() shouldn't have sibling combinators to the right, \ + this makes no sense! {:?}", + sibling_invalidations + ); + } + + if let Some(shadow) = element.shadow_root() { + if element.exports_any_part() { + any |= self.invalidate_parts_in_shadow_tree(shadow, invalidations) + } + } + } + + any + } + + fn invalidate_parts(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + let shadow = match self.element.shadow_root() { + Some(s) => s, + None => return false, + }; + + self.invalidate_parts_in_shadow_tree(shadow, invalidations) + } + + fn invalidate_slotted_elements(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + let slot = self.element; + self.invalidate_slotted_elements_in_slot(slot, invalidations) + } + + fn invalidate_slotted_elements_in_slot( + &mut self, + slot: E, + invalidations: &[Invalidation<'b>], + ) -> bool { + let mut any = false; + + let mut sibling_invalidations = InvalidationVector::new(); + for node in slot.slotted_nodes() { + let element = match node.as_element() { + Some(e) => e, + None => continue, + }; + + if element.is_html_slot_element() { + any |= self.invalidate_slotted_elements_in_slot(element, invalidations); + } else { + any |= self.invalidate_child( + element, + invalidations, + &mut sibling_invalidations, + DescendantInvalidationKind::Slotted, + ); + } + + debug_assert!( + sibling_invalidations.is_empty(), + "::slotted() shouldn't have sibling combinators to the right, \ + this makes no sense! {:?}", + sibling_invalidations + ); + } + + any + } + + fn invalidate_non_slotted_descendants(&mut self, invalidations: &[Invalidation<'b>]) -> bool { + if invalidations.is_empty() { + return false; + } + + if self.processor.light_tree_only() { + let node = self.element.as_node(); + return self.invalidate_dom_descendants_of(node, invalidations); + } + + let mut any_descendant = false; + + // NOTE(emilio): This is only needed for Shadow DOM to invalidate + // correctly on :host(..) changes. Instead of doing this, we could add + // a third kind of invalidation list that walks shadow root children, + // but it's not clear it's worth it. + // + // Also, it's needed as of right now for document state invalidation, + // where we rely on iterating every element that ends up in the composed + // doc, but we could fix that invalidating per subtree. + if let Some(root) = self.element.shadow_root() { + any_descendant |= self.invalidate_dom_descendants_of(root.as_node(), invalidations); + } + + if let Some(marker) = self.element.marker_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(marker, invalidations); + } + + if let Some(before) = self.element.before_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(before, invalidations); + } + + let node = self.element.as_node(); + any_descendant |= self.invalidate_dom_descendants_of(node, invalidations); + + if let Some(after) = self.element.after_pseudo_element() { + any_descendant |= self.invalidate_pseudo_element_or_nac(after, invalidations); + } + + any_descendant |= self.invalidate_nac(invalidations); + + any_descendant + } + + /// Given the descendant invalidation lists, go through the current + /// element's descendants, and invalidate style on them. + fn invalidate_descendants(&mut self, invalidations: &DescendantInvalidationLists<'b>) -> bool { + if invalidations.is_empty() { + return false; + } + + debug!( + "StyleTreeInvalidator::invalidate_descendants({:?})", + self.element + ); + debug!(" > {:?}", invalidations); + + let should_process = self.processor.should_process_descendants(self.element); + + if !should_process { + return false; + } + + if let Some(checker) = self.stack_limit_checker { + if checker.limit_exceeded() { + self.processor.recursion_limit_exceeded(self.element); + return true; + } + } + + let mut any_descendant = false; + + any_descendant |= self.invalidate_non_slotted_descendants(&invalidations.dom_descendants); + any_descendant |= self.invalidate_slotted_elements(&invalidations.slotted_descendants); + any_descendant |= self.invalidate_parts(&invalidations.parts); + + any_descendant + } + + /// Process the given sibling invalidations coming from our previous + /// sibling. + /// + /// The sibling invalidations are somewhat special because they can be + /// modified on the fly. New invalidations may be added and removed. + /// + /// In particular, all descendants get the same set of invalidations from + /// the parent, but the invalidations from a given sibling depend on the + /// ones we got from the previous one. + /// + /// Returns whether invalidated the current element's style. + fn process_sibling_invalidations( + &mut self, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + let mut i = 0; + let mut new_sibling_invalidations = InvalidationVector::new(); + let mut invalidated_self = false; + + while i < sibling_invalidations.len() { + let result = self.process_invalidation( + &sibling_invalidations[i], + descendant_invalidations, + &mut new_sibling_invalidations, + InvalidationKind::Sibling, + ); + + invalidated_self |= result.invalidated_self; + sibling_invalidations[i].matched_by_any_previous |= result.matched; + if sibling_invalidations[i].effective_for_next() { + i += 1; + } else { + sibling_invalidations.remove(i); + } + } + + sibling_invalidations.extend(new_sibling_invalidations.drain(..)); + invalidated_self + } + + /// Process a given invalidation list coming from our parent, + /// adding to `descendant_invalidations` and `sibling_invalidations` as + /// needed. + /// + /// Returns whether our style was invalidated as a result. + fn process_descendant_invalidations( + &mut self, + invalidations: &[Invalidation<'b>], + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + descendant_invalidation_kind: DescendantInvalidationKind, + ) -> bool { + let mut invalidated = false; + + for invalidation in invalidations { + let result = self.process_invalidation( + invalidation, + descendant_invalidations, + sibling_invalidations, + InvalidationKind::Descendant(descendant_invalidation_kind), + ); + + invalidated |= result.invalidated_self; + if invalidation.effective_for_next() { + let mut invalidation = invalidation.clone(); + invalidation.matched_by_any_previous |= result.matched; + debug_assert_eq!( + descendant_invalidation_kind, + DescendantInvalidationKind::Dom, + "Slotted or part invalidations don't propagate." + ); + descendant_invalidations.dom_descendants.push(invalidation); + } + } + + invalidated + } + + /// Processes a given invalidation, potentially invalidating the style of + /// the current element. + /// + /// Returns whether invalidated the style of the element, and whether the + /// invalidation should be effective to subsequent siblings or descendants + /// down in the tree. + fn process_invalidation( + &mut self, + invalidation: &Invalidation<'b>, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + invalidation_kind: InvalidationKind, + ) -> SingleInvalidationResult { + debug!( + "TreeStyleInvalidator::process_invalidation({:?}, {:?}, {:?})", + self.element, invalidation, invalidation_kind + ); + + let matching_result = { + let context = self.processor.matching_context(); + context.current_host = invalidation.scope; + + matches_compound_selector_from( + &invalidation.dependency.selector, + invalidation.offset, + context, + &self.element, + ) + }; + + let next_invalidation = match matching_result { + CompoundSelectorMatchingResult::NotMatched => { + return SingleInvalidationResult { + invalidated_self: false, + matched: false, + } + }, + CompoundSelectorMatchingResult::FullyMatched => { + debug!(" > Invalidation matched completely"); + // We matched completely. If we're an inner selector now we need + // to go outside our selector and carry on invalidating. + let mut cur_dependency = invalidation.dependency; + loop { + cur_dependency = match cur_dependency.parent { + None => { + return SingleInvalidationResult { + invalidated_self: true, + matched: true, + } + }, + Some(ref p) => { + let invalidation_kind = p.invalidation_kind(); + match invalidation_kind { + DependencyInvalidationKind::Normal(_) => &**p, + DependencyInvalidationKind::Relative(kind) => { + self.processor.found_relative_selector_invalidation( + self.element, + kind, + &**p, + ); + return SingleInvalidationResult { + invalidated_self: false, + matched: true, + }; + }, + } + }, + }; + + debug!(" > Checking outer dependency {:?}", cur_dependency); + + // The inner selector changed, now check if the full + // previous part of the selector did, before keeping + // checking for descendants. + if !self + .processor + .check_outer_dependency(cur_dependency, self.element) + { + return SingleInvalidationResult { + invalidated_self: false, + matched: false, + }; + } + + if cur_dependency.normal_invalidation_kind() == + NormalDependencyInvalidationKind::Element + { + continue; + } + + debug!(" > Generating invalidation"); + break Invalidation::new(cur_dependency, invalidation.scope); + } + }, + CompoundSelectorMatchingResult::Matched { + next_combinator_offset, + } => Invalidation { + dependency: invalidation.dependency, + scope: invalidation.scope, + offset: next_combinator_offset + 1, + matched_by_any_previous: false, + }, + }; + + debug_assert_ne!( + next_invalidation.offset, 0, + "Rightmost selectors shouldn't generate more invalidations", + ); + + let mut invalidated_self = false; + let next_combinator = next_invalidation + .dependency + .selector + .combinator_at_parse_order(next_invalidation.offset - 1); + + if matches!(next_combinator, Combinator::PseudoElement) && + self.processor.invalidates_on_pseudo_element() + { + // We need to invalidate the element whenever pseudos change, for + // two reasons: + // + // * Eager pseudo styles are stored as part of the originating + // element's computed style. + // + // * Lazy pseudo-styles might be cached on the originating + // element's pseudo-style cache. + // + // This could be more fine-grained (perhaps with a RESTYLE_PSEUDOS + // hint?). + // + // Note that we'll also restyle the pseudo-element because it would + // match this invalidation. + // + // FIXME: For non-element-backed pseudos this is still not quite + // correct. For example for ::selection even though we invalidate + // the style properly there's nothing that triggers a repaint + // necessarily. Though this matches old Gecko behavior, and the + // ::selection implementation needs to change significantly anyway + // to implement https://github.com/w3c/csswg-drafts/issues/2474 for + // example. + invalidated_self = true; + } + + debug!( + " > Invalidation matched, next: {:?}, ({:?})", + next_invalidation, next_combinator + ); + + let next_invalidation_kind = next_invalidation.kind(); + + // We can skip pushing under some circumstances, and we should + // because otherwise the invalidation list could grow + // exponentially. + // + // * First of all, both invalidations need to be of the same + // kind. This is because of how we propagate them going to + // the right of the tree for sibling invalidations and going + // down the tree for children invalidations. A sibling + // invalidation that ends up generating a children + // invalidation ends up (correctly) in five different lists, + // not in the same list five different times. + // + // * Then, the invalidation needs to be matched by a previous + // ancestor/sibling, in order to know that this invalidation + // has been generated already. + // + // * Finally, the new invalidation needs to be + // `effective_for_next()`, in order for us to know that it is + // still in the list, since we remove the dependencies that + // aren't from the lists for our children / siblings. + // + // To go through an example, let's imagine we are processing a + // dom subtree like: + // + // <div><address><div><div/></div></address></div> + // + // And an invalidation list with a single invalidation like: + // + // [div div div] + // + // When we process the invalidation list for the outer div, we + // match it, and generate a `div div` invalidation, so for the + // <address> child we have: + // + // [div div div, div div] + // + // With the first of them marked as `matched`. + // + // When we process the <address> child, we don't match any of + // them, so both invalidations go untouched to our children. + // + // When we process the second <div>, we match _both_ + // invalidations. + // + // However, when matching the first, we can tell it's been + // matched, and not push the corresponding `div div` + // invalidation, since we know it's necessarily already on the + // list. + // + // Thus, without skipping the push, we'll arrive to the + // innermost <div> with: + // + // [div div div, div div, div div, div] + // + // While skipping it, we won't arrive here with duplicating + // dependencies: + // + // [div div div, div div, div] + // + let can_skip_pushing = next_invalidation_kind == invalidation_kind && + invalidation.matched_by_any_previous && + next_invalidation.effective_for_next(); + + if can_skip_pushing { + debug!( + " > Can avoid push, since the invalidation had \ + already been matched before" + ); + } else { + match next_invalidation_kind { + InvalidationKind::Descendant(DescendantInvalidationKind::Dom) => { + descendant_invalidations + .dom_descendants + .push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Part) => { + descendant_invalidations.parts.push(next_invalidation); + }, + InvalidationKind::Descendant(DescendantInvalidationKind::Slotted) => { + descendant_invalidations + .slotted_descendants + .push(next_invalidation); + }, + InvalidationKind::Sibling => { + sibling_invalidations.push(next_invalidation); + }, + } + } + + SingleInvalidationResult { + invalidated_self, + matched: true, + } + } +} diff --git a/servo/components/style/invalidation/element/mod.rs b/servo/components/style/invalidation/element/mod.rs new file mode 100644 index 0000000000..0ddb9f1863 --- /dev/null +++ b/servo/components/style/invalidation/element/mod.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidation of element styles due to attribute or style changes. + +pub mod document_state; +pub mod element_wrapper; +pub mod invalidation_map; +pub mod invalidator; +pub mod relative_selector; +pub mod restyle_hints; +pub mod state_and_attributes; diff --git a/servo/components/style/invalidation/element/relative_selector.rs b/servo/components/style/invalidation/element/relative_selector.rs new file mode 100644 index 0000000000..ccb48e349f --- /dev/null +++ b/servo/components/style/invalidation/element/relative_selector.rs @@ -0,0 +1,1164 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidation of element styles relative selectors. + +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::gecko_bindings::structs::ServoElementSnapshotTable; +use crate::invalidation::element::element_wrapper::ElementWrapper; +use crate::invalidation::element::invalidation_map::{ + Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind, + RelativeDependencyInvalidationKind, RelativeSelectorInvalidationMap, +}; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, Invalidation, InvalidationProcessor, InvalidationResult, + InvalidationVector, SiblingTraversalMap, TreeStyleInvalidator, +}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::invalidation::element::state_and_attributes::{ + check_dependency, dependency_may_be_relevant, invalidated_descendants, invalidated_self, + invalidated_sibling, push_invalidation, should_process_descendants, +}; +use crate::stylist::{CascadeData, Stylist}; +use dom::ElementState; +use fxhash::FxHashMap; +use selectors::matching::{ + matches_compound_selector_from, matches_selector, CompoundSelectorMatchingResult, + ElementSelectorFlags, MatchingContext, MatchingForInvalidation, MatchingMode, + NeedsSelectorFlags, QuirksMode, SelectorCaches, VisitedHandlingMode, +}; +use selectors::parser::{Combinator, SelectorKey}; +use selectors::OpaqueElement; +use smallvec::SmallVec; +use std::ops::DerefMut; + +/// Kind of DOM mutation this relative selector invalidation is being carried out in. +#[derive(Clone, Copy)] +pub enum DomMutationOperation { + /// Insertion operation, can cause side effect, but presumed already happened. + Insert, + /// Append operation, cannot cause side effect. + Append, + /// Removal operation, can cause side effect, but presumed already happened. Sibling relationships are destroyed. + Remove, + /// Invalidating for side effect of a DOM operation, for the previous sibling. + SideEffectPrevSibling, + /// Invalidating for side effect of a DOM operation, for the next sibling. + SideEffectNextSibling, +} + +impl DomMutationOperation { + fn accept<E: TElement>(&self, d: &Dependency, e: E) -> bool { + match self { + Self::Insert | Self::Append | Self::Remove => { + e.relative_selector_search_direction().is_some() + }, + // `:has(+ .a + .b)` with `.anchor + .a + .remove + .b` - `.a` would be present + // in the search path. + Self::SideEffectPrevSibling => { + e.relative_selector_search_direction().is_some() && + d.right_combinator_is_next_sibling() + }, + // If an element is being removed and would cause next-sibling match to happen, + // e.g. `:has(+ .a)` with `.anchor + .remove + .a`, `.a` isn't yet searched + // for relative selector matching. + Self::SideEffectNextSibling => d.dependency_is_relative_with_single_next_sibling(), + } + } + + fn is_side_effect(&self) -> bool { + match self { + Self::Insert | Self::Append | Self::Remove => false, + Self::SideEffectPrevSibling | Self::SideEffectNextSibling => true, + } + } +} + +/// Context required to try and optimize away relative dependencies. +struct OptimizationContext<'a, E: TElement> { + sibling_traversal_map: &'a SiblingTraversalMap<E>, + quirks_mode: QuirksMode, + operation: DomMutationOperation, +} + +impl<'a, E: TElement> OptimizationContext<'a, E> { + fn can_be_ignored( + &self, + is_subtree: bool, + element: E, + host: Option<OpaqueElement>, + dependency: &Dependency, + ) -> bool { + if is_subtree { + // Subtree elements don't have unaffected sibling to look at. + return false; + } + debug_assert!( + matches!( + dependency.invalidation_kind(), + DependencyInvalidationKind::Relative(..) + ), + "Non-relative selector being evaluated for optimization" + ); + // This optimization predecates on the fact that there may be a sibling that can readily + // "take over" this element. + let sibling = match self.sibling_traversal_map.prev_sibling_for(&element) { + None => { + if matches!(self.operation, DomMutationOperation::Append) { + return false; + } + match self.sibling_traversal_map.next_sibling_for(&element) { + Some(s) => s, + None => return false, + } + }, + Some(s) => s, + }; + { + // Run through the affected compund. + let mut iter = dependency.selector.iter_from(dependency.selector_offset); + while let Some(c) = iter.next() { + if c.has_indexed_selector_in_subject() { + // We do not calculate indices during invalidation as they're wasteful - as a side effect, + // such selectors always return true, breaking this optimization. Note that we only check + // this compound only because the check to skip compares against this element's sibling. + // i.e. Given `:has(:nth-child(2) .foo)`, we'd try to find `.foo`'s sibling, which + // shares `:nth-child` up the selector. + return false; + } + } + } + let is_rightmost = dependency.selector_offset == 0; + if !is_rightmost { + let combinator = dependency + .selector + .combinator_at_match_order(dependency.selector_offset - 1); + if combinator.is_ancestor() { + // We can safely ignore these, since we're about to traverse the + // rest of the affected tree anyway to find the rightmost invalidated element. + return true; + } + if combinator.is_sibling() && matches!(self.operation, DomMutationOperation::Append) { + // If we're in the subtree, same argument applies as ancestor combinator case. + // If we're at the top of the DOM tree being mutated, we can ignore it if the + // operation is append - we know we'll cover all the later siblings and their descendants. + return true; + } + } + let mut caches = SelectorCaches::default(); + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + None, + &mut caches, + self.quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + matching_context.current_host = host; + let sibling_matches = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + &sibling, + &mut matching_context, + ); + if sibling_matches { + // Remember that at this point, we know that the combinator to the right of this + // compound is a sibling combinator. Effectively, we've found a standin for the + // element we're mutating. + // e.g. Given `:has(... .a ~ .b ...)`, we're the mutating element matching `... .a`, + // if we find a sibling that matches the `... .a`, it can stand in for us. + debug_assert!(dependency.parent.is_some(), "No relative selector outer dependency?"); + return dependency.parent.as_ref().map_or(false, |par| { + // ... However, if the standin sibling can be the anchor, we can't skip it, since + // that sibling should be invlidated to become the anchor. + !matches_selector( + &par.selector, + par.selector_offset, + None, + &sibling, + &mut matching_context + ) + }); + } + // Ok, there's no standin element - but would this element have matched the upstream + // selector anyway? If we don't, either the match exists somewhere far from us + // (In which case our mutation doesn't really matter), or it doesn't exist at all, + // so we can just skip the invalidation. + let (combinator, prev_offset) = { + let mut iter = dependency.selector.iter_from(dependency.selector_offset); + let mut o = dependency.selector_offset; + while iter.next().is_some() { + o += 1; + } + let combinator = iter.next_sequence(); + o += 1; + debug_assert!( + combinator.is_some(), + "Should at least see a relative combinator" + ); + (combinator.unwrap(), o) + }; + if combinator.is_sibling() { + if prev_offset >= dependency.selector.len() - 1 { + // Hit the relative combinator - we don't have enough information to + // see if there's going to be a downstream match. + return false; + } + if matches!(self.operation, DomMutationOperation::Remove) { + // This is sad :( The sibling relation of a removed element is lost, and we don't + // propagate sibling traversal map to selector matching context, so we need to do + // manual matching here. TODO(dshin): Worth changing selector matching for this? + + // Try matching this compound, then... + // Note: We'll not hit the leftmost sequence (Since we would have returned early + // if we'd hit the relative selector anchor). + if matches!( + matches_compound_selector_from( + &dependency.selector, + dependency.selector.len() - prev_offset + 1, + &mut matching_context, + &element + ), + CompoundSelectorMatchingResult::NotMatched + ) { + return true; + } + + // ... Match the rest of the selector, manually traversing. + let mut prev_sibling = self.sibling_traversal_map.prev_sibling_for(&element); + while let Some(sib) = prev_sibling { + if matches_selector( + &dependency.selector, + prev_offset, + None, + &sib, + &mut matching_context, + ) { + return false; + } + if matches!(combinator, Combinator::NextSibling) { + break; + } + prev_sibling = self.sibling_traversal_map.prev_sibling_for(&sib); + } + return true; + } + } + !matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + &element, + &mut matching_context, + ) + } +} + +/// Overall invalidator for handling relative selector invalidations. +pub struct RelativeSelectorInvalidator<'a, 'b, E> +where + E: TElement + 'a, +{ + /// Element triggering the invalidation. + pub element: E, + /// Quirks mode of the current invalidation. + pub quirks_mode: QuirksMode, + /// Snapshot containing changes to invalidate against. + /// Can be None if it's a DOM mutation. + pub snapshot_table: Option<&'b ServoElementSnapshotTable>, + /// Callback to trigger when the subject element is invalidated. + pub invalidated: fn(E, &InvalidationResult), + /// The traversal map that should be used to process invalidations. + pub sibling_traversal_map: SiblingTraversalMap<E>, + /// Marker for 'a lifetime. + pub _marker: ::std::marker::PhantomData<&'a ()>, +} + +struct RelativeSelectorInvalidation<'a> { + host: Option<OpaqueElement>, + kind: RelativeDependencyInvalidationKind, + dependency: &'a Dependency, +} + +type ElementDependencies<'a> = SmallVec<[(Option<OpaqueElement>, &'a Dependency); 1]>; +type Dependencies<'a, E> = SmallVec<[(E, ElementDependencies<'a>); 1]>; +type AlreadyInvalidated<'a, E> = SmallVec<[(E, Option<OpaqueElement>, &'a Dependency); 2]>; + +/// Interface for collecting relative selector dependencies. +pub struct RelativeSelectorDependencyCollector<'a, E> +where + E: TElement, +{ + /// Dependencies that need to run through the normal invalidation that may generate + /// a relative selector invalidation. + dependencies: FxHashMap<E, ElementDependencies<'a>>, + /// Dependencies that created an invalidation right away. + invalidations: AlreadyInvalidated<'a, E>, + /// The top element in the subtree being invalidated. + top: E, + /// Optional context that will be used to try and skip invalidations + /// by running selector matches. + optimization_context: Option<OptimizationContext<'a, E>>, +} + +type Invalidations<'a> = SmallVec<[RelativeSelectorInvalidation<'a>; 1]>; +type InnerInvalidations<'a, E> = SmallVec<[(E, RelativeSelectorInvalidation<'a>); 1]>; + +struct ToInvalidate<'a, E: TElement + 'a> { + /// Dependencies to run through normal invalidator. + dependencies: Dependencies<'a, E>, + /// Dependencies already invalidated. + invalidations: Invalidations<'a>, +} + +impl<'a, E: TElement + 'a> Default for ToInvalidate<'a, E> { + fn default() -> Self { + Self { + dependencies: Dependencies::default(), + invalidations: Invalidations::default(), + } + } +} + +fn dependency_selectors_match(a: &Dependency, b: &Dependency) -> bool { + if a.invalidation_kind() != b.invalidation_kind() { + return false; + } + if SelectorKey::new(&a.selector) != SelectorKey::new(&b.selector) { + return false; + } + let mut a_parent = a.parent.as_ref(); + let mut b_parent = b.parent.as_ref(); + while let (Some(a_p), Some(b_p)) = (a_parent, b_parent) { + if SelectorKey::new(&a_p.selector) != SelectorKey::new(&b_p.selector) { + return false; + } + a_parent = a_p.parent.as_ref(); + b_parent = b_p.parent.as_ref(); + } + a_parent.is_none() && b_parent.is_none() +} + +impl<'a, E> RelativeSelectorDependencyCollector<'a, E> +where + E: TElement, +{ + fn new(top: E, optimization_context: Option<OptimizationContext<'a, E>>) -> Self { + Self { + dependencies: FxHashMap::default(), + invalidations: AlreadyInvalidated::default(), + top, + optimization_context, + } + } + + fn insert_invalidation( + &mut self, + element: E, + dependency: &'a Dependency, + host: Option<OpaqueElement>, + ) { + match self + .invalidations + .iter_mut() + .find(|(_, _, d)| dependency_selectors_match(dependency, d)) + { + Some((e, h, d)) => { + // Just keep one. + if d.selector_offset > dependency.selector_offset { + (*e, *h, *d) = (element, host, dependency); + } + }, + None => { + self.invalidations.push((element, host, dependency)); + }, + } + } + + /// Add this dependency, if it is unique (i.e. Different outer dependency or same outer dependency + /// but requires a different invalidation traversal). + pub fn add_dependency( + &mut self, + dependency: &'a Dependency, + element: E, + host: Option<OpaqueElement>, + ) { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(..) => { + self.dependencies + .entry(element) + .and_modify(|v| v.push((host, dependency))) + .or_default() + .push((host, dependency)); + }, + DependencyInvalidationKind::Relative(kind) => { + debug_assert!( + dependency.parent.is_some(), + "Orphaned inner relative selector?" + ); + if element != self.top && + matches!( + kind, + RelativeDependencyInvalidationKind::Parent | + RelativeDependencyInvalidationKind::PrevSibling | + RelativeDependencyInvalidationKind::EarlierSibling + ) + { + return; + } + self.insert_invalidation(element, dependency, host); + }, + }; + } + + /// Get the dependencies in a list format. + fn get(self) -> ToInvalidate<'a, E> { + let mut result = ToInvalidate::default(); + for (element, host, dependency) in self.invalidations { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => { + unreachable!("Inner selector in invalidation?") + }, + DependencyInvalidationKind::Relative(kind) => { + if let Some(context) = self.optimization_context.as_ref() { + if context.can_be_ignored(element != self.top, element, host, dependency) { + continue; + } + } + let dependency = dependency.parent.as_ref().unwrap(); + result.invalidations.push(RelativeSelectorInvalidation { + kind, + host, + dependency, + }); + // We move the invalidation up to the top of the subtree to avoid unnecessary traveral, but + // this means that we need to take ancestor-earlier sibling invalidations into account, as + // they'd look into earlier siblings of the top of the subtree as well. + if element != self.top && + matches!( + kind, + RelativeDependencyInvalidationKind::AncestorEarlierSibling | + RelativeDependencyInvalidationKind::AncestorPrevSibling + ) + { + result.invalidations.push(RelativeSelectorInvalidation { + kind: if matches!( + kind, + RelativeDependencyInvalidationKind::AncestorPrevSibling + ) { + RelativeDependencyInvalidationKind::PrevSibling + } else { + RelativeDependencyInvalidationKind::EarlierSibling + }, + host, + dependency, + }); + } + }, + }; + } + for (key, element_dependencies) in self.dependencies { + // At least for now, we don't try to optimize away dependencies emitted from nested selectors. + result.dependencies.push((key, element_dependencies)); + } + result + } + + fn collect_all_dependencies_for_element( + &mut self, + element: E, + scope: Option<OpaqueElement>, + quirks_mode: QuirksMode, + map: &'a RelativeSelectorInvalidationMap, + operation: DomMutationOperation, + ) { + element + .id() + .map(|v| match map.map.id_to_selector.get(v, quirks_mode) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }); + element.each_class(|v| match map.map.class_to_selector.get(v, quirks_mode) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }); + element.each_attr_name( + |v| match map.map.other_attribute_affecting_selectors.get(v) { + Some(v) => { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + }, + None => (), + }, + ); + let state = element.state(); + map.map.state_affecting_selectors.lookup_with_additional( + element, + quirks_mode, + None, + &[], + ElementState::empty(), + |dependency| { + if !dependency.state.intersects(state) { + return true; + } + if !operation.accept(&dependency.dep, element) { + return true; + } + self.add_dependency(&dependency.dep, element, scope); + true + }, + ); + + if let Some(v) = map.type_to_selector.get(element.local_name()) { + for dependency in v { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + } + + for dependency in &map.any_to_selector { + if !operation.accept(dependency, element) { + continue; + } + self.add_dependency(dependency, element, scope); + } + } + + fn is_empty(&self) -> bool { + self.invalidations.is_empty() && self.dependencies.is_empty() + } +} + +impl<'a, 'b, E> RelativeSelectorInvalidator<'a, 'b, E> +where + E: TElement + 'a, +{ + /// Gather relative selector dependencies for the given element, and invalidate as necessary. + #[inline(never)] + pub fn invalidate_relative_selectors_for_this<F>( + self, + stylist: &'a Stylist, + mut gather_dependencies: F, + ) where + F: FnMut( + &E, + Option<OpaqueElement>, + &'a CascadeData, + QuirksMode, + &mut RelativeSelectorDependencyCollector<'a, E>, + ), + { + let mut collector = RelativeSelectorDependencyCollector::new(self.element, None); + stylist.for_each_cascade_data_with_scope(self.element, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + gather_dependencies( + &self.element, + scope.map(|e| e.opaque()), + data, + self.quirks_mode, + &mut collector, + ); + }); + if collector.is_empty() { + return; + } + self.invalidate_from_dependencies(collector.get()); + } + + /// Gather relative selector dependencies for the given element (And its subtree) that mutated, and invalidate as necessary. + #[inline(never)] + pub fn invalidate_relative_selectors_for_dom_mutation( + self, + subtree: bool, + stylist: &'a Stylist, + inherited_search_path: ElementSelectorFlags, + operation: DomMutationOperation, + ) { + let mut collector = RelativeSelectorDependencyCollector::new( + self.element, + if operation.is_side_effect() { + None + } else { + Some(OptimizationContext { + sibling_traversal_map: &self.sibling_traversal_map, + quirks_mode: self.quirks_mode, + operation, + }) + }, + ); + let mut traverse_subtree = false; + self.element.apply_selector_flags(inherited_search_path); + stylist.for_each_cascade_data_with_scope(self.element, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + traverse_subtree |= map.needs_ancestors_traversal; + collector.collect_all_dependencies_for_element( + self.element, + scope.map(|e| e.opaque()), + self.quirks_mode, + map, + operation, + ); + }); + + if subtree && traverse_subtree { + for node in self.element.as_node().dom_descendants() { + let descendant = match node.as_element() { + Some(e) => e, + None => continue, + }; + descendant.apply_selector_flags(inherited_search_path); + stylist.for_each_cascade_data_with_scope(descendant, |data, scope| { + let map = data.relative_selector_invalidation_map(); + if !map.used { + return; + } + collector.collect_all_dependencies_for_element( + descendant, + scope.map(|e| e.opaque()), + self.quirks_mode, + map, + operation, + ); + }); + } + } + if collector.is_empty() { + return; + } + self.invalidate_from_dependencies(collector.get()); + } + + /// Carry out complete invalidation triggered by a relative selector invalidation. + fn invalidate_from_dependencies(&self, to_invalidate: ToInvalidate<'a, E>) { + for (element, dependencies) in to_invalidate.dependencies { + let mut selector_caches = SelectorCaches::default(); + let mut processor = RelativeSelectorInnerInvalidationProcessor::new( + self.quirks_mode, + self.snapshot_table, + &dependencies, + &mut selector_caches, + &self.sibling_traversal_map, + ); + TreeStyleInvalidator::new(element, None, &mut processor).invalidate(); + for (element, invalidation) in processor.take_invalidations() { + self.invalidate_upwards(element, &invalidation); + } + } + for invalidation in to_invalidate.invalidations { + self.invalidate_upwards(self.element, &invalidation); + } + } + + fn invalidate_upwards(&self, element: E, invalidation: &RelativeSelectorInvalidation<'a>) { + // This contains the main reason for why relative selector invalidation is handled + // separately - It travels ancestor and/or earlier sibling direction. + match invalidation.kind { + RelativeDependencyInvalidationKind::Parent => { + element.parent_element().map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + }, + RelativeDependencyInvalidationKind::Ancestors => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + self.handle_anchor(par, invalidation.dependency, invalidation.host); + parent = par.parent_element(); + } + }, + RelativeDependencyInvalidationKind::PrevSibling => { + self.sibling_traversal_map + .prev_sibling_for(&element) + .map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + }, + RelativeDependencyInvalidationKind::AncestorPrevSibling => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + par.prev_sibling_element().map(|e| { + if !Self::in_search_direction( + &e, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(e, invalidation.dependency, invalidation.host); + }); + parent = par.parent_element(); + } + }, + RelativeDependencyInvalidationKind::EarlierSibling => { + let mut sibling = self.sibling_traversal_map.prev_sibling_for(&element); + while let Some(sib) = sibling { + if !Self::in_search_direction( + &sib, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(sib, invalidation.dependency, invalidation.host); + sibling = sib.prev_sibling_element(); + } + }, + RelativeDependencyInvalidationKind::AncestorEarlierSibling => { + let mut parent = element.parent_element(); + while let Some(par) = parent { + if !Self::in_search_direction( + &par, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_ANCESTOR, + ) { + return; + } + let mut sibling = par.prev_sibling_element(); + while let Some(sib) = sibling { + if !Self::in_search_direction( + &sib, + ElementSelectorFlags::RELATIVE_SELECTOR_SEARCH_DIRECTION_SIBLING, + ) { + return; + } + self.handle_anchor(sib, invalidation.dependency, invalidation.host); + sibling = sib.prev_sibling_element(); + } + parent = par.parent_element(); + } + }, + } + } + + /// Is this element in the direction of the given relative selector search path? + fn in_search_direction(element: &E, desired: ElementSelectorFlags) -> bool { + if let Some(direction) = element.relative_selector_search_direction() { + direction.intersects(desired) + } else { + false + } + } + + /// Handle a potential relative selector anchor. + fn handle_anchor( + &self, + element: E, + outer_dependency: &Dependency, + host: Option<OpaqueElement>, + ) { + let is_rightmost = Self::is_subject(outer_dependency); + if (is_rightmost && + !element.has_selector_flags(ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR)) || + (!is_rightmost && + !element.has_selector_flags( + ElementSelectorFlags::ANCHORS_RELATIVE_SELECTOR_NON_SUBJECT, + )) + { + // If it was never a relative selector anchor, don't bother. + return; + } + let mut selector_caches = SelectorCaches::default(); + let matching_context = MatchingContext::<'_, E::Impl>::new_for_visited( + MatchingMode::Normal, + None, + &mut selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + self.quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + let mut data = match element.mutate_data() { + Some(data) => data, + None => return, + }; + let mut processor = RelativeSelectorOuterInvalidationProcessor { + element, + host, + data: data.deref_mut(), + dependency: &*outer_dependency, + matching_context, + traversal_map: &self.sibling_traversal_map, + }; + let result = TreeStyleInvalidator::new(element, None, &mut processor).invalidate(); + (self.invalidated)(element, &result); + } + + /// Does this relative selector dependency have its relative selector in the subject position? + fn is_subject(outer_dependency: &Dependency) -> bool { + debug_assert!( + matches!( + outer_dependency.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Outer selector of relative selector is relative?" + ); + + if let Some(p) = outer_dependency.parent.as_ref() { + if !Self::is_subject(p.as_ref()) { + // Not subject in outer selector. + return false; + } + } + outer_dependency.selector.is_rightmost(outer_dependency.selector_offset) + } +} + +/// Blindly invalidate everything outside of a relative selector. +/// Consider `:is(.a :has(.b) .c ~ .d) ~ .e .f`, where .b gets deleted. +/// Since the tree mutated, we cannot rely on snapshots. +pub struct RelativeSelectorOuterInvalidationProcessor<'a, 'b, E: TElement> { + /// Element being invalidated. + pub element: E, + /// The current shadow host, if any. + pub host: Option<OpaqueElement>, + /// Data for the element being invalidated. + pub data: &'a mut ElementData, + /// Dependency to be processed. + pub dependency: &'b Dependency, + /// Matching context to use for invalidation. + pub matching_context: MatchingContext<'a, E::Impl>, + /// Traversal map for this invalidation. + pub traversal_map: &'a SiblingTraversalMap<E>, +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'b, 'a, E> + for RelativeSelectorOuterInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, _dependency: &Dependency, _element: E) -> bool { + // At this point, we know a relative selector invalidated, and are ignoring them. + true + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + self.traversal_map + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'b>, + descendant_invalidations: &mut DescendantInvalidationLists<'b>, + sibling_invalidations: &mut InvalidationVector<'b>, + ) -> bool { + debug_assert_eq!(element, self.element); + debug_assert!( + self.matching_context.matching_for_invalidation(), + "Not matching for invalidation?" + ); + + // Ok, this element can potentially an anchor to the given dependency. + // Before we do the potentially-costly ancestor/earlier sibling traversal, + // See if it can actuall be an anchor by trying to match the "rest" of the selector + // outside and to the left of `:has` in question. + // e.g. Element under consideration can only be the anchor to `:has` in + // `.foo .bar ~ .baz:has()`, iff it matches `.foo .bar ~ .baz`. + let invalidated_self = { + let mut d = self.dependency; + loop { + debug_assert!( + matches!( + d.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Unexpected outer relative dependency" + ); + if !dependency_may_be_relevant(d, &element, false) { + break false; + } + if !matches_selector( + &d.selector, + d.selector_offset, + None, + &element, + self.matching_context(), + ) { + break false; + } + let invalidation_kind = d.normal_invalidation_kind(); + if matches!(invalidation_kind, NormalDependencyInvalidationKind::Element) { + if let Some(ref parent) = d.parent { + d = parent; + continue; + } + break true; + } + debug_assert_ne!(d.selector_offset, 0); + debug_assert_ne!(d.selector_offset, d.selector.len()); + let invalidation = Invalidation::new(&d, self.host); + break push_invalidation( + invalidation, + invalidation_kind, + descendant_invalidations, + sibling_invalidations + ); + } + }; + + if invalidated_self { + self.data.hint.insert(RestyleHint::RESTYLE_SELF); + } + invalidated_self + } + + fn should_process_descendants(&mut self, element: E) -> bool { + if element == self.element { + return should_process_descendants(&self.data); + } + + match element.borrow_data() { + Some(d) => should_process_descendants(&d), + None => return false, + } + } + + fn recursion_limit_exceeded(&mut self, _element: E) { + unreachable!("Unexpected recursion limit"); + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + debug_assert_ne!(element, self.element); + invalidated_self(element); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +/// Invalidation for the selector(s) inside a relative selector. +pub struct RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + /// Matching context to be used. + matching_context: MatchingContext<'b, E::Impl>, + /// Table of snapshots. + snapshot_table: Option<&'c ServoElementSnapshotTable>, + /// Incoming dependencies to be processed. + dependencies: &'c ElementDependencies<'a>, + /// Generated invalidations. + invalidations: InnerInvalidations<'a, E>, + /// Traversal map for this invalidation. + traversal_map: &'b SiblingTraversalMap<E>, +} + +impl<'a, 'b, 'c, E> RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + fn new( + quirks_mode: QuirksMode, + snapshot_table: Option<&'c ServoElementSnapshotTable>, + dependencies: &'c ElementDependencies<'a>, + selector_caches: &'b mut SelectorCaches, + traversal_map: &'b SiblingTraversalMap<E>, + ) -> Self { + let matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + quirks_mode, + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + Self { + matching_context, + snapshot_table, + dependencies, + invalidations: InnerInvalidations::default(), + traversal_map, + } + } + + fn note_dependency( + &mut self, + element: E, + scope: Option<OpaqueElement>, + dependency: &'a Dependency, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) { + match dependency.invalidation_kind() { + DependencyInvalidationKind::Normal(_) => (), + DependencyInvalidationKind::Relative(kind) => { + self.found_relative_selector_invalidation(element, kind, dependency); + return; + }, + } + if matches!( + dependency.normal_invalidation_kind(), + NormalDependencyInvalidationKind::Element + ) { + // Ok, keep heading outwards. + debug_assert!( + dependency.parent.is_some(), + "Orphaned inner selector dependency?" + ); + if let Some(parent) = dependency.parent.as_ref() { + self.note_dependency( + element, + scope, + parent, + descendant_invalidations, + sibling_invalidations, + ); + } + return; + } + let invalidation = Invalidation::new(&dependency, scope); + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Descendants => { + // Descendant invalidations are simplified due to pseudo-elements not being available within the relative selector. + descendant_invalidations.dom_descendants.push(invalidation) + }, + NormalDependencyInvalidationKind::Siblings => sibling_invalidations.push(invalidation), + _ => unreachable!(), + } + } + + /// Take the generated invalidations. + fn take_invalidations(self) -> InnerInvalidations<'a, E> { + self.invalidations + } +} + +impl<'a, 'b, 'c, E> InvalidationProcessor<'a, 'b, E> + for RelativeSelectorInnerInvalidationProcessor<'a, 'b, 'c, E> +where + E: TElement + 'a, +{ + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + if let Some(snapshot_table) = self.snapshot_table { + let wrapper = ElementWrapper::new(element, snapshot_table); + return check_dependency(dependency, &element, &wrapper, &mut self.matching_context); + } + // Just invalidate if we don't have a snapshot. + true + } + + fn matching_context(&mut self) -> &mut MatchingContext<'b, E::Impl> { + return &mut self.matching_context; + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + for (scope, dependency) in self.dependencies { + self.note_dependency( + element, + *scope, + dependency, + descendant_invalidations, + sibling_invalidations, + ) + } + false + } + + fn should_process_descendants(&mut self, _element: E) -> bool { + true + } + + fn recursion_limit_exceeded(&mut self, _element: E) { + unreachable!("Unexpected recursion limit"); + } + + // Don't do anything for normal invalidations. + fn invalidated_self(&mut self, _element: E) {} + fn invalidated_sibling(&mut self, _sibling: E, _of: E) {} + fn invalidated_descendants(&mut self, _element: E, _child: E) {} + + fn found_relative_selector_invalidation( + &mut self, + element: E, + kind: RelativeDependencyInvalidationKind, + dep: &'a Dependency, + ) { + debug_assert!(dep.parent.is_some(), "Orphaned inners selector?"); + if element.relative_selector_search_direction().is_none() { + return; + } + self.invalidations.push(( + element, + RelativeSelectorInvalidation { + host: self.matching_context.current_host, + kind, + dependency: dep.parent.as_ref().unwrap(), + }, + )); + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + &self.traversal_map + } +} diff --git a/servo/components/style/invalidation/element/restyle_hints.rs b/servo/components/style/invalidation/element/restyle_hints.rs new file mode 100644 index 0000000000..fe89636e88 --- /dev/null +++ b/servo/components/style/invalidation/element/restyle_hints.rs @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Restyle hints: an optimization to avoid unnecessarily matching selectors. + +use crate::traversal_flags::TraversalFlags; + +bitflags! { + /// The kind of restyle we need to do for a given element. + #[repr(C)] + #[derive(Clone, Copy, Debug)] + pub struct RestyleHint: u16 { + /// Do a selector match of the element. + const RESTYLE_SELF = 1 << 0; + + /// Do a selector match of the element's pseudo-elements. Always to be combined with + /// RESTYLE_SELF. + const RESTYLE_PSEUDOS = 1 << 1; + + /// Do a selector match if the element is a pseudo-element. + const RESTYLE_SELF_IF_PSEUDO = 1 << 2; + + /// Do a selector match of the element's descendants. + const RESTYLE_DESCENDANTS = 1 << 3; + + /// Recascade the current element. + const RECASCADE_SELF = 1 << 4; + + /// Recascade the current element if it inherits any reset style. + const RECASCADE_SELF_IF_INHERIT_RESET_STYLE = 1 << 5; + + /// Recascade all descendant elements. + const RECASCADE_DESCENDANTS = 1 << 6; + + /// Replace the style data coming from CSS transitions without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_TRANSITIONS = 1 << 7; + + /// Replace the style data coming from CSS animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_CSS_ANIMATIONS = 1 << 8; + + /// Don't re-run selector-matching on the element, only the style + /// attribute has changed, and this change didn't have any other + /// dependencies. + const RESTYLE_STYLE_ATTRIBUTE = 1 << 9; + + /// Replace the style data coming from SMIL animations without updating + /// any other style data. This hint is only processed in animation-only + /// traversal which is prior to normal traversal. + const RESTYLE_SMIL = 1 << 10; + } +} + +impl RestyleHint { + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be fully restyled. + pub fn restyle_subtree() -> Self { + RestyleHint::RESTYLE_SELF | RestyleHint::RESTYLE_DESCENDANTS + } + + /// Creates a new `RestyleHint` indicating that the current element and all + /// its descendants must be recascaded. + pub fn recascade_subtree() -> Self { + RestyleHint::RECASCADE_SELF | RestyleHint::RECASCADE_DESCENDANTS + } + + /// Returns whether this hint invalidates the element and all its + /// descendants. + pub fn contains_subtree(&self) -> bool { + self.contains(Self::restyle_subtree()) + } + + /// Returns whether we'll recascade all of the descendants. + pub fn will_recascade_subtree(&self) -> bool { + self.contains_subtree() || self.contains(Self::recascade_subtree()) + } + + /// Returns whether we need to restyle this element. + pub fn has_non_animation_invalidations(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Propagates this restyle hint to a child element. + pub fn propagate(&mut self, traversal_flags: &TraversalFlags) -> Self { + use std::mem; + + // In the middle of an animation only restyle, we don't need to + // propagate any restyle hints, and we need to remove ourselves. + if traversal_flags.for_animation_only() { + self.remove_animation_hints(); + return Self::empty(); + } + + debug_assert!( + !self.has_animation_hint(), + "There should not be any animation restyle hints \ + during normal traversal" + ); + + // Else we should clear ourselves, and return the propagated hint. + mem::replace(self, Self::empty()).propagate_for_non_animation_restyle() + } + + /// Returns a new `RestyleHint` appropriate for children of the current element. + fn propagate_for_non_animation_restyle(&self) -> Self { + if self.contains(RestyleHint::RESTYLE_DESCENDANTS) { + return Self::restyle_subtree(); + } + let mut result = Self::empty(); + if self.contains(RestyleHint::RESTYLE_PSEUDOS) { + result |= Self::RESTYLE_SELF_IF_PSEUDO; + } + if self.contains(RestyleHint::RECASCADE_DESCENDANTS) { + result |= Self::recascade_subtree(); + } + result + } + + /// Returns a hint that contains all the replacement hints. + pub fn replacements() -> Self { + RestyleHint::RESTYLE_STYLE_ATTRIBUTE | Self::for_animations() + } + + /// The replacements for the animation cascade levels. + #[inline] + pub fn for_animations() -> Self { + RestyleHint::RESTYLE_SMIL | + RestyleHint::RESTYLE_CSS_ANIMATIONS | + RestyleHint::RESTYLE_CSS_TRANSITIONS + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint(&self) -> bool { + self.intersects(Self::for_animations()) + } + + /// Returns whether the hint specifies that an animation cascade level must + /// be replaced. + #[inline] + pub fn has_animation_hint_or_recascade(&self) -> bool { + self.intersects( + Self::for_animations() | + Self::RECASCADE_SELF | + Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE, + ) + } + + /// Returns whether the hint specifies some restyle work other than an + /// animation cascade level replacement. + #[inline] + pub fn has_non_animation_hint(&self) -> bool { + !(*self & !Self::for_animations()).is_empty() + } + + /// Returns whether the hint specifies that some cascade levels must be + /// replaced. + #[inline] + pub fn has_replacements(&self) -> bool { + self.intersects(Self::replacements()) + } + + /// Removes all of the animation-related hints. + #[inline] + pub fn remove_animation_hints(&mut self) { + self.remove(Self::for_animations()); + + // While RECASCADE_SELF is not animation-specific, we only ever add and process it during + // traversal. If we are here, removing animation hints, then we are in an animation-only + // traversal, and we know that any RECASCADE_SELF flag must have been set due to changes in + // inherited values after restyling for animations, and thus we want to remove it so that + // we don't later try to restyle the element during a normal restyle. + // (We could have separate RECASCADE_SELF_NORMAL and RECASCADE_SELF_ANIMATIONS flags to + // make it clear, but this isn't currently necessary.) + self.remove(Self::RECASCADE_SELF | Self::RECASCADE_SELF_IF_INHERIT_RESET_STYLE); + } +} + +impl Default for RestyleHint { + fn default() -> Self { + Self::empty() + } +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(RestyleHint); diff --git a/servo/components/style/invalidation/element/state_and_attributes.rs b/servo/components/style/invalidation/element/state_and_attributes.rs new file mode 100644 index 0000000000..1c58cddf1e --- /dev/null +++ b/servo/components/style/invalidation/element/state_and_attributes.rs @@ -0,0 +1,601 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An invalidation processor for style changes due to state and attribute +//! changes. + +use crate::context::SharedStyleContext; +use crate::data::ElementData; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::invalidation_map::*; +use crate::invalidation::element::invalidator::{ + DescendantInvalidationLists, InvalidationVector, SiblingTraversalMap, +}; +use crate::invalidation::element::invalidator::{Invalidation, InvalidationProcessor}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::selector_map::SelectorMap; +use crate::selector_parser::Snapshot; +use crate::stylesheets::origin::OriginSet; +use crate::{Atom, WeakAtom}; +use dom::ElementState; +use selectors::attr::CaseSensitivity; +use selectors::matching::{ + matches_selector, MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, + SelectorCaches, VisitedHandlingMode, +}; +use smallvec::SmallVec; + +/// The collector implementation. +struct Collector<'a, 'b: 'a, 'selectors: 'a, E> +where + E: TElement, +{ + element: E, + wrapper: ElementWrapper<'b, E>, + snapshot: &'a Snapshot, + matching_context: &'a mut MatchingContext<'b, E::Impl>, + lookup_element: E, + removed_id: Option<&'a WeakAtom>, + added_id: Option<&'a WeakAtom>, + classes_removed: &'a SmallVec<[Atom; 8]>, + classes_added: &'a SmallVec<[Atom; 8]>, + state_changes: ElementState, + descendant_invalidations: &'a mut DescendantInvalidationLists<'selectors>, + sibling_invalidations: &'a mut InvalidationVector<'selectors>, + invalidates_self: bool, +} + +/// An invalidation processor for style changes due to state and attribute +/// changes. +pub struct StateAndAttrInvalidationProcessor<'a, 'b: 'a, E: TElement> { + shared_context: &'a SharedStyleContext<'b>, + element: E, + data: &'a mut ElementData, + matching_context: MatchingContext<'a, E::Impl>, + traversal_map: SiblingTraversalMap<E>, +} + +impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E> { + /// Creates a new StateAndAttrInvalidationProcessor. + pub fn new( + shared_context: &'a SharedStyleContext<'b>, + element: E, + data: &'a mut ElementData, + selector_caches: &'a mut SelectorCaches, + ) -> Self { + let matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + None, + selector_caches, + VisitedHandlingMode::AllLinksVisitedAndUnvisited, + shared_context.quirks_mode(), + NeedsSelectorFlags::No, + MatchingForInvalidation::Yes, + ); + + Self { + shared_context, + element, + data, + matching_context, + traversal_map: SiblingTraversalMap::default(), + } + } +} + +/// Checks a dependency against a given element and wrapper, to see if something +/// changed. +pub fn check_dependency<E, W>( + dependency: &Dependency, + element: &E, + wrapper: &W, + context: &mut MatchingContext<'_, E::Impl>, +) -> bool +where + E: TElement, + W: selectors::Element<Impl = E::Impl>, +{ + let matches_now = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + element, + context, + ); + + let matched_then = matches_selector( + &dependency.selector, + dependency.selector_offset, + None, + wrapper, + context, + ); + + matched_then != matches_now +} + +/// Whether we should process the descendants of a given element for style +/// invalidation. +pub fn should_process_descendants(data: &ElementData) -> bool { + !data.styles.is_display_none() && !data.hint.contains(RestyleHint::RESTYLE_DESCENDANTS) +} + +/// Propagates the bits after invalidating a descendant child. +pub fn propagate_dirty_bit_up_to<E>(ancestor: E, child: E) +where + E: TElement, +{ + // The child may not be a flattened tree child of the current element, + // but may be arbitrarily deep. + // + // Since we keep the traversal flags in terms of the flattened tree, + // we need to propagate it as appropriate. + let mut current = child.traversal_parent(); + while let Some(parent) = current.take() { + unsafe { parent.set_dirty_descendants() }; + current = parent.traversal_parent(); + + if parent == ancestor { + return; + } + } + debug_assert!( + false, + "Should've found {:?} as an ancestor of {:?}", + ancestor, child + ); +} + +/// Propagates the bits after invalidating a descendant child, if needed. +pub fn invalidated_descendants<E>(element: E, child: E) +where + E: TElement, +{ + if !child.has_data() { + return; + } + propagate_dirty_bit_up_to(element, child) +} + +/// Sets the appropriate restyle hint after invalidating the style of a given +/// element. +pub fn invalidated_self<E>(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + data.hint.insert(RestyleHint::RESTYLE_SELF); + true +} + +/// Sets the appropriate hint after invalidating the style of a sibling. +pub fn invalidated_sibling<E>(element: E, of: E) +where + E: TElement, +{ + debug_assert_eq!( + element.as_node().parent_node(), + of.as_node().parent_node(), + "Should be siblings" + ); + if !invalidated_self(element) { + return; + } + if element.traversal_parent() != of.traversal_parent() { + let parent = element.as_node().parent_element_or_host(); + debug_assert!( + parent.is_some(), + "How can we have siblings without parent nodes?" + ); + if let Some(e) = parent { + propagate_dirty_bit_up_to(e, element) + } + } +} + +impl<'a, 'b: 'a, E: 'a> InvalidationProcessor<'a, 'a, E> + for StateAndAttrInvalidationProcessor<'a, 'b, E> +where + E: TElement, +{ + /// We need to invalidate style on pseudo-elements, in order to process + /// changes that could otherwise end up in ::before or ::after content being + /// generated, and invalidate lazy pseudo caches. + fn invalidates_on_pseudo_element(&self) -> bool { + true + } + + fn check_outer_dependency(&mut self, dependency: &Dependency, element: E) -> bool { + // We cannot assert about `element` having a snapshot here (in fact it + // most likely won't), because it may be an arbitrary descendant or + // later-sibling of the element we started invalidating with. + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + check_dependency(dependency, &element, &wrapper, &mut self.matching_context) + } + + fn matching_context(&mut self) -> &mut MatchingContext<'a, E::Impl> { + &mut self.matching_context + } + + fn sibling_traversal_map(&self) -> &SiblingTraversalMap<E> { + &self.traversal_map + } + + fn collect_invalidations( + &mut self, + element: E, + _self_invalidations: &mut InvalidationVector<'a>, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, + ) -> bool { + debug_assert_eq!(element, self.element); + debug_assert!(element.has_snapshot(), "Why bothering?"); + + let wrapper = ElementWrapper::new(element, &*self.shared_context.snapshot_map); + + let state_changes = wrapper.state_changes(); + let Some(snapshot) = wrapper.snapshot() else { + return false; + }; + + if !snapshot.has_attrs() && state_changes.is_empty() { + return false; + } + + let mut classes_removed = SmallVec::<[Atom; 8]>::new(); + let mut classes_added = SmallVec::<[Atom; 8]>::new(); + if snapshot.class_changed() { + // TODO(emilio): Do this more efficiently! + snapshot.each_class(|c| { + if !element.has_class(c, CaseSensitivity::CaseSensitive) { + classes_removed.push(c.0.clone()) + } + }); + + element.each_class(|c| { + if !snapshot.has_class(c, CaseSensitivity::CaseSensitive) { + classes_added.push(c.0.clone()) + } + }) + } + + let mut id_removed = None; + let mut id_added = None; + if snapshot.id_changed() { + let old_id = snapshot.id_attr(); + let current_id = element.id(); + + if old_id != current_id { + id_removed = old_id; + id_added = current_id; + } + } + + if log_enabled!(::log::Level::Debug) { + debug!("Collecting changes for: {:?}", element); + if !state_changes.is_empty() { + debug!(" > state: {:?}", state_changes); + } + if snapshot.id_changed() { + debug!(" > id changed: +{:?} -{:?}", id_added, id_removed); + } + if snapshot.class_changed() { + debug!( + " > class changed: +{:?} -{:?}", + classes_added, classes_removed + ); + } + let mut attributes_changed = false; + snapshot.each_attr_changed(|_| { + attributes_changed = true; + }); + if attributes_changed { + debug!( + " > attributes changed, old: {}", + snapshot.debug_list_attributes() + ) + } + } + + let lookup_element = if element.implemented_pseudo_element().is_some() { + element.pseudo_element_originating_element().unwrap() + } else { + element + }; + + let mut shadow_rule_datas = SmallVec::<[_; 3]>::new(); + let matches_document_author_rules = + element.each_applicable_non_document_style_rule_data(|data, host| { + shadow_rule_datas.push((data, host.opaque())) + }); + + let invalidated_self = { + let mut collector = Collector { + wrapper, + lookup_element, + state_changes, + element, + snapshot: &snapshot, + matching_context: &mut self.matching_context, + removed_id: id_removed, + added_id: id_added, + classes_removed: &classes_removed, + classes_added: &classes_added, + descendant_invalidations, + sibling_invalidations, + invalidates_self: false, + }; + + let document_origins = if !matches_document_author_rules { + OriginSet::ORIGIN_USER_AGENT | OriginSet::ORIGIN_USER + } else { + OriginSet::all() + }; + + for (cascade_data, origin) in self.shared_context.stylist.iter_origins() { + if document_origins.contains(origin.into()) { + collector + .collect_dependencies_in_invalidation_map(cascade_data.invalidation_map()); + } + } + + for &(ref data, ref host) in &shadow_rule_datas { + collector.matching_context.current_host = Some(host.clone()); + collector.collect_dependencies_in_invalidation_map(data.invalidation_map()); + } + + collector.invalidates_self + }; + + // If we generated a ton of descendant invalidations, it's probably not + // worth to go ahead and try to process them. + // + // Just restyle the descendants directly. + // + // This number is completely made-up, but the page that made us add this + // code generated 1960+ invalidations (bug 1420741). + // + // We don't look at slotted_descendants because those don't propagate + // down more than one level anyway. + if descendant_invalidations.dom_descendants.len() > 150 { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + + if invalidated_self { + self.data.hint.insert(RestyleHint::RESTYLE_SELF); + } + + invalidated_self + } + + fn should_process_descendants(&mut self, element: E) -> bool { + if element == self.element { + return should_process_descendants(&self.data); + } + + match element.borrow_data() { + Some(d) => should_process_descendants(&d), + None => return false, + } + } + + fn recursion_limit_exceeded(&mut self, element: E) { + if element == self.element { + self.data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + return; + } + + if let Some(mut data) = element.mutate_data() { + data.hint.insert(RestyleHint::RESTYLE_DESCENDANTS); + } + } + + fn invalidated_descendants(&mut self, element: E, child: E) { + invalidated_descendants(element, child) + } + + fn invalidated_self(&mut self, element: E) { + debug_assert_ne!(element, self.element); + invalidated_self(element); + } + + fn invalidated_sibling(&mut self, element: E, of: E) { + debug_assert_ne!(element, self.element); + invalidated_sibling(element, of); + } +} + +impl<'a, 'b, 'selectors, E> Collector<'a, 'b, 'selectors, E> +where + E: TElement, + 'selectors: 'a, +{ + fn collect_dependencies_in_invalidation_map(&mut self, map: &'selectors InvalidationMap) { + let quirks_mode = self.matching_context.quirks_mode(); + let removed_id = self.removed_id; + if let Some(ref id) = removed_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + let added_id = self.added_id; + if let Some(ref id) = added_id { + if let Some(deps) = map.id_to_selector.get(id, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + for class in self.classes_added.iter().chain(self.classes_removed.iter()) { + if let Some(deps) = map.class_to_selector.get(class, quirks_mode) { + for dep in deps { + self.scan_dependency(dep); + } + } + } + + self.snapshot.each_attr_changed(|attribute| { + if let Some(deps) = map.other_attribute_affecting_selectors.get(attribute) { + for dep in deps { + self.scan_dependency(dep); + } + } + }); + + self.collect_state_dependencies(&map.state_affecting_selectors) + } + + fn collect_state_dependencies(&mut self, map: &'selectors SelectorMap<StateDependency>) { + if self.state_changes.is_empty() { + return; + } + map.lookup_with_additional( + self.lookup_element, + self.matching_context.quirks_mode(), + self.removed_id, + self.classes_removed, + self.state_changes, + |dependency| { + if !dependency.state.intersects(self.state_changes) { + return true; + } + self.scan_dependency(&dependency.dep); + true + }, + ); + } + + /// Check whether a dependency should be taken into account. + #[inline] + fn check_dependency(&mut self, dependency: &Dependency) -> bool { + check_dependency( + dependency, + &self.element, + &self.wrapper, + &mut self.matching_context, + ) + } + + fn scan_dependency(&mut self, dependency: &'selectors Dependency) { + debug_assert!( + matches!( + dependency.invalidation_kind(), + DependencyInvalidationKind::Normal(_) + ), + "Found relative selector dependency" + ); + debug!( + "TreeStyleInvalidator::scan_dependency({:?}, {:?})", + self.element, dependency + ); + + if !self.dependency_may_be_relevant(dependency) { + return; + } + + if self.check_dependency(dependency) { + return self.note_dependency(dependency); + } + } + + fn note_dependency(&mut self, dependency: &'selectors Dependency) { + debug_assert!(self.dependency_may_be_relevant(dependency)); + + let invalidation_kind = dependency.normal_invalidation_kind(); + if matches!(invalidation_kind, NormalDependencyInvalidationKind::Element) { + if let Some(ref parent) = dependency.parent { + // We know something changed in the inner selector, go outwards + // now. + self.scan_dependency(parent); + } else { + self.invalidates_self = true; + } + return; + } + + debug_assert_ne!(dependency.selector_offset, 0); + debug_assert_ne!(dependency.selector_offset, dependency.selector.len()); + + let invalidation = + Invalidation::new(&dependency, self.matching_context.current_host.clone()); + + self.invalidates_self |= push_invalidation( + invalidation, + invalidation_kind, + self.descendant_invalidations, + self.sibling_invalidations, + ); + } + + /// Returns whether `dependency` may cause us to invalidate the style of + /// more elements than what we've already invalidated. + fn dependency_may_be_relevant(&self, dependency: &Dependency) -> bool { + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Element => !self.invalidates_self, + NormalDependencyInvalidationKind::SlottedElements => { + self.element.is_html_slot_element() + }, + NormalDependencyInvalidationKind::Parts => self.element.shadow_root().is_some(), + NormalDependencyInvalidationKind::ElementAndDescendants | + NormalDependencyInvalidationKind::Siblings | + NormalDependencyInvalidationKind::Descendants => true, + } + } +} + +pub(crate) fn push_invalidation<'a>( + invalidation: Invalidation<'a>, + invalidation_kind: NormalDependencyInvalidationKind, + descendant_invalidations: &mut DescendantInvalidationLists<'a>, + sibling_invalidations: &mut InvalidationVector<'a>, +) -> bool { + match invalidation_kind { + NormalDependencyInvalidationKind::Element => unreachable!(), + NormalDependencyInvalidationKind::ElementAndDescendants => { + descendant_invalidations.dom_descendants.push(invalidation); + true + }, + NormalDependencyInvalidationKind::Descendants => { + descendant_invalidations.dom_descendants.push(invalidation); + false + }, + NormalDependencyInvalidationKind::Siblings => { + sibling_invalidations.push(invalidation); + false + }, + NormalDependencyInvalidationKind::Parts => { + descendant_invalidations.parts.push(invalidation); + false + }, + NormalDependencyInvalidationKind::SlottedElements => { + descendant_invalidations + .slotted_descendants + .push(invalidation); + false + }, + } +} + +pub(crate) fn dependency_may_be_relevant<E: TElement>( + dependency: &Dependency, + element: &E, + already_invalidated_self: bool, +) -> bool { + match dependency.normal_invalidation_kind() { + NormalDependencyInvalidationKind::Element => !already_invalidated_self, + NormalDependencyInvalidationKind::SlottedElements => element.is_html_slot_element(), + NormalDependencyInvalidationKind::Parts => element.shadow_root().is_some(), + NormalDependencyInvalidationKind::ElementAndDescendants | + NormalDependencyInvalidationKind::Siblings | + NormalDependencyInvalidationKind::Descendants => true, + } +} diff --git a/servo/components/style/invalidation/media_queries.rs b/servo/components/style/invalidation/media_queries.rs new file mode 100644 index 0000000000..6928b29d3d --- /dev/null +++ b/servo/components/style/invalidation/media_queries.rs @@ -0,0 +1,130 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code related to the invalidation of media-query-affected rules. + +use crate::context::QuirksMode; +use crate::media_queries::Device; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{DocumentRule, ImportRule, MediaRule}; +use crate::stylesheets::{NestedRuleIterationCondition, StylesheetContents, SupportsRule}; +use fxhash::FxHashSet; + +/// A key for a given media query result. +/// +/// NOTE: It happens to be the case that all the media lists we care about +/// happen to have a stable address, so we can just use an opaque pointer to +/// represent them. +/// +/// Also, note that right now when a rule or stylesheet is removed, we do a full +/// style flush, so there's no need to worry about other item created with the +/// same pointer address. +/// +/// If this changes, though, we may need to remove the item from the cache if +/// present before it goes away. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub struct MediaListKey(usize); + +impl MediaListKey { + /// Create a MediaListKey from a raw usize. + pub fn from_raw(k: usize) -> Self { + MediaListKey(k) + } +} + +/// A trait to get a given `MediaListKey` for a given item that can hold a +/// `MediaList`. +pub trait ToMediaListKey: Sized { + /// Get a `MediaListKey` for this item. This key needs to uniquely identify + /// the item. + fn to_media_list_key(&self) -> MediaListKey { + MediaListKey(self as *const Self as usize) + } +} + +impl ToMediaListKey for StylesheetContents {} +impl ToMediaListKey for ImportRule {} +impl ToMediaListKey for MediaRule {} + +/// A struct that holds the result of a media query evaluation pass for the +/// media queries that evaluated successfully. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct EffectiveMediaQueryResults { + /// The set of media lists that matched last time. + set: FxHashSet<MediaListKey>, +} + +impl EffectiveMediaQueryResults { + /// Trivially constructs an empty `EffectiveMediaQueryResults`. + pub fn new() -> Self { + Self { + set: FxHashSet::default(), + } + } + + /// Resets the results, using an empty key. + pub fn clear(&mut self) { + self.set.clear() + } + + /// Returns whether a given item was known to be effective when the results + /// were cached. + pub fn was_effective<T>(&self, item: &T) -> bool + where + T: ToMediaListKey, + { + self.set.contains(&item.to_media_list_key()) + } + + /// Notices that an effective item has been seen, and caches it as matching. + pub fn saw_effective<T>(&mut self, item: &T) + where + T: ToMediaListKey, + { + // NOTE(emilio): We can't assert that we don't cache the same item twice + // because of stylesheet reusing... shrug. + self.set.insert(item.to_media_list_key()); + } +} + +/// A filter that filters over effective rules, but allowing all potentially +/// effective `@media` rules. +pub struct PotentiallyEffectiveMediaRules; + +impl NestedRuleIterationCondition for PotentiallyEffectiveMediaRules { + fn process_import( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &ImportRule, + ) -> bool { + true + } + + fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool { + true + } + + /// Whether we should process the nested rules in a given `@-moz-document` rule. + fn process_document( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &DocumentRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_document(guard, device, quirks_mode, rule) + } + + /// Whether we should process the nested rules in a given `@supports` rule. + fn process_supports( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &SupportsRule, + ) -> bool { + use crate::stylesheets::EffectiveRules; + EffectiveRules::process_supports(guard, device, quirks_mode, rule) + } +} diff --git a/servo/components/style/invalidation/mod.rs b/servo/components/style/invalidation/mod.rs new file mode 100644 index 0000000000..12b0d06853 --- /dev/null +++ b/servo/components/style/invalidation/mod.rs @@ -0,0 +1,10 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Different bits of code related to invalidating style. + +pub mod element; +pub mod media_queries; +pub mod stylesheets; +pub mod viewport_units; diff --git a/servo/components/style/invalidation/stylesheets.rs b/servo/components/style/invalidation/stylesheets.rs new file mode 100644 index 0000000000..d845897aa4 --- /dev/null +++ b/servo/components/style/invalidation/stylesheets.rs @@ -0,0 +1,651 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A collection of invalidations due to changes in which stylesheets affect a +//! document. + +#![deny(unsafe_code)] + +use crate::context::QuirksMode; +use crate::dom::{TDocument, TElement, TNode}; +use crate::invalidation::element::element_wrapper::{ElementSnapshot, ElementWrapper}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::media_queries::Device; +use crate::selector_map::{MaybeCaseInsensitiveHashMap, PrecomputedHashMap}; +use crate::selector_parser::{SelectorImpl, Snapshot, SnapshotMap}; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{CssRule, StylesheetInDocument}; +use crate::stylesheets::{EffectiveRules, EffectiveRulesIterator}; +use crate::values::AtomIdent; +use crate::LocalName as SelectorLocalName; +use crate::{Atom, ShrinkIfNeeded}; +use selectors::parser::{Component, LocalName, Selector}; + +/// The kind of change that happened for a given rule. +#[repr(u32)] +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub enum RuleChangeKind { + /// The rule was inserted. + Insertion, + /// The rule was removed. + Removal, + /// Some change in the rule which we don't know about, and could have made + /// the rule change in any way. + Generic, + /// A change in the declarations of a style rule. + StyleRuleDeclarations, +} + +/// A style sheet invalidation represents a kind of element or subtree that may +/// need to be restyled. Whether it represents a whole subtree or just a single +/// element is determined by the given InvalidationKind in +/// StylesheetInvalidationSet's maps. +#[derive(Debug, Eq, Hash, MallocSizeOf, PartialEq)] +enum Invalidation { + /// An element with a given id. + ID(AtomIdent), + /// An element with a given class name. + Class(AtomIdent), + /// An element with a given local name. + LocalName { + name: SelectorLocalName, + lower_name: SelectorLocalName, + }, +} + +impl Invalidation { + fn is_id(&self) -> bool { + matches!(*self, Invalidation::ID(..)) + } + + fn is_id_or_class(&self) -> bool { + matches!(*self, Invalidation::ID(..) | Invalidation::Class(..)) + } +} + +/// Whether we should invalidate just the element, or the whole subtree within +/// it. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)] +enum InvalidationKind { + None = 0, + Element, + Scope, +} + +impl std::ops::BitOrAssign for InvalidationKind { + #[inline] + fn bitor_assign(&mut self, other: Self) { + *self = std::cmp::max(*self, other); + } +} + +impl InvalidationKind { + #[inline] + fn is_scope(self) -> bool { + matches!(self, Self::Scope) + } + + #[inline] + fn add(&mut self, other: Option<&InvalidationKind>) { + if let Some(other) = other { + *self |= *other; + } + } +} + +/// A set of invalidations due to stylesheet additions. +/// +/// TODO(emilio): We might be able to do the same analysis for media query +/// changes too (or even selector changes?). +#[derive(Debug, Default, MallocSizeOf)] +pub struct StylesheetInvalidationSet { + classes: MaybeCaseInsensitiveHashMap<Atom, InvalidationKind>, + ids: MaybeCaseInsensitiveHashMap<Atom, InvalidationKind>, + local_names: PrecomputedHashMap<SelectorLocalName, InvalidationKind>, + fully_invalid: bool, +} + +impl StylesheetInvalidationSet { + /// Create an empty `StylesheetInvalidationSet`. + pub fn new() -> Self { + Default::default() + } + + /// Mark the DOM tree styles' as fully invalid. + pub fn invalidate_fully(&mut self) { + debug!("StylesheetInvalidationSet::invalidate_fully"); + self.clear(); + self.fully_invalid = true; + } + + fn shrink_if_needed(&mut self) { + if self.fully_invalid { + return; + } + self.classes.shrink_if_needed(); + self.ids.shrink_if_needed(); + self.local_names.shrink_if_needed(); + } + + /// Analyze the given stylesheet, and collect invalidations from their + /// rules, in order to avoid doing a full restyle when we style the document + /// next time. + pub fn collect_invalidations_for<S>( + &mut self, + device: &Device, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + ) where + S: StylesheetInDocument, + { + debug!("StylesheetInvalidationSet::collect_invalidations_for"); + if self.fully_invalid { + debug!(" > Fully invalid already"); + return; + } + + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + let quirks_mode = device.quirks_mode(); + for rule in stylesheet.effective_rules(device, guard) { + self.collect_invalidations_for_rule( + rule, + guard, + device, + quirks_mode, + /* is_generic_change = */ false, + ); + if self.fully_invalid { + break; + } + } + + self.shrink_if_needed(); + + debug!(" > resulting class invalidations: {:?}", self.classes); + debug!(" > resulting id invalidations: {:?}", self.ids); + debug!( + " > resulting local name invalidations: {:?}", + self.local_names + ); + debug!(" > fully_invalid: {}", self.fully_invalid); + } + + /// Clears the invalidation set, invalidating elements as needed if + /// `document_element` is provided. + /// + /// Returns true if any invalidations ocurred. + pub fn flush<E>(&mut self, document_element: Option<E>, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!( + "Stylist::flush({:?}, snapshots: {})", + document_element, + snapshots.is_some() + ); + let have_invalidations = match document_element { + Some(e) => self.process_invalidations(e, snapshots), + None => false, + }; + self.clear(); + have_invalidations + } + + /// Returns whether there's no invalidation to process. + pub fn is_empty(&self) -> bool { + !self.fully_invalid && + self.classes.is_empty() && + self.ids.is_empty() && + self.local_names.is_empty() + } + + fn invalidation_kind_for<E>( + &self, + element: E, + snapshot: Option<&Snapshot>, + quirks_mode: QuirksMode, + ) -> InvalidationKind + where + E: TElement, + { + debug_assert!(!self.fully_invalid); + + let mut kind = InvalidationKind::None; + + if !self.classes.is_empty() { + element.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + + if let Some(snapshot) = snapshot { + snapshot.each_class(|c| { + kind.add(self.classes.get(c, quirks_mode)); + }); + + if kind.is_scope() { + return kind; + } + } + } + + if !self.ids.is_empty() { + if let Some(ref id) = element.id() { + kind.add(self.ids.get(id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + + if let Some(ref old_id) = snapshot.and_then(|s| s.id_attr()) { + kind.add(self.ids.get(old_id, quirks_mode)); + if kind.is_scope() { + return kind; + } + } + } + + if !self.local_names.is_empty() { + kind.add(self.local_names.get(element.local_name())); + } + + kind + } + + /// Clears the invalidation set without processing. + pub fn clear(&mut self) { + self.classes.clear(); + self.ids.clear(); + self.local_names.clear(); + self.fully_invalid = false; + debug_assert!(self.is_empty()); + } + + fn process_invalidations<E>(&self, element: E, snapshots: Option<&SnapshotMap>) -> bool + where + E: TElement, + { + debug!("Stylist::process_invalidations({:?}, {:?})", element, self); + + { + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if self.fully_invalid { + debug!("process_invalidations: fully_invalid({:?})", element); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + } + } + + if self.is_empty() { + debug!("process_invalidations: empty invalidation set"); + return false; + } + + let quirks_mode = element.as_node().owner_doc().quirks_mode(); + self.process_invalidations_in_subtree(element, snapshots, quirks_mode) + } + + /// Process style invalidations in a given subtree. This traverses the + /// subtree looking for elements that match the invalidations in our hash + /// map members. + /// + /// Returns whether it invalidated at least one element's style. + #[allow(unsafe_code)] + fn process_invalidations_in_subtree<E>( + &self, + element: E, + snapshots: Option<&SnapshotMap>, + quirks_mode: QuirksMode, + ) -> bool + where + E: TElement, + { + debug!("process_invalidations_in_subtree({:?})", element); + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if !data.has_styles() { + return false; + } + + if data.hint.contains_subtree() { + debug!( + "process_invalidations_in_subtree: {:?} was already invalid", + element + ); + return false; + } + + let element_wrapper = snapshots.map(|s| ElementWrapper::new(element, s)); + let snapshot = element_wrapper.as_ref().and_then(|e| e.snapshot()); + + match self.invalidation_kind_for(element, snapshot, quirks_mode) { + InvalidationKind::None => {}, + InvalidationKind::Element => { + debug!( + "process_invalidations_in_subtree: {:?} matched self", + element + ); + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + InvalidationKind::Scope => { + debug!( + "process_invalidations_in_subtree: {:?} matched subtree", + element + ); + data.hint.insert(RestyleHint::restyle_subtree()); + return true; + }, + } + + let mut any_children_invalid = false; + + for child in element.traversal_children() { + let child = match child.as_element() { + Some(e) => e, + None => continue, + }; + + any_children_invalid |= + self.process_invalidations_in_subtree(child, snapshots, quirks_mode); + } + + if any_children_invalid { + debug!( + "Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + data.hint.contains(RestyleHint::RESTYLE_SELF) || any_children_invalid + } + + /// TODO(emilio): Reuse the bucket stuff from selectormap? That handles + /// :is() / :where() etc. + fn scan_component( + component: &Component<SelectorImpl>, + invalidation: &mut Option<Invalidation>, + ) { + match *component { + Component::LocalName(LocalName { + ref name, + ref lower_name, + }) => { + if invalidation.is_none() { + *invalidation = Some(Invalidation::LocalName { + name: name.clone(), + lower_name: lower_name.clone(), + }); + } + }, + Component::Class(ref class) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id_or_class()) { + *invalidation = Some(Invalidation::Class(class.clone())); + } + }, + Component::ID(ref id) => { + if invalidation.as_ref().map_or(true, |s| !s.is_id()) { + *invalidation = Some(Invalidation::ID(id.clone())); + } + }, + _ => { + // Ignore everything else, at least for now. + }, + } + } + + /// Collect invalidations for a given selector. + /// + /// We look at the outermost local name, class, or ID selector to the left + /// of an ancestor combinator, in order to restyle only a given subtree. + /// + /// If the selector has no ancestor combinator, then we do the same for + /// the only sequence it has, but record it as an element invalidation + /// instead of a subtree invalidation. + /// + /// We prefer IDs to classs, and classes to local names, on the basis + /// that the former should be more specific than the latter. We also + /// prefer to generate subtree invalidations for the outermost part + /// of the selector, to reduce the amount of traversal we need to do + /// when flushing invalidations. + fn collect_invalidations( + &mut self, + selector: &Selector<SelectorImpl>, + quirks_mode: QuirksMode, + ) { + debug!( + "StylesheetInvalidationSet::collect_invalidations({:?})", + selector + ); + + let mut element_invalidation: Option<Invalidation> = None; + let mut subtree_invalidation: Option<Invalidation> = None; + + let mut scan_for_element_invalidation = true; + let mut scan_for_subtree_invalidation = false; + + let mut iter = selector.iter(); + + loop { + for component in &mut iter { + if scan_for_element_invalidation { + Self::scan_component(component, &mut element_invalidation); + } else if scan_for_subtree_invalidation { + Self::scan_component(component, &mut subtree_invalidation); + } + } + match iter.next_sequence() { + None => break, + Some(combinator) => { + scan_for_subtree_invalidation = combinator.is_ancestor(); + }, + } + scan_for_element_invalidation = false; + } + + if let Some(s) = subtree_invalidation { + debug!(" > Found subtree invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Scope, quirks_mode) { + return; + } + } + + if let Some(s) = element_invalidation { + debug!(" > Found element invalidation: {:?}", s); + if self.insert_invalidation(s, InvalidationKind::Element, quirks_mode) { + return; + } + } + + // The selector was of a form that we can't handle. Any element could + // match it, so let's just bail out. + debug!(" > Can't handle selector or OOMd, marking fully invalid"); + self.invalidate_fully() + } + + fn insert_invalidation( + &mut self, + invalidation: Invalidation, + kind: InvalidationKind, + quirks_mode: QuirksMode, + ) -> bool { + match invalidation { + Invalidation::Class(c) => { + let entry = match self.classes.try_entry(c.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::ID(i) => { + let entry = match self.ids.try_entry(i.0, quirks_mode) { + Ok(e) => e, + Err(..) => return false, + }; + *entry.or_insert(InvalidationKind::None) |= kind; + }, + Invalidation::LocalName { name, lower_name } => { + let insert_lower = name != lower_name; + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(name); + *entry.or_insert(InvalidationKind::None) |= kind; + if insert_lower { + if self.local_names.try_reserve(1).is_err() { + return false; + } + let entry = self.local_names.entry(lower_name); + *entry.or_insert(InvalidationKind::None) |= kind; + } + }, + } + + true + } + + /// Collects invalidations for a given CSS rule, if not fully invalid + /// already. + /// + /// TODO(emilio): we can't check whether the rule is inside a non-effective + /// subtree, we potentially could do that. + pub fn rule_changed<S>( + &mut self, + stylesheet: &S, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + change_kind: RuleChangeKind, + ) where + S: StylesheetInDocument, + { + debug!("StylesheetInvalidationSet::rule_changed"); + if self.fully_invalid { + return; + } + + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + debug!(" > Stylesheet was not effective"); + return; // Nothing to do here. + } + + // If the change is generic, we don't have the old rule information to know e.g., the old + // media condition, or the old selector text, so we might need to invalidate more + // aggressively. That only applies to the changed rules, for other rules we can just + // collect invalidations as normal. + let is_generic_change = change_kind == RuleChangeKind::Generic; + self.collect_invalidations_for_rule(rule, guard, device, quirks_mode, is_generic_change); + if self.fully_invalid { + return; + } + + if !is_generic_change && !EffectiveRules::is_effective(guard, device, quirks_mode, rule) { + return; + } + + let rules = EffectiveRulesIterator::effective_children(device, quirks_mode, guard, rule); + for rule in rules { + self.collect_invalidations_for_rule( + rule, + guard, + device, + quirks_mode, + /* is_generic_change = */ false, + ); + if self.fully_invalid { + break; + } + } + } + + /// Collects invalidations for a given CSS rule. + fn collect_invalidations_for_rule( + &mut self, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + is_generic_change: bool, + ) { + use crate::stylesheets::CssRule::*; + debug!("StylesheetInvalidationSet::collect_invalidations_for_rule"); + debug_assert!(!self.fully_invalid, "Not worth being here!"); + + match *rule { + Style(ref lock) => { + if is_generic_change { + // TODO(emilio): We need to do this for selector / keyframe + // name / font-face changes, because we don't have the old + // selector / name. If we distinguish those changes + // specially, then we can at least use this invalidation for + // style declaration changes. + return self.invalidate_fully(); + } + + let style_rule = lock.read_with(guard); + for selector in style_rule.selectors.slice() { + self.collect_invalidations(selector, quirks_mode); + if self.fully_invalid { + return; + } + } + }, + Namespace(..) => { + // It's not clear what handling changes for this correctly would + // look like. + }, + LayerStatement(..) => { + // Layer statement insertions might alter styling order, so we need to always + // invalidate fully. + return self.invalidate_fully(); + }, + Document(..) | Import(..) | Media(..) | Supports(..) | Container(..) | + LayerBlock(..) => { + // Do nothing, relevant nested rules are visited as part of rule iteration. + }, + FontFace(..) => { + // Do nothing, @font-face doesn't affect computed style information on it's own. + // We'll restyle when the font face loads, if needed. + }, + Page(..) | Margin(..) => { + // Do nothing, we don't support OM mutations on print documents, and page rules + // can't affect anything else. + }, + Keyframes(ref lock) => { + if is_generic_change { + return self.invalidate_fully(); + } + let keyframes_rule = lock.read_with(guard); + if device.animation_name_may_be_referenced(&keyframes_rule.name) { + debug!( + " > Found @keyframes rule potentially referenced \ + from the page, marking the whole tree invalid." + ); + self.invalidate_fully(); + } else { + // Do nothing, this animation can't affect the style of existing elements. + } + }, + CounterStyle(..) | Property(..) | FontFeatureValues(..) | FontPaletteValues(..) => { + debug!(" > Found unsupported rule, marking the whole subtree invalid."); + self.invalidate_fully(); + }, + } + } +} diff --git a/servo/components/style/invalidation/viewport_units.rs b/servo/components/style/invalidation/viewport_units.rs new file mode 100644 index 0000000000..06faeb14c4 --- /dev/null +++ b/servo/components/style/invalidation/viewport_units.rs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Invalidates style of all elements that depend on viewport units. + +use crate::data::ViewportUnitUsage; +use crate::dom::{TElement, TNode}; +use crate::invalidation::element::restyle_hints::RestyleHint; + +/// Invalidates style of all elements that depend on viewport units. +/// +/// Returns whether any element was invalidated. +pub fn invalidate<E>(root: E) -> bool +where + E: TElement, +{ + debug!("invalidation::viewport_units::invalidate({:?})", root); + invalidate_recursively(root) +} + +fn invalidate_recursively<E>(element: E) -> bool +where + E: TElement, +{ + let mut data = match element.mutate_data() { + Some(data) => data, + None => return false, + }; + + if data.hint.will_recascade_subtree() { + debug!("invalidate_recursively: {:?} was already invalid", element); + return false; + } + + let usage = data.styles.viewport_unit_usage(); + let uses_viewport_units = usage != ViewportUnitUsage::None; + if uses_viewport_units { + debug!( + "invalidate_recursively: {:?} uses viewport units {:?}", + element, usage + ); + } + + match usage { + ViewportUnitUsage::None => {}, + ViewportUnitUsage::FromQuery => { + data.hint.insert(RestyleHint::RESTYLE_SELF); + }, + ViewportUnitUsage::FromDeclaration => { + data.hint.insert(RestyleHint::RECASCADE_SELF); + }, + } + + let mut any_children_invalid = false; + for child in element.traversal_children() { + if let Some(child) = child.as_element() { + any_children_invalid |= invalidate_recursively(child); + } + } + + if any_children_invalid { + debug!( + "invalidate_recursively: Children of {:?} changed, setting dirty descendants", + element + ); + unsafe { element.set_dirty_descendants() } + } + + uses_viewport_units || any_children_invalid +} diff --git a/servo/components/style/lib.rs b/servo/components/style/lib.rs new file mode 100644 index 0000000000..b2a1e20ce8 --- /dev/null +++ b/servo/components/style/lib.rs @@ -0,0 +1,332 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Calculate [specified][specified] and [computed values][computed] from a +//! tree of DOM nodes and a set of stylesheets. +//! +//! [computed]: https://drafts.csswg.org/css-cascade/#computed +//! [specified]: https://drafts.csswg.org/css-cascade/#specified +//! +//! In particular, this crate contains the definitions of supported properties, +//! the code to parse them into specified values and calculate the computed +//! values based on the specified values, as well as the code to serialize both +//! specified and computed values. +//! +//! The main entry point is [`recalc_style_at`][recalc_style_at]. +//! +//! [recalc_style_at]: traversal/fn.recalc_style_at.html +//! +//! Major dependencies are the [cssparser][cssparser] and [selectors][selectors] +//! crates. +//! +//! [cssparser]: ../cssparser/index.html +//! [selectors]: ../selectors/index.html + +#![deny(missing_docs)] + +#[macro_use] +extern crate bitflags; +#[macro_use] +extern crate cssparser; +#[macro_use] +extern crate debug_unreachable; +#[macro_use] +extern crate derive_more; +#[macro_use] +extern crate gecko_profiler; +#[cfg(feature = "gecko")] +#[macro_use] +pub mod gecko_string_cache; +#[cfg(feature = "servo")] +#[macro_use] +extern crate html5ever; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate log; +#[macro_use] +extern crate malloc_size_of; +#[macro_use] +extern crate malloc_size_of_derive; +#[allow(unused_extern_crates)] +#[macro_use] +extern crate matches; +#[cfg(feature = "gecko")] +pub use nsstring; +#[cfg(feature = "gecko")] +extern crate num_cpus; +#[macro_use] +extern crate num_derive; +#[macro_use] +extern crate serde; +pub use servo_arc; +#[cfg(feature = "servo")] +#[macro_use] +extern crate servo_atoms; +#[macro_use] +extern crate static_assertions; +#[macro_use] +extern crate style_derive; +#[macro_use] +extern crate to_shmem_derive; + +#[macro_use] +mod macros; + +pub mod animation; +pub mod applicable_declarations; +#[allow(missing_docs)] // TODO. +#[cfg(feature = "servo")] +pub mod attr; +pub mod author_styles; +pub mod bezier; +pub mod bloom; +pub mod color; +#[path = "properties/computed_value_flags.rs"] +pub mod computed_value_flags; +pub mod context; +pub mod counter_style; +pub mod custom_properties; +pub mod custom_properties_map; +pub mod data; +pub mod dom; +pub mod dom_apis; +pub mod driver; +#[cfg(feature = "servo")] +mod encoding_support; +pub mod error_reporting; +pub mod font_face; +pub mod font_metrics; +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko_bindings; +pub mod global_style_data; +pub mod invalidation; +#[allow(missing_docs)] // TODO. +pub mod logical_geometry; +pub mod matching; +pub mod media_queries; +pub mod parallel; +pub mod parser; +pub mod piecewise_linear; +pub mod properties_and_values; +#[macro_use] +pub mod queries; +pub mod rule_cache; +pub mod rule_collector; +pub mod rule_tree; +pub mod scoped_tls; +pub mod selector_map; +pub mod selector_parser; +pub mod shared_lock; +pub mod sharing; +pub mod str; +pub mod style_adjuster; +pub mod style_resolver; +pub mod stylesheet_set; +pub mod stylesheets; +pub mod stylist; +pub mod thread_state; +pub mod traversal; +pub mod traversal_flags; +pub mod use_counters; +#[macro_use] +#[allow(non_camel_case_types)] +pub mod values; + +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache as string_cache; +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache::Atom; +/// The namespace prefix type for Gecko, which is just an atom. +#[cfg(feature = "gecko")] +pub type Prefix = crate::values::AtomIdent; +/// The local name of an element for Gecko, which is just an atom. +#[cfg(feature = "gecko")] +pub type LocalName = crate::values::AtomIdent; +#[cfg(feature = "gecko")] +pub use crate::gecko_string_cache::Namespace; + +#[cfg(feature = "servo")] +pub use servo_atoms::Atom; + +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type LocalName = crate::values::GenericAtomIdent<html5ever::LocalNameStaticSet>; +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type Namespace = crate::values::GenericAtomIdent<html5ever::NamespaceStaticSet>; +#[cfg(feature = "servo")] +#[allow(missing_docs)] +pub type Prefix = crate::values::GenericAtomIdent<html5ever::PrefixStaticSet>; + +pub use style_traits::arc_slice::ArcSlice; +pub use style_traits::owned_slice::OwnedSlice; +pub use style_traits::owned_str::OwnedStr; + +use std::hash::{BuildHasher, Hash}; + +pub mod properties; + +#[cfg(feature = "gecko")] +#[allow(unsafe_code)] +pub mod gecko; + +// uses a macro from properties +#[cfg(feature = "servo")] +#[allow(unsafe_code)] +pub mod servo; + +macro_rules! reexport_computed_values { + ( $( { $name: ident } )+ ) => { + /// Types for [computed values][computed]. + /// + /// [computed]: https://drafts.csswg.org/css-cascade/#computed + pub mod computed_values { + $( + pub use crate::properties::longhands::$name::computed_value as $name; + )+ + // Don't use a side-specific name needlessly: + pub use crate::properties::longhands::border_top_style::computed_value as border_style; + } + } +} +longhand_properties_idents!(reexport_computed_values); +#[cfg(feature = "gecko")] +use crate::gecko_string_cache::WeakAtom; +#[cfg(feature = "servo")] +use servo_atoms::Atom as WeakAtom; + +/// Extension methods for selectors::attr::CaseSensitivity +pub trait CaseSensitivityExt { + /// Return whether two atoms compare equal according to this case sensitivity. + fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool; +} + +impl CaseSensitivityExt for selectors::attr::CaseSensitivity { + #[inline] + fn eq_atom(self, a: &WeakAtom, b: &WeakAtom) -> bool { + match self { + selectors::attr::CaseSensitivity::CaseSensitive => a == b, + selectors::attr::CaseSensitivity::AsciiCaseInsensitive => a.eq_ignore_ascii_case(b), + } + } +} + +/// A trait pretty much similar to num_traits::Zero, but without the need of +/// implementing `Add`. +pub trait Zero { + /// Returns the zero value. + fn zero() -> Self; + + /// Returns whether this value is zero. + fn is_zero(&self) -> bool; +} + +impl<T> Zero for T +where + T: num_traits::Zero, +{ + fn zero() -> Self { + <Self as num_traits::Zero>::zero() + } + + fn is_zero(&self) -> bool { + <Self as num_traits::Zero>::is_zero(self) + } +} + +/// A trait implementing a function to tell if the number is zero without a percent +pub trait ZeroNoPercent { + /// So, `0px` should return `true`, but `0%` or `1px` should return `false` + fn is_zero_no_percent(&self) -> bool; +} + +/// A trait pretty much similar to num_traits::One, but without the need of +/// implementing `Mul`. +pub trait One { + /// Reutrns the one value. + fn one() -> Self; + + /// Returns whether this value is one. + fn is_one(&self) -> bool; +} + +impl<T> One for T +where + T: num_traits::One + PartialEq, +{ + fn one() -> Self { + <Self as num_traits::One>::one() + } + + fn is_one(&self) -> bool { + *self == One::one() + } +} + +/// An allocation error. +/// +/// TODO(emilio): Would be nice to have more information here, or for SmallVec +/// to return the standard error type (and then we can just return that). +/// +/// But given we use these mostly to bail out and ignore them, it's not a big +/// deal. +#[derive(Debug)] +pub struct AllocErr; + +impl From<smallvec::CollectionAllocErr> for AllocErr { + #[inline] + fn from(_: smallvec::CollectionAllocErr) -> Self { + Self + } +} + +impl From<std::collections::TryReserveError> for AllocErr { + #[inline] + fn from(_: std::collections::TryReserveError) -> Self { + Self + } +} + +/// Shrink the capacity of the collection if needed. +pub(crate) trait ShrinkIfNeeded { + fn shrink_if_needed(&mut self); +} + +/// We shrink the capacity of a collection if we're wasting more than a 25% of +/// its capacity, and if the collection is arbitrarily big enough +/// (>= CAPACITY_THRESHOLD entries). +#[inline] +fn should_shrink(len: usize, capacity: usize) -> bool { + const CAPACITY_THRESHOLD: usize = 64; + capacity >= CAPACITY_THRESHOLD && len + capacity / 4 < capacity +} + +impl<K, V, H> ShrinkIfNeeded for std::collections::HashMap<K, V, H> +where + K: Eq + Hash, + H: BuildHasher, +{ + fn shrink_if_needed(&mut self) { + if should_shrink(self.len(), self.capacity()) { + self.shrink_to_fit(); + } + } +} + +impl<T, H> ShrinkIfNeeded for std::collections::HashSet<T, H> +where + T: Eq + Hash, + H: BuildHasher, +{ + fn shrink_if_needed(&mut self) { + if should_shrink(self.len(), self.capacity()) { + self.shrink_to_fit(); + } + } +} + +// TODO(emilio): Measure and see if we're wasting a lot of memory on Vec / +// SmallVec, and if so consider shrinking those as well. diff --git a/servo/components/style/logical_geometry.rs b/servo/components/style/logical_geometry.rs new file mode 100644 index 0000000000..03272ae545 --- /dev/null +++ b/servo/components/style/logical_geometry.rs @@ -0,0 +1,1629 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Geometry in flow-relative space. + +use crate::properties::style_structs; +use euclid::default::{Point2D, Rect, SideOffsets2D, Size2D}; +use euclid::num::Zero; +use std::cmp::{max, min}; +use std::fmt::{self, Debug, Error, Formatter}; +use std::ops::{Add, Sub}; +use unicode_bidi as bidi; + +pub enum BlockFlowDirection { + TopToBottom, + RightToLeft, + LeftToRight, +} + +pub enum InlineBaseDirection { + LeftToRight, + RightToLeft, +} + +// TODO: improve the readability of the WritingMode serialization, refer to the Debug:fmt() +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, Serialize)] +#[repr(C)] +pub struct WritingMode(u8); +bitflags!( + impl WritingMode: u8 { + /// A vertical writing mode; writing-mode is vertical-rl, + /// vertical-lr, sideways-lr, or sideways-rl. + const VERTICAL = 1 << 0; + /// The inline flow direction is reversed against the physical + /// direction (i.e. right-to-left or bottom-to-top); writing-mode is + /// sideways-lr or direction is rtl (but not both). + /// + /// (This bit can be derived from the others, but we store it for + /// convenience.) + const INLINE_REVERSED = 1 << 1; + /// A vertical writing mode whose block progression direction is left- + /// to-right; writing-mode is vertical-lr or sideways-lr. + /// + /// Never set without VERTICAL. + const VERTICAL_LR = 1 << 2; + /// The line-over/line-under sides are inverted with respect to the + /// block-start/block-end edge; writing-mode is vertical-lr. + /// + /// Never set without VERTICAL and VERTICAL_LR. + const LINE_INVERTED = 1 << 3; + /// direction is rtl. + const RTL = 1 << 4; + /// All text within a vertical writing mode is displayed sideways + /// and runs top-to-bottom or bottom-to-top; set in these cases: + /// + /// * writing-mode: sideways-rl; + /// * writing-mode: sideways-lr; + /// + /// Never set without VERTICAL. + const VERTICAL_SIDEWAYS = 1 << 5; + /// Similar to VERTICAL_SIDEWAYS, but is set via text-orientation; + /// set in these cases: + /// + /// * writing-mode: vertical-rl; text-orientation: sideways; + /// * writing-mode: vertical-lr; text-orientation: sideways; + /// + /// Never set without VERTICAL. + const TEXT_SIDEWAYS = 1 << 6; + /// Horizontal text within a vertical writing mode is displayed with each + /// glyph upright; set in these cases: + /// + /// * writing-mode: vertical-rl; text-orientation: upright; + /// * writing-mode: vertical-lr: text-orientation: upright; + /// + /// Never set without VERTICAL. + const UPRIGHT = 1 << 7; + } +); + +impl WritingMode { + /// Return a WritingMode bitflags from the relevant CSS properties. + pub fn new(inheritedbox_style: &style_structs::InheritedBox) -> Self { + use crate::properties::longhands::direction::computed_value::T as Direction; + use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; + + let mut flags = WritingMode::empty(); + + let direction = inheritedbox_style.clone_direction(); + let writing_mode = inheritedbox_style.clone_writing_mode(); + + match direction { + Direction::Ltr => {}, + Direction::Rtl => { + flags.insert(WritingMode::RTL); + }, + } + + match writing_mode { + SpecifiedWritingMode::HorizontalTb => { + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + SpecifiedWritingMode::VerticalRl => { + flags.insert(WritingMode::VERTICAL); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + SpecifiedWritingMode::VerticalLr => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_LR); + flags.insert(WritingMode::LINE_INVERTED); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + #[cfg(feature = "gecko")] + SpecifiedWritingMode::SidewaysRl => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_SIDEWAYS); + if direction == Direction::Rtl { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + #[cfg(feature = "gecko")] + SpecifiedWritingMode::SidewaysLr => { + flags.insert(WritingMode::VERTICAL); + flags.insert(WritingMode::VERTICAL_LR); + flags.insert(WritingMode::VERTICAL_SIDEWAYS); + if direction == Direction::Ltr { + flags.insert(WritingMode::INLINE_REVERSED); + } + }, + } + + #[cfg(feature = "gecko")] + { + use crate::properties::longhands::text_orientation::computed_value::T as TextOrientation; + + // text-orientation only has an effect for vertical-rl and + // vertical-lr values of writing-mode. + match writing_mode { + SpecifiedWritingMode::VerticalRl | SpecifiedWritingMode::VerticalLr => { + match inheritedbox_style.clone_text_orientation() { + TextOrientation::Mixed => {}, + TextOrientation::Upright => { + flags.insert(WritingMode::UPRIGHT); + + // https://drafts.csswg.org/css-writing-modes-3/#valdef-text-orientation-upright: + // + // > This value causes the used value of direction + // > to be ltr, and for the purposes of bidi + // > reordering, causes all characters to be treated + // > as strong LTR. + flags.remove(WritingMode::RTL); + flags.remove(WritingMode::INLINE_REVERSED); + }, + TextOrientation::Sideways => { + flags.insert(WritingMode::TEXT_SIDEWAYS); + }, + } + }, + _ => {}, + } + } + + flags + } + + /// Returns the `horizontal-tb` value. + pub fn horizontal_tb() -> Self { + Self::empty() + } + + #[inline] + pub fn is_vertical(&self) -> bool { + self.intersects(WritingMode::VERTICAL) + } + + #[inline] + pub fn is_horizontal(&self) -> bool { + !self.is_vertical() + } + + /// Assuming .is_vertical(), does the block direction go left to right? + #[inline] + pub fn is_vertical_lr(&self) -> bool { + self.intersects(WritingMode::VERTICAL_LR) + } + + /// Assuming .is_vertical(), does the inline direction go top to bottom? + #[inline] + pub fn is_inline_tb(&self) -> bool { + // https://drafts.csswg.org/css-writing-modes-3/#logical-to-physical + !self.intersects(WritingMode::INLINE_REVERSED) + } + + #[inline] + pub fn is_bidi_ltr(&self) -> bool { + !self.intersects(WritingMode::RTL) + } + + #[inline] + pub fn is_sideways(&self) -> bool { + self.intersects(WritingMode::VERTICAL_SIDEWAYS | WritingMode::TEXT_SIDEWAYS) + } + + #[inline] + pub fn is_upright(&self) -> bool { + self.intersects(WritingMode::UPRIGHT) + } + + /// https://drafts.csswg.org/css-writing-modes/#logical-to-physical + /// + /// | Return | line-left is… | line-right is… | + /// |---------|---------------|----------------| + /// | `true` | inline-start | inline-end | + /// | `false` | inline-end | inline-start | + #[inline] + pub fn line_left_is_inline_start(&self) -> bool { + // https://drafts.csswg.org/css-writing-modes/#inline-start + // “For boxes with a used direction value of ltr, this means the line-left side. + // For boxes with a used direction value of rtl, this means the line-right side.” + self.is_bidi_ltr() + } + + #[inline] + pub fn inline_start_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) { + (false, _, true) => PhysicalSide::Left, + (false, _, false) => PhysicalSide::Right, + (true, true, _) => PhysicalSide::Top, + (true, false, _) => PhysicalSide::Bottom, + } + } + + #[inline] + pub fn inline_end_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_inline_tb(), self.is_bidi_ltr()) { + (false, _, true) => PhysicalSide::Right, + (false, _, false) => PhysicalSide::Left, + (true, true, _) => PhysicalSide::Bottom, + (true, false, _) => PhysicalSide::Top, + } + } + + #[inline] + pub fn block_start_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => PhysicalSide::Top, + (true, true) => PhysicalSide::Left, + (true, false) => PhysicalSide::Right, + } + } + + #[inline] + pub fn block_end_physical_side(&self) -> PhysicalSide { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => PhysicalSide::Bottom, + (true, true) => PhysicalSide::Right, + (true, false) => PhysicalSide::Left, + } + } + + #[inline] + pub fn start_start_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + self.block_start_physical_side(), + self.inline_start_physical_side(), + ) + } + + #[inline] + pub fn start_end_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + self.block_start_physical_side(), + self.inline_end_physical_side(), + ) + } + + #[inline] + pub fn end_start_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + self.block_end_physical_side(), + self.inline_start_physical_side(), + ) + } + + #[inline] + pub fn end_end_physical_corner(&self) -> PhysicalCorner { + PhysicalCorner::from_sides( + self.block_end_physical_side(), + self.inline_end_physical_side(), + ) + } + + #[inline] + pub fn block_flow_direction(&self) -> BlockFlowDirection { + match (self.is_vertical(), self.is_vertical_lr()) { + (false, _) => BlockFlowDirection::TopToBottom, + (true, true) => BlockFlowDirection::LeftToRight, + (true, false) => BlockFlowDirection::RightToLeft, + } + } + + #[inline] + pub fn inline_base_direction(&self) -> InlineBaseDirection { + if self.intersects(WritingMode::RTL) { + InlineBaseDirection::RightToLeft + } else { + InlineBaseDirection::LeftToRight + } + } + + #[inline] + /// The default bidirectional embedding level for this writing mode. + /// + /// Returns bidi level 0 if the mode is LTR, or 1 otherwise. + pub fn to_bidi_level(&self) -> bidi::Level { + if self.is_bidi_ltr() { + bidi::Level::ltr() + } else { + bidi::Level::rtl() + } + } + + #[inline] + /// Is the text layout vertical? + pub fn is_text_vertical(&self) -> bool { + self.is_vertical() && !self.is_sideways() + } +} + +impl fmt::Display for WritingMode { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + if self.is_vertical() { + write!(formatter, "V")?; + if self.is_vertical_lr() { + write!(formatter, " LR")?; + } else { + write!(formatter, " RL")?; + } + if self.is_sideways() { + write!(formatter, " Sideways")?; + } + if self.intersects(WritingMode::LINE_INVERTED) { + write!(formatter, " Inverted")?; + } + } else { + write!(formatter, "H")?; + } + if self.is_bidi_ltr() { + write!(formatter, " LTR") + } else { + write!(formatter, " RTL") + } + } +} + +/// Wherever logical geometry is used, the writing mode is known based on context: +/// every method takes a `mode` parameter. +/// However, this context is easy to get wrong. +/// In debug builds only, logical geometry objects store their writing mode +/// (in addition to taking it as a parameter to methods) and check it. +/// In non-debug builds, make this storage zero-size and the checks no-ops. +#[cfg(not(debug_assertions))] +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +struct DebugWritingMode; + +#[cfg(debug_assertions)] +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +struct DebugWritingMode { + mode: WritingMode, +} + +#[cfg(not(debug_assertions))] +impl DebugWritingMode { + #[inline] + fn check(&self, _other: WritingMode) {} + + #[inline] + fn check_debug(&self, _other: DebugWritingMode) {} + + #[inline] + fn new(_mode: WritingMode) -> DebugWritingMode { + DebugWritingMode + } +} + +#[cfg(debug_assertions)] +impl DebugWritingMode { + #[inline] + fn check(&self, other: WritingMode) { + assert_eq!(self.mode, other) + } + + #[inline] + fn check_debug(&self, other: DebugWritingMode) { + assert_eq!(self.mode, other.mode) + } + + #[inline] + fn new(mode: WritingMode) -> DebugWritingMode { + DebugWritingMode { mode } + } +} + +impl Debug for DebugWritingMode { + #[cfg(not(debug_assertions))] + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!(formatter, "?") + } + + #[cfg(debug_assertions)] + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!(formatter, "{}", self.mode) + } +} + +// Used to specify the logical direction. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub enum Direction { + Inline, + Block, +} + +/// A 2D size in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalSize<T> { + pub inline: T, // inline-size, a.k.a. logical width, a.k.a. measure + pub block: T, // block-size, a.k.a. logical height, a.k.a. extent + debug_writing_mode: DebugWritingMode, +} + +impl<T: Debug> Debug for LogicalSize<T> { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!( + formatter, + "LogicalSize({:?}, i{:?}×b{:?})", + self.debug_writing_mode, self.inline, self.block + ) + } +} + +// Can not implement the Zero trait: its zero() method does not have the `mode` parameter. +impl<T: Zero> LogicalSize<T> { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalSize<T> { + LogicalSize { + inline: Zero::zero(), + block: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl<T> LogicalSize<T> { + #[inline] + pub fn new(mode: WritingMode, inline: T, block: T) -> LogicalSize<T> { + LogicalSize { + inline: inline, + block: block, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_physical(mode: WritingMode, size: Size2D<T>) -> LogicalSize<T> { + if mode.is_vertical() { + LogicalSize::new(mode, size.height, size.width) + } else { + LogicalSize::new(mode, size.width, size.height) + } + } +} + +impl<T: Copy> LogicalSize<T> { + #[inline] + pub fn width(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block + } else { + self.inline + } + } + + #[inline] + pub fn set_width(&mut self, mode: WritingMode, width: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block = width + } else { + self.inline = width + } + } + + #[inline] + pub fn height(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline + } else { + self.block + } + } + + #[inline] + pub fn set_height(&mut self, mode: WritingMode, height: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline = height + } else { + self.block = height + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode) -> Size2D<T> { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + Size2D::new(self.block, self.inline) + } else { + Size2D::new(self.inline, self.block) + } + } + + #[inline] + pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalSize<T> { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalSize::from_physical(mode_to, self.to_physical(mode_from)) + } + } +} + +impl<T: Add<T, Output = T>> Add for LogicalSize<T> { + type Output = LogicalSize<T>; + + #[inline] + fn add(self, other: LogicalSize<T>) -> LogicalSize<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalSize { + debug_writing_mode: self.debug_writing_mode, + inline: self.inline + other.inline, + block: self.block + other.block, + } + } +} + +impl<T: Sub<T, Output = T>> Sub for LogicalSize<T> { + type Output = LogicalSize<T>; + + #[inline] + fn sub(self, other: LogicalSize<T>) -> LogicalSize<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalSize { + debug_writing_mode: self.debug_writing_mode, + inline: self.inline - other.inline, + block: self.block - other.block, + } + } +} + +/// A 2D point in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalPoint<T> { + /// inline-axis coordinate + pub i: T, + /// block-axis coordinate + pub b: T, + debug_writing_mode: DebugWritingMode, +} + +impl<T: Debug> Debug for LogicalPoint<T> { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + write!( + formatter, + "LogicalPoint({:?} (i{:?}, b{:?}))", + self.debug_writing_mode, self.i, self.b + ) + } +} + +// Can not implement the Zero trait: its zero() method does not have the `mode` parameter. +impl<T: Zero> LogicalPoint<T> { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalPoint<T> { + LogicalPoint { + i: Zero::zero(), + b: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl<T: Copy> LogicalPoint<T> { + #[inline] + pub fn new(mode: WritingMode, i: T, b: T) -> LogicalPoint<T> { + LogicalPoint { + i: i, + b: b, + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl<T: Copy + Sub<T, Output = T>> LogicalPoint<T> { + #[inline] + pub fn from_physical( + mode: WritingMode, + point: Point2D<T>, + container_size: Size2D<T>, + ) -> LogicalPoint<T> { + if mode.is_vertical() { + LogicalPoint { + i: if mode.is_inline_tb() { + point.y + } else { + container_size.height - point.y + }, + b: if mode.is_vertical_lr() { + point.x + } else { + container_size.width - point.x + }, + debug_writing_mode: DebugWritingMode::new(mode), + } + } else { + LogicalPoint { + i: if mode.is_bidi_ltr() { + point.x + } else { + container_size.width - point.x + }, + b: point.y, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + } + + #[inline] + pub fn x(&self, mode: WritingMode, container_size: Size2D<T>) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.b + } else { + container_size.width - self.b + } + } else { + if mode.is_bidi_ltr() { + self.i + } else { + container_size.width - self.i + } + } + } + + #[inline] + pub fn set_x(&mut self, mode: WritingMode, x: T, container_size: Size2D<T>) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.b = if mode.is_vertical_lr() { + x + } else { + container_size.width - x + } + } else { + self.i = if mode.is_bidi_ltr() { + x + } else { + container_size.width - x + } + } + } + + #[inline] + pub fn y(&self, mode: WritingMode, container_size: Size2D<T>) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.i + } else { + container_size.height - self.i + } + } else { + self.b + } + } + + #[inline] + pub fn set_y(&mut self, mode: WritingMode, y: T, container_size: Size2D<T>) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.i = if mode.is_inline_tb() { + y + } else { + container_size.height - y + } + } else { + self.b = y + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode, container_size: Size2D<T>) -> Point2D<T> { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + Point2D::new( + if mode.is_vertical_lr() { + self.b + } else { + container_size.width - self.b + }, + if mode.is_inline_tb() { + self.i + } else { + container_size.height - self.i + }, + ) + } else { + Point2D::new( + if mode.is_bidi_ltr() { + self.i + } else { + container_size.width - self.i + }, + self.b, + ) + } + } + + #[inline] + pub fn convert( + &self, + mode_from: WritingMode, + mode_to: WritingMode, + container_size: Size2D<T>, + ) -> LogicalPoint<T> { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalPoint::from_physical( + mode_to, + self.to_physical(mode_from, container_size), + container_size, + ) + } + } +} + +impl<T: Copy + Add<T, Output = T>> LogicalPoint<T> { + /// This doesn’t really makes sense, + /// but happens when dealing with multiple origins. + #[inline] + pub fn add_point(&self, other: &LogicalPoint<T>) -> LogicalPoint<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i + other.i, + b: self.b + other.b, + } + } +} + +impl<T: Copy + Add<T, Output = T>> Add<LogicalSize<T>> for LogicalPoint<T> { + type Output = LogicalPoint<T>; + + #[inline] + fn add(self, other: LogicalSize<T>) -> LogicalPoint<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i + other.inline, + b: self.b + other.block, + } + } +} + +impl<T: Copy + Sub<T, Output = T>> Sub<LogicalSize<T>> for LogicalPoint<T> { + type Output = LogicalPoint<T>; + + #[inline] + fn sub(self, other: LogicalSize<T>) -> LogicalPoint<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalPoint { + debug_writing_mode: self.debug_writing_mode, + i: self.i - other.inline, + b: self.b - other.block, + } + } +} + +/// A "margin" in flow-relative dimensions +/// Represents the four sides of the margins, borders, or padding of a CSS box, +/// or a combination of those. +/// A positive "margin" can be added to a rectangle to obtain a bigger rectangle. +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalMargin<T> { + pub block_start: T, + pub inline_end: T, + pub block_end: T, + pub inline_start: T, + debug_writing_mode: DebugWritingMode, +} + +impl<T: Debug> Debug for LogicalMargin<T> { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + let writing_mode_string = if cfg!(debug_assertions) { + format!("{:?}, ", self.debug_writing_mode) + } else { + "".to_owned() + }; + + write!( + formatter, + "LogicalMargin({}i:{:?}..{:?} b:{:?}..{:?})", + writing_mode_string, + self.inline_start, + self.inline_end, + self.block_start, + self.block_end + ) + } +} + +impl<T: Zero> LogicalMargin<T> { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalMargin<T> { + LogicalMargin { + block_start: Zero::zero(), + inline_end: Zero::zero(), + block_end: Zero::zero(), + inline_start: Zero::zero(), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl<T> LogicalMargin<T> { + #[inline] + pub fn new( + mode: WritingMode, + block_start: T, + inline_end: T, + block_end: T, + inline_start: T, + ) -> LogicalMargin<T> { + LogicalMargin { + block_start, + inline_end, + block_end, + inline_start, + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_physical(mode: WritingMode, offsets: SideOffsets2D<T>) -> LogicalMargin<T> { + let block_start; + let inline_end; + let block_end; + let inline_start; + if mode.is_vertical() { + if mode.is_vertical_lr() { + block_start = offsets.left; + block_end = offsets.right; + } else { + block_start = offsets.right; + block_end = offsets.left; + } + if mode.is_inline_tb() { + inline_start = offsets.top; + inline_end = offsets.bottom; + } else { + inline_start = offsets.bottom; + inline_end = offsets.top; + } + } else { + block_start = offsets.top; + block_end = offsets.bottom; + if mode.is_bidi_ltr() { + inline_start = offsets.left; + inline_end = offsets.right; + } else { + inline_start = offsets.right; + inline_end = offsets.left; + } + } + LogicalMargin::new(mode, block_start, inline_end, block_end, inline_start) + } +} + +impl<T: Copy> LogicalMargin<T> { + #[inline] + pub fn new_all_same(mode: WritingMode, value: T) -> LogicalMargin<T> { + LogicalMargin::new(mode, value, value, value, value) + } + + #[inline] + pub fn top(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_start + } else { + self.inline_end + } + } else { + self.block_start + } + } + + #[inline] + pub fn set_top(&mut self, mode: WritingMode, top: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_start = top + } else { + self.inline_end = top + } + } else { + self.block_start = top + } + } + + #[inline] + pub fn right(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_end + } else { + self.block_start + } + } else { + if mode.is_bidi_ltr() { + self.inline_end + } else { + self.inline_start + } + } + } + + #[inline] + pub fn set_right(&mut self, mode: WritingMode, right: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_end = right + } else { + self.block_start = right + } + } else { + if mode.is_bidi_ltr() { + self.inline_end = right + } else { + self.inline_start = right + } + } + } + + #[inline] + pub fn bottom(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_end + } else { + self.inline_start + } + } else { + self.block_end + } + } + + #[inline] + pub fn set_bottom(&mut self, mode: WritingMode, bottom: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_inline_tb() { + self.inline_end = bottom + } else { + self.inline_start = bottom + } + } else { + self.block_end = bottom + } + } + + #[inline] + pub fn left(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_start + } else { + self.block_end + } + } else { + if mode.is_bidi_ltr() { + self.inline_start + } else { + self.inline_end + } + } + } + + #[inline] + pub fn set_left(&mut self, mode: WritingMode, left: T) { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + if mode.is_vertical_lr() { + self.block_start = left + } else { + self.block_end = left + } + } else { + if mode.is_bidi_ltr() { + self.inline_start = left + } else { + self.inline_end = left + } + } + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode) -> SideOffsets2D<T> { + self.debug_writing_mode.check(mode); + let top; + let right; + let bottom; + let left; + if mode.is_vertical() { + if mode.is_vertical_lr() { + left = self.block_start; + right = self.block_end; + } else { + right = self.block_start; + left = self.block_end; + } + if mode.is_inline_tb() { + top = self.inline_start; + bottom = self.inline_end; + } else { + bottom = self.inline_start; + top = self.inline_end; + } + } else { + top = self.block_start; + bottom = self.block_end; + if mode.is_bidi_ltr() { + left = self.inline_start; + right = self.inline_end; + } else { + right = self.inline_start; + left = self.inline_end; + } + } + SideOffsets2D::new(top, right, bottom, left) + } + + #[inline] + pub fn convert(&self, mode_from: WritingMode, mode_to: WritingMode) -> LogicalMargin<T> { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalMargin::from_physical(mode_to, self.to_physical(mode_from)) + } + } +} + +impl<T: PartialEq + Zero> LogicalMargin<T> { + #[inline] + pub fn is_zero(&self) -> bool { + self.block_start == Zero::zero() && + self.inline_end == Zero::zero() && + self.block_end == Zero::zero() && + self.inline_start == Zero::zero() + } +} + +impl<T: Copy + Add<T, Output = T>> LogicalMargin<T> { + #[inline] + pub fn inline_start_end(&self) -> T { + self.inline_start + self.inline_end + } + + #[inline] + pub fn block_start_end(&self) -> T { + self.block_start + self.block_end + } + + #[inline] + pub fn start_end(&self, direction: Direction) -> T { + match direction { + Direction::Inline => self.inline_start + self.inline_end, + Direction::Block => self.block_start + self.block_end, + } + } + + #[inline] + pub fn top_bottom(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.inline_start_end() + } else { + self.block_start_end() + } + } + + #[inline] + pub fn left_right(&self, mode: WritingMode) -> T { + self.debug_writing_mode.check(mode); + if mode.is_vertical() { + self.block_start_end() + } else { + self.inline_start_end() + } + } +} + +impl<T: Add<T, Output = T>> Add for LogicalMargin<T> { + type Output = LogicalMargin<T>; + + #[inline] + fn add(self, other: LogicalMargin<T>) -> LogicalMargin<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalMargin { + debug_writing_mode: self.debug_writing_mode, + block_start: self.block_start + other.block_start, + inline_end: self.inline_end + other.inline_end, + block_end: self.block_end + other.block_end, + inline_start: self.inline_start + other.inline_start, + } + } +} + +impl<T: Sub<T, Output = T>> Sub for LogicalMargin<T> { + type Output = LogicalMargin<T>; + + #[inline] + fn sub(self, other: LogicalMargin<T>) -> LogicalMargin<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalMargin { + debug_writing_mode: self.debug_writing_mode, + block_start: self.block_start - other.block_start, + inline_end: self.inline_end - other.inline_end, + block_end: self.block_end - other.block_end, + inline_start: self.inline_start - other.inline_start, + } + } +} + +/// A rectangle in flow-relative dimensions +#[derive(Clone, Copy, Eq, PartialEq)] +#[cfg_attr(feature = "servo", derive(Serialize))] +pub struct LogicalRect<T> { + pub start: LogicalPoint<T>, + pub size: LogicalSize<T>, + debug_writing_mode: DebugWritingMode, +} + +impl<T: Debug> Debug for LogicalRect<T> { + fn fmt(&self, formatter: &mut Formatter) -> Result<(), Error> { + let writing_mode_string = if cfg!(debug_assertions) { + format!("{:?}, ", self.debug_writing_mode) + } else { + "".to_owned() + }; + + write!( + formatter, + "LogicalRect({}i{:?}×b{:?}, @ (i{:?},b{:?}))", + writing_mode_string, self.size.inline, self.size.block, self.start.i, self.start.b + ) + } +} + +impl<T: Zero> LogicalRect<T> { + #[inline] + pub fn zero(mode: WritingMode) -> LogicalRect<T> { + LogicalRect { + start: LogicalPoint::zero(mode), + size: LogicalSize::zero(mode), + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl<T: Copy> LogicalRect<T> { + #[inline] + pub fn new( + mode: WritingMode, + inline_start: T, + block_start: T, + inline: T, + block: T, + ) -> LogicalRect<T> { + LogicalRect { + start: LogicalPoint::new(mode, inline_start, block_start), + size: LogicalSize::new(mode, inline, block), + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn from_point_size( + mode: WritingMode, + start: LogicalPoint<T>, + size: LogicalSize<T>, + ) -> LogicalRect<T> { + start.debug_writing_mode.check(mode); + size.debug_writing_mode.check(mode); + LogicalRect { + start: start, + size: size, + debug_writing_mode: DebugWritingMode::new(mode), + } + } +} + +impl<T: Copy + Add<T, Output = T> + Sub<T, Output = T>> LogicalRect<T> { + #[inline] + pub fn from_physical( + mode: WritingMode, + rect: Rect<T>, + container_size: Size2D<T>, + ) -> LogicalRect<T> { + let inline_start; + let block_start; + let inline; + let block; + if mode.is_vertical() { + inline = rect.size.height; + block = rect.size.width; + if mode.is_vertical_lr() { + block_start = rect.origin.x; + } else { + block_start = container_size.width - (rect.origin.x + rect.size.width); + } + if mode.is_inline_tb() { + inline_start = rect.origin.y; + } else { + inline_start = container_size.height - (rect.origin.y + rect.size.height); + } + } else { + inline = rect.size.width; + block = rect.size.height; + block_start = rect.origin.y; + if mode.is_bidi_ltr() { + inline_start = rect.origin.x; + } else { + inline_start = container_size.width - (rect.origin.x + rect.size.width); + } + } + LogicalRect { + start: LogicalPoint::new(mode, inline_start, block_start), + size: LogicalSize::new(mode, inline, block), + debug_writing_mode: DebugWritingMode::new(mode), + } + } + + #[inline] + pub fn inline_end(&self) -> T { + self.start.i + self.size.inline + } + + #[inline] + pub fn block_end(&self) -> T { + self.start.b + self.size.block + } + + #[inline] + pub fn to_physical(&self, mode: WritingMode, container_size: Size2D<T>) -> Rect<T> { + self.debug_writing_mode.check(mode); + let x; + let y; + let width; + let height; + if mode.is_vertical() { + width = self.size.block; + height = self.size.inline; + if mode.is_vertical_lr() { + x = self.start.b; + } else { + x = container_size.width - self.block_end(); + } + if mode.is_inline_tb() { + y = self.start.i; + } else { + y = container_size.height - self.inline_end(); + } + } else { + width = self.size.inline; + height = self.size.block; + y = self.start.b; + if mode.is_bidi_ltr() { + x = self.start.i; + } else { + x = container_size.width - self.inline_end(); + } + } + Rect { + origin: Point2D::new(x, y), + size: Size2D::new(width, height), + } + } + + #[inline] + pub fn convert( + &self, + mode_from: WritingMode, + mode_to: WritingMode, + container_size: Size2D<T>, + ) -> LogicalRect<T> { + if mode_from == mode_to { + self.debug_writing_mode.check(mode_from); + *self + } else { + LogicalRect::from_physical( + mode_to, + self.to_physical(mode_from, container_size), + container_size, + ) + } + } + + pub fn translate_by_size(&self, offset: LogicalSize<T>) -> LogicalRect<T> { + LogicalRect { + start: self.start + offset, + ..*self + } + } + + pub fn translate(&self, offset: &LogicalPoint<T>) -> LogicalRect<T> { + LogicalRect { + start: self.start + + LogicalSize { + inline: offset.i, + block: offset.b, + debug_writing_mode: offset.debug_writing_mode, + }, + size: self.size, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl<T: Copy + Ord + Add<T, Output = T> + Sub<T, Output = T>> LogicalRect<T> { + #[inline] + pub fn union(&self, other: &LogicalRect<T>) -> LogicalRect<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + + let inline_start = min(self.start.i, other.start.i); + let block_start = min(self.start.b, other.start.b); + LogicalRect { + start: LogicalPoint { + i: inline_start, + b: block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: max(self.inline_end(), other.inline_end()) - inline_start, + block: max(self.block_end(), other.block_end()) - block_start, + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl<T: Copy + Add<T, Output = T> + Sub<T, Output = T>> Add<LogicalMargin<T>> for LogicalRect<T> { + type Output = LogicalRect<T>; + + #[inline] + fn add(self, other: LogicalMargin<T>) -> LogicalRect<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalRect { + start: LogicalPoint { + // Growing a rectangle on the start side means pushing its + // start point on the negative direction. + i: self.start.i - other.inline_start, + b: self.start.b - other.block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: self.size.inline + other.inline_start_end(), + block: self.size.block + other.block_start_end(), + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +impl<T: Copy + Add<T, Output = T> + Sub<T, Output = T>> Sub<LogicalMargin<T>> for LogicalRect<T> { + type Output = LogicalRect<T>; + + #[inline] + fn sub(self, other: LogicalMargin<T>) -> LogicalRect<T> { + self.debug_writing_mode + .check_debug(other.debug_writing_mode); + LogicalRect { + start: LogicalPoint { + // Shrinking a rectangle on the start side means pushing its + // start point on the positive direction. + i: self.start.i + other.inline_start, + b: self.start.b + other.block_start, + debug_writing_mode: self.debug_writing_mode, + }, + size: LogicalSize { + inline: self.size.inline - other.inline_start_end(), + block: self.size.block - other.block_start_end(), + debug_writing_mode: self.debug_writing_mode, + }, + debug_writing_mode: self.debug_writing_mode, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum LogicalAxis { + Block = 0, + Inline, +} + +impl LogicalAxis { + #[inline] + pub fn to_physical(self, wm: WritingMode) -> PhysicalAxis { + if wm.is_horizontal() == (self == Self::Inline) { + PhysicalAxis::Horizontal + } else { + PhysicalAxis::Vertical + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum LogicalSide { + BlockStart = 0, + BlockEnd, + InlineStart, + InlineEnd, +} + +impl LogicalSide { + fn is_block(self) -> bool { + matches!(self, Self::BlockStart | Self::BlockEnd) + } + + #[inline] + pub fn to_physical(self, wm: WritingMode) -> PhysicalSide { + // Block mapping depends only on vertical+vertical-lr + static BLOCK_MAPPING: [[PhysicalSide; 2]; 4] = [ + [PhysicalSide::Top, PhysicalSide::Bottom], // horizontal-tb + [PhysicalSide::Right, PhysicalSide::Left], // vertical-rl + [PhysicalSide::Bottom, PhysicalSide::Top], // (horizontal-bt) + [PhysicalSide::Left, PhysicalSide::Right], // vertical-lr + ]; + + if self.is_block() { + let vertical = wm.is_vertical(); + let lr = wm.is_vertical_lr(); + let index = (vertical as usize) | ((lr as usize) << 1); + return BLOCK_MAPPING[index][self as usize]; + } + + // start = 0, end = 1 + let edge = self as usize - 2; + // Inline axis sides depend on all three of writing-mode, text-orientation and direction, + // which are encoded in the VERTICAL, INLINE_REVERSED, VERTICAL_LR and LINE_INVERTED bits. + // + // bit 0 = the VERTICAL value + // bit 1 = the INLINE_REVERSED value + // bit 2 = the VERTICAL_LR value + // bit 3 = the LINE_INVERTED value + // + // Note that not all of these combinations can actually be specified via CSS: there is no + // horizontal-bt writing-mode, and no text-orientation value that produces "inverted" + // text. (The former 'sideways-left' value, no longer in the spec, would have produced + // this in vertical-rl mode.) + static INLINE_MAPPING: [[PhysicalSide; 2]; 16] = [ + [PhysicalSide::Left, PhysicalSide::Right], // horizontal-tb ltr + [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-rl ltr + [PhysicalSide::Right, PhysicalSide::Left], // horizontal-tb rtl + [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-rl rtl + [PhysicalSide::Right, PhysicalSide::Left], // (horizontal-bt) (inverted) ltr + [PhysicalSide::Top, PhysicalSide::Bottom], // sideways-lr rtl + [PhysicalSide::Left, PhysicalSide::Right], // (horizontal-bt) (inverted) rtl + [PhysicalSide::Bottom, PhysicalSide::Top], // sideways-lr ltr + [PhysicalSide::Left, PhysicalSide::Right], // horizontal-tb (inverted) rtl + [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-rl (inverted) rtl + [PhysicalSide::Right, PhysicalSide::Left], // horizontal-tb (inverted) ltr + [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-rl (inverted) ltr + [PhysicalSide::Left, PhysicalSide::Right], // (horizontal-bt) ltr + [PhysicalSide::Top, PhysicalSide::Bottom], // vertical-lr ltr + [PhysicalSide::Right, PhysicalSide::Left], // (horizontal-bt) rtl + [PhysicalSide::Bottom, PhysicalSide::Top], // vertical-lr rtl + ]; + + debug_assert!( + WritingMode::VERTICAL.bits() == 0x01 && + WritingMode::INLINE_REVERSED.bits() == 0x02 && + WritingMode::VERTICAL_LR.bits() == 0x04 && + WritingMode::LINE_INVERTED.bits() == 0x08 + ); + let index = (wm.bits() & 0xF) as usize; + INLINE_MAPPING[index][edge] + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum LogicalCorner { + StartStart = 0, + StartEnd, + EndStart, + EndEnd, +} + +impl LogicalCorner { + #[inline] + pub fn to_physical(self, wm: WritingMode) -> PhysicalCorner { + static CORNER_TO_SIDES: [[LogicalSide; 2]; 4] = [ + [LogicalSide::BlockStart, LogicalSide::InlineStart], + [LogicalSide::BlockStart, LogicalSide::InlineEnd], + [LogicalSide::BlockEnd, LogicalSide::InlineStart], + [LogicalSide::BlockEnd, LogicalSide::InlineEnd], + ]; + + let [block, inline] = CORNER_TO_SIDES[self as usize]; + let block = block.to_physical(wm); + let inline = inline.to_physical(wm); + PhysicalCorner::from_sides(block, inline) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum PhysicalAxis { + Vertical = 0, + Horizontal, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum PhysicalSide { + Top = 0, + Right, + Bottom, + Left, +} + +impl PhysicalSide { + fn orthogonal_to(self, other: Self) -> bool { + matches!(self, Self::Top | Self::Bottom) != matches!(other, Self::Top | Self::Bottom) + } +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum PhysicalCorner { + TopLeft = 0, + TopRight, + BottomRight, + BottomLeft, +} + +impl PhysicalCorner { + fn from_sides(a: PhysicalSide, b: PhysicalSide) -> Self { + debug_assert!(a.orthogonal_to(b), "Sides should be orthogonal"); + // Only some of these are possible, since we expect only orthogonal values. If the two + // sides were to be parallel, we fall back to returning TopLeft. + const IMPOSSIBLE: PhysicalCorner = PhysicalCorner::TopLeft; + static SIDES_TO_CORNER: [[PhysicalCorner; 4]; 4] = [ + [ + IMPOSSIBLE, + PhysicalCorner::TopRight, + IMPOSSIBLE, + PhysicalCorner::TopLeft, + ], + [ + PhysicalCorner::TopRight, + IMPOSSIBLE, + PhysicalCorner::BottomRight, + IMPOSSIBLE, + ], + [ + IMPOSSIBLE, + PhysicalCorner::BottomRight, + IMPOSSIBLE, + PhysicalCorner::BottomLeft, + ], + [ + PhysicalCorner::TopLeft, + IMPOSSIBLE, + PhysicalCorner::BottomLeft, + IMPOSSIBLE, + ], + ]; + SIDES_TO_CORNER[a as usize][b as usize] + } +} diff --git a/servo/components/style/macros.rs b/servo/components/style/macros.rs new file mode 100644 index 0000000000..4795345185 --- /dev/null +++ b/servo/components/style/macros.rs @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Various macro helpers. + +macro_rules! exclusive_value { + (($value:ident, $set:expr) => $ident:path) => { + if $value.intersects($set) { + return Err(()); + } else { + $ident + } + }; +} + +#[cfg(feature = "gecko")] +macro_rules! impl_gecko_keyword_conversions { + ($name:ident, $utype:ty) => { + impl From<$utype> for $name { + fn from(bits: $utype) -> $name { + $name::from_gecko_keyword(bits) + } + } + + impl From<$name> for $utype { + fn from(v: $name) -> $utype { + v.to_gecko_keyword() + } + } + }; +} + +macro_rules! trivial_to_computed_value { + ($name:ty) => { + impl $crate::values::computed::ToComputedValue for $name { + type ComputedValue = $name; + + fn to_computed_value(&self, _: &$crate::values::computed::Context) -> Self { + self.clone() + } + + fn from_computed_value(other: &Self) -> Self { + other.clone() + } + } + }; +} + +/// A macro to parse an identifier, or return an `UnexpectedIdent` error +/// otherwise. +/// +/// FIXME(emilio): The fact that `UnexpectedIdent` is a `SelectorParseError` +/// doesn't make a lot of sense to me. +macro_rules! try_match_ident_ignore_ascii_case { + ($input:expr, $( $match_body:tt )*) => {{ + let location = $input.current_source_location(); + let ident = $input.expect_ident()?; + match_ignore_ascii_case! { &ident, + $( $match_body )* + _ => return Err(location.new_custom_error( + ::selectors::parser::SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )) + } + }} +} + +#[cfg(feature = "servo")] +macro_rules! local_name { + ($s:tt) => { + $crate::values::GenericAtomIdent(html5ever::local_name!($s)) + }; +} + +#[cfg(feature = "servo")] +macro_rules! ns { + () => { + $crate::values::GenericAtomIdent(html5ever::ns!()) + }; + ($s:tt) => { + $crate::values::GenericAtomIdent(html5ever::ns!($s)) + }; +} + +#[cfg(feature = "gecko")] +macro_rules! local_name { + ($s:tt) => { + $crate::values::AtomIdent(atom!($s)) + }; +} + +/// Asserts the size of a type at compile time. +macro_rules! size_of_test { + ($t: ty, $expected_size: expr) => { + #[cfg(target_pointer_width = "64")] + const_assert_eq!(std::mem::size_of::<$t>(), $expected_size); + }; +} diff --git a/servo/components/style/matching.rs b/servo/components/style/matching.rs new file mode 100644 index 0000000000..646d08a9e3 --- /dev/null +++ b/servo/components/style/matching.rs @@ -0,0 +1,1128 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! High-level interface to CSS selector matching. + +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{CascadeInputs, ElementCascadeInputs, QuirksMode}; +use crate::context::{SharedStyleContext, StyleContext}; +use crate::data::{ElementData, ElementStyles}; +use crate::dom::TElement; +#[cfg(feature = "servo")] +use crate::dom::TNode; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::ComputedValues; +use crate::properties::PropertyDeclarationBlock; +use crate::rule_tree::{CascadeLevel, StrongRuleNode}; +use crate::selector_parser::{PseudoElement, RestyleDamage}; +use crate::shared_lock::Locked; +use crate::style_resolver::ResolvedElementStyles; +use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement}; +use crate::stylesheets::layer_rule::LayerOrder; +use crate::stylist::RuleInclusion; +use crate::traversal_flags::TraversalFlags; +use servo_arc::{Arc, ArcBorrow}; + +/// Represents the result of comparing an element's old and new style. +#[derive(Debug)] +pub struct StyleDifference { + /// The resulting damage. + pub damage: RestyleDamage, + + /// Whether any styles changed. + pub change: StyleChange, +} + +/// Represents whether or not the style of an element has changed. +#[derive(Clone, Copy, Debug)] +pub enum StyleChange { + /// The style hasn't changed. + Unchanged, + /// The style has changed. + Changed { + /// Whether only reset structs changed. + reset_only: bool, + }, +} + +/// Whether or not newly computed values for an element need to be cascaded to +/// children (or children might need to be re-matched, e.g., for container +/// queries). +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum ChildRestyleRequirement { + /// Old and new computed values were the same, or we otherwise know that + /// we won't bother recomputing style for children, so we can skip cascading + /// the new values into child elements. + CanSkipCascade = 0, + /// The same as `MustCascadeChildren`, but we only need to actually + /// recascade if the child inherits any explicit reset style. + MustCascadeChildrenIfInheritResetStyle = 1, + /// Old and new computed values were different, so we must cascade the + /// new values to children. + MustCascadeChildren = 2, + /// The same as `MustCascadeChildren`, but for the entire subtree. This is + /// used to handle root font-size updates needing to recascade the whole + /// document. + MustCascadeDescendants = 3, + /// We need to re-match the whole subttree. This is used to handle container + /// query relative unit changes for example. Container query size changes + /// also trigger re-match, but after layout. + MustMatchDescendants = 4, +} + +/// Determines which styles are being cascaded currently. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CascadeVisitedMode { + /// Cascade the regular, unvisited styles. + Unvisited, + /// Cascade the styles used when an element's relevant link is visited. A + /// "relevant link" is the element being matched if it is a link or the + /// nearest ancestor link. + Visited, +} + +trait PrivateMatchMethods: TElement { + fn replace_single_rule_node( + context: &SharedStyleContext, + level: CascadeLevel, + layer_order: LayerOrder, + pdb: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>, + path: &mut StrongRuleNode, + ) -> bool { + let stylist = &context.stylist; + let guards = &context.guards; + + let mut important_rules_changed = false; + let new_node = stylist.rule_tree().update_rule_at_level( + level, + layer_order, + pdb, + path, + guards, + &mut important_rules_changed, + ); + if let Some(n) = new_node { + *path = n; + } + important_rules_changed + } + + /// Updates the rule nodes without re-running selector matching, using just + /// the rule tree, for a specific visited mode. + /// + /// Returns true if an !important rule was replaced. + fn replace_rules_internal( + &self, + replacements: RestyleHint, + context: &mut StyleContext<Self>, + cascade_visited: CascadeVisitedMode, + cascade_inputs: &mut ElementCascadeInputs, + ) -> bool { + debug_assert!( + replacements.intersects(RestyleHint::replacements()) && + (replacements & !RestyleHint::replacements()).is_empty() + ); + + let primary_rules = match cascade_visited { + CascadeVisitedMode::Unvisited => cascade_inputs.primary.rules.as_mut(), + CascadeVisitedMode::Visited => cascade_inputs.primary.visited_rules.as_mut(), + }; + + let primary_rules = match primary_rules { + Some(r) => r, + None => return false, + }; + + if !context.shared.traversal_flags.for_animation_only() { + let mut result = false; + if replacements.contains(RestyleHint::RESTYLE_STYLE_ATTRIBUTE) { + let style_attribute = self.style_attribute(); + result |= Self::replace_single_rule_node( + context.shared, + CascadeLevel::same_tree_author_normal(), + LayerOrder::root(), + style_attribute, + primary_rules, + ); + result |= Self::replace_single_rule_node( + context.shared, + CascadeLevel::same_tree_author_important(), + LayerOrder::root(), + style_attribute, + primary_rules, + ); + // FIXME(emilio): Still a hack! + self.unset_dirty_style_attribute(); + } + return result; + } + + // Animation restyle hints are processed prior to other restyle + // hints in the animation-only traversal. + // + // Non-animation restyle hints will be processed in a subsequent + // normal traversal. + if replacements.intersects(RestyleHint::for_animations()) { + debug_assert!(context.shared.traversal_flags.for_animation_only()); + + if replacements.contains(RestyleHint::RESTYLE_SMIL) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::SMILOverride, + LayerOrder::root(), + self.smil_override(), + primary_rules, + ); + } + + if replacements.contains(RestyleHint::RESTYLE_CSS_TRANSITIONS) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::Transitions, + LayerOrder::root(), + self.transition_rule(&context.shared) + .as_ref() + .map(|a| a.borrow_arc()), + primary_rules, + ); + } + + if replacements.contains(RestyleHint::RESTYLE_CSS_ANIMATIONS) { + Self::replace_single_rule_node( + context.shared, + CascadeLevel::Animations, + LayerOrder::root(), + self.animation_rule(&context.shared) + .as_ref() + .map(|a| a.borrow_arc()), + primary_rules, + ); + } + } + + false + } + + /// If there is no transition rule in the ComputedValues, it returns None. + fn after_change_style( + &self, + context: &mut StyleContext<Self>, + primary_style: &Arc<ComputedValues>, + ) -> Option<Arc<ComputedValues>> { + let rule_node = primary_style.rules(); + let without_transition_rules = context + .shared + .stylist + .rule_tree() + .remove_transition_rule_if_applicable(rule_node); + if without_transition_rules == *rule_node { + // We don't have transition rule in this case, so return None to let + // the caller use the original ComputedValues. + return None; + } + + // FIXME(bug 868975): We probably need to transition visited style as + // well. + let inputs = CascadeInputs { + rules: Some(without_transition_rules), + visited_rules: primary_style.visited_rules().cloned(), + flags: primary_style.flags.for_cascade_inputs(), + }; + + // Actually `PseudoElementResolution` doesn't really matter. + let style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_with_default_parents(inputs); + + Some(style.0) + } + + fn needs_animations_update( + &self, + context: &mut StyleContext<Self>, + old_style: Option<&ComputedValues>, + new_style: &ComputedValues, + pseudo_element: Option<PseudoElement>, + ) -> bool { + let new_ui_style = new_style.get_ui(); + let new_style_specifies_animations = new_ui_style.specifies_animations(); + + let has_animations = self.has_css_animations(&context.shared, pseudo_element); + if !new_style_specifies_animations && !has_animations { + return false; + } + + let old_style = match old_style { + Some(old) => old, + // If we have no old style but have animations, we may be a + // pseudo-element which was re-created without style changes. + // + // This can happen when we reframe the pseudo-element without + // restyling it (due to content insertion on a flex container or + // such, for example). See bug 1564366. + // + // FIXME(emilio): The really right fix for this is keeping the + // pseudo-element itself around on reframes, but that's a bit + // harder. If we do that we can probably remove quite a lot of the + // EffectSet complexity though, since right now it's stored on the + // parent element for pseudo-elements given we need to keep it + // around... + None => { + return new_style_specifies_animations || new_style.is_pseudo_style(); + }, + }; + + let old_ui_style = old_style.get_ui(); + + let keyframes_could_have_changed = context + .shared + .traversal_flags + .contains(TraversalFlags::ForCSSRuleChanges); + + // If the traversal is triggered due to changes in CSS rules changes, we + // need to try to update all CSS animations on the element if the + // element has or will have CSS animation style regardless of whether + // the animation is running or not. + // + // TODO: We should check which @keyframes were added/changed/deleted and + // update only animations corresponding to those @keyframes. + if keyframes_could_have_changed { + return true; + } + + // If the animations changed, well... + if !old_ui_style.animations_equals(new_ui_style) { + return true; + } + + let old_display = old_style.clone_display(); + let new_display = new_style.clone_display(); + + // If we were display: none, we may need to trigger animations. + if old_display == Display::None && new_display != Display::None { + return new_style_specifies_animations; + } + + // If we are becoming display: none, we may need to stop animations. + if old_display != Display::None && new_display == Display::None { + return has_animations; + } + + // We might need to update animations if writing-mode or direction + // changed, and any of the animations contained logical properties. + // + // We may want to be more granular, but it's probably not worth it. + if new_style.writing_mode != old_style.writing_mode { + return has_animations; + } + + false + } + + fn might_need_transitions_update( + &self, + context: &StyleContext<Self>, + old_style: Option<&ComputedValues>, + new_style: &ComputedValues, + pseudo_element: Option<PseudoElement>, + ) -> bool { + let old_style = match old_style { + Some(v) => v, + None => return false, + }; + + if !self.has_css_transitions(context.shared, pseudo_element) && + !new_style.get_ui().specifies_transitions() + { + return false; + } + + if old_style.clone_display().is_none() { + return false; + } + + return true; + } + + /// Create a SequentialTask for resolving descendants in a SMIL display + /// property animation if the display property changed from none. + #[cfg(feature = "gecko")] + fn handle_display_change_for_smil_if_needed( + &self, + context: &mut StyleContext<Self>, + old_values: Option<&ComputedValues>, + new_values: &ComputedValues, + restyle_hints: RestyleHint, + ) { + use crate::context::PostAnimationTasks; + + if !restyle_hints.intersects(RestyleHint::RESTYLE_SMIL) { + return; + } + + if new_values.is_display_property_changed_from_none(old_values) { + // When display value is changed from none to other, we need to + // traverse descendant elements in a subsequent normal + // traversal (we can't traverse them in this animation-only restyle + // since we have no way to know whether the decendants + // need to be traversed at the beginning of the animation-only + // restyle). + let task = crate::context::SequentialTask::process_post_animation( + *self, + PostAnimationTasks::DISPLAY_CHANGED_FROM_NONE_FOR_SMIL, + ); + context.thread_local.tasks.push(task); + } + } + + #[cfg(feature = "gecko")] + fn process_animations( + &self, + context: &mut StyleContext<Self>, + old_styles: &mut ElementStyles, + new_styles: &mut ResolvedElementStyles, + restyle_hint: RestyleHint, + important_rules_changed: bool, + ) { + use crate::context::UpdateAnimationsTasks; + + let new_values = new_styles.primary_style_mut(); + let old_values = &old_styles.primary; + if context.shared.traversal_flags.for_animation_only() { + self.handle_display_change_for_smil_if_needed( + context, + old_values.as_deref(), + new_values, + restyle_hint, + ); + return; + } + + // Bug 868975: These steps should examine and update the visited styles + // in addition to the unvisited styles. + + let mut tasks = UpdateAnimationsTasks::empty(); + + if old_values.as_deref().map_or_else( + || new_values.get_ui().specifies_scroll_timelines(), + |old| !old.get_ui().scroll_timelines_equals(new_values.get_ui()), + ) { + tasks.insert(UpdateAnimationsTasks::SCROLL_TIMELINES); + } + + if old_values.as_deref().map_or_else( + || new_values.get_ui().specifies_view_timelines(), + |old| !old.get_ui().view_timelines_equals(new_values.get_ui()), + ) { + tasks.insert(UpdateAnimationsTasks::VIEW_TIMELINES); + } + + if self.needs_animations_update( + context, + old_values.as_deref(), + new_values, + /* pseudo_element = */ None, + ) { + tasks.insert(UpdateAnimationsTasks::CSS_ANIMATIONS); + } + + let before_change_style = if self.might_need_transitions_update( + context, + old_values.as_deref(), + new_values, + /* pseudo_element = */ None, + ) { + let after_change_style = + if self.has_css_transitions(context.shared, /* pseudo_element = */ None) { + self.after_change_style(context, new_values) + } else { + None + }; + + // In order to avoid creating a SequentialTask for transitions which + // may not be updated, we check it per property to make sure Gecko + // side will really update transition. + let needs_transitions_update = { + // We borrow new_values here, so need to add a scope to make + // sure we release it before assigning a new value to it. + let after_change_style_ref = after_change_style.as_ref().unwrap_or(&new_values); + + self.needs_transitions_update(old_values.as_ref().unwrap(), after_change_style_ref) + }; + + if needs_transitions_update { + if let Some(values_without_transitions) = after_change_style { + *new_values = values_without_transitions; + } + tasks.insert(UpdateAnimationsTasks::CSS_TRANSITIONS); + + // We need to clone old_values into SequentialTask, so we can + // use it later. + old_values.clone() + } else { + None + } + } else { + None + }; + + if self.has_animations(&context.shared) { + tasks.insert(UpdateAnimationsTasks::EFFECT_PROPERTIES); + if important_rules_changed { + tasks.insert(UpdateAnimationsTasks::CASCADE_RESULTS); + } + if new_values.is_display_property_changed_from_none(old_values.as_deref()) { + tasks.insert(UpdateAnimationsTasks::DISPLAY_CHANGED_FROM_NONE); + } + } + + if !tasks.is_empty() { + let task = crate::context::SequentialTask::update_animations( + *self, + before_change_style, + tasks, + ); + context.thread_local.tasks.push(task); + } + } + + #[cfg(feature = "servo")] + fn process_animations( + &self, + context: &mut StyleContext<Self>, + old_styles: &mut ElementStyles, + new_resolved_styles: &mut ResolvedElementStyles, + _restyle_hint: RestyleHint, + _important_rules_changed: bool, + ) { + use crate::animation::AnimationSetKey; + use crate::dom::TDocument; + + let style_changed = self.process_animations_for_style( + context, + &mut old_styles.primary, + new_resolved_styles.primary_style_mut(), + /* pseudo_element = */ None, + ); + + // If we have modified animation or transitions, we recascade style for this node. + if style_changed { + let mut rule_node = new_resolved_styles.primary_style().rules().clone(); + let declarations = context.shared.animations.get_all_declarations( + &AnimationSetKey::new_for_non_pseudo(self.as_node().opaque()), + context.shared.current_time_for_animations, + self.as_node().owner_doc().shared_lock(), + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Transitions, + declarations.transitions.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Animations, + declarations.animations.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + + if rule_node != *new_resolved_styles.primary_style().rules() { + let inputs = CascadeInputs { + rules: Some(rule_node), + visited_rules: new_resolved_styles.primary_style().visited_rules().cloned(), + }; + + new_resolved_styles.primary.style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_with_default_parents(inputs); + } + } + + self.process_animations_for_pseudo( + context, + old_styles, + new_resolved_styles, + PseudoElement::Before, + ); + self.process_animations_for_pseudo( + context, + old_styles, + new_resolved_styles, + PseudoElement::After, + ); + } + + #[cfg(feature = "servo")] + fn process_animations_for_pseudo( + &self, + context: &mut StyleContext<Self>, + old_styles: &mut ElementStyles, + new_resolved_styles: &mut ResolvedElementStyles, + pseudo_element: PseudoElement, + ) { + use crate::animation::AnimationSetKey; + use crate::dom::TDocument; + + let key = AnimationSetKey::new_for_pseudo(self.as_node().opaque(), pseudo_element.clone()); + let mut style = match new_resolved_styles.pseudos.get(&pseudo_element) { + Some(style) => Arc::clone(style), + None => { + context + .shared + .animations + .cancel_all_animations_for_key(&key); + return; + }, + }; + + let mut old_style = old_styles.pseudos.get(&pseudo_element).cloned(); + self.process_animations_for_style( + context, + &mut old_style, + &mut style, + Some(pseudo_element.clone()), + ); + + let declarations = context.shared.animations.get_all_declarations( + &key, + context.shared.current_time_for_animations, + self.as_node().owner_doc().shared_lock(), + ); + if declarations.is_empty() { + return; + } + + let mut rule_node = style.rules().clone(); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Transitions, + LayerOrder::root(), + declarations.transitions.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + Self::replace_single_rule_node( + &context.shared, + CascadeLevel::Animations, + LayerOrder::root(), + declarations.animations.as_ref().map(|a| a.borrow_arc()), + &mut rule_node, + ); + if rule_node == *style.rules() { + return; + } + + let inputs = CascadeInputs { + rules: Some(rule_node), + visited_rules: style.visited_rules().cloned(), + }; + + let new_style = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ) + .cascade_style_and_visited_for_pseudo_with_default_parents( + inputs, + &pseudo_element, + &new_resolved_styles.primary, + ); + + new_resolved_styles + .pseudos + .set(&pseudo_element, new_style.0); + } + + #[cfg(feature = "servo")] + fn process_animations_for_style( + &self, + context: &mut StyleContext<Self>, + old_values: &mut Option<Arc<ComputedValues>>, + new_values: &mut Arc<ComputedValues>, + pseudo_element: Option<PseudoElement>, + ) -> bool { + use crate::animation::{AnimationSetKey, AnimationState}; + + // We need to call this before accessing the `ElementAnimationSet` from the + // map because this call will do a RwLock::read(). + let needs_animations_update = self.needs_animations_update( + context, + old_values.as_deref(), + new_values, + pseudo_element, + ); + + let might_need_transitions_update = self.might_need_transitions_update( + context, + old_values.as_deref(), + new_values, + pseudo_element, + ); + + let mut after_change_style = None; + if might_need_transitions_update { + after_change_style = self.after_change_style(context, new_values); + } + + let key = AnimationSetKey::new(self.as_node().opaque(), pseudo_element); + let shared_context = context.shared; + let mut animation_set = shared_context + .animations + .sets + .write() + .remove(&key) + .unwrap_or_default(); + + // Starting animations is expensive, because we have to recalculate the style + // for all the keyframes. We only want to do this if we think that there's a + // chance that the animations really changed. + if needs_animations_update { + let mut resolver = StyleResolverForElement::new( + *self, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ); + + animation_set.update_animations_for_new_style::<Self>( + *self, + &shared_context, + &new_values, + &mut resolver, + ); + } + + animation_set.update_transitions_for_new_style( + might_need_transitions_update, + &shared_context, + old_values.as_ref(), + after_change_style.as_ref().unwrap_or(new_values), + ); + + // We clear away any finished transitions, but retain animations, because they + // might still be used for proper calculation of `animation-fill-mode`. This + // should change the computed values in the style, so we don't need to mark + // this set as dirty. + animation_set + .transitions + .retain(|transition| transition.state != AnimationState::Finished); + + // If the ElementAnimationSet is empty, and don't store it in order to + // save memory and to avoid extra processing later. + let changed_animations = animation_set.dirty; + if !animation_set.is_empty() { + animation_set.dirty = false; + shared_context + .animations + .sets + .write() + .insert(key, animation_set); + } + + changed_animations + } + + /// Computes and applies non-redundant damage. + fn accumulate_damage_for( + &self, + shared_context: &SharedStyleContext, + damage: &mut RestyleDamage, + old_values: &ComputedValues, + new_values: &ComputedValues, + pseudo: Option<&PseudoElement>, + ) -> ChildRestyleRequirement { + debug!("accumulate_damage_for: {:?}", self); + debug_assert!(!shared_context + .traversal_flags + .contains(TraversalFlags::FinalAnimationTraversal)); + + let difference = self.compute_style_difference(old_values, new_values, pseudo); + + *damage |= difference.damage; + + debug!(" > style difference: {:?}", difference); + + // We need to cascade the children in order to ensure the correct + // propagation of inherited computed value flags. + if old_values.flags.maybe_inherited() != new_values.flags.maybe_inherited() { + debug!( + " > flags changed: {:?} != {:?}", + old_values.flags, new_values.flags + ); + return ChildRestyleRequirement::MustCascadeChildren; + } + + if old_values.effective_zoom != new_values.effective_zoom { + // Zoom changes need to get propagated to children. + debug!( + " > zoom changed: {:?} != {:?}", + old_values.effective_zoom, new_values.effective_zoom + ); + return ChildRestyleRequirement::MustCascadeChildren; + } + + match difference.change { + StyleChange::Unchanged => return ChildRestyleRequirement::CanSkipCascade, + StyleChange::Changed { reset_only } => { + // If inherited properties changed, the best we can do is + // cascade the children. + if !reset_only { + return ChildRestyleRequirement::MustCascadeChildren; + } + }, + } + + let old_display = old_values.clone_display(); + let new_display = new_values.clone_display(); + + if old_display != new_display { + // If we used to be a display: none element, and no longer are, our + // children need to be restyled because they're unstyled. + if old_display == Display::None { + return ChildRestyleRequirement::MustCascadeChildren; + } + // Blockification of children may depend on our display value, + // so we need to actually do the recascade. We could potentially + // do better, but it doesn't seem worth it. + if old_display.is_item_container() != new_display.is_item_container() { + return ChildRestyleRequirement::MustCascadeChildren; + } + // We may also need to blockify and un-blockify descendants if our + // display goes from / to display: contents, since the "layout + // parent style" changes. + if old_display.is_contents() || new_display.is_contents() { + return ChildRestyleRequirement::MustCascadeChildren; + } + // Line break suppression may also be affected if the display + // type changes from ruby to non-ruby. + #[cfg(feature = "gecko")] + { + if old_display.is_ruby_type() != new_display.is_ruby_type() { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + } + + // Children with justify-items: auto may depend on our + // justify-items property value. + // + // Similarly, we could potentially do better, but this really + // seems not common enough to care about. + #[cfg(feature = "gecko")] + { + use crate::values::specified::align::AlignFlags; + + let old_justify_items = old_values.get_position().clone_justify_items(); + let new_justify_items = new_values.get_position().clone_justify_items(); + + let was_legacy_justify_items = + old_justify_items.computed.0.contains(AlignFlags::LEGACY); + + let is_legacy_justify_items = new_justify_items.computed.0.contains(AlignFlags::LEGACY); + + if is_legacy_justify_items != was_legacy_justify_items { + return ChildRestyleRequirement::MustCascadeChildren; + } + + if was_legacy_justify_items && old_justify_items.computed != new_justify_items.computed + { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + + #[cfg(feature = "servo")] + { + // We may need to set or propagate the CAN_BE_FRAGMENTED bit + // on our children. + if old_values.is_multicol() != new_values.is_multicol() { + return ChildRestyleRequirement::MustCascadeChildren; + } + } + + // We could prove that, if our children don't inherit reset + // properties, we can stop the cascade. + ChildRestyleRequirement::MustCascadeChildrenIfInheritResetStyle + } +} + +impl<E: TElement> PrivateMatchMethods for E {} + +/// The public API that elements expose for selector matching. +pub trait MatchMethods: TElement { + /// Returns the closest parent element that doesn't have a display: contents + /// style (and thus generates a box). + /// + /// This is needed to correctly handle blockification of flex and grid + /// items. + /// + /// Returns itself if the element has no parent. In practice this doesn't + /// happen because the root element is blockified per spec, but it could + /// happen if we decide to not blockify for roots of disconnected subtrees, + /// which is a kind of dubious behavior. + fn layout_parent(&self) -> Self { + let mut current = self.clone(); + loop { + current = match current.traversal_parent() { + Some(el) => el, + None => return current, + }; + + let is_display_contents = current + .borrow_data() + .unwrap() + .styles + .primary() + .is_display_contents(); + + if !is_display_contents { + return current; + } + } + } + + /// Updates the styles with the new ones, diffs them, and stores the restyle + /// damage. + fn finish_restyle( + &self, + context: &mut StyleContext<Self>, + data: &mut ElementData, + mut new_styles: ResolvedElementStyles, + important_rules_changed: bool, + ) -> ChildRestyleRequirement { + use std::cmp; + + self.process_animations( + context, + &mut data.styles, + &mut new_styles, + data.hint, + important_rules_changed, + ); + + // First of all, update the styles. + let old_styles = data.set_styles(new_styles); + + let new_primary_style = data.styles.primary.as_ref().unwrap(); + + let mut restyle_requirement = ChildRestyleRequirement::CanSkipCascade; + let is_root = new_primary_style + .flags + .contains(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE); + let is_container = !new_primary_style + .get_box() + .clone_container_type() + .is_normal(); + if is_root || is_container { + let device = context.shared.stylist.device(); + let old_style = old_styles.primary.as_ref(); + let new_font_size = new_primary_style.get_font().clone_font_size(); + let old_font_size = old_style.map(|s| s.get_font().clone_font_size()); + + if old_font_size != Some(new_font_size) { + if is_root { + debug_assert!(self.owner_doc_matches_for_testing(device)); + device.set_root_font_size(new_font_size.computed_size().into()); + if device.used_root_font_size() { + // If the root font-size changed since last time, and something + // in the document did use rem units, ensure we recascade the + // entire tree. + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; + } + } + + if is_container && old_font_size.is_some() { + // TODO(emilio): Maybe only do this if we were matched + // against relative font sizes? + // Also, maybe we should do this as well for font-family / + // etc changes (for ex/ch/ic units to work correctly)? We + // should probably do the optimization mentioned above if + // so. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + } + + // For line-height, we want the fully resolved value, as `normal` also depends on other font properties. + let new_line_height = device + .calc_line_height( + &new_primary_style.get_font(), + new_primary_style.writing_mode, + None, + ) + .0; + let old_line_height = old_style.map(|s| { + device + .calc_line_height(&s.get_font(), s.writing_mode, None) + .0 + }); + + if restyle_requirement != ChildRestyleRequirement::MustMatchDescendants && + old_line_height != Some(new_line_height) + { + if is_root { + debug_assert!(self.owner_doc_matches_for_testing(device)); + device.set_root_line_height(new_line_height.into()); + if device.used_root_line_height() { + restyle_requirement = ChildRestyleRequirement::MustCascadeDescendants; + } + } + + if is_container && old_line_height.is_some() { + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + } + } + + if context.shared.stylist.quirks_mode() == QuirksMode::Quirks { + if self.is_html_document_body_element() { + // NOTE(emilio): We _could_ handle dynamic changes to it if it + // changes and before we reach our children the cascade stops, + // but we don't track right now whether we use the document body + // color, and nobody else handles that properly anyway. + let device = context.shared.stylist.device(); + + // Needed for the "inherit from body" quirk. + let text_color = new_primary_style.get_inherited_text().clone_color(); + device.set_body_text_color(text_color); + } + } + + // Don't accumulate damage if we're in the final animation traversal. + if context + .shared + .traversal_flags + .contains(TraversalFlags::FinalAnimationTraversal) + { + return ChildRestyleRequirement::MustCascadeChildren; + } + + // Also, don't do anything if there was no style. + let old_primary_style = match old_styles.primary { + Some(s) => s, + None => return ChildRestyleRequirement::MustCascadeChildren, + }; + + let old_container_type = old_primary_style.clone_container_type(); + let new_container_type = new_primary_style.clone_container_type(); + if old_container_type != new_container_type && !new_container_type.is_size_container_type() + { + // Stopped being a size container. Re-evaluate container queries and units on all our descendants. + // Changes into and between different size containment is handled in `UpdateContainerQueryStyles`. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } else if old_container_type.is_size_container_type() && + !old_primary_style.is_display_contents() && + new_primary_style.is_display_contents() + { + // Also re-evaluate when a container gets 'display: contents', since size queries will now evaluate to unknown. + // Other displays like 'inline' will keep generating a box, so they are handled in `UpdateContainerQueryStyles`. + restyle_requirement = ChildRestyleRequirement::MustMatchDescendants; + } + + restyle_requirement = cmp::max( + restyle_requirement, + self.accumulate_damage_for( + context.shared, + &mut data.damage, + &old_primary_style, + new_primary_style, + None, + ), + ); + + if data.styles.pseudos.is_empty() && old_styles.pseudos.is_empty() { + // This is the common case; no need to examine pseudos here. + return restyle_requirement; + } + + let pseudo_styles = old_styles + .pseudos + .as_array() + .iter() + .zip(data.styles.pseudos.as_array().iter()); + + for (i, (old, new)) in pseudo_styles.enumerate() { + match (old, new) { + (&Some(ref old), &Some(ref new)) => { + self.accumulate_damage_for( + context.shared, + &mut data.damage, + old, + new, + Some(&PseudoElement::from_eager_index(i)), + ); + }, + (&None, &None) => {}, + _ => { + // It's possible that we're switching from not having + // ::before/::after at all to having styles for them but not + // actually having a useful pseudo-element. Check for that + // case. + let pseudo = PseudoElement::from_eager_index(i); + let new_pseudo_should_exist = + new.as_ref().map_or(false, |s| pseudo.should_exist(s)); + let old_pseudo_should_exist = + old.as_ref().map_or(false, |s| pseudo.should_exist(s)); + if new_pseudo_should_exist != old_pseudo_should_exist { + data.damage |= RestyleDamage::reconstruct(); + return restyle_requirement; + } + }, + } + } + + restyle_requirement + } + + /// Updates the rule nodes without re-running selector matching, using just + /// the rule tree. + /// + /// Returns true if an !important rule was replaced. + fn replace_rules( + &self, + replacements: RestyleHint, + context: &mut StyleContext<Self>, + cascade_inputs: &mut ElementCascadeInputs, + ) -> bool { + let mut result = false; + result |= self.replace_rules_internal( + replacements, + context, + CascadeVisitedMode::Unvisited, + cascade_inputs, + ); + result |= self.replace_rules_internal( + replacements, + context, + CascadeVisitedMode::Visited, + cascade_inputs, + ); + result + } + + /// Given the old and new style of this element, and whether it's a + /// pseudo-element, compute the restyle damage used to determine which + /// kind of layout or painting operations we'll need. + fn compute_style_difference( + &self, + old_values: &ComputedValues, + new_values: &ComputedValues, + pseudo: Option<&PseudoElement>, + ) -> StyleDifference { + debug_assert!(pseudo.map_or(true, |p| p.is_eager())); + RestyleDamage::compute_style_difference(old_values, new_values) + } +} + +impl<E: TElement> MatchMethods for E {} diff --git a/servo/components/style/media_queries/media_list.rs b/servo/components/style/media_queries/media_list.rs new file mode 100644 index 0000000000..3c2ba9ee5c --- /dev/null +++ b/servo/components/style/media_queries/media_list.rs @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A media query list: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query-list + +use super::{Device, MediaQuery, Qualifier}; +use crate::context::QuirksMode; +use crate::error_reporting::ContextualParseError; +use crate::parser::ParserContext; +use crate::queries::condition::KleeneValue; +use crate::values::computed; +use cssparser::{Delimiter, Parser}; +use cssparser::{ParserInput, Token}; + +/// A type that encapsulates a media query list. +#[derive(Clone, MallocSizeOf, ToCss, ToShmem)] +#[css(comma, derive_debug)] +pub struct MediaList { + /// The list of media queries. + #[css(iterable)] + pub media_queries: Vec<MediaQuery>, +} + +impl MediaList { + /// Parse a media query list from CSS. + /// + /// Always returns a media query list. If any invalid media query is + /// found, the media query list is only filled with the equivalent of + /// "not all", see: + /// + /// <https://drafts.csswg.org/mediaqueries/#error-handling> + pub fn parse(context: &ParserContext, input: &mut Parser) -> Self { + if input.is_exhausted() { + return Self::empty(); + } + + let mut media_queries = vec![]; + loop { + let start_position = input.position(); + match input.parse_until_before(Delimiter::Comma, |i| MediaQuery::parse(context, i)) { + Ok(mq) => { + media_queries.push(mq); + }, + Err(err) => { + media_queries.push(MediaQuery::never_matching()); + let location = err.location; + let error = ContextualParseError::InvalidMediaRule( + input.slice_from(start_position), + err, + ); + context.log_css_error(location, error); + }, + } + + match input.next() { + Ok(&Token::Comma) => {}, + Ok(_) => unreachable!(), + Err(_) => break, + } + } + + MediaList { media_queries } + } + + /// Create an empty MediaList. + pub fn empty() -> Self { + MediaList { + media_queries: vec![], + } + } + + /// Evaluate a whole `MediaList` against `Device`. + pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> bool { + // Check if it is an empty media query list or any queries match. + // https://drafts.csswg.org/mediaqueries-4/#mq-list + if self.media_queries.is_empty() { + return true; + } + + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + self.media_queries.iter().any(|mq| { + let mut query_match = if mq.media_type.matches(device.media_type()) { + mq.condition + .as_ref() + .map_or(KleeneValue::True, |c| c.matches(context)) + } else { + KleeneValue::False + }; + + // Apply the logical NOT qualifier to the result + if matches!(mq.qualifier, Some(Qualifier::Not)) { + query_match = !query_match; + } + query_match.to_bool(/* unknown = */ false) + }) + }) + } + + /// Whether this `MediaList` contains no media queries. + pub fn is_empty(&self) -> bool { + self.media_queries.is_empty() + } + + /// Whether this `MediaList` depends on the viewport size. + pub fn is_viewport_dependent(&self) -> bool { + self.media_queries.iter().any(|q| q.is_viewport_dependent()) + } + + /// Append a new media query item to the media list. + /// <https://drafts.csswg.org/cssom/#dom-medialist-appendmedium> + /// + /// Returns true if added, false if fail to parse the medium string. + pub fn append_medium(&mut self, context: &ParserContext, new_medium: &str) -> bool { + let mut input = ParserInput::new(new_medium); + let mut parser = Parser::new(&mut input); + let new_query = match MediaQuery::parse(&context, &mut parser) { + Ok(query) => query, + Err(_) => { + return false; + }, + }; + // This algorithm doesn't actually matches the current spec, + // but it matches the behavior of Gecko and Edge. + // See https://github.com/w3c/csswg-drafts/issues/697 + self.media_queries.retain(|query| query != &new_query); + self.media_queries.push(new_query); + true + } + + /// Delete a media query from the media list. + /// <https://drafts.csswg.org/cssom/#dom-medialist-deletemedium> + /// + /// Returns true if found and deleted, false otherwise. + pub fn delete_medium(&mut self, context: &ParserContext, old_medium: &str) -> bool { + let mut input = ParserInput::new(old_medium); + let mut parser = Parser::new(&mut input); + let old_query = match MediaQuery::parse(context, &mut parser) { + Ok(query) => query, + Err(_) => { + return false; + }, + }; + let old_len = self.media_queries.len(); + self.media_queries.retain(|query| query != &old_query); + old_len != self.media_queries.len() + } +} diff --git a/servo/components/style/media_queries/media_query.rs b/servo/components/style/media_queries/media_query.rs new file mode 100644 index 0000000000..c30a445393 --- /dev/null +++ b/servo/components/style/media_queries/media_query.rs @@ -0,0 +1,193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A media query: +//! +//! https://drafts.csswg.org/mediaqueries/#typedef-media-query + +use crate::parser::ParserContext; +use crate::queries::{FeatureFlags, FeatureType, QueryCondition}; +use crate::str::string_as_ascii_lowercase; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// <https://drafts.csswg.org/mediaqueries/#mq-prefix> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +pub enum Qualifier { + /// Hide a media query from legacy UAs: + /// <https://drafts.csswg.org/mediaqueries/#mq-only> + Only, + /// Negate a media query: + /// <https://drafts.csswg.org/mediaqueries/#mq-not> + Not, +} + +/// <https://drafts.csswg.org/mediaqueries/#media-types> +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub struct MediaType(pub CustomIdent); + +impl MediaType { + /// The `screen` media type. + pub fn screen() -> Self { + MediaType(CustomIdent(atom!("screen"))) + } + + /// The `print` media type. + pub fn print() -> Self { + MediaType(CustomIdent(atom!("print"))) + } + + fn parse(name: &str) -> Result<Self, ()> { + // From https://drafts.csswg.org/mediaqueries/#mq-syntax: + // + // The <media-type> production does not include the keywords only, not, and, or, and layer. + // + // Here we also perform the to-ascii-lowercase part of the serialization + // algorithm: https://drafts.csswg.org/cssom/#serializing-media-queries + match_ignore_ascii_case! { name, + "not" | "or" | "and" | "only" | "layer" => Err(()), + _ => Ok(MediaType(CustomIdent(Atom::from(string_as_ascii_lowercase(name))))), + } + } +} + +/// A [media query][mq]. +/// +/// [mq]: https://drafts.csswg.org/mediaqueries/ +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct MediaQuery { + /// The qualifier for this query. + pub qualifier: Option<Qualifier>, + /// The media type for this query, that can be known, unknown, or "all". + pub media_type: MediaQueryType, + /// The condition that this media query contains. This cannot have `or` + /// in the first level. + pub condition: Option<QueryCondition>, +} + +impl ToCss for MediaQuery { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if let Some(qual) = self.qualifier { + qual.to_css(dest)?; + dest.write_char(' ')?; + } + + match self.media_type { + MediaQueryType::All => { + // We need to print "all" if there's a qualifier, or there's + // just an empty list of expressions. + // + // Otherwise, we'd serialize media queries like "(min-width: + // 40px)" in "all (min-width: 40px)", which is unexpected. + if self.qualifier.is_some() || self.condition.is_none() { + dest.write_str("all")?; + } + }, + MediaQueryType::Concrete(MediaType(ref desc)) => desc.to_css(dest)?, + } + + let condition = match self.condition { + Some(ref c) => c, + None => return Ok(()), + }; + + if self.media_type != MediaQueryType::All || self.qualifier.is_some() { + dest.write_str(" and ")?; + } + + condition.to_css(dest) + } +} + +impl MediaQuery { + /// Return a media query that never matches, used for when we fail to parse + /// a given media query. + pub fn never_matching() -> Self { + Self { + qualifier: Some(Qualifier::Not), + media_type: MediaQueryType::All, + condition: None, + } + } + + /// Returns whether this media query depends on the viewport. + pub fn is_viewport_dependent(&self) -> bool { + self.condition.as_ref().map_or(false, |c| { + return c + .cumulative_flags() + .contains(FeatureFlags::VIEWPORT_DEPENDENT); + }) + } + + /// Parse a media query given css input. + /// + /// Returns an error if any of the expressions is unknown. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let (qualifier, explicit_media_type) = input + .try_parse(|input| -> Result<_, ()> { + let qualifier = input.try_parse(Qualifier::parse).ok(); + let ident = input.expect_ident().map_err(|_| ())?; + let media_type = MediaQueryType::parse(&ident)?; + Ok((qualifier, Some(media_type))) + }) + .unwrap_or_default(); + + let condition = if explicit_media_type.is_none() { + Some(QueryCondition::parse(context, input, FeatureType::Media)?) + } else if input.try_parse(|i| i.expect_ident_matching("and")).is_ok() { + Some(QueryCondition::parse_disallow_or( + context, + input, + FeatureType::Media, + )?) + } else { + None + }; + + let media_type = explicit_media_type.unwrap_or(MediaQueryType::All); + Ok(Self { + qualifier, + media_type, + condition, + }) + } +} + +/// <http://dev.w3.org/csswg/mediaqueries-3/#media0> +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum MediaQueryType { + /// A media type that matches every device. + All, + /// A specific media type. + Concrete(MediaType), +} + +impl MediaQueryType { + fn parse(ident: &str) -> Result<Self, ()> { + match_ignore_ascii_case! { ident, + "all" => return Ok(MediaQueryType::All), + _ => (), + }; + + // If parseable, accept this type as a concrete type. + MediaType::parse(ident).map(MediaQueryType::Concrete) + } + + /// Returns whether this media query type matches a MediaType. + pub fn matches(&self, other: MediaType) -> bool { + match *self { + MediaQueryType::All => true, + MediaQueryType::Concrete(ref known_type) => *known_type == other, + } + } +} diff --git a/servo/components/style/media_queries/mod.rs b/servo/components/style/media_queries/mod.rs new file mode 100644 index 0000000000..833f6f53cb --- /dev/null +++ b/servo/components/style/media_queries/mod.rs @@ -0,0 +1,18 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Media queries][mq]. +//! +//! [mq]: https://drafts.csswg.org/mediaqueries/ + +mod media_list; +mod media_query; + +pub use self::media_list::MediaList; +pub use self::media_query::{MediaQuery, MediaQueryType, MediaType, Qualifier}; + +#[cfg(feature = "gecko")] +pub use crate::gecko::media_queries::Device; +#[cfg(feature = "servo")] +pub use crate::servo::media_queries::Device; diff --git a/servo/components/style/parallel.rs b/servo/components/style/parallel.rs new file mode 100644 index 0000000000..0b2ccf46d1 --- /dev/null +++ b/servo/components/style/parallel.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Implements parallel traversal over the DOM tree. +//! +//! This traversal is based on Rayon, and therefore its safety is largely +//! verified by the type system. +//! +//! The primary trickiness and fine print for the above relates to the +//! thread safety of the DOM nodes themselves. Accessing a DOM element +//! concurrently on multiple threads is actually mostly "safe", since all +//! the mutable state is protected by an AtomicRefCell, and so we'll +//! generally panic if something goes wrong. Still, we try to to enforce our +//! thread invariants at compile time whenever possible. As such, TNode and +//! TElement are not Send, so ordinary style system code cannot accidentally +//! share them with other threads. In the parallel traversal, we explicitly +//! invoke |unsafe { SendNode::new(n) }| to put nodes in containers that may +//! be sent to other threads. This occurs in only a handful of places and is +//! easy to grep for. At the time of this writing, there is no other unsafe +//! code in the parallel traversal. + +#![deny(missing_docs)] + +use crate::context::{StyleContext, ThreadLocalStyleContext}; +use crate::dom::{OpaqueNode, SendNode, TElement}; +use crate::scoped_tls::ScopedTLS; +use crate::traversal::{DomTraversal, PerLevelTraversalData}; +use rayon; +use std::collections::VecDeque; + +/// The minimum stack size for a thread in the styling pool, in kilobytes. +pub const STYLE_THREAD_STACK_SIZE_KB: usize = 256; + +/// The stack margin. If we get this deep in the stack, we will skip recursive +/// optimizations to ensure that there is sufficient room for non-recursive work. +/// +/// We allocate large safety margins because certain OS calls can use very large +/// amounts of stack space [1]. Reserving a larger-than-necessary stack costs us +/// address space, but if we keep our safety margin big, we will generally avoid +/// committing those extra pages, and only use them in edge cases that would +/// otherwise cause crashes. +/// +/// When measured with 128KB stacks and 40KB margin, we could support 53 +/// levels of recursion before the limiter kicks in, on x86_64-Linux [2]. When +/// we doubled the stack size, we added it all to the safety margin, so we should +/// be able to get the same amount of recursion. +/// +/// [1] https://bugzilla.mozilla.org/show_bug.cgi?id=1395708#c15 +/// [2] See Gecko bug 1376883 for more discussion on the measurements. +pub const STACK_SAFETY_MARGIN_KB: usize = 168; + +/// A callback to create our thread local context. This needs to be +/// out of line so we don't allocate stack space for the entire struct +/// in the caller. +#[inline(never)] +pub(crate) fn create_thread_local_context<'scope, E>(slot: &mut Option<ThreadLocalStyleContext<E>>) +where + E: TElement + 'scope, +{ + *slot = Some(ThreadLocalStyleContext::new()); +} + +// Sends one chunk of work to the thread-pool. +fn distribute_one_chunk<'a, 'scope, E, D>( + items: VecDeque<SendNode<E::ConcreteNode>>, + traversal_root: OpaqueNode, + work_unit_max: usize, + traversal_data: PerLevelTraversalData, + scope: &'a rayon::ScopeFifo<'scope>, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>, +) where + E: TElement + 'scope, + D: DomTraversal<E>, +{ + scope.spawn_fifo(move |scope| { + gecko_profiler_label!(Layout, StyleComputation); + let mut tlc = tls.ensure(create_thread_local_context); + let mut context = StyleContext { + shared: traversal.shared_context(), + thread_local: &mut *tlc, + }; + style_trees( + &mut context, + items, + traversal_root, + work_unit_max, + traversal_data, + Some(scope), + traversal, + tls, + ); + }) +} + +/// Distributes all items into the thread pool, in `work_unit_max` chunks. +fn distribute_work<'a, 'scope, E, D>( + mut items: VecDeque<SendNode<E::ConcreteNode>>, + traversal_root: OpaqueNode, + work_unit_max: usize, + traversal_data: PerLevelTraversalData, + scope: &'a rayon::ScopeFifo<'scope>, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>, +) where + E: TElement + 'scope, + D: DomTraversal<E>, +{ + while items.len() > work_unit_max { + let rest = items.split_off(work_unit_max); + distribute_one_chunk( + items, + traversal_root, + work_unit_max, + traversal_data, + scope, + traversal, + tls, + ); + items = rest; + } + distribute_one_chunk( + items, + traversal_root, + work_unit_max, + traversal_data, + scope, + traversal, + tls, + ); +} + +/// Processes `discovered` items, possibly spawning work in other threads as needed. +#[inline] +pub fn style_trees<'a, 'scope, E, D>( + context: &mut StyleContext<E>, + mut discovered: VecDeque<SendNode<E::ConcreteNode>>, + traversal_root: OpaqueNode, + work_unit_max: usize, + mut traversal_data: PerLevelTraversalData, + scope: Option<&'a rayon::ScopeFifo<'scope>>, + traversal: &'scope D, + tls: &'scope ScopedTLS<'scope, ThreadLocalStyleContext<E>>, +) where + E: TElement + 'scope, + D: DomTraversal<E>, +{ + let local_queue_size = if tls.current_thread_index() == 0 { + static_prefs::pref!("layout.css.stylo-local-work-queue.in-main-thread") + } else { + static_prefs::pref!("layout.css.stylo-local-work-queue.in-worker") + } as usize; + + let mut nodes_remaining_at_current_depth = discovered.len(); + while let Some(node) = discovered.pop_front() { + let mut children_to_process = 0isize; + traversal.process_preorder(&traversal_data, context, *node, |n| { + children_to_process += 1; + discovered.push_back(unsafe { SendNode::new(n) }); + }); + + traversal.handle_postorder_traversal(context, traversal_root, *node, children_to_process); + + nodes_remaining_at_current_depth -= 1; + + // If we have enough children at the next depth in the DOM, spawn them to a different job + // relatively soon, while keeping always at least `local_queue_size` worth of work for + // ourselves. + let discovered_children = discovered.len() - nodes_remaining_at_current_depth; + if discovered_children >= work_unit_max && + discovered.len() >= local_queue_size + work_unit_max && + scope.is_some() + { + let kept_work = std::cmp::max(nodes_remaining_at_current_depth, local_queue_size); + let mut traversal_data_copy = traversal_data.clone(); + traversal_data_copy.current_dom_depth += 1; + distribute_work( + discovered.split_off(kept_work), + traversal_root, + work_unit_max, + traversal_data_copy, + scope.unwrap(), + traversal, + tls, + ); + } + + if nodes_remaining_at_current_depth == 0 { + traversal_data.current_dom_depth += 1; + nodes_remaining_at_current_depth = discovered.len(); + } + } +} diff --git a/servo/components/style/parser.rs b/servo/components/style/parser.rs new file mode 100644 index 0000000000..893625854f --- /dev/null +++ b/servo/components/style/parser.rs @@ -0,0 +1,178 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The context within which CSS code is parsed. + +use crate::context::QuirksMode; +use crate::error_reporting::{ContextualParseError, ParseErrorReporter}; +use crate::stylesheets::{CssRuleType, CssRuleTypes, Namespaces, Origin, UrlExtraData}; +use crate::use_counters::UseCounters; +use cssparser::{Parser, SourceLocation, UnicodeRange}; +use std::borrow::Cow; +use style_traits::{OneOrMoreSeparated, ParseError, ParsingMode, Separator}; + +/// The data that the parser needs from outside in order to parse a stylesheet. +pub struct ParserContext<'a> { + /// The `Origin` of the stylesheet, whether it's a user, author or + /// user-agent stylesheet. + pub stylesheet_origin: Origin, + /// The extra data we need for resolving url values. + pub url_data: &'a UrlExtraData, + /// The current rule types, if any. + pub rule_types: CssRuleTypes, + /// The mode to use when parsing. + pub parsing_mode: ParsingMode, + /// The quirks mode of this stylesheet. + pub quirks_mode: QuirksMode, + /// The active error reporter, or none if error reporting is disabled. + error_reporter: Option<&'a dyn ParseErrorReporter>, + /// The currently active namespaces. + pub namespaces: Cow<'a, Namespaces>, + /// The use counters we want to record while parsing style rules, if any. + pub use_counters: Option<&'a UseCounters>, +} + +impl<'a> ParserContext<'a> { + /// Create a parser context. + #[inline] + pub fn new( + stylesheet_origin: Origin, + url_data: &'a UrlExtraData, + rule_type: Option<CssRuleType>, + parsing_mode: ParsingMode, + quirks_mode: QuirksMode, + namespaces: Cow<'a, Namespaces>, + error_reporter: Option<&'a dyn ParseErrorReporter>, + use_counters: Option<&'a UseCounters>, + ) -> Self { + Self { + stylesheet_origin, + url_data, + rule_types: rule_type.map(CssRuleTypes::from).unwrap_or_default(), + parsing_mode, + quirks_mode, + error_reporter, + namespaces, + use_counters, + } + } + + /// Temporarily sets the rule_type and executes the callback function, returning its result. + pub fn nest_for_rule<R>( + &mut self, + rule_type: CssRuleType, + cb: impl FnOnce(&mut Self) -> R, + ) -> R { + let old_rule_types = self.rule_types; + self.rule_types.insert(rule_type); + let r = cb(self); + self.rule_types = old_rule_types; + r + } + + /// Whether we're in a @page rule. + #[inline] + pub fn in_page_rule(&self) -> bool { + self.rule_types.contains(CssRuleType::Page) + } + + /// Get the rule type, which assumes that one is available. + pub fn rule_types(&self) -> CssRuleTypes { + self.rule_types + } + + /// Returns whether CSS error reporting is enabled. + #[inline] + pub fn error_reporting_enabled(&self) -> bool { + self.error_reporter.is_some() + } + + /// Record a CSS parse error with this context’s error reporting. + pub fn log_css_error(&self, location: SourceLocation, error: ContextualParseError) { + let error_reporter = match self.error_reporter { + Some(r) => r, + None => return, + }; + + error_reporter.report_error(self.url_data, location, error) + } + + /// Whether we're in a user-agent stylesheet. + #[inline] + pub fn in_ua_sheet(&self) -> bool { + self.stylesheet_origin == Origin::UserAgent + } + + /// Returns whether chrome-only rules should be parsed. + #[inline] + pub fn chrome_rules_enabled(&self) -> bool { + self.url_data.chrome_rules_enabled() || self.stylesheet_origin != Origin::Author + } +} + +/// A trait to abstract parsing of a specified value given a `ParserContext` and +/// CSS input. +/// +/// This can be derived on keywords with `#[derive(Parse)]`. +/// +/// The derive code understands the following attributes on each of the variants: +/// +/// * `#[parse(aliases = "foo,bar")]` can be used to alias a value with another +/// at parse-time. +/// +/// * `#[parse(condition = "function")]` can be used to make the parsing of the +/// value conditional on `function`, which needs to fulfill +/// `fn(&ParserContext) -> bool`. +pub trait Parse: Sized { + /// Parse a value of this type. + /// + /// Returns an error on failure. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>>; +} + +impl<T> Parse for Vec<T> +where + T: Parse + OneOrMoreSeparated, + <T as OneOrMoreSeparated>::S: Separator, +{ + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + <T as OneOrMoreSeparated>::S::parse(input, |i| T::parse(context, i)) + } +} + +impl<T> Parse for Box<T> +where + T: Parse, +{ + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + T::parse(context, input).map(Box::new) + } +} + +impl Parse for crate::OwnedStr { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(input.expect_string()?.as_ref().to_owned().into()) + } +} + +impl Parse for UnicodeRange { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(UnicodeRange::parse(input)?) + } +} diff --git a/servo/components/style/piecewise_linear.rs b/servo/components/style/piecewise_linear.rs new file mode 100644 index 0000000000..1cabd01ea1 --- /dev/null +++ b/servo/components/style/piecewise_linear.rs @@ -0,0 +1,281 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A piecewise linear function, following CSS linear easing +use crate::values::computed::Percentage; +use core::slice::Iter; +/// draft as in https://github.com/w3c/csswg-drafts/pull/6533. +use euclid::approxeq::ApproxEq; +use itertools::Itertools; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +use crate::values::CSSFloat; + +type ValueType = CSSFloat; +/// a single entry in a piecewise linear function. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[repr(C)] +pub struct PiecewiseLinearFunctionEntry { + pub x: ValueType, + pub y: ValueType, +} + +impl ToCss for PiecewiseLinearFunctionEntry { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.y.to_css(dest)?; + dest.write_char(' ')?; + Percentage(self.x).to_css(dest) + } +} + +/// Representation of a piecewise linear function, a series of linear functions. +#[derive( + Default, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToResolvedValue, + ToCss, + ToShmem, + Serialize, + Deserialize, +)] +#[repr(C)] +#[css(comma)] +pub struct PiecewiseLinearFunction { + #[css(iterable)] + #[ignore_malloc_size_of = "Arc"] + #[shmem(field_bound)] + entries: crate::ArcSlice<PiecewiseLinearFunctionEntry>, +} + +/// Parameters to define one linear stop. +pub type PiecewiseLinearFunctionBuildParameters = (CSSFloat, Option<CSSFloat>); + +impl PiecewiseLinearFunction { + /// Interpolate y value given x and two points. The linear function will be rooted at the asymptote. + fn interpolate( + x: ValueType, + prev: PiecewiseLinearFunctionEntry, + next: PiecewiseLinearFunctionEntry, + asymptote: &PiecewiseLinearFunctionEntry, + ) -> ValueType { + // Short circuit if the x is on prev or next. + // `next` point is preferred as per spec. + if x.approx_eq(&next.x) { + return next.y; + } + if x.approx_eq(&prev.x) { + return prev.y; + } + // Avoid division by zero. + if prev.x.approx_eq(&next.x) { + return next.y; + } + let slope = (next.y - prev.y) / (next.x - prev.x); + return slope * (x - asymptote.x) + asymptote.y; + } + + /// Get the y value of the piecewise linear function given the x value, as per + /// https://drafts.csswg.org/css-easing-2/#linear-easing-function-output + pub fn at(&self, x: ValueType) -> ValueType { + if !x.is_finite() { + return if x > 0.0 { 1.0 } else { 0.0 }; + } + if self.entries.is_empty() { + // Implied y = x, as per spec. + return x; + } + if self.entries.len() == 1 { + // Implied y = <constant>, as per spec. + return self.entries[0].y; + } + // Spec dictates the valid input domain is [0, 1]. Outside of this range, the output + // should be calculated as if the slopes at start and end extend to infinity. However, if the + // start/end have two points of the same position, the line should extend along the x-axis. + // The function doesn't have to cover the input domain, in which case the extension logic + // applies even if the input falls in the input domain. + // Also, we're guaranteed to have at least two elements at this point. + if x < self.entries[0].x { + return Self::interpolate(x, self.entries[0], self.entries[1], &self.entries[0]); + } + let mut rev_iter = self.entries.iter().rev(); + let last = rev_iter.next().unwrap(); + if x >= last.x { + let second_last = rev_iter.next().unwrap(); + return Self::interpolate(x, *second_last, *last, last); + } + + // Now we know the input sits within the domain explicitly defined by our function. + for (point_b, point_a) in self.entries.iter().rev().tuple_windows() { + // Need to let point A be the _last_ point where its x is less than the input x, + // hence the reverse traversal. + if x < point_a.x { + continue; + } + return Self::interpolate(x, *point_a, *point_b, point_a); + } + unreachable!("Input is supposed to be within the entries' min & max!"); + } + + #[allow(missing_docs)] + pub fn iter(&self) -> Iter<PiecewiseLinearFunctionEntry> { + self.entries.iter() + } +} + +/// Entry of a piecewise linear function while building, where the calculation of x value can be deferred. +#[derive(Clone, Copy)] +struct BuildEntry { + x: Option<ValueType>, + y: ValueType, +} + +/// Builder object to generate a linear function. +#[derive(Default)] +pub struct PiecewiseLinearFunctionBuilder { + largest_x: Option<ValueType>, + smallest_x: Option<ValueType>, + entries: Vec<BuildEntry>, +} + +impl PiecewiseLinearFunctionBuilder { + /// Create a builder for a known amount of linear stop entries. + pub fn with_capacity(len: usize) -> Self { + PiecewiseLinearFunctionBuilder { + largest_x: None, + smallest_x: None, + entries: Vec::with_capacity(len), + } + } + + fn create_entry(&mut self, y: ValueType, x: Option<ValueType>) { + let x = match x { + Some(x) if x.is_finite() => x, + _ if self.entries.is_empty() => 0.0, // First x is 0 if not specified (Or not finite) + _ => { + self.entries.push(BuildEntry { x: None, y }); + return; + }, + }; + // Specified x value cannot regress, as per spec. + let x = match self.largest_x { + Some(largest_x) => x.max(largest_x), + None => x, + }; + self.largest_x = Some(x); + // Whatever we see the earliest is the smallest value. + if self.smallest_x.is_none() { + self.smallest_x = Some(x); + } + self.entries.push(BuildEntry { x: Some(x), y }); + } + + /// Add a new entry into the piecewise linear function with specified y value. + /// If the start x value is given, that is where the x value will be. Otherwise, + /// the x value is calculated later. If the end x value is specified, a flat segment + /// is generated. If start x value is not specified but end x is, it is treated as + /// start x. + pub fn push(&mut self, y: CSSFloat, x_start: Option<CSSFloat>) { + self.create_entry(y, x_start) + } + + /// Finish building the piecewise linear function by resolving all undefined x values, + /// then return the result. + pub fn build(mut self) -> PiecewiseLinearFunction { + if self.entries.is_empty() { + return PiecewiseLinearFunction::default(); + } + if self.entries.len() == 1 { + // Don't bother resolving anything. + return PiecewiseLinearFunction { + entries: crate::ArcSlice::from_iter(std::iter::once( + PiecewiseLinearFunctionEntry { + x: 0., + y: self.entries[0].y, + }, + )), + }; + } + // Guaranteed at least two elements. + // Start element's x value should've been assigned when the first value was pushed. + debug_assert!( + self.entries[0].x.is_some(), + "Expected an entry with x defined!" + ); + // Spec asserts that if the last entry does not have an x value, it is assigned the largest seen x value. + self.entries + .last_mut() + .unwrap() + .x + .get_or_insert(self.largest_x.filter(|x| x > &1.0).unwrap_or(1.0)); + // Now we have at least two elements with x values, with start & end x values guaranteed. + + let mut result = Vec::with_capacity(self.entries.len()); + result.push(PiecewiseLinearFunctionEntry { + x: self.entries[0].x.unwrap(), + y: self.entries[0].y, + }); + for (i, e) in self.entries.iter().enumerate().skip(1) { + if e.x.is_none() { + // Need to calculate x values by first finding an entry with the first + // defined x value (Guaranteed to exist as the list end has it defined). + continue; + } + // x is defined for this element. + let divisor = i - result.len() + 1; + // Any element(s) with undefined x to assign? + if divisor != 1 { + // Have at least one element in result at all times. + let start_x = result.last().unwrap().x; + let increment = (e.x.unwrap() - start_x) / divisor as ValueType; + // Grab every element with undefined x to this point, which starts at the end of the result + // array, and ending right before the current index. Then, assigned the evenly divided + // x values. + result.extend( + self.entries[result.len()..i] + .iter() + .enumerate() + .map(|(j, e)| { + debug_assert!(e.x.is_none(), "Expected an entry with x undefined!"); + PiecewiseLinearFunctionEntry { + x: increment * (j + 1) as ValueType + start_x, + y: e.y, + } + }), + ); + } + result.push(PiecewiseLinearFunctionEntry { + x: e.x.unwrap(), + y: e.y, + }); + } + debug_assert_eq!( + result.len(), + self.entries.len(), + "Should've mapped one-to-one!" + ); + PiecewiseLinearFunction { + entries: crate::ArcSlice::from_iter(result.into_iter()), + } + } +} diff --git a/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl Binary files differnew file mode 100644 index 0000000000..9593025a47 --- /dev/null +++ b/servo/components/style/properties/Mako-1.1.2-py2.py3-none-any.whl diff --git a/servo/components/style/properties/build.py b/servo/components/style/properties/build.py new file mode 100644 index 0000000000..6c3ee0cf66 --- /dev/null +++ b/servo/components/style/properties/build.py @@ -0,0 +1,176 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import json +import os.path +import re +import sys + +BASE = os.path.dirname(__file__.replace("\\", "/")) +sys.path.insert(0, os.path.join(BASE, "Mako-1.1.2-py2.py3-none-any.whl")) +sys.path.insert(0, BASE) # For importing `data.py` + +from mako import exceptions +from mako.lookup import TemplateLookup +from mako.template import Template + +import data + +RE_PYTHON_ADDR = re.compile(r"<.+? object at 0x[0-9a-fA-F]+>") + +OUT_DIR = os.environ.get("OUT_DIR", "") + +STYLE_STRUCT_LIST = [ + "background", + "border", + "box", + "column", + "counters", + "effects", + "font", + "inherited_box", + "inherited_svg", + "inherited_table", + "inherited_text", + "inherited_ui", + "list", + "margin", + "outline", + "page", + "padding", + "position", + "svg", + "table", + "text", + "ui", + "xul", +] + + +def main(): + usage = ( + "Usage: %s [ servo-2013 | servo-2020 | gecko ] [ style-crate | geckolib <template> | html ]" + % sys.argv[0] + ) + if len(sys.argv) < 3: + abort(usage) + engine = sys.argv[1] + output = sys.argv[2] + + if engine not in ["servo-2013", "servo-2020", "gecko"] or output not in [ + "style-crate", + "geckolib", + "html", + ]: + abort(usage) + + properties = data.PropertiesData(engine=engine) + files = {} + for kind in ["longhands", "shorthands"]: + files[kind] = {} + for struct in STYLE_STRUCT_LIST: + file_name = os.path.join(BASE, kind, "{}.mako.rs".format(struct)) + if kind == "shorthands" and not os.path.exists(file_name): + files[kind][struct] = "" + continue + files[kind][struct] = render( + file_name, + engine=engine, + data=properties, + ) + properties_template = os.path.join(BASE, "properties.mako.rs") + files["properties"] = render( + properties_template, + engine=engine, + data=properties, + __file__=properties_template, + OUT_DIR=OUT_DIR, + ) + if output == "style-crate": + write(OUT_DIR, "properties.rs", files["properties"]) + for kind in ["longhands", "shorthands"]: + for struct in files[kind]: + write( + os.path.join(OUT_DIR, kind), + "{}.rs".format(struct), + files[kind][struct], + ) + + if engine == "gecko": + template = os.path.join(BASE, "gecko.mako.rs") + rust = render(template, data=properties) + write(OUT_DIR, "gecko_properties.rs", rust) + + if engine in ["servo-2013", "servo-2020"]: + if engine == "servo-2013": + pref_attr = "servo_2013_pref" + if engine == "servo-2020": + pref_attr = "servo_2020_pref" + properties_dict = { + kind: { + p.name: {"pref": getattr(p, pref_attr)} + for prop in properties_list + if prop.enabled_in_content() + for p in [prop] + prop.alias + } + for kind, properties_list in [ + ("longhands", properties.longhands), + ("shorthands", properties.shorthands), + ] + } + as_html = render( + os.path.join(BASE, "properties.html.mako"), properties=properties_dict + ) + as_json = json.dumps(properties_dict, indent=4, sort_keys=True) + doc_servo = os.path.join(BASE, "..", "..", "..", "target", "doc", "servo") + write(doc_servo, "css-properties.html", as_html) + write(doc_servo, "css-properties.json", as_json) + write(OUT_DIR, "css-properties.json", as_json) + elif output == "geckolib": + if len(sys.argv) < 4: + abort(usage) + template = sys.argv[3] + header = render(template, data=properties) + sys.stdout.write(header) + + +def abort(message): + print(message, file=sys.stderr) + sys.exit(1) + + +def render(filename, **context): + try: + lookup = TemplateLookup( + directories=[BASE], input_encoding="utf8", strict_undefined=True + ) + template = Template( + open(filename, "rb").read(), + filename=filename, + input_encoding="utf8", + lookup=lookup, + strict_undefined=True, + ) + # Uncomment to debug generated Python code: + # write("/tmp", "mako_%s.py" % os.path.basename(filename), template.code) + return template.render(**context) + except Exception: + # Uncomment to see a traceback in generated Python code: + # raise + abort(exceptions.text_error_template().render()) + + +def write(directory, filename, content): + if not os.path.exists(directory): + os.makedirs(directory) + full_path = os.path.join(directory, filename) + open(full_path, "w", encoding="utf-8").write(content) + + python_addr = RE_PYTHON_ADDR.search(content) + if python_addr: + abort('Found "{}" in {} ({})'.format(python_addr.group(0), filename, full_path)) + + +if __name__ == "__main__": + main() diff --git a/servo/components/style/properties/cascade.rs b/servo/components/style/properties/cascade.rs new file mode 100644 index 0000000000..59a8a65876 --- /dev/null +++ b/servo/components/style/properties/cascade.rs @@ -0,0 +1,1381 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The main cascading algorithm of the style system. + +use crate::applicable_declarations::CascadePriority; +use crate::color::AbsoluteColor; +use crate::computed_value_flags::ComputedValueFlags; +use crate::custom_properties::{ + CustomPropertiesBuilder, DeferFontRelativeCustomPropertyResolution, +}; +use crate::dom::TElement; +use crate::font_metrics::FontMetricsOrientation; +use crate::logical_geometry::WritingMode; +use crate::properties::{ + property_counts, CSSWideKeyword, ComputedValues, DeclarationImportanceIterator, Importance, + LonghandId, LonghandIdSet, PrioritaryPropertyId, PropertyDeclaration, PropertyDeclarationId, + PropertyFlags, ShorthandsWithPropertyReferencesCache, StyleBuilder, CASCADE_PROPERTY, +}; +use crate::rule_cache::{RuleCache, RuleCacheConditions}; +use crate::rule_tree::{CascadeLevel, StrongRuleNode}; +use crate::selector_parser::PseudoElement; +use crate::shared_lock::StylesheetGuards; +use crate::style_adjuster::StyleAdjuster; +use crate::stylesheets::container_rule::ContainerSizeQuery; +use crate::stylesheets::{layer_rule::LayerOrder, Origin}; +use crate::stylist::Stylist; +use crate::values::specified::length::FontBaseSize; +use crate::values::{computed, specified}; +use fxhash::FxHashMap; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::borrow::Cow; +use std::mem; + +/// Whether we're resolving a style with the purposes of reparenting for ::first-line. +#[derive(Copy, Clone)] +#[allow(missing_docs)] +pub enum FirstLineReparenting<'a> { + No, + Yes { + /// The style we're re-parenting for ::first-line. ::first-line only affects inherited + /// properties so we use this to avoid some work and also ensure correctness by copying the + /// reset structs from this style. + style_to_reparent: &'a ComputedValues, + }, +} + +/// Performs the CSS cascade, computing new styles for an element from its parent style. +/// +/// The arguments are: +/// +/// * `device`: Used to get the initial viewport and other external state. +/// +/// * `rule_node`: The rule node in the tree that represent the CSS rules that +/// matched. +/// +/// * `parent_style`: The parent style, if applicable; if `None`, this is the root node. +/// +/// Returns the computed values. +/// * `flags`: Various flags. +/// +pub fn cascade<E>( + stylist: &Stylist, + pseudo: Option<&PseudoElement>, + rule_node: &StrongRuleNode, + guards: &StylesheetGuards, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + first_line_reparenting: FirstLineReparenting, + visited_rules: Option<&StrongRuleNode>, + cascade_input_flags: ComputedValueFlags, + rule_cache: Option<&RuleCache>, + rule_cache_conditions: &mut RuleCacheConditions, + element: Option<E>, +) -> Arc<ComputedValues> +where + E: TElement, +{ + cascade_rules( + stylist, + pseudo, + rule_node, + guards, + parent_style, + layout_parent_style, + first_line_reparenting, + CascadeMode::Unvisited { visited_rules }, + cascade_input_flags, + rule_cache, + rule_cache_conditions, + element, + ) +} + +struct DeclarationIterator<'a> { + // Global to the iteration. + guards: &'a StylesheetGuards<'a>, + restriction: Option<PropertyFlags>, + // The rule we're iterating over. + current_rule_node: Option<&'a StrongRuleNode>, + // Per rule state. + declarations: DeclarationImportanceIterator<'a>, + origin: Origin, + importance: Importance, + priority: CascadePriority, +} + +impl<'a> DeclarationIterator<'a> { + #[inline] + fn new( + rule_node: &'a StrongRuleNode, + guards: &'a StylesheetGuards, + pseudo: Option<&PseudoElement>, + ) -> Self { + let restriction = pseudo.and_then(|p| p.property_restriction()); + let mut iter = Self { + guards, + current_rule_node: Some(rule_node), + origin: Origin::UserAgent, + importance: Importance::Normal, + priority: CascadePriority::new(CascadeLevel::UANormal, LayerOrder::root()), + declarations: DeclarationImportanceIterator::default(), + restriction, + }; + iter.update_for_node(rule_node); + iter + } + + fn update_for_node(&mut self, node: &'a StrongRuleNode) { + self.priority = node.cascade_priority(); + let level = self.priority.cascade_level(); + self.origin = level.origin(); + self.importance = level.importance(); + let guard = match self.origin { + Origin::Author => self.guards.author, + Origin::User | Origin::UserAgent => self.guards.ua_or_user, + }; + self.declarations = match node.style_source() { + Some(source) => source.read(guard).declaration_importance_iter(), + None => DeclarationImportanceIterator::default(), + }; + } +} + +impl<'a> Iterator for DeclarationIterator<'a> { + type Item = (&'a PropertyDeclaration, CascadePriority); + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + loop { + if let Some((decl, importance)) = self.declarations.next_back() { + if self.importance != importance { + continue; + } + + if let Some(restriction) = self.restriction { + // decl.id() is either a longhand or a custom + // property. Custom properties are always allowed, but + // longhands are only allowed if they have our + // restriction flag set. + if let PropertyDeclarationId::Longhand(id) = decl.id() { + if !id.flags().contains(restriction) && self.origin != Origin::UserAgent { + continue; + } + } + } + + return Some((decl, self.priority)); + } + + let next_node = self.current_rule_node.take()?.parent()?; + self.current_rule_node = Some(next_node); + self.update_for_node(next_node); + } + } +} + +fn cascade_rules<E>( + stylist: &Stylist, + pseudo: Option<&PseudoElement>, + rule_node: &StrongRuleNode, + guards: &StylesheetGuards, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + first_line_reparenting: FirstLineReparenting, + cascade_mode: CascadeMode, + cascade_input_flags: ComputedValueFlags, + rule_cache: Option<&RuleCache>, + rule_cache_conditions: &mut RuleCacheConditions, + element: Option<E>, +) -> Arc<ComputedValues> +where + E: TElement, +{ + apply_declarations( + stylist, + pseudo, + rule_node, + guards, + DeclarationIterator::new(rule_node, guards, pseudo), + parent_style, + layout_parent_style, + first_line_reparenting, + cascade_mode, + cascade_input_flags, + rule_cache, + rule_cache_conditions, + element, + ) +} + +/// Whether we're cascading for visited or unvisited styles. +#[derive(Clone, Copy)] +pub enum CascadeMode<'a, 'b> { + /// We're cascading for unvisited styles. + Unvisited { + /// The visited rules that should match the visited style. + visited_rules: Option<&'a StrongRuleNode>, + }, + /// We're cascading for visited styles. + Visited { + /// The cascade for our unvisited style. + unvisited_context: &'a computed::Context<'b>, + }, +} + +fn iter_declarations<'builder, 'decls: 'builder>( + iter: impl Iterator<Item = (&'decls PropertyDeclaration, CascadePriority)>, + declarations: &mut Declarations<'decls>, + mut custom_builder: Option<&mut CustomPropertiesBuilder<'builder, 'decls>>, +) { + for (declaration, priority) in iter { + if let PropertyDeclaration::Custom(ref declaration) = *declaration { + if let Some(ref mut builder) = custom_builder { + builder.cascade(declaration, priority); + } + } else { + let id = declaration.id().as_longhand().unwrap(); + declarations.note_declaration(declaration, priority, id); + if let Some(ref mut builder) = custom_builder { + if let PropertyDeclaration::WithVariables(ref v) = declaration { + builder.note_potentially_cyclic_non_custom_dependency(id, v); + } + } + } + } +} + +/// NOTE: This function expects the declaration with more priority to appear +/// first. +pub fn apply_declarations<'a, E, I>( + stylist: &'a Stylist, + pseudo: Option<&'a PseudoElement>, + rules: &StrongRuleNode, + guards: &StylesheetGuards, + iter: I, + parent_style: Option<&'a ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + first_line_reparenting: FirstLineReparenting<'a>, + cascade_mode: CascadeMode, + cascade_input_flags: ComputedValueFlags, + rule_cache: Option<&'a RuleCache>, + rule_cache_conditions: &'a mut RuleCacheConditions, + element: Option<E>, +) -> Arc<ComputedValues> +where + E: TElement + 'a, + I: Iterator<Item = (&'a PropertyDeclaration, CascadePriority)>, +{ + debug_assert!(layout_parent_style.is_none() || parent_style.is_some()); + let device = stylist.device(); + let inherited_style = parent_style.unwrap_or(device.default_computed_values()); + let is_root_element = pseudo.is_none() && element.map_or(false, |e| e.is_root()); + + let container_size_query = + ContainerSizeQuery::for_option_element(element, Some(inherited_style), pseudo.is_some()); + + let mut context = computed::Context::new( + // We'd really like to own the rules here to avoid refcount traffic, but + // animation's usage of `apply_declarations` make this tricky. See bug + // 1375525. + StyleBuilder::new( + device, + Some(stylist), + parent_style, + pseudo, + Some(rules.clone()), + is_root_element, + ), + stylist.quirks_mode(), + rule_cache_conditions, + container_size_query, + ); + + context.style().add_flags(cascade_input_flags); + + let using_cached_reset_properties; + let ignore_colors = !context.builder.device.use_document_colors(); + let mut cascade = Cascade::new(first_line_reparenting, ignore_colors); + let mut declarations = Default::default(); + let mut shorthand_cache = ShorthandsWithPropertyReferencesCache::default(); + let properties_to_apply = match cascade_mode { + CascadeMode::Visited { unvisited_context } => { + context.builder.custom_properties = unvisited_context.builder.custom_properties.clone(); + context.builder.writing_mode = unvisited_context.builder.writing_mode; + // We never insert visited styles into the cache so we don't need to try looking it up. + // It also wouldn't be super-profitable, only a handful :visited properties are + // non-inherited. + using_cached_reset_properties = false; + // TODO(bug 1859385): If we match the same rules when visited and unvisited, we could + // try to avoid gathering the declarations. That'd be: + // unvisited_context.builder.rules.as_ref() == Some(rules) + iter_declarations(iter, &mut declarations, None); + + LonghandIdSet::visited_dependent() + }, + CascadeMode::Unvisited { visited_rules } => { + let deferred_custom_properties = { + let mut builder = CustomPropertiesBuilder::new(stylist, &mut context); + iter_declarations(iter, &mut declarations, Some(&mut builder)); + // Detect cycles, remove properties participating in them, and resolve properties, except: + // * Registered custom properties that depend on font-relative properties (Resolved) + // when prioritary properties are resolved), and + // * Any property that, in turn, depend on properties like above. + builder.build(DeferFontRelativeCustomPropertyResolution::Yes) + }; + + // Resolve prioritary properties - Guaranteed to not fall into a cycle with existing custom + // properties. + cascade.apply_prioritary_properties(&mut context, &declarations, &mut shorthand_cache); + + // Resolve the deferred custom properties. + if let Some(deferred) = deferred_custom_properties { + CustomPropertiesBuilder::build_deferred(deferred, stylist, &mut context); + } + + if let Some(visited_rules) = visited_rules { + cascade.compute_visited_style_if_needed( + &mut context, + element, + parent_style, + layout_parent_style, + visited_rules, + guards, + ); + } + + using_cached_reset_properties = cascade.try_to_use_cached_reset_properties( + &mut context.builder, + rule_cache, + guards, + ); + + if using_cached_reset_properties { + LonghandIdSet::late_group_only_inherited() + } else { + LonghandIdSet::late_group() + } + }, + }; + + cascade.apply_non_prioritary_properties( + &mut context, + &declarations.longhand_declarations, + &mut shorthand_cache, + &properties_to_apply, + ); + + cascade.finished_applying_properties(&mut context.builder); + + std::mem::drop(cascade); + + context.builder.clear_modified_reset(); + + if matches!(cascade_mode, CascadeMode::Unvisited { .. }) { + StyleAdjuster::new(&mut context.builder) + .adjust(layout_parent_style.unwrap_or(inherited_style), element); + } + + if context.builder.modified_reset() || using_cached_reset_properties { + // If we adjusted any reset structs, we can't cache this ComputedValues. + // + // Also, if we re-used existing reset structs, don't bother caching it back again. (Aside + // from being wasted effort, it will be wrong, since context.rule_cache_conditions won't be + // set appropriately if we didn't compute those reset properties.) + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + + context.builder.build() +} + +/// For ignored colors mode, we sometimes want to do something equivalent to +/// "revert-or-initial", where we `revert` for a given origin, but then apply a +/// given initial value if nothing in other origins did override it. +/// +/// This is a bit of a clunky way of achieving this. +type DeclarationsToApplyUnlessOverriden = SmallVec<[PropertyDeclaration; 2]>; + +fn tweak_when_ignoring_colors( + context: &computed::Context, + longhand_id: LonghandId, + origin: Origin, + declaration: &mut Cow<PropertyDeclaration>, + declarations_to_apply_unless_overridden: &mut DeclarationsToApplyUnlessOverriden, +) { + use crate::values::computed::ToComputedValue; + use crate::values::specified::Color; + + if !longhand_id.ignored_when_document_colors_disabled() { + return; + } + + let is_ua_or_user_rule = matches!(origin, Origin::User | Origin::UserAgent); + if is_ua_or_user_rule { + return; + } + + // Always honor colors if forced-color-adjust is set to none. + let forced = context + .builder + .get_inherited_text() + .clone_forced_color_adjust(); + if forced == computed::ForcedColorAdjust::None { + return; + } + + // Don't override background-color on ::-moz-color-swatch. It is set as an + // author style (via the style attribute), but it's pretty important for it + // to show up for obvious reasons :) + if context + .builder + .pseudo + .map_or(false, |p| p.is_color_swatch()) && + longhand_id == LonghandId::BackgroundColor + { + return; + } + + fn alpha_channel(color: &Color, context: &computed::Context) -> f32 { + // We assume here currentColor is opaque. + color + .to_computed_value(context) + .resolve_to_absolute(&AbsoluteColor::BLACK) + .alpha + } + + // A few special-cases ahead. + match **declaration { + PropertyDeclaration::BackgroundColor(ref color) => { + // We honor system colors and transparent colors unconditionally. + // + // NOTE(emilio): We honor transparent unconditionally, like we do + // for color, even though it causes issues like bug 1625036. The + // reasoning is that the conditions that trigger that (having + // mismatched widget and default backgrounds) are both uncommon, and + // broken in other applications as well, and not honoring + // transparent makes stuff uglier or break unconditionally + // (bug 1666059, bug 1755713). + if color.honored_in_forced_colors_mode(/* allow_transparent = */ true) { + return; + } + // For background-color, we revert or initial-with-preserved-alpha + // otherwise, this is needed to preserve semi-transparent + // backgrounds. + let alpha = alpha_channel(color, context); + if alpha == 0.0 { + return; + } + let mut color = context.builder.device.default_background_color(); + color.alpha = alpha; + declarations_to_apply_unless_overridden + .push(PropertyDeclaration::BackgroundColor(color.into())) + }, + PropertyDeclaration::Color(ref color) => { + // We honor color: transparent and system colors. + if color + .0 + .honored_in_forced_colors_mode(/* allow_transparent = */ true) + { + return; + } + // If the inherited color would be transparent, but we would + // override this with a non-transparent color, then override it with + // the default color. Otherwise just let it inherit through. + if context + .builder + .get_parent_inherited_text() + .clone_color() + .alpha == + 0.0 + { + let color = context.builder.device.default_color(); + declarations_to_apply_unless_overridden.push(PropertyDeclaration::Color( + specified::ColorPropertyValue(color.into()), + )) + } + }, + // We honor url background-images if backplating. + #[cfg(feature = "gecko")] + PropertyDeclaration::BackgroundImage(ref bkg) => { + use crate::values::generics::image::Image; + if static_prefs::pref!("browser.display.permit_backplate") { + if bkg + .0 + .iter() + .all(|image| matches!(*image, Image::Url(..) | Image::None)) + { + return; + } + } + }, + _ => { + // We honor system colors more generally for all colors. + // + // We used to honor transparent but that causes accessibility + // regressions like bug 1740924. + // + // NOTE(emilio): This doesn't handle caret-color and accent-color + // because those use a slightly different syntax (<color> | auto for + // example). + // + // That's probably fine though, as using a system color for + // caret-color doesn't make sense (using currentColor is fine), and + // we ignore accent-color in high-contrast-mode anyways. + if let Some(color) = declaration.color_value() { + if color.honored_in_forced_colors_mode(/* allow_transparent = */ false) { + return; + } + } + }, + } + + *declaration.to_mut() = + PropertyDeclaration::css_wide_keyword(longhand_id, CSSWideKeyword::Revert); +} + +/// We track the index only for prioritary properties. For other properties we can just iterate. +type DeclarationIndex = u16; + +/// "Prioritary" properties are properties that other properties depend on in one way or another. +/// +/// We keep track of their position in the declaration vector, in order to be able to cascade them +/// separately in precise order. +#[derive(Copy, Clone)] +struct PrioritaryDeclarationPosition { + // DeclarationIndex::MAX signals no index. + most_important: DeclarationIndex, + least_important: DeclarationIndex, +} + +impl Default for PrioritaryDeclarationPosition { + fn default() -> Self { + Self { + most_important: DeclarationIndex::MAX, + least_important: DeclarationIndex::MAX, + } + } +} + +#[derive(Copy, Clone)] +struct Declaration<'a> { + decl: &'a PropertyDeclaration, + priority: CascadePriority, + next_index: DeclarationIndex, +} + +/// The set of property declarations from our rules. +#[derive(Default)] +struct Declarations<'a> { + /// Whether we have any prioritary property. This is just a minor optimization. + has_prioritary_properties: bool, + /// A list of all the applicable longhand declarations. + longhand_declarations: SmallVec<[Declaration<'a>; 32]>, + /// The prioritary property position data. + prioritary_positions: [PrioritaryDeclarationPosition; property_counts::PRIORITARY], +} + +impl<'a> Declarations<'a> { + fn note_prioritary_property(&mut self, id: PrioritaryPropertyId) { + let new_index = self.longhand_declarations.len(); + if new_index >= DeclarationIndex::MAX as usize { + // This prioritary property is past the amount of declarations we can track. Let's give + // up applying it to prevent getting confused. + return; + } + + self.has_prioritary_properties = true; + let new_index = new_index as DeclarationIndex; + let position = &mut self.prioritary_positions[id as usize]; + if position.most_important == DeclarationIndex::MAX { + // We still haven't seen this property, record the current position as the most + // prioritary index. + position.most_important = new_index; + } else { + // Let the previous item in the list know about us. + self.longhand_declarations[position.least_important as usize].next_index = new_index; + } + position.least_important = new_index; + } + + fn note_declaration( + &mut self, + decl: &'a PropertyDeclaration, + priority: CascadePriority, + id: LonghandId, + ) { + if let Some(id) = PrioritaryPropertyId::from_longhand(id) { + self.note_prioritary_property(id); + } + self.longhand_declarations.push(Declaration { + decl, + priority, + next_index: 0, + }); + } +} + +struct Cascade<'b> { + first_line_reparenting: FirstLineReparenting<'b>, + ignore_colors: bool, + seen: LonghandIdSet, + author_specified: LonghandIdSet, + reverted_set: LonghandIdSet, + reverted: FxHashMap<LonghandId, (CascadePriority, bool)>, + declarations_to_apply_unless_overridden: DeclarationsToApplyUnlessOverriden, +} + +impl<'b> Cascade<'b> { + fn new(first_line_reparenting: FirstLineReparenting<'b>, ignore_colors: bool) -> Self { + Self { + first_line_reparenting, + ignore_colors, + seen: LonghandIdSet::default(), + author_specified: LonghandIdSet::default(), + reverted_set: Default::default(), + reverted: Default::default(), + declarations_to_apply_unless_overridden: Default::default(), + } + } + + fn substitute_variables_if_needed<'cache, 'decl>( + &self, + context: &mut computed::Context, + shorthand_cache: &'cache mut ShorthandsWithPropertyReferencesCache, + declaration: &'decl PropertyDeclaration, + ) -> Cow<'decl, PropertyDeclaration> + where + 'cache: 'decl, + { + let declaration = match *declaration { + PropertyDeclaration::WithVariables(ref declaration) => declaration, + ref d => return Cow::Borrowed(d), + }; + + if !declaration.id.inherited() { + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + + // NOTE(emilio): We only really need to add the `display` / + // `content` flag if the CSS variable has not been specified on our + // declarations, but we don't have that information at this point, + // and it doesn't seem like an important enough optimization to + // warrant it. + match declaration.id { + LonghandId::Display => { + context + .builder + .add_flags(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE); + }, + LonghandId::Content => { + context + .builder + .add_flags(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE); + }, + _ => {}, + } + } + + debug_assert!( + context.builder.stylist.is_some(), + "Need a Stylist to substitute variables!" + ); + declaration.value.substitute_variables( + declaration.id, + context.builder.custom_properties(), + context.builder.stylist.unwrap(), + context, + shorthand_cache, + ) + } + + fn apply_one_prioritary_property( + &mut self, + context: &mut computed::Context, + decls: &Declarations, + cache: &mut ShorthandsWithPropertyReferencesCache, + id: PrioritaryPropertyId, + ) -> bool { + let mut index = decls.prioritary_positions[id as usize].most_important; + if index == DeclarationIndex::MAX { + return false; + } + + let longhand_id = id.to_longhand(); + debug_assert!( + !longhand_id.is_logical(), + "That could require more book-keeping" + ); + loop { + let decl = decls.longhand_declarations[index as usize]; + self.apply_one_longhand(context, longhand_id, decl.decl, decl.priority, cache); + if self.seen.contains(longhand_id) { + return true; // Common case, we're done. + } + debug_assert!( + self.reverted_set.contains(longhand_id), + "How else can we fail to apply a prioritary property?" + ); + debug_assert!( + decl.next_index == 0 || decl.next_index > index, + "should make progress! {} -> {}", + index, + decl.next_index, + ); + index = decl.next_index; + if index == 0 { + break; + } + } + false + } + + fn apply_prioritary_properties( + &mut self, + context: &mut computed::Context, + decls: &Declarations, + cache: &mut ShorthandsWithPropertyReferencesCache, + ) { + // Keeps apply_one_prioritary_property calls readable, considering the repititious + // arguments. + macro_rules! apply { + ($prop:ident) => { + self.apply_one_prioritary_property( + context, + decls, + cache, + PrioritaryPropertyId::$prop, + ) + }; + } + + if !decls.has_prioritary_properties { + return; + } + + let has_writing_mode = apply!(WritingMode) | apply!(Direction) | apply!(TextOrientation); + if has_writing_mode { + self.compute_writing_mode(context); + } + + if apply!(Zoom) { + self.compute_zoom(context); + } + + // Compute font-family. + let has_font_family = apply!(FontFamily); + let has_lang = apply!(XLang); + if has_lang { + self.recompute_initial_font_family_if_needed(&mut context.builder); + } + if has_font_family { + self.prioritize_user_fonts_if_needed(&mut context.builder); + } + + // Compute font-size. + if apply!(XTextScale) { + self.unzoom_fonts_if_needed(&mut context.builder); + } + let has_font_size = apply!(FontSize); + let has_math_depth = apply!(MathDepth); + let has_min_font_size_ratio = apply!(MozMinFontSizeRatio); + + if has_math_depth && has_font_size { + self.recompute_math_font_size_if_needed(context); + } + if has_lang || has_font_family { + self.recompute_keyword_font_size_if_needed(context); + } + if has_font_size || has_min_font_size_ratio || has_lang || has_font_family { + self.constrain_font_size_if_needed(&mut context.builder); + } + + // Compute the rest of the first-available-font-affecting properties. + apply!(FontWeight); + apply!(FontStretch); + apply!(FontStyle); + apply!(FontSizeAdjust); + + apply!(ColorScheme); + apply!(ForcedColorAdjust); + + // Compute the line height. + apply!(LineHeight); + } + + fn apply_non_prioritary_properties( + &mut self, + context: &mut computed::Context, + longhand_declarations: &[Declaration], + shorthand_cache: &mut ShorthandsWithPropertyReferencesCache, + properties_to_apply: &LonghandIdSet, + ) { + debug_assert!(!properties_to_apply.contains_any(LonghandIdSet::prioritary_properties())); + debug_assert!(self.declarations_to_apply_unless_overridden.is_empty()); + for declaration in &*longhand_declarations { + let mut longhand_id = declaration.decl.id().as_longhand().unwrap(); + if !properties_to_apply.contains(longhand_id) { + continue; + } + debug_assert!(PrioritaryPropertyId::from_longhand(longhand_id).is_none()); + let is_logical = longhand_id.is_logical(); + if is_logical { + let wm = context.builder.writing_mode; + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(wm); + longhand_id = longhand_id.to_physical(wm); + } + self.apply_one_longhand( + context, + longhand_id, + declaration.decl, + declaration.priority, + shorthand_cache, + ); + } + if !self.declarations_to_apply_unless_overridden.is_empty() { + debug_assert!(self.ignore_colors); + for declaration in std::mem::take(&mut self.declarations_to_apply_unless_overridden) { + let longhand_id = declaration.id().as_longhand().unwrap(); + debug_assert!(!longhand_id.is_logical()); + if !self.seen.contains(longhand_id) { + unsafe { + self.do_apply_declaration(context, longhand_id, &declaration); + } + } + } + } + } + + fn apply_one_longhand( + &mut self, + context: &mut computed::Context, + longhand_id: LonghandId, + declaration: &PropertyDeclaration, + priority: CascadePriority, + cache: &mut ShorthandsWithPropertyReferencesCache, + ) { + debug_assert!(!longhand_id.is_logical()); + let origin = priority.cascade_level().origin(); + if self.seen.contains(longhand_id) { + return; + } + + if self.reverted_set.contains(longhand_id) { + if let Some(&(reverted_priority, is_origin_revert)) = self.reverted.get(&longhand_id) { + if !reverted_priority.allows_when_reverted(&priority, is_origin_revert) { + return; + } + } + } + + let mut declaration = self.substitute_variables_if_needed(context, cache, declaration); + + // When document colors are disabled, do special handling of + // properties that are marked as ignored in that mode. + if self.ignore_colors { + tweak_when_ignoring_colors( + context, + longhand_id, + origin, + &mut declaration, + &mut self.declarations_to_apply_unless_overridden, + ); + } + + let is_unset = match declaration.get_css_wide_keyword() { + Some(keyword) => match keyword { + CSSWideKeyword::RevertLayer | CSSWideKeyword::Revert => { + let origin_revert = keyword == CSSWideKeyword::Revert; + // We intentionally don't want to insert it into `self.seen`, `reverted` takes + // care of rejecting other declarations as needed. + self.reverted_set.insert(longhand_id); + self.reverted.insert(longhand_id, (priority, origin_revert)); + return; + }, + CSSWideKeyword::Unset => true, + CSSWideKeyword::Inherit => longhand_id.inherited(), + CSSWideKeyword::Initial => !longhand_id.inherited(), + }, + None => false, + }; + + self.seen.insert(longhand_id); + if origin == Origin::Author { + self.author_specified.insert(longhand_id); + } + + if is_unset { + return; + } + + unsafe { self.do_apply_declaration(context, longhand_id, &declaration) } + } + + #[inline] + unsafe fn do_apply_declaration( + &self, + context: &mut computed::Context, + longhand_id: LonghandId, + declaration: &PropertyDeclaration, + ) { + debug_assert!(!longhand_id.is_logical()); + // We could (and used to) use a pattern match here, but that bloats this + // function to over 100K of compiled code! + // + // To improve i-cache behavior, we outline the individual functions and + // use virtual dispatch instead. + (CASCADE_PROPERTY[longhand_id as usize])(&declaration, context); + } + + fn compute_zoom(&self, context: &mut computed::Context) { + context.builder.effective_zoom = context + .builder + .inherited_effective_zoom() + .compute_effective(context.builder.specified_zoom()); + } + + fn compute_writing_mode(&self, context: &mut computed::Context) { + context.builder.writing_mode = WritingMode::new(context.builder.get_inherited_box()) + } + + fn compute_visited_style_if_needed<E>( + &self, + context: &mut computed::Context, + element: Option<E>, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + visited_rules: &StrongRuleNode, + guards: &StylesheetGuards, + ) where + E: TElement, + { + let is_link = context.builder.pseudo.is_none() && element.unwrap().is_link(); + + macro_rules! visited_parent { + ($parent:expr) => { + if is_link { + $parent + } else { + $parent.map(|p| p.visited_style().unwrap_or(p)) + } + }; + } + + // We could call apply_declarations directly, but that'd cause + // another instantiation of this function which is not great. + let style = cascade_rules( + context.builder.stylist.unwrap(), + context.builder.pseudo, + visited_rules, + guards, + visited_parent!(parent_style), + visited_parent!(layout_parent_style), + self.first_line_reparenting, + CascadeMode::Visited { + unvisited_context: &*context, + }, + // Cascade input flags don't matter for the visited style, they are + // in the main (unvisited) style. + Default::default(), + // The rule cache doesn't care about caching :visited + // styles, we cache the unvisited style instead. We still do + // need to set the caching dependencies properly if present + // though, so the cache conditions need to match. + None, // rule_cache + &mut *context.rule_cache_conditions.borrow_mut(), + element, + ); + context.builder.visited_style = Some(style); + } + + fn finished_applying_properties(&self, builder: &mut StyleBuilder) { + #[cfg(feature = "gecko")] + { + if let Some(bg) = builder.get_background_if_mutated() { + bg.fill_arrays(); + } + + if let Some(svg) = builder.get_svg_if_mutated() { + svg.fill_arrays(); + } + } + + if self + .author_specified + .contains_any(LonghandIdSet::border_background_properties()) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND); + } + + if self.author_specified.contains(LonghandId::FontFamily) { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY); + } + + if self.author_specified.contains(LonghandId::Color) { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_TEXT_COLOR); + } + + if self.author_specified.contains(LonghandId::LetterSpacing) { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING); + } + + if self.author_specified.contains(LonghandId::WordSpacing) { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING); + } + + if self + .author_specified + .contains(LonghandId::FontSynthesisWeight) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT); + } + + if self + .author_specified + .contains(LonghandId::FontSynthesisStyle) + { + builder.add_flags(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE); + } + + #[cfg(feature = "servo")] + { + if let Some(font) = builder.get_font_if_mutated() { + font.compute_font_hash(); + } + } + } + + fn try_to_use_cached_reset_properties( + &self, + builder: &mut StyleBuilder<'b>, + cache: Option<&'b RuleCache>, + guards: &StylesheetGuards, + ) -> bool { + let style = match self.first_line_reparenting { + FirstLineReparenting::Yes { style_to_reparent } => style_to_reparent, + FirstLineReparenting::No => { + let Some(cache) = cache else { return false }; + let Some(style) = cache.find(guards, builder) else { + return false; + }; + style + }, + }; + + builder.copy_reset_from(style); + + // We're using the same reset style as another element, and we'll skip + // applying the relevant properties. So we need to do the relevant + // bookkeeping here to keep these bits correct. + // + // Note that the border/background properties are non-inherited, so we + // don't need to do anything else other than just copying the bits over. + // + // When using this optimization, we also need to copy whether the old + // style specified viewport units / used font-relative lengths, this one + // would as well. It matches the same rules, so it is the right thing + // to do anyways, even if it's only used on inherited properties. + let bits_to_copy = ComputedValueFlags::HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND | + ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS | + ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS | + ComputedValueFlags::USES_CONTAINER_UNITS | + ComputedValueFlags::USES_VIEWPORT_UNITS; + builder.add_flags(style.flags & bits_to_copy); + + true + } + + /// The initial font depends on the current lang group so we may need to + /// recompute it if the language changed. + #[inline] + #[cfg(feature = "gecko")] + fn recompute_initial_font_family_if_needed(&self, builder: &mut StyleBuilder) { + use crate::gecko_bindings::bindings; + use crate::values::computed::font::FontFamily; + + let default_font_type = { + let font = builder.get_font(); + + if !font.mFont.family.is_initial { + return; + } + + let default_font_type = unsafe { + bindings::Gecko_nsStyleFont_ComputeFallbackFontTypeForLanguage( + builder.device.document(), + font.mLanguage.mRawPtr, + ) + }; + + let initial_generic = font.mFont.family.families.single_generic(); + debug_assert!( + initial_generic.is_some(), + "Initial font should be just one generic font" + ); + if initial_generic == Some(default_font_type) { + return; + } + + default_font_type + }; + + // NOTE: Leaves is_initial untouched. + builder.mutate_font().mFont.family.families = + FontFamily::generic(default_font_type).families.clone(); + } + + /// Prioritize user fonts if needed by pref. + #[inline] + #[cfg(feature = "gecko")] + fn prioritize_user_fonts_if_needed(&self, builder: &mut StyleBuilder) { + use crate::gecko_bindings::bindings; + + // Check the use_document_fonts setting for content, but for chrome + // documents they're treated as always enabled. + if static_prefs::pref!("browser.display.use_document_fonts") != 0 || + builder.device.chrome_rules_enabled_for_document() + { + return; + } + + let default_font_type = { + let font = builder.get_font(); + + if font.mFont.family.is_system_font { + return; + } + + if !font.mFont.family.families.needs_user_font_prioritization() { + return; + } + + unsafe { + bindings::Gecko_nsStyleFont_ComputeFallbackFontTypeForLanguage( + builder.device.document(), + font.mLanguage.mRawPtr, + ) + } + }; + + let font = builder.mutate_font(); + font.mFont + .family + .families + .prioritize_first_generic_or_prepend(default_font_type); + } + + /// Some keyword sizes depend on the font family and language. + #[cfg(feature = "gecko")] + fn recompute_keyword_font_size_if_needed(&self, context: &mut computed::Context) { + use crate::values::computed::ToComputedValue; + + if !self.seen.contains(LonghandId::XLang) && !self.seen.contains(LonghandId::FontFamily) { + return; + } + + let new_size = { + let font = context.builder.get_font(); + let info = font.clone_font_size().keyword_info; + let new_size = match info.kw { + specified::FontSizeKeyword::None => return, + _ => { + context.for_non_inherited_property = false; + specified::FontSize::Keyword(info).to_computed_value(context) + }, + }; + + if font.mScriptUnconstrainedSize == new_size.computed_size { + return; + } + + new_size + }; + + context.builder.mutate_font().set_font_size(new_size); + } + + /// Some properties, plus setting font-size itself, may make us go out of + /// our minimum font-size range. + #[cfg(feature = "gecko")] + fn constrain_font_size_if_needed(&self, builder: &mut StyleBuilder) { + use crate::gecko_bindings::bindings; + use crate::values::generics::NonNegative; + + let min_font_size = { + let font = builder.get_font(); + let min_font_size = unsafe { + bindings::Gecko_nsStyleFont_ComputeMinSize(&**font, builder.device.document()) + }; + + if font.mFont.size.0 >= min_font_size { + return; + } + + NonNegative(min_font_size) + }; + + builder.mutate_font().mFont.size = min_font_size; + } + + /// <svg:text> is not affected by text zoom, and it uses a preshint to disable it. We fix up + /// the struct when this happens by unzooming its contained font values, which will have been + /// zoomed in the parent. + /// + /// FIXME(emilio): Why doing this _before_ handling font-size? That sounds wrong. + #[cfg(feature = "gecko")] + fn unzoom_fonts_if_needed(&self, builder: &mut StyleBuilder) { + debug_assert!(self.seen.contains(LonghandId::XTextScale)); + + let parent_text_scale = builder.get_parent_font().clone__x_text_scale(); + let text_scale = builder.get_font().clone__x_text_scale(); + if parent_text_scale == text_scale { + return; + } + debug_assert_ne!( + parent_text_scale.text_zoom_enabled(), + text_scale.text_zoom_enabled(), + "There's only one value that disables it" + ); + debug_assert!( + !text_scale.text_zoom_enabled(), + "We only ever disable text zoom (in svg:text), never enable it" + ); + let device = builder.device; + builder.mutate_font().unzoom_fonts(device); + } + + /// Special handling of font-size: math (used for MathML). + /// https://w3c.github.io/mathml-core/#the-math-script-level-property + /// TODO: Bug: 1548471: MathML Core also does not specify a script min size + /// should we unship that feature or standardize it? + #[cfg(feature = "gecko")] + fn recompute_math_font_size_if_needed(&self, context: &mut computed::Context) { + use crate::values::generics::NonNegative; + + // Do not do anything if font-size: math or math-depth is not set. + if context.builder.get_font().clone_font_size().keyword_info.kw != + specified::FontSizeKeyword::Math + { + return; + } + + const SCALE_FACTOR_WHEN_INCREMENTING_MATH_DEPTH_BY_ONE: f32 = 0.71; + + // Helper function that calculates the scale factor applied to font-size + // when math-depth goes from parent_math_depth to computed_math_depth. + // This function is essentially a modification of the MathML3's formula + // 0.71^(parent_math_depth - computed_math_depth) so that a scale factor + // of parent_script_percent_scale_down is applied when math-depth goes + // from 0 to 1 and parent_script_script_percent_scale_down is applied + // when math-depth goes from 0 to 2. This is also a straightforward + // implementation of the specification's algorithm: + // https://w3c.github.io/mathml-core/#the-math-script-level-property + fn scale_factor_for_math_depth_change( + parent_math_depth: i32, + computed_math_depth: i32, + parent_script_percent_scale_down: Option<f32>, + parent_script_script_percent_scale_down: Option<f32>, + ) -> f32 { + let mut a = parent_math_depth; + let mut b = computed_math_depth; + let c = SCALE_FACTOR_WHEN_INCREMENTING_MATH_DEPTH_BY_ONE; + let scale_between_0_and_1 = parent_script_percent_scale_down.unwrap_or_else(|| c); + let scale_between_0_and_2 = + parent_script_script_percent_scale_down.unwrap_or_else(|| c * c); + let mut s = 1.0; + let mut invert_scale_factor = false; + if a == b { + return s; + } + if b < a { + mem::swap(&mut a, &mut b); + invert_scale_factor = true; + } + let mut e = b - a; + if a <= 0 && b >= 2 { + s *= scale_between_0_and_2; + e -= 2; + } else if a == 1 { + s *= scale_between_0_and_2 / scale_between_0_and_1; + e -= 1; + } else if b == 1 { + s *= scale_between_0_and_1; + e -= 1; + } + s *= (c as f32).powi(e); + if invert_scale_factor { + 1.0 / s.max(f32::MIN_POSITIVE) + } else { + s + } + } + + let (new_size, new_unconstrained_size) = { + let builder = &context.builder; + let font = builder.get_font(); + let parent_font = builder.get_parent_font(); + + let delta = font.mMathDepth.saturating_sub(parent_font.mMathDepth); + + if delta == 0 { + return; + } + + let mut min = parent_font.mScriptMinSize; + if font.mXTextScale.text_zoom_enabled() { + min = builder.device.zoom_text(min); + } + + // Calculate scale factor following MathML Core's algorithm. + let scale = { + // Script scale factors are independent of orientation. + let font_metrics = context.query_font_metrics( + FontBaseSize::InheritedStyle, + FontMetricsOrientation::Horizontal, + /* retrieve_math_scales = */ true, + ); + scale_factor_for_math_depth_change( + parent_font.mMathDepth as i32, + font.mMathDepth as i32, + font_metrics.script_percent_scale_down, + font_metrics.script_script_percent_scale_down, + ) + }; + + let parent_size = parent_font.mSize.0; + let parent_unconstrained_size = parent_font.mScriptUnconstrainedSize.0; + let new_size = parent_size.scale_by(scale); + let new_unconstrained_size = parent_unconstrained_size.scale_by(scale); + + if scale <= 1. { + // The parent size can be smaller than scriptminsize, e.g. if it + // was specified explicitly. Don't scale in this case, but we + // don't want to set it to scriptminsize either since that will + // make it larger. + if parent_size <= min { + (parent_size, new_unconstrained_size) + } else { + (min.max(new_size), new_unconstrained_size) + } + } else { + // If the new unconstrained size is larger than the min size, + // this means we have escaped the grasp of scriptminsize and can + // revert to using the unconstrained size. + // However, if the new size is even larger (perhaps due to usage + // of em units), use that instead. + ( + new_size.min(new_unconstrained_size.max(min)), + new_unconstrained_size, + ) + } + }; + let font = context.builder.mutate_font(); + font.mFont.size = NonNegative(new_size); + font.mSize = NonNegative(new_size); + font.mScriptUnconstrainedSize = NonNegative(new_unconstrained_size); + } +} diff --git a/servo/components/style/properties/computed_value_flags.rs b/servo/components/style/properties/computed_value_flags.rs new file mode 100644 index 0000000000..0952cb1799 --- /dev/null +++ b/servo/components/style/properties/computed_value_flags.rs @@ -0,0 +1,194 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Misc information about a given computed style. + +bitflags! { + /// Misc information about a given computed style. + /// + /// All flags are currently inherited for text, pseudo elements, and + /// anonymous boxes, see StyleBuilder::for_inheritance and its callsites. + /// If we ever want to add some flags that shouldn't inherit for them, + /// we might want to add a function to handle this. + #[repr(C)] + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ComputedValueFlags: u32 { + /// Whether the style or any of the ancestors has a text-decoration-line + /// property that should get propagated to descendants. + /// + /// text-decoration-line is a reset property, but gets propagated in the + /// frame/box tree. + const HAS_TEXT_DECORATION_LINES = 1 << 0; + + /// Whether line break inside should be suppressed. + /// + /// If this flag is set, the line should not be broken inside, + /// which means inlines act as if nowrap is set, <br> element is + /// suppressed, and blocks are inlinized. + /// + /// This bit is propagated to all children of line participants. + /// It is currently used by ruby to make its content unbreakable. + const SHOULD_SUPPRESS_LINEBREAK = 1 << 1; + + /// A flag used to mark text that that has text-combine-upright. + /// + /// This is used from Gecko's layout engine. + const IS_TEXT_COMBINED = 1 << 2; + + /// A flag used to mark styles under a relevant link that is also + /// visited. + const IS_RELEVANT_LINK_VISITED = 1 << 3; + + /// A flag used to mark styles which are a pseudo-element or under one. + const IS_IN_PSEUDO_ELEMENT_SUBTREE = 1 << 4; + + /// A flag used to mark styles which have contain:style or under one. + const SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE = 1 << 5; + + /// Whether this style's `display` property depends on our parent style. + /// + /// This is important because it may affect our optimizations to avoid + /// computing the style of pseudo-elements, given whether the + /// pseudo-element is generated depends on the `display` value. + const DISPLAY_DEPENDS_ON_INHERITED_STYLE = 1 << 6; + + /// Whether this style's `content` depends on our parent style. + /// + /// Important because of the same reason. + const CONTENT_DEPENDS_ON_INHERITED_STYLE = 1 << 7; + + /// Whether the child explicitly inherits any reset property. + const INHERITS_RESET_STYLE = 1 << 8; + + /// Whether any value on our style is font-metric-dependent on our + /// primary font. + const DEPENDS_ON_SELF_FONT_METRICS = 1 << 9; + + /// Whether any value on our style is font-metric-dependent on the + /// primary font of our parent. + const DEPENDS_ON_INHERITED_FONT_METRICS = 1 << 10; + + /// Whether the style or any of the ancestors has a multicol style. + /// + /// Only used in Servo. + const CAN_BE_FRAGMENTED = 1 << 11; + + /// Whether this style is the style of the document element. + const IS_ROOT_ELEMENT_STYLE = 1 << 12; + + /// Whether this element is inside an `opacity: 0` subtree. + const IS_IN_OPACITY_ZERO_SUBTREE = 1 << 13; + + /// Whether there are author-specified rules for border-* properties + /// (except border-image-*), background-color, or background-image. + /// + /// TODO(emilio): Maybe do include border-image, see: + /// + /// https://github.com/w3c/csswg-drafts/issues/4777#issuecomment-604424845 + const HAS_AUTHOR_SPECIFIED_BORDER_BACKGROUND = 1 << 14; + + /// Whether there are author-specified rules for `font-family`. + const HAS_AUTHOR_SPECIFIED_FONT_FAMILY = 1 << 16; + + /// Whether there are author-specified rules for `font-synthesis-weight`. + const HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT = 1 << 17; + + /// Whether there are author-specified rules for `font-synthesis-style`. + const HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE = 1 << 18; + + // (There's also font-synthesis-small-caps and font-synthesis-position, + // but we don't currently need to keep track of those.) + + /// Whether there are author-specified rules for `letter-spacing`. + const HAS_AUTHOR_SPECIFIED_LETTER_SPACING = 1 << 19; + + /// Whether there are author-specified rules for `word-spacing`. + const HAS_AUTHOR_SPECIFIED_WORD_SPACING = 1 << 20; + + /// Whether the style depends on viewport units. + const USES_VIEWPORT_UNITS = 1 << 21; + + /// Whether the style depends on viewport units on container queries. + /// + /// This needs to be a separate flag from `USES_VIEWPORT_UNITS` because + /// it causes us to re-match the style (rather than re-cascascading it, + /// which is enough for other uses of viewport units). + const USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES = 1 << 22; + + /// A flag used to mark styles which have `container-type` of `size` or + /// `inline-size`, or under one. + const SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE = 1 << 23; + + /// Whether the style evaluated any relative selector. + const CONSIDERED_RELATIVE_SELECTOR = 1 << 24; + + /// Whether the style evaluated the matched element to be an anchor of + /// a relative selector. + const ANCHORS_RELATIVE_SELECTOR = 1 << 25; + + /// Whether the style uses container query units, in which case the style depends on the + /// container's size and we can't reuse it across cousins (without double-checking the + /// container at least). + const USES_CONTAINER_UNITS = 1 << 26; + + /// Whether there are author-specific rules for text `color`. + const HAS_AUTHOR_SPECIFIED_TEXT_COLOR = 1 << 27; + } +} + +impl Default for ComputedValueFlags { + #[inline] + fn default() -> Self { + Self::empty() + } +} + +impl ComputedValueFlags { + /// Flags that are unconditionally propagated to descendants. + #[inline] + fn inherited_flags() -> Self { + Self::IS_RELEVANT_LINK_VISITED | + Self::CAN_BE_FRAGMENTED | + Self::IS_IN_PSEUDO_ELEMENT_SUBTREE | + Self::HAS_TEXT_DECORATION_LINES | + Self::IS_IN_OPACITY_ZERO_SUBTREE | + Self::SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE | + Self::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE + } + + /// Flags that may be propagated to descendants. + #[inline] + fn maybe_inherited_flags() -> Self { + Self::inherited_flags() | Self::SHOULD_SUPPRESS_LINEBREAK + } + + /// Flags that are an input to the cascade. + #[inline] + fn cascade_input_flags() -> Self { + Self::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES | + Self::CONSIDERED_RELATIVE_SELECTOR | + Self::ANCHORS_RELATIVE_SELECTOR + } + + /// Returns the flags that are always propagated to descendants. + /// + /// See StyleAdjuster::set_bits and StyleBuilder. + #[inline] + pub fn inherited(self) -> Self { + self & Self::inherited_flags() + } + + /// Flags that are conditionally propagated to descendants, just to handle + /// properly style invalidation. + #[inline] + pub fn maybe_inherited(self) -> Self { + self & Self::maybe_inherited_flags() + } + + /// Flags that are an input to the cascade. + #[inline] + pub fn for_cascade_inputs(self) -> Self { + self & Self::cascade_input_flags() + } +} diff --git a/servo/components/style/properties/counted_unknown_properties.py b/servo/components/style/properties/counted_unknown_properties.py new file mode 100644 index 0000000000..b1b800812d --- /dev/null +++ b/servo/components/style/properties/counted_unknown_properties.py @@ -0,0 +1,110 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +COUNTED_UNKNOWN_PROPERTIES = [ + "-webkit-font-smoothing", + "-webkit-tap-highlight-color", + "speak", + "text-size-adjust", + "-webkit-font-feature-settings", + "-webkit-user-drag", + "orphans", + "widows", + "-webkit-user-modify", + "-webkit-margin-before", + "-webkit-margin-after", + "-webkit-margin-start", + "-webkit-column-break-inside", + "-webkit-padding-start", + "-webkit-margin-end", + "-webkit-box-reflect", + "-webkit-print-color-adjust", + "-webkit-mask-box-image", + "-webkit-line-break", + "alignment-baseline", + "-webkit-writing-mode", + "baseline-shift", + "-webkit-hyphenate-character", + "-webkit-highlight", + "background-repeat-x", + "-webkit-padding-end", + "background-repeat-y", + "-webkit-text-emphasis-color", + "-webkit-margin-top-collapse", + "-webkit-rtl-ordering", + "-webkit-padding-before", + "-webkit-text-decorations-in-effect", + "-webkit-border-vertical-spacing", + "-webkit-locale", + "-webkit-padding-after", + "-webkit-border-horizontal-spacing", + "color-rendering", + "-webkit-column-break-before", + "-webkit-transform-origin-x", + "-webkit-transform-origin-y", + "-webkit-text-emphasis-position", + "buffered-rendering", + "-webkit-text-orientation", + "-webkit-text-combine", + "-webkit-text-emphasis-style", + "-webkit-text-emphasis", + "-webkit-mask-box-image-width", + "-webkit-mask-box-image-source", + "-webkit-mask-box-image-outset", + "-webkit-mask-box-image-slice", + "-webkit-mask-box-image-repeat", + "-webkit-margin-after-collapse", + "-webkit-border-before-color", + "-webkit-border-before-width", + "-webkit-perspective-origin-x", + "-webkit-perspective-origin-y", + "-webkit-margin-before-collapse", + "-webkit-border-before-style", + "-webkit-margin-bottom-collapse", + "-webkit-ruby-position", + "-webkit-column-break-after", + "-webkit-margin-collapse", + "-webkit-border-before", + "-webkit-border-end", + "-webkit-border-after", + "-webkit-border-start", + "-webkit-min-logical-width", + "-webkit-logical-height", + "-webkit-transform-origin-z", + "-webkit-font-size-delta", + "-webkit-logical-width", + "-webkit-max-logical-width", + "-webkit-min-logical-height", + "-webkit-max-logical-height", + "-webkit-border-end-color", + "-webkit-border-end-width", + "-webkit-border-start-color", + "-webkit-border-start-width", + "-webkit-border-after-color", + "-webkit-border-after-width", + "-webkit-border-end-style", + "-webkit-border-after-style", + "-webkit-border-start-style", + "-webkit-mask-repeat-x", + "-webkit-mask-repeat-y", + "user-zoom", + "min-zoom", + "-webkit-box-decoration-break", + "orientation", + "max-zoom", + "-webkit-app-region", + "-webkit-column-rule", + "-webkit-column-span", + "-webkit-column-gap", + "-webkit-shape-outside", + "-webkit-column-rule-width", + "-webkit-column-count", + "-webkit-opacity", + "-webkit-column-width", + "-webkit-shape-image-threshold", + "-webkit-column-rule-style", + "-webkit-columns", + "-webkit-column-rule-color", + "-webkit-shape-margin", +] diff --git a/servo/components/style/properties/data.py b/servo/components/style/properties/data.py new file mode 100644 index 0000000000..093f1cb75f --- /dev/null +++ b/servo/components/style/properties/data.py @@ -0,0 +1,1083 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import re +from counted_unknown_properties import COUNTED_UNKNOWN_PROPERTIES + +# It is important that the order of these physical / logical variants matches +# the order of the enum variants in logical_geometry.rs +PHYSICAL_SIDES = ["top", "right", "bottom", "left"] +PHYSICAL_CORNERS = ["top-left", "top-right", "bottom-right", "bottom-left"] +PHYSICAL_AXES = ["y", "x"] +PHYSICAL_SIZES = ["height", "width"] +LOGICAL_SIDES = ["block-start", "block-end", "inline-start", "inline-end"] +LOGICAL_CORNERS = ["start-start", "start-end", "end-start", "end-end"] +LOGICAL_SIZES = ["block-size", "inline-size"] +LOGICAL_AXES = ["block", "inline"] + +# bool is True when logical +ALL_SIDES = [(side, False) for side in PHYSICAL_SIDES] + [ + (side, True) for side in LOGICAL_SIDES +] +ALL_SIZES = [(size, False) for size in PHYSICAL_SIZES] + [ + (size, True) for size in LOGICAL_SIZES +] +ALL_CORNERS = [(corner, False) for corner in PHYSICAL_CORNERS] + [ + (corner, True) for corner in LOGICAL_CORNERS +] +ALL_AXES = [(axis, False) for axis in PHYSICAL_AXES] + [ + (axis, True) for axis in LOGICAL_AXES +] + +SYSTEM_FONT_LONGHANDS = """font_family font_size font_style + font_stretch font_weight""".split() + +PRIORITARY_PROPERTIES = set( + [ + # The writing-mode group has the most priority of all property groups, as + # sizes like font-size can depend on it. + "writing-mode", + "direction", + "text-orientation", + # The fonts and colors group has the second priority, as all other lengths + # and colors depend on them. + # + # There are some interdependencies between these, but we fix them up in + # Cascade::fixup_font_stuff. + # Needed to properly compute the zoomed font-size. + "-x-text-scale", + # Needed to do font-size computation in a language-dependent way. + "-x-lang", + # Needed for ruby to respect language-dependent min-font-size + # preferences properly, see bug 1165538. + "-moz-min-font-size-ratio", + # font-size depends on math-depth's computed value. + "math-depth", + # Needed to compute the first available font and its used size, + # in order to compute font-relative units correctly. + "font-size", + "font-size-adjust", + "font-weight", + "font-stretch", + "font-style", + "font-family", + # color-scheme affects how system colors resolve. + "color-scheme", + # forced-color-adjust affects whether colors are adjusted. + "forced-color-adjust", + # Zoom affects all absolute lengths. + "zoom", + # Line height lengths depend on this. + "line-height", + ] +) + +VISITED_DEPENDENT_PROPERTIES = set( + [ + "column-rule-color", + "text-emphasis-color", + "-webkit-text-fill-color", + "-webkit-text-stroke-color", + "text-decoration-color", + "fill", + "stroke", + "caret-color", + "background-color", + "border-top-color", + "border-right-color", + "border-bottom-color", + "border-left-color", + "border-block-start-color", + "border-inline-end-color", + "border-block-end-color", + "border-inline-start-color", + "outline-color", + "color", + ] +) + +# Bitfield values for all rule types which can have property declarations. +STYLE_RULE = 1 << 0 +PAGE_RULE = 1 << 1 +KEYFRAME_RULE = 1 << 2 + +ALL_RULES = STYLE_RULE | PAGE_RULE | KEYFRAME_RULE +DEFAULT_RULES = STYLE_RULE | KEYFRAME_RULE +DEFAULT_RULES_AND_PAGE = DEFAULT_RULES | PAGE_RULE +DEFAULT_RULES_EXCEPT_KEYFRAME = STYLE_RULE + +# Rule name to value dict +RULE_VALUES = { + "Style": STYLE_RULE, + "Page": PAGE_RULE, + "Keyframe": KEYFRAME_RULE, +} + + +def rule_values_from_arg(that): + if isinstance(that, int): + return that + mask = 0 + for rule in that.split(): + mask |= RULE_VALUES[rule] + return mask + + +def maybe_moz_logical_alias(engine, side, prop): + if engine == "gecko" and side[1]: + axis, dir = side[0].split("-") + if axis == "inline": + return prop % dir + return None + + +def to_rust_ident(name): + name = name.replace("-", "_") + if name in ["static", "super", "box", "move"]: # Rust keywords + name += "_" + return name + + +def to_snake_case(ident): + return re.sub("([A-Z]+)", lambda m: "_" + m.group(1).lower(), ident).strip("_") + + +def to_camel_case(ident): + return re.sub( + "(^|_|-)([a-z0-9])", lambda m: m.group(2).upper(), ident.strip("_").strip("-") + ) + + +def to_camel_case_lower(ident): + camel = to_camel_case(ident) + return camel[0].lower() + camel[1:] + + +# https://drafts.csswg.org/cssom/#css-property-to-idl-attribute +def to_idl_name(ident): + return re.sub("-([a-z])", lambda m: m.group(1).upper(), ident) + + +def parse_aliases(value): + aliases = {} + for pair in value.split(): + [a, v] = pair.split("=") + aliases[a] = v + return aliases + + +class Keyword(object): + def __init__( + self, + name, + values, + gecko_constant_prefix=None, + gecko_enum_prefix=None, + custom_consts=None, + extra_gecko_values=None, + extra_servo_2013_values=None, + extra_servo_2020_values=None, + gecko_aliases=None, + servo_2013_aliases=None, + servo_2020_aliases=None, + gecko_strip_moz_prefix=None, + gecko_inexhaustive=None, + ): + self.name = name + self.values = values.split() + if gecko_constant_prefix and gecko_enum_prefix: + raise TypeError( + "Only one of gecko_constant_prefix and gecko_enum_prefix " + "can be specified" + ) + self.gecko_constant_prefix = ( + gecko_constant_prefix or "NS_STYLE_" + self.name.upper().replace("-", "_") + ) + self.gecko_enum_prefix = gecko_enum_prefix + self.extra_gecko_values = (extra_gecko_values or "").split() + self.extra_servo_2013_values = (extra_servo_2013_values or "").split() + self.extra_servo_2020_values = (extra_servo_2020_values or "").split() + self.gecko_aliases = parse_aliases(gecko_aliases or "") + self.servo_2013_aliases = parse_aliases(servo_2013_aliases or "") + self.servo_2020_aliases = parse_aliases(servo_2020_aliases or "") + self.consts_map = {} if custom_consts is None else custom_consts + self.gecko_strip_moz_prefix = ( + True if gecko_strip_moz_prefix is None else gecko_strip_moz_prefix + ) + self.gecko_inexhaustive = gecko_inexhaustive or (gecko_enum_prefix is None) + + def values_for(self, engine): + if engine == "gecko": + return self.values + self.extra_gecko_values + elif engine == "servo-2013": + return self.values + self.extra_servo_2013_values + elif engine == "servo-2020": + return self.values + self.extra_servo_2020_values + else: + raise Exception("Bad engine: " + engine) + + def aliases_for(self, engine): + if engine == "gecko": + return self.gecko_aliases + elif engine == "servo-2013": + return self.servo_2013_aliases + elif engine == "servo-2020": + return self.servo_2020_aliases + else: + raise Exception("Bad engine: " + engine) + + def gecko_constant(self, value): + moz_stripped = ( + value.replace("-moz-", "") + if self.gecko_strip_moz_prefix + else value.replace("-moz-", "moz-") + ) + mapped = self.consts_map.get(value) + if self.gecko_enum_prefix: + parts = moz_stripped.replace("-", "_").split("_") + parts = mapped if mapped else [p.title() for p in parts] + return self.gecko_enum_prefix + "::" + "".join(parts) + else: + suffix = mapped if mapped else moz_stripped.replace("-", "_") + return self.gecko_constant_prefix + "_" + suffix.upper() + + def needs_cast(self): + return self.gecko_enum_prefix is None + + def maybe_cast(self, type_str): + return "as " + type_str if self.needs_cast() else "" + + def casted_constant_name(self, value, cast_type): + if cast_type is None: + raise TypeError("We should specify the cast_type.") + + if self.gecko_enum_prefix is None: + return cast_type.upper() + "_" + self.gecko_constant(value) + else: + return ( + cast_type.upper() + + "_" + + self.gecko_constant(value).upper().replace("::", "_") + ) + + +def arg_to_bool(arg): + if isinstance(arg, bool): + return arg + assert arg in ["True", "False"], "Unexpected value for boolean arguement: " + repr( + arg + ) + return arg == "True" + + +def parse_property_aliases(alias_list): + result = [] + if alias_list: + for alias in alias_list.split(): + (name, _, pref) = alias.partition(":") + result.append((name, pref)) + return result + + +def to_phys(name, logical, physical): + return name.replace(logical, physical).replace("inset-", "") + + +class Property(object): + def __init__( + self, + name, + spec, + servo_2013_pref, + servo_2020_pref, + gecko_pref, + enabled_in, + rule_types_allowed, + aliases, + extra_prefixes, + flags, + ): + self.name = name + if not spec: + raise TypeError("Spec should be specified for " + name) + self.spec = spec + self.ident = to_rust_ident(name) + self.camel_case = to_camel_case(self.ident) + self.servo_2013_pref = servo_2013_pref + self.servo_2020_pref = servo_2020_pref + self.gecko_pref = gecko_pref + self.rule_types_allowed = rule_values_from_arg(rule_types_allowed) + # For enabled_in, the setup is as follows: + # It needs to be one of the four values: ["", "ua", "chrome", "content"] + # * "chrome" implies "ua", and implies that they're explicitly + # enabled. + # * "" implies the property will never be parsed. + # * "content" implies the property is accessible unconditionally, + # modulo a pref, set via servo_pref / gecko_pref. + assert enabled_in in ("", "ua", "chrome", "content") + self.enabled_in = enabled_in + self.aliases = parse_property_aliases(aliases) + self.extra_prefixes = parse_property_aliases(extra_prefixes) + self.flags = flags.split() if flags else [] + + def rule_types_allowed_names(self): + for name in RULE_VALUES: + if self.rule_types_allowed & RULE_VALUES[name] != 0: + yield name + + def experimental(self, engine): + if engine == "gecko": + return bool(self.gecko_pref) + elif engine == "servo-2013": + return bool(self.servo_2013_pref) + elif engine == "servo-2020": + return bool(self.servo_2020_pref) + else: + raise Exception("Bad engine: " + engine) + + def explicitly_enabled_in_ua_sheets(self): + return self.enabled_in in ("ua", "chrome") + + def explicitly_enabled_in_chrome(self): + return self.enabled_in == "chrome" + + def enabled_in_content(self): + return self.enabled_in == "content" + + def is_visited_dependent(self): + return self.name in VISITED_DEPENDENT_PROPERTIES + + def is_prioritary(self): + return self.name in PRIORITARY_PROPERTIES + + def nscsspropertyid(self): + return "nsCSSPropertyID::eCSSProperty_" + self.ident + + +class Longhand(Property): + def __init__( + self, + style_struct, + name, + spec=None, + animation_value_type=None, + keyword=None, + predefined_type=None, + servo_2013_pref=None, + servo_2020_pref=None, + gecko_pref=None, + enabled_in="content", + need_index=False, + gecko_ffi_name=None, + has_effect_on_gecko_scrollbars=None, + rule_types_allowed=DEFAULT_RULES, + cast_type="u8", + logical=False, + logical_group=None, + aliases=None, + extra_prefixes=None, + boxed=False, + flags=None, + allow_quirks="No", + ignored_when_colors_disabled=False, + simple_vector_bindings=False, + vector=False, + servo_restyle_damage="repaint", + affects=None, + ): + Property.__init__( + self, + name=name, + spec=spec, + servo_2013_pref=servo_2013_pref, + servo_2020_pref=servo_2020_pref, + gecko_pref=gecko_pref, + enabled_in=enabled_in, + rule_types_allowed=rule_types_allowed, + aliases=aliases, + extra_prefixes=extra_prefixes, + flags=flags, + ) + + self.affects = affects + self.flags += self.affects_flags() + + self.keyword = keyword + self.predefined_type = predefined_type + self.style_struct = style_struct + self.has_effect_on_gecko_scrollbars = has_effect_on_gecko_scrollbars + assert ( + has_effect_on_gecko_scrollbars in [None, False, True] + and not style_struct.inherited + or (gecko_pref is None and enabled_in != "") + == (has_effect_on_gecko_scrollbars is None) + ), ( + "Property " + + name + + ": has_effect_on_gecko_scrollbars must be " + + "specified, and must have a value of True or False, iff a " + + "property is inherited and is behind a Gecko pref or internal" + ) + self.need_index = need_index + self.gecko_ffi_name = gecko_ffi_name or "m" + self.camel_case + self.cast_type = cast_type + self.logical = arg_to_bool(logical) + self.logical_group = logical_group + if self.logical: + assert logical_group, "Property " + name + " must have a logical group" + + self.boxed = arg_to_bool(boxed) + self.allow_quirks = allow_quirks + self.ignored_when_colors_disabled = ignored_when_colors_disabled + self.is_vector = vector + self.simple_vector_bindings = simple_vector_bindings + + # This is done like this since just a plain bool argument seemed like + # really random. + if animation_value_type is None: + raise TypeError( + "animation_value_type should be specified for (" + name + ")" + ) + self.animation_value_type = animation_value_type + + self.animatable = animation_value_type != "none" + self.is_animatable_with_computed_value = ( + animation_value_type == "ComputedValue" + or animation_value_type == "discrete" + ) + + # See compute_damage for the various values this can take + self.servo_restyle_damage = servo_restyle_damage + + def affects_flags(self): + # Layout is the stronger hint. This property animation affects layout + # or frame construction. `display` or `width` are examples that should + # use this. + if self.affects == "layout": + return ["AFFECTS_LAYOUT"] + # This property doesn't affect layout, but affects overflow. + # `transform` and co. are examples of this. + if self.affects == "overflow": + return ["AFFECTS_OVERFLOW"] + # This property affects the rendered output but doesn't affect layout. + # `opacity`, `color`, or `z-index` are examples of this. + if self.affects == "paint": + return ["AFFECTS_PAINT"] + # This property doesn't affect rendering in any way. + # `user-select` is an example of this. + assert self.affects == "", ( + "Property " + + self.name + + ': affects must be specified and be one of ["layout", "overflow", "paint", ""], see Longhand.affects_flags for documentation' + ) + return [] + + @staticmethod + def type(): + return "longhand" + + # For a given logical property, return the kind of mapping we need to + # perform, and which logical value we represent, in a tuple. + def logical_mapping_data(self, data): + if not self.logical: + return [] + # Sizes and axes are basically the same for mapping, we just need + # slightly different replacements (block-size -> height, etc rather + # than -x/-y) below. + for [ty, logical_items, physical_items] in [ + ["Side", LOGICAL_SIDES, PHYSICAL_SIDES], + ["Corner", LOGICAL_CORNERS, PHYSICAL_CORNERS], + ["Axis", LOGICAL_SIZES, PHYSICAL_SIZES], + ["Axis", LOGICAL_AXES, PHYSICAL_AXES], + ]: + candidate = [s for s in logical_items if s in self.name] + if candidate: + assert len(candidate) == 1 + return [ty, candidate[0], logical_items, physical_items] + assert False, "Don't know how to deal with " + self.name + + def logical_mapping_kind(self, data): + assert self.logical + [kind, item, _, _] = self.logical_mapping_data(data) + return "LogicalMappingKind::{}(Logical{}::{})".format( + kind, kind, to_camel_case(item.replace("-size", "")) + ) + + # For a given logical property return all the physical property names + # corresponding to it. + def all_physical_mapped_properties(self, data): + if not self.logical: + return [] + [_, logical_side, _, physical_items] = self.logical_mapping_data(data) + return [ + data.longhands_by_name[to_phys(self.name, logical_side, physical_side)] + for physical_side in physical_items + ] + + def may_be_disabled_in(self, shorthand, engine): + if engine == "gecko": + return self.gecko_pref and self.gecko_pref != shorthand.gecko_pref + elif engine == "servo-2013": + return ( + self.servo_2013_pref + and self.servo_2013_pref != shorthand.servo_2013_pref + ) + elif engine == "servo-2020": + return ( + self.servo_2020_pref + and self.servo_2020_pref != shorthand.servo_2020_pref + ) + else: + raise Exception("Bad engine: " + engine) + + def base_type(self): + if self.predefined_type and not self.is_vector: + return "crate::values::specified::{}".format(self.predefined_type) + return "longhands::{}::SpecifiedValue".format(self.ident) + + def specified_type(self): + if self.predefined_type and not self.is_vector: + ty = "crate::values::specified::{}".format(self.predefined_type) + else: + ty = "longhands::{}::SpecifiedValue".format(self.ident) + if self.boxed: + ty = "Box<{}>".format(ty) + return ty + + def specified_is_copy(self): + if self.is_vector or self.boxed: + return False + if self.predefined_type: + return self.predefined_type in { + "AlignContent", + "AlignItems", + "AlignSelf", + "Appearance", + "AnimationComposition", + "AnimationDirection", + "AnimationFillMode", + "AnimationPlayState", + "AspectRatio", + "BaselineSource", + "BreakBetween", + "BreakWithin", + "BackgroundRepeat", + "BorderImageRepeat", + "BorderStyle", + "table::CaptionSide", + "Clear", + "ColumnCount", + "Contain", + "ContentVisibility", + "ContainerType", + "Display", + "FillRule", + "Float", + "FontLanguageOverride", + "FontSizeAdjust", + "FontStretch", + "FontStyle", + "FontSynthesis", + "FontVariantEastAsian", + "FontVariantLigatures", + "FontVariantNumeric", + "FontWeight", + "GreaterThanOrEqualToOneNumber", + "GridAutoFlow", + "ImageRendering", + "InitialLetter", + "Integer", + "JustifyContent", + "JustifyItems", + "JustifySelf", + "LineBreak", + "LineClamp", + "MasonryAutoFlow", + "ui::MozTheme", + "BoolInteger", + "text::MozControlCharacterVisibility", + "MathDepth", + "MozScriptMinSize", + "MozScriptSizeMultiplier", + "TransformBox", + "TextDecorationSkipInk", + "NonNegativeNumber", + "OffsetRotate", + "Opacity", + "OutlineStyle", + "Overflow", + "OverflowAnchor", + "OverflowClipBox", + "OverflowWrap", + "OverscrollBehavior", + "PageOrientation", + "Percentage", + "PrintColorAdjust", + "ForcedColorAdjust", + "Resize", + "RubyPosition", + "SVGOpacity", + "SVGPaintOrder", + "ScrollbarGutter", + "ScrollSnapAlign", + "ScrollSnapAxis", + "ScrollSnapStop", + "ScrollSnapStrictness", + "ScrollSnapType", + "TextAlign", + "TextAlignLast", + "TextDecorationLine", + "TextEmphasisPosition", + "TextJustify", + "TextTransform", + "TextUnderlinePosition", + "TouchAction", + "TransformStyle", + "UserSelect", + "WordBreak", + "XSpan", + "XTextScale", + "ZIndex", + "Zoom", + } + if self.name == "overflow-y": + return True + return bool(self.keyword) + + def animated_type(self): + assert self.animatable + computed = "<{} as ToComputedValue>::ComputedValue".format(self.base_type()) + if self.is_animatable_with_computed_value: + return computed + return "<{} as ToAnimatedValue>::AnimatedValue".format(computed) + + +class Shorthand(Property): + def __init__( + self, + name, + sub_properties, + spec=None, + servo_2013_pref=None, + servo_2020_pref=None, + gecko_pref=None, + enabled_in="content", + rule_types_allowed=DEFAULT_RULES, + aliases=None, + extra_prefixes=None, + flags=None, + ): + Property.__init__( + self, + name=name, + spec=spec, + servo_2013_pref=servo_2013_pref, + servo_2020_pref=servo_2020_pref, + gecko_pref=gecko_pref, + enabled_in=enabled_in, + rule_types_allowed=rule_types_allowed, + aliases=aliases, + extra_prefixes=extra_prefixes, + flags=flags, + ) + self.sub_properties = sub_properties + + def get_animatable(self): + for sub in self.sub_properties: + if sub.animatable: + return True + return False + + animatable = property(get_animatable) + + @staticmethod + def type(): + return "shorthand" + + +class Alias(object): + def __init__(self, name, original, gecko_pref): + self.name = name + self.ident = to_rust_ident(name) + self.camel_case = to_camel_case(self.ident) + self.original = original + self.enabled_in = original.enabled_in + self.animatable = original.animatable + self.servo_2013_pref = original.servo_2013_pref + self.servo_2020_pref = original.servo_2020_pref + self.gecko_pref = gecko_pref + self.rule_types_allowed = original.rule_types_allowed + self.flags = original.flags + + @staticmethod + def type(): + return "alias" + + def rule_types_allowed_names(self): + for name in RULE_VALUES: + if self.rule_types_allowed & RULE_VALUES[name] != 0: + yield name + + def experimental(self, engine): + if engine == "gecko": + return bool(self.gecko_pref) + elif engine == "servo-2013": + return bool(self.servo_2013_pref) + elif engine == "servo-2020": + return bool(self.servo_2020_pref) + else: + raise Exception("Bad engine: " + engine) + + def explicitly_enabled_in_ua_sheets(self): + return self.enabled_in in ["ua", "chrome"] + + def explicitly_enabled_in_chrome(self): + return self.enabled_in == "chrome" + + def enabled_in_content(self): + return self.enabled_in == "content" + + def nscsspropertyid(self): + return "nsCSSPropertyID::eCSSPropertyAlias_%s" % self.ident + + +class Method(object): + def __init__(self, name, return_type=None, arg_types=None, is_mut=False): + self.name = name + self.return_type = return_type + self.arg_types = arg_types or [] + self.is_mut = is_mut + + def arg_list(self): + args = ["_: " + x for x in self.arg_types] + args = ["&mut self" if self.is_mut else "&self"] + args + return ", ".join(args) + + def signature(self): + sig = "fn %s(%s)" % (self.name, self.arg_list()) + if self.return_type: + sig = sig + " -> " + self.return_type + return sig + + def declare(self): + return self.signature() + ";" + + def stub(self): + return self.signature() + "{ unimplemented!() }" + + +class StyleStruct(object): + def __init__(self, name, inherited, gecko_name=None, additional_methods=None): + self.gecko_struct_name = "Gecko" + name + self.name = name + self.name_lower = to_snake_case(name) + self.ident = to_rust_ident(self.name_lower) + self.longhands = [] + self.inherited = inherited + self.gecko_name = gecko_name or name + self.gecko_ffi_name = "nsStyle" + self.gecko_name + self.additional_methods = additional_methods or [] + self.document_dependent = self.gecko_name in ["Font", "Visibility", "Text"] + + +class PropertiesData(object): + def __init__(self, engine): + self.engine = engine + self.style_structs = [] + self.current_style_struct = None + self.longhands = [] + self.longhands_by_name = {} + self.longhands_by_logical_group = {} + self.longhand_aliases = [] + self.shorthands = [] + self.shorthands_by_name = {} + self.shorthand_aliases = [] + self.counted_unknown_properties = [ + CountedUnknownProperty(p) for p in COUNTED_UNKNOWN_PROPERTIES + ] + + def new_style_struct(self, *args, **kwargs): + style_struct = StyleStruct(*args, **kwargs) + self.style_structs.append(style_struct) + self.current_style_struct = style_struct + + def active_style_structs(self): + return [s for s in self.style_structs if s.additional_methods or s.longhands] + + def add_prefixed_aliases(self, property): + # FIXME Servo's DOM architecture doesn't support vendor-prefixed properties. + # See servo/servo#14941. + if self.engine == "gecko": + for prefix, pref in property.extra_prefixes: + property.aliases.append(("-%s-%s" % (prefix, property.name), pref)) + + def declare_longhand(self, name, engines=None, **kwargs): + engines = engines.split() + if self.engine not in engines: + return + + longhand = Longhand(self.current_style_struct, name, **kwargs) + self.add_prefixed_aliases(longhand) + longhand.aliases = [Alias(xp[0], longhand, xp[1]) for xp in longhand.aliases] + self.longhand_aliases += longhand.aliases + self.current_style_struct.longhands.append(longhand) + self.longhands.append(longhand) + self.longhands_by_name[name] = longhand + if longhand.logical_group: + self.longhands_by_logical_group.setdefault( + longhand.logical_group, [] + ).append(longhand) + + return longhand + + def declare_shorthand(self, name, sub_properties, engines, *args, **kwargs): + engines = engines.split() + if self.engine not in engines: + return + + sub_properties = [self.longhands_by_name[s] for s in sub_properties] + shorthand = Shorthand(name, sub_properties, *args, **kwargs) + self.add_prefixed_aliases(shorthand) + shorthand.aliases = [Alias(xp[0], shorthand, xp[1]) for xp in shorthand.aliases] + self.shorthand_aliases += shorthand.aliases + self.shorthands.append(shorthand) + self.shorthands_by_name[name] = shorthand + return shorthand + + def shorthands_except_all(self): + return [s for s in self.shorthands if s.name != "all"] + + def all_aliases(self): + return self.longhand_aliases + self.shorthand_aliases + + +def _add_logical_props(data, props): + groups = set() + for prop in props: + if prop not in data.longhands_by_name: + assert data.engine in ["servo-2013", "servo-2020"] + continue + prop = data.longhands_by_name[prop] + if prop.logical_group: + groups.add(prop.logical_group) + for group in groups: + for prop in data.longhands_by_logical_group[group]: + props.add(prop.name) + + +# These are probably Gecko bugs and should be supported per spec. +def _remove_common_first_line_and_first_letter_properties(props, engine): + if engine == "gecko": + props.remove("tab-size") + props.remove("hyphens") + props.remove("line-break") + props.remove("text-align-last") + props.remove("text-emphasis-position") + props.remove("text-emphasis-style") + props.remove("text-emphasis-color") + + props.remove("overflow-wrap") + props.remove("text-align") + props.remove("text-justify") + props.remove("white-space-collapse") + props.remove("text-wrap-mode") + props.remove("text-wrap-style") + props.remove("word-break") + props.remove("text-indent") + + +class PropertyRestrictions: + @staticmethod + def logical_group(data, group): + return [p.name for p in data.longhands_by_logical_group[group]] + + @staticmethod + def shorthand(data, shorthand): + if shorthand not in data.shorthands_by_name: + return [] + return [p.name for p in data.shorthands_by_name[shorthand].sub_properties] + + @staticmethod + def spec(data, spec_path): + return [p.name for p in data.longhands if spec_path in p.spec] + + # https://svgwg.org/svg2-draft/propidx.html + @staticmethod + def svg_text_properties(): + props = set( + [ + "fill", + "fill-opacity", + "fill-rule", + "paint-order", + "stroke", + "stroke-dasharray", + "stroke-dashoffset", + "stroke-linecap", + "stroke-linejoin", + "stroke-miterlimit", + "stroke-opacity", + "stroke-width", + "text-rendering", + "vector-effect", + ] + ) + return props + + @staticmethod + def webkit_text_properties(): + props = set( + [ + # Kinda like css-text? + "-webkit-text-stroke-width", + "-webkit-text-fill-color", + "-webkit-text-stroke-color", + ] + ) + return props + + # https://drafts.csswg.org/css-pseudo/#first-letter-styling + @staticmethod + def first_letter(data): + props = set( + [ + "color", + "opacity", + "float", + "initial-letter", + # Kinda like css-fonts? + "-moz-osx-font-smoothing", + "vertical-align", + # Will become shorthand of vertical-align (Bug 1830771) + "baseline-source", + "line-height", + # Kinda like css-backgrounds? + "background-blend-mode", + ] + + PropertyRestrictions.shorthand(data, "padding") + + PropertyRestrictions.shorthand(data, "margin") + + PropertyRestrictions.spec(data, "css-fonts") + + PropertyRestrictions.spec(data, "css-backgrounds") + + PropertyRestrictions.spec(data, "css-text") + + PropertyRestrictions.spec(data, "css-shapes") + + PropertyRestrictions.spec(data, "css-text-decor") + ) + props = props.union(PropertyRestrictions.svg_text_properties()) + props = props.union(PropertyRestrictions.webkit_text_properties()) + + _add_logical_props(data, props) + + _remove_common_first_line_and_first_letter_properties(props, data.engine) + return props + + # https://drafts.csswg.org/css-pseudo/#first-line-styling + @staticmethod + def first_line(data): + props = set( + [ + # Per spec. + "color", + "opacity", + # Kinda like css-fonts? + "-moz-osx-font-smoothing", + "vertical-align", + # Will become shorthand of vertical-align (Bug 1830771) + "baseline-source", + "line-height", + # Kinda like css-backgrounds? + "background-blend-mode", + ] + + PropertyRestrictions.spec(data, "css-fonts") + + PropertyRestrictions.spec(data, "css-backgrounds") + + PropertyRestrictions.spec(data, "css-text") + + PropertyRestrictions.spec(data, "css-text-decor") + ) + props = props.union(PropertyRestrictions.svg_text_properties()) + props = props.union(PropertyRestrictions.webkit_text_properties()) + + # These are probably Gecko bugs and should be supported per spec. + for prop in PropertyRestrictions.shorthand(data, "border"): + props.remove(prop) + for prop in PropertyRestrictions.shorthand(data, "border-radius"): + props.remove(prop) + props.remove("box-shadow") + + _remove_common_first_line_and_first_letter_properties(props, data.engine) + return props + + # https://drafts.csswg.org/css-pseudo/#placeholder + # + # The spec says that placeholder and first-line have the same restrictions, + # but that's not true in Gecko and we also allow a handful other properties + # for ::placeholder. + @staticmethod + def placeholder(data): + props = PropertyRestrictions.first_line(data) + props.add("opacity") + props.add("text-overflow") + props.add("text-align") + props.add("text-justify") + for p in PropertyRestrictions.shorthand(data, "text-wrap"): + props.add(p) + for p in PropertyRestrictions.shorthand(data, "white-space"): + props.add(p) + # ::placeholder can't be SVG text + props -= PropertyRestrictions.svg_text_properties() + + return props + + # https://drafts.csswg.org/css-pseudo/#marker-pseudo + @staticmethod + def marker(data): + return set( + [ + "color", + "text-combine-upright", + "text-transform", + "unicode-bidi", + "direction", + "content", + "line-height", + "-moz-osx-font-smoothing", + ] + + PropertyRestrictions.shorthand(data, "text-wrap") + + PropertyRestrictions.shorthand(data, "white-space") + + PropertyRestrictions.spec(data, "css-fonts") + + PropertyRestrictions.spec(data, "css-animations") + + PropertyRestrictions.spec(data, "css-transitions") + ) + + # https://www.w3.org/TR/webvtt1/#the-cue-pseudo-element + @staticmethod + def cue(data): + return set( + [ + "color", + "opacity", + "visibility", + "text-shadow", + "text-combine-upright", + "ruby-position", + # XXX Should these really apply to cue? + "-moz-osx-font-smoothing", + # FIXME(emilio): background-blend-mode should be part of the + # background shorthand, and get reset, per + # https://drafts.fxtf.org/compositing/#background-blend-mode + "background-blend-mode", + ] + + PropertyRestrictions.shorthand(data, "text-decoration") + + PropertyRestrictions.shorthand(data, "text-wrap") + + PropertyRestrictions.shorthand(data, "white-space") + + PropertyRestrictions.shorthand(data, "background") + + PropertyRestrictions.shorthand(data, "outline") + + PropertyRestrictions.shorthand(data, "font") + + PropertyRestrictions.shorthand(data, "font-synthesis") + ) + + +class CountedUnknownProperty: + def __init__(self, name): + self.name = name + self.ident = to_rust_ident(name) + self.camel_case = to_camel_case(self.ident) diff --git a/servo/components/style/properties/declaration_block.rs b/servo/components/style/properties/declaration_block.rs new file mode 100644 index 0000000000..81d7148e62 --- /dev/null +++ b/servo/components/style/properties/declaration_block.rs @@ -0,0 +1,1642 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A property declaration block. + +#![deny(missing_docs)] + +use super::{ + property_counts, AllShorthand, ComputedValues, LogicalGroupSet, LonghandIdSet, + LonghandIdSetIterator, NonCustomPropertyIdSet, PropertyDeclaration, PropertyDeclarationId, + PropertyId, ShorthandId, SourcePropertyDeclaration, SourcePropertyDeclarationDrain, + SubpropertiesVec, +}; +use crate::context::QuirksMode; +use crate::custom_properties; +use crate::error_reporting::{ContextualParseError, ParseErrorReporter}; +use crate::parser::ParserContext; +use crate::properties::{ + animated_properties::{AnimationValue, AnimationValueMap}, + StyleBuilder, +}; +use crate::rule_cache::RuleCacheConditions; +use crate::selector_map::PrecomputedHashSet; +use crate::selector_parser::SelectorImpl; +use crate::shared_lock::Locked; +use crate::str::{CssString, CssStringWriter}; +use crate::stylesheets::container_rule::ContainerSizeQuery; +use crate::stylesheets::{CssRuleType, Origin, UrlExtraData}; +use crate::stylist::Stylist; +use crate::values::computed::Context; +use cssparser::{ + parse_important, AtRuleParser, CowRcStr, DeclarationParser, Delimiter, ParseErrorKind, Parser, + ParserInput, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, +}; +use itertools::Itertools; +use selectors::SelectorList; +use servo_arc::Arc; +use smallbitvec::{self, SmallBitVec}; +use smallvec::SmallVec; +use std::fmt::{self, Write}; +use std::iter::{DoubleEndedIterator, Zip}; +use std::slice::Iter; +use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss}; +use thin_vec::ThinVec; + +/// A set of property declarations including animations and transitions. +#[derive(Default)] +pub struct AnimationDeclarations { + /// Declarations for animations. + pub animations: Option<Arc<Locked<PropertyDeclarationBlock>>>, + /// Declarations for transitions. + pub transitions: Option<Arc<Locked<PropertyDeclarationBlock>>>, +} + +impl AnimationDeclarations { + /// Whether or not this `AnimationDeclarations` is empty. + pub fn is_empty(&self) -> bool { + self.animations.is_none() && self.transitions.is_none() + } +} + +/// An enum describes how a declaration should update +/// the declaration block. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum DeclarationUpdate { + /// The given declaration doesn't update anything. + None, + /// The given declaration is new, and should be append directly. + Append, + /// The given declaration can be updated in-place at the given position. + UpdateInPlace { pos: usize }, + /// The given declaration cannot be updated in-place, and an existing + /// one needs to be removed at the given position. + AppendAndRemove { pos: usize }, +} + +/// A struct describes how a declaration block should be updated by +/// a `SourcePropertyDeclaration`. +#[derive(Default)] +pub struct SourcePropertyDeclarationUpdate { + updates: SubpropertiesVec<DeclarationUpdate>, + new_count: usize, + any_removal: bool, +} + +/// A declaration [importance][importance]. +/// +/// [importance]: https://drafts.csswg.org/css-cascade/#importance +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +pub enum Importance { + /// Indicates a declaration without `!important`. + Normal, + + /// Indicates a declaration with `!important`. + Important, +} + +impl Default for Importance { + fn default() -> Self { + Self::Normal + } +} + +impl Importance { + /// Return whether this is an important declaration. + pub fn important(self) -> bool { + match self { + Self::Normal => false, + Self::Important => true, + } + } +} + +/// A set of properties. +#[derive(Clone, Debug, ToShmem, Default, MallocSizeOf)] +pub struct PropertyDeclarationIdSet { + longhands: LonghandIdSet, + custom: PrecomputedHashSet<custom_properties::Name>, +} + +impl PropertyDeclarationIdSet { + /// Add the given property to the set. + pub fn insert(&mut self, id: PropertyDeclarationId) -> bool { + match id { + PropertyDeclarationId::Longhand(id) => { + if self.longhands.contains(id) { + return false; + } + self.longhands.insert(id); + return true; + }, + PropertyDeclarationId::Custom(name) => self.custom.insert((*name).clone()), + } + } + + /// Return whether the given property is in the set. + pub fn contains(&self, id: PropertyDeclarationId) -> bool { + match id { + PropertyDeclarationId::Longhand(id) => self.longhands.contains(id), + PropertyDeclarationId::Custom(name) => self.custom.contains(name), + } + } + + /// Remove the given property from the set. + pub fn remove(&mut self, id: PropertyDeclarationId) { + match id { + PropertyDeclarationId::Longhand(id) => self.longhands.remove(id), + PropertyDeclarationId::Custom(name) => { + self.custom.remove(name); + }, + } + } + + /// Remove all properties from the set. + pub fn clear(&mut self) { + self.longhands.clear(); + self.custom.clear(); + } + + /// Returns whether the set is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.longhands.is_empty() && self.custom.is_empty() + } + /// Returns whether this set contains any reset longhand. + #[inline] + pub fn contains_any_reset(&self) -> bool { + self.longhands.contains_any_reset() + } + + /// Returns whether this set contains all longhands in the specified set. + #[inline] + pub fn contains_all_longhands(&self, longhands: &LonghandIdSet) -> bool { + self.longhands.contains_all(longhands) + } + + /// Returns whether this set contains all properties in the specified set. + #[inline] + pub fn contains_all(&self, properties: &PropertyDeclarationIdSet) -> bool { + if !self.longhands.contains_all(&properties.longhands) { + return false; + } + if properties.custom.len() > self.custom.len() { + return false; + } + properties + .custom + .iter() + .all(|item| self.custom.contains(item)) + } + + /// Iterate over the current property declaration id set. + pub fn iter(&self) -> PropertyDeclarationIdSetIterator { + PropertyDeclarationIdSetIterator { + longhands: self.longhands.iter(), + custom: self.custom.iter(), + } + } +} + +/// An iterator over a set of longhand ids. +pub struct PropertyDeclarationIdSetIterator<'a> { + longhands: LonghandIdSetIterator<'a>, + custom: std::collections::hash_set::Iter<'a, custom_properties::Name>, +} + +impl<'a> Iterator for PropertyDeclarationIdSetIterator<'a> { + type Item = PropertyDeclarationId<'a>; + + fn next(&mut self) -> Option<Self::Item> { + // LonghandIdSetIterator's implementation always returns None + // after it did it once, so the code below will then continue + // to iterate over the custom properties. + match self.longhands.next() { + Some(id) => Some(PropertyDeclarationId::Longhand(id)), + None => match self.custom.next() { + Some(a) => Some(PropertyDeclarationId::Custom(a)), + None => None, + }, + } + } +} + +/// Overridden declarations are skipped. +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, ToShmem, Default)] +pub struct PropertyDeclarationBlock { + /// The group of declarations, along with their importance. + /// + /// Only deduplicated declarations appear here. + declarations: ThinVec<PropertyDeclaration>, + + /// The "important" flag for each declaration in `declarations`. + declarations_importance: SmallBitVec, + + /// The set of properties that are present in the block. + property_ids: PropertyDeclarationIdSet, +} + +/// Iterator over `(PropertyDeclaration, Importance)` pairs. +pub struct DeclarationImportanceIterator<'a> { + iter: Zip<Iter<'a, PropertyDeclaration>, smallbitvec::Iter<'a>>, +} + +impl<'a> Default for DeclarationImportanceIterator<'a> { + fn default() -> Self { + Self { + iter: [].iter().zip(smallbitvec::Iter::default()), + } + } +} + +impl<'a> DeclarationImportanceIterator<'a> { + /// Constructor. + fn new(declarations: &'a [PropertyDeclaration], important: &'a SmallBitVec) -> Self { + DeclarationImportanceIterator { + iter: declarations.iter().zip(important.iter()), + } + } +} + +impl<'a> Iterator for DeclarationImportanceIterator<'a> { + type Item = (&'a PropertyDeclaration, Importance); + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + self.iter.next().map(|(decl, important)| { + ( + decl, + if important { + Importance::Important + } else { + Importance::Normal + }, + ) + }) + } + + #[inline] + fn size_hint(&self) -> (usize, Option<usize>) { + self.iter.size_hint() + } +} + +impl<'a> DoubleEndedIterator for DeclarationImportanceIterator<'a> { + #[inline(always)] + fn next_back(&mut self) -> Option<Self::Item> { + self.iter.next_back().map(|(decl, important)| { + ( + decl, + if important { + Importance::Important + } else { + Importance::Normal + }, + ) + }) + } +} + +/// Iterator for AnimationValue to be generated from PropertyDeclarationBlock. +pub struct AnimationValueIterator<'a, 'cx, 'cx_a: 'cx> { + iter: DeclarationImportanceIterator<'a>, + context: &'cx mut Context<'cx_a>, + default_values: &'a ComputedValues, +} + +impl<'a, 'cx, 'cx_a: 'cx> AnimationValueIterator<'a, 'cx, 'cx_a> { + fn new( + declarations: &'a PropertyDeclarationBlock, + context: &'cx mut Context<'cx_a>, + default_values: &'a ComputedValues, + ) -> AnimationValueIterator<'a, 'cx, 'cx_a> { + AnimationValueIterator { + iter: declarations.declaration_importance_iter(), + context, + default_values, + } + } +} + +impl<'a, 'cx, 'cx_a: 'cx> Iterator for AnimationValueIterator<'a, 'cx, 'cx_a> { + type Item = AnimationValue; + #[inline] + fn next(&mut self) -> Option<Self::Item> { + loop { + let (decl, importance) = self.iter.next()?; + + if importance.important() { + continue; + } + + let animation = + AnimationValue::from_declaration(decl, &mut self.context, self.default_values); + + if let Some(anim) = animation { + return Some(anim); + } + } + } +} + +impl fmt::Debug for PropertyDeclarationBlock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.declarations.fmt(f) + } +} + +impl PropertyDeclarationBlock { + /// Returns the number of declarations in the block. + #[inline] + pub fn len(&self) -> usize { + self.declarations.len() + } + + /// Returns whether the block is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.declarations.is_empty() + } + + /// Create an empty block + #[inline] + pub fn new() -> Self { + PropertyDeclarationBlock { + declarations: ThinVec::new(), + declarations_importance: SmallBitVec::new(), + property_ids: PropertyDeclarationIdSet::default(), + } + } + + /// Create a block with a single declaration + pub fn with_one(declaration: PropertyDeclaration, importance: Importance) -> Self { + let mut property_ids = PropertyDeclarationIdSet::default(); + property_ids.insert(declaration.id()); + let mut declarations = ThinVec::with_capacity(1); + declarations.push(declaration); + PropertyDeclarationBlock { + declarations, + declarations_importance: SmallBitVec::from_elem(1, importance.important()), + property_ids, + } + } + + /// The declarations in this block + #[inline] + pub fn declarations(&self) -> &[PropertyDeclaration] { + &self.declarations + } + + /// The `important` flags for declarations in this block + #[inline] + pub fn declarations_importance(&self) -> &SmallBitVec { + &self.declarations_importance + } + + /// Iterate over `(PropertyDeclaration, Importance)` pairs + #[inline] + pub fn declaration_importance_iter(&self) -> DeclarationImportanceIterator { + DeclarationImportanceIterator::new(&self.declarations, &self.declarations_importance) + } + + /// Iterate over `PropertyDeclaration` for Importance::Normal + #[inline] + pub fn normal_declaration_iter<'a>( + &'a self, + ) -> impl DoubleEndedIterator<Item = &'a PropertyDeclaration> { + self.declaration_importance_iter() + .filter(|(_, importance)| !importance.important()) + .map(|(declaration, _)| declaration) + } + + /// Return an iterator of (AnimatableLonghand, AnimationValue). + #[inline] + pub fn to_animation_value_iter<'a, 'cx, 'cx_a: 'cx>( + &'a self, + context: &'cx mut Context<'cx_a>, + default_values: &'a ComputedValues, + ) -> AnimationValueIterator<'a, 'cx, 'cx_a> { + AnimationValueIterator::new(self, context, default_values) + } + + /// Returns whether this block contains any declaration with `!important`. + /// + /// This is based on the `declarations_importance` bit-vector, + /// which should be maintained whenever `declarations` is changed. + #[inline] + pub fn any_important(&self) -> bool { + !self.declarations_importance.all_false() + } + + /// Returns whether this block contains any declaration without `!important`. + /// + /// This is based on the `declarations_importance` bit-vector, + /// which should be maintained whenever `declarations` is changed. + #[inline] + pub fn any_normal(&self) -> bool { + !self.declarations_importance.all_true() + } + + /// Returns a `PropertyDeclarationIdSet` representing the properties that are changed in + /// this block. + #[inline] + pub fn property_ids(&self) -> &PropertyDeclarationIdSet { + &self.property_ids + } + + /// Returns whether this block contains a declaration of a given property id. + #[inline] + pub fn contains(&self, id: PropertyDeclarationId) -> bool { + self.property_ids.contains(id) + } + + /// Returns whether this block contains any reset longhand. + #[inline] + pub fn contains_any_reset(&self) -> bool { + self.property_ids.contains_any_reset() + } + + /// Get a declaration for a given property. + /// + /// NOTE: This is linear time in the case of custom properties or in the + /// case the longhand is actually in the declaration block. + #[inline] + pub fn get( + &self, + property: PropertyDeclarationId, + ) -> Option<(&PropertyDeclaration, Importance)> { + if !self.contains(property) { + return None; + } + self.declaration_importance_iter() + .find(|(declaration, _)| declaration.id() == property) + } + + /// Tries to serialize a given shorthand from the declarations in this + /// block. + pub fn shorthand_to_css( + &self, + shorthand: ShorthandId, + dest: &mut CssStringWriter, + ) -> fmt::Result { + // Step 1.2.1 of + // https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue + let mut list = SmallVec::<[&_; 10]>::new(); + let mut important_count = 0; + + // Step 1.2.2 + for longhand in shorthand.longhands() { + // Step 1.2.2.1 + let declaration = self.get(PropertyDeclarationId::Longhand(longhand)); + + // Step 1.2.2.2 & 1.2.2.3 + match declaration { + Some((declaration, importance)) => { + list.push(declaration); + if importance.important() { + important_count += 1; + } + }, + None => return Ok(()), + } + } + + // If there is one or more longhand with important, and one or more + // without important, we don't serialize it as a shorthand. + if important_count > 0 && important_count != list.len() { + return Ok(()); + } + + // Step 1.2.3 + // We don't print !important when serializing individual properties, + // so we treat this as a normal-importance property + match shorthand.get_shorthand_appendable_value(&list) { + Some(appendable_value) => append_declaration_value(dest, appendable_value), + None => return Ok(()), + } + } + + /// Find the value of the given property in this block and serialize it + /// + /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertyvalue> + pub fn property_value_to_css( + &self, + property: &PropertyId, + dest: &mut CssStringWriter, + ) -> fmt::Result { + // Step 1.1: done when parsing a string to PropertyId + + // Step 1.2 + let longhand_or_custom = match property.as_shorthand() { + Ok(shorthand) => return self.shorthand_to_css(shorthand, dest), + Err(longhand_or_custom) => longhand_or_custom, + }; + + if let Some((value, _importance)) = self.get(longhand_or_custom) { + // Step 2 + value.to_css(dest) + } else { + // Step 3 + Ok(()) + } + } + + /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-getpropertypriority> + pub fn property_priority(&self, property: &PropertyId) -> Importance { + // Step 1: done when parsing a string to PropertyId + + // Step 2 + match property.as_shorthand() { + Ok(shorthand) => { + // Step 2.1 & 2.2 & 2.3 + if shorthand.longhands().all(|l| { + self.get(PropertyDeclarationId::Longhand(l)) + .map_or(false, |(_, importance)| importance.important()) + }) { + Importance::Important + } else { + Importance::Normal + } + }, + Err(longhand_or_custom) => { + // Step 3 + self.get(longhand_or_custom) + .map_or(Importance::Normal, |(_, importance)| importance) + }, + } + } + + /// Adds or overrides the declaration for a given property in this block. + /// + /// See the documentation of `push` to see what impact `source` has when the + /// property is already there. + pub fn extend( + &mut self, + mut drain: SourcePropertyDeclarationDrain, + importance: Importance, + ) -> bool { + let all_shorthand_len = match drain.all_shorthand { + AllShorthand::NotSet => 0, + AllShorthand::CSSWideKeyword(_) | AllShorthand::WithVariables(_) => { + property_counts::ALL_SHORTHAND_EXPANDED + }, + }; + let push_calls_count = drain.declarations.len() + all_shorthand_len; + + // With deduplication the actual length increase may be less than this. + self.declarations.reserve(push_calls_count); + + let mut changed = false; + for decl in &mut drain.declarations { + changed |= self.push(decl, importance); + } + drain + .all_shorthand + .declarations() + .fold(changed, |changed, decl| { + changed | self.push(decl, importance) + }) + } + + /// Adds or overrides the declaration for a given property in this block. + /// + /// Returns whether the declaration has changed. + /// + /// This is only used for parsing and internal use. + pub fn push(&mut self, declaration: PropertyDeclaration, importance: Importance) -> bool { + let id = declaration.id(); + if !self.property_ids.insert(id) { + let mut index_to_remove = None; + for (i, slot) in self.declarations.iter_mut().enumerate() { + if slot.id() != id { + continue; + } + + let important = self.declarations_importance[i]; + + // For declarations from parsing, non-important declarations + // shouldn't override existing important one. + if important && !importance.important() { + return false; + } + + index_to_remove = Some(i); + break; + } + + if let Some(index) = index_to_remove { + self.declarations.remove(index); + self.declarations_importance.remove(index); + self.declarations.push(declaration); + self.declarations_importance.push(importance.important()); + return true; + } + } + + self.declarations.push(declaration); + self.declarations_importance.push(importance.important()); + true + } + + /// Prepares updating this declaration block with the given + /// `SourcePropertyDeclaration` and importance, and returns whether + /// there is something to update. + pub fn prepare_for_update( + &self, + source_declarations: &SourcePropertyDeclaration, + importance: Importance, + updates: &mut SourcePropertyDeclarationUpdate, + ) -> bool { + debug_assert!(updates.updates.is_empty()); + // Check whether we are updating for an all shorthand change. + if !matches!(source_declarations.all_shorthand, AllShorthand::NotSet) { + debug_assert!(source_declarations.declarations.is_empty()); + return source_declarations + .all_shorthand + .declarations() + .any(|decl| { + !self.contains(decl.id()) || + self.declarations + .iter() + .enumerate() + .find(|&(_, ref d)| d.id() == decl.id()) + .map_or(true, |(i, d)| { + let important = self.declarations_importance[i]; + *d != decl || important != importance.important() + }) + }); + } + // Fill `updates` with update information. + let mut any_update = false; + let new_count = &mut updates.new_count; + let any_removal = &mut updates.any_removal; + let updates = &mut updates.updates; + updates.extend( + source_declarations + .declarations + .iter() + .map(|declaration| { + if !self.contains(declaration.id()) { + return DeclarationUpdate::Append; + } + let longhand_id = declaration.id().as_longhand(); + if let Some(longhand_id) = longhand_id { + if let Some(logical_group) = longhand_id.logical_group() { + let mut needs_append = false; + for (pos, decl) in self.declarations.iter().enumerate().rev() { + let id = match decl.id().as_longhand() { + Some(id) => id, + None => continue, + }; + if id == longhand_id { + if needs_append { + return DeclarationUpdate::AppendAndRemove { pos }; + } + let important = self.declarations_importance[pos]; + if decl == declaration && important == importance.important() { + return DeclarationUpdate::None; + } + return DeclarationUpdate::UpdateInPlace { pos }; + } + if !needs_append && + id.logical_group() == Some(logical_group) && + id.is_logical() != longhand_id.is_logical() + { + needs_append = true; + } + } + unreachable!("Longhand should be found in loop above"); + } + } + self.declarations + .iter() + .enumerate() + .find(|&(_, ref decl)| decl.id() == declaration.id()) + .map_or(DeclarationUpdate::Append, |(pos, decl)| { + let important = self.declarations_importance[pos]; + if decl == declaration && important == importance.important() { + DeclarationUpdate::None + } else { + DeclarationUpdate::UpdateInPlace { pos } + } + }) + }) + .inspect(|update| { + if matches!(update, DeclarationUpdate::None) { + return; + } + any_update = true; + match update { + DeclarationUpdate::Append => { + *new_count += 1; + }, + DeclarationUpdate::AppendAndRemove { .. } => { + *any_removal = true; + }, + _ => {}, + } + }), + ); + any_update + } + + /// Update this declaration block with the given data. + pub fn update( + &mut self, + drain: SourcePropertyDeclarationDrain, + importance: Importance, + updates: &mut SourcePropertyDeclarationUpdate, + ) { + let important = importance.important(); + if !matches!(drain.all_shorthand, AllShorthand::NotSet) { + debug_assert!(updates.updates.is_empty()); + for decl in drain.all_shorthand.declarations() { + let id = decl.id(); + if self.property_ids.insert(id) { + self.declarations.push(decl); + self.declarations_importance.push(important); + } else { + let (idx, slot) = self + .declarations + .iter_mut() + .enumerate() + .find(|&(_, ref d)| d.id() == decl.id()) + .unwrap(); + *slot = decl; + self.declarations_importance.set(idx, important); + } + } + return; + } + + self.declarations.reserve(updates.new_count); + if updates.any_removal { + // Prepare for removal and fixing update positions. + struct UpdateOrRemoval<'a> { + item: &'a mut DeclarationUpdate, + pos: usize, + remove: bool, + } + let mut updates_and_removals: SubpropertiesVec<UpdateOrRemoval> = updates + .updates + .iter_mut() + .filter_map(|item| { + let (pos, remove) = match *item { + DeclarationUpdate::UpdateInPlace { pos } => (pos, false), + DeclarationUpdate::AppendAndRemove { pos } => (pos, true), + _ => return None, + }; + Some(UpdateOrRemoval { item, pos, remove }) + }) + .collect(); + // Execute removals. It's important to do it in reverse index order, + // so that removing doesn't invalidate following positions. + updates_and_removals.sort_unstable_by_key(|update| update.pos); + updates_and_removals + .iter() + .rev() + .filter(|update| update.remove) + .for_each(|update| { + self.declarations.remove(update.pos); + self.declarations_importance.remove(update.pos); + }); + // Fixup pos field for updates. + let mut removed_count = 0; + for update in updates_and_removals.iter_mut() { + if update.remove { + removed_count += 1; + continue; + } + debug_assert_eq!( + *update.item, + DeclarationUpdate::UpdateInPlace { pos: update.pos } + ); + *update.item = DeclarationUpdate::UpdateInPlace { + pos: update.pos - removed_count, + }; + } + } + // Execute updates and appends. + for (decl, update) in drain.declarations.zip_eq(updates.updates.iter()) { + match *update { + DeclarationUpdate::None => {}, + DeclarationUpdate::Append | DeclarationUpdate::AppendAndRemove { .. } => { + self.property_ids.insert(decl.id()); + self.declarations.push(decl); + self.declarations_importance.push(important); + }, + DeclarationUpdate::UpdateInPlace { pos } => { + self.declarations[pos] = decl; + self.declarations_importance.set(pos, important); + }, + } + } + updates.updates.clear(); + } + + /// Returns the first declaration that would be removed by removing + /// `property`. + #[inline] + pub fn first_declaration_to_remove(&self, property: &PropertyId) -> Option<usize> { + if let Err(longhand_or_custom) = property.as_shorthand() { + if !self.contains(longhand_or_custom) { + return None; + } + } + + self.declarations + .iter() + .position(|declaration| declaration.id().is_or_is_longhand_of(property)) + } + + /// Removes a given declaration at a given index. + #[inline] + fn remove_declaration_at(&mut self, i: usize) { + self.property_ids.remove(self.declarations[i].id()); + self.declarations_importance.remove(i); + self.declarations.remove(i); + } + + /// Clears all the declarations from this block. + #[inline] + pub fn clear(&mut self) { + self.declarations_importance.clear(); + self.declarations.clear(); + self.property_ids.clear(); + } + + /// <https://drafts.csswg.org/cssom/#dom-cssstyledeclaration-removeproperty> + /// + /// `first_declaration` needs to be the result of + /// `first_declaration_to_remove`. + #[inline] + pub fn remove_property(&mut self, property: &PropertyId, first_declaration: usize) { + debug_assert_eq!( + Some(first_declaration), + self.first_declaration_to_remove(property) + ); + debug_assert!(self.declarations[first_declaration] + .id() + .is_or_is_longhand_of(property)); + + self.remove_declaration_at(first_declaration); + + let shorthand = match property.as_shorthand() { + Ok(s) => s, + Err(_longhand_or_custom) => return, + }; + + let mut i = first_declaration; + let mut len = self.len(); + while i < len { + if !self.declarations[i].id().is_longhand_of(shorthand) { + i += 1; + continue; + } + + self.remove_declaration_at(i); + len -= 1; + } + } + + /// Take a declaration block known to contain a single property and serialize it. + pub fn single_value_to_css( + &self, + property: &PropertyId, + dest: &mut CssStringWriter, + computed_values: Option<&ComputedValues>, + stylist: &Stylist, + ) -> fmt::Result { + if let Ok(shorthand) = property.as_shorthand() { + return self.shorthand_to_css(shorthand, dest); + } + + // FIXME(emilio): Should this assert, or assert that the declaration is + // the property we expect? + let declaration = match self.declarations.get(0) { + Some(d) => d, + None => return Err(fmt::Error), + }; + + let mut rule_cache_conditions = RuleCacheConditions::default(); + let mut context = Context::new( + StyleBuilder::new( + stylist.device(), + Some(stylist), + computed_values, + None, + None, + false, + ), + stylist.quirks_mode(), + &mut rule_cache_conditions, + ContainerSizeQuery::none(), + ); + + if let Some(cv) = computed_values { + context.builder.custom_properties = cv.custom_properties.clone(); + }; + + match (declaration, computed_values) { + // If we have a longhand declaration with variables, those variables + // will be stored as unparsed values. + // + // As a temporary measure to produce sensible results in Gecko's + // getKeyframes() implementation for CSS animations, if + // |computed_values| is supplied, we use it to expand such variable + // declarations. This will be fixed properly in Gecko bug 1391537. + (&PropertyDeclaration::WithVariables(ref declaration), Some(_)) => declaration + .value + .substitute_variables( + declaration.id, + &context.builder.custom_properties, + stylist, + &context, + &mut Default::default(), + ) + .to_css(dest), + (ref d, _) => d.to_css(dest), + } + } + + /// Convert AnimationValueMap to PropertyDeclarationBlock. + pub fn from_animation_value_map(animation_value_map: &AnimationValueMap) -> Self { + let len = animation_value_map.len(); + let mut declarations = ThinVec::with_capacity(len); + let mut property_ids = PropertyDeclarationIdSet::default(); + + for (property, animation_value) in animation_value_map.iter() { + property_ids.insert(property.as_borrowed()); + declarations.push(animation_value.uncompute()); + } + + PropertyDeclarationBlock { + declarations, + property_ids, + declarations_importance: SmallBitVec::from_elem(len, false), + } + } + + /// Returns true if the declaration block has a CSSWideKeyword for the given + /// property. + pub fn has_css_wide_keyword(&self, property: &PropertyId) -> bool { + if let Err(longhand_or_custom) = property.as_shorthand() { + if !self.property_ids.contains(longhand_or_custom) { + return false; + } + } + self.declarations.iter().any(|decl| { + decl.id().is_or_is_longhand_of(property) && decl.get_css_wide_keyword().is_some() + }) + } + + /// Like the method on ToCss, but without the type parameter to avoid + /// accidentally monomorphizing this large function multiple times for + /// different writers. + /// + /// https://drafts.csswg.org/cssom/#serialize-a-css-declaration-block + pub fn to_css(&self, dest: &mut CssStringWriter) -> fmt::Result { + let mut is_first_serialization = true; // trailing serializations should have a prepended space + + // Step 1 -> dest = result list + + // Step 2 + // + // NOTE(emilio): We reuse this set for both longhands and shorthands + // with subtly different meaning. For longhands, only longhands that + // have actually been serialized (either by themselves, or as part of a + // shorthand) appear here. For shorthands, all the shorthands that we've + // attempted to serialize appear here. + let mut already_serialized = NonCustomPropertyIdSet::new(); + + // Step 3 + 'declaration_loop: for (declaration, importance) in self.declaration_importance_iter() { + // Step 3.1 + let property = declaration.id(); + let longhand_id = match property { + PropertyDeclarationId::Longhand(id) => id, + PropertyDeclarationId::Custom(..) => { + // Given the invariants that there are no duplicate + // properties in a declaration block, and that custom + // properties can't be part of a shorthand, we can just care + // about them here. + append_serialization( + dest, + &property, + AppendableValue::Declaration(declaration), + importance, + &mut is_first_serialization, + )?; + continue; + }, + }; + + // Step 3.2 + if already_serialized.contains(longhand_id.into()) { + continue; + } + + // Steps 3.3 & 3.4 + for shorthand in longhand_id.shorthands() { + // We already attempted to serialize this shorthand before. + if already_serialized.contains(shorthand.into()) { + continue; + } + already_serialized.insert(shorthand.into()); + + if shorthand.is_legacy_shorthand() { + continue; + } + + // Step 3.3.1: + // Let longhands be an array consisting of all CSS + // declarations in declaration block’s declarations that + // that are not in already serialized and have a property + // name that maps to one of the shorthand properties in + // shorthands. + let longhands = { + // TODO(emilio): This could just index in an array if we + // remove pref-controlled longhands. + let mut ids = LonghandIdSet::new(); + for longhand in shorthand.longhands() { + ids.insert(longhand); + } + ids + }; + + // Step 3.4.2 + // If all properties that map to shorthand are not present + // in longhands, continue with the steps labeled shorthand + // loop. + if !self.property_ids.contains_all_longhands(&longhands) { + continue; + } + + // Step 3.4.3: + // Let current longhands be an empty array. + let mut current_longhands = SmallVec::<[&_; 10]>::new(); + let mut logical_groups = LogicalGroupSet::new(); + let mut saw_one = false; + let mut logical_mismatch = false; + let mut seen = LonghandIdSet::new(); + let mut important_count = 0; + + // Step 3.4.4: + // Append all CSS declarations in longhands that have a + // property name that maps to shorthand to current longhands. + for (declaration, importance) in self.declaration_importance_iter() { + let longhand = match declaration.id() { + PropertyDeclarationId::Longhand(id) => id, + PropertyDeclarationId::Custom(..) => continue, + }; + + if longhands.contains(longhand) { + saw_one = true; + if importance.important() { + important_count += 1; + } + current_longhands.push(declaration); + if shorthand != ShorthandId::All { + // All is special because it contains both physical + // and logical longhands. + if let Some(g) = longhand.logical_group() { + logical_groups.insert(g); + } + seen.insert(longhand); + if seen == longhands { + break; + } + } + } else if saw_one { + if let Some(g) = longhand.logical_group() { + if logical_groups.contains(g) { + logical_mismatch = true; + break; + } + } + } + } + + // 3.4.5: + // If there is one or more CSS declarations in current + // longhands have their important flag set and one or more + // with it unset, continue with the steps labeled shorthand + // loop. + let is_important = important_count > 0; + if is_important && important_count != current_longhands.len() { + continue; + } + + // 3.4.6: + // If there’s any declaration in declaration block in between + // the first and the last longhand in current longhands which + // belongs to the same logical property group, but has a + // different mapping logic as any of the longhands in current + // longhands, and is not in current longhands, continue with + // the steps labeled shorthand loop. + if logical_mismatch { + continue; + } + + let importance = if is_important { + Importance::Important + } else { + Importance::Normal + }; + + // 3.4.7: + // Let value be the result of invoking serialize a CSS value + // of current longhands. + let appendable_value = + match shorthand.get_shorthand_appendable_value(¤t_longhands) { + None => continue, + Some(appendable_value) => appendable_value, + }; + + // We avoid re-serializing if we're already an + // AppendableValue::Css. + let mut v = CssString::new(); + let value = match appendable_value { + AppendableValue::Css(css) => { + debug_assert!(!css.is_empty()); + appendable_value + }, + other => { + append_declaration_value(&mut v, other)?; + + // 3.4.8: + // If value is the empty string, continue with the + // steps labeled shorthand loop. + if v.is_empty() { + continue; + } + + AppendableValue::Css({ + // Safety: serialization only generates valid utf-8. + #[cfg(feature = "gecko")] + unsafe { + v.as_str_unchecked() + } + #[cfg(feature = "servo")] + &v + }) + }, + }; + + // 3.4.9: + // Let serialized declaration be the result of invoking + // serialize a CSS declaration with property name shorthand, + // value value, and the important flag set if the CSS + // declarations in current longhands have their important + // flag set. + // + // 3.4.10: + // Append serialized declaration to list. + append_serialization( + dest, + &shorthand, + value, + importance, + &mut is_first_serialization, + )?; + + // 3.4.11: + // Append the property names of all items of current + // longhands to already serialized. + for current_longhand in ¤t_longhands { + let longhand_id = match current_longhand.id() { + PropertyDeclarationId::Longhand(id) => id, + PropertyDeclarationId::Custom(..) => unreachable!(), + }; + + // Substep 9 + already_serialized.insert(longhand_id.into()); + } + + // 3.4.12: + // Continue with the steps labeled declaration loop. + continue 'declaration_loop; + } + + // Steps 3.5, 3.6 & 3.7: + // Let value be the result of invoking serialize a CSS value of + // declaration. + // + // Let serialized declaration be the result of invoking + // serialize a CSS declaration with property name property, + // value value, and the important flag set if declaration has + // its important flag set. + // + // Append serialized declaration to list. + append_serialization( + dest, + &property, + AppendableValue::Declaration(declaration), + importance, + &mut is_first_serialization, + )?; + + // Step 3.8: + // Append property to already serialized. + already_serialized.insert(longhand_id.into()); + } + + // Step 4 + Ok(()) + } +} + +/// A convenient enum to represent different kinds of stuff that can represent a +/// _value_ in the serialization of a property declaration. +pub enum AppendableValue<'a, 'b: 'a> { + /// A given declaration, of which we'll serialize just the value. + Declaration(&'a PropertyDeclaration), + /// A set of declarations for a given shorthand. + /// + /// FIXME: This needs more docs, where are the shorthands expanded? We print + /// the property name before-hand, don't we? + DeclarationsForShorthand(ShorthandId, &'a [&'b PropertyDeclaration]), + /// A raw CSS string, coming for example from a property with CSS variables, + /// or when storing a serialized shorthand value before appending directly. + Css(&'a str), +} + +/// Potentially appends whitespace after the first (property: value;) pair. +fn handle_first_serialization<W>(dest: &mut W, is_first_serialization: &mut bool) -> fmt::Result +where + W: Write, +{ + if !*is_first_serialization { + dest.write_char(' ') + } else { + *is_first_serialization = false; + Ok(()) + } +} + +/// Append a given kind of appendable value to a serialization. +pub fn append_declaration_value<'a, 'b: 'a>( + dest: &mut CssStringWriter, + appendable_value: AppendableValue<'a, 'b>, +) -> fmt::Result { + match appendable_value { + AppendableValue::Css(css) => dest.write_str(css), + AppendableValue::Declaration(decl) => decl.to_css(dest), + AppendableValue::DeclarationsForShorthand(shorthand, decls) => { + shorthand.longhands_to_css(decls, dest) + }, + } +} + +/// Append a given property and value pair to a serialization. +pub fn append_serialization<'a, 'b: 'a, N>( + dest: &mut CssStringWriter, + property_name: &N, + appendable_value: AppendableValue<'a, 'b>, + importance: Importance, + is_first_serialization: &mut bool, +) -> fmt::Result +where + N: ToCss, +{ + handle_first_serialization(dest, is_first_serialization)?; + + property_name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(": ")?; + + append_declaration_value(dest, appendable_value)?; + + if importance.important() { + dest.write_str(" !important")?; + } + + dest.write_char(';') +} + +/// A helper to parse the style attribute of an element, in order for this to be +/// shared between Servo and Gecko. +/// +/// Inline because we call this cross-crate. +#[inline] +pub fn parse_style_attribute( + input: &str, + url_data: &UrlExtraData, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + rule_type: CssRuleType, +) -> PropertyDeclarationBlock { + let context = ParserContext::new( + Origin::Author, + url_data, + Some(rule_type), + ParsingMode::DEFAULT, + quirks_mode, + /* namespaces = */ Default::default(), + error_reporter, + None, + ); + + let mut input = ParserInput::new(input); + parse_property_declaration_list(&context, &mut Parser::new(&mut input), &[]) +} + +/// Parse a given property declaration. Can result in multiple +/// `PropertyDeclaration`s when expanding a shorthand, for example. +/// +/// This does not attempt to parse !important at all. +#[inline] +pub fn parse_one_declaration_into( + declarations: &mut SourcePropertyDeclaration, + id: PropertyId, + input: &str, + origin: Origin, + url_data: &UrlExtraData, + error_reporter: Option<&dyn ParseErrorReporter>, + parsing_mode: ParsingMode, + quirks_mode: QuirksMode, + rule_type: CssRuleType, +) -> Result<(), ()> { + let context = ParserContext::new( + origin, + url_data, + Some(rule_type), + parsing_mode, + quirks_mode, + /* namespaces = */ Default::default(), + error_reporter, + None, + ); + + let property_id_for_error_reporting = if context.error_reporting_enabled() { + Some(id.clone()) + } else { + None + }; + + let mut input = ParserInput::new(input); + let mut parser = Parser::new(&mut input); + let start_position = parser.position(); + parser + .parse_entirely(|parser| { + PropertyDeclaration::parse_into(declarations, id, &context, parser) + }) + .map_err(|err| { + if context.error_reporting_enabled() { + report_one_css_error( + &context, + None, + &[], + err, + parser.slice_from(start_position), + property_id_for_error_reporting, + ) + } + }) +} + +/// A struct to parse property declarations. +struct PropertyDeclarationParser<'a, 'b: 'a, 'i> { + context: &'a ParserContext<'b>, + state: &'a mut DeclarationParserState<'i>, +} + +/// The state needed to parse a declaration block. +/// +/// It stores declarations in output_block. +#[derive(Default)] +pub struct DeclarationParserState<'i> { + /// The output block where results are stored. + output_block: PropertyDeclarationBlock, + /// Declarations from the last declaration parsed. (note that a shorthand might expand to + /// multiple declarations). + declarations: SourcePropertyDeclaration, + /// The importance from the last declaration parsed. + importance: Importance, + /// A list of errors that have happened so far. Not all of them might be reported. + errors: SmallParseErrorVec<'i>, + /// The last parsed property id, if any. + last_parsed_property_id: Option<PropertyId>, +} + +impl<'i> DeclarationParserState<'i> { + /// Returns whether any parsed declarations have been parsed so far. + pub fn has_parsed_declarations(&self) -> bool { + !self.output_block.is_empty() + } + + /// Takes the parsed declarations. + pub fn take_declarations(&mut self) -> PropertyDeclarationBlock { + std::mem::take(&mut self.output_block) + } + + /// Parse a single declaration value. + pub fn parse_value<'t>( + &mut self, + context: &ParserContext, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let id = match PropertyId::parse(&name, context) { + Ok(id) => id, + Err(..) => { + return Err(input.new_custom_error(StyleParseErrorKind::UnknownProperty(name))); + }, + }; + if context.error_reporting_enabled() { + self.last_parsed_property_id = Some(id.clone()); + } + input.parse_until_before(Delimiter::Bang, |input| { + PropertyDeclaration::parse_into(&mut self.declarations, id, context, input) + })?; + self.importance = match input.try_parse(parse_important) { + Ok(()) => Importance::Important, + Err(_) => Importance::Normal, + }; + // In case there is still unparsed text in the declaration, we should roll back. + input.expect_exhausted()?; + self.output_block + .extend(self.declarations.drain(), self.importance); + // We've successfully parsed a declaration, so forget about + // `last_parsed_property_id`. It'd be wrong to associate any + // following error with this property. + self.last_parsed_property_id = None; + Ok(()) + } + + /// Reports any CSS errors that have ocurred if needed. + #[inline] + pub fn report_errors_if_needed( + &mut self, + context: &ParserContext, + selectors: &[SelectorList<SelectorImpl>], + ) { + if self.errors.is_empty() { + return; + } + self.do_report_css_errors(context, selectors); + } + + #[cold] + fn do_report_css_errors( + &mut self, + context: &ParserContext, + selectors: &[SelectorList<SelectorImpl>], + ) { + for (error, slice, property) in self.errors.drain(..) { + report_one_css_error( + context, + Some(&self.output_block), + selectors, + error, + slice, + property, + ) + } + } + + /// Resets the declaration parser state, and reports the error if needed. + #[inline] + pub fn did_error(&mut self, context: &ParserContext, error: ParseError<'i>, slice: &'i str) { + self.declarations.clear(); + if !context.error_reporting_enabled() { + return; + } + let property = self.last_parsed_property_id.take(); + self.errors.push((error, slice, property)); + } +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for PropertyDeclarationParser<'a, 'b, 'i> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +/// Default methods reject all rules. +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyDeclarationParser<'a, 'b, 'i> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +/// Based on NonMozillaVendorIdentifier from Gecko's CSS parser. +fn is_non_mozilla_vendor_identifier(name: &str) -> bool { + (name.starts_with("-") && !name.starts_with("-moz-")) || name.starts_with("_") +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for PropertyDeclarationParser<'a, 'b, 'i> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + self.state.parse_value(self.context, name, input) + } +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for PropertyDeclarationParser<'a, 'b, 'i> +{ + fn parse_declarations(&self) -> bool { + true + } + // TODO(emilio): Nesting. + fn parse_qualified(&self) -> bool { + false + } +} + +type SmallParseErrorVec<'i> = SmallVec<[(ParseError<'i>, &'i str, Option<PropertyId>); 2]>; + +fn alias_of_known_property(name: &str) -> Option<PropertyId> { + let mut prefixed = String::with_capacity(name.len() + 5); + prefixed.push_str("-moz-"); + prefixed.push_str(name); + PropertyId::parse_enabled_for_all_content(&prefixed).ok() +} + +#[cold] +fn report_one_css_error<'i>( + context: &ParserContext, + block: Option<&PropertyDeclarationBlock>, + selectors: &[SelectorList<SelectorImpl>], + mut error: ParseError<'i>, + slice: &str, + property: Option<PropertyId>, +) { + debug_assert!(context.error_reporting_enabled()); + + fn all_properties_in_block(block: &PropertyDeclarationBlock, property: &PropertyId) -> bool { + match property.as_shorthand() { + Ok(id) => id + .longhands() + .all(|longhand| block.contains(PropertyDeclarationId::Longhand(longhand))), + Err(longhand_or_custom) => block.contains(longhand_or_custom), + } + } + + if let ParseErrorKind::Custom(StyleParseErrorKind::UnknownProperty(ref name)) = error.kind { + if is_non_mozilla_vendor_identifier(name) { + // If the unrecognized property looks like a vendor-specific property, + // silently ignore it instead of polluting the error output. + return; + } + if let Some(alias) = alias_of_known_property(name) { + // This is an unknown property, but its -moz-* version is known. + // We don't want to report error if the -moz-* version is already + // specified. + if let Some(block) = block { + if all_properties_in_block(block, &alias) { + return; + } + } + } + } + + if let Some(ref property) = property { + if let Some(block) = block { + if all_properties_in_block(block, property) { + return; + } + } + error = match *property { + PropertyId::Custom(ref c) => { + StyleParseErrorKind::new_invalid(format!("--{}", c), error) + }, + _ => StyleParseErrorKind::new_invalid(property.non_custom_id().unwrap().name(), error), + }; + } + + let location = error.location; + let error = ContextualParseError::UnsupportedPropertyDeclaration(slice, error, selectors); + context.log_css_error(location, error); +} + +/// Parse a list of property declarations and return a property declaration +/// block. +pub fn parse_property_declaration_list( + context: &ParserContext, + input: &mut Parser, + selectors: &[SelectorList<SelectorImpl>], +) -> PropertyDeclarationBlock { + let mut state = DeclarationParserState::default(); + let mut parser = PropertyDeclarationParser { + context, + state: &mut state, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + match declaration { + Ok(()) => {}, + Err((error, slice)) => iter.parser.state.did_error(context, error, slice), + } + } + parser.state.report_errors_if_needed(context, selectors); + state.output_block +} diff --git a/servo/components/style/properties/gecko.mako.rs b/servo/components/style/properties/gecko.mako.rs new file mode 100644 index 0000000000..f5ae0cade3 --- /dev/null +++ b/servo/components/style/properties/gecko.mako.rs @@ -0,0 +1,1806 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// `data` comes from components/style/properties.mako.rs; see build.rs for more details. + +<%! + from data import to_camel_case, to_camel_case_lower + from data import Keyword +%> +<%namespace name="helpers" file="/helpers.mako.rs" /> + +use crate::Atom; +use app_units::Au; +use crate::computed_value_flags::*; +use crate::custom_properties::ComputedCustomProperties; +use crate::gecko_bindings::bindings; +% for style_struct in data.style_structs: +use crate::gecko_bindings::bindings::Gecko_Construct_Default_${style_struct.gecko_ffi_name}; +use crate::gecko_bindings::bindings::Gecko_CopyConstruct_${style_struct.gecko_ffi_name}; +use crate::gecko_bindings::bindings::Gecko_Destroy_${style_struct.gecko_ffi_name}; +% endfor +use crate::gecko_bindings::bindings::Gecko_CopyCounterStyle; +use crate::gecko_bindings::bindings::Gecko_EnsureImageLayersLength; +use crate::gecko_bindings::bindings::Gecko_nsStyleFont_SetLang; +use crate::gecko_bindings::bindings::Gecko_nsStyleFont_CopyLangFrom; +use crate::gecko_bindings::structs; +use crate::gecko_bindings::structs::mozilla::PseudoStyleType; +use crate::gecko::data::PerDocumentStyleData; +use crate::logical_geometry::WritingMode; +use crate::media_queries::Device; +use crate::properties::longhands; +use crate::rule_tree::StrongRuleNode; +use crate::selector_parser::PseudoElement; +use servo_arc::{Arc, UniqueArc}; +use std::mem::{forget, MaybeUninit, ManuallyDrop}; +use std::{cmp, ops, ptr}; +use crate::values; +use crate::values::computed::{BorderStyle, Percentage, Time, Zoom}; +use crate::values::computed::font::FontSize; +use crate::values::generics::column::ColumnCount; + + +pub mod style_structs { + % for style_struct in data.style_structs: + pub use super::${style_struct.gecko_struct_name} as ${style_struct.name}; + + unsafe impl Send for ${style_struct.name} {} + unsafe impl Sync for ${style_struct.name} {} + % endfor +} + +/// FIXME(emilio): This is completely duplicated with the other properties code. +pub type ComputedValuesInner = structs::ServoComputedData; + +#[repr(C)] +pub struct ComputedValues(structs::mozilla::ComputedStyle); + +impl ComputedValues { + #[inline] + pub (crate) fn as_gecko_computed_style(&self) -> &structs::ComputedStyle { + &self.0 + } + + pub fn new( + pseudo: Option<<&PseudoElement>, + custom_properties: ComputedCustomProperties, + writing_mode: WritingMode, + effective_zoom: Zoom, + flags: ComputedValueFlags, + rules: Option<StrongRuleNode>, + visited_style: Option<Arc<ComputedValues>>, + % for style_struct in data.style_structs: + ${style_struct.ident}: Arc<style_structs::${style_struct.name}>, + % endfor + ) -> Arc<Self> { + ComputedValuesInner::new( + custom_properties, + writing_mode, + effective_zoom, + flags, + rules, + visited_style, + % for style_struct in data.style_structs: + ${style_struct.ident}, + % endfor + ).to_outer(pseudo) + } + + pub fn default_values(doc: &structs::Document) -> Arc<Self> { + ComputedValuesInner::new( + ComputedCustomProperties::default(), + WritingMode::empty(), // FIXME(bz): This seems dubious + Zoom::ONE, + ComputedValueFlags::empty(), + /* rules = */ None, + /* visited_style = */ None, + % for style_struct in data.style_structs: + style_structs::${style_struct.name}::default(doc), + % endfor + ).to_outer(None) + } + + /// Converts the computed values to an Arc<> from a reference. + pub fn to_arc(&self) -> Arc<Self> { + // SAFETY: We're guaranteed to be allocated as an Arc<> since the + // functions above are the only ones that create ComputedValues + // instances in Gecko (and that must be the case since ComputedValues' + // member is private). + unsafe { Arc::from_raw_addrefed(self) } + } + + #[inline] + pub fn is_pseudo_style(&self) -> bool { + self.0.mPseudoType != PseudoStyleType::NotPseudo + } + + #[inline] + pub fn pseudo(&self) -> Option<PseudoElement> { + if !self.is_pseudo_style() { + return None; + } + PseudoElement::from_pseudo_type(self.0.mPseudoType, None) + } + + #[inline] + pub fn is_first_line_style(&self) -> bool { + self.pseudo() == Some(PseudoElement::FirstLine) + } + + /// Returns true if the display property is changed from 'none' to others. + pub fn is_display_property_changed_from_none( + &self, + old_values: Option<<&ComputedValues> + ) -> bool { + use crate::properties::longhands::display::computed_value::T as Display; + + old_values.map_or(false, |old| { + let old_display_style = old.get_box().clone_display(); + let new_display_style = self.get_box().clone_display(); + old_display_style == Display::None && + new_display_style != Display::None + }) + } + +} + +impl Drop for ComputedValues { + fn drop(&mut self) { + // XXX this still relies on the destructor of ComputedValuesInner to run on the rust side, + // that's pretty wild. + unsafe { + bindings::Gecko_ComputedStyle_Destroy(&mut self.0); + } + } +} + +unsafe impl Sync for ComputedValues {} +unsafe impl Send for ComputedValues {} + +impl Clone for ComputedValues { + fn clone(&self) -> Self { + unreachable!() + } +} + +impl Clone for ComputedValuesInner { + fn clone(&self) -> Self { + ComputedValuesInner { + % for style_struct in data.style_structs: + ${style_struct.gecko_name}: Arc::into_raw(unsafe { Arc::from_raw_addrefed(self.${style_struct.name_lower}_ptr()) }) as *const _, + % endfor + custom_properties: self.custom_properties.clone(), + writing_mode: self.writing_mode.clone(), + flags: self.flags.clone(), + effective_zoom: self.effective_zoom, + rules: self.rules.clone(), + visited_style: if self.visited_style.is_null() { + ptr::null() + } else { + Arc::into_raw(unsafe { Arc::from_raw_addrefed(self.visited_style_ptr()) }) as *const _ + }, + } + } +} + + +impl Drop for ComputedValuesInner { + fn drop(&mut self) { + % for style_struct in data.style_structs: + let _ = unsafe { Arc::from_raw(self.${style_struct.name_lower}_ptr()) }; + % endfor + if !self.visited_style.is_null() { + let _ = unsafe { Arc::from_raw(self.visited_style_ptr()) }; + } + } +} + +impl ComputedValuesInner { + pub fn new( + custom_properties: ComputedCustomProperties, + writing_mode: WritingMode, + effective_zoom: Zoom, + flags: ComputedValueFlags, + rules: Option<StrongRuleNode>, + visited_style: Option<Arc<ComputedValues>>, + % for style_struct in data.style_structs: + ${style_struct.ident}: Arc<style_structs::${style_struct.name}>, + % endfor + ) -> Self { + Self { + custom_properties, + writing_mode, + rules, + visited_style: visited_style.map_or(ptr::null(), |p| Arc::into_raw(p)) as *const _, + flags, + effective_zoom, + % for style_struct in data.style_structs: + ${style_struct.gecko_name}: Arc::into_raw(${style_struct.ident}) as *const _, + % endfor + } + } + + fn to_outer(self, pseudo: Option<<&PseudoElement>) -> Arc<ComputedValues> { + let pseudo_ty = match pseudo { + Some(p) => p.pseudo_type(), + None => structs::PseudoStyleType::NotPseudo, + }; + unsafe { + let mut arc = UniqueArc::<ComputedValues>::new_uninit(); + bindings::Gecko_ComputedStyle_Init( + arc.as_mut_ptr() as *mut _, + &self, + pseudo_ty, + ); + // We're simulating move semantics by having C++ do a memcpy and + // then forgetting it on this end. + forget(self); + UniqueArc::assume_init(arc).shareable() + } + } +} + +impl ops::Deref for ComputedValues { + type Target = ComputedValuesInner; + #[inline] + fn deref(&self) -> &ComputedValuesInner { + &self.0.mSource + } +} + +impl ops::DerefMut for ComputedValues { + #[inline] + fn deref_mut(&mut self) -> &mut ComputedValuesInner { + &mut self.0.mSource + } +} + +impl ComputedValuesInner { + /// Returns true if the value of the `content` property would make a + /// pseudo-element not rendered. + #[inline] + pub fn ineffective_content_property(&self) -> bool { + self.get_counters().ineffective_content_property() + } + + #[inline] + fn visited_style_ptr(&self) -> *const ComputedValues { + self.visited_style as *const _ + } + + /// Returns the visited style, if any. + pub fn visited_style(&self) -> Option<<&ComputedValues> { + unsafe { self.visited_style_ptr().as_ref() } + } + + % for style_struct in data.style_structs: + #[inline] + fn ${style_struct.name_lower}_ptr(&self) -> *const style_structs::${style_struct.name} { + // This is sound because the wrapper we create is repr(transparent). + self.${style_struct.gecko_name} as *const _ + } + + #[inline] + pub fn clone_${style_struct.name_lower}(&self) -> Arc<style_structs::${style_struct.name}> { + unsafe { Arc::from_raw_addrefed(self.${style_struct.name_lower}_ptr()) } + } + #[inline] + pub fn get_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} { + unsafe { &*self.${style_struct.name_lower}_ptr() } + } + + #[inline] + pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} { + unsafe { + let mut arc = Arc::from_raw(self.${style_struct.name_lower}_ptr()); + let ptr = Arc::make_mut(&mut arc) as *mut _; + // Sound for the same reason _ptr() is sound. + self.${style_struct.gecko_name} = Arc::into_raw(arc) as *const _; + &mut *ptr + } + } + % endfor +} + +<%def name="impl_simple_setter(ident, gecko_ffi_name)"> + #[allow(non_snake_case)] + pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) { + ${set_gecko_property(gecko_ffi_name, "From::from(v)")} + } +</%def> + +<%def name="impl_simple_clone(ident, gecko_ffi_name)"> + #[allow(non_snake_case)] + pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T { + From::from(self.${gecko_ffi_name}.clone()) + } +</%def> + +<%def name="impl_simple_copy(ident, gecko_ffi_name, *kwargs)"> + #[allow(non_snake_case)] + pub fn copy_${ident}_from(&mut self, other: &Self) { + self.${gecko_ffi_name} = other.${gecko_ffi_name}.clone(); + } + + #[allow(non_snake_case)] + pub fn reset_${ident}(&mut self, other: &Self) { + self.copy_${ident}_from(other) + } +</%def> + +<%! +def get_gecko_property(ffi_name, self_param = "self"): + return "%s.%s" % (self_param, ffi_name) + +def set_gecko_property(ffi_name, expr): + return "self.%s = %s;" % (ffi_name, expr) +%> + +<%def name="impl_keyword_setter(ident, gecko_ffi_name, keyword, cast_type='u8')"> + #[allow(non_snake_case)] + pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) { + use crate::properties::longhands::${ident}::computed_value::T as Keyword; + // FIXME(bholley): Align binary representations and ditch |match| for cast + static_asserts + let result = match v { + % for value in keyword.values_for('gecko'): + Keyword::${to_camel_case(value)} => + structs::${keyword.gecko_constant(value)} ${keyword.maybe_cast(cast_type)}, + % endfor + }; + ${set_gecko_property(gecko_ffi_name, "result")} + } +</%def> + +<%def name="impl_keyword_clone(ident, gecko_ffi_name, keyword, cast_type='u8')"> + #[allow(non_snake_case)] + pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T { + use crate::properties::longhands::${ident}::computed_value::T as Keyword; + // FIXME(bholley): Align binary representations and ditch |match| for cast + static_asserts + + // Some constant macros in the gecko are defined as negative integer(e.g. font-stretch). + // And they are convert to signed integer in Rust bindings. We need to cast then + // as signed type when we have both signed/unsigned integer in order to use them + // as match's arms. + // Also, to use same implementation here we use casted constant if we have only singed values. + % if keyword.gecko_enum_prefix is None: + % for value in keyword.values_for('gecko'): + const ${keyword.casted_constant_name(value, cast_type)} : ${cast_type} = + structs::${keyword.gecko_constant(value)} as ${cast_type}; + % endfor + + match ${get_gecko_property(gecko_ffi_name)} as ${cast_type} { + % for value in keyword.values_for('gecko'): + ${keyword.casted_constant_name(value, cast_type)} => Keyword::${to_camel_case(value)}, + % endfor + % if keyword.gecko_inexhaustive: + _ => panic!("Found unexpected value in style struct for ${ident} property"), + % endif + } + % else: + match ${get_gecko_property(gecko_ffi_name)} { + % for value in keyword.values_for('gecko'): + structs::${keyword.gecko_constant(value)} => Keyword::${to_camel_case(value)}, + % endfor + % if keyword.gecko_inexhaustive: + _ => panic!("Found unexpected value in style struct for ${ident} property"), + % endif + } + % endif + } +</%def> + +<%def name="impl_keyword(ident, gecko_ffi_name, keyword, cast_type='u8', **kwargs)"> +<%call expr="impl_keyword_setter(ident, gecko_ffi_name, keyword, cast_type, **kwargs)"></%call> +<%call expr="impl_simple_copy(ident, gecko_ffi_name, **kwargs)"></%call> +<%call expr="impl_keyword_clone(ident, gecko_ffi_name, keyword, cast_type)"></%call> +</%def> + +<%def name="impl_simple(ident, gecko_ffi_name)"> +<%call expr="impl_simple_setter(ident, gecko_ffi_name)"></%call> +<%call expr="impl_simple_copy(ident, gecko_ffi_name)"></%call> +<%call expr="impl_simple_clone(ident, gecko_ffi_name)"></%call> +</%def> + +<%def name="impl_border_width(ident, gecko_ffi_name, inherit_from)"> + #[allow(non_snake_case)] + pub fn set_${ident}(&mut self, v: Au) { + let value = v.0; + self.${inherit_from} = value; + self.${gecko_ffi_name} = value; + } + + #[allow(non_snake_case)] + pub fn copy_${ident}_from(&mut self, other: &Self) { + self.${inherit_from} = other.${inherit_from}; + // NOTE: This is needed to easily handle the `unset` and `initial` + // keywords, which are implemented calling this function. + // + // In practice, this means that we may have an incorrect value here, but + // we'll adjust that properly in the style fixup phase. + // + // FIXME(emilio): We could clean this up a bit special-casing the reset_ + // function below. + self.${gecko_ffi_name} = other.${inherit_from}; + } + + #[allow(non_snake_case)] + pub fn reset_${ident}(&mut self, other: &Self) { + self.copy_${ident}_from(other) + } + + #[allow(non_snake_case)] + pub fn clone_${ident}(&self) -> Au { + Au(self.${gecko_ffi_name}) + } +</%def> + +<%def name="impl_split_style_coord(ident, gecko_ffi_name, index)"> + #[allow(non_snake_case)] + pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) { + self.${gecko_ffi_name}.${index} = v; + } + #[allow(non_snake_case)] + pub fn copy_${ident}_from(&mut self, other: &Self) { + self.${gecko_ffi_name}.${index} = + other.${gecko_ffi_name}.${index}.clone(); + } + #[allow(non_snake_case)] + pub fn reset_${ident}(&mut self, other: &Self) { + self.copy_${ident}_from(other) + } + + #[allow(non_snake_case)] + pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T { + self.${gecko_ffi_name}.${index}.clone() + } +</%def> + +<%def name="copy_sides_style_coord(ident)"> + <% gecko_ffi_name = "m" + to_camel_case(ident) %> + #[allow(non_snake_case)] + pub fn copy_${ident}_from(&mut self, other: &Self) { + % for side in SIDES: + self.${gecko_ffi_name}.data_at_mut(${side.index}) + .copy_from(&other.${gecko_ffi_name}.data_at(${side.index})); + % endfor + ${ caller.body() } + } + + #[allow(non_snake_case)] + pub fn reset_${ident}(&mut self, other: &Self) { + self.copy_${ident}_from(other) + } +</%def> + +<%def name="impl_corner_style_coord(ident, gecko_ffi_name, corner)"> + #[allow(non_snake_case)] + pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) { + self.${gecko_ffi_name}.${corner} = v; + } + #[allow(non_snake_case)] + pub fn copy_${ident}_from(&mut self, other: &Self) { + self.${gecko_ffi_name}.${corner} = + other.${gecko_ffi_name}.${corner}.clone(); + } + #[allow(non_snake_case)] + pub fn reset_${ident}(&mut self, other: &Self) { + self.copy_${ident}_from(other) + } + #[allow(non_snake_case)] + pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T { + self.${gecko_ffi_name}.${corner}.clone() + } +</%def> + +<%def name="impl_style_struct(style_struct)"> +/// A wrapper for ${style_struct.gecko_ffi_name}, to be able to manually construct / destruct / +/// clone it. +#[repr(transparent)] +pub struct ${style_struct.gecko_struct_name}(ManuallyDrop<structs::${style_struct.gecko_ffi_name}>); + +impl ops::Deref for ${style_struct.gecko_struct_name} { + type Target = structs::${style_struct.gecko_ffi_name}; + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for ${style_struct.gecko_struct_name} { + #[inline] + fn deref_mut(&mut self) -> &mut <Self as ops::Deref>::Target { + &mut self.0 + } +} + +impl ${style_struct.gecko_struct_name} { + #[allow(dead_code, unused_variables)] + pub fn default(document: &structs::Document) -> Arc<Self> { +% if style_struct.document_dependent: + unsafe { + let mut result = UniqueArc::<Self>::new_uninit(); + Gecko_Construct_Default_${style_struct.gecko_ffi_name}( + result.as_mut_ptr() as *mut _, + document, + ); + UniqueArc::assume_init(result).shareable() + } +% else: + lazy_static! { + static ref DEFAULT: Arc<${style_struct.gecko_struct_name}> = unsafe { + let mut result = UniqueArc::<${style_struct.gecko_struct_name}>::new_uninit(); + Gecko_Construct_Default_${style_struct.gecko_ffi_name}( + result.as_mut_ptr() as *mut _, + std::ptr::null(), + ); + let arc = UniqueArc::assume_init(result).shareable(); + arc.mark_as_intentionally_leaked(); + arc + }; + }; + DEFAULT.clone() +% endif + } +} + +impl Drop for ${style_struct.gecko_struct_name} { + fn drop(&mut self) { + unsafe { + Gecko_Destroy_${style_struct.gecko_ffi_name}(&mut **self); + } + } +} +impl Clone for ${style_struct.gecko_struct_name} { + fn clone(&self) -> Self { + unsafe { + let mut result = MaybeUninit::<Self>::uninit(); + // FIXME(bug 1595895): Zero the memory to keep valgrind happy, but + // these looks like Valgrind false-positives at a quick glance. + ptr::write_bytes::<Self>(result.as_mut_ptr(), 0, 1); + Gecko_CopyConstruct_${style_struct.gecko_ffi_name}(result.as_mut_ptr() as *mut _, &**self); + result.assume_init() + } + } +} +</%def> + +<%def name="impl_simple_type_with_conversion(ident, gecko_ffi_name)"> + #[allow(non_snake_case)] + pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) { + self.${gecko_ffi_name} = From::from(v) + } + + <% impl_simple_copy(ident, gecko_ffi_name) %> + + #[allow(non_snake_case)] + pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T { + From::from(self.${gecko_ffi_name}) + } +</%def> + +<%def name="impl_font_settings(ident, gecko_type, tag_type, value_type, gecko_value_type)"> + <% + gecko_ffi_name = to_camel_case_lower(ident) + %> + + pub fn set_${ident}(&mut self, v: longhands::${ident}::computed_value::T) { + let iter = v.0.iter().map(|other| structs::${gecko_type} { + mTag: other.tag.0, + mValue: other.value as ${gecko_value_type}, + }); + self.mFont.${gecko_ffi_name}.assign_from_iter_pod(iter); + } + + pub fn copy_${ident}_from(&mut self, other: &Self) { + let iter = other.mFont.${gecko_ffi_name}.iter().map(|s| *s); + self.mFont.${gecko_ffi_name}.assign_from_iter_pod(iter); + } + + pub fn reset_${ident}(&mut self, other: &Self) { + self.copy_${ident}_from(other) + } + + pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T { + use crate::values::generics::font::{FontSettings, FontTag, ${tag_type}}; + + FontSettings( + self.mFont.${gecko_ffi_name}.iter().map(|gecko_font_setting| { + ${tag_type} { + tag: FontTag(gecko_font_setting.mTag), + value: gecko_font_setting.mValue as ${value_type}, + } + }).collect::<Vec<_>>().into_boxed_slice() + ) + } +</%def> + +<%def name="impl_trait(style_struct_name, skip_longhands='')"> +<% + style_struct = next(x for x in data.style_structs if x.name == style_struct_name) + longhands = [x for x in style_struct.longhands + if not (skip_longhands == "*" or x.name in skip_longhands.split())] + + def longhand_method(longhand): + args = dict(ident=longhand.ident, gecko_ffi_name=longhand.gecko_ffi_name) + + if longhand.logical: + return + # get the method and pass additional keyword or type-specific arguments + if longhand.keyword: + method = impl_keyword + args.update(keyword=longhand.keyword) + if "font" in longhand.ident: + args.update(cast_type=longhand.cast_type) + else: + method = impl_simple + + method(**args) +%> +impl ${style_struct.gecko_struct_name} { + /* + * Manually-Implemented Methods. + */ + ${caller.body().strip()} + + /* + * Auto-Generated Methods. + */ + <% + for longhand in longhands: + longhand_method(longhand) + %> +} +</%def> + +<%! +class Side(object): + def __init__(self, name, index): + self.name = name + self.ident = name.lower() + self.index = index + +SIDES = [Side("Top", 0), Side("Right", 1), Side("Bottom", 2), Side("Left", 3)] +CORNERS = ["top_left", "top_right", "bottom_right", "bottom_left"] +%> + +#[allow(dead_code)] +fn static_assert() { + // Note: using the above technique with an enum hits a rust bug when |structs| is in a different crate. + % for side in SIDES: + { const DETAIL: u32 = [0][(structs::Side::eSide${side.name} as usize != ${side.index}) as usize]; let _ = DETAIL; } + % endfor +} + + +<% skip_border_longhands = " ".join(["border-{0}-{1}".format(x.ident, y) + for x in SIDES + for y in ["style", "width"]] + + ["border-{0}-radius".format(x.replace("_", "-")) + for x in CORNERS]) %> + +<%self:impl_trait style_struct_name="Border" + skip_longhands="${skip_border_longhands} border-image-repeat"> + % for side in SIDES: + pub fn set_border_${side.ident}_style(&mut self, v: BorderStyle) { + self.mBorderStyle[${side.index}] = v; + + // This is needed because the initial mComputedBorder value is set to + // zero. + // + // In order to compute stuff, we start from the initial struct, and keep + // going down the tree applying properties. + // + // That means, effectively, that when we set border-style to something + // non-hidden, we should use the initial border instead. + // + // Servo stores the initial border-width in the initial struct, and then + // adjusts as needed in the fixup phase. This means that the initial + // struct is technically not valid without fixups, and that you lose + // pretty much any sharing of the initial struct, which is kind of + // unfortunate. + // + // Gecko has two fields for this, one that stores the "specified" + // border, and other that stores the actual computed one. That means + // that when we set border-style, border-width may change and we need to + // sync back to the specified one. This is what this function does. + // + // Note that this doesn't impose any dependency in the order of + // computation of the properties. This is only relevant if border-style + // is specified, but border-width isn't. If border-width is specified at + // some point, the two mBorder and mComputedBorder fields would be the + // same already. + // + // Once we're here, we know that we'll run style fixups, so it's fine to + // just copy the specified border here, we'll adjust it if it's + // incorrect later. + self.mComputedBorder.${side.ident} = self.mBorder.${side.ident}; + } + + pub fn copy_border_${side.ident}_style_from(&mut self, other: &Self) { + self.set_border_${side.ident}_style(other.mBorderStyle[${side.index}]); + } + + pub fn reset_border_${side.ident}_style(&mut self, other: &Self) { + self.copy_border_${side.ident}_style_from(other); + } + + #[inline] + pub fn clone_border_${side.ident}_style(&self) -> BorderStyle { + self.mBorderStyle[${side.index}] + } + + ${impl_border_width("border_%s_width" % side.ident, "mComputedBorder.%s" % side.ident, "mBorder.%s" % side.ident)} + + pub fn border_${side.ident}_has_nonzero_width(&self) -> bool { + self.mComputedBorder.${side.ident} != 0 + } + % endfor + + % for corner in CORNERS: + <% impl_corner_style_coord("border_%s_radius" % corner, + "mBorderRadius", + corner) %> + % endfor + + <% + border_image_repeat_keywords = ["Stretch", "Repeat", "Round", "Space"] + %> + + pub fn set_border_image_repeat(&mut self, v: longhands::border_image_repeat::computed_value::T) { + use crate::values::specified::border::BorderImageRepeatKeyword; + use crate::gecko_bindings::structs::StyleBorderImageRepeat; + + % for i, side in enumerate(["H", "V"]): + self.mBorderImageRepeat${side} = match v.${i} { + % for keyword in border_image_repeat_keywords: + BorderImageRepeatKeyword::${keyword} => StyleBorderImageRepeat::${keyword}, + % endfor + }; + % endfor + } + + pub fn copy_border_image_repeat_from(&mut self, other: &Self) { + self.mBorderImageRepeatH = other.mBorderImageRepeatH; + self.mBorderImageRepeatV = other.mBorderImageRepeatV; + } + + pub fn reset_border_image_repeat(&mut self, other: &Self) { + self.copy_border_image_repeat_from(other) + } + + pub fn clone_border_image_repeat(&self) -> longhands::border_image_repeat::computed_value::T { + use crate::values::specified::border::BorderImageRepeatKeyword; + use crate::gecko_bindings::structs::StyleBorderImageRepeat; + + % for side in ["H", "V"]: + let servo_${side.lower()} = match self.mBorderImageRepeat${side} { + % for keyword in border_image_repeat_keywords: + StyleBorderImageRepeat::${keyword} => BorderImageRepeatKeyword::${keyword}, + % endfor + }; + % endfor + longhands::border_image_repeat::computed_value::T(servo_h, servo_v) + } +</%self:impl_trait> + +<% skip_scroll_margin_longhands = " ".join(["scroll-margin-%s" % x.ident for x in SIDES]) %> +<% skip_margin_longhands = " ".join(["margin-%s" % x.ident for x in SIDES]) %> +<%self:impl_trait style_struct_name="Margin" + skip_longhands="${skip_margin_longhands} + ${skip_scroll_margin_longhands}"> + % for side in SIDES: + <% impl_split_style_coord("margin_%s" % side.ident, + "mMargin", + side.index) %> + <% impl_split_style_coord("scroll_margin_%s" % side.ident, + "mScrollMargin", + side.index) %> + % endfor +</%self:impl_trait> + +<% skip_scroll_padding_longhands = " ".join(["scroll-padding-%s" % x.ident for x in SIDES]) %> +<% skip_padding_longhands = " ".join(["padding-%s" % x.ident for x in SIDES]) %> +<%self:impl_trait style_struct_name="Padding" + skip_longhands="${skip_padding_longhands} + ${skip_scroll_padding_longhands}"> + + % for side in SIDES: + <% impl_split_style_coord("padding_%s" % side.ident, + "mPadding", + side.index) %> + <% impl_split_style_coord("scroll_padding_%s" % side.ident, "mScrollPadding", side.index) %> + % endfor +</%self:impl_trait> + +<%self:impl_trait style_struct_name="Page"> +</%self:impl_trait> + +<% skip_position_longhands = " ".join(x.ident for x in SIDES) %> +<%self:impl_trait style_struct_name="Position" + skip_longhands="${skip_position_longhands} + masonry-auto-flow"> + % for side in SIDES: + <% impl_split_style_coord(side.ident, "mOffset", side.index) %> + % endfor + pub fn set_computed_justify_items(&mut self, v: values::specified::JustifyItems) { + debug_assert_ne!(v.0, crate::values::specified::align::AlignFlags::LEGACY); + self.mJustifyItems.computed = v; + } + + ${impl_simple_type_with_conversion("masonry_auto_flow", "mMasonryAutoFlow")} +</%self:impl_trait> + +<%self:impl_trait style_struct_name="Outline" + skip_longhands="outline-style outline-width"> + + pub fn set_outline_style(&mut self, v: longhands::outline_style::computed_value::T) { + self.mOutlineStyle = v; + // NB: This is needed to correctly handling the initial value of + // outline-width when outline-style changes, see the + // update_border_${side.ident} comment for more details. + self.mActualOutlineWidth = self.mOutlineWidth; + } + + pub fn copy_outline_style_from(&mut self, other: &Self) { + self.set_outline_style(other.mOutlineStyle); + } + + pub fn reset_outline_style(&mut self, other: &Self) { + self.copy_outline_style_from(other) + } + + pub fn clone_outline_style(&self) -> longhands::outline_style::computed_value::T { + self.mOutlineStyle.clone() + } + + ${impl_border_width("outline_width", "mActualOutlineWidth", "mOutlineWidth")} + + pub fn outline_has_nonzero_width(&self) -> bool { + self.mActualOutlineWidth != 0 + } +</%self:impl_trait> + +<% skip_font_longhands = """font-family font-size font-size-adjust font-weight + font-style font-stretch -x-lang + font-variant-alternates font-variant-east-asian + font-variant-ligatures font-variant-numeric + font-language-override font-feature-settings + font-variation-settings -moz-min-font-size-ratio""" %> +<%self:impl_trait style_struct_name="Font" + skip_longhands="${skip_font_longhands}"> + + // Negative numbers are invalid at parse time, but <integer> is still an + // i32. + <% impl_font_settings("font_feature_settings", "gfxFontFeature", "FeatureTagValue", "i32", "u32") %> + <% impl_font_settings("font_variation_settings", "gfxFontVariation", "VariationValue", "f32", "f32") %> + + pub fn unzoom_fonts(&mut self, device: &Device) { + use crate::values::generics::NonNegative; + self.mSize = NonNegative(device.unzoom_text(self.mSize.0)); + self.mScriptUnconstrainedSize = NonNegative(device.unzoom_text(self.mScriptUnconstrainedSize.0)); + self.mFont.size = NonNegative(device.unzoom_text(self.mFont.size.0)); + } + + pub fn copy_font_size_from(&mut self, other: &Self) { + self.mScriptUnconstrainedSize = other.mScriptUnconstrainedSize; + + self.mSize = other.mScriptUnconstrainedSize; + // NOTE: Intentionally not copying from mFont.size. The cascade process + // recomputes the used size as needed. + self.mFont.size = other.mSize; + self.mFontSizeKeyword = other.mFontSizeKeyword; + + // TODO(emilio): Should we really copy over these two? + self.mFontSizeFactor = other.mFontSizeFactor; + self.mFontSizeOffset = other.mFontSizeOffset; + } + + pub fn reset_font_size(&mut self, other: &Self) { + self.copy_font_size_from(other) + } + + pub fn set_font_size(&mut self, v: FontSize) { + let computed_size = v.computed_size; + self.mScriptUnconstrainedSize = computed_size; + + // These two may be changed from Cascade::fixup_font_stuff. + self.mSize = computed_size; + // NOTE: Intentionally not copying from used_size. The cascade process + // recomputes the used size as needed. + self.mFont.size = computed_size; + + self.mFontSizeKeyword = v.keyword_info.kw; + self.mFontSizeFactor = v.keyword_info.factor; + self.mFontSizeOffset = v.keyword_info.offset; + } + + pub fn clone_font_size(&self) -> FontSize { + use crate::values::specified::font::KeywordInfo; + + FontSize { + computed_size: self.mSize, + used_size: self.mFont.size, + keyword_info: KeywordInfo { + kw: self.mFontSizeKeyword, + factor: self.mFontSizeFactor, + offset: self.mFontSizeOffset, + } + } + } + + ${impl_simple('font_weight', 'mFont.weight')} + ${impl_simple('font_stretch', 'mFont.stretch')} + ${impl_simple('font_style', 'mFont.style')} + + ${impl_simple("font_variant_alternates", "mFont.variantAlternates")} + + ${impl_simple("font_size_adjust", "mFont.sizeAdjust")} + + ${impl_simple("font_family", "mFont.family")} + + #[allow(non_snake_case)] + pub fn set__x_lang(&mut self, v: longhands::_x_lang::computed_value::T) { + let ptr = v.0.as_ptr(); + forget(v); + unsafe { + Gecko_nsStyleFont_SetLang(&mut **self, ptr); + } + } + + #[allow(non_snake_case)] + pub fn copy__x_lang_from(&mut self, other: &Self) { + unsafe { + Gecko_nsStyleFont_CopyLangFrom(&mut **self, &**other); + } + } + + #[allow(non_snake_case)] + pub fn reset__x_lang(&mut self, other: &Self) { + self.copy__x_lang_from(other) + } + + #[allow(non_snake_case)] + pub fn clone__x_lang(&self) -> longhands::_x_lang::computed_value::T { + longhands::_x_lang::computed_value::T(unsafe { + Atom::from_raw(self.mLanguage.mRawPtr) + }) + } + + + ${impl_simple_type_with_conversion("font_language_override", "mFont.languageOverride")} + ${impl_simple_type_with_conversion("font_variant_ligatures", "mFont.variantLigatures")} + ${impl_simple_type_with_conversion("font_variant_east_asian", "mFont.variantEastAsian")} + ${impl_simple_type_with_conversion("font_variant_numeric", "mFont.variantNumeric")} + + #[allow(non_snake_case)] + pub fn clone__moz_min_font_size_ratio( + &self, + ) -> longhands::_moz_min_font_size_ratio::computed_value::T { + Percentage(self.mMinFontSizeRatio as f32 / 100.) + } + + #[allow(non_snake_case)] + pub fn set__moz_min_font_size_ratio(&mut self, v: longhands::_moz_min_font_size_ratio::computed_value::T) { + let scaled = v.0 * 100.; + let percentage = if scaled > 255. { + 255. + } else if scaled < 0. { + 0. + } else { + scaled + }; + + self.mMinFontSizeRatio = percentage as u8; + } + + ${impl_simple_copy('_moz_min_font_size_ratio', 'mMinFontSizeRatio')} +</%self:impl_trait> + +<%def name="impl_coordinated_property_copy(type, ident, gecko_ffi_name)"> + #[allow(non_snake_case)] + pub fn copy_${type}_${ident}_from(&mut self, other: &Self) { + self.m${to_camel_case(type)}s.ensure_len(other.m${to_camel_case(type)}s.len()); + + let count = other.m${to_camel_case(type)}${gecko_ffi_name}Count; + self.m${to_camel_case(type)}${gecko_ffi_name}Count = count; + + let iter = self.m${to_camel_case(type)}s.iter_mut().take(count as usize).zip( + other.m${to_camel_case(type)}s.iter() + ); + + for (ours, others) in iter { + ours.m${gecko_ffi_name} = others.m${gecko_ffi_name}.clone(); + } + } + #[allow(non_snake_case)] + pub fn reset_${type}_${ident}(&mut self, other: &Self) { + self.copy_${type}_${ident}_from(other) + } +</%def> + +<%def name="impl_coordinated_property_count(type, ident, gecko_ffi_name)"> + #[allow(non_snake_case)] + pub fn ${type}_${ident}_count(&self) -> usize { + self.m${to_camel_case(type)}${gecko_ffi_name}Count as usize + } +</%def> + +<%def name="impl_coordinated_property(type, ident, gecko_ffi_name)"> + #[allow(non_snake_case)] + pub fn set_${type}_${ident}<I>(&mut self, v: I) + where + I: IntoIterator<Item = longhands::${type}_${ident}::computed_value::single_value::T>, + I::IntoIter: ExactSizeIterator + Clone + { + let v = v.into_iter(); + debug_assert_ne!(v.len(), 0); + let input_len = v.len(); + self.m${to_camel_case(type)}s.ensure_len(input_len); + + self.m${to_camel_case(type)}${gecko_ffi_name}Count = input_len as u32; + for (gecko, servo) in self.m${to_camel_case(type)}s.iter_mut().take(input_len as usize).zip(v) { + gecko.m${gecko_ffi_name} = servo; + } + } + #[allow(non_snake_case)] + pub fn ${type}_${ident}_at(&self, index: usize) + -> longhands::${type}_${ident}::computed_value::SingleComputedValue { + self.m${to_camel_case(type)}s[index % self.${type}_${ident}_count()].m${gecko_ffi_name}.clone() + } + ${impl_coordinated_property_copy(type, ident, gecko_ffi_name)} + ${impl_coordinated_property_count(type, ident, gecko_ffi_name)} +</%def> + +<% skip_box_longhands= """display contain""" %> +<%self:impl_trait style_struct_name="Box" skip_longhands="${skip_box_longhands}"> + #[inline] + pub fn set_display(&mut self, v: longhands::display::computed_value::T) { + self.mDisplay = v; + self.mOriginalDisplay = v; + } + + #[inline] + pub fn copy_display_from(&mut self, other: &Self) { + self.set_display(other.mDisplay); + } + + #[inline] + pub fn reset_display(&mut self, other: &Self) { + self.copy_display_from(other) + } + + #[inline] + pub fn set_adjusted_display( + &mut self, + v: longhands::display::computed_value::T, + _is_item_or_root: bool + ) { + self.mDisplay = v; + } + + #[inline] + pub fn clone_display(&self) -> longhands::display::computed_value::T { + self.mDisplay + } + + #[inline] + pub fn set_contain(&mut self, v: longhands::contain::computed_value::T) { + self.mContain = v; + self.mEffectiveContainment = v; + } + + #[inline] + pub fn copy_contain_from(&mut self, other: &Self) { + self.set_contain(other.mContain); + } + + #[inline] + pub fn reset_contain(&mut self, other: &Self) { + self.copy_contain_from(other) + } + + #[inline] + pub fn clone_contain(&self) -> longhands::contain::computed_value::T { + self.mContain + } + + #[inline] + pub fn set_effective_containment( + &mut self, + v: longhands::contain::computed_value::T + ) { + self.mEffectiveContainment = v; + } + + #[inline] + pub fn clone_effective_containment(&self) -> longhands::contain::computed_value::T { + self.mEffectiveContainment + } +</%self:impl_trait> + +<%def name="simple_image_array_property(name, shorthand, field_name)"> + <% + image_layers_field = "mImage" if shorthand == "background" else "mMask" + copy_simple_image_array_property(name, shorthand, image_layers_field, field_name) + %> + + pub fn set_${shorthand}_${name}<I>(&mut self, v: I) + where I: IntoIterator<Item=longhands::${shorthand}_${name}::computed_value::single_value::T>, + I::IntoIter: ExactSizeIterator + { + use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType; + let v = v.into_iter(); + + unsafe { + Gecko_EnsureImageLayersLength(&mut self.${image_layers_field}, v.len(), + LayerType::${shorthand.title()}); + } + + self.${image_layers_field}.${field_name}Count = v.len() as u32; + for (servo, geckolayer) in v.zip(self.${image_layers_field}.mLayers.iter_mut()) { + geckolayer.${field_name} = { + ${caller.body()} + }; + } + } +</%def> + +<%def name="copy_simple_image_array_property(name, shorthand, layers_field_name, field_name)"> + pub fn copy_${shorthand}_${name}_from(&mut self, other: &Self) { + use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType; + + let count = other.${layers_field_name}.${field_name}Count; + unsafe { + Gecko_EnsureImageLayersLength(&mut self.${layers_field_name}, + count as usize, + LayerType::${shorthand.title()}); + } + // FIXME(emilio): This may be bogus in the same way as bug 1426246. + for (layer, other) in self.${layers_field_name}.mLayers.iter_mut() + .zip(other.${layers_field_name}.mLayers.iter()) + .take(count as usize) { + layer.${field_name} = other.${field_name}.clone(); + } + self.${layers_field_name}.${field_name}Count = count; + } + + pub fn reset_${shorthand}_${name}(&mut self, other: &Self) { + self.copy_${shorthand}_${name}_from(other) + } +</%def> + +<%def name="impl_simple_image_array_property(name, shorthand, layer_field_name, field_name, struct_name)"> + <% + ident = "%s_%s" % (shorthand, name) + style_struct = next(x for x in data.style_structs if x.name == struct_name) + longhand = next(x for x in style_struct.longhands if x.ident == ident) + keyword = longhand.keyword + %> + + <% copy_simple_image_array_property(name, shorthand, layer_field_name, field_name) %> + + pub fn set_${ident}<I>(&mut self, v: I) + where + I: IntoIterator<Item=longhands::${ident}::computed_value::single_value::T>, + I::IntoIter: ExactSizeIterator, + { + use crate::properties::longhands::${ident}::single_value::computed_value::T as Keyword; + use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType; + + let v = v.into_iter(); + + unsafe { + Gecko_EnsureImageLayersLength(&mut self.${layer_field_name}, v.len(), + LayerType::${shorthand.title()}); + } + + self.${layer_field_name}.${field_name}Count = v.len() as u32; + for (servo, geckolayer) in v.zip(self.${layer_field_name}.mLayers.iter_mut()) { + geckolayer.${field_name} = { + match servo { + % for value in keyword.values_for("gecko"): + Keyword::${to_camel_case(value)} => + structs::${keyword.gecko_constant(value)} ${keyword.maybe_cast('u8')}, + % endfor + } + }; + } + } + + pub fn clone_${ident}(&self) -> longhands::${ident}::computed_value::T { + use crate::properties::longhands::${ident}::single_value::computed_value::T as Keyword; + + % if keyword.needs_cast(): + % for value in keyword.values_for('gecko'): + const ${keyword.casted_constant_name(value, "u8")} : u8 = + structs::${keyword.gecko_constant(value)} as u8; + % endfor + % endif + + longhands::${ident}::computed_value::List( + self.${layer_field_name}.mLayers.iter() + .take(self.${layer_field_name}.${field_name}Count as usize) + .map(|ref layer| { + match layer.${field_name} { + % for value in longhand.keyword.values_for("gecko"): + % if keyword.needs_cast(): + ${keyword.casted_constant_name(value, "u8")} + % else: + structs::${keyword.gecko_constant(value)} + % endif + => Keyword::${to_camel_case(value)}, + % endfor + % if keyword.gecko_inexhaustive: + _ => panic!("Found unexpected value in style struct for ${ident} property"), + % endif + } + }).collect() + ) + } +</%def> + +<%def name="impl_common_image_layer_properties(shorthand)"> + <% + if shorthand == "background": + image_layers_field = "mImage" + struct_name = "Background" + else: + image_layers_field = "mMask" + struct_name = "SVG" + %> + + <%self:simple_image_array_property name="repeat" shorthand="${shorthand}" field_name="mRepeat"> + use crate::values::specified::background::BackgroundRepeatKeyword; + use crate::gecko_bindings::structs::nsStyleImageLayers_Repeat; + use crate::gecko_bindings::structs::StyleImageLayerRepeat; + + fn to_ns(repeat: BackgroundRepeatKeyword) -> StyleImageLayerRepeat { + match repeat { + BackgroundRepeatKeyword::Repeat => StyleImageLayerRepeat::Repeat, + BackgroundRepeatKeyword::Space => StyleImageLayerRepeat::Space, + BackgroundRepeatKeyword::Round => StyleImageLayerRepeat::Round, + BackgroundRepeatKeyword::NoRepeat => StyleImageLayerRepeat::NoRepeat, + } + } + + let repeat_x = to_ns(servo.0); + let repeat_y = to_ns(servo.1); + nsStyleImageLayers_Repeat { + mXRepeat: repeat_x, + mYRepeat: repeat_y, + } + </%self:simple_image_array_property> + + pub fn clone_${shorthand}_repeat(&self) -> longhands::${shorthand}_repeat::computed_value::T { + use crate::properties::longhands::${shorthand}_repeat::single_value::computed_value::T; + use crate::values::specified::background::BackgroundRepeatKeyword; + use crate::gecko_bindings::structs::StyleImageLayerRepeat; + + fn to_servo(repeat: StyleImageLayerRepeat) -> BackgroundRepeatKeyword { + match repeat { + StyleImageLayerRepeat::Repeat => BackgroundRepeatKeyword::Repeat, + StyleImageLayerRepeat::Space => BackgroundRepeatKeyword::Space, + StyleImageLayerRepeat::Round => BackgroundRepeatKeyword::Round, + StyleImageLayerRepeat::NoRepeat => BackgroundRepeatKeyword::NoRepeat, + _ => panic!("Found unexpected value in style struct for ${shorthand}_repeat property"), + } + } + + longhands::${shorthand}_repeat::computed_value::List( + self.${image_layers_field}.mLayers.iter() + .take(self.${image_layers_field}.mRepeatCount as usize) + .map(|ref layer| { + T(to_servo(layer.mRepeat.mXRepeat), to_servo(layer.mRepeat.mYRepeat)) + }).collect() + ) + } + + <% impl_simple_image_array_property("clip", shorthand, image_layers_field, "mClip", struct_name) %> + <% impl_simple_image_array_property("origin", shorthand, image_layers_field, "mOrigin", struct_name) %> + + % for (orientation, keyword) in [("x", "horizontal"), ("y", "vertical")]: + pub fn copy_${shorthand}_position_${orientation}_from(&mut self, other: &Self) { + use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType; + + let count = other.${image_layers_field}.mPosition${orientation.upper()}Count; + + unsafe { + Gecko_EnsureImageLayersLength(&mut self.${image_layers_field}, + count as usize, + LayerType::${shorthand.capitalize()}); + } + + for (layer, other) in self.${image_layers_field}.mLayers.iter_mut() + .zip(other.${image_layers_field}.mLayers.iter()) + .take(count as usize) { + layer.mPosition.${keyword} = other.mPosition.${keyword}.clone(); + } + self.${image_layers_field}.mPosition${orientation.upper()}Count = count; + } + + pub fn reset_${shorthand}_position_${orientation}(&mut self, other: &Self) { + self.copy_${shorthand}_position_${orientation}_from(other) + } + + pub fn clone_${shorthand}_position_${orientation}(&self) + -> longhands::${shorthand}_position_${orientation}::computed_value::T { + longhands::${shorthand}_position_${orientation}::computed_value::List( + self.${image_layers_field}.mLayers.iter() + .take(self.${image_layers_field}.mPosition${orientation.upper()}Count as usize) + .map(|position| position.mPosition.${keyword}.clone()) + .collect() + ) + } + + pub fn set_${shorthand}_position_${orientation[0]}<I>(&mut self, + v: I) + where I: IntoIterator<Item = longhands::${shorthand}_position_${orientation[0]} + ::computed_value::single_value::T>, + I::IntoIter: ExactSizeIterator + { + use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType; + + let v = v.into_iter(); + + unsafe { + Gecko_EnsureImageLayersLength(&mut self.${image_layers_field}, v.len(), + LayerType::${shorthand.capitalize()}); + } + + self.${image_layers_field}.mPosition${orientation[0].upper()}Count = v.len() as u32; + for (servo, geckolayer) in v.zip(self.${image_layers_field} + .mLayers.iter_mut()) { + geckolayer.mPosition.${keyword} = servo; + } + } + % endfor + + <%self:simple_image_array_property name="size" shorthand="${shorthand}" field_name="mSize"> + servo + </%self:simple_image_array_property> + + pub fn clone_${shorthand}_size(&self) -> longhands::${shorthand}_size::computed_value::T { + longhands::${shorthand}_size::computed_value::List( + self.${image_layers_field}.mLayers.iter().map(|layer| layer.mSize.clone()).collect() + ) + } + + pub fn copy_${shorthand}_image_from(&mut self, other: &Self) { + use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType; + unsafe { + let count = other.${image_layers_field}.mImageCount; + Gecko_EnsureImageLayersLength(&mut self.${image_layers_field}, + count as usize, + LayerType::${shorthand.capitalize()}); + + for (layer, other) in self.${image_layers_field}.mLayers.iter_mut() + .zip(other.${image_layers_field}.mLayers.iter()) + .take(count as usize) { + layer.mImage = other.mImage.clone(); + } + self.${image_layers_field}.mImageCount = count; + } + } + + pub fn reset_${shorthand}_image(&mut self, other: &Self) { + self.copy_${shorthand}_image_from(other) + } + + #[allow(unused_variables)] + pub fn set_${shorthand}_image<I>(&mut self, images: I) + where I: IntoIterator<Item = longhands::${shorthand}_image::computed_value::single_value::T>, + I::IntoIter: ExactSizeIterator + { + use crate::gecko_bindings::structs::nsStyleImageLayers_LayerType as LayerType; + + let images = images.into_iter(); + + unsafe { + Gecko_EnsureImageLayersLength( + &mut self.${image_layers_field}, + images.len(), + LayerType::${shorthand.title()}, + ); + } + + self.${image_layers_field}.mImageCount = images.len() as u32; + for (image, geckoimage) in images.zip(self.${image_layers_field} + .mLayers.iter_mut()) { + geckoimage.mImage = image; + } + } + + pub fn clone_${shorthand}_image(&self) -> longhands::${shorthand}_image::computed_value::T { + longhands::${shorthand}_image::computed_value::List( + self.${image_layers_field}.mLayers.iter() + .take(self.${image_layers_field}.mImageCount as usize) + .map(|layer| layer.mImage.clone()) + .collect() + ) + } + + <% + fill_fields = "mRepeat mClip mOrigin mPositionX mPositionY mImage mSize" + if shorthand == "background": + fill_fields += " mAttachment mBlendMode" + else: + # mSourceURI uses mImageCount + fill_fields += " mMaskMode mComposite" + %> + pub fn fill_arrays(&mut self) { + use crate::gecko_bindings::bindings::Gecko_FillAllImageLayers; + use std::cmp; + let mut max_len = 1; + % for member in fill_fields.split(): + max_len = cmp::max(max_len, self.${image_layers_field}.${member}Count); + % endfor + unsafe { + // While we could do this manually, we'd need to also manually + // run all the copy constructors, so we just delegate to gecko + Gecko_FillAllImageLayers(&mut self.${image_layers_field}, max_len); + } + } +</%def> + +// TODO: Gecko accepts lists in most background-related properties. We just use +// the first element (which is the common case), but at some point we want to +// add support for parsing these lists in servo and pushing to nsTArray's. +<% skip_background_longhands = """background-repeat + background-image background-clip + background-origin background-attachment + background-size background-position + background-blend-mode + background-position-x + background-position-y""" %> +<%self:impl_trait style_struct_name="Background" + skip_longhands="${skip_background_longhands}"> + + <% impl_common_image_layer_properties("background") %> + <% impl_simple_image_array_property("attachment", "background", "mImage", "mAttachment", "Background") %> + <% impl_simple_image_array_property("blend_mode", "background", "mImage", "mBlendMode", "Background") %> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="List" skip_longhands="list-style-type"> + pub fn set_list_style_type(&mut self, v: longhands::list_style_type::computed_value::T) { + use nsstring::{nsACString, nsCStr}; + use self::longhands::list_style_type::computed_value::T; + match v { + T::None => unsafe { + bindings::Gecko_SetCounterStyleToNone(&mut self.mCounterStyle) + } + T::CounterStyle(s) => s.to_gecko_value(&mut self.mCounterStyle), + T::String(s) => unsafe { + bindings::Gecko_SetCounterStyleToString( + &mut self.mCounterStyle, + &nsCStr::from(&s) as &nsACString, + ) + } + } + } + + pub fn copy_list_style_type_from(&mut self, other: &Self) { + unsafe { + Gecko_CopyCounterStyle(&mut self.mCounterStyle, &other.mCounterStyle); + } + } + + pub fn reset_list_style_type(&mut self, other: &Self) { + self.copy_list_style_type_from(other) + } + + pub fn clone_list_style_type(&self) -> longhands::list_style_type::computed_value::T { + use self::longhands::list_style_type::computed_value::T; + use crate::values::Either; + use crate::values::generics::CounterStyle; + use crate::gecko_bindings::bindings; + + let name = unsafe { + bindings::Gecko_CounterStyle_GetName(&self.mCounterStyle) + }; + if !name.is_null() { + let name = unsafe { Atom::from_raw(name) }; + if name == atom!("none") { + return T::None; + } + } + let result = CounterStyle::from_gecko_value(&self.mCounterStyle); + match result { + Either::First(counter_style) => T::CounterStyle(counter_style), + Either::Second(string) => T::String(string), + } + } +</%self:impl_trait> + +<%self:impl_trait style_struct_name="Table"> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="Effects"> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="InheritedBox"> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="InheritedTable" + skip_longhands="border-spacing"> + + pub fn set_border_spacing(&mut self, v: longhands::border_spacing::computed_value::T) { + self.mBorderSpacingCol = v.horizontal().0; + self.mBorderSpacingRow = v.vertical().0; + } + + pub fn copy_border_spacing_from(&mut self, other: &Self) { + self.mBorderSpacingCol = other.mBorderSpacingCol; + self.mBorderSpacingRow = other.mBorderSpacingRow; + } + + pub fn reset_border_spacing(&mut self, other: &Self) { + self.copy_border_spacing_from(other) + } + + pub fn clone_border_spacing(&self) -> longhands::border_spacing::computed_value::T { + longhands::border_spacing::computed_value::T::new( + Au(self.mBorderSpacingCol).into(), + Au(self.mBorderSpacingRow).into() + ) + } +</%self:impl_trait> + + +<%self:impl_trait style_struct_name="InheritedText"> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="Text" skip_longhands="initial-letter"> + pub fn set_initial_letter(&mut self, v: longhands::initial_letter::computed_value::T) { + use crate::values::generics::text::InitialLetter; + match v { + InitialLetter::Normal => { + self.mInitialLetterSize = 0.; + self.mInitialLetterSink = 0; + }, + InitialLetter::Specified(size, sink) => { + self.mInitialLetterSize = size; + if let Some(sink) = sink { + self.mInitialLetterSink = sink; + } else { + self.mInitialLetterSink = size.floor() as i32; + } + } + } + } + + pub fn copy_initial_letter_from(&mut self, other: &Self) { + self.mInitialLetterSize = other.mInitialLetterSize; + self.mInitialLetterSink = other.mInitialLetterSink; + } + + pub fn reset_initial_letter(&mut self, other: &Self) { + self.copy_initial_letter_from(other) + } + + pub fn clone_initial_letter(&self) -> longhands::initial_letter::computed_value::T { + use crate::values::generics::text::InitialLetter; + + if self.mInitialLetterSize == 0. && self.mInitialLetterSink == 0 { + InitialLetter::Normal + } else if self.mInitialLetterSize.floor() as i32 == self.mInitialLetterSink { + InitialLetter::Specified(self.mInitialLetterSize, None) + } else { + InitialLetter::Specified(self.mInitialLetterSize, Some(self.mInitialLetterSink)) + } + } +</%self:impl_trait> + +<% skip_svg_longhands = """ +mask-mode mask-repeat mask-clip mask-origin mask-composite mask-position-x mask-position-y mask-size mask-image +""" +%> +<%self:impl_trait style_struct_name="SVG" + skip_longhands="${skip_svg_longhands}"> + <% impl_common_image_layer_properties("mask") %> + <% impl_simple_image_array_property("mode", "mask", "mMask", "mMaskMode", "SVG") %> + <% impl_simple_image_array_property("composite", "mask", "mMask", "mComposite", "SVG") %> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="InheritedSVG"> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="InheritedUI"> +</%self:impl_trait> + +<%self:impl_trait style_struct_name="Column" + skip_longhands="column-count column-rule-width column-rule-style"> + + #[allow(unused_unsafe)] + pub fn set_column_count(&mut self, v: longhands::column_count::computed_value::T) { + use crate::gecko_bindings::structs::{nsStyleColumn_kColumnCountAuto, nsStyleColumn_kMaxColumnCount}; + + self.mColumnCount = match v { + ColumnCount::Integer(integer) => { + cmp::min(integer.0 as u32, unsafe { nsStyleColumn_kMaxColumnCount }) + }, + ColumnCount::Auto => nsStyleColumn_kColumnCountAuto + }; + } + + ${impl_simple_copy('column_count', 'mColumnCount')} + + pub fn clone_column_count(&self) -> longhands::column_count::computed_value::T { + use crate::gecko_bindings::structs::{nsStyleColumn_kColumnCountAuto, nsStyleColumn_kMaxColumnCount}; + if self.mColumnCount != nsStyleColumn_kColumnCountAuto { + debug_assert!(self.mColumnCount >= 1 && + self.mColumnCount <= nsStyleColumn_kMaxColumnCount); + ColumnCount::Integer((self.mColumnCount as i32).into()) + } else { + ColumnCount::Auto + } + } + + pub fn set_column_rule_style(&mut self, v: longhands::column_rule_style::computed_value::T) { + self.mColumnRuleStyle = v; + // NB: This is needed to correctly handling the initial value of + // column-rule-width when colun-rule-style changes, see the + // update_border_${side.ident} comment for more details. + self.mActualColumnRuleWidth = self.mColumnRuleWidth; + } + + pub fn copy_column_rule_style_from(&mut self, other: &Self) { + self.set_column_rule_style(other.mColumnRuleStyle); + } + + pub fn reset_column_rule_style(&mut self, other: &Self) { + self.copy_column_rule_style_from(other) + } + + pub fn clone_column_rule_style(&self) -> longhands::column_rule_style::computed_value::T { + self.mColumnRuleStyle.clone() + } + + ${impl_border_width("column_rule_width", "mActualColumnRuleWidth", "mColumnRuleWidth")} + + pub fn column_rule_has_nonzero_width(&self) -> bool { + self.mActualColumnRuleWidth != 0 + } +</%self:impl_trait> + +<%self:impl_trait style_struct_name="Counters"> + pub fn ineffective_content_property(&self) -> bool { + !self.mContent.is_items() + } +</%self:impl_trait> + +<% skip_ui_longhands = """animation-name animation-delay animation-duration + animation-direction animation-fill-mode + animation-play-state animation-iteration-count + animation-timing-function animation-composition animation-timeline + transition-duration transition-delay + transition-timing-function transition-property + scroll-timeline-name scroll-timeline-axis + view-timeline-name view-timeline-axis view-timeline-inset""" %> + +<%self:impl_trait style_struct_name="UI" skip_longhands="${skip_ui_longhands}"> + ${impl_coordinated_property('transition', 'delay', 'Delay')} + ${impl_coordinated_property('transition', 'duration', 'Duration')} + ${impl_coordinated_property('transition', 'timing_function', 'TimingFunction')} + ${impl_coordinated_property('transition', 'property', 'Property')} + + pub fn transition_combined_duration_at(&self, index: usize) -> Time { + // https://drafts.csswg.org/css-transitions/#transition-combined-duration + Time::from_seconds( + self.transition_duration_at(index).seconds().max(0.0) + + self.transition_delay_at(index).seconds() + ) + } + + /// Returns whether there are any transitions specified. + pub fn specifies_transitions(&self) -> bool { + if self.mTransitionPropertyCount == 1 && + self.transition_combined_duration_at(0).seconds() <= 0.0f32 { + return false; + } + self.mTransitionPropertyCount > 0 + } + + pub fn animations_equals(&self, other: &Self) -> bool { + return self.mAnimationNameCount == other.mAnimationNameCount + && self.mAnimationDelayCount == other.mAnimationDelayCount + && self.mAnimationDirectionCount == other.mAnimationDirectionCount + && self.mAnimationDurationCount == other.mAnimationDurationCount + && self.mAnimationFillModeCount == other.mAnimationFillModeCount + && self.mAnimationIterationCountCount == other.mAnimationIterationCountCount + && self.mAnimationPlayStateCount == other.mAnimationPlayStateCount + && self.mAnimationTimingFunctionCount == other.mAnimationTimingFunctionCount + && self.mAnimationCompositionCount == other.mAnimationCompositionCount + && self.mAnimationTimelineCount == other.mAnimationTimelineCount + && unsafe { bindings::Gecko_StyleAnimationsEquals(&self.mAnimations, &other.mAnimations) } + } + + ${impl_coordinated_property('animation', 'name', 'Name')} + ${impl_coordinated_property('animation', 'delay', 'Delay')} + ${impl_coordinated_property('animation', 'duration', 'Duration')} + ${impl_coordinated_property('animation', 'direction', 'Direction')} + ${impl_coordinated_property('animation', 'fill_mode', 'FillMode')} + ${impl_coordinated_property('animation', 'play_state', 'PlayState')} + ${impl_coordinated_property('animation', 'composition', 'Composition')} + ${impl_coordinated_property('animation', 'iteration_count', 'IterationCount')} + ${impl_coordinated_property('animation', 'timeline', 'Timeline')} + ${impl_coordinated_property('animation', 'timing_function', 'TimingFunction')} + + ${impl_coordinated_property('scroll_timeline', 'name', 'Name')} + ${impl_coordinated_property('scroll_timeline', 'axis', 'Axis')} + + pub fn scroll_timelines_equals(&self, other: &Self) -> bool { + self.mScrollTimelineNameCount == other.mScrollTimelineNameCount + && self.mScrollTimelineAxisCount == other.mScrollTimelineAxisCount + && unsafe { + bindings::Gecko_StyleScrollTimelinesEquals( + &self.mScrollTimelines, + &other.mScrollTimelines, + ) + } + } + + ${impl_coordinated_property('view_timeline', 'name', 'Name')} + ${impl_coordinated_property('view_timeline', 'axis', 'Axis')} + ${impl_coordinated_property('view_timeline', 'inset', 'Inset')} + + pub fn view_timelines_equals(&self, other: &Self) -> bool { + self.mViewTimelineNameCount == other.mViewTimelineNameCount + && self.mViewTimelineAxisCount == other.mViewTimelineAxisCount + && self.mViewTimelineInsetCount == other.mViewTimelineInsetCount + && unsafe { + bindings::Gecko_StyleViewTimelinesEquals( + &self.mViewTimelines, + &other.mViewTimelines, + ) + } + } +</%self:impl_trait> + +<%self:impl_trait style_struct_name="XUL"> +</%self:impl_trait> + +% for style_struct in data.style_structs: +${impl_style_struct(style_struct)} +% endfor + +/// Assert that the initial values set in Gecko style struct constructors +/// match the values returned by `get_initial_value()` for each longhand. +#[cfg(feature = "gecko")] +#[inline] +pub fn assert_initial_values_match(data: &PerDocumentStyleData) { + if cfg!(debug_assertions) { + let data = data.borrow(); + let cv = data.stylist.device().default_computed_values(); + <% + # Skip properties with initial values that change at computed + # value time, or whose initial value depends on the document + # / other prefs. + SKIPPED = [ + "border-top-width", + "border-bottom-width", + "border-left-width", + "border-right-width", + "column-rule-width", + "font-family", + "font-size", + "outline-width", + "color", + ] + TO_TEST = [p for p in data.longhands if p.enabled_in != "" and not p.logical and not p.name in SKIPPED] + %> + % for property in TO_TEST: + assert_eq!( + cv.clone_${property.ident}(), + longhands::${property.ident}::get_initial_value(), + concat!( + "initial value in Gecko style struct for ", + stringify!(${property.ident}), + " must match longhands::", + stringify!(${property.ident}), + "::get_initial_value()" + ) + ); + % endfor + } +} diff --git a/servo/components/style/properties/helpers.mako.rs b/servo/components/style/properties/helpers.mako.rs new file mode 100644 index 0000000000..968a97aa00 --- /dev/null +++ b/servo/components/style/properties/helpers.mako.rs @@ -0,0 +1,909 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%! + from data import Keyword, to_rust_ident, to_phys, to_camel_case, SYSTEM_FONT_LONGHANDS + from data import (LOGICAL_CORNERS, PHYSICAL_CORNERS, LOGICAL_SIDES, + PHYSICAL_SIDES, LOGICAL_SIZES, LOGICAL_AXES) +%> + +<%def name="predefined_type(name, type, initial_value, parse_method='parse', + vector=False, none_value=None, initial_specified_value=None, + allow_quirks='No', **kwargs)"> + <%def name="predefined_type_inner(name, type, initial_value, parse_method)"> + #[allow(unused_imports)] + use app_units::Au; + #[allow(unused_imports)] + use crate::values::specified::AllowQuirks; + #[allow(unused_imports)] + use crate::Zero; + #[allow(unused_imports)] + use smallvec::SmallVec; + pub use crate::values::specified::${type} as SpecifiedValue; + pub mod computed_value { + pub use crate::values::computed::${type} as T; + } + % if initial_value: + #[inline] pub fn get_initial_value() -> computed_value::T { ${initial_value} } + % endif + % if initial_specified_value: + #[inline] pub fn get_initial_specified_value() -> SpecifiedValue { ${initial_specified_value} } + % endif + #[allow(unused_variables)] + #[inline] + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SpecifiedValue, ParseError<'i>> { + % if allow_quirks != "No": + specified::${type}::${parse_method}_quirky(context, input, AllowQuirks::${allow_quirks}) + % elif parse_method != "parse": + specified::${type}::${parse_method}(context, input) + % else: + <specified::${type} as crate::parser::Parse>::parse(context, input) + % endif + } + </%def> + % if vector: + <%call + expr="vector_longhand(name, predefined_type=type, allow_empty=not initial_value, none_value=none_value, **kwargs)" + > + ${predefined_type_inner(name, type, initial_value, parse_method)} + % if caller: + ${caller.body()} + % endif + </%call> + % else: + <%call expr="longhand(name, predefined_type=type, **kwargs)"> + ${predefined_type_inner(name, type, initial_value, parse_method)} + % if caller: + ${caller.body()} + % endif + </%call> + % endif +</%def> + +// The setup here is roughly: +// +// * UnderlyingList is the list that is stored in the computed value. This may +// be a shared ArcSlice if the property is inherited. +// * UnderlyingOwnedList is the list that is used for animation. +// * Specified values always use OwnedSlice, since it's more compact. +// * computed_value::List is just a convenient alias that you can use for the +// computed value list, since this is in the computed_value module. +// +// If simple_vector_bindings is true, then we don't use the complex iterator +// machinery and set_foo_from, and just compute the value like any other +// longhand. +<%def name="vector_longhand(name, animation_value_type=None, + vector_animation_type=None, allow_empty=False, + none_value=None, + simple_vector_bindings=False, + separator='Comma', + **kwargs)"> + <%call expr="longhand(name, animation_value_type=animation_value_type, vector=True, + simple_vector_bindings=simple_vector_bindings, **kwargs)"> + #[allow(unused_imports)] + use smallvec::SmallVec; + + pub mod single_value { + #[allow(unused_imports)] + use cssparser::{Parser, BasicParseError}; + #[allow(unused_imports)] + use crate::parser::{Parse, ParserContext}; + #[allow(unused_imports)] + use crate::properties::ShorthandId; + #[allow(unused_imports)] + use selectors::parser::SelectorParseErrorKind; + #[allow(unused_imports)] + use style_traits::{ParseError, StyleParseErrorKind}; + #[allow(unused_imports)] + use crate::values::computed::{Context, ToComputedValue}; + #[allow(unused_imports)] + use crate::values::{computed, specified}; + ${caller.body()} + } + + /// The definition of the computed value for ${name}. + pub mod computed_value { + #[allow(unused_imports)] + use crate::values::animated::ToAnimatedValue; + #[allow(unused_imports)] + use crate::values::resolved::ToResolvedValue; + pub use super::single_value::computed_value as single_value; + pub use self::single_value::T as SingleComputedValue; + % if not allow_empty: + use smallvec::SmallVec; + % endif + use crate::values::computed::ComputedVecIter; + + <% + is_shared_list = allow_empty and \ + data.longhands_by_name[name].style_struct.inherited + %> + + // FIXME(emilio): Add an OwnedNonEmptySlice type, and figure out + // something for transition-name, which is the only remaining user + // of NotInitial. + pub type UnderlyingList<T> = + % if allow_empty: + % if data.longhands_by_name[name].style_struct.inherited: + crate::ArcSlice<T>; + % else: + crate::OwnedSlice<T>; + % endif + % else: + SmallVec<[T; 1]>; + % endif + + pub type UnderlyingOwnedList<T> = + % if allow_empty: + crate::OwnedSlice<T>; + % else: + SmallVec<[T; 1]>; + % endif + + + /// The generic type defining the animated and resolved values for + /// this property. + /// + /// Making this type generic allows the compiler to figure out the + /// animated value for us, instead of having to implement it + /// manually for every type we care about. + #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToResolvedValue, ToCss)] + % if separator == "Comma": + #[css(comma)] + % endif + pub struct OwnedList<T>( + % if not allow_empty: + #[css(iterable)] + % else: + #[css(if_empty = "none", iterable)] + % endif + pub UnderlyingOwnedList<T>, + ); + + /// The computed value for this property. + % if not is_shared_list: + pub type ComputedList = OwnedList<single_value::T>; + pub use self::OwnedList as List; + % else: + pub use self::ComputedList as List; + + #[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)] + % if separator == "Comma": + #[css(comma)] + % endif + pub struct ComputedList( + % if not allow_empty: + #[css(iterable)] + % else: + #[css(if_empty = "none", iterable)] + % endif + % if is_shared_list: + #[ignore_malloc_size_of = "Arc"] + % endif + pub UnderlyingList<single_value::T>, + ); + + type ResolvedList = OwnedList<<single_value::T as ToResolvedValue>::ResolvedValue>; + impl ToResolvedValue for ComputedList { + type ResolvedValue = ResolvedList; + + fn to_resolved_value(self, context: &crate::values::resolved::Context) -> Self::ResolvedValue { + OwnedList( + self.0 + .iter() + .cloned() + .map(|v| v.to_resolved_value(context)) + .collect() + ) + } + + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + % if not is_shared_list: + use std::iter::FromIterator; + % endif + let iter = + resolved.0.into_iter().map(ToResolvedValue::from_resolved_value); + ComputedList(UnderlyingList::from_iter(iter)) + } + } + % endif + + % if simple_vector_bindings: + impl From<ComputedList> for UnderlyingList<single_value::T> { + #[inline] + fn from(l: ComputedList) -> Self { + l.0 + } + } + impl From<UnderlyingList<single_value::T>> for ComputedList { + #[inline] + fn from(l: UnderlyingList<single_value::T>) -> Self { + List(l) + } + } + % endif + + % if vector_animation_type: + % if not animation_value_type: + Sorry, this is stupid but needed for now. + % endif + + use crate::values::animated::{Animate, ToAnimatedZero, Procedure, lists}; + use crate::values::distance::{SquaredDistance, ComputeSquaredDistance}; + + // FIXME(emilio): For some reason rust thinks that this alias is + // unused, even though it's clearly used below? + #[allow(unused)] + type AnimatedList = OwnedList<<single_value::T as ToAnimatedValue>::AnimatedValue>; + + % if is_shared_list: + impl ToAnimatedValue for ComputedList { + type AnimatedValue = AnimatedList; + + fn to_animated_value(self) -> Self::AnimatedValue { + OwnedList( + self.0.iter().map(|v| v.clone().to_animated_value()).collect() + ) + } + + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + let iter = + animated.0.into_iter().map(ToAnimatedValue::from_animated_value); + ComputedList(UnderlyingList::from_iter(iter)) + } + } + % endif + + impl ToAnimatedZero for AnimatedList { + fn to_animated_zero(&self) -> Result<Self, ()> { Err(()) } + } + + impl Animate for AnimatedList { + fn animate( + &self, + other: &Self, + procedure: Procedure, + ) -> Result<Self, ()> { + Ok(OwnedList( + lists::${vector_animation_type}::animate(&self.0, &other.0, procedure)? + )) + } + } + impl ComputeSquaredDistance for AnimatedList { + fn compute_squared_distance( + &self, + other: &Self, + ) -> Result<SquaredDistance, ()> { + lists::${vector_animation_type}::squared_distance(&self.0, &other.0) + } + } + % endif + + /// The computed value, effectively a list of single values. + pub use self::ComputedList as T; + + pub type Iter<'a, 'cx, 'cx_a> = ComputedVecIter<'a, 'cx, 'cx_a, super::single_value::SpecifiedValue>; + } + + /// The specified value of ${name}. + #[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] + % if none_value: + #[value_info(other_values = "none")] + % endif + % if separator == "Comma": + #[css(comma)] + % endif + pub struct SpecifiedValue( + % if not allow_empty: + #[css(iterable)] + % else: + #[css(if_empty = "none", iterable)] + % endif + pub crate::OwnedSlice<single_value::SpecifiedValue>, + ); + + pub fn get_initial_value() -> computed_value::T { + % if allow_empty: + computed_value::List(Default::default()) + % else: + let mut v = SmallVec::new(); + v.push(single_value::get_initial_value()); + computed_value::List(v) + % endif + } + + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SpecifiedValue, ParseError<'i>> { + use style_traits::Separator; + + % if allow_empty or none_value: + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + % if allow_empty: + return Ok(SpecifiedValue(Default::default())) + % else: + return Ok(SpecifiedValue(crate::OwnedSlice::from(vec![${none_value}]))) + % endif + } + % endif + + let v = style_traits::${separator}::parse(input, |parser| { + single_value::parse(context, parser) + })?; + Ok(SpecifiedValue(v.into())) + } + + pub use self::single_value::SpecifiedValue as SingleSpecifiedValue; + + % if not simple_vector_bindings and engine == "gecko": + impl SpecifiedValue { + fn compute_iter<'a, 'cx, 'cx_a>( + &'a self, + context: &'cx Context<'cx_a>, + ) -> computed_value::Iter<'a, 'cx, 'cx_a> { + computed_value::Iter::new(context, &self.0) + } + } + % endif + + impl ToComputedValue for SpecifiedValue { + type ComputedValue = computed_value::T; + + #[inline] + fn to_computed_value(&self, context: &Context) -> computed_value::T { + % if not is_shared_list: + use std::iter::FromIterator; + % endif + computed_value::List(computed_value::UnderlyingList::from_iter( + self.0.iter().map(|i| i.to_computed_value(context)) + )) + } + + #[inline] + fn from_computed_value(computed: &computed_value::T) -> Self { + let iter = computed.0.iter().map(ToComputedValue::from_computed_value); + SpecifiedValue(iter.collect()) + } + } + </%call> +</%def> +<%def name="longhand(*args, **kwargs)"> + <% + property = data.declare_longhand(*args, **kwargs) + if property is None: + return "" + %> + /// ${property.spec} + pub mod ${property.ident} { + #[allow(unused_imports)] + use cssparser::{Parser, BasicParseError, Token}; + #[allow(unused_imports)] + use crate::parser::{Parse, ParserContext}; + #[allow(unused_imports)] + use crate::properties::{UnparsedValue, ShorthandId}; + #[allow(unused_imports)] + use crate::error_reporting::ParseErrorReporter; + #[allow(unused_imports)] + use crate::properties::longhands; + #[allow(unused_imports)] + use crate::properties::{LonghandId, LonghandIdSet}; + #[allow(unused_imports)] + use crate::properties::{CSSWideKeyword, ComputedValues, PropertyDeclaration}; + #[allow(unused_imports)] + use crate::properties::style_structs; + #[allow(unused_imports)] + use selectors::parser::SelectorParseErrorKind; + #[allow(unused_imports)] + use servo_arc::Arc; + #[allow(unused_imports)] + use style_traits::{ParseError, StyleParseErrorKind}; + #[allow(unused_imports)] + use crate::values::computed::{Context, ToComputedValue}; + #[allow(unused_imports)] + use crate::values::{computed, generics, specified}; + #[allow(unused_imports)] + use crate::Atom; + ${caller.body()} + #[allow(unused_variables)] + pub unsafe fn cascade_property( + declaration: &PropertyDeclaration, + context: &mut computed::Context, + ) { + % if property.logical: + declaration.debug_crash("Should physicalize before entering here"); + % else: + context.for_non_inherited_property = ${"false" if property.style_struct.inherited else "true"}; + % if property.logical_group: + debug_assert_eq!( + declaration.id().as_longhand().unwrap().logical_group(), + LonghandId::${property.camel_case}.logical_group(), + ); + % else: + debug_assert_eq!( + declaration.id().as_longhand().unwrap(), + LonghandId::${property.camel_case}, + ); + % endif + let specified_value = match *declaration { + PropertyDeclaration::CSSWideKeyword(ref wk) => { + match wk.keyword { + % if not property.style_struct.inherited: + CSSWideKeyword::Unset | + % endif + CSSWideKeyword::Initial => { + % if not property.style_struct.inherited: + declaration.debug_crash("Unexpected initial or unset for non-inherited property"); + % else: + context.builder.reset_${property.ident}(); + % endif + }, + % if property.style_struct.inherited: + CSSWideKeyword::Unset | + % endif + CSSWideKeyword::Inherit => { + % if property.style_struct.inherited: + declaration.debug_crash("Unexpected inherit or unset for inherited property"); + % else: + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + context.builder.inherit_${property.ident}(); + % endif + } + CSSWideKeyword::RevertLayer | + CSSWideKeyword::Revert => { + declaration.debug_crash("Found revert/revert-layer not deal with"); + }, + } + return; + }, + #[cfg(debug_assertions)] + PropertyDeclaration::WithVariables(..) => { + declaration.debug_crash("Found variables not substituted"); + return; + }, + _ => unsafe { + declaration.unchecked_value_as::<${property.specified_type()}>() + }, + }; + + % if property.ident in SYSTEM_FONT_LONGHANDS and engine == "gecko": + if let Some(sf) = specified_value.get_system() { + longhands::system_font::resolve_system_font(sf, context); + } + % endif + + % if property.is_vector and not property.simple_vector_bindings and engine == "gecko": + // In the case of a vector property we want to pass down an + // iterator so that this can be computed without allocation. + // + // However, computing requires a context, but the style struct + // being mutated is on the context. We temporarily remove it, + // mutate it, and then put it back. Vector longhands cannot + // touch their own style struct whilst computing, else this will + // panic. + let mut s = + context.builder.take_${data.current_style_struct.name_lower}(); + { + let iter = specified_value.compute_iter(context); + s.set_${property.ident}(iter); + } + context.builder.put_${data.current_style_struct.name_lower}(s); + % else: + % if property.boxed: + let computed = (**specified_value).to_computed_value(context); + % else: + let computed = specified_value.to_computed_value(context); + % endif + context.builder.set_${property.ident}(computed) + % endif + % endif + } + + pub fn parse_declared<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<PropertyDeclaration, ParseError<'i>> { + % if property.allow_quirks != "No": + parse_quirky(context, input, specified::AllowQuirks::${property.allow_quirks}) + % else: + parse(context, input) + % endif + % if property.boxed: + .map(Box::new) + % endif + .map(PropertyDeclaration::${property.camel_case}) + } + } +</%def> + +<%def name="gecko_keyword_conversion(keyword, values=None, type='SpecifiedValue', cast_to=None)"> + <% + if not values: + values = keyword.values_for(engine) + maybe_cast = "as %s" % cast_to if cast_to else "" + const_type = cast_to if cast_to else "u32" + %> + #[cfg(feature = "gecko")] + impl ${type} { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u32) -> Self { + use crate::gecko_bindings::structs; + % for value in values: + // We can't match on enum values if we're matching on a u32 + const ${to_rust_ident(value).upper()}: ${const_type} + = structs::${keyword.gecko_constant(value)} as ${const_type}; + % endfor + match kw ${maybe_cast} { + % for value in values: + ${to_rust_ident(value).upper()} => ${type}::${to_camel_case(value)}, + % endfor + _ => panic!("Found unexpected value in style struct for ${keyword.name} property"), + } + } + } +</%def> + +<%def name="gecko_bitflags_conversion(bit_map, gecko_bit_prefix, type, kw_type='u8')"> + #[cfg(feature = "gecko")] + impl ${type} { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: ${kw_type}) -> Self { + % for gecko_bit in bit_map.values(): + use crate::gecko_bindings::structs::${gecko_bit_prefix}${gecko_bit}; + % endfor + + let mut bits = ${type}::empty(); + % for servo_bit, gecko_bit in bit_map.items(): + if kw & (${gecko_bit_prefix}${gecko_bit} as ${kw_type}) != 0 { + bits |= ${servo_bit}; + } + % endfor + bits + } + + pub fn to_gecko_keyword(self) -> ${kw_type} { + % for gecko_bit in bit_map.values(): + use crate::gecko_bindings::structs::${gecko_bit_prefix}${gecko_bit}; + % endfor + + let mut bits: ${kw_type} = 0; + // FIXME: if we ensure that the Servo bitflags storage is the same + // as Gecko's one, we can just copy it. + % for servo_bit, gecko_bit in bit_map.items(): + if self.contains(${servo_bit}) { + bits |= ${gecko_bit_prefix}${gecko_bit} as ${kw_type}; + } + % endfor + bits + } + } +</%def> + +<%def name="single_keyword(name, values, vector=False, + needs_conversion=False, **kwargs)"> + <% + keyword_kwargs = {a: kwargs.pop(a, None) for a in [ + 'gecko_constant_prefix', + 'gecko_enum_prefix', + 'extra_gecko_values', + 'extra_servo_2013_values', + 'extra_servo_2020_values', + 'gecko_aliases', + 'servo_2013_aliases', + 'servo_2020_aliases', + 'custom_consts', + 'gecko_inexhaustive', + 'gecko_strip_moz_prefix', + ]} + %> + + <%def name="inner_body(keyword, needs_conversion=False)"> + pub use self::computed_value::T as SpecifiedValue; + pub mod computed_value { + #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] + #[derive(Clone, Copy, Debug, Eq, FromPrimitive, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] + pub enum T { + % for variant in keyword.values_for(engine): + <% + aliases = [] + for alias, v in keyword.aliases_for(engine).items(): + if variant == v: + aliases.append(alias) + %> + % if aliases: + #[parse(aliases = "${','.join(sorted(aliases))}")] + % endif + ${to_camel_case(variant)}, + % endfor + } + } + #[inline] + pub fn get_initial_value() -> computed_value::T { + computed_value::T::${to_camel_case(values.split()[0])} + } + #[inline] + pub fn get_initial_specified_value() -> SpecifiedValue { + SpecifiedValue::${to_camel_case(values.split()[0])} + } + #[inline] + pub fn parse<'i, 't>(_context: &ParserContext, input: &mut Parser<'i, 't>) + -> Result<SpecifiedValue, ParseError<'i>> { + SpecifiedValue::parse(input) + } + + % if needs_conversion: + <% + conversion_values = keyword.values_for(engine) + list(keyword.aliases_for(engine).keys()) + %> + ${gecko_keyword_conversion(keyword, values=conversion_values)} + % endif + </%def> + % if vector: + <%call expr="vector_longhand(name, keyword=Keyword(name, values, **keyword_kwargs), **kwargs)"> + ${inner_body(Keyword(name, values, **keyword_kwargs))} + % if caller: + ${caller.body()} + % endif + </%call> + % else: + <%call expr="longhand(name, keyword=Keyword(name, values, **keyword_kwargs), **kwargs)"> + ${inner_body(Keyword(name, values, **keyword_kwargs), + needs_conversion=needs_conversion)} + % if caller: + ${caller.body()} + % endif + </%call> + % endif +</%def> + +<%def name="shorthand(name, sub_properties, derive_serialize=False, + derive_value_info=True, **kwargs)"> +<% + shorthand = data.declare_shorthand(name, sub_properties.split(), **kwargs) + # mako doesn't accept non-string value in parameters with <% %> form, so + # we have to workaround it this way. + if not isinstance(derive_value_info, bool): + derive_value_info = eval(derive_value_info) +%> + % if shorthand: + /// ${shorthand.spec} + pub mod ${shorthand.ident} { + use cssparser::Parser; + use crate::parser::ParserContext; + use crate::properties::{PropertyDeclaration, SourcePropertyDeclaration, MaybeBoxed, longhands}; + #[allow(unused_imports)] + use selectors::parser::SelectorParseErrorKind; + #[allow(unused_imports)] + use std::fmt::{self, Write}; + #[allow(unused_imports)] + use style_traits::{ParseError, StyleParseErrorKind}; + #[allow(unused_imports)] + use style_traits::{CssWriter, KeywordsCollectFn, SpecifiedValueInfo, ToCss}; + + % if derive_value_info: + #[derive(SpecifiedValueInfo)] + % endif + pub struct Longhands { + % for sub_property in shorthand.sub_properties: + pub ${sub_property.ident}: + % if sub_property.boxed: + Box< + % endif + longhands::${sub_property.ident}::SpecifiedValue + % if sub_property.boxed: + > + % endif + , + % endfor + } + + /// Represents a serializable set of all of the longhand properties that + /// correspond to a shorthand. + % if derive_serialize: + #[derive(ToCss)] + % endif + pub struct LonghandsToSerialize<'a> { + % for sub_property in shorthand.sub_properties: + pub ${sub_property.ident}: + % if sub_property.may_be_disabled_in(shorthand, engine): + Option< + % endif + &'a longhands::${sub_property.ident}::SpecifiedValue, + % if sub_property.may_be_disabled_in(shorthand, engine): + >, + % endif + % endfor + } + + impl<'a> LonghandsToSerialize<'a> { + /// Tries to get a serializable set of longhands given a set of + /// property declarations. + pub fn from_iter(iter: impl Iterator<Item = &'a PropertyDeclaration>) -> Result<Self, ()> { + // Define all of the expected variables that correspond to the shorthand + % for sub_property in shorthand.sub_properties: + let mut ${sub_property.ident} = + None::< &'a longhands::${sub_property.ident}::SpecifiedValue>; + % endfor + + // Attempt to assign the incoming declarations to the expected variables + for declaration in iter { + match *declaration { + % for sub_property in shorthand.sub_properties: + PropertyDeclaration::${sub_property.camel_case}(ref value) => { + ${sub_property.ident} = Some(value) + }, + % endfor + _ => {} + }; + } + + // If any of the expected variables are missing, return an error + match ( + % for sub_property in shorthand.sub_properties: + ${sub_property.ident}, + % endfor + ) { + + ( + % for sub_property in shorthand.sub_properties: + % if sub_property.may_be_disabled_in(shorthand, engine): + ${sub_property.ident}, + % else: + Some(${sub_property.ident}), + % endif + % endfor + ) => + Ok(LonghandsToSerialize { + % for sub_property in shorthand.sub_properties: + ${sub_property.ident}, + % endfor + }), + _ => Err(()) + } + } + } + + /// Parse the given shorthand and fill the result into the + /// `declarations` vector. + pub fn parse_into<'i, 't>( + declarations: &mut SourcePropertyDeclaration, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + #[allow(unused_imports)] + use crate::properties::{NonCustomPropertyId, LonghandId}; + input.parse_entirely(|input| parse_value(context, input)).map(|longhands| { + % for sub_property in shorthand.sub_properties: + % if sub_property.may_be_disabled_in(shorthand, engine): + if NonCustomPropertyId::from(LonghandId::${sub_property.camel_case}) + .allowed_in_ignoring_rule_type(context) { + % endif + declarations.push(PropertyDeclaration::${sub_property.camel_case}( + longhands.${sub_property.ident} + )); + % if sub_property.may_be_disabled_in(shorthand, engine): + } + % endif + % endfor + }) + } + + /// Try to serialize a given shorthand to a string. + pub fn to_css(declarations: &[&PropertyDeclaration], dest: &mut crate::str::CssStringWriter) -> fmt::Result { + match LonghandsToSerialize::from_iter(declarations.iter().cloned()) { + Ok(longhands) => longhands.to_css(&mut CssWriter::new(dest)), + Err(_) => Ok(()) + } + } + + ${caller.body()} + } + % endif +</%def> + +// A shorthand of kind `<property-1> <property-2>?` where both properties have +// the same type. +<%def name="two_properties_shorthand( + name, + first_property, + second_property, + parser_function='crate::parser::Parse::parse', + **kwargs +)"> +<%call expr="self.shorthand(name, sub_properties=' '.join([first_property, second_property]), **kwargs)"> + #[allow(unused_imports)] + use crate::parser::Parse; + #[allow(unused_imports)] + use crate::values::specified; + + fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let parse_one = |c: &ParserContext, input: &mut Parser<'i, 't>| -> Result< + crate::properties::longhands::${to_rust_ident(first_property)}::SpecifiedValue, + ParseError<'i> + > { + ${parser_function}(c, input) + }; + + let first = parse_one(context, input)?; + let second = + input.try_parse(|input| parse_one(context, input)).unwrap_or_else(|_| first.clone()); + Ok(expanded! { + ${to_rust_ident(first_property)}: first, + ${to_rust_ident(second_property)}: second, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let first = &self.${to_rust_ident(first_property)}; + let second = &self.${to_rust_ident(second_property)}; + + first.to_css(dest)?; + if first != second { + dest.write_char(' ')?; + second.to_css(dest)?; + } + Ok(()) + } + } +</%call> +</%def> + +<%def name="four_sides_shorthand(name, sub_property_pattern, + parser_function='crate::parser::Parse::parse', + allow_quirks='No', **kwargs)"> + <% sub_properties=' '.join(sub_property_pattern % side for side in PHYSICAL_SIDES) %> + <%call expr="self.shorthand(name, sub_properties=sub_properties, **kwargs)"> + #[allow(unused_imports)] + use crate::parser::Parse; + use crate::values::generics::rect::Rect; + #[allow(unused_imports)] + use crate::values::specified; + + fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let rect = Rect::parse_with(context, input, |c, i| -> Result< + crate::properties::longhands::${to_rust_ident(sub_property_pattern % "top")}::SpecifiedValue, + ParseError<'i> + > { + % if allow_quirks != "No": + ${parser_function}_quirky(c, i, specified::AllowQuirks::${allow_quirks}) + % else: + ${parser_function}(c, i) + % endif + })?; + Ok(expanded! { + % for index, side in enumerate(["top", "right", "bottom", "left"]): + ${to_rust_ident(sub_property_pattern % side)}: rect.${index}, + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let rect = Rect::new( + % for side in ["top", "right", "bottom", "left"]: + &self.${to_rust_ident(sub_property_pattern % side)}, + % endfor + ); + rect.to_css(dest) + } + } + </%call> +</%def> diff --git a/servo/components/style/properties/helpers/animated_properties.mako.rs b/servo/components/style/properties/helpers/animated_properties.mako.rs new file mode 100644 index 0000000000..290684cdab --- /dev/null +++ b/servo/components/style/properties/helpers/animated_properties.mako.rs @@ -0,0 +1,785 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% + from data import to_idl_name, SYSTEM_FONT_LONGHANDS, to_camel_case + from itertools import groupby +%> + +#[cfg(feature = "gecko")] use crate::gecko_bindings::structs::nsCSSPropertyID; +use crate::properties::{ + longhands::{ + self, content_visibility::computed_value::T as ContentVisibility, + visibility::computed_value::T as Visibility, + }, + CSSWideKeyword, NonCustomPropertyId, LonghandId, NonCustomPropertyIterator, + PropertyDeclaration, PropertyDeclarationId, +}; +use std::ptr; +use std::mem; +use fxhash::FxHashMap; +use super::ComputedValues; +use crate::properties::OwnedPropertyDeclarationId; +use crate::values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero}; +use crate::values::animated::effects::AnimatedFilter; +#[cfg(feature = "gecko")] use crate::values::computed::TransitionProperty; +use crate::values::computed::{ClipRect, Context}; +use crate::values::computed::ToComputedValue; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::effects::Filter; +use void::{self, Void}; +use crate::properties_and_values::value::CustomAnimatedValue; + +/// Convert nsCSSPropertyID to TransitionProperty +#[cfg(feature = "gecko")] +#[allow(non_upper_case_globals)] +impl From<nsCSSPropertyID> for TransitionProperty { + fn from(property: nsCSSPropertyID) -> TransitionProperty { + TransitionProperty::NonCustom(NonCustomPropertyId::from_nscsspropertyid(property).unwrap()) + } +} + +/// A collection of AnimationValue that were composed on an element. +/// This HashMap stores the values that are the last AnimationValue to be +/// composed for each TransitionProperty. +pub type AnimationValueMap = FxHashMap<OwnedPropertyDeclarationId, AnimationValue>; + +/// An enum to represent a single computed value belonging to an animated +/// property in order to be interpolated with another one. When interpolating, +/// both values need to belong to the same property. +#[derive(Debug, MallocSizeOf)] +#[repr(u16)] +pub enum AnimationValue { + % for prop in data.longhands: + /// `${prop.name}` + % if prop.animatable and not prop.logical: + ${prop.camel_case}(${prop.animated_type()}), + % else: + ${prop.camel_case}(Void), + % endif + % endfor + /// A custom property. + Custom(CustomAnimatedValue), +} + +<% + animated = [] + unanimated = [] + animated_with_logical = [] + for prop in data.longhands: + if prop.animatable: + animated_with_logical.append(prop) + if prop.animatable and not prop.logical: + animated.append(prop) + else: + unanimated.append(prop) +%> + +#[repr(C)] +struct AnimationValueVariantRepr<T> { + tag: u16, + value: T +} + +impl Clone for AnimationValue { + #[inline] + fn clone(&self) -> Self { + use self::AnimationValue::*; + + <% + [copy, others] = [list(g) for _, g in groupby(animated, key=lambda x: not x.specified_is_copy())] + %> + + let self_tag = unsafe { *(self as *const _ as *const u16) }; + if self_tag <= LonghandId::${copy[-1].camel_case} as u16 { + #[derive(Clone, Copy)] + #[repr(u16)] + enum CopyVariants { + % for prop in copy: + _${prop.camel_case}(${prop.animated_type()}), + % endfor + } + + unsafe { + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut CopyVariants, + *(self as *const _ as *const CopyVariants), + ); + return out.assume_init(); + } + } + + match *self { + % for ty, props in groupby(others, key=lambda x: x.animated_type()): + <% props = list(props) %> + ${" |\n".join("{}(ref value)".format(prop.camel_case) for prop in props)} => { + % if len(props) == 1: + ${props[0].camel_case}(value.clone()) + % else: + unsafe { + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut AnimationValueVariantRepr<${ty}>, + AnimationValueVariantRepr { + tag: *(self as *const _ as *const u16), + value: value.clone(), + }, + ); + out.assume_init() + } + % endif + } + % endfor + Custom(ref animated_value) => Custom(animated_value.clone()), + _ => unsafe { debug_unreachable!() } + } + } +} + +impl PartialEq for AnimationValue { + #[inline] + fn eq(&self, other: &Self) -> bool { + use self::AnimationValue::*; + + unsafe { + let this_tag = *(self as *const _ as *const u16); + let other_tag = *(other as *const _ as *const u16); + if this_tag != other_tag { + return false; + } + + match *self { + % for ty, props in groupby(animated, key=lambda x: x.animated_type()): + ${" |\n".join("{}(ref this)".format(prop.camel_case) for prop in props)} => { + let other_repr = + &*(other as *const _ as *const AnimationValueVariantRepr<${ty}>); + *this == other_repr.value + } + % endfor + ${" |\n".join("{}(void)".format(prop.camel_case) for prop in unanimated)} => { + void::unreachable(void) + }, + AnimationValue::Custom(ref this) => { + let other_repr = + &*(other as *const _ as *const AnimationValueVariantRepr<CustomAnimatedValue>); + *this == other_repr.value + }, + } + } + } +} + +impl AnimationValue { + /// Returns the longhand id this animated value corresponds to. + #[inline] + pub fn id(&self) -> PropertyDeclarationId { + if let AnimationValue::Custom(animated_value) = self { + return PropertyDeclarationId::Custom(&animated_value.name); + } + + let id = unsafe { *(self as *const _ as *const LonghandId) }; + debug_assert_eq!(id, match *self { + % for prop in data.longhands: + % if prop.animatable and not prop.logical: + AnimationValue::${prop.camel_case}(..) => LonghandId::${prop.camel_case}, + % else: + AnimationValue::${prop.camel_case}(void) => void::unreachable(void), + % endif + % endfor + AnimationValue::Custom(..) => unsafe { debug_unreachable!() }, + }); + PropertyDeclarationId::Longhand(id) + } + + /// Returns whether this value is interpolable with another one. + pub fn interpolable_with(&self, other: &Self) -> bool { + self.animate(other, Procedure::Interpolate { progress: 0.5 }).is_ok() + } + + /// "Uncompute" this animation value in order to be used inside the CSS + /// cascade. + pub fn uncompute(&self) -> PropertyDeclaration { + use crate::properties::longhands; + use self::AnimationValue::*; + + use super::PropertyDeclarationVariantRepr; + + match *self { + <% keyfunc = lambda x: (x.base_type(), x.specified_type(), x.boxed, x.is_animatable_with_computed_value) %> + % for (ty, specified, boxed, computed), props in groupby(animated, key=keyfunc): + <% props = list(props) %> + ${" |\n".join("{}(ref value)".format(prop.camel_case) for prop in props)} => { + % if not computed: + let ref value = ToAnimatedValue::from_animated_value(value.clone()); + % endif + let value = ${ty}::from_computed_value(&value); + % if boxed: + let value = Box::new(value); + % endif + % if len(props) == 1: + PropertyDeclaration::${props[0].camel_case}(value) + % else: + unsafe { + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut PropertyDeclarationVariantRepr<${specified}>, + PropertyDeclarationVariantRepr { + tag: *(self as *const _ as *const u16), + value, + }, + ); + out.assume_init() + } + % endif + } + % endfor + ${" |\n".join("{}(void)".format(prop.camel_case) for prop in unanimated)} => { + void::unreachable(void) + }, + Custom(ref animated_value) => animated_value.to_declaration(), + } + } + + /// Construct an AnimationValue from a property declaration. + pub fn from_declaration( + decl: &PropertyDeclaration, + context: &mut Context, + initial: &ComputedValues, + ) -> Option<Self> { + use super::PropertyDeclarationVariantRepr; + + <% + keyfunc = lambda x: ( + x.specified_type(), + x.animated_type(), + x.boxed, + not x.is_animatable_with_computed_value, + x.style_struct.inherited, + x.ident in SYSTEM_FONT_LONGHANDS and engine == "gecko", + ) + %> + + let animatable = match *decl { + % for (specified_ty, ty, boxed, to_animated, inherit, system), props in groupby(animated_with_logical, key=keyfunc): + ${" |\n".join("PropertyDeclaration::{}(ref value)".format(prop.camel_case) for prop in props)} => { + let decl_repr = unsafe { + &*(decl as *const _ as *const PropertyDeclarationVariantRepr<${specified_ty}>) + }; + let longhand_id = unsafe { + *(&decl_repr.tag as *const u16 as *const LonghandId) + }; + context.for_non_inherited_property = ${"false" if inherit else "true"}; + % if system: + if let Some(sf) = value.get_system() { + longhands::system_font::resolve_system_font(sf, context) + } + % endif + % if boxed: + let value = (**value).to_computed_value(context); + % else: + let value = value.to_computed_value(context); + % endif + % if to_animated: + let value = value.to_animated_value(); + % endif + + unsafe { + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut AnimationValueVariantRepr<${ty}>, + AnimationValueVariantRepr { + tag: longhand_id.to_physical(context.builder.writing_mode) as u16, + value, + }, + ); + out.assume_init() + } + } + % endfor + PropertyDeclaration::CSSWideKeyword(ref declaration) => { + match declaration.id.to_physical(context.builder.writing_mode) { + // We put all the animatable properties first in the hopes + // that it might increase match locality. + % for prop in data.longhands: + % if prop.animatable and not prop.logical: + LonghandId::${prop.camel_case} => { + // FIXME(emilio, bug 1533327): I think revert (and + // revert-layer) handling is not fine here, but what to + // do instead? + // + // Seems we'd need the computed value as if it was + // revert, somehow. Treating it as `unset` seems fine + // for now... + let style_struct = match declaration.keyword { + % if not prop.style_struct.inherited: + CSSWideKeyword::Revert | + CSSWideKeyword::RevertLayer | + CSSWideKeyword::Unset | + % endif + CSSWideKeyword::Initial => { + initial.get_${prop.style_struct.name_lower}() + }, + % if prop.style_struct.inherited: + CSSWideKeyword::Revert | + CSSWideKeyword::RevertLayer | + CSSWideKeyword::Unset | + % endif + CSSWideKeyword::Inherit => { + context.builder + .get_parent_${prop.style_struct.name_lower}() + }, + }; + let computed = style_struct + % if prop.logical: + .clone_${prop.ident}(context.builder.writing_mode); + % else: + .clone_${prop.ident}(); + % endif + + % if not prop.is_animatable_with_computed_value: + let computed = computed.to_animated_value(); + % endif + AnimationValue::${prop.camel_case}(computed) + }, + % endif + % endfor + % for prop in data.longhands: + % if not prop.animatable or prop.logical: + LonghandId::${prop.camel_case} => return None, + % endif + % endfor + } + }, + PropertyDeclaration::WithVariables(ref declaration) => { + let mut cache = Default::default(); + let substituted = { + let custom_properties = &context.style().custom_properties(); + + debug_assert!( + context.builder.stylist.is_some(), + "Need a Stylist to substitute variables!" + ); + declaration.value.substitute_variables( + declaration.id, + custom_properties, + context.builder.stylist.unwrap(), + context, + &mut cache, + ) + }; + return AnimationValue::from_declaration( + &substituted, + context, + initial, + ) + }, + PropertyDeclaration::Custom(ref declaration) => { + AnimationValue::Custom(CustomAnimatedValue::from_declaration( + declaration, + context, + initial, + )?) + }, + _ => return None // non animatable properties will get included because of shorthands. ignore. + }; + Some(animatable) + } + + /// Get an AnimationValue for an declaration id from a given computed values. + pub fn from_computed_values( + property: PropertyDeclarationId, + style: &ComputedValues, + ) -> Option<Self> { + let property = match property { + PropertyDeclarationId::Longhand(id) => id, + PropertyDeclarationId::Custom(ref name) => { + // FIXME(bug 1869476): This should use a stylist to determine whether the name + // corresponds to an inherited custom property and then choose the + // inherited/non_inherited map accordingly. + let p = &style.custom_properties(); + let value = p.inherited.get(*name).or_else(|| p.non_inherited.get(*name))?; + return Some(AnimationValue::Custom(CustomAnimatedValue::from_computed(name, value))) + } + }; + + Some(match property { + % for prop in data.longhands: + % if prop.animatable and not prop.logical: + LonghandId::${prop.camel_case} => { + let computed = style.clone_${prop.ident}(); + AnimationValue::${prop.camel_case}( + % if prop.is_animatable_with_computed_value: + computed + % else: + computed.to_animated_value() + % endif + ) + } + % endif + % endfor + _ => return None, + }) + } + + /// Update `style` with the value of this `AnimationValue`. + /// + /// SERVO ONLY: This doesn't properly handle things like updating 'em' units + /// when animated font-size. + #[cfg(feature = "servo")] + pub fn set_in_style_for_servo(&self, style: &mut ComputedValues) { + match self { + % for prop in data.longhands: + % if prop.animatable and not prop.logical: + AnimationValue::${prop.camel_case}(ref value) => { + % if not prop.is_animatable_with_computed_value: + let value: longhands::${prop.ident}::computed_value::T = + ToAnimatedValue::from_animated_value(value.clone()); + style.mutate_${prop.style_struct.name_lower}().set_${prop.ident}(value); + % else: + style.mutate_${prop.style_struct.name_lower}().set_${prop.ident}(value.clone()); + % endif + } + % else: + AnimationValue::${prop.camel_case}(..) => unreachable!(), + % endif + % endfor + AnimationValue::Custom(..) => unreachable!(), + } + } + + /// As above, but a stub for Gecko. + #[cfg(feature = "gecko")] + pub fn set_in_style_for_servo(&self, _: &mut ComputedValues) { + } +} + +fn animate_discrete<T: Clone>(this: &T, other: &T, procedure: Procedure) -> Result<T, ()> { + if let Procedure::Interpolate { progress } = procedure { + Ok(if progress < 0.5 { this.clone() } else { other.clone() }) + } else { + Err(()) + } +} + +impl Animate for AnimationValue { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(unsafe { + use self::AnimationValue::*; + + let this_tag = *(self as *const _ as *const u16); + let other_tag = *(other as *const _ as *const u16); + if this_tag != other_tag { + panic!("Unexpected AnimationValue::animate call"); + } + + match *self { + <% keyfunc = lambda x: (x.animated_type(), x.animation_value_type == "discrete") %> + % for (ty, discrete), props in groupby(animated, key=keyfunc): + ${" |\n".join("{}(ref this)".format(prop.camel_case) for prop in props)} => { + let other_repr = + &*(other as *const _ as *const AnimationValueVariantRepr<${ty}>); + % if discrete: + let value = animate_discrete(this, &other_repr.value, procedure)?; + % else: + let value = this.animate(&other_repr.value, procedure)?; + % endif + + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut AnimationValueVariantRepr<${ty}>, + AnimationValueVariantRepr { + tag: this_tag, + value, + }, + ); + out.assume_init() + }, + % endfor + ${" |\n".join("{}(void)".format(prop.camel_case) for prop in unanimated)} => { + void::unreachable(void) + }, + Custom(ref self_value) => { + let Custom(ref other_value) = *other else { unreachable!() }; + Custom(self_value.animate(other_value, procedure)?) + }, + } + }) + } +} + +<% + nondiscrete = [] + for prop in animated: + if prop.animation_value_type != "discrete": + nondiscrete.append(prop) +%> + +impl ComputeSquaredDistance for AnimationValue { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + unsafe { + use self::AnimationValue::*; + + let this_tag = *(self as *const _ as *const u16); + let other_tag = *(other as *const _ as *const u16); + if this_tag != other_tag { + panic!("Unexpected AnimationValue::compute_squared_distance call"); + } + + match *self { + % for ty, props in groupby(nondiscrete, key=lambda x: x.animated_type()): + ${" |\n".join("{}(ref this)".format(prop.camel_case) for prop in props)} => { + let other_repr = + &*(other as *const _ as *const AnimationValueVariantRepr<${ty}>); + + this.compute_squared_distance(&other_repr.value) + } + % endfor + _ => Err(()), + } + } + } +} + +impl ToAnimatedZero for AnimationValue { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + match *self { + % for prop in data.longhands: + % if prop.animatable and not prop.logical and prop.animation_value_type != "discrete": + AnimationValue::${prop.camel_case}(ref base) => { + Ok(AnimationValue::${prop.camel_case}(base.to_animated_zero()?)) + }, + % endif + % endfor + AnimationValue::Custom(..) => { + // TODO(bug 1869185): For some non-universal registered custom properties, it may make sense to implement this. + Err(()) + }, + _ => Err(()), + } + } +} + +/// <https://drafts.csswg.org/web-animations-1/#animating-visibility> +impl Animate for Visibility { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match procedure { + Procedure::Interpolate { .. } => { + let (this_weight, other_weight) = procedure.weights(); + match (*self, *other) { + (Visibility::Visible, _) => { + Ok(if this_weight > 0.0 { *self } else { *other }) + }, + (_, Visibility::Visible) => { + Ok(if other_weight > 0.0 { *other } else { *self }) + }, + _ => Err(()), + } + }, + _ => Err(()), + } + } +} + +impl ComputeSquaredDistance for Visibility { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(SquaredDistance::from_sqrt(if *self == *other { 0. } else { 1. })) + } +} + +impl ToAnimatedZero for Visibility { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +/// <https://drafts.csswg.org/css-contain-3/#content-visibility-animation> +impl Animate for ContentVisibility { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match procedure { + Procedure::Interpolate { .. } => { + let (this_weight, other_weight) = procedure.weights(); + match (*self, *other) { + (ContentVisibility::Hidden, _) => { + Ok(if other_weight > 0.0 { *other } else { *self }) + }, + (_, ContentVisibility::Hidden) => { + Ok(if this_weight > 0.0 { *self } else { *other }) + }, + _ => Err(()), + } + }, + _ => Err(()), + } + } +} + +impl ComputeSquaredDistance for ContentVisibility { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(SquaredDistance::from_sqrt(if *self == *other { 0. } else { 1. })) + } +} + +impl ToAnimatedZero for ContentVisibility { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +/// <https://drafts.csswg.org/css-transitions/#animtype-rect> +impl Animate for ClipRect { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use crate::values::computed::LengthOrAuto; + let animate_component = |this: &LengthOrAuto, other: &LengthOrAuto| { + let result = this.animate(other, procedure)?; + if let Procedure::Interpolate { .. } = procedure { + return Ok(result); + } + if result.is_auto() { + // FIXME(emilio): Why? A couple SMIL tests fail without this, + // but it seems extremely fishy. + return Err(()); + } + Ok(result) + }; + + Ok(ClipRect { + top: animate_component(&self.top, &other.top)?, + right: animate_component(&self.right, &other.right)?, + bottom: animate_component(&self.bottom, &other.bottom)?, + left: animate_component(&self.left, &other.left)?, + }) + } +} + +<% + FILTER_FUNCTIONS = [ 'Blur', 'Brightness', 'Contrast', 'Grayscale', + 'HueRotate', 'Invert', 'Opacity', 'Saturate', + 'Sepia' ] +%> + +/// <https://drafts.fxtf.org/filters/#animation-of-filters> +impl Animate for AnimatedFilter { + fn animate( + &self, + other: &Self, + procedure: Procedure, + ) -> Result<Self, ()> { + use crate::values::animated::animate_multiplicative_factor; + match (self, other) { + % for func in ['Blur', 'Grayscale', 'HueRotate', 'Invert', 'Sepia']: + (&Filter::${func}(ref this), &Filter::${func}(ref other)) => { + Ok(Filter::${func}(this.animate(other, procedure)?)) + }, + % endfor + % for func in ['Brightness', 'Contrast', 'Opacity', 'Saturate']: + (&Filter::${func}(this), &Filter::${func}(other)) => { + Ok(Filter::${func}(animate_multiplicative_factor(this, other, procedure)?)) + }, + % endfor + % if engine == "gecko": + (&Filter::DropShadow(ref this), &Filter::DropShadow(ref other)) => { + Ok(Filter::DropShadow(this.animate(other, procedure)?)) + }, + % endif + _ => Err(()), + } + } +} + +/// <http://dev.w3.org/csswg/css-transforms/#none-transform-animation> +impl ToAnimatedZero for AnimatedFilter { + fn to_animated_zero(&self) -> Result<Self, ()> { + match *self { + % for func in ['Blur', 'Grayscale', 'HueRotate', 'Invert', 'Sepia']: + Filter::${func}(ref this) => Ok(Filter::${func}(this.to_animated_zero()?)), + % endfor + % for func in ['Brightness', 'Contrast', 'Opacity', 'Saturate']: + Filter::${func}(_) => Ok(Filter::${func}(1.)), + % endfor + % if engine == "gecko": + Filter::DropShadow(ref this) => Ok(Filter::DropShadow(this.to_animated_zero()?)), + % endif + _ => Err(()), + } + } +} + +/// An iterator over all the properties that transition on a given style. +pub struct TransitionPropertyIterator<'a> { + style: &'a ComputedValues, + index_range: core::ops::Range<usize>, + longhand_iterator: Option<NonCustomPropertyIterator<LonghandId>>, +} + +impl<'a> TransitionPropertyIterator<'a> { + /// Create a `TransitionPropertyIterator` for the given style. + pub fn from_style(style: &'a ComputedValues) -> Self { + Self { + style, + index_range: 0..style.get_ui().transition_property_count(), + longhand_iterator: None, + } + } +} + +/// A single iteration of the TransitionPropertyIterator. +pub struct TransitionPropertyIteration { + /// The id of the longhand for this property. + pub longhand_id: LonghandId, + + /// The index of this property in the list of transition properties for this + /// iterator's style. + pub index: usize, +} + +impl<'a> Iterator for TransitionPropertyIterator<'a> { + type Item = TransitionPropertyIteration; + + fn next(&mut self) -> Option<Self::Item> { + use crate::values::computed::TransitionProperty; + loop { + if let Some(ref mut longhand_iterator) = self.longhand_iterator { + if let Some(longhand_id) = longhand_iterator.next() { + return Some(TransitionPropertyIteration { + longhand_id, + index: self.index_range.start - 1, + }); + } + self.longhand_iterator = None; + } + + let index = self.index_range.next()?; + match self.style.get_ui().transition_property_at(index) { + TransitionProperty::NonCustom(id) => { + match id.longhand_or_shorthand() { + Ok(longhand_id) => { + return Some(TransitionPropertyIteration { + longhand_id, + index, + }); + }, + Err(shorthand_id) => { + // In the other cases, we set up our state so that we are ready to + // compute the next value of the iterator and then loop (equivalent + // to calling self.next()). + self.longhand_iterator = Some(shorthand_id.longhands()); + }, + } + } + TransitionProperty::Custom(..) | TransitionProperty::Unsupported(..) => {} + } + } + } +} diff --git a/servo/components/style/properties/longhands/background.mako.rs b/servo/components/style/properties/longhands/background.mako.rs new file mode 100644 index 0000000000..48270f748e --- /dev/null +++ b/servo/components/style/properties/longhands/background.mako.rs @@ -0,0 +1,126 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("Background", inherited=False) %> + +${helpers.predefined_type( + "background-color", + "Color", + "computed::Color::TRANSPARENT_BLACK", + engines="gecko servo-2013 servo-2020", + initial_specified_value="SpecifiedValue::transparent()", + spec="https://drafts.csswg.org/css-backgrounds/#background-color", + animation_value_type="AnimatedColor", + ignored_when_colors_disabled=True, + allow_quirks="Yes", + flags="CAN_ANIMATE_ON_COMPOSITOR", + affects="paint", +)} + +${helpers.predefined_type( + "background-image", + "Image", + engines="gecko servo-2013 servo-2020", + initial_value="computed::Image::None", + initial_specified_value="specified::Image::None", + spec="https://drafts.csswg.org/css-backgrounds/#the-background-image", + vector="True", + animation_value_type="discrete", + ignored_when_colors_disabled="True", + affects="paint", +)} + +% for (axis, direction, initial) in [("x", "Horizontal", "left"), ("y", "Vertical", "top")]: + ${helpers.predefined_type( + "background-position-" + axis, + "position::" + direction + "Position", + "computed::LengthPercentage::zero_percent()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="SpecifiedValue::initial_specified_value()", + spec="https://drafts.csswg.org/css-backgrounds-4/#propdef-background-position-" + axis, + animation_value_type="ComputedValue", + vector=True, + vector_animation_type="repeatable_list", + affects="paint", + )} +% endfor + +${helpers.predefined_type( + "background-repeat", + "BackgroundRepeat", + "computed::BackgroundRepeat::repeat()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::BackgroundRepeat::repeat()", + animation_value_type="discrete", + vector=True, + spec="https://drafts.csswg.org/css-backgrounds/#the-background-repeat", + affects="paint", +)} + +${helpers.single_keyword( + "background-attachment", + "scroll" + (" fixed" if engine in ["gecko", "servo-2013"] else "") + (" local" if engine == "gecko" else ""), + engines="gecko servo-2013 servo-2020", + vector=True, + gecko_enum_prefix="StyleImageLayerAttachment", + spec="https://drafts.csswg.org/css-backgrounds/#the-background-attachment", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.single_keyword( + "background-clip", + "border-box padding-box content-box", + engines="gecko servo-2013 servo-2020", + extra_gecko_values="text", + vector=True, extra_prefixes="webkit", + gecko_enum_prefix="StyleGeometryBox", + gecko_inexhaustive=True, + spec="https://drafts.csswg.org/css-backgrounds/#the-background-clip", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.single_keyword( + "background-origin", + "padding-box border-box content-box", + engines="gecko servo-2013 servo-2020", + vector=True, extra_prefixes="webkit", + gecko_enum_prefix="StyleGeometryBox", + gecko_inexhaustive=True, + spec="https://drafts.csswg.org/css-backgrounds/#the-background-origin", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.predefined_type( + "background-size", + "BackgroundSize", + engines="gecko servo-2013 servo-2020", + initial_value="computed::BackgroundSize::auto()", + initial_specified_value="specified::BackgroundSize::auto()", + spec="https://drafts.csswg.org/css-backgrounds/#the-background-size", + vector=True, + vector_animation_type="repeatable_list", + animation_value_type="BackgroundSizeList", + extra_prefixes="webkit", + affects="paint", +)} + +// https://drafts.fxtf.org/compositing/#background-blend-mode +${helpers.single_keyword( + "background-blend-mode", + """normal multiply screen overlay darken lighten color-dodge + color-burn hard-light soft-light difference exclusion hue + saturation color luminosity""", + gecko_enum_prefix="StyleBlend", + vector=True, + engines="gecko", + animation_value_type="discrete", + gecko_inexhaustive=True, + spec="https://drafts.fxtf.org/compositing/#background-blend-mode", + affects="paint", +)} diff --git a/servo/components/style/properties/longhands/border.mako.rs b/servo/components/style/properties/longhands/border.mako.rs new file mode 100644 index 0000000000..4d0676f678 --- /dev/null +++ b/servo/components/style/properties/longhands/border.mako.rs @@ -0,0 +1,170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import Keyword, Method, ALL_CORNERS, PHYSICAL_SIDES, ALL_SIDES, maybe_moz_logical_alias %> + +<% data.new_style_struct("Border", inherited=False, + additional_methods=[Method("border_" + side + "_has_nonzero_width", + "bool") for side in ["top", "right", "bottom", "left"]]) %> +<% + def maybe_logical_spec(side, kind): + if side[1]: # if it is logical + return "https://drafts.csswg.org/css-logical-props/#propdef-border-%s-%s" % (side[0], kind) + else: + return "https://drafts.csswg.org/css-backgrounds/#border-%s-%s" % (side[0], kind) +%> +% for side in ALL_SIDES: + <% + side_name = side[0] + is_logical = side[1] + %> + ${helpers.predefined_type( + "border-%s-color" % side_name, "Color", + "computed_value::T::currentcolor()", + engines="gecko servo-2013 servo-2020", + aliases=maybe_moz_logical_alias(engine, side, "-moz-border-%s-color"), + spec=maybe_logical_spec(side, "color"), + animation_value_type="AnimatedColor", + logical=is_logical, + logical_group="border-color", + allow_quirks="No" if is_logical else "Yes", + ignored_when_colors_disabled=True, + affects="paint", + )} + + ${helpers.predefined_type( + "border-%s-style" % side_name, "BorderStyle", + "specified::BorderStyle::None", + engines="gecko servo-2013 servo-2020", + aliases=maybe_moz_logical_alias(engine, side, "-moz-border-%s-style"), + spec=maybe_logical_spec(side, "style"), + animation_value_type="discrete" if not is_logical else "none", + logical=is_logical, + logical_group="border-style", + affects="layout", + )} + + ${helpers.predefined_type( + "border-%s-width" % side_name, + "BorderSideWidth", + "app_units::Au::from_px(3)", + engines="gecko servo-2013 servo-2020", + aliases=maybe_moz_logical_alias(engine, side, "-moz-border-%s-width"), + spec=maybe_logical_spec(side, "width"), + animation_value_type="NonNegativeLength", + logical=is_logical, + logical_group="border-width", + allow_quirks="No" if is_logical else "Yes", + servo_restyle_damage="reflow rebuild_and_reflow_inline", + affects="layout", + )} +% endfor + +% for corner in ALL_CORNERS: + <% + corner_name = corner[0] + is_logical = corner[1] + if is_logical: + prefixes = None + else: + prefixes = "webkit" + %> + ${helpers.predefined_type( + "border-%s-radius" % corner_name, + "BorderCornerRadius", + "computed::BorderCornerRadius::zero()", + "parse", + engines="gecko servo-2013 servo-2020", + extra_prefixes=prefixes, + spec=maybe_logical_spec(corner, "radius"), + boxed=True, + animation_value_type="BorderCornerRadius", + logical_group="border-radius", + logical=is_logical, + affects="paint", + )} +% endfor + +${helpers.single_keyword( + "box-decoration-break", + "slice clone", + engines="gecko", + gecko_enum_prefix="StyleBoxDecorationBreak", + spec="https://drafts.csswg.org/css-break/#propdef-box-decoration-break", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.single_keyword( + "-moz-float-edge", + "content-box margin-box", + engines="gecko", + gecko_ffi_name="mFloatEdge", + gecko_enum_prefix="StyleFloatEdge", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-float-edge)", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "border-image-source", + "Image", + engines="gecko servo-2013 servo-2020", + initial_value="computed::Image::None", + initial_specified_value="specified::Image::None", + spec="https://drafts.csswg.org/css-backgrounds/#the-background-image", + vector=False, + animation_value_type="discrete", + boxed=engine == "servo-2013", + ignored_when_colors_disabled=True, + affects="paint", +)} + +${helpers.predefined_type( + "border-image-outset", + "NonNegativeLengthOrNumberRect", + engines="gecko servo-2013 servo-2020", + initial_value="generics::rect::Rect::all(computed::NonNegativeLengthOrNumber::zero())", + initial_specified_value="generics::rect::Rect::all(specified::NonNegativeLengthOrNumber::zero())", + spec="https://drafts.csswg.org/css-backgrounds/#border-image-outset", + animation_value_type="NonNegativeLengthOrNumberRect", + boxed=True, + affects="paint", +)} + +${helpers.predefined_type( + "border-image-repeat", + "BorderImageRepeat", + "computed::BorderImageRepeat::stretch()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::BorderImageRepeat::stretch()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat", + affects="paint", +)} + +${helpers.predefined_type( + "border-image-width", + "BorderImageWidth", + engines="gecko servo-2013 servo-2020", + initial_value="computed::BorderImageWidth::all(computed::BorderImageSideWidth::one())", + initial_specified_value="specified::BorderImageWidth::all(specified::BorderImageSideWidth::one())", + spec="https://drafts.csswg.org/css-backgrounds/#border-image-width", + animation_value_type="BorderImageWidth", + boxed=True, + affects="paint", +)} + +${helpers.predefined_type( + "border-image-slice", + "BorderImageSlice", + engines="gecko servo-2013 servo-2020", + initial_value="computed::BorderImageSlice::hundred_percent()", + initial_specified_value="specified::BorderImageSlice::hundred_percent()", + spec="https://drafts.csswg.org/css-backgrounds/#border-image-slice", + animation_value_type="BorderImageSlice", + boxed=True, + affects="paint", +)} diff --git a/servo/components/style/properties/longhands/box.mako.rs b/servo/components/style/properties/longhands/box.mako.rs new file mode 100644 index 0000000000..017bef38ea --- /dev/null +++ b/servo/components/style/properties/longhands/box.mako.rs @@ -0,0 +1,644 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import ALL_AXES, Keyword, Method, to_rust_ident, to_camel_case%> + +<% data.new_style_struct("Box", + inherited=False, + gecko_name="Display") %> + +${helpers.predefined_type( + "display", + "Display", + "computed::Display::inline()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::Display::inline()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-display/#propdef-display", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.single_keyword( + "-moz-top-layer", + "none top", + engines="gecko", + gecko_enum_prefix="StyleTopLayer", + gecko_ffi_name="mTopLayer", + animation_value_type="none", + enabled_in="ua", + spec="Internal (not web-exposed)", + affects="layout", +)} + +// An internal-only property for elements in a top layer +// https://fullscreen.spec.whatwg.org/#top-layer +${helpers.single_keyword( + "-servo-top-layer", + "none top", + engines="servo-2013 servo-2020", + animation_value_type="none", + enabled_in="ua", + spec="Internal (not web-exposed)", + affects="layout", +)} + +<%helpers:single_keyword + name="position" + values="static absolute relative fixed ${'sticky' if engine in ['gecko', 'servo-2013'] else ''}" + engines="gecko servo-2013 servo-2020" + animation_value_type="discrete" + gecko_enum_prefix="StylePositionProperty" + spec="https://drafts.csswg.org/css-position/#position-property" + servo_restyle_damage="rebuild_and_reflow" + affects="layout" +> +impl computed_value::T { + pub fn is_absolutely_positioned(self) -> bool { + matches!(self, Self::Absolute | Self::Fixed) + } + pub fn is_relative(self) -> bool { + self == Self::Relative + } +} +</%helpers:single_keyword> + +${helpers.predefined_type( + "float", + "Float", + "computed::Float::None", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + initial_specified_value="specified::Float::None", + spec="https://drafts.csswg.org/css-box/#propdef-float", + animation_value_type="discrete", + servo_restyle_damage="rebuild_and_reflow", + gecko_ffi_name="mFloat", + affects="layout", +)} + +${helpers.predefined_type( + "clear", + "Clear", + "computed::Clear::None", + engines="gecko servo-2013", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css2/#propdef-clear", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "vertical-align", + "VerticalAlign", + "computed::VerticalAlign::baseline()", + engines="gecko servo-2013", + animation_value_type="ComputedValue", + spec="https://www.w3.org/TR/CSS2/visudet.html#propdef-vertical-align", + servo_restyle_damage = "reflow", + affects="layout", +)} + +${helpers.predefined_type( + "baseline-source", + "BaselineSource", + "computed::BaselineSource::Auto", + engines="gecko servo-2013", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-inline-3/#baseline-source", + servo_restyle_damage = "reflow", + affects="layout", +)} + +// CSS 2.1, Section 11 - Visual effects + +${helpers.single_keyword( + "-servo-overflow-clip-box", + "padding-box content-box", + engines="servo-2013", + animation_value_type="none", + enabled_in="ua", + spec="Internal, not web-exposed, \ + may be standardized in the future (https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-box)", + affects="layout", +)} + +% for direction in ["inline", "block"]: + ${helpers.predefined_type( + "overflow-clip-box-" + direction, + "OverflowClipBox", + "computed::OverflowClipBox::PaddingBox", + engines="gecko", + enabled_in="ua", + gecko_pref="layout.css.overflow-clip-box.enabled", + animation_value_type="discrete", + spec="Internal, may be standardized in the future: \ + https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-box", + affects="layout", + )} +% endfor + +% for (axis, logical) in ALL_AXES: + <% full_name = "overflow-{}".format(axis) %> + ${helpers.predefined_type( + full_name, + "Overflow", + "computed::Overflow::Visible", + engines="gecko servo-2013 servo-2020", + logical_group="overflow", + logical=logical, + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-overflow-3/#propdef-{}".format(full_name), + servo_restyle_damage = "reflow", + affects="layout", + )} +% endfor + +${helpers.predefined_type( + "overflow-anchor", + "OverflowAnchor", + "computed::OverflowAnchor::Auto", + engines="gecko", + initial_specified_value="specified::OverflowAnchor::Auto", + gecko_pref="layout.css.scroll-anchoring.enabled", + spec="https://drafts.csswg.org/css-scroll-anchoring/#exclusion-api", + animation_value_type="discrete", + affects="", +)} + +<% transform_extra_prefixes = "moz:layout.css.prefixes.transforms webkit" %> + +${helpers.predefined_type( + "transform", + "Transform", + "generics::transform::Transform::none()", + engines="gecko servo-2013 servo-2020", + extra_prefixes=transform_extra_prefixes, + animation_value_type="ComputedValue", + flags="CAN_ANIMATE_ON_COMPOSITOR", + spec="https://drafts.csswg.org/css-transforms/#propdef-transform", + servo_restyle_damage="reflow_out_of_flow", + affects="overflow", +)} + +${helpers.predefined_type( + "rotate", + "Rotate", + "generics::transform::Rotate::None", + engines="gecko servo-2013", + animation_value_type="ComputedValue", + boxed=True, + flags="CAN_ANIMATE_ON_COMPOSITOR", + gecko_pref="layout.css.individual-transform.enabled", + spec="https://drafts.csswg.org/css-transforms-2/#individual-transforms", + servo_restyle_damage = "reflow_out_of_flow", + affects="overflow", +)} + +${helpers.predefined_type( + "scale", + "Scale", + "generics::transform::Scale::None", + engines="gecko servo-2013", + animation_value_type="ComputedValue", + boxed=True, + flags="CAN_ANIMATE_ON_COMPOSITOR", + gecko_pref="layout.css.individual-transform.enabled", + spec="https://drafts.csswg.org/css-transforms-2/#individual-transforms", + servo_restyle_damage = "reflow_out_of_flow", + affects="overflow", +)} + +${helpers.predefined_type( + "translate", + "Translate", + "generics::transform::Translate::None", + engines="gecko servo-2013", + animation_value_type="ComputedValue", + boxed=True, + flags="CAN_ANIMATE_ON_COMPOSITOR", + gecko_pref="layout.css.individual-transform.enabled", + spec="https://drafts.csswg.org/css-transforms-2/#individual-transforms", + servo_restyle_damage="reflow_out_of_flow", + affects="overflow", +)} + +// Motion Path Module Level 1 +${helpers.predefined_type( + "offset-path", + "OffsetPath", + "computed::OffsetPath::none()", + engines="gecko", + animation_value_type="motion::OffsetPath", + flags="CAN_ANIMATE_ON_COMPOSITOR", + spec="https://drafts.fxtf.org/motion-1/#offset-path-property", + servo_restyle_damage="reflow_out_of_flow", + affects="overflow", +)} + +// Motion Path Module Level 1 +${helpers.predefined_type( + "offset-distance", + "LengthPercentage", + "computed::LengthPercentage::zero()", + engines="gecko", + animation_value_type="ComputedValue", + flags="CAN_ANIMATE_ON_COMPOSITOR", + spec="https://drafts.fxtf.org/motion-1/#offset-distance-property", + servo_restyle_damage="reflow_out_of_flow", + affects="overflow", +)} + +// Motion Path Module Level 1 +${helpers.predefined_type( + "offset-rotate", + "OffsetRotate", + "computed::OffsetRotate::auto()", + engines="gecko", + animation_value_type="ComputedValue", + flags="CAN_ANIMATE_ON_COMPOSITOR", + spec="https://drafts.fxtf.org/motion-1/#offset-rotate-property", + servo_restyle_damage="reflow_out_of_flow", + affects="overflow", +)} + +// Motion Path Module Level 1 +${helpers.predefined_type( + "offset-anchor", + "PositionOrAuto", + "computed::PositionOrAuto::auto()", + engines="gecko", + animation_value_type="ComputedValue", + flags="CAN_ANIMATE_ON_COMPOSITOR", + spec="https://drafts.fxtf.org/motion-1/#offset-anchor-property", + servo_restyle_damage="reflow_out_of_flow", + boxed=True, + affects="overflow", +)} + +// Motion Path Module Level 1 +${helpers.predefined_type( + "offset-position", + "OffsetPosition", + "computed::OffsetPosition::normal()", + engines="gecko", + animation_value_type="ComputedValue", + gecko_pref="layout.css.motion-path-offset-position.enabled", + flags="CAN_ANIMATE_ON_COMPOSITOR", + spec="https://drafts.fxtf.org/motion-1/#offset-position-property", + servo_restyle_damage="reflow_out_of_flow", + boxed=True, + affects="overflow", +)} + +// CSSOM View Module +// https://www.w3.org/TR/cssom-view-1/ +${helpers.single_keyword( + "scroll-behavior", + "auto smooth", + engines="gecko", + spec="https://drafts.csswg.org/cssom-view/#propdef-scroll-behavior", + animation_value_type="discrete", + gecko_enum_prefix="StyleScrollBehavior", + affects="", +)} + +${helpers.predefined_type( + "scroll-snap-align", + "ScrollSnapAlign", + "computed::ScrollSnapAlign::none()", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.predefined_type( + "scroll-snap-type", + "ScrollSnapType", + "computed::ScrollSnapType::none()", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.predefined_type( + "scroll-snap-stop", + "ScrollSnapStop", + "computed::ScrollSnapStop::Normal", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-stop", + animation_value_type="discrete", + affects="paint", +)} + +% for (axis, logical) in ALL_AXES: + ${helpers.predefined_type( + "overscroll-behavior-" + axis, + "OverscrollBehavior", + "computed::OverscrollBehavior::Auto", + engines="gecko", + logical_group="overscroll-behavior", + logical=logical, + gecko_pref="layout.css.overscroll-behavior.enabled", + spec="https://wicg.github.io/overscroll-behavior/#overscroll-behavior-properties", + animation_value_type="discrete", + affects="paint", + )} +% endfor + +// Compositing and Blending Level 1 +// http://www.w3.org/TR/compositing-1/ +${helpers.single_keyword( + "isolation", + "auto isolate", + engines="gecko", + spec="https://drafts.fxtf.org/compositing/#isolation", + gecko_enum_prefix="StyleIsolation", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.predefined_type( + "break-after", + "BreakBetween", + "computed::BreakBetween::Auto", + engines="gecko", + spec="https://drafts.csswg.org/css-break/#propdef-break-after", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "break-before", + "BreakBetween", + "computed::BreakBetween::Auto", + engines="gecko", + spec="https://drafts.csswg.org/css-break/#propdef-break-before", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "break-inside", + "BreakWithin", + "computed::BreakWithin::Auto", + engines="gecko", + spec="https://drafts.csswg.org/css-break/#propdef-break-inside", + animation_value_type="discrete", + affects="layout", +)} + +// CSS Basic User Interface Module Level 3 +// http://dev.w3.org/csswg/css-ui +${helpers.predefined_type( + "resize", + "Resize", + "computed::Resize::None", + engines="gecko", + animation_value_type="discrete", + gecko_ffi_name="mResize", + spec="https://drafts.csswg.org/css-ui/#propdef-resize", + affects="layout", +)} + +${helpers.predefined_type( + "perspective", + "Perspective", + "computed::Perspective::none()", + engines="gecko servo-2013 servo-2020", + gecko_ffi_name="mChildPerspective", + spec="https://drafts.csswg.org/css-transforms/#perspective", + extra_prefixes=transform_extra_prefixes, + animation_value_type="AnimatedPerspective", + servo_restyle_damage = "reflow_out_of_flow", + affects="overflow", +)} + +${helpers.predefined_type( + "perspective-origin", + "Position", + "computed::position::Position::center()", + engines="gecko servo-2013 servo-2020", + boxed=True, + extra_prefixes=transform_extra_prefixes, + spec="https://drafts.csswg.org/css-transforms-2/#perspective-origin-property", + animation_value_type="ComputedValue", + servo_restyle_damage="reflow_out_of_flow", + affects="overflow", +)} + +${helpers.single_keyword( + "backface-visibility", + "visible hidden", + engines="gecko servo-2013 servo-2020", + gecko_enum_prefix="StyleBackfaceVisibility", + spec="https://drafts.csswg.org/css-transforms/#backface-visibility-property", + extra_prefixes=transform_extra_prefixes, + animation_value_type="discrete", + affects="paint", +)} + +${helpers.predefined_type( + "transform-box", + "TransformBox", + "computed::TransformBox::ViewBox", + engines="gecko", + spec="https://drafts.csswg.org/css-transforms/#transform-box", + animation_value_type="discrete", + affects="overflow", +)} + +${helpers.predefined_type( + "transform-style", + "TransformStyle", + "computed::TransformStyle::Flat", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-transforms-2/#transform-style-property", + extra_prefixes=transform_extra_prefixes, + animation_value_type="discrete", + servo_restyle_damage = "reflow_out_of_flow", + affects="overflow", +)} + +${helpers.predefined_type( + "transform-origin", + "TransformOrigin", + "computed::TransformOrigin::initial_value()", + engines="gecko servo-2013 servo-2020", + animation_value_type="ComputedValue", + extra_prefixes=transform_extra_prefixes, + gecko_ffi_name="mTransformOrigin", + boxed=True, + spec="https://drafts.csswg.org/css-transforms/#transform-origin-property", + servo_restyle_damage="reflow_out_of_flow", + affects="overflow", +)} + +${helpers.predefined_type( + "contain", + "Contain", + "specified::Contain::empty()", + engines="gecko", + animation_value_type="none", + spec="https://drafts.csswg.org/css-contain/#contain-property", + affects="layout", +)} + +${helpers.predefined_type( + "content-visibility", + "ContentVisibility", + "computed::ContentVisibility::Visible", + engines="gecko", + spec="https://drafts.csswg.org/css-contain/#content-visibility", + gecko_pref="layout.css.content-visibility.enabled", + animation_value_type="ComputedValue", + affects="layout", +)} + +${helpers.predefined_type( + "container-type", + "ContainerType", + "computed::ContainerType::Normal", + engines="gecko", + animation_value_type="none", + enabled_in="ua", + gecko_pref="layout.css.container-queries.enabled", + spec="https://drafts.csswg.org/css-contain-3/#container-type", + affects="layout", +)} + +${helpers.predefined_type( + "container-name", + "ContainerName", + "computed::ContainerName::none()", + engines="gecko", + animation_value_type="none", + enabled_in="ua", + gecko_pref="layout.css.container-queries.enabled", + spec="https://drafts.csswg.org/css-contain-3/#container-name", + affects="", +)} + +${helpers.predefined_type( + "appearance", + "Appearance", + "computed::Appearance::None", + engines="gecko", + aliases="-moz-appearance -webkit-appearance", + spec="https://drafts.csswg.org/css-ui-4/#propdef-appearance", + animation_value_type="discrete", + gecko_ffi_name="mAppearance", + affects="paint", +)} + +// The inherent widget type of an element, selected by specifying +// `appearance: auto`. +${helpers.predefined_type( + "-moz-default-appearance", + "Appearance", + "computed::Appearance::None", + engines="gecko", + animation_value_type="none", + spec="Internal (not web-exposed)", + enabled_in="chrome", + gecko_ffi_name="mDefaultAppearance", + affects="paint", +)} + +${helpers.single_keyword( + "-moz-orient", + "inline block horizontal vertical", + engines="gecko", + gecko_ffi_name="mOrient", + gecko_enum_prefix="StyleOrient", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-orient)", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "will-change", + "WillChange", + "computed::WillChange::auto()", + engines="gecko", + animation_value_type="none", + spec="https://drafts.csswg.org/css-will-change/#will-change", + affects="layout", +)} + +// The spec issue for the parse_method: https://github.com/w3c/csswg-drafts/issues/4102. +${helpers.predefined_type( + "shape-image-threshold", + "Opacity", + "0.0", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-shapes/#shape-image-threshold-property", + affects="layout", +)} + +${helpers.predefined_type( + "shape-margin", + "NonNegativeLengthPercentage", + "computed::NonNegativeLengthPercentage::zero()", + engines="gecko", + animation_value_type="NonNegativeLengthPercentage", + spec="https://drafts.csswg.org/css-shapes/#shape-margin-property", + affects="layout", +)} + +${helpers.predefined_type( + "shape-outside", + "basic_shape::ShapeOutside", + "generics::basic_shape::ShapeOutside::None", + engines="gecko", + animation_value_type="basic_shape::ShapeOutside", + spec="https://drafts.csswg.org/css-shapes/#shape-outside-property", + affects="layout", +)} + +${helpers.predefined_type( + "touch-action", + "TouchAction", + "computed::TouchAction::auto()", + engines="gecko", + animation_value_type="discrete", + spec="https://compat.spec.whatwg.org/#touch-action", + affects="paint", +)} + +${helpers.predefined_type( + "-webkit-line-clamp", + "LineClamp", + "computed::LineClamp::none()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-overflow-3/#line-clamp", + affects="layout", +)} + +${helpers.predefined_type( + "scrollbar-gutter", + "ScrollbarGutter", + "computed::ScrollbarGutter::AUTO", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-overflow-3/#scrollbar-gutter-property", + affects="layout", +)} + +${helpers.predefined_type( + "zoom", + "Zoom", + "computed::box_::Zoom::ONE", + engines="gecko", + animation_value_type="Number", + spec="Non-standard (https://github.com/atanassov/css-zoom/ is the closest)", + gecko_pref="layout.css.zoom.enabled", + affects="layout", + enabled_in="chrome", +)} diff --git a/servo/components/style/properties/longhands/column.mako.rs b/servo/components/style/properties/longhands/column.mako.rs new file mode 100644 index 0000000000..38c32938c6 --- /dev/null +++ b/servo/components/style/properties/longhands/column.mako.rs @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("Column", inherited=False) %> + +${helpers.predefined_type( + "column-width", + "length::NonNegativeLengthOrAuto", + "computed::length::NonNegativeLengthOrAuto::auto()", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + initial_specified_value="specified::length::NonNegativeLengthOrAuto::auto()", + animation_value_type="NonNegativeLengthOrAuto", + servo_2013_pref="layout.columns.enabled", + spec="https://drafts.csswg.org/css-multicol/#propdef-column-width", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "column-count", + "ColumnCount", + "computed::ColumnCount::auto()", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + initial_specified_value="specified::ColumnCount::auto()", + servo_2013_pref="layout.columns.enabled", + animation_value_type="AnimatedColumnCount", + spec="https://drafts.csswg.org/css-multicol/#propdef-column-count", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.single_keyword( + "column-fill", + "balance auto", + engines="gecko", + animation_value_type="discrete", + gecko_enum_prefix="StyleColumnFill", + spec="https://drafts.csswg.org/css-multicol/#propdef-column-fill", + affects="layout", +)} + +${helpers.predefined_type( + "column-rule-width", + "BorderSideWidth", + "app_units::Au::from_px(3)", + engines="gecko", + initial_specified_value="specified::BorderSideWidth::medium()", + spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule-width", + animation_value_type="NonNegativeLength", + affects="layout", +)} + +// https://drafts.csswg.org/css-multicol-1/#crc +${helpers.predefined_type( + "column-rule-color", + "Color", + "computed_value::T::currentcolor()", + engines="gecko", + initial_specified_value="specified::Color::currentcolor()", + animation_value_type="AnimatedColor", + ignored_when_colors_disabled=True, + spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule-color", + affects="paint", +)} + +${helpers.single_keyword( + "column-span", + "none all", + engines="gecko", + animation_value_type="discrete", + gecko_enum_prefix="StyleColumnSpan", + spec="https://drafts.csswg.org/css-multicol/#propdef-column-span", + affects="layout", +)} + +${helpers.predefined_type( + "column-rule-style", + "BorderStyle", + "computed::BorderStyle::None", + engines="gecko", + initial_specified_value="specified::BorderStyle::None", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule-style", + affects="paint", +)} diff --git a/servo/components/style/properties/longhands/counters.mako.rs b/servo/components/style/properties/longhands/counters.mako.rs new file mode 100644 index 0000000000..6c844c3567 --- /dev/null +++ b/servo/components/style/properties/longhands/counters.mako.rs @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("Counters", inherited=False, gecko_name="Content") %> + +${helpers.predefined_type( + "content", + "Content", + "computed::Content::normal()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::Content::normal()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-content/#propdef-content", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "counter-increment", + "CounterIncrement", + engines="gecko servo-2013", + initial_value="Default::default()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-lists/#propdef-counter-increment", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "counter-reset", + "CounterReset", + engines="gecko servo-2013", + initial_value="Default::default()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-lists-3/#propdef-counter-reset", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "counter-set", + "CounterSet", + engines="gecko", + initial_value="Default::default()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-lists-3/#propdef-counter-set", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/effects.mako.rs b/servo/components/style/properties/longhands/effects.mako.rs new file mode 100644 index 0000000000..b301aab5dd --- /dev/null +++ b/servo/components/style/properties/longhands/effects.mako.rs @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +// Box-shadow, etc. +<% data.new_style_struct("Effects", inherited=False) %> + +${helpers.predefined_type( + "opacity", + "Opacity", + "1.0", + engines="gecko servo-2013 servo-2020", + animation_value_type="ComputedValue", + flags="CAN_ANIMATE_ON_COMPOSITOR", + spec="https://drafts.csswg.org/css-color/#transparency", + servo_restyle_damage = "reflow_out_of_flow", + affects="paint", +)} + +${helpers.predefined_type( + "box-shadow", + "BoxShadow", + None, + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + vector=True, + simple_vector_bindings=True, + animation_value_type="AnimatedBoxShadowList", + vector_animation_type="with_zero", + extra_prefixes="webkit", + ignored_when_colors_disabled=True, + spec="https://drafts.csswg.org/css-backgrounds/#box-shadow", + affects="overflow", +)} + +${helpers.predefined_type( + "clip", + "ClipRectOrAuto", + "computed::ClipRectOrAuto::auto()", + engines="gecko servo-2013 servo-2020", + animation_value_type="ComputedValue", + boxed=True, + allow_quirks="Yes", + spec="https://drafts.fxtf.org/css-masking/#clip-property", + affects="overflow", +)} + +${helpers.predefined_type( + "filter", + "Filter", + None, + engines="gecko servo-2013 servo-2020", + vector=True, + simple_vector_bindings=True, + gecko_ffi_name="mFilters", + separator="Space", + animation_value_type="AnimatedFilterList", + vector_animation_type="with_zero", + extra_prefixes="webkit", + spec="https://drafts.fxtf.org/filters/#propdef-filter", + affects="overflow", +)} + +${helpers.predefined_type( + "backdrop-filter", + "Filter", + None, + engines="gecko", + vector=True, + simple_vector_bindings=True, + gecko_ffi_name="mBackdropFilters", + separator="Space", + animation_value_type="AnimatedFilterList", + vector_animation_type="with_zero", + gecko_pref="layout.css.backdrop-filter.enabled", + spec="https://drafts.fxtf.org/filter-effects-2/#propdef-backdrop-filter", + affects="overflow", +)} + +${helpers.single_keyword( + "mix-blend-mode", + """normal multiply screen overlay darken lighten color-dodge + color-burn hard-light soft-light difference exclusion hue + saturation color luminosity plus-lighter""", + engines="gecko servo-2013 servo-2020", + gecko_enum_prefix="StyleBlend", + animation_value_type="discrete", + spec="https://drafts.fxtf.org/compositing/#propdef-mix-blend-mode", + affects="paint", +)} diff --git a/servo/components/style/properties/longhands/font.mako.rs b/servo/components/style/properties/longhands/font.mako.rs new file mode 100644 index 0000000000..f188af5b1f --- /dev/null +++ b/servo/components/style/properties/longhands/font.mako.rs @@ -0,0 +1,505 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import Method, to_camel_case, to_rust_ident, to_camel_case_lower, SYSTEM_FONT_LONGHANDS %> + +<% data.new_style_struct("Font", inherited=True) %> + +${helpers.predefined_type( + "font-family", + "FontFamily", + engines="gecko servo-2013 servo-2020", + initial_value="computed::FontFamily::serif()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-family", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "font-style", + "FontStyle", + engines="gecko servo-2013 servo-2020", + initial_value="computed::FontStyle::normal()", + initial_specified_value="specified::FontStyle::normal()", + animation_value_type="FontStyle", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-style", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +<% font_variant_caps_custom_consts= { "small-caps": "SMALLCAPS", + "all-small-caps": "ALLSMALL", + "petite-caps": "PETITECAPS", + "all-petite-caps": "ALLPETITE", + "titling-caps": "TITLING" } %> + +${helpers.single_keyword( + "font-variant-caps", + "normal small-caps", + engines="gecko servo-2013 servo-2020", + extra_gecko_values="all-small-caps petite-caps all-petite-caps unicase titling-caps", + gecko_constant_prefix="NS_FONT_VARIANT_CAPS", + gecko_ffi_name="mFont.variantCaps", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-caps", + custom_consts=font_variant_caps_custom_consts, + animation_value_type="discrete", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "font-weight", + "FontWeight", + engines="gecko servo-2013 servo-2020", + initial_value="computed::FontWeight::normal()", + initial_specified_value="specified::FontWeight::normal()", + animation_value_type="Number", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-weight", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "font-size", + "FontSize", + engines="gecko servo-2013 servo-2020", + initial_value="computed::FontSize::medium()", + initial_specified_value="specified::FontSize::medium()", + animation_value_type="NonNegativeLength", + allow_quirks="Yes", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-size", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "font-size-adjust", + "FontSizeAdjust", + engines="gecko", + initial_value="computed::FontSizeAdjust::None", + initial_specified_value="specified::FontSizeAdjust::None", + animation_value_type="FontSizeAdjust", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-size-adjust", + affects="layout", +)} + +${helpers.predefined_type( + "font-synthesis-weight", + "FontSynthesis", + engines="gecko", + initial_value="computed::FontSynthesis::Auto", + initial_specified_value="specified::FontSynthesis::Auto", + gecko_ffi_name="mFont.synthesisWeight", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-weight", + affects="layout", +)} + +${helpers.predefined_type( + "font-synthesis-style", + "FontSynthesis", + engines="gecko", + initial_value="computed::FontSynthesis::Auto", + initial_specified_value="specified::FontSynthesis::Auto", + gecko_ffi_name="mFont.synthesisStyle", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-style", + affects="layout", +)} + +${helpers.predefined_type( + "font-synthesis-small-caps", + "FontSynthesis", + engines="gecko", + initial_value="computed::FontSynthesis::Auto", + initial_specified_value="specified::FontSynthesis::Auto", + gecko_ffi_name="mFont.synthesisSmallCaps", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-small-caps", + affects="layout", +)} + +${helpers.predefined_type( + "font-synthesis-position", + "FontSynthesis", + engines="gecko", + initial_value="computed::FontSynthesis::Auto", + initial_specified_value="specified::FontSynthesis::Auto", + gecko_ffi_name="mFont.synthesisPosition", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts-4/#font-synthesis-position", + affects="layout", +)} + +${helpers.predefined_type( + "font-stretch", + "FontStretch", + engines="gecko servo-2013 servo-2020", + initial_value="computed::FontStretch::hundred()", + initial_specified_value="specified::FontStretch::normal()", + animation_value_type="Percentage", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-stretch", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.single_keyword( + "font-kerning", + "auto none normal", + engines="gecko", + gecko_ffi_name="mFont.kerning", + gecko_constant_prefix="NS_FONT_KERNING", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-kerning", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "font-variant-alternates", + "FontVariantAlternates", + engines="gecko", + initial_value="computed::FontVariantAlternates::default()", + initial_specified_value="specified::FontVariantAlternates::default()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-alternates", + affects="layout", +)} + +${helpers.predefined_type( + "font-variant-east-asian", + "FontVariantEastAsian", + engines="gecko", + initial_value="computed::FontVariantEastAsian::empty()", + initial_specified_value="specified::FontVariantEastAsian::empty()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-east-asian", + affects="layout", +)} + +${helpers.single_keyword( + "font-variant-emoji", + "normal text emoji unicode", + engines="gecko", + gecko_pref="layout.css.font-variant-emoji.enabled", + has_effect_on_gecko_scrollbars=False, + gecko_enum_prefix="StyleFontVariantEmoji", + gecko_ffi_name="mFont.variantEmoji", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-emoji", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "font-variant-ligatures", + "FontVariantLigatures", + engines="gecko", + initial_value="computed::FontVariantLigatures::empty()", + initial_specified_value="specified::FontVariantLigatures::empty()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-ligatures", + affects="layout", +)} + +${helpers.predefined_type( + "font-variant-numeric", + "FontVariantNumeric", + engines="gecko", + initial_value="computed::FontVariantNumeric::empty()", + initial_specified_value="specified::FontVariantNumeric::empty()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-numeric", + affects="layout", +)} + +${helpers.single_keyword( + "font-variant-position", + "normal sub super", + engines="gecko", + gecko_ffi_name="mFont.variantPosition", + gecko_constant_prefix="NS_FONT_VARIANT_POSITION", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-variant-position", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "font-feature-settings", + "FontFeatureSettings", + engines="gecko", + initial_value="computed::FontFeatureSettings::normal()", + initial_specified_value="specified::FontFeatureSettings::normal()", + extra_prefixes="moz:layout.css.prefixes.font-features", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-fonts/#propdef-font-feature-settings", + affects="layout", +)} + +${helpers.predefined_type( + "font-variation-settings", + "FontVariationSettings", + engines="gecko", + gecko_pref="layout.css.font-variations.enabled", + has_effect_on_gecko_scrollbars=False, + initial_value="computed::FontVariationSettings::normal()", + initial_specified_value="specified::FontVariationSettings::normal()", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-fonts-4/#propdef-font-variation-settings", + affects="layout", +)} + +${helpers.predefined_type( + "font-language-override", + "FontLanguageOverride", + engines="gecko", + initial_value="computed::FontLanguageOverride::normal()", + initial_specified_value="specified::FontLanguageOverride::normal()", + animation_value_type="discrete", + extra_prefixes="moz:layout.css.prefixes.font-features", + spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-language-override", + affects="layout", +)} + +${helpers.single_keyword( + "font-optical-sizing", + "auto none", + engines="gecko", + gecko_pref="layout.css.font-variations.enabled", + has_effect_on_gecko_scrollbars=False, + gecko_ffi_name="mFont.opticalSizing", + gecko_constant_prefix="NS_FONT_OPTICAL_SIZING", + animation_value_type="discrete", + spec="https://www.w3.org/TR/css-fonts-4/#font-optical-sizing-def", + affects="layout", +)} + +${helpers.predefined_type( + "font-palette", + "FontPalette", + engines="gecko", + initial_value="computed::FontPalette::normal()", + initial_specified_value="specified::FontPalette::normal()", + animation_value_type="discrete", + gecko_pref="layout.css.font-palette.enabled", + has_effect_on_gecko_scrollbars=False, + spec="https://drafts.csswg.org/css-fonts/#font-palette-prop", + affects="layout", +)} + +${helpers.predefined_type( + "-x-lang", + "XLang", + engines="gecko", + initial_value="computed::XLang::get_initial_value()", + animation_value_type="none", + enabled_in="", + has_effect_on_gecko_scrollbars=False, + spec="Internal (not web-exposed)", + affects="layout", +)} + +${helpers.predefined_type( + "math-depth", + "MathDepth", + "0", + engines="gecko", + gecko_pref="layout.css.math-depth.enabled", + has_effect_on_gecko_scrollbars=False, + animation_value_type="none", + enabled_in="ua", + spec="https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property", + affects="", +)} + +${helpers.single_keyword( + "math-style", + "normal compact", + engines="gecko", + gecko_enum_prefix="StyleMathStyle", + gecko_pref="layout.css.math-style.enabled", + spec="https://mathml-refresh.github.io/mathml-core/#the-math-style-property", + has_effect_on_gecko_scrollbars=False, + animation_value_type="none", + enabled_in="ua", + needs_conversion=True, + affects="layout", +)} + +${helpers.single_keyword( + "-moz-math-variant", + """none normal bold italic bold-italic script bold-script + fraktur double-struck bold-fraktur sans-serif + bold-sans-serif sans-serif-italic sans-serif-bold-italic + monospace initial tailed looped stretched""", + engines="gecko", + gecko_enum_prefix="StyleMathVariant", + gecko_ffi_name="mMathVariant", + spec="Internal (not web-exposed)", + animation_value_type="none", + enabled_in="", + has_effect_on_gecko_scrollbars=False, + needs_conversion=True, + affects="layout", +)} + +${helpers.predefined_type( + "-x-text-scale", + "XTextScale", + "computed::XTextScale::All", + engines="gecko", + animation_value_type="none", + enabled_in="", + has_effect_on_gecko_scrollbars=False, + spec="Internal (not web-exposed)", + affects="layout", +)} + +${helpers.predefined_type( + "line-height", + "LineHeight", + "computed::LineHeight::normal()", + engines="gecko servo-2013 servo-2020", + animation_value_type="LineHeight", + spec="https://drafts.csswg.org/css2/visudet.html#propdef-line-height", + servo_restyle_damage="reflow", + affects="layout", +)} + +% if engine == "gecko": +pub mod system_font { + //! We deal with system fonts here + //! + //! System fonts can only be set as a group via the font shorthand. + //! They resolve at compute time (not parse time -- this lets the + //! browser respond to changes to the OS font settings). + //! + //! While Gecko handles these as a separate property and keyword + //! values on each property indicating that the font should be picked + //! from the -x-system-font property, we avoid this. Instead, + //! each font longhand has a special SystemFont variant which contains + //! the specified system font. When the cascade function (in helpers) + //! detects that a value has a system font, it will resolve it, and + //! cache it on the ComputedValues. After this, it can be just fetched + //! whenever a font longhand on the same element needs the system font. + //! + //! When a longhand property is holding a SystemFont, it's serialized + //! to an empty string as if its value comes from a shorthand with + //! variable reference. We may want to improve this behavior at some + //! point. See also https://github.com/w3c/csswg-drafts/issues/1586. + + use crate::properties::longhands; + use std::hash::{Hash, Hasher}; + use crate::values::computed::{ToComputedValue, Context}; + use crate::values::specified::font::SystemFont; + // ComputedValues are compared at times + // so we need these impls. We don't want to + // add Eq to Number (which contains a float) + // so instead we have an eq impl which skips the + // cached values + impl PartialEq for ComputedSystemFont { + fn eq(&self, other: &Self) -> bool { + self.system_font == other.system_font + } + } + impl Eq for ComputedSystemFont {} + + impl Hash for ComputedSystemFont { + fn hash<H: Hasher>(&self, hasher: &mut H) { + self.system_font.hash(hasher) + } + } + + impl ToComputedValue for SystemFont { + type ComputedValue = ComputedSystemFont; + + fn to_computed_value(&self, cx: &Context) -> Self::ComputedValue { + use crate::gecko_bindings::bindings; + use crate::gecko_bindings::structs::nsFont; + use crate::values::computed::font::FontSize; + use crate::values::specified::font::KeywordInfo; + use crate::values::generics::NonNegative; + use std::mem; + + let mut system = mem::MaybeUninit::<nsFont>::uninit(); + let system = unsafe { + bindings::Gecko_nsFont_InitSystem( + system.as_mut_ptr(), + *self, + &**cx.style().get_font(), + cx.device().document() + ); + &mut *system.as_mut_ptr() + }; + let size = NonNegative(cx.maybe_zoom_text(system.size.0)); + let ret = ComputedSystemFont { + font_family: system.family.clone(), + font_size: FontSize { + computed_size: size, + used_size: size, + keyword_info: KeywordInfo::none() + }, + font_weight: system.weight, + font_stretch: system.stretch, + font_style: system.style, + system_font: *self, + }; + unsafe { bindings::Gecko_nsFont_Destroy(system); } + ret + } + + fn from_computed_value(_: &ComputedSystemFont) -> Self { + unreachable!() + } + } + + #[inline] + /// Compute and cache a system font + /// + /// Must be called before attempting to compute a system font + /// specified value + pub fn resolve_system_font(system: SystemFont, context: &mut Context) { + // Checking if context.cached_system_font.is_none() isn't enough, + // if animating from one system font to another the cached system font + // may change + if Some(system) != context.cached_system_font.as_ref().map(|x| x.system_font) { + let computed = system.to_computed_value(context); + context.cached_system_font = Some(computed); + } + } + + #[derive(Clone, Debug)] + pub struct ComputedSystemFont { + % for name in SYSTEM_FONT_LONGHANDS: + pub ${name}: longhands::${name}::computed_value::T, + % endfor + pub system_font: SystemFont, + } + +} +% endif + +${helpers.single_keyword( + "-moz-osx-font-smoothing", + "auto grayscale", + engines="gecko", + gecko_constant_prefix="NS_FONT_SMOOTHING", + gecko_ffi_name="mFont.smoothing", + gecko_pref="layout.css.osx-font-smoothing.enabled", + has_effect_on_gecko_scrollbars=False, + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/font-smooth)", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.predefined_type( + "-moz-min-font-size-ratio", + "Percentage", + "computed::Percentage::hundred()", + engines="gecko", + animation_value_type="none", + enabled_in="ua", + spec="Nonstandard (Internal-only)", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/inherited_box.mako.rs b/servo/components/style/properties/longhands/inherited_box.mako.rs new file mode 100644 index 0000000000..7fd94d1a1f --- /dev/null +++ b/servo/components/style/properties/longhands/inherited_box.mako.rs @@ -0,0 +1,105 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("InheritedBox", inherited=True, gecko_name="Visibility") %> + +// TODO: collapse. Well, do tables first. +${helpers.single_keyword( + "visibility", + "visible hidden collapse", + engines="gecko servo-2013 servo-2020", + gecko_ffi_name="mVisible", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-box/#propdef-visibility", + gecko_enum_prefix="StyleVisibility", + affects="paint", +)} + +// CSS Writing Modes Level 3 +// https://drafts.csswg.org/css-writing-modes-3 +${helpers.single_keyword( + "writing-mode", + "horizontal-tb vertical-rl vertical-lr", + engines="gecko servo-2013 servo-2020", + extra_gecko_values="sideways-rl sideways-lr", + gecko_aliases="lr=horizontal-tb lr-tb=horizontal-tb \ + rl=horizontal-tb rl-tb=horizontal-tb \ + tb=vertical-rl tb-rl=vertical-rl", + servo_2013_pref="layout.writing-mode.enabled", + servo_2020_pref="layout.writing-mode.enabled", + animation_value_type="none", + spec="https://drafts.csswg.org/css-writing-modes/#propdef-writing-mode", + gecko_enum_prefix="StyleWritingModeProperty", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.single_keyword( + "direction", + "ltr rtl", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + animation_value_type="none", + spec="https://drafts.csswg.org/css-writing-modes/#propdef-direction", + gecko_enum_prefix="StyleDirection", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.single_keyword( + "-moz-box-collapse", + "flex legacy", + engines="gecko", + gecko_enum_prefix="StyleMozBoxCollapse", + animation_value_type="none", + enabled_in="chrome", + spec="None (internal)", + affects="layout", +)} + +${helpers.single_keyword( + "text-orientation", + "mixed upright sideways", + engines="gecko", + gecko_aliases="sideways-right=sideways", + gecko_enum_prefix="StyleTextOrientation", + animation_value_type="none", + spec="https://drafts.csswg.org/css-writing-modes/#propdef-text-orientation", + affects="layout", +)} + +${helpers.predefined_type( + "print-color-adjust", + "PrintColorAdjust", + "computed::PrintColorAdjust::Economy", + engines="gecko", + aliases="color-adjust", + spec="https://drafts.csswg.org/css-color-adjust/#print-color-adjust", + animation_value_type="discrete", + affects="paint", +)} + +// According to to CSS-IMAGES-3, `optimizespeed` and `optimizequality` are synonyms for `auto` +// And, firefox doesn't support `pixelated` yet (https://bugzilla.mozilla.org/show_bug.cgi?id=856337) +${helpers.predefined_type( + "image-rendering", + "ImageRendering", + "computed::ImageRendering::Auto", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-images/#propdef-image-rendering", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.single_keyword( + "image-orientation", + "from-image none", + engines="gecko", + gecko_enum_prefix="StyleImageOrientation", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-images/#propdef-image-orientation", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/inherited_svg.mako.rs b/servo/components/style/properties/longhands/inherited_svg.mako.rs new file mode 100644 index 0000000000..90443f962a --- /dev/null +++ b/servo/components/style/properties/longhands/inherited_svg.mako.rs @@ -0,0 +1,239 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +// SVG 2 +// https://svgwg.org/svg2-draft/ +<% data.new_style_struct("InheritedSVG", inherited=True, gecko_name="SVG") %> + +// Section 10 - Text + +${helpers.single_keyword( + "dominant-baseline", + """auto ideographic alphabetic hanging mathematical central middle + text-after-edge text-before-edge""", + engines="gecko", + animation_value_type="discrete", + spec="https://www.w3.org/TR/css-inline-3/#propdef-dominant-baseline", + gecko_enum_prefix="StyleDominantBaseline", + affects="layout", +)} + +${helpers.single_keyword( + "text-anchor", + "start middle end", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/text.html#TextAnchorProperty", + gecko_enum_prefix="StyleTextAnchor", + affects="layout", +)} + +// Section 11 - Painting: Filling, Stroking and Marker Symbols +${helpers.single_keyword( + "color-interpolation", + "srgb auto linearrgb", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#ColorInterpolationProperty", + gecko_enum_prefix="StyleColorInterpolation", + affects="paint", +)} + +${helpers.single_keyword( + "color-interpolation-filters", + "linearrgb auto srgb", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#ColorInterpolationFiltersProperty", + gecko_enum_prefix="StyleColorInterpolation", + affects="paint", +)} + +${helpers.predefined_type( + "fill", + "SVGPaint", + "crate::values::computed::SVGPaint::BLACK", + engines="gecko", + animation_value_type="IntermediateSVGPaint", + boxed=True, + spec="https://svgwg.org/svg2-draft/painting.html#SpecifyingFillPaint", + affects="paint", +)} + +${helpers.predefined_type( + "fill-opacity", + "SVGOpacity", + "Default::default()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/painting.html#FillOpacity", + affects="paint", +)} + +${helpers.predefined_type( + "fill-rule", + "FillRule", + "Default::default()", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#FillRuleProperty", + affects="paint", +)} + +${helpers.single_keyword( + "shape-rendering", + "auto optimizespeed crispedges geometricprecision", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#ShapeRenderingProperty", + gecko_enum_prefix = "StyleShapeRendering", + affects="paint", +)} + +${helpers.predefined_type( + "stroke", + "SVGPaint", + "Default::default()", + engines="gecko", + animation_value_type="IntermediateSVGPaint", + boxed=True, + spec="https://svgwg.org/svg2-draft/painting.html#SpecifyingStrokePaint", + affects="paint", +)} + +${helpers.predefined_type( + "stroke-width", + "SVGWidth", + "computed::SVGWidth::one()", + engines="gecko", + animation_value_type="crate::values::computed::SVGWidth", + spec="https://svgwg.org/svg2-draft/painting.html#StrokeWidth", + affects="layout", +)} + +${helpers.single_keyword( + "stroke-linecap", + "butt round square", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#StrokeLinecapProperty", + gecko_enum_prefix = "StyleStrokeLinecap", + affects="layout", +)} + +${helpers.single_keyword( + "stroke-linejoin", + "miter round bevel", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#StrokeLinejoinProperty", + gecko_enum_prefix = "StyleStrokeLinejoin", + affects="layout", +)} + +${helpers.predefined_type( + "stroke-miterlimit", + "NonNegativeNumber", + "From::from(4.0)", + engines="gecko", + animation_value_type="crate::values::computed::NonNegativeNumber", + spec="https://svgwg.org/svg2-draft/painting.html#StrokeMiterlimitProperty", + affects="layout", +)} + +${helpers.predefined_type( + "stroke-opacity", + "SVGOpacity", + "Default::default()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/painting.html#StrokeOpacity", + affects="paint", +)} + +${helpers.predefined_type( + "stroke-dasharray", + "SVGStrokeDashArray", + "Default::default()", + engines="gecko", + animation_value_type="crate::values::computed::SVGStrokeDashArray", + spec="https://svgwg.org/svg2-draft/painting.html#StrokeDashing", + affects="paint", +)} + +${helpers.predefined_type( + "stroke-dashoffset", + "SVGLength", + "computed::SVGLength::zero()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/painting.html#StrokeDashing", + affects="paint", +)} + +// Section 14 - Clipping, Masking and Compositing +${helpers.predefined_type( + "clip-rule", + "FillRule", + "Default::default()", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/masking.html#ClipRuleProperty", + affects="paint", +)} + +${helpers.predefined_type( + "marker-start", + "url::UrlOrNone", + "computed::url::UrlOrNone::none()", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties", + affects="layout", +)} + +${helpers.predefined_type( + "marker-mid", + "url::UrlOrNone", + "computed::url::UrlOrNone::none()", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties", + affects="layout", +)} + +${helpers.predefined_type( + "marker-end", + "url::UrlOrNone", + "computed::url::UrlOrNone::none()", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#VertexMarkerProperties", + affects="layout", +)} + +${helpers.predefined_type( + "paint-order", + "SVGPaintOrder", + "computed::SVGPaintOrder::normal()", + engines="gecko", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#PaintOrder", + affects="paint", +)} + +${helpers.predefined_type( + "-moz-context-properties", + "MozContextProperties", + "computed::MozContextProperties::default()", + engines="gecko", + enabled_in="chrome", + gecko_pref="svg.context-properties.content.enabled", + has_effect_on_gecko_scrollbars=False, + animation_value_type="none", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-context-properties)", + affects="paint", +)} diff --git a/servo/components/style/properties/longhands/inherited_table.mako.rs b/servo/components/style/properties/longhands/inherited_table.mako.rs new file mode 100644 index 0000000000..7eb42a6eb2 --- /dev/null +++ b/servo/components/style/properties/longhands/inherited_table.mako.rs @@ -0,0 +1,53 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("InheritedTable", inherited=True, gecko_name="TableBorder") %> + +${helpers.single_keyword( + "border-collapse", + "separate collapse", + engines="gecko servo-2013", + gecko_enum_prefix="StyleBorderCollapse", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-tables/#propdef-border-collapse", + servo_restyle_damage = "reflow", + affects="layout", +)} + +${helpers.single_keyword( + "empty-cells", + "show hide", + engines="gecko servo-2013", + gecko_enum_prefix="StyleEmptyCells", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-tables/#propdef-empty-cells", + servo_restyle_damage="rebuild_and_reflow", + affects="paint", +)} + +${helpers.predefined_type( + "caption-side", + "table::CaptionSide", + "computed::table::CaptionSide::Top", + engines="gecko servo-2013", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-tables/#propdef-caption-side", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "border-spacing", + "BorderSpacing", + "computed::BorderSpacing::zero()", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + animation_value_type="BorderSpacing", + boxed=True, + spec="https://drafts.csswg.org/css-tables/#propdef-border-spacing", + servo_restyle_damage="reflow", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/inherited_text.mako.rs b/servo/components/style/properties/longhands/inherited_text.mako.rs new file mode 100644 index 0000000000..544ba99bf7 --- /dev/null +++ b/servo/components/style/properties/longhands/inherited_text.mako.rs @@ -0,0 +1,414 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import Keyword %> +<% data.new_style_struct("InheritedText", inherited=True, gecko_name="Text") %> + +${helpers.predefined_type( + "color", + "ColorPropertyValue", + "crate::color::AbsoluteColor::BLACK", + engines="gecko servo-2013 servo-2020", + animation_value_type="AbsoluteColor", + ignored_when_colors_disabled="True", + spec="https://drafts.csswg.org/css-color/#color", + affects="paint", +)} + +// CSS Text Module Level 3 + +${helpers.predefined_type( + "text-transform", + "TextTransform", + "computed::TextTransform::none()", + engines="gecko servo-2013", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text/#propdef-text-transform", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.single_keyword( + "hyphens", + "manual none auto", + engines="gecko", + gecko_enum_prefix="StyleHyphens", + animation_value_type="discrete", + extra_prefixes="moz", + spec="https://drafts.csswg.org/css-text/#propdef-hyphens", + affects="layout", +)} + +// TODO: Support <percentage> +${helpers.single_keyword( + "-moz-text-size-adjust", + "auto none", + engines="gecko", + gecko_enum_prefix="StyleTextSizeAdjust", + gecko_ffi_name="mTextSizeAdjust", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-size-adjust/#adjustment-control", + aliases="-webkit-text-size-adjust", + affects="layout", +)} + +${helpers.predefined_type( + "text-indent", + "TextIndent", + "computed::TextIndent::zero()", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-text/#propdef-text-indent", + servo_restyle_damage = "reflow", + affects="layout", +)} + +// Also known as "word-wrap" (which is more popular because of IE), but this is +// the preferred name per CSS-TEXT 6.2. +${helpers.predefined_type( + "overflow-wrap", + "OverflowWrap", + "computed::OverflowWrap::Normal", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text/#propdef-overflow-wrap", + aliases="word-wrap", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "word-break", + "WordBreak", + "computed::WordBreak::Normal", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text/#propdef-word-break", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "text-justify", + "TextJustify", + "computed::TextJustify::Auto", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text/#propdef-text-justify", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "text-align-last", + "TextAlignLast", + "computed::text::TextAlignLast::Auto", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text/#propdef-text-align-last", + affects="layout", +)} + +// TODO make this a shorthand and implement text-align-last/text-align-all +${helpers.predefined_type( + "text-align", + "TextAlign", + "computed::TextAlign::Start", + engines="gecko servo-2013 servo-2020", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text/#propdef-text-align", + servo_restyle_damage = "reflow", + affects="layout", +)} + +${helpers.predefined_type( + "letter-spacing", + "LetterSpacing", + "computed::LetterSpacing::normal()", + engines="gecko servo-2013 servo-2020", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-text/#propdef-letter-spacing", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "word-spacing", + "WordSpacing", + "computed::WordSpacing::zero()", + engines="gecko servo-2013 servo-2020", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-text/#propdef-word-spacing", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +// TODO: `white-space-collapse: discard` not yet supported +${helpers.single_keyword( + name="white-space-collapse", + values="collapse preserve preserve-breaks preserve-spaces break-spaces", + engines="gecko", + gecko_enum_prefix="StyleWhiteSpaceCollapse", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-4/#propdef-white-space-collapse", + affects="layout", +)} + +${helpers.predefined_type( + "text-shadow", + "SimpleShadow", + None, + engines="gecko servo-2013", + vector=True, + vector_animation_type="with_zero", + animation_value_type="AnimatedTextShadowList", + ignored_when_colors_disabled=True, + simple_vector_bindings=True, + spec="https://drafts.csswg.org/css-text-decor-3/#text-shadow-property", + affects="overflow", +)} + +${helpers.predefined_type( + "text-emphasis-style", + "TextEmphasisStyle", + "computed::TextEmphasisStyle::None", + engines="gecko", + initial_specified_value="SpecifiedValue::None", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style", + affects="overflow", +)} + +${helpers.predefined_type( + "text-emphasis-position", + "TextEmphasisPosition", + "computed::TextEmphasisPosition::OVER", + engines="gecko", + initial_specified_value="specified::TextEmphasisPosition::OVER", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-position", + affects="layout", +)} + +${helpers.predefined_type( + "text-emphasis-color", + "Color", + "computed_value::T::currentcolor()", + engines="gecko", + initial_specified_value="specified::Color::currentcolor()", + animation_value_type="AnimatedColor", + ignored_when_colors_disabled=True, + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-color", + affects="paint", +)} + +${helpers.predefined_type( + "tab-size", + "NonNegativeLengthOrNumber", + "generics::length::LengthOrNumber::Number(From::from(8.0))", + engines="gecko", + animation_value_type="LengthOrNumber", + spec="https://drafts.csswg.org/css-text-3/#tab-size-property", + aliases="-moz-tab-size", + affects="layout", +)} + +${helpers.predefined_type( + "line-break", + "LineBreak", + "computed::LineBreak::Auto", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-3/#line-break-property", + affects="layout", +)} + +// CSS Compatibility +// https://compat.spec.whatwg.org +${helpers.predefined_type( + "-webkit-text-fill-color", + "Color", + "computed_value::T::currentcolor()", + engines="gecko", + animation_value_type="AnimatedColor", + ignored_when_colors_disabled=True, + spec="https://compat.spec.whatwg.org/#the-webkit-text-fill-color", + affects="paint", +)} + +${helpers.predefined_type( + "-webkit-text-stroke-color", + "Color", + "computed_value::T::currentcolor()", + initial_specified_value="specified::Color::currentcolor()", + engines="gecko", + animation_value_type="AnimatedColor", + ignored_when_colors_disabled=True, + spec="https://compat.spec.whatwg.org/#the-webkit-text-stroke-color", + affects="paint", +)} + +${helpers.predefined_type( + "-webkit-text-stroke-width", + "LineWidth", + "app_units::Au(0)", + engines="gecko", + initial_specified_value="specified::LineWidth::zero()", + spec="https://compat.spec.whatwg.org/#the-webkit-text-stroke-width", + animation_value_type="discrete", + affects="overflow", +)} + +// CSS Ruby Layout Module Level 1 +// https://drafts.csswg.org/css-ruby/ +${helpers.single_keyword( + "ruby-align", + "space-around start center space-between", + engines="gecko", + animation_value_type="discrete", + gecko_enum_prefix="StyleRubyAlign", + spec="https://drafts.csswg.org/css-ruby/#ruby-align-property", + affects="layout", +)} + +${helpers.predefined_type( + "ruby-position", + "RubyPosition", + "computed::RubyPosition::AlternateOver", + engines="gecko", + spec="https://drafts.csswg.org/css-ruby/#ruby-position-property", + animation_value_type="discrete", + affects="layout", +)} + +// CSS Writing Modes Module Level 3 +// https://drafts.csswg.org/css-writing-modes-3/ + +${helpers.single_keyword( + "text-combine-upright", + "none all", + engines="gecko", + gecko_enum_prefix="StyleTextCombineUpright", + animation_value_type="none", + spec="https://drafts.csswg.org/css-writing-modes-3/#text-combine-upright", + affects="layout", +)} + +// SVG 2: Section 13 - Painting: Filling, Stroking and Marker Symbols +${helpers.single_keyword( + "text-rendering", + "auto optimizespeed optimizelegibility geometricprecision", + engines="gecko servo-2013 servo-2020", + gecko_enum_prefix="StyleTextRendering", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/painting.html#TextRenderingProperty", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "-moz-control-character-visibility", + "text::MozControlCharacterVisibility", + "Default::default()", + engines="gecko", + enabled_in="chrome", + gecko_pref="layout.css.moz-control-character-visibility.enabled", + has_effect_on_gecko_scrollbars=False, + animation_value_type="none", + spec="Nonstandard", + affects="layout", +)} + +// text underline offset +${helpers.predefined_type( + "text-underline-offset", + "LengthPercentageOrAuto", + "computed::LengthPercentageOrAuto::auto()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-text-decor-4/#underline-offset", + affects="overflow", +)} + +// text underline position +${helpers.predefined_type( + "text-underline-position", + "TextUnderlinePosition", + "computed::TextUnderlinePosition::AUTO", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-decor-3/#text-underline-position-property", + affects="overflow", +)} + +// text decoration skip ink +${helpers.predefined_type( + "text-decoration-skip-ink", + "TextDecorationSkipInk", + "computed::TextDecorationSkipInk::Auto", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property", + affects="overflow", +)} + +// hyphenation character +${helpers.predefined_type( + "hyphenate-character", + "HyphenateCharacter", + "computed::HyphenateCharacter::Auto", + engines="gecko", + animation_value_type="discrete", + spec="https://www.w3.org/TR/css-text-4/#hyphenate-character", + affects="layout", +)} + +${helpers.predefined_type( + "forced-color-adjust", + "ForcedColorAdjust", + "computed::ForcedColorAdjust::Auto", + engines="gecko", + gecko_pref="layout.css.forced-color-adjust.enabled", + has_effect_on_gecko_scrollbars=False, + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop", + affects="paint", +)} + +${helpers.single_keyword( + "-webkit-text-security", + "none circle disc square", + engines="gecko", + gecko_enum_prefix="StyleTextSecurity", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text/#MISSING", + affects="layout", +)} + +${helpers.single_keyword( + "text-wrap-mode", + "wrap nowrap", + engines="gecko", + gecko_enum_prefix="StyleTextWrapMode", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-4/#propdef-text-wrap-mode", + affects="layout", +)} + +${helpers.single_keyword( + "text-wrap-style", + "auto stable balance", + engines="gecko", + gecko_pref="layout.css.text-wrap-balance.enabled", + has_effect_on_gecko_scrollbars=False, + gecko_enum_prefix="StyleTextWrapStyle", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-4/#text-wrap-style", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/inherited_ui.mako.rs b/servo/components/style/properties/longhands/inherited_ui.mako.rs new file mode 100644 index 0000000000..6cdf721336 --- /dev/null +++ b/servo/components/style/properties/longhands/inherited_ui.mako.rs @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("InheritedUI", inherited=True, gecko_name="UI") %> + +${helpers.predefined_type( + "cursor", + "Cursor", + "computed::Cursor::auto()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::Cursor::auto()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-ui/#cursor", + affects="paint", +)} + +// NB: `pointer-events: auto` (and use of `pointer-events` in anything that isn't SVG, in fact) +// is nonstandard, slated for CSS4-UI. +// TODO(pcwalton): SVG-only values. +${helpers.single_keyword( + "pointer-events", + "auto none", + engines="gecko servo-2013 servo-2020", + animation_value_type="discrete", + extra_gecko_values="visiblepainted visiblefill visiblestroke visible painted fill stroke all", + spec="https://svgwg.org/svg2-draft/interact.html#PointerEventsProperty", + gecko_enum_prefix="StylePointerEvents", + affects="paint", +)} + +${helpers.single_keyword( + "-moz-inert", + "none inert", + engines="gecko", + gecko_ffi_name="mInert", + gecko_enum_prefix="StyleInert", + animation_value_type="discrete", + enabled_in="ua", + spec="Nonstandard (https://html.spec.whatwg.org/multipage/#inert-subtrees)", + affects="paint", +)} + +${helpers.single_keyword( + "-moz-user-input", + "auto none", + engines="gecko", + gecko_ffi_name="mUserInput", + gecko_enum_prefix="StyleUserInput", + animation_value_type="discrete", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-user-input)", + affects="", +)} + +${helpers.single_keyword( + "-moz-user-modify", + "read-only read-write write-only", + engines="gecko", + gecko_ffi_name="mUserModify", + gecko_enum_prefix="StyleUserModify", + needs_conversion=True, + animation_value_type="discrete", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-user-modify)", + affects="", +)} + +${helpers.single_keyword( + "-moz-user-focus", + "normal none ignore", + engines="gecko", + gecko_ffi_name="mUserFocus", + gecko_enum_prefix="StyleUserFocus", + animation_value_type="discrete", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-user-focus)", + enabled_in="chrome", + affects="", +)} + +${helpers.predefined_type( + "caret-color", + "color::CaretColor", + "generics::color::CaretColor::auto()", + engines="gecko", + spec="https://drafts.csswg.org/css-ui/#caret-color", + animation_value_type="CaretColor", + ignored_when_colors_disabled=True, + affects="paint", +)} + +${helpers.predefined_type( + "accent-color", + "ColorOrAuto", + "generics::color::ColorOrAuto::Auto", + engines="gecko", + spec="https://drafts.csswg.org/css-ui-4/#widget-accent", + animation_value_type="ColorOrAuto", + ignored_when_colors_disabled=True, + affects="paint", +)} + +${helpers.predefined_type( + "color-scheme", + "ColorScheme", + "specified::color::ColorScheme::normal()", + engines="gecko", + spec="https://drafts.csswg.org/css-color-adjust/#color-scheme-prop", + animation_value_type="discrete", + ignored_when_colors_disabled=True, + affects="paint", +)} + +${helpers.predefined_type( + "scrollbar-color", + "ui::ScrollbarColor", + "Default::default()", + engines="gecko", + spec="https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color", + animation_value_type="ScrollbarColor", + boxed=True, + ignored_when_colors_disabled=True, + affects="paint", +)} + +${helpers.predefined_type( + "-moz-theme", + "ui::MozTheme", + "specified::ui::MozTheme::Auto", + engines="gecko", + enabled_in="chrome", + animation_value_type="discrete", + spec="Internal", + affects="paint", +)} diff --git a/servo/components/style/properties/longhands/list.mako.rs b/servo/components/style/properties/longhands/list.mako.rs new file mode 100644 index 0000000000..619724bd32 --- /dev/null +++ b/servo/components/style/properties/longhands/list.mako.rs @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("List", inherited=True) %> + +${helpers.single_keyword( + "list-style-position", + "outside inside", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + gecko_enum_prefix="StyleListStylePosition", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-lists/#propdef-list-style-position", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +// TODO(pcwalton): Implement the full set of counter styles per CSS-COUNTER-STYLES [1] 6.1: +// +// decimal-leading-zero, armenian, upper-armenian, lower-armenian, georgian, lower-roman, +// upper-roman +// +// [1]: http://dev.w3.org/csswg/css-counter-styles/ +% if engine in ["servo-2013", "servo-2020"]: + ${helpers.single_keyword( + "list-style-type", + "disc none circle square disclosure-open disclosure-closed", + extra_servo_2013_values=""" + decimal lower-alpha upper-alpha arabic-indic bengali cambodian cjk-decimal devanagari + gujarati gurmukhi kannada khmer lao malayalam mongolian myanmar oriya persian telugu + thai tibetan cjk-earthly-branch cjk-heavenly-stem lower-greek hiragana hiragana-iroha + katakana katakana-iroha + """, + engines="servo-2013 servo-2020", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-lists/#propdef-list-style-type", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", + )} +% endif +% if engine == "gecko": + ${helpers.predefined_type( + "list-style-type", + "ListStyleType", + "computed::ListStyleType::disc()", + engines="gecko", + initial_specified_value="specified::ListStyleType::disc()", + animation_value_type="discrete", + boxed=True, + spec="https://drafts.csswg.org/css-lists/#propdef-list-style-type", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", + )} +% endif + +${helpers.predefined_type( + "list-style-image", + "Image", + engines="gecko servo-2013 servo-2020", + initial_value="computed::Image::None", + initial_specified_value="specified::Image::None", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-lists/#propdef-list-style-image", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "quotes", + "Quotes", + "computed::Quotes::get_initial_value()", + engines="gecko servo-2013", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-content/#propdef-quotes", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/margin.mako.rs b/servo/components/style/properties/longhands/margin.mako.rs new file mode 100644 index 0000000000..b5a87f9683 --- /dev/null +++ b/servo/components/style/properties/longhands/margin.mako.rs @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import ALL_SIDES, DEFAULT_RULES_AND_PAGE, maybe_moz_logical_alias %> +<% data.new_style_struct("Margin", inherited=False) %> + +% for side in ALL_SIDES: + <% + spec = "https://drafts.csswg.org/css-box/#propdef-margin-%s" % side[0] + if side[1]: + spec = "https://drafts.csswg.org/css-logical-props/#propdef-margin-%s" % side[1] + %> + ${helpers.predefined_type( + "margin-%s" % side[0], + "LengthPercentageOrAuto", + "computed::LengthPercentageOrAuto::zero()", + engines="gecko servo-2013 servo-2020", + aliases=maybe_moz_logical_alias(engine, side, "-moz-margin-%s"), + allow_quirks="No" if side[1] else "Yes", + animation_value_type="ComputedValue", + logical=side[1], + logical_group="margin", + spec=spec, + rule_types_allowed=DEFAULT_RULES_AND_PAGE, + servo_restyle_damage="reflow", + affects="layout", + )} +% endfor + +${helpers.predefined_type( + "overflow-clip-margin", + "Length", + "computed::Length::zero()", + parse_method="parse_non_negative", + engines="gecko", + spec="https://drafts.csswg.org/css-overflow/#propdef-overflow-clip-margin", + animation_value_type="ComputedValue", + affects="overflow", +)} + +% for side in ALL_SIDES: + ${helpers.predefined_type( + "scroll-margin-%s" % side[0], + "Length", + "computed::Length::zero()", + engines="gecko", + logical=side[1], + logical_group="scroll-margin", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-%s" % side[0], + animation_value_type="ComputedValue", + affects="", + )} +% endfor diff --git a/servo/components/style/properties/longhands/outline.mako.rs b/servo/components/style/properties/longhands/outline.mako.rs new file mode 100644 index 0000000000..8e7f956bf5 --- /dev/null +++ b/servo/components/style/properties/longhands/outline.mako.rs @@ -0,0 +1,57 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import Method %> + +<% data.new_style_struct("Outline", + inherited=False, + additional_methods=[Method("outline_has_nonzero_width", "bool")]) %> + +// TODO(pcwalton): `invert` +${helpers.predefined_type( + "outline-color", + "Color", + "computed_value::T::currentcolor()", + engines="gecko servo-2013", + initial_specified_value="specified::Color::currentcolor()", + animation_value_type="AnimatedColor", + ignored_when_colors_disabled=True, + spec="https://drafts.csswg.org/css-ui/#propdef-outline-color", + affects="paint", +)} + +${helpers.predefined_type( + "outline-style", + "OutlineStyle", + "computed::OutlineStyle::none()", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + initial_specified_value="specified::OutlineStyle::none()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-ui/#propdef-outline-style", + affects="overflow", +)} + +${helpers.predefined_type( + "outline-width", + "BorderSideWidth", + "app_units::Au::from_px(3)", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.2020.unimplemented", + initial_specified_value="specified::BorderSideWidth::medium()", + animation_value_type="NonNegativeLength", + spec="https://drafts.csswg.org/css-ui/#propdef-outline-width", + affects="overflow", +)} + +${helpers.predefined_type( + "outline-offset", + "Length", + "crate::values::computed::Length::new(0.)", + engines="gecko servo-2013", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-ui/#propdef-outline-offset", + affects="overflow", +)} diff --git a/servo/components/style/properties/longhands/padding.mako.rs b/servo/components/style/properties/longhands/padding.mako.rs new file mode 100644 index 0000000000..a165e2cd34 --- /dev/null +++ b/servo/components/style/properties/longhands/padding.mako.rs @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import ALL_SIDES, maybe_moz_logical_alias %> +<% data.new_style_struct("Padding", inherited=False) %> + +% for side in ALL_SIDES: + <% + spec = "https://drafts.csswg.org/css-box/#propdef-padding-%s" % side[0] + if side[1]: + spec = "https://drafts.csswg.org/css-logical-props/#propdef-padding-%s" % side[1] + %> + ${helpers.predefined_type( + "padding-%s" % side[0], + "NonNegativeLengthPercentage", + "computed::NonNegativeLengthPercentage::zero()", + engines="gecko servo-2013 servo-2020", + aliases=maybe_moz_logical_alias(engine, side, "-moz-padding-%s"), + animation_value_type="NonNegativeLengthPercentage", + logical=side[1], + logical_group="padding", + spec=spec, + allow_quirks="No" if side[1] else "Yes", + servo_restyle_damage="reflow rebuild_and_reflow_inline", + affects="layout", + )} +% endfor + +% for side in ALL_SIDES: + ${helpers.predefined_type( + "scroll-padding-%s" % side[0], + "NonNegativeLengthPercentageOrAuto", + "computed::NonNegativeLengthPercentageOrAuto::auto()", + engines="gecko", + logical=side[1], + logical_group="scroll-padding", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-%s" % side[0], + animation_value_type="NonNegativeLengthPercentageOrAuto", + affects="", + )} +% endfor diff --git a/servo/components/style/properties/longhands/page.mako.rs b/servo/components/style/properties/longhands/page.mako.rs new file mode 100644 index 0000000000..86cd284e18 --- /dev/null +++ b/servo/components/style/properties/longhands/page.mako.rs @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import PAGE_RULE %> + +<% data.new_style_struct("Page", inherited=False) %> + +${helpers.predefined_type( + "size", + "PageSize", + "computed::PageSize::auto()", + engines="gecko", + initial_specified_value="specified::PageSize::auto()", + spec="https://drafts.csswg.org/css-page-3/#page-size-prop", + boxed=True, + animation_value_type="none", + rule_types_allowed=PAGE_RULE, + affects="layout", +)} + +${helpers.predefined_type( + "page", + "PageName", + "computed::PageName::auto()", + engines="gecko", + spec="https://drafts.csswg.org/css-page-3/#using-named-pages", + animation_value_type="discrete", + affects="layout", +)} + +${helpers.predefined_type( + "page-orientation", + "PageOrientation", + "computed::PageOrientation::Upright", + engines="gecko", + gecko_pref="layout.css.page-orientation.enabled", + initial_specified_value="specified::PageOrientation::Upright", + spec="https://drafts.csswg.org/css-page-3/#page-orientation-prop", + animation_value_type="none", + rule_types_allowed=PAGE_RULE, + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/position.mako.rs b/servo/components/style/properties/longhands/position.mako.rs new file mode 100644 index 0000000000..fb68baa6b4 --- /dev/null +++ b/servo/components/style/properties/longhands/position.mako.rs @@ -0,0 +1,485 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%! from data import to_rust_ident %> +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import ALL_SIZES, PHYSICAL_SIDES, LOGICAL_SIDES %> + +<% data.new_style_struct("Position", inherited=False) %> + +// "top" / "left" / "bottom" / "right" +% for side in PHYSICAL_SIDES: + ${helpers.predefined_type( + side, + "LengthPercentageOrAuto", + "computed::LengthPercentageOrAuto::auto()", + engines="gecko servo-2013 servo-2020", + spec="https://www.w3.org/TR/CSS2/visuren.html#propdef-%s" % side, + animation_value_type="ComputedValue", + allow_quirks="Yes", + servo_restyle_damage="reflow_out_of_flow", + logical_group="inset", + affects="layout", + )} +% endfor +// inset-* logical properties, map to "top" / "left" / "bottom" / "right" +% for side in LOGICAL_SIDES: + ${helpers.predefined_type( + "inset-%s" % side, + "LengthPercentageOrAuto", + "computed::LengthPercentageOrAuto::auto()", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical-props/#propdef-inset-%s" % side, + animation_value_type="ComputedValue", + logical=True, + logical_group="inset", + affects="layout", + )} +% endfor + +${helpers.predefined_type( + "z-index", + "ZIndex", + "computed::ZIndex::auto()", + engines="gecko servo-2013 servo-2020", + spec="https://www.w3.org/TR/CSS2/visuren.html#z-index", + animation_value_type="ComputedValue", + affects="paint", +)} + +// CSS Flexible Box Layout Module Level 1 +// http://www.w3.org/TR/css3-flexbox/ + +// Flex container properties +${helpers.single_keyword( + "flex-direction", + "row row-reverse column column-reverse", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + spec="https://drafts.csswg.org/css-flexbox/#flex-direction-property", + extra_prefixes="webkit", + animation_value_type="discrete", + servo_restyle_damage = "reflow", + gecko_enum_prefix = "StyleFlexDirection", + affects="layout", +)} + +${helpers.single_keyword( + "flex-wrap", + "nowrap wrap wrap-reverse", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + spec="https://drafts.csswg.org/css-flexbox/#flex-wrap-property", + extra_prefixes="webkit", + animation_value_type="discrete", + servo_restyle_damage = "reflow", + gecko_enum_prefix = "StyleFlexWrap", + affects="layout", +)} + +% if engine == "servo-2013": + // FIXME: Update Servo to support the same Syntax as Gecko. + ${helpers.single_keyword( + "justify-content", + "flex-start stretch flex-end center space-between space-around", + engines="servo-2013", + extra_prefixes="webkit", + spec="https://drafts.csswg.org/css-align/#propdef-justify-content", + animation_value_type="discrete", + servo_restyle_damage = "reflow", + affects="layout", + )} +% endif +% if engine == "gecko": + ${helpers.predefined_type( + "justify-content", + "JustifyContent", + "specified::JustifyContent(specified::ContentDistribution::normal())", + engines="gecko", + spec="https://drafts.csswg.org/css-align/#propdef-justify-content", + extra_prefixes="webkit", + animation_value_type="discrete", + servo_restyle_damage="reflow", + affects="layout", + )} + + ${helpers.predefined_type( + "justify-tracks", + "JustifyTracks", + "specified::JustifyTracks::default()", + engines="gecko", + gecko_pref="layout.css.grid-template-masonry-value.enabled", + animation_value_type="discrete", + servo_restyle_damage="reflow", + spec="https://github.com/w3c/csswg-drafts/issues/4650", + affects="layout", + )} +% endif + +% if engine in ["servo-2013", "servo-2020"]: + // FIXME: Update Servo to support the same Syntax as Gecko. + ${helpers.single_keyword( + "align-content", + "stretch flex-start flex-end center space-between space-around", + engines="servo-2013", + extra_prefixes="webkit", + spec="https://drafts.csswg.org/css-align/#propdef-align-content", + animation_value_type="discrete", + servo_restyle_damage="reflow", + affects="layout", + )} + + ${helpers.single_keyword( + "align-items", + "stretch flex-start flex-end center baseline", + engines="servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + extra_prefixes="webkit", + spec="https://drafts.csswg.org/css-flexbox/#align-items-property", + animation_value_type="discrete", + servo_restyle_damage="reflow", + affects="layout", + )} +% endif +% if engine == "gecko": + ${helpers.predefined_type( + "align-content", + "AlignContent", + "specified::AlignContent(specified::ContentDistribution::normal())", + engines="gecko", + spec="https://drafts.csswg.org/css-align/#propdef-align-content", + extra_prefixes="webkit", + animation_value_type="discrete", + servo_restyle_damage="reflow", + affects="layout", + )} + + ${helpers.predefined_type( + "align-tracks", + "AlignTracks", + "specified::AlignTracks::default()", + engines="gecko", + gecko_pref="layout.css.grid-template-masonry-value.enabled", + animation_value_type="discrete", + servo_restyle_damage="reflow", + spec="https://github.com/w3c/csswg-drafts/issues/4650", + affects="layout", + )} + + ${helpers.predefined_type( + "align-items", + "AlignItems", + "specified::AlignItems::normal()", + engines="gecko", + spec="https://drafts.csswg.org/css-align/#propdef-align-items", + extra_prefixes="webkit", + animation_value_type="discrete", + servo_restyle_damage="reflow", + affects="layout", + )} + + ${helpers.predefined_type( + "justify-items", + "JustifyItems", + "computed::JustifyItems::legacy()", + engines="gecko", + spec="https://drafts.csswg.org/css-align/#propdef-justify-items", + animation_value_type="discrete", + affects="layout", + )} +% endif + +// Flex item properties +${helpers.predefined_type( + "flex-grow", + "NonNegativeNumber", + "From::from(0.0)", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + spec="https://drafts.csswg.org/css-flexbox/#flex-grow-property", + extra_prefixes="webkit", + animation_value_type="NonNegativeNumber", + servo_restyle_damage="reflow", + affects="layout", +)} + +${helpers.predefined_type( + "flex-shrink", + "NonNegativeNumber", + "From::from(1.0)", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + spec="https://drafts.csswg.org/css-flexbox/#flex-shrink-property", + extra_prefixes="webkit", + animation_value_type="NonNegativeNumber", + servo_restyle_damage = "reflow", + affects="layout", +)} + +// https://drafts.csswg.org/css-align/#align-self-property +% if engine in ["servo-2013", "servo-2020"]: + // FIXME: Update Servo to support the same syntax as Gecko. + ${helpers.single_keyword( + "align-self", + "auto stretch flex-start flex-end center baseline", + engines="servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + extra_prefixes="webkit", + spec="https://drafts.csswg.org/css-flexbox/#propdef-align-self", + animation_value_type="discrete", + servo_restyle_damage = "reflow", + affects="layout", + )} +% endif +% if engine == "gecko": + ${helpers.predefined_type( + "align-self", + "AlignSelf", + "specified::AlignSelf(specified::SelfAlignment::auto())", + engines="gecko", + spec="https://drafts.csswg.org/css-align/#align-self-property", + extra_prefixes="webkit", + animation_value_type="discrete", + affects="layout", + )} + + ${helpers.predefined_type( + "justify-self", + "JustifySelf", + "specified::JustifySelf(specified::SelfAlignment::auto())", + engines="gecko", + spec="https://drafts.csswg.org/css-align/#justify-self-property", + animation_value_type="discrete", + affects="layout", + )} +% endif + +// https://drafts.csswg.org/css-flexbox/#propdef-order +${helpers.predefined_type( + "order", + "Integer", + "0", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + extra_prefixes="webkit", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-flexbox/#order-property", + servo_restyle_damage="reflow", + affects="layout", +)} + +${helpers.predefined_type( + "flex-basis", + "FlexBasis", + "computed::FlexBasis::auto()", + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + spec="https://drafts.csswg.org/css-flexbox/#flex-basis-property", + extra_prefixes="webkit", + animation_value_type="FlexBasis", + servo_restyle_damage="reflow", + boxed=True, + affects="layout", +)} + +% for (size, logical) in ALL_SIZES: + <% + spec = "https://drafts.csswg.org/css-box/#propdef-%s" + if logical: + spec = "https://drafts.csswg.org/css-logical-props/#propdef-%s" + %> + // width, height, block-size, inline-size + ${helpers.predefined_type( + size, + "Size", + "computed::Size::auto()", + engines="gecko servo-2013 servo-2020", + logical=logical, + logical_group="size", + allow_quirks="No" if logical else "Yes", + spec=spec % size, + animation_value_type="Size", + servo_restyle_damage="reflow", + affects="layout", + )} + // min-width, min-height, min-block-size, min-inline-size + ${helpers.predefined_type( + "min-%s" % size, + "Size", + "computed::Size::auto()", + engines="gecko servo-2013 servo-2020", + logical=logical, + logical_group="min-size", + allow_quirks="No" if logical else "Yes", + spec=spec % size, + animation_value_type="Size", + servo_restyle_damage="reflow", + affects="layout", + )} + ${helpers.predefined_type( + "max-%s" % size, + "MaxSize", + "computed::MaxSize::none()", + engines="gecko servo-2013 servo-2020", + logical=logical, + logical_group="max-size", + allow_quirks="No" if logical else "Yes", + spec=spec % size, + animation_value_type="MaxSize", + servo_restyle_damage="reflow", + affects="layout", + )} +% endfor + +${helpers.single_keyword( + "box-sizing", + "content-box border-box", + engines="gecko servo-2013 servo-2020", + extra_prefixes="moz:layout.css.prefixes.box-sizing webkit", + spec="https://drafts.csswg.org/css-ui/#propdef-box-sizing", + gecko_enum_prefix="StyleBoxSizing", + custom_consts={ "content-box": "Content", "border-box": "Border" }, + animation_value_type="discrete", + servo_restyle_damage = "reflow", + affects="layout", +)} + +${helpers.single_keyword( + "object-fit", + "fill contain cover none scale-down", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-images/#propdef-object-fit", + gecko_enum_prefix = "StyleObjectFit", + affects="layout", +)} + +${helpers.predefined_type( + "object-position", + "Position", + "computed::Position::center()", + engines="gecko", + boxed=True, + spec="https://drafts.csswg.org/css-images-3/#the-object-position", + animation_value_type="ComputedValue", + affects="layout", +)} + +% for kind in ["row", "column"]: + % for range in ["start", "end"]: + ${helpers.predefined_type( + "grid-%s-%s" % (kind, range), + "GridLine", + "Default::default()", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-%s-%s" % (kind, range), + affects="layout", + )} + % endfor + + ${helpers.predefined_type( + "grid-auto-%ss" % kind, + "ImplicitGridTracks", + "Default::default()", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-auto-%ss" % kind, + affects="layout", + )} + + ${helpers.predefined_type( + "grid-template-%ss" % kind, + "GridTemplateComponent", + "specified::GenericGridTemplateComponent::None", + engines="gecko", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-template-%ss" % kind, + animation_value_type="ComputedValue", + affects="layout", + )} + +% endfor + +${helpers.predefined_type( + "masonry-auto-flow", + "MasonryAutoFlow", + "computed::MasonryAutoFlow::initial()", + engines="gecko", + gecko_pref="layout.css.grid-template-masonry-value.enabled", + animation_value_type="discrete", + spec="https://github.com/w3c/csswg-drafts/issues/4650", + affects="layout", +)} + +${helpers.predefined_type( + "grid-auto-flow", + "GridAutoFlow", + "computed::GridAutoFlow::ROW", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-auto-flow", + affects="layout", +)} + +${helpers.predefined_type( + "grid-template-areas", + "GridTemplateAreas", + "computed::GridTemplateAreas::none()", + engines="gecko", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-template-areas", + affects="layout", +)} + +${helpers.predefined_type( + "column-gap", + "length::NonNegativeLengthPercentageOrNormal", + "computed::length::NonNegativeLengthPercentageOrNormal::normal()", + engines="gecko servo-2013", + aliases="grid-column-gap" if engine == "gecko" else "", + servo_2013_pref="layout.columns.enabled", + spec="https://drafts.csswg.org/css-align-3/#propdef-column-gap", + animation_value_type="NonNegativeLengthPercentageOrNormal", + servo_restyle_damage="reflow", + affects="layout", +)} + +// no need for -moz- prefixed alias for this property +${helpers.predefined_type( + "row-gap", + "length::NonNegativeLengthPercentageOrNormal", + "computed::length::NonNegativeLengthPercentageOrNormal::normal()", + engines="gecko", + aliases="grid-row-gap", + spec="https://drafts.csswg.org/css-align-3/#propdef-row-gap", + animation_value_type="NonNegativeLengthPercentageOrNormal", + servo_restyle_damage="reflow", + affects="layout", +)} + +${helpers.predefined_type( + "aspect-ratio", + "AspectRatio", + "computed::AspectRatio::auto()", + engines="gecko servo-2013", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-sizing-4/#aspect-ratio", + servo_restyle_damage="reflow", + affects="layout", +)} + +% for (size, logical) in ALL_SIZES: + ${helpers.predefined_type( + "contain-intrinsic-" + size, + "ContainIntrinsicSize", + "computed::ContainIntrinsicSize::None", + engines="gecko", + logical_group="contain-intrinsic-size", + logical=logical, + gecko_pref="layout.css.contain-intrinsic-size.enabled", + spec="https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override", + animation_value_type="NonNegativeLength", + affects="layout", + )} +% endfor diff --git a/servo/components/style/properties/longhands/svg.mako.rs b/servo/components/style/properties/longhands/svg.mako.rs new file mode 100644 index 0000000000..10788d4802 --- /dev/null +++ b/servo/components/style/properties/longhands/svg.mako.rs @@ -0,0 +1,282 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("SVG", inherited=False, gecko_name="SVGReset") %> + +${helpers.single_keyword( + "vector-effect", + "none non-scaling-stroke", + engines="gecko", + gecko_enum_prefix="StyleVectorEffect", + animation_value_type="discrete", + spec="https://svgwg.org/svg2-draft/coords.html#VectorEffects", + affects="layout", +)} + +// Section 14 - Gradients and Patterns + +${helpers.predefined_type( + "stop-color", + "Color", + "computed::Color::BLACK", + engines="gecko", + animation_value_type="AnimatedRGBA", + spec="https://svgwg.org/svg2-draft/pservers.html#StopColorProperties", + affects="paint", +)} + +${helpers.predefined_type( + "stop-opacity", + "Opacity", + "1.0", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/pservers.html#StopOpacityProperty", + affects="paint", +)} + +// Filter Effects Module + +${helpers.predefined_type( + "flood-color", + "Color", + "computed::Color::BLACK", + engines="gecko", + animation_value_type="AnimatedColor", + spec="https://drafts.fxtf.org/filter-effects-1/#FloodColorProperty", + affects="paint", +)} + +${helpers.predefined_type( + "flood-opacity", + "Opacity", + "1.0", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://drafts.fxtf.org/filter-effects-1/#FloodOpacityProperty", + affects="paint", +)} + +${helpers.predefined_type( + "lighting-color", + "Color", + "computed::Color::WHITE", + engines="gecko", + animation_value_type="AnimatedColor", + spec="https://drafts.fxtf.org/filter-effects-1#LightingColorProperty", + affects="paint", +)} + +// CSS Masking Module Level 1 +// https://drafts.fxtf.org/css-masking-1 +${helpers.single_keyword( + "mask-type", + "luminance alpha", + engines="gecko", + gecko_enum_prefix="StyleMaskType", + animation_value_type="discrete", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-type", + affects="paint", +)} + +${helpers.predefined_type( + "clip-path", + "basic_shape::ClipPath", + "generics::basic_shape::ClipPath::None", + engines="gecko", + extra_prefixes="webkit", + animation_value_type="basic_shape::ClipPath", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-clip-path", + affects="paint", +)} + +${helpers.single_keyword( + "mask-mode", + "match-source alpha luminance", + engines="gecko", + gecko_enum_prefix="StyleMaskMode", + vector=True, + animation_value_type="discrete", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-mode", + affects="paint", +)} + +${helpers.predefined_type( + "mask-repeat", + "BackgroundRepeat", + "computed::BackgroundRepeat::repeat()", + engines="gecko", + initial_specified_value="specified::BackgroundRepeat::repeat()", + extra_prefixes="webkit", + animation_value_type="discrete", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-repeat", + vector=True, + affects="paint", +)} + +% for (axis, direction) in [("x", "Horizontal"), ("y", "Vertical")]: + ${helpers.predefined_type( + "mask-position-" + axis, + "position::" + direction + "Position", + "computed::LengthPercentage::zero_percent()", + engines="gecko", + extra_prefixes="webkit", + initial_specified_value="specified::PositionComponent::Center", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-position", + animation_value_type="ComputedValue", + vector_animation_type="repeatable_list", + vector=True, + affects="paint", + )} +% endfor + +${helpers.single_keyword( + "mask-clip", + "border-box content-box padding-box", + engines="gecko", + extra_gecko_values="fill-box stroke-box view-box no-clip", + vector=True, + extra_prefixes="webkit", + gecko_enum_prefix="StyleGeometryBox", + gecko_inexhaustive=True, + animation_value_type="discrete", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-clip", + affects="paint", +)} + +${helpers.single_keyword( + "mask-origin", + "border-box content-box padding-box", + engines="gecko", + extra_gecko_values="fill-box stroke-box view-box", + vector=True, + extra_prefixes="webkit", + gecko_enum_prefix="StyleGeometryBox", + gecko_inexhaustive=True, + animation_value_type="discrete", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-origin", + affects="paint", +)} + +${helpers.predefined_type( + "mask-size", + "background::BackgroundSize", + "computed::BackgroundSize::auto()", + engines="gecko", + initial_specified_value="specified::BackgroundSize::auto()", + extra_prefixes="webkit", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-size", + animation_value_type="MaskSizeList", + vector=True, + vector_animation_type="repeatable_list", + affects="paint", +)} + +${helpers.single_keyword( + "mask-composite", + "add subtract intersect exclude", + engines="gecko", + gecko_enum_prefix="StyleMaskComposite", + vector=True, + extra_prefixes="webkit", + animation_value_type="discrete", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-composite", + affects="paint", +)} + +${helpers.predefined_type( + "mask-image", + "Image", + engines="gecko", + initial_value="computed::Image::None", + initial_specified_value="specified::Image::None", + parse_method="parse_with_cors_anonymous", + spec="https://drafts.fxtf.org/css-masking-1/#propdef-mask-image", + vector=True, + extra_prefixes="webkit", + animation_value_type="discrete", + affects="paint", +)} + +${helpers.predefined_type( + "x", + "LengthPercentage", + "computed::LengthPercentage::zero()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/geometry.html#X", + affects="layout", +)} + +${helpers.predefined_type( + "y", + "LengthPercentage", + "computed::LengthPercentage::zero()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/geometry.html#Y", + affects="layout", +)} + +${helpers.predefined_type( + "cx", + "LengthPercentage", + "computed::LengthPercentage::zero()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/geometry.html#CX", + affects="layout", +)} + +${helpers.predefined_type( + "cy", + "LengthPercentage", + "computed::LengthPercentage::zero()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/geometry.html#CY", + affects="layout", +)} + +${helpers.predefined_type( + "rx", + "NonNegativeLengthPercentageOrAuto", + "computed::NonNegativeLengthPercentageOrAuto::auto()", + engines="gecko", + animation_value_type="LengthPercentageOrAuto", + spec="https://svgwg.org/svg2-draft/geometry.html#RX", + affects="layout", +)} + +${helpers.predefined_type( + "ry", + "NonNegativeLengthPercentageOrAuto", + "computed::NonNegativeLengthPercentageOrAuto::auto()", + engines="gecko", + animation_value_type="LengthPercentageOrAuto", + spec="https://svgwg.org/svg2-draft/geometry.html#RY", + affects="layout", +)} + +${helpers.predefined_type( + "r", + "NonNegativeLengthPercentage", + "computed::NonNegativeLengthPercentage::zero()", + engines="gecko", + animation_value_type="LengthPercentage", + spec="https://svgwg.org/svg2-draft/geometry.html#R", + affects="layout", +)} + +${helpers.predefined_type( + "d", + "DProperty", + "specified::DProperty::none()", + engines="gecko", + animation_value_type="ComputedValue", + spec="https://svgwg.org/svg2-draft/paths.html#TheDProperty", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/table.mako.rs b/servo/components/style/properties/longhands/table.mako.rs new file mode 100644 index 0000000000..3a756636ad --- /dev/null +++ b/servo/components/style/properties/longhands/table.mako.rs @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<% data.new_style_struct("Table", inherited=False) %> + +${helpers.single_keyword( + "table-layout", + "auto fixed", + engines="gecko servo-2013", + gecko_ffi_name="mLayoutStrategy", + animation_value_type="discrete", + gecko_enum_prefix="StyleTableLayout", + spec="https://drafts.csswg.org/css-tables/#propdef-table-layout", + servo_restyle_damage="reflow", + affects="layout", +)} + +${helpers.predefined_type( + "-x-span", + "Integer", + "1", + engines="gecko", + spec="Internal-only (for `<col span>` pres attr)", + animation_value_type="none", + enabled_in="", + affects="layout", +)} diff --git a/servo/components/style/properties/longhands/text.mako.rs b/servo/components/style/properties/longhands/text.mako.rs new file mode 100644 index 0000000000..0ee8ba3168 --- /dev/null +++ b/servo/components/style/properties/longhands/text.mako.rs @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import Method %> + +<% data.new_style_struct("Text", inherited=False, gecko_name="TextReset") %> + +${helpers.predefined_type( + "text-overflow", + "TextOverflow", + "computed::TextOverflow::get_initial_value()", + engines="gecko servo-2013", + animation_value_type="discrete", + boxed=True, + spec="https://drafts.csswg.org/css-ui/#propdef-text-overflow", + servo_restyle_damage="rebuild_and_reflow", + affects="paint", +)} + +${helpers.single_keyword( + "unicode-bidi", + "normal embed isolate bidi-override isolate-override plaintext", + engines="gecko servo-2013", + gecko_enum_prefix="StyleUnicodeBidi", + animation_value_type="none", + spec="https://drafts.csswg.org/css-writing-modes/#propdef-unicode-bidi", + servo_restyle_damage="rebuild_and_reflow", + affects="layout", +)} + +${helpers.predefined_type( + "text-decoration-line", + "TextDecorationLine", + "specified::TextDecorationLine::none()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::TextDecorationLine::none()", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration-line", + servo_restyle_damage="rebuild_and_reflow", + affects="overflow", +)} + +${helpers.single_keyword( + "text-decoration-style", + "solid double dotted dashed wavy -moz-none", + engines="gecko servo-2020", + gecko_enum_prefix="StyleTextDecorationStyle", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration-style", + affects="overflow", +)} + +${helpers.predefined_type( + "text-decoration-color", + "Color", + "computed_value::T::currentcolor()", + engines="gecko servo-2020", + initial_specified_value="specified::Color::currentcolor()", + animation_value_type="AnimatedColor", + ignored_when_colors_disabled=True, + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration-color", + affects="paint", +)} + +${helpers.predefined_type( + "initial-letter", + "InitialLetter", + "computed::InitialLetter::normal()", + engines="gecko", + initial_specified_value="specified::InitialLetter::normal()", + animation_value_type="discrete", + gecko_pref="layout.css.initial-letter.enabled", + spec="https://drafts.csswg.org/css-inline/#sizing-drop-initials", + affects="layout", +)} + +${helpers.predefined_type( + "text-decoration-thickness", + "TextDecorationLength", + "generics::text::GenericTextDecorationLength::Auto", + engines="gecko", + initial_specified_value="generics::text::GenericTextDecorationLength::Auto", + animation_value_type="ComputedValue", + spec="https://drafts.csswg.org/css-text-decor-4/#text-decoration-width-property", + affects="overflow", +)} diff --git a/servo/components/style/properties/longhands/ui.mako.rs b/servo/components/style/properties/longhands/ui.mako.rs new file mode 100644 index 0000000000..1150816ac0 --- /dev/null +++ b/servo/components/style/properties/longhands/ui.mako.rs @@ -0,0 +1,422 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import DEFAULT_RULES_EXCEPT_KEYFRAME, Method %> + +// CSS Basic User Interface Module Level 1 +// https://drafts.csswg.org/css-ui-3/ +<% data.new_style_struct("UI", inherited=False, gecko_name="UIReset") %> + +// TODO spec says that UAs should not support this +// we should probably remove from gecko (https://bugzilla.mozilla.org/show_bug.cgi?id=1328331) +${helpers.single_keyword( + "ime-mode", + "auto normal active disabled inactive", + engines="gecko", + gecko_enum_prefix="StyleImeMode", + gecko_ffi_name="mIMEMode", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-ui/#input-method-editor", + affects="", +)} + +${helpers.single_keyword( + "scrollbar-width", + "auto thin none", + engines="gecko", + gecko_enum_prefix="StyleScrollbarWidth", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-scrollbars-1/#scrollbar-width", + affects="layout", +)} + +${helpers.predefined_type( + "user-select", + "UserSelect", + "computed::UserSelect::Auto", + engines="gecko", + extra_prefixes="moz webkit", + animation_value_type="discrete", + spec="https://drafts.csswg.org/css-ui-4/#propdef-user-select", + affects="", +)} + +// TODO(emilio): This probably should be hidden from content. +${helpers.single_keyword( + "-moz-window-dragging", + "default drag no-drag", + engines="gecko", + gecko_ffi_name="mWindowDragging", + gecko_enum_prefix="StyleWindowDragging", + animation_value_type="discrete", + spec="None (Nonstandard Firefox-only property)", + affects="paint", +)} + +// TODO(emilio): Maybe make shadow behavior on macOS match Linux / Windows, and remove this? But +// that requires making -moz-window-input-region-margin work there... +${helpers.single_keyword( + "-moz-window-shadow", + "auto none", + engines="gecko", + gecko_ffi_name="mWindowShadow", + gecko_enum_prefix="StyleWindowShadow", + animation_value_type="discrete", + enabled_in="chrome", + spec="None (Nonstandard internal property)", + affects="overflow", +)} + +${helpers.predefined_type( + "-moz-window-opacity", + "Opacity", + "1.0", + engines="gecko", + gecko_ffi_name="mWindowOpacity", + animation_value_type="ComputedValue", + spec="None (Nonstandard internal property)", + enabled_in="chrome", + affects="paint", +)} + +${helpers.predefined_type( + "-moz-window-transform", + "Transform", + "generics::transform::Transform::none()", + engines="gecko", + animation_value_type="ComputedValue", + spec="None (Nonstandard internal property)", + enabled_in="chrome", + affects="overflow", +)} + +${helpers.predefined_type( + "-moz-window-transform-origin", + "TransformOrigin", + "computed::TransformOrigin::initial_value()", + engines="gecko", + animation_value_type="ComputedValue", + gecko_ffi_name="mWindowTransformOrigin", + boxed=True, + spec="None (Nonstandard internal property)", + enabled_in="chrome", + affects="overflow", +)} + +${helpers.predefined_type( + "-moz-window-input-region-margin", + "Length", + "computed::Length::zero()", + engines="gecko", + animation_value_type="ComputedValue", + spec="None (Nonstandard internal property)", + enabled_in="chrome", + affects="", +)} + +// Hack to allow chrome to hide stuff only visually (without hiding it from a11y). +${helpers.predefined_type( + "-moz-subtree-hidden-only-visually", + "BoolInteger", + "computed::BoolInteger::zero()", + engines="gecko", + animation_value_type="discrete", + spec="None (Nonstandard internal property)", + enabled_in="chrome", + affects="paint", +)} + +// TODO(emilio): Probably also should be hidden from content. +${helpers.predefined_type( + "-moz-force-broken-image-icon", + "BoolInteger", + "computed::BoolInteger::zero()", + engines="gecko", + animation_value_type="discrete", + spec="None (Nonstandard Firefox-only property)", + affects="layout", +)} + +<% transition_extra_prefixes = "moz:layout.css.prefixes.transitions webkit" %> + +${helpers.predefined_type( + "transition-duration", + "Time", + "computed::Time::zero()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::Time::zero()", + parse_method="parse_non_negative", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=transition_extra_prefixes, + spec="https://drafts.csswg.org/css-transitions/#propdef-transition-duration", + affects="", +)} + +${helpers.predefined_type( + "transition-timing-function", + "TimingFunction", + "computed::TimingFunction::ease()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::TimingFunction::ease()", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=transition_extra_prefixes, + spec="https://drafts.csswg.org/css-transitions/#propdef-transition-timing-function", + affects="", +)} + +${helpers.predefined_type( + "transition-property", + "TransitionProperty", + "computed::TransitionProperty::all()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::TransitionProperty::all()", + vector=True, + none_value="computed::TransitionProperty::none()", + need_index=True, + animation_value_type="none", + extra_prefixes=transition_extra_prefixes, + spec="https://drafts.csswg.org/css-transitions/#propdef-transition-property", + affects="", +)} + +${helpers.predefined_type( + "transition-delay", + "Time", + "computed::Time::zero()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::Time::zero()", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=transition_extra_prefixes, + spec="https://drafts.csswg.org/css-transitions/#propdef-transition-delay", + affects="", +)} + +<% animation_extra_prefixes = "moz:layout.css.prefixes.animations webkit" %> + +${helpers.predefined_type( + "animation-name", + "AnimationName", + "computed::AnimationName::none()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::AnimationName::none()", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + spec="https://drafts.csswg.org/css-animations/#propdef-animation-name", + affects="", +)} + +${helpers.predefined_type( + "animation-duration", + "Time", + "computed::Time::zero()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::Time::zero()", + parse_method="parse_non_negative", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + spec="https://drafts.csswg.org/css-transitions/#propdef-transition-duration", + affects="", +)} + +// animation-timing-function is the exception to the rule for allowed_in_keyframe_block: +// https://drafts.csswg.org/css-animations/#keyframes +${helpers.predefined_type( + "animation-timing-function", + "TimingFunction", + "computed::TimingFunction::ease()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::TimingFunction::ease()", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + spec="https://drafts.csswg.org/css-transitions/#propdef-animation-timing-function", + affects="", +)} + +${helpers.predefined_type( + "animation-iteration-count", + "AnimationIterationCount", + "computed::AnimationIterationCount::one()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::AnimationIterationCount::one()", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + spec="https://drafts.csswg.org/css-animations/#propdef-animation-iteration-count", + affects="", +)} + +${helpers.predefined_type( + "animation-direction", + "AnimationDirection", + "computed::AnimationDirection::Normal", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::AnimationDirection::Normal", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + spec="https://drafts.csswg.org/css-animations/#propdef-animation-direction", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "animation-play-state", + "AnimationPlayState", + "computed::AnimationPlayState::Running", + engines="gecko servo-2013 servo-2020", + initial_specified_value="computed::AnimationPlayState::Running", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + spec="https://drafts.csswg.org/css-animations/#propdef-animation-play-state", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "animation-fill-mode", + "AnimationFillMode", + "computed::AnimationFillMode::None", + engines="gecko servo-2013 servo-2020", + initial_specified_value="computed::AnimationFillMode::None", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + spec="https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "animation-composition", + "AnimationComposition", + "computed::AnimationComposition::Replace", + engines="gecko", + initial_specified_value="computed::AnimationComposition::Replace", + vector=True, + need_index=True, + animation_value_type="none", + gecko_pref="layout.css.animation-composition.enabled", + spec="https://drafts.csswg.org/css-animations-2/#animation-composition", + affects="", +)} + +${helpers.predefined_type( + "animation-delay", + "Time", + "computed::Time::zero()", + engines="gecko servo-2013 servo-2020", + initial_specified_value="specified::Time::zero()", + vector=True, + need_index=True, + animation_value_type="none", + extra_prefixes=animation_extra_prefixes, + spec="https://drafts.csswg.org/css-animations/#propdef-animation-delay", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "animation-timeline", + "AnimationTimeline", + "computed::AnimationTimeline::auto()", + engines="gecko", + initial_specified_value="specified::AnimationTimeline::auto()", + vector=True, + need_index=True, + animation_value_type="none", + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/css-animations-2/#propdef-animation-timeline", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "scroll-timeline-name", + "ScrollTimelineName", + "computed::ScrollTimelineName::none()", + vector=True, + need_index=True, + engines="gecko", + animation_value_type="none", + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-name", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "scroll-timeline-axis", + "ScrollAxis", + "computed::ScrollAxis::default()", + vector=True, + need_index=True, + engines="gecko", + animation_value_type="none", + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "view-timeline-name", + "ScrollTimelineName", + "computed::ScrollTimelineName::none()", + vector=True, + need_index=True, + engines="gecko", + animation_value_type="none", + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-name", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "view-timeline-axis", + "ScrollAxis", + "computed::ScrollAxis::default()", + vector=True, + need_index=True, + engines="gecko", + animation_value_type="none", + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} + +${helpers.predefined_type( + "view-timeline-inset", + "ViewTimelineInset", + "computed::ViewTimelineInset::default()", + vector=True, + need_index=True, + engines="gecko", + animation_value_type="none", + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis", + rule_types_allowed=DEFAULT_RULES_EXCEPT_KEYFRAME, + affects="", +)} diff --git a/servo/components/style/properties/longhands/xul.mako.rs b/servo/components/style/properties/longhands/xul.mako.rs new file mode 100644 index 0000000000..8974ac30dc --- /dev/null +++ b/servo/components/style/properties/longhands/xul.mako.rs @@ -0,0 +1,85 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import Method %> + +// Non-standard properties that Gecko uses for XUL elements. +<% data.new_style_struct("XUL", inherited=False) %> + +${helpers.single_keyword( + "-moz-box-align", + "stretch start center baseline end", + engines="gecko", + gecko_ffi_name="mBoxAlign", + gecko_enum_prefix="StyleBoxAlign", + animation_value_type="discrete", + aliases="-webkit-box-align", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-align)", + affects="layout", +)} + +${helpers.single_keyword( + "-moz-box-direction", + "normal reverse", + engines="gecko", + gecko_ffi_name="mBoxDirection", + gecko_enum_prefix="StyleBoxDirection", + animation_value_type="discrete", + aliases="-webkit-box-direction", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-direction)", + affects="layout", +)} + +${helpers.predefined_type( + "-moz-box-flex", + "NonNegativeNumber", + "From::from(0.)", + engines="gecko", + gecko_ffi_name="mBoxFlex", + animation_value_type="NonNegativeNumber", + aliases="-webkit-box-flex", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-flex)", + affects="layout", +)} + +${helpers.single_keyword( + "-moz-box-orient", + "horizontal vertical", + engines="gecko", + gecko_ffi_name="mBoxOrient", + gecko_aliases="inline-axis=horizontal block-axis=vertical", + gecko_enum_prefix="StyleBoxOrient", + animation_value_type="discrete", + aliases="-webkit-box-orient", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-orient)", + affects="layout", +)} + +${helpers.single_keyword( + "-moz-box-pack", + "start center end justify", + engines="gecko", + gecko_ffi_name="mBoxPack", + gecko_enum_prefix="StyleBoxPack", + animation_value_type="discrete", + aliases="-webkit-box-pack", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/box-pack)", + affects="layout", +)} + +// NOTE(heycam): Odd that the initial value is 1 yet 0 is a valid value. There +// are uses of `-moz-box-ordinal-group: 0` in the tree, too. +${helpers.predefined_type( + "-moz-box-ordinal-group", + "Integer", + "1", + engines="gecko", + parse_method="parse_non_negative", + aliases="-webkit-box-ordinal-group", + gecko_ffi_name="mBoxOrdinal", + animation_value_type="discrete", + spec="Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-box-ordinal-group)", + affects="layout", +)} diff --git a/servo/components/style/properties/mod.rs b/servo/components/style/properties/mod.rs new file mode 100644 index 0000000000..7adb6d4ae6 --- /dev/null +++ b/servo/components/style/properties/mod.rs @@ -0,0 +1,1531 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Supported CSS properties and the cascade. + +pub mod cascade; +pub mod declaration_block; + +pub use self::cascade::*; +pub use self::declaration_block::*; +pub use self::generated::*; +/// The CSS properties supported by the style system. +/// Generated from the properties.mako.rs template by build.rs +#[macro_use] +#[allow(unsafe_code)] +#[deny(missing_docs)] +pub mod generated { + include!(concat!(env!("OUT_DIR"), "/properties.rs")); + + #[cfg(feature = "gecko")] + #[allow(unsafe_code, missing_docs)] + pub mod gecko { + include!(concat!(env!("OUT_DIR"), "/gecko_properties.rs")); + } +} + +use crate::custom_properties::{self, ComputedCustomProperties}; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs::{nsCSSPropertyID, AnimatedPropertyID, RefPtr}; +use crate::logical_geometry::WritingMode; +use crate::parser::ParserContext; +use crate::str::CssString; +use crate::stylesheets::Origin; +use crate::stylist::Stylist; +use crate::values::{computed, serialize_atom_name}; +use arrayvec::{ArrayVec, Drain as ArrayVecDrain}; +use cssparser::{Parser, ParserInput}; +use fxhash::FxHashMap; +use servo_arc::Arc; +use std::{ + borrow::Cow, + fmt::{self, Write}, + mem, +}; +use style_traits::{ + CssWriter, KeywordsCollectFn, ParseError, ParsingMode, SpecifiedValueInfo, ToCss, +}; + +bitflags! { + /// A set of flags for properties. + #[derive(Clone, Copy)] + pub struct PropertyFlags: u16 { + /// This longhand property applies to ::first-letter. + const APPLIES_TO_FIRST_LETTER = 1 << 1; + /// This longhand property applies to ::first-line. + const APPLIES_TO_FIRST_LINE = 1 << 2; + /// This longhand property applies to ::placeholder. + const APPLIES_TO_PLACEHOLDER = 1 << 3; + /// This longhand property applies to ::cue. + const APPLIES_TO_CUE = 1 << 4; + /// This longhand property applies to ::marker. + const APPLIES_TO_MARKER = 1 << 5; + /// This property is a legacy shorthand. + /// + /// https://drafts.csswg.org/css-cascade/#legacy-shorthand + const IS_LEGACY_SHORTHAND = 1 << 6; + + /* The following flags are currently not used in Rust code, they + * only need to be listed in corresponding properties so that + * they can be checked in the C++ side via ServoCSSPropList.h. */ + + /// This property can be animated on the compositor. + const CAN_ANIMATE_ON_COMPOSITOR = 0; + /// This shorthand property is accessible from getComputedStyle. + const SHORTHAND_IN_GETCS = 0; + /// See data.py's documentation about the affects_flags. + const AFFECTS_LAYOUT = 0; + #[allow(missing_docs)] + const AFFECTS_OVERFLOW = 0; + #[allow(missing_docs)] + const AFFECTS_PAINT = 0; + } +} + +/// An enum to represent a CSS Wide keyword. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum CSSWideKeyword { + /// The `initial` keyword. + Initial, + /// The `inherit` keyword. + Inherit, + /// The `unset` keyword. + Unset, + /// The `revert` keyword. + Revert, + /// The `revert-layer` keyword. + RevertLayer, +} + +impl CSSWideKeyword { + /// Returns the string representation of the keyword. + pub fn to_str(&self) -> &'static str { + match *self { + CSSWideKeyword::Initial => "initial", + CSSWideKeyword::Inherit => "inherit", + CSSWideKeyword::Unset => "unset", + CSSWideKeyword::Revert => "revert", + CSSWideKeyword::RevertLayer => "revert-layer", + } + } +} + +impl CSSWideKeyword { + /// Parses a CSS wide keyword from a CSS identifier. + pub fn from_ident(ident: &str) -> Result<Self, ()> { + Ok(match_ignore_ascii_case! { ident, + "initial" => CSSWideKeyword::Initial, + "inherit" => CSSWideKeyword::Inherit, + "unset" => CSSWideKeyword::Unset, + "revert" => CSSWideKeyword::Revert, + "revert-layer" => CSSWideKeyword::RevertLayer, + _ => return Err(()), + }) + } + + /// Parses a CSS wide keyword completely. + pub fn parse(input: &mut Parser) -> Result<Self, ()> { + let keyword = { + let ident = input.expect_ident().map_err(|_| ())?; + Self::from_ident(ident)? + }; + input.expect_exhausted().map_err(|_| ())?; + Ok(keyword) + } +} + +/// A declaration using a CSS-wide keyword. +#[derive(Clone, PartialEq, ToCss, ToShmem, MallocSizeOf)] +pub struct WideKeywordDeclaration { + #[css(skip)] + id: LonghandId, + /// The CSS-wide keyword. + pub keyword: CSSWideKeyword, +} + +/// An unparsed declaration that contains `var()` functions. +#[derive(Clone, PartialEq, ToCss, ToShmem, MallocSizeOf)] +pub struct VariableDeclaration { + /// The id of the property this declaration represents. + #[css(skip)] + id: LonghandId, + /// The unparsed value of the variable. + #[ignore_malloc_size_of = "Arc"] + pub value: Arc<UnparsedValue>, +} + +/// A custom property declaration value is either an unparsed value or a CSS +/// wide-keyword. +#[derive(Clone, PartialEq, ToCss, ToShmem)] +pub enum CustomDeclarationValue { + /// A value. + Value(Arc<custom_properties::SpecifiedValue>), + /// A wide keyword. + CSSWideKeyword(CSSWideKeyword), +} + +/// A custom property declaration with the property name and the declared value. +#[derive(Clone, PartialEq, ToCss, ToShmem, MallocSizeOf)] +pub struct CustomDeclaration { + /// The name of the custom property. + #[css(skip)] + pub name: custom_properties::Name, + /// The value of the custom property. + #[ignore_malloc_size_of = "Arc"] + pub value: CustomDeclarationValue, +} + +impl fmt::Debug for PropertyDeclaration { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.id().to_css(&mut CssWriter::new(f))?; + f.write_str(": ")?; + + // Because PropertyDeclaration::to_css requires CssStringWriter, we can't write + // it directly to f, and need to allocate an intermediate string. This is + // fine for debug-only code. + let mut s = CssString::new(); + self.to_css(&mut s)?; + write!(f, "{}", s) + } +} + +/// A longhand or shorthand property. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, ToComputedValue, ToResolvedValue, ToShmem, MallocSizeOf)] +#[repr(C)] +pub struct NonCustomPropertyId(u16); + +impl ToCss for NonCustomPropertyId { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(self.name()) + } +} + +impl NonCustomPropertyId { + /// Returns the underlying index, used for use counter. + pub fn bit(self) -> usize { + self.0 as usize + } + + /// Convert a `NonCustomPropertyId` into a `nsCSSPropertyID`. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_nscsspropertyid(self) -> nsCSSPropertyID { + // unsafe: guaranteed by static_assert_nscsspropertyid. + unsafe { mem::transmute(self.0 as i32) } + } + + /// Convert an `nsCSSPropertyID` into a `NonCustomPropertyId`. + #[cfg(feature = "gecko")] + #[inline] + pub fn from_nscsspropertyid(prop: nsCSSPropertyID) -> Option<Self> { + let prop = prop as i32; + if prop < 0 || prop >= property_counts::NON_CUSTOM as i32 { + return None; + } + // guaranteed by static_assert_nscsspropertyid above. + Some(NonCustomPropertyId(prop as u16)) + } + + /// Resolves the alias of a given property if needed. + pub fn unaliased(self) -> Self { + let Some(alias_id) = self.as_alias() else { + return self; + }; + alias_id.aliased_property() + } + + /// Turns this `NonCustomPropertyId` into a `PropertyId`. + #[inline] + pub fn to_property_id(self) -> PropertyId { + PropertyId::NonCustom(self) + } + + /// Returns a longhand id, if this property is one. + #[inline] + pub fn as_longhand(self) -> Option<LonghandId> { + if self.0 < property_counts::LONGHANDS as u16 { + return Some(unsafe { mem::transmute(self.0 as u16) }); + } + None + } + + /// Returns a shorthand id, if this property is one. + #[inline] + pub fn as_shorthand(self) -> Option<ShorthandId> { + if self.0 >= property_counts::LONGHANDS as u16 && + self.0 < property_counts::LONGHANDS_AND_SHORTHANDS as u16 + { + return Some(unsafe { mem::transmute(self.0 - (property_counts::LONGHANDS as u16)) }); + } + None + } + + /// Returns an alias id, if this property is one. + #[inline] + pub fn as_alias(self) -> Option<AliasId> { + debug_assert!((self.0 as usize) < property_counts::NON_CUSTOM); + if self.0 >= property_counts::LONGHANDS_AND_SHORTHANDS as u16 { + return Some(unsafe { + mem::transmute(self.0 - (property_counts::LONGHANDS_AND_SHORTHANDS as u16)) + }); + } + None + } + + /// Returns either a longhand or a shorthand, resolving aliases. + #[inline] + pub fn longhand_or_shorthand(self) -> Result<LonghandId, ShorthandId> { + let id = self.unaliased(); + match id.as_longhand() { + Some(lh) => Ok(lh), + None => Err(id.as_shorthand().unwrap()), + } + } + + /// Converts a longhand id into a non-custom property id. + #[inline] + pub const fn from_longhand(id: LonghandId) -> Self { + Self(id as u16) + } + + /// Converts a shorthand id into a non-custom property id. + #[inline] + pub const fn from_shorthand(id: ShorthandId) -> Self { + Self((id as u16) + (property_counts::LONGHANDS as u16)) + } + + /// Converts an alias id into a non-custom property id. + #[inline] + pub const fn from_alias(id: AliasId) -> Self { + Self((id as u16) + (property_counts::LONGHANDS_AND_SHORTHANDS as u16)) + } +} + +impl From<LonghandId> for NonCustomPropertyId { + #[inline] + fn from(id: LonghandId) -> Self { + Self::from_longhand(id) + } +} + +impl From<ShorthandId> for NonCustomPropertyId { + #[inline] + fn from(id: ShorthandId) -> Self { + Self::from_shorthand(id) + } +} + +impl From<AliasId> for NonCustomPropertyId { + #[inline] + fn from(id: AliasId) -> Self { + Self::from_alias(id) + } +} + +/// Representation of a CSS property, that is, either a longhand, a shorthand, or a custom +/// property. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum PropertyId { + /// An alias for a shorthand property. + NonCustom(NonCustomPropertyId), + /// A custom property. + Custom(custom_properties::Name), +} + +impl ToCss for PropertyId { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + PropertyId::NonCustom(id) => dest.write_str(id.name()), + PropertyId::Custom(ref name) => { + dest.write_str("--")?; + serialize_atom_name(name, dest) + }, + } + } +} + +impl PropertyId { + /// Return the longhand id that this property id represents. + #[inline] + pub fn longhand_id(&self) -> Option<LonghandId> { + self.non_custom_non_alias_id()?.as_longhand() + } + + /// Returns true if this property is one of the animatable properties. + pub fn is_animatable(&self) -> bool { + match self { + Self::NonCustom(id) => id.is_animatable(), + Self::Custom(..) => true, + } + } + + /// Returns a given property from the given name, _regardless of whether it is enabled or + /// not_, or Err(()) for unknown properties. + /// + /// Do not use for non-testing purposes. + pub fn parse_unchecked_for_testing(name: &str) -> Result<Self, ()> { + Self::parse_unchecked(name, None) + } + + /// Parses a property name, and returns an error if it's unknown or isn't enabled for all + /// content. + #[inline] + pub fn parse_enabled_for_all_content(name: &str) -> Result<Self, ()> { + let id = Self::parse_unchecked(name, None)?; + + if !id.enabled_for_all_content() { + return Err(()); + } + + Ok(id) + } + + /// Parses a property name, and returns an error if it's unknown or isn't allowed in this + /// context. + #[inline] + pub fn parse(name: &str, context: &ParserContext) -> Result<Self, ()> { + let id = Self::parse_unchecked(name, context.use_counters)?; + if !id.allowed_in(context) { + return Err(()); + } + Ok(id) + } + + /// Parses a property name, and returns an error if it's unknown or isn't allowed in this + /// context, ignoring the rule_type checks. + /// + /// This is useful for parsing stuff from CSS values, for example. + #[inline] + pub fn parse_ignoring_rule_type(name: &str, context: &ParserContext) -> Result<Self, ()> { + let id = Self::parse_unchecked(name, None)?; + if !id.allowed_in_ignoring_rule_type(context) { + return Err(()); + } + Ok(id) + } + + /// Returns a property id from Gecko's nsCSSPropertyID. + #[cfg(feature = "gecko")] + #[inline] + pub fn from_nscsspropertyid(id: nsCSSPropertyID) -> Option<Self> { + Some(NonCustomPropertyId::from_nscsspropertyid(id)?.to_property_id()) + } + + /// Returns a property id from Gecko's AnimatedPropertyID. + #[cfg(feature = "gecko")] + #[inline] + pub fn from_gecko_animated_property_id(property: &AnimatedPropertyID) -> Option<Self> { + Some( + if property.mID == nsCSSPropertyID::eCSSPropertyExtra_variable { + debug_assert!(!property.mCustomName.mRawPtr.is_null()); + Self::Custom(unsafe { crate::Atom::from_raw(property.mCustomName.mRawPtr) }) + } else { + Self::NonCustom(NonCustomPropertyId::from_nscsspropertyid(property.mID)?) + }, + ) + } + + /// Returns true if the property is a shorthand or shorthand alias. + #[inline] + pub fn is_shorthand(&self) -> bool { + self.as_shorthand().is_ok() + } + + /// Given this property id, get it either as a shorthand or as a + /// `PropertyDeclarationId`. + pub fn as_shorthand(&self) -> Result<ShorthandId, PropertyDeclarationId> { + match *self { + Self::NonCustom(id) => match id.longhand_or_shorthand() { + Ok(lh) => Err(PropertyDeclarationId::Longhand(lh)), + Err(sh) => Ok(sh), + }, + Self::Custom(ref name) => Err(PropertyDeclarationId::Custom(name)), + } + } + + /// Returns the `NonCustomPropertyId` corresponding to this property id. + pub fn non_custom_id(&self) -> Option<NonCustomPropertyId> { + match *self { + Self::Custom(_) => None, + Self::NonCustom(id) => Some(id), + } + } + + /// Returns non-alias NonCustomPropertyId corresponding to this + /// property id. + fn non_custom_non_alias_id(&self) -> Option<NonCustomPropertyId> { + self.non_custom_id().map(NonCustomPropertyId::unaliased) + } + + /// Whether the property is enabled for all content regardless of the + /// stylesheet it was declared on (that is, in practice only checks prefs). + #[inline] + pub fn enabled_for_all_content(&self) -> bool { + let id = match self.non_custom_id() { + // Custom properties are allowed everywhere + None => return true, + Some(id) => id, + }; + + id.enabled_for_all_content() + } + + /// Converts this PropertyId in nsCSSPropertyID, resolving aliases to the + /// resolved property, and returning eCSSPropertyExtra_variable for custom + /// properties. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_nscsspropertyid_resolving_aliases(&self) -> nsCSSPropertyID { + match self.non_custom_non_alias_id() { + Some(id) => id.to_nscsspropertyid(), + None => nsCSSPropertyID::eCSSPropertyExtra_variable, + } + } + + fn allowed_in(&self, context: &ParserContext) -> bool { + let id = match self.non_custom_id() { + // Custom properties are allowed everywhere + None => return true, + Some(id) => id, + }; + id.allowed_in(context) + } + + #[inline] + fn allowed_in_ignoring_rule_type(&self, context: &ParserContext) -> bool { + let id = match self.non_custom_id() { + // Custom properties are allowed everywhere + None => return true, + Some(id) => id, + }; + id.allowed_in_ignoring_rule_type(context) + } + + /// Whether the property supports the given CSS type. + /// `ty` should a bitflags of constants in style_traits::CssType. + pub fn supports_type(&self, ty: u8) -> bool { + let id = self.non_custom_non_alias_id(); + id.map_or(0, |id| id.supported_types()) & ty != 0 + } + + /// Collect supported starting word of values of this property. + /// + /// See style_traits::SpecifiedValueInfo::collect_completion_keywords for more + /// details. + pub fn collect_property_completion_keywords(&self, f: KeywordsCollectFn) { + if let Some(id) = self.non_custom_non_alias_id() { + id.collect_property_completion_keywords(f); + } + CSSWideKeyword::collect_completion_keywords(f); + } +} + +impl ToCss for LonghandId { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(self.name()) + } +} + +impl fmt::Debug for LonghandId { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str(self.name()) + } +} + +impl LonghandId { + /// Get the name of this longhand property. + #[inline] + pub fn name(&self) -> &'static str { + NonCustomPropertyId::from(*self).name() + } + + /// Returns whether the longhand property is inherited by default. + #[inline] + pub fn inherited(self) -> bool { + !LonghandIdSet::reset().contains(self) + } + + /// Returns true if the property is one that is ignored when document + /// colors are disabled. + #[inline] + pub fn ignored_when_document_colors_disabled(self) -> bool { + LonghandIdSet::ignored_when_colors_disabled().contains(self) + } + + /// Returns whether this longhand is `non_custom` or is a longhand of it. + pub fn is_or_is_longhand_of(self, non_custom: NonCustomPropertyId) -> bool { + match non_custom.longhand_or_shorthand() { + Ok(lh) => self == lh, + Err(sh) => self.is_longhand_of(sh), + } + } + + /// Returns whether this longhand is a longhand of `shorthand`. + pub fn is_longhand_of(self, shorthand: ShorthandId) -> bool { + self.shorthands().any(|s| s == shorthand) + } + + /// Returns whether this property is animatable. + #[inline] + pub fn is_animatable(self) -> bool { + NonCustomPropertyId::from(self).is_animatable() + } + + /// Returns whether this property is animatable in a discrete way. + #[inline] + pub fn is_discrete_animatable(self) -> bool { + LonghandIdSet::discrete_animatable().contains(self) + } + + /// Converts from a LonghandId to an adequate nsCSSPropertyID. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_nscsspropertyid(self) -> nsCSSPropertyID { + NonCustomPropertyId::from(self).to_nscsspropertyid() + } + + #[cfg(feature = "gecko")] + /// Returns a longhand id from Gecko's nsCSSPropertyID. + pub fn from_nscsspropertyid(id: nsCSSPropertyID) -> Option<Self> { + NonCustomPropertyId::from_nscsspropertyid(id)? + .unaliased() + .as_longhand() + } + + /// Return whether this property is logical. + #[inline] + pub fn is_logical(self) -> bool { + LonghandIdSet::logical().contains(self) + } +} + +impl ToCss for ShorthandId { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(self.name()) + } +} + +impl ShorthandId { + /// Get the name for this shorthand property. + #[inline] + pub fn name(&self) -> &'static str { + NonCustomPropertyId::from(*self).name() + } + + /// Converts from a ShorthandId to an adequate nsCSSPropertyID. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_nscsspropertyid(self) -> nsCSSPropertyID { + NonCustomPropertyId::from(self).to_nscsspropertyid() + } + + /// Converts from a nsCSSPropertyID to a ShorthandId. + #[cfg(feature = "gecko")] + #[inline] + pub fn from_nscsspropertyid(id: nsCSSPropertyID) -> Option<Self> { + NonCustomPropertyId::from_nscsspropertyid(id)? + .unaliased() + .as_shorthand() + } + + /// Finds and returns an appendable value for the given declarations. + /// + /// Returns the optional appendable value. + pub fn get_shorthand_appendable_value<'a, 'b: 'a>( + self, + declarations: &'a [&'b PropertyDeclaration], + ) -> Option<AppendableValue<'a, 'b>> { + let first_declaration = declarations.get(0)?; + let rest = || declarations.iter().skip(1); + + // https://drafts.csswg.org/css-variables/#variables-in-shorthands + if let Some(css) = first_declaration.with_variables_from_shorthand(self) { + if rest().all(|d| d.with_variables_from_shorthand(self) == Some(css)) { + return Some(AppendableValue::Css(css)); + } + return None; + } + + // Check whether they are all the same CSS-wide keyword. + if let Some(keyword) = first_declaration.get_css_wide_keyword() { + if rest().all(|d| d.get_css_wide_keyword() == Some(keyword)) { + return Some(AppendableValue::Css(keyword.to_str())); + } + return None; + } + + if self == ShorthandId::All { + // 'all' only supports variables and CSS wide keywords. + return None; + } + + // Check whether all declarations can be serialized as part of shorthand. + if declarations + .iter() + .all(|d| d.may_serialize_as_part_of_shorthand()) + { + return Some(AppendableValue::DeclarationsForShorthand( + self, + declarations, + )); + } + + None + } + + /// Returns whether this property is a legacy shorthand. + #[inline] + pub fn is_legacy_shorthand(self) -> bool { + self.flags().contains(PropertyFlags::IS_LEGACY_SHORTHAND) + } +} + +impl PropertyDeclaration { + fn with_variables_from_shorthand(&self, shorthand: ShorthandId) -> Option<&str> { + match *self { + PropertyDeclaration::WithVariables(ref declaration) => { + let s = declaration.value.from_shorthand?; + if s != shorthand { + return None; + } + Some(&*declaration.value.variable_value.css) + }, + _ => None, + } + } + + /// Returns a CSS-wide keyword declaration for a given property. + #[inline] + pub fn css_wide_keyword(id: LonghandId, keyword: CSSWideKeyword) -> Self { + Self::CSSWideKeyword(WideKeywordDeclaration { id, keyword }) + } + + /// Returns a CSS-wide keyword if the declaration's value is one. + #[inline] + pub fn get_css_wide_keyword(&self) -> Option<CSSWideKeyword> { + match *self { + PropertyDeclaration::CSSWideKeyword(ref declaration) => Some(declaration.keyword), + _ => None, + } + } + + /// Returns whether the declaration may be serialized as part of a shorthand. + /// + /// This method returns false if this declaration contains variable or has a + /// CSS-wide keyword value, since these values cannot be serialized as part + /// of a shorthand. + /// + /// Caller should check `with_variables_from_shorthand()` and whether all + /// needed declarations has the same CSS-wide keyword first. + /// + /// Note that, serialization of a shorthand may still fail because of other + /// property-specific requirement even when this method returns true for all + /// the longhand declarations. + pub fn may_serialize_as_part_of_shorthand(&self) -> bool { + match *self { + PropertyDeclaration::CSSWideKeyword(..) | PropertyDeclaration::WithVariables(..) => { + false + }, + PropertyDeclaration::Custom(..) => { + unreachable!("Serializing a custom property as part of shorthand?") + }, + _ => true, + } + } + + /// Returns true if this property declaration is for one of the animatable properties. + pub fn is_animatable(&self) -> bool { + self.id().is_animatable() + } + + /// Returns true if this property is a custom property, false + /// otherwise. + pub fn is_custom(&self) -> bool { + matches!(*self, PropertyDeclaration::Custom(..)) + } + + /// The `context` parameter controls this: + /// + /// <https://drafts.csswg.org/css-animations/#keyframes> + /// > The <declaration-list> inside of <keyframe-block> accepts any CSS property + /// > except those defined in this specification, + /// > but does accept the `animation-play-state` property and interprets it specially. + /// + /// This will not actually parse Importance values, and will always set things + /// to Importance::Normal. Parsing Importance values is the job of PropertyDeclarationParser, + /// we only set them here so that we don't have to reallocate + pub fn parse_into<'i, 't>( + declarations: &mut SourcePropertyDeclaration, + id: PropertyId, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + assert!(declarations.is_empty()); + debug_assert!(id.allowed_in(context), "{:?}", id); + input.skip_whitespace(); + + let start = input.state(); + let non_custom_id = match id { + PropertyId::Custom(property_name) => { + let value = match input.try_parse(CSSWideKeyword::parse) { + Ok(keyword) => CustomDeclarationValue::CSSWideKeyword(keyword), + Err(()) => CustomDeclarationValue::Value(Arc::new( + custom_properties::VariableValue::parse(input, &context.url_data)?, + )), + }; + declarations.push(PropertyDeclaration::Custom(CustomDeclaration { + name: property_name, + value, + })); + return Ok(()); + }, + PropertyId::NonCustom(id) => id, + }; + match non_custom_id.longhand_or_shorthand() { + Ok(longhand_id) => { + let declaration = input + .try_parse(CSSWideKeyword::parse) + .map(|keyword| PropertyDeclaration::css_wide_keyword(longhand_id, keyword)) + .or_else(|()| { + input.look_for_var_or_env_functions(); + input.parse_entirely(|input| longhand_id.parse_value(context, input)) + }) + .or_else(|err| { + while let Ok(_) = input.next() {} // Look for var() after the error. + if !input.seen_var_or_env_functions() { + return Err(err); + } + input.reset(&start); + let variable_value = + custom_properties::VariableValue::parse(input, &context.url_data)?; + Ok(PropertyDeclaration::WithVariables(VariableDeclaration { + id: longhand_id, + value: Arc::new(UnparsedValue { + variable_value, + from_shorthand: None, + }), + })) + })?; + declarations.push(declaration) + }, + Err(shorthand_id) => { + if let Ok(keyword) = input.try_parse(CSSWideKeyword::parse) { + if shorthand_id == ShorthandId::All { + declarations.all_shorthand = AllShorthand::CSSWideKeyword(keyword) + } else { + for longhand in shorthand_id.longhands() { + declarations + .push(PropertyDeclaration::css_wide_keyword(longhand, keyword)); + } + } + } else { + input.look_for_var_or_env_functions(); + // Not using parse_entirely here: each + // ${shorthand.ident}::parse_into function needs to do so + // *before* pushing to `declarations`. + shorthand_id + .parse_into(declarations, context, input) + .or_else(|err| { + while let Ok(_) = input.next() {} // Look for var() after the error. + if !input.seen_var_or_env_functions() { + return Err(err); + } + + input.reset(&start); + let variable_value = + custom_properties::VariableValue::parse(input, &context.url_data)?; + let unparsed = Arc::new(UnparsedValue { + variable_value, + from_shorthand: Some(shorthand_id), + }); + if shorthand_id == ShorthandId::All { + declarations.all_shorthand = AllShorthand::WithVariables(unparsed) + } else { + for id in shorthand_id.longhands() { + declarations.push(PropertyDeclaration::WithVariables( + VariableDeclaration { + id, + value: unparsed.clone(), + }, + )) + } + } + Ok(()) + })?; + } + }, + } + if let Some(use_counters) = context.use_counters { + use_counters.non_custom_properties.record(non_custom_id); + } + Ok(()) + } +} + +/// A PropertyDeclarationId without references, for use as a hash map key. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum OwnedPropertyDeclarationId { + /// A longhand. + Longhand(LonghandId), + /// A custom property declaration. + Custom(custom_properties::Name), +} + +impl OwnedPropertyDeclarationId { + /// Return whether this property is logical. + #[inline] + pub fn is_logical(&self) -> bool { + self.as_borrowed().is_logical() + } + + /// Returns the corresponding PropertyDeclarationId. + #[inline] + pub fn as_borrowed(&self) -> PropertyDeclarationId { + match self { + Self::Longhand(id) => PropertyDeclarationId::Longhand(*id), + Self::Custom(name) => PropertyDeclarationId::Custom(name), + } + } + + /// Convert an `AnimatedPropertyID` into an `OwnedPropertyDeclarationId`. + #[cfg(feature = "gecko")] + #[inline] + pub fn from_gecko_animated_property_id(property: &AnimatedPropertyID) -> Option<Self> { + Some( + match PropertyId::from_gecko_animated_property_id(property)? { + PropertyId::Custom(name) => Self::Custom(name), + PropertyId::NonCustom(id) => Self::Longhand(id.as_longhand()?), + }, + ) + } +} + +/// An identifier for a given property declaration, which can be either a +/// longhand or a custom property. +#[derive(Clone, Copy, Debug, PartialEq, MallocSizeOf)] +pub enum PropertyDeclarationId<'a> { + /// A longhand. + Longhand(LonghandId), + /// A custom property declaration. + Custom(&'a custom_properties::Name), +} + +impl<'a> ToCss for PropertyDeclarationId<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + PropertyDeclarationId::Longhand(id) => dest.write_str(id.name()), + PropertyDeclarationId::Custom(name) => { + dest.write_str("--")?; + serialize_atom_name(name, dest) + }, + } + } +} + +impl<'a> PropertyDeclarationId<'a> { + /// Returns PropertyFlags for given property. + #[inline(always)] + pub fn flags(&self) -> PropertyFlags { + match self { + Self::Longhand(id) => id.flags(), + Self::Custom(_) => PropertyFlags::empty(), + } + } + + /// Convert to an OwnedPropertyDeclarationId. + pub fn to_owned(&self) -> OwnedPropertyDeclarationId { + match self { + PropertyDeclarationId::Longhand(id) => OwnedPropertyDeclarationId::Longhand(*id), + PropertyDeclarationId::Custom(name) => { + OwnedPropertyDeclarationId::Custom((*name).clone()) + }, + } + } + + /// Whether a given declaration id is either the same as `other`, or a + /// longhand of it. + pub fn is_or_is_longhand_of(&self, other: &PropertyId) -> bool { + match *self { + PropertyDeclarationId::Longhand(id) => match *other { + PropertyId::NonCustom(non_custom_id) => id.is_or_is_longhand_of(non_custom_id), + PropertyId::Custom(_) => false, + }, + PropertyDeclarationId::Custom(name) => { + matches!(*other, PropertyId::Custom(ref other_name) if name == other_name) + }, + } + } + + /// Whether a given declaration id is a longhand belonging to this + /// shorthand. + pub fn is_longhand_of(&self, shorthand: ShorthandId) -> bool { + match *self { + PropertyDeclarationId::Longhand(ref id) => id.is_longhand_of(shorthand), + _ => false, + } + } + + /// Returns the name of the property without CSS escaping. + pub fn name(&self) -> Cow<'static, str> { + match *self { + PropertyDeclarationId::Longhand(id) => id.name().into(), + PropertyDeclarationId::Custom(name) => { + let mut s = String::new(); + write!(&mut s, "--{}", name).unwrap(); + s.into() + }, + } + } + + /// Returns longhand id if it is, None otherwise. + #[inline] + pub fn as_longhand(&self) -> Option<LonghandId> { + match *self { + PropertyDeclarationId::Longhand(id) => Some(id), + _ => None, + } + } + + /// Return whether this property is logical. + #[inline] + pub fn is_logical(&self) -> bool { + match self { + PropertyDeclarationId::Longhand(id) => id.is_logical(), + PropertyDeclarationId::Custom(_) => false, + } + } + + /// If this is a logical property, return the corresponding physical one in + /// the given writing mode. + /// + /// Otherwise, return unchanged. + #[inline] + pub fn to_physical(&self, wm: WritingMode) -> Self { + match self { + Self::Longhand(id) => Self::Longhand(id.to_physical(wm)), + Self::Custom(_) => self.clone(), + } + } + + /// Returns whether this property is animatable. + #[inline] + pub fn is_animatable(&self) -> bool { + match self { + Self::Longhand(id) => id.is_animatable(), + Self::Custom(_) => true, + } + } + + /// Returns whether this property is animatable in a discrete way. + #[inline] + pub fn is_discrete_animatable(&self) -> bool { + match self { + Self::Longhand(longhand) => longhand.is_discrete_animatable(), + // TODO(bug 1846516): Refine this? + Self::Custom(_) => true, + } + } + + /// Converts from a to an adequate nsCSSPropertyID, returning + /// eCSSPropertyExtra_variable for custom properties. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_nscsspropertyid(self) -> nsCSSPropertyID { + match self { + PropertyDeclarationId::Longhand(id) => id.to_nscsspropertyid(), + PropertyDeclarationId::Custom(_) => nsCSSPropertyID::eCSSPropertyExtra_variable, + } + } + + /// Convert a `PropertyDeclarationId` into an `AnimatedPropertyID` + /// Note that the rust AnimatedPropertyID doesn't implement Drop, so owned controls whether the + /// custom name should be addrefed or not. + /// + /// FIXME(emilio, bug 1870107): This is a bit error-prone. We should consider using cbindgen to + /// generate the property id representation or so. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_gecko_animated_property_id(&self, owned: bool) -> AnimatedPropertyID { + match self { + Self::Longhand(id) => AnimatedPropertyID { + mID: id.to_nscsspropertyid(), + mCustomName: RefPtr::null(), + }, + Self::Custom(name) => { + let mut property_id = AnimatedPropertyID { + mID: nsCSSPropertyID::eCSSPropertyExtra_variable, + mCustomName: RefPtr::null(), + }; + property_id.mCustomName.mRawPtr = if owned { + (*name).clone().into_addrefed() + } else { + name.as_ptr() + }; + property_id + }, + } + } +} + +/// A set of all properties. +#[derive(Clone, PartialEq, Default)] +pub struct NonCustomPropertyIdSet { + storage: [u32; ((property_counts::NON_CUSTOM as usize) - 1 + 32) / 32], +} + +impl NonCustomPropertyIdSet { + /// Creates an empty `NonCustomPropertyIdSet`. + pub fn new() -> Self { + Self { + storage: Default::default(), + } + } + + /// Insert a non-custom-property in the set. + #[inline] + pub fn insert(&mut self, id: NonCustomPropertyId) { + let bit = id.0 as usize; + self.storage[bit / 32] |= 1 << (bit % 32); + } + + /// Return whether the given property is in the set + #[inline] + pub fn contains(&self, id: NonCustomPropertyId) -> bool { + let bit = id.0 as usize; + (self.storage[bit / 32] & (1 << (bit % 32))) != 0 + } +} + +/// A set of longhand properties +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq)] +pub struct LonghandIdSet { + storage: [u32; ((property_counts::LONGHANDS as usize) - 1 + 32) / 32], +} + +to_shmem::impl_trivial_to_shmem!(LonghandIdSet); + +impl LonghandIdSet { + /// Return an empty LonghandIdSet. + #[inline] + pub fn new() -> Self { + Self { + storage: Default::default(), + } + } + + /// Iterate over the current longhand id set. + pub fn iter(&self) -> LonghandIdSetIterator { + LonghandIdSetIterator { + longhands: self, + cur: 0, + } + } + + /// Returns whether this set contains at least every longhand that `other` + /// also contains. + pub fn contains_all(&self, other: &Self) -> bool { + for (self_cell, other_cell) in self.storage.iter().zip(other.storage.iter()) { + if (*self_cell & *other_cell) != *other_cell { + return false; + } + } + true + } + + /// Returns whether this set contains any longhand that `other` also contains. + pub fn contains_any(&self, other: &Self) -> bool { + for (self_cell, other_cell) in self.storage.iter().zip(other.storage.iter()) { + if (*self_cell & *other_cell) != 0 { + return true; + } + } + false + } + + /// Remove all the given properties from the set. + #[inline] + pub fn remove_all(&mut self, other: &Self) { + for (self_cell, other_cell) in self.storage.iter_mut().zip(other.storage.iter()) { + *self_cell &= !*other_cell; + } + } + + /// Return whether the given property is in the set + #[inline] + pub fn contains(&self, id: LonghandId) -> bool { + let bit = id as usize; + (self.storage[bit / 32] & (1 << (bit % 32))) != 0 + } + + /// Return whether this set contains any reset longhand. + #[inline] + pub fn contains_any_reset(&self) -> bool { + self.contains_any(Self::reset()) + } + + /// Add the given property to the set + #[inline] + pub fn insert(&mut self, id: LonghandId) { + let bit = id as usize; + self.storage[bit / 32] |= 1 << (bit % 32); + } + + /// Remove the given property from the set + #[inline] + pub fn remove(&mut self, id: LonghandId) { + let bit = id as usize; + self.storage[bit / 32] &= !(1 << (bit % 32)); + } + + /// Clear all bits + #[inline] + pub fn clear(&mut self) { + for cell in &mut self.storage { + *cell = 0 + } + } + + /// Returns whether the set is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.storage.iter().all(|c| *c == 0) + } +} + +/// An iterator over a set of longhand ids. +pub struct LonghandIdSetIterator<'a> { + longhands: &'a LonghandIdSet, + cur: usize, +} + +impl<'a> Iterator for LonghandIdSetIterator<'a> { + type Item = LonghandId; + + fn next(&mut self) -> Option<Self::Item> { + loop { + if self.cur >= property_counts::LONGHANDS { + return None; + } + + let id: LonghandId = unsafe { mem::transmute(self.cur as u16) }; + self.cur += 1; + + if self.longhands.contains(id) { + return Some(id); + } + } + } +} + +/// An ArrayVec of subproperties, contains space for the longest shorthand except all. +pub type SubpropertiesVec<T> = ArrayVec<T, { property_counts::MAX_SHORTHAND_EXPANDED }>; + +/// A stack-allocated vector of `PropertyDeclaration` +/// large enough to parse one CSS `key: value` declaration. +/// (Shorthands expand to multiple `PropertyDeclaration`s.) +#[derive(Default)] +pub struct SourcePropertyDeclaration { + /// The storage for the actual declarations (except for all). + pub declarations: SubpropertiesVec<PropertyDeclaration>, + /// Stored separately to keep SubpropertiesVec smaller. + pub all_shorthand: AllShorthand, +} + +// This is huge, but we allocate it on the stack and then never move it, +// we only pass `&mut SourcePropertyDeclaration` references around. +size_of_test!(SourcePropertyDeclaration, 632); + +impl SourcePropertyDeclaration { + /// Create one with a single PropertyDeclaration. + #[inline] + pub fn with_one(decl: PropertyDeclaration) -> Self { + let mut result = Self::default(); + result.declarations.push(decl); + result + } + + /// Similar to Vec::drain: leaves this empty when the return value is dropped. + pub fn drain(&mut self) -> SourcePropertyDeclarationDrain { + SourcePropertyDeclarationDrain { + declarations: self.declarations.drain(..), + all_shorthand: mem::replace(&mut self.all_shorthand, AllShorthand::NotSet), + } + } + + /// Reset to initial state + pub fn clear(&mut self) { + self.declarations.clear(); + self.all_shorthand = AllShorthand::NotSet; + } + + /// Whether we're empty. + pub fn is_empty(&self) -> bool { + self.declarations.is_empty() && matches!(self.all_shorthand, AllShorthand::NotSet) + } + + /// Push a single declaration. + pub fn push(&mut self, declaration: PropertyDeclaration) { + let _result = self.declarations.try_push(declaration); + debug_assert!(_result.is_ok()); + } +} + +/// Return type of SourcePropertyDeclaration::drain +pub struct SourcePropertyDeclarationDrain<'a> { + /// A drain over the non-all declarations. + pub declarations: + ArrayVecDrain<'a, PropertyDeclaration, { property_counts::MAX_SHORTHAND_EXPANDED }>, + /// The all shorthand that was set. + pub all_shorthand: AllShorthand, +} + +/// An unparsed property value that contains `var()` functions. +#[derive(Debug, Eq, PartialEq, ToShmem)] +pub struct UnparsedValue { + /// The variable value, references and so on. + pub(super) variable_value: custom_properties::VariableValue, + /// The shorthand this came from. + from_shorthand: Option<ShorthandId>, +} + +impl ToCss for UnparsedValue { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + // https://drafts.csswg.org/css-variables/#variables-in-shorthands + if self.from_shorthand.is_none() { + self.variable_value.to_css(dest)?; + } + Ok(()) + } +} + +/// A simple cache for properties that come from a shorthand and have variable +/// references. +/// +/// This cache works because of the fact that you can't have competing values +/// for a given longhand coming from the same shorthand (but note that this is +/// why the shorthand needs to be part of the cache key). +pub type ShorthandsWithPropertyReferencesCache = + FxHashMap<(ShorthandId, LonghandId), PropertyDeclaration>; + +impl UnparsedValue { + fn substitute_variables<'cache>( + &self, + longhand_id: LonghandId, + custom_properties: &ComputedCustomProperties, + stylist: &Stylist, + computed_context: &computed::Context, + shorthand_cache: &'cache mut ShorthandsWithPropertyReferencesCache, + ) -> Cow<'cache, PropertyDeclaration> { + let invalid_at_computed_value_time = || { + let keyword = if longhand_id.inherited() { + CSSWideKeyword::Inherit + } else { + CSSWideKeyword::Initial + }; + Cow::Owned(PropertyDeclaration::css_wide_keyword(longhand_id, keyword)) + }; + + if computed_context + .builder + .invalid_non_custom_properties + .contains(longhand_id) + { + return invalid_at_computed_value_time(); + } + + if let Some(shorthand_id) = self.from_shorthand { + let key = (shorthand_id, longhand_id); + if shorthand_cache.contains_key(&key) { + // FIXME: This double lookup should be avoidable, but rustc + // doesn't like that, see: + // + // https://github.com/rust-lang/rust/issues/82146 + return Cow::Borrowed(&shorthand_cache[&key]); + } + } + + let css = match custom_properties::substitute( + &self.variable_value, + custom_properties, + stylist, + computed_context, + ) { + Ok(css) => css, + Err(..) => return invalid_at_computed_value_time(), + }; + + // As of this writing, only the base URL is used for property + // values. + // + // NOTE(emilio): we intentionally pase `None` as the rule type here. + // If something starts depending on it, it's probably a bug, since + // it'd change how values are parsed depending on whether we're in a + // @keyframes rule or not, for example... So think twice about + // whether you want to do this! + // + // FIXME(emilio): ParsingMode is slightly fishy... + let context = ParserContext::new( + Origin::Author, + &self.variable_value.url_data, + None, + ParsingMode::DEFAULT, + computed_context.quirks_mode, + /* namespaces = */ Default::default(), + None, + None, + ); + + let mut input = ParserInput::new(&css); + let mut input = Parser::new(&mut input); + input.skip_whitespace(); + + if let Ok(keyword) = input.try_parse(CSSWideKeyword::parse) { + return Cow::Owned(PropertyDeclaration::css_wide_keyword(longhand_id, keyword)); + } + + let shorthand = match self.from_shorthand { + None => { + return match input.parse_entirely(|input| longhand_id.parse_value(&context, input)) + { + Ok(decl) => Cow::Owned(decl), + Err(..) => invalid_at_computed_value_time(), + } + }, + Some(shorthand) => shorthand, + }; + + let mut decls = SourcePropertyDeclaration::default(); + // parse_into takes care of doing `parse_entirely` for us. + if shorthand + .parse_into(&mut decls, &context, &mut input) + .is_err() + { + return invalid_at_computed_value_time(); + } + + for declaration in decls.declarations.drain(..) { + let longhand = declaration.id().as_longhand().unwrap(); + if longhand.is_logical() { + let writing_mode = computed_context.builder.writing_mode; + shorthand_cache.insert( + (shorthand, longhand.to_physical(writing_mode)), + declaration.clone(), + ); + } + shorthand_cache.insert((shorthand, longhand), declaration); + } + + let key = (shorthand, longhand_id); + match shorthand_cache.get(&key) { + Some(decl) => Cow::Borrowed(decl), + None => { + // FIXME: We should always have the key here but it seems + // sometimes we don't, see bug 1696409. + #[cfg(feature = "gecko")] + { + if crate::gecko_bindings::structs::GECKO_IS_NIGHTLY { + panic!("Expected {:?} to be in the cache but it was not!", key); + } + } + invalid_at_computed_value_time() + }, + } + } +} +/// A parsed all-shorthand value. +pub enum AllShorthand { + /// Not present. + NotSet, + /// A CSS-wide keyword. + CSSWideKeyword(CSSWideKeyword), + /// An all shorthand with var() references that we can't resolve right now. + WithVariables(Arc<UnparsedValue>), +} + +impl Default for AllShorthand { + fn default() -> Self { + Self::NotSet + } +} + +impl AllShorthand { + /// Iterates property declarations from the given all shorthand value. + #[inline] + pub fn declarations(&self) -> AllShorthandDeclarationIterator { + AllShorthandDeclarationIterator { + all_shorthand: self, + longhands: ShorthandId::All.longhands(), + } + } +} + +/// An iterator over the all shorthand's shorthand declarations. +pub struct AllShorthandDeclarationIterator<'a> { + all_shorthand: &'a AllShorthand, + longhands: NonCustomPropertyIterator<LonghandId>, +} + +impl<'a> Iterator for AllShorthandDeclarationIterator<'a> { + type Item = PropertyDeclaration; + + #[inline] + fn next(&mut self) -> Option<Self::Item> { + match *self.all_shorthand { + AllShorthand::NotSet => None, + AllShorthand::CSSWideKeyword(ref keyword) => Some( + PropertyDeclaration::css_wide_keyword(self.longhands.next()?, *keyword), + ), + AllShorthand::WithVariables(ref unparsed) => { + Some(PropertyDeclaration::WithVariables(VariableDeclaration { + id: self.longhands.next()?, + value: unparsed.clone(), + })) + }, + } + } +} + +/// An iterator over all the property ids that are enabled for a given +/// shorthand, if that shorthand is enabled for all content too. +pub struct NonCustomPropertyIterator<Item: 'static> { + filter: bool, + iter: std::slice::Iter<'static, Item>, +} + +impl<Item> Iterator for NonCustomPropertyIterator<Item> +where + Item: 'static + Copy + Into<NonCustomPropertyId>, +{ + type Item = Item; + + fn next(&mut self) -> Option<Self::Item> { + loop { + let id = *self.iter.next()?; + if !self.filter || id.into().enabled_for_all_content() { + return Some(id); + } + } + } +} diff --git a/servo/components/style/properties/properties.html.mako b/servo/components/style/properties/properties.html.mako new file mode 100644 index 0000000000..5c51593517 --- /dev/null +++ b/servo/components/style/properties/properties.html.mako @@ -0,0 +1,31 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>Supported CSS properties in Servo</title> + <link rel="stylesheet" type="text/css" href="../normalize.css"> + <link rel="stylesheet" type="text/css" href="../rustdoc.css"> + <link rel="stylesheet" type="text/css" href="../light.css"> +</head> +<body class="rustdoc"> + <section id='main' class="content mod"> + <h1 class='fqn'><span class='in-band'>CSS properties currently supported in Servo</span></h1> + % for kind, props in sorted(properties.items()): + <h2>${kind.capitalize()}</h2> + <table> + <tr> + <th>Name</th> + <th>Pref</th> + </tr> + % for name, data in sorted(props.items()): + <tr> + <td><code>${name}</code></td> + <td><code>${data['pref'] or ''}</code></td> + </tr> + % endfor + </table> + % endfor + </section> +</body> +</html> diff --git a/servo/components/style/properties/properties.mako.rs b/servo/components/style/properties/properties.mako.rs new file mode 100644 index 0000000000..b08314d7d5 --- /dev/null +++ b/servo/components/style/properties/properties.mako.rs @@ -0,0 +1,2958 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +// This file is a Mako template: http://www.makotemplates.org/ + +// Please note that valid Rust syntax may be mangled by the Mako parser. +// For example, Vec<&Foo> will be mangled as Vec&Foo>. To work around these issues, the code +// can be escaped. In the above example, Vec<<&Foo> or Vec< &Foo> achieves the desired result of Vec<&Foo>. + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +use app_units::Au; +use servo_arc::{Arc, UniqueArc}; +use std::{ops, ptr}; +use std::{fmt, mem}; + +#[cfg(feature = "servo")] use euclid::SideOffsets2D; +#[cfg(feature = "gecko")] use crate::gecko_bindings::structs::{self, nsCSSPropertyID}; +#[cfg(feature = "servo")] use crate::logical_geometry::LogicalMargin; +#[cfg(feature = "servo")] use crate::computed_values; +use crate::logical_geometry::WritingMode; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use crate::computed_value_flags::*; +use cssparser::Parser; +use crate::media_queries::Device; +use crate::parser::ParserContext; +use crate::selector_parser::PseudoElement; +use crate::stylist::Stylist; +#[cfg(feature = "servo")] use servo_config::prefs; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; +use crate::stylesheets::{CssRuleType, CssRuleTypes, Origin}; +use crate::logical_geometry::{LogicalAxis, LogicalCorner, LogicalSide}; +use crate::use_counters::UseCounters; +use crate::rule_tree::StrongRuleNode; +use crate::str::CssStringWriter; +use crate::values::{ + computed, + resolved, + specified::{font::SystemFont, length::LineHeightBase}, +}; +use std::cell::Cell; +use super::{ + PropertyDeclarationId, PropertyId, NonCustomPropertyId, + NonCustomPropertyIdSet, PropertyFlags, SourcePropertyDeclaration, + LonghandIdSet, VariableDeclaration, CustomDeclaration, + WideKeywordDeclaration, NonCustomPropertyIterator, +}; + +<%! + from collections import defaultdict + from data import Method, PropertyRestrictions, Keyword, to_rust_ident, \ + to_camel_case, RULE_VALUES, SYSTEM_FONT_LONGHANDS, PRIORITARY_PROPERTIES + import os.path +%> + +/// Conversion with fewer impls than From/Into +pub trait MaybeBoxed<Out> { + /// Convert + fn maybe_boxed(self) -> Out; +} + +impl<T> MaybeBoxed<T> for T { + #[inline] + fn maybe_boxed(self) -> T { self } +} + +impl<T> MaybeBoxed<Box<T>> for T { + #[inline] + fn maybe_boxed(self) -> Box<T> { Box::new(self) } +} + +macro_rules! expanded { + ( $( $name: ident: $value: expr ),+ ) => { + expanded!( $( $name: $value, )+ ) + }; + ( $( $name: ident: $value: expr, )+ ) => { + Longhands { + $( + $name: MaybeBoxed::maybe_boxed($value), + )+ + } + } +} + +/// A module with all the code for longhand properties. +#[allow(missing_docs)] +pub mod longhands { + % for style_struct in data.style_structs: + include!("${repr(os.path.join(OUT_DIR, 'longhands/{}.rs'.format(style_struct.name_lower)))[1:-1]}"); + % endfor +} + +macro_rules! unwrap_or_initial { + ($prop: ident) => (unwrap_or_initial!($prop, $prop)); + ($prop: ident, $expr: expr) => + ($expr.unwrap_or_else(|| $prop::get_initial_specified_value())); +} + +/// A module with code for all the shorthand css properties, and a few +/// serialization helpers. +#[allow(missing_docs)] +pub mod shorthands { + use cssparser::Parser; + use crate::parser::{Parse, ParserContext}; + use style_traits::{ParseError, StyleParseErrorKind}; + use crate::values::specified; + + % for style_struct in data.style_structs: + include!("${repr(os.path.join(OUT_DIR, 'shorthands/{}.rs'.format(style_struct.name_lower)))[1:-1]}"); + % endfor + + // We didn't define the 'all' shorthand using the regular helpers:shorthand + // mechanism, since it causes some very large types to be generated. + // + // Also, make sure logical properties appear before its physical + // counter-parts, in order to prevent bugs like: + // + // https://bugzilla.mozilla.org/show_bug.cgi?id=1410028 + // + // FIXME(emilio): Adopt the resolution from: + // + // https://github.com/w3c/csswg-drafts/issues/1898 + // + // when there is one, whatever that is. + <% + logical_longhands = [] + other_longhands = [] + + for p in data.longhands: + if p.name in ['direction', 'unicode-bidi']: + continue; + if not p.enabled_in_content() and not p.experimental(engine): + continue; + if "Style" not in p.rule_types_allowed_names(): + continue; + if p.logical: + logical_longhands.append(p.name) + else: + other_longhands.append(p.name) + + data.declare_shorthand( + "all", + logical_longhands + other_longhands, + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-cascade-3/#all-shorthand" + ) + ALL_SHORTHAND_LEN = len(logical_longhands) + len(other_longhands); + %> +} + +<% + from itertools import groupby + + # After this code, `data.longhands` is sorted in the following order: + # - first all keyword variants and all variants known to be Copy, + # - second all the other variants, such as all variants with the same field + # have consecutive discriminants. + # The variable `variants` contain the same entries as `data.longhands` in + # the same order, but must exist separately to the data source, because + # we then need to add three additional variants `WideKeywordDeclaration`, + # `VariableDeclaration` and `CustomDeclaration`. + + variants = [] + for property in data.longhands: + variants.append({ + "name": property.camel_case, + "type": property.specified_type(), + "doc": "`" + property.name + "`", + "copy": property.specified_is_copy(), + }) + + groups = {} + keyfunc = lambda x: x["type"] + sortkeys = {} + for ty, group in groupby(sorted(variants, key=keyfunc), keyfunc): + group = list(group) + groups[ty] = group + for v in group: + if len(group) == 1: + sortkeys[v["name"]] = (not v["copy"], 1, v["name"], "") + else: + sortkeys[v["name"]] = (not v["copy"], len(group), ty, v["name"]) + variants.sort(key=lambda x: sortkeys[x["name"]]) + + # It is extremely important to sort the `data.longhands` array here so + # that it is in the same order as `variants`, for `LonghandId` and + # `PropertyDeclarationId` to coincide. + data.longhands.sort(key=lambda x: sortkeys[x.camel_case]) +%> + +// WARNING: It is *really* important for the variants of `LonghandId` +// and `PropertyDeclaration` to be defined in the exact same order, +// with the exception of `CSSWideKeyword`, `WithVariables` and `Custom`, +// which don't exist in `LonghandId`. + +<% + extra_variants = [ + { + "name": "CSSWideKeyword", + "type": "WideKeywordDeclaration", + "doc": "A CSS-wide keyword.", + "copy": False, + }, + { + "name": "WithVariables", + "type": "VariableDeclaration", + "doc": "An unparsed declaration.", + "copy": False, + }, + { + "name": "Custom", + "type": "CustomDeclaration", + "doc": "A custom property declaration.", + "copy": False, + }, + ] + for v in extra_variants: + variants.append(v) + groups[v["type"]] = [v] +%> + +/// Servo's representation for a property declaration. +#[derive(ToShmem)] +#[repr(u16)] +pub enum PropertyDeclaration { + % for variant in variants: + /// ${variant["doc"]} + ${variant["name"]}(${variant["type"]}), + % endfor +} + +// There's one of these for each parsed declaration so it better be small. +size_of_test!(PropertyDeclaration, 32); + +#[repr(C)] +struct PropertyDeclarationVariantRepr<T> { + tag: u16, + value: T +} + +impl Clone for PropertyDeclaration { + #[inline] + fn clone(&self) -> Self { + use self::PropertyDeclaration::*; + + <% + [copy, others] = [list(g) for _, g in groupby(variants, key=lambda x: not x["copy"])] + %> + + let self_tag = unsafe { + (*(self as *const _ as *const PropertyDeclarationVariantRepr<()>)).tag + }; + if self_tag <= LonghandId::${copy[-1]["name"]} as u16 { + #[derive(Clone, Copy)] + #[repr(u16)] + enum CopyVariants { + % for v in copy: + _${v["name"]}(${v["type"]}), + % endfor + } + + unsafe { + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut CopyVariants, + *(self as *const _ as *const CopyVariants), + ); + return out.assume_init(); + } + } + + // This function ensures that all properties not handled above + // do not have a specified value implements Copy. If you hit + // compile error here, you may want to add the type name into + // Longhand.specified_is_copy in data.py. + fn _static_assert_others_are_not_copy() { + struct Helper<T>(T); + trait AssertCopy { fn assert() {} } + trait AssertNotCopy { fn assert() {} } + impl<T: Copy> AssertCopy for Helper<T> {} + % for ty in sorted(set(x["type"] for x in others)): + impl AssertNotCopy for Helper<${ty}> {} + Helper::<${ty}>::assert(); + % endfor + } + + match *self { + ${" |\n".join("{}(..)".format(v["name"]) for v in copy)} => { + unsafe { debug_unreachable!() } + } + % for ty, vs in groupby(others, key=lambda x: x["type"]): + <% + vs = list(vs) + %> + % if len(vs) == 1: + ${vs[0]["name"]}(ref value) => { + ${vs[0]["name"]}(value.clone()) + } + % else: + ${" |\n".join("{}(ref value)".format(v["name"]) for v in vs)} => { + unsafe { + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut PropertyDeclarationVariantRepr<${ty}>, + PropertyDeclarationVariantRepr { + tag: *(self as *const _ as *const u16), + value: value.clone(), + }, + ); + out.assume_init() + } + } + % endif + % endfor + } + } +} + +impl PartialEq for PropertyDeclaration { + #[inline] + fn eq(&self, other: &Self) -> bool { + use self::PropertyDeclaration::*; + + unsafe { + let this_repr = + &*(self as *const _ as *const PropertyDeclarationVariantRepr<()>); + let other_repr = + &*(other as *const _ as *const PropertyDeclarationVariantRepr<()>); + if this_repr.tag != other_repr.tag { + return false; + } + match *self { + % for ty, vs in groupby(variants, key=lambda x: x["type"]): + ${" |\n".join("{}(ref this)".format(v["name"]) for v in vs)} => { + let other_repr = + &*(other as *const _ as *const PropertyDeclarationVariantRepr<${ty}>); + *this == other_repr.value + } + % endfor + } + } + } +} + +impl MallocSizeOf for PropertyDeclaration { + #[inline] + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + use self::PropertyDeclaration::*; + + match *self { + % for ty, vs in groupby(variants, key=lambda x: x["type"]): + ${" | ".join("{}(ref value)".format(v["name"]) for v in vs)} => { + value.size_of(ops) + } + % endfor + } + } +} + + +impl PropertyDeclaration { + /// Returns the given value for this declaration as a particular type. + /// It's the caller's responsibility to guarantee that the longhand id has the right specified + /// value representation. + pub(crate) unsafe fn unchecked_value_as<T>(&self) -> &T { + &(*(self as *const _ as *const PropertyDeclarationVariantRepr<T>)).value + } + + /// Dumps the property declaration before crashing. + #[cold] + #[cfg(debug_assertions)] + pub(crate) fn debug_crash(&self, reason: &str) { + panic!("{}: {:?}", reason, self); + } + #[cfg(not(debug_assertions))] + #[inline(always)] + pub(crate) fn debug_crash(&self, _reason: &str) {} + + /// Returns whether this is a variant of the Longhand(Value) type, rather + /// than one of the special variants in extra_variants. + fn is_longhand_value(&self) -> bool { + match *self { + % for v in extra_variants: + PropertyDeclaration::${v["name"]}(..) => false, + % endfor + _ => true, + } + } + + /// Like the method on ToCss, but without the type parameter to avoid + /// accidentally monomorphizing this large function multiple times for + /// different writers. + pub fn to_css(&self, dest: &mut CssStringWriter) -> fmt::Result { + use self::PropertyDeclaration::*; + + let mut dest = CssWriter::new(dest); + match *self { + % for ty, vs in groupby(variants, key=lambda x: x["type"]): + ${" | ".join("{}(ref value)".format(v["name"]) for v in vs)} => { + value.to_css(&mut dest) + } + % endfor + } + } + + /// Returns the color value of a given property, for high-contrast-mode tweaks. + pub(super) fn color_value(&self) -> Option<<&crate::values::specified::Color> { + ${static_longhand_id_set("COLOR_PROPERTIES", lambda p: p.predefined_type == "Color")} + <% + # sanity check + assert data.longhands_by_name["background-color"].predefined_type == "Color" + + color_specified_type = data.longhands_by_name["background-color"].specified_type() + %> + let id = self.id().as_longhand()?; + if !COLOR_PROPERTIES.contains(id) || !self.is_longhand_value() { + return None; + } + let repr = self as *const _ as *const PropertyDeclarationVariantRepr<${color_specified_type}>; + Some(unsafe { &(*repr).value }) + } +} + +/// A module with all the code related to animated properties. +/// +/// This needs to be "included" by mako at least after all longhand modules, +/// given they populate the global data. +pub mod animated_properties { + <%include file="/helpers/animated_properties.mako.rs" /> +} + +/// A module to group various interesting property counts. +pub mod property_counts { + /// The number of (non-alias) longhand properties. + pub const LONGHANDS: usize = ${len(data.longhands)}; + /// The number of (non-alias) shorthand properties. + pub const SHORTHANDS: usize = ${len(data.shorthands)}; + /// The number of aliases. + pub const ALIASES: usize = ${len(data.all_aliases())}; + /// The number of counted unknown properties. + pub const COUNTED_UNKNOWN: usize = ${len(data.counted_unknown_properties)}; + /// The number of (non-alias) longhands and shorthands. + pub const LONGHANDS_AND_SHORTHANDS: usize = LONGHANDS + SHORTHANDS; + /// The number of non-custom properties. + pub const NON_CUSTOM: usize = LONGHANDS_AND_SHORTHANDS + ALIASES; + /// The number of prioritary properties that we have. + pub const PRIORITARY: usize = ${len(PRIORITARY_PROPERTIES)}; + /// The max number of longhands that a shorthand other than "all" expands to. + pub const MAX_SHORTHAND_EXPANDED: usize = + ${max(len(s.sub_properties) for s in data.shorthands_except_all())}; + /// The max amount of longhands that the `all` shorthand will ever contain. + pub const ALL_SHORTHAND_EXPANDED: usize = ${ALL_SHORTHAND_LEN}; + /// The number of animatable properties. + pub const ANIMATABLE: usize = ${sum(1 for prop in data.longhands if prop.animatable)}; +} + +% if engine == "gecko": +#[allow(dead_code)] +unsafe fn static_assert_nscsspropertyid() { + % for i, property in enumerate(data.longhands + data.shorthands + data.all_aliases()): + std::mem::transmute::<[u8; ${i}], [u8; ${property.nscsspropertyid()} as usize]>([0; ${i}]); // ${property.name} + % endfor +} +% endif + +impl NonCustomPropertyId { + /// Get the property name. + #[inline] + pub fn name(self) -> &'static str { + static MAP: [&'static str; property_counts::NON_CUSTOM] = [ + % for property in data.longhands + data.shorthands + data.all_aliases(): + "${property.name}", + % endfor + ]; + MAP[self.0 as usize] + } + + /// Returns whether this property is animatable. + #[inline] + pub fn is_animatable(self) -> bool { + ${static_non_custom_property_id_set("ANIMATABLE", lambda p: p.animatable)} + ANIMATABLE.contains(self) + } + + /// Whether this property is enabled for all content right now. + #[inline] + pub(super) fn enabled_for_all_content(self) -> bool { + ${static_non_custom_property_id_set( + "EXPERIMENTAL", + lambda p: p.experimental(engine) + )} + + ${static_non_custom_property_id_set( + "ALWAYS_ENABLED", + lambda p: (not p.experimental(engine)) and p.enabled_in_content() + )} + + let passes_pref_check = || { + % if engine == "gecko": + unsafe { structs::nsCSSProps_gPropertyEnabled[self.0 as usize] } + % else: + static PREF_NAME: [Option< &str>; ${ + len(data.longhands) + len(data.shorthands) + len(data.all_aliases()) + }] = [ + % for property in data.longhands + data.shorthands + data.all_aliases(): + <% + attrs = {"servo-2013": "servo_2013_pref", "servo-2020": "servo_2020_pref"} + pref = getattr(property, attrs[engine]) + %> + % if pref: + Some("${pref}"), + % else: + None, + % endif + % endfor + ]; + let pref = match PREF_NAME[self.0 as usize] { + None => return true, + Some(pref) => pref, + }; + + prefs::pref_map().get(pref).as_bool().unwrap_or(false) + % endif + }; + + if ALWAYS_ENABLED.contains(self) { + return true + } + + if EXPERIMENTAL.contains(self) && passes_pref_check() { + return true + } + + false + } + + /// Returns whether a given rule allows a given property. + #[inline] + pub fn allowed_in_rule(self, rule_types: CssRuleTypes) -> bool { + debug_assert!( + rule_types.contains(CssRuleType::Keyframe) || + rule_types.contains(CssRuleType::Page) || + rule_types.contains(CssRuleType::Style), + "Declarations are only expected inside a keyframe, page, or style rule." + ); + + static MAP: [u32; property_counts::NON_CUSTOM] = [ + % for property in data.longhands + data.shorthands + data.all_aliases(): + % for name in RULE_VALUES: + % if property.rule_types_allowed & RULE_VALUES[name] != 0: + CssRuleType::${name}.bit() | + % endif + % endfor + 0, + % endfor + ]; + MAP[self.0 as usize] & rule_types.bits() != 0 + } + + pub(super) fn allowed_in(self, context: &ParserContext) -> bool { + if !self.allowed_in_rule(context.rule_types()) { + return false; + } + + self.allowed_in_ignoring_rule_type(context) + } + + + pub(super) fn allowed_in_ignoring_rule_type(self, context: &ParserContext) -> bool { + // The semantics of these are kinda hard to reason about, what follows + // is a description of the different combinations that can happen with + // these three sets. + // + // Experimental properties are generally controlled by prefs, but an + // experimental property explicitly enabled in certain context (UA or + // chrome sheets) is always usable in the context regardless of the + // pref value. + // + // Non-experimental properties are either normal properties which are + // usable everywhere, or internal-only properties which are only usable + // in certain context they are explicitly enabled in. + if self.enabled_for_all_content() { + return true; + } + + ${static_non_custom_property_id_set( + "ENABLED_IN_UA_SHEETS", + lambda p: p.explicitly_enabled_in_ua_sheets() + )} + ${static_non_custom_property_id_set( + "ENABLED_IN_CHROME", + lambda p: p.explicitly_enabled_in_chrome() + )} + + if context.stylesheet_origin == Origin::UserAgent && + ENABLED_IN_UA_SHEETS.contains(self) + { + return true + } + + if context.chrome_rules_enabled() && ENABLED_IN_CHROME.contains(self) { + return true + } + + false + } + + /// The supported types of this property. The return value should be + /// style_traits::CssType when it can become a bitflags type. + pub(super) fn supported_types(&self) -> u8 { + const SUPPORTED_TYPES: [u8; ${len(data.longhands) + len(data.shorthands)}] = [ + % for prop in data.longhands: + <${prop.specified_type()} as SpecifiedValueInfo>::SUPPORTED_TYPES, + % endfor + % for prop in data.shorthands: + % if prop.name == "all": + 0, // 'all' accepts no value other than CSS-wide keywords + % else: + <shorthands::${prop.ident}::Longhands as SpecifiedValueInfo>::SUPPORTED_TYPES, + % endif + % endfor + ]; + SUPPORTED_TYPES[self.0 as usize] + } + + /// See PropertyId::collect_property_completion_keywords. + pub(super) fn collect_property_completion_keywords(&self, f: KeywordsCollectFn) { + fn do_nothing(_: KeywordsCollectFn) {} + const COLLECT_FUNCTIONS: [fn(KeywordsCollectFn); + ${len(data.longhands) + len(data.shorthands)}] = [ + % for prop in data.longhands: + <${prop.specified_type()} as SpecifiedValueInfo>::collect_completion_keywords, + % endfor + % for prop in data.shorthands: + % if prop.name == "all": + do_nothing, // 'all' accepts no value other than CSS-wide keywords + % else: + <shorthands::${prop.ident}::Longhands as SpecifiedValueInfo>:: + collect_completion_keywords, + % endif + % endfor + ]; + COLLECT_FUNCTIONS[self.0 as usize](f); + } +} + +<%def name="static_non_custom_property_id_set(name, is_member)"> +static ${name}: NonCustomPropertyIdSet = NonCustomPropertyIdSet { + <% + storage = [0] * int((len(data.longhands) + len(data.shorthands) + len(data.all_aliases()) - 1 + 32) / 32) + for i, property in enumerate(data.longhands + data.shorthands + data.all_aliases()): + if is_member(property): + storage[int(i / 32)] |= 1 << (i % 32) + %> + storage: [${", ".join("0x%x" % word for word in storage)}] +}; +</%def> + +<%def name="static_longhand_id_set(name, is_member)"> +static ${name}: LonghandIdSet = LonghandIdSet { + <% + storage = [0] * int((len(data.longhands) - 1 + 32) / 32) + for i, property in enumerate(data.longhands): + if is_member(property): + storage[int(i / 32)] |= 1 << (i % 32) + %> + storage: [${", ".join("0x%x" % word for word in storage)}] +}; +</%def> + +<% + logical_groups = defaultdict(list) + for prop in data.longhands: + if prop.logical_group: + logical_groups[prop.logical_group].append(prop) + + for group, props in logical_groups.items(): + logical_count = sum(1 for p in props if p.logical) + if logical_count * 2 != len(props): + raise RuntimeError("Logical group {} has ".format(group) + + "unbalanced logical / physical properties") + + FIRST_LINE_RESTRICTIONS = PropertyRestrictions.first_line(data) + FIRST_LETTER_RESTRICTIONS = PropertyRestrictions.first_letter(data) + MARKER_RESTRICTIONS = PropertyRestrictions.marker(data) + PLACEHOLDER_RESTRICTIONS = PropertyRestrictions.placeholder(data) + CUE_RESTRICTIONS = PropertyRestrictions.cue(data) + + def restriction_flags(property): + name = property.name + flags = [] + if name in FIRST_LINE_RESTRICTIONS: + flags.append("APPLIES_TO_FIRST_LINE") + if name in FIRST_LETTER_RESTRICTIONS: + flags.append("APPLIES_TO_FIRST_LETTER") + if name in PLACEHOLDER_RESTRICTIONS: + flags.append("APPLIES_TO_PLACEHOLDER") + if name in MARKER_RESTRICTIONS: + flags.append("APPLIES_TO_MARKER") + if name in CUE_RESTRICTIONS: + flags.append("APPLIES_TO_CUE") + return flags + +%> + +/// A group for properties which may override each other via logical resolution. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[repr(u8)] +pub enum LogicalGroupId { + % for i, group in enumerate(logical_groups.keys()): + /// ${group} + ${to_camel_case(group)} = ${i}, + % endfor +} + +impl LogicalGroupId { + /// Return the list of physical mapped properties for a given logical group. + fn physical_properties(self) -> &'static [LonghandId] { + static PROPS: [[LonghandId; 4]; ${len(logical_groups)}] = [ + % for group, props in logical_groups.items(): + [ + <% physical_props = [p for p in props if p.logical][0].all_physical_mapped_properties(data) %> + % for phys in physical_props: + LonghandId::${phys.camel_case}, + % endfor + % for i in range(len(physical_props), 4): + LonghandId::${physical_props[0].camel_case}, + % endfor + ], + % endfor + ]; + &PROPS[self as usize] + } +} + +/// A set of logical groups. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq)] +pub struct LogicalGroupSet { + storage: [u32; (${len(logical_groups)} - 1 + 32) / 32] +} + +impl LogicalGroupSet { + /// Creates an empty `NonCustomPropertyIdSet`. + pub fn new() -> Self { + Self { + storage: Default::default(), + } + } + + /// Return whether the given group is in the set + #[inline] + pub fn contains(&self, g: LogicalGroupId) -> bool { + let bit = g as usize; + (self.storage[bit / 32] & (1 << (bit % 32))) != 0 + } + + /// Insert a group the set. + #[inline] + pub fn insert(&mut self, g: LogicalGroupId) { + let bit = g as usize; + self.storage[bit / 32] |= 1 << (bit % 32); + } +} + + +#[repr(u8)] +#[derive(Copy, Clone, Debug)] +pub(crate) enum PrioritaryPropertyId { + % for p in data.longhands: + % if p.is_prioritary(): + ${p.camel_case}, + % endif + % endfor +} + +impl PrioritaryPropertyId { + #[inline] + pub fn to_longhand(self) -> LonghandId { + static PRIORITARY_TO_LONGHAND: [LonghandId; property_counts::PRIORITARY] = [ + % for p in data.longhands: + % if p.is_prioritary(): + LonghandId::${p.camel_case}, + % endif + % endfor + ]; + PRIORITARY_TO_LONGHAND[self as usize] + } + #[inline] + pub fn from_longhand(l: LonghandId) -> Option<Self> { + static LONGHAND_TO_PRIORITARY: [Option<PrioritaryPropertyId>; ${len(data.longhands)}] = [ + % for p in data.longhands: + % if p.is_prioritary(): + Some(PrioritaryPropertyId::${p.camel_case}), + % else: + None, + % endif + % endfor + ]; + LONGHAND_TO_PRIORITARY[l as usize] + } +} + +impl LonghandIdSet { + /// The set of non-inherited longhands. + #[inline] + pub(super) fn reset() -> &'static Self { + ${static_longhand_id_set("RESET", lambda p: not p.style_struct.inherited)} + &RESET + } + + #[inline] + pub(super) fn discrete_animatable() -> &'static Self { + ${static_longhand_id_set("DISCRETE_ANIMATABLE", lambda p: p.animation_value_type == "discrete")} + &DISCRETE_ANIMATABLE + } + + #[inline] + pub(super) fn logical() -> &'static Self { + ${static_longhand_id_set("LOGICAL", lambda p: p.logical)} + &LOGICAL + } + + /// Returns the set of longhands that are ignored when document colors are + /// disabled. + #[inline] + pub(super) fn ignored_when_colors_disabled() -> &'static Self { + ${static_longhand_id_set( + "IGNORED_WHEN_COLORS_DISABLED", + lambda p: p.ignored_when_colors_disabled + )} + &IGNORED_WHEN_COLORS_DISABLED + } + + /// Only a few properties are allowed to depend on the visited state of + /// links. When cascading visited styles, we can save time by only + /// processing these properties. + pub(super) fn visited_dependent() -> &'static Self { + ${static_longhand_id_set("VISITED_DEPENDENT", lambda p: p.is_visited_dependent())} + debug_assert!(Self::late_group().contains_all(&VISITED_DEPENDENT)); + &VISITED_DEPENDENT + } + + #[inline] + pub(super) fn prioritary_properties() -> &'static Self { + ${static_longhand_id_set("PRIORITARY_PROPERTIES", lambda p: p.is_prioritary())} + &PRIORITARY_PROPERTIES + } + + #[inline] + pub(super) fn late_group_only_inherited() -> &'static Self { + ${static_longhand_id_set("LATE_GROUP_ONLY_INHERITED", lambda p: p.style_struct.inherited and not p.is_prioritary())} + &LATE_GROUP_ONLY_INHERITED + } + + #[inline] + pub(super) fn late_group() -> &'static Self { + ${static_longhand_id_set("LATE_GROUP", lambda p: not p.is_prioritary())} + &LATE_GROUP + } + + /// Returns the set of properties that are declared as having no effect on + /// Gecko <scrollbar> elements or their descendant scrollbar parts. + #[cfg(debug_assertions)] + #[cfg(feature = "gecko")] + #[inline] + pub fn has_no_effect_on_gecko_scrollbars() -> &'static Self { + // data.py asserts that has_no_effect_on_gecko_scrollbars is True or + // False for properties that are inherited and Gecko pref controlled, + // and is None for all other properties. + ${static_longhand_id_set( + "HAS_NO_EFFECT_ON_SCROLLBARS", + lambda p: p.has_effect_on_gecko_scrollbars is False + )} + &HAS_NO_EFFECT_ON_SCROLLBARS + } + + /// Returns the set of border properties for the purpose of disabling native + /// appearance. + #[inline] + pub fn border_background_properties() -> &'static Self { + ${static_longhand_id_set( + "BORDER_BACKGROUND_PROPERTIES", + lambda p: (p.logical_group and p.logical_group.startswith("border")) or \ + p.name in ["background-color", "background-image"] + )} + &BORDER_BACKGROUND_PROPERTIES + } +} + +/// An identifier for a given longhand property. +#[derive(Clone, Copy, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] +#[repr(u16)] +pub enum LonghandId { + % for i, property in enumerate(data.longhands): + /// ${property.name} + ${property.camel_case} = ${i}, + % endfor +} + +enum LogicalMappingKind { + Side(LogicalSide), + Corner(LogicalCorner), + Axis(LogicalAxis), +} + +struct LogicalMappingData { + group: LogicalGroupId, + kind: LogicalMappingKind, +} + +impl LogicalMappingData { + fn to_physical(&self, wm: WritingMode) -> LonghandId { + let index = match self.kind { + LogicalMappingKind::Side(s) => s.to_physical(wm) as usize, + LogicalMappingKind::Corner(c) => c.to_physical(wm) as usize, + LogicalMappingKind::Axis(a) => a.to_physical(wm) as usize, + }; + self.group.physical_properties()[index] + } +} + +impl LonghandId { + /// Returns an iterator over all the shorthands that include this longhand. + pub fn shorthands(self) -> NonCustomPropertyIterator<ShorthandId> { + // first generate longhand to shorthands lookup map + // + // NOTE(emilio): This currently doesn't exclude the "all" shorthand. It + // could potentially do so, which would speed up serialization + // algorithms and what not, I guess. + <% + from functools import cmp_to_key + longhand_to_shorthand_map = {} + num_sub_properties = {} + for shorthand in data.shorthands: + num_sub_properties[shorthand.camel_case] = len(shorthand.sub_properties) + for sub_property in shorthand.sub_properties: + if sub_property.ident not in longhand_to_shorthand_map: + longhand_to_shorthand_map[sub_property.ident] = [] + + longhand_to_shorthand_map[sub_property.ident].append(shorthand.camel_case) + + def cmp(a, b): + return (a > b) - (a < b) + + def preferred_order(x, y): + # Since we want properties in order from most subproperties to least, + # reverse the arguments to cmp from the expected order. + result = cmp(num_sub_properties.get(y, 0), num_sub_properties.get(x, 0)) + if result: + return result + # Fall back to lexicographic comparison. + return cmp(x, y) + + # Sort the lists of shorthand properties according to preferred order: + # https://drafts.csswg.org/cssom/#concept-shorthands-preferred-order + for shorthand_list in longhand_to_shorthand_map.values(): + shorthand_list.sort(key=cmp_to_key(preferred_order)) + %> + + // based on lookup results for each longhand, create result arrays + static MAP: [&'static [ShorthandId]; property_counts::LONGHANDS] = [ + % for property in data.longhands: + &[ + % for shorthand in longhand_to_shorthand_map.get(property.ident, []): + ShorthandId::${shorthand}, + % endfor + ], + % endfor + ]; + + NonCustomPropertyIterator { + filter: NonCustomPropertyId::from(self).enabled_for_all_content(), + iter: MAP[self as usize].iter(), + } + } + + pub(super) fn parse_value<'i, 't>( + self, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<PropertyDeclaration, ParseError<'i>> { + type ParsePropertyFn = for<'i, 't> fn( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<PropertyDeclaration, ParseError<'i>>; + static PARSE_PROPERTY: [ParsePropertyFn; ${len(data.longhands)}] = [ + % for property in data.longhands: + longhands::${property.ident}::parse_declared, + % endfor + ]; + (PARSE_PROPERTY[self as usize])(context, input) + } + + /// Return the relevant data to map a particular logical property into physical. + fn logical_mapping_data(self) -> Option<<&'static LogicalMappingData> { + const LOGICAL_MAPPING_DATA: [Option<LogicalMappingData>; ${len(data.longhands)}] = [ + % for prop in data.longhands: + % if prop.logical: + Some(LogicalMappingData { + group: LogicalGroupId::${to_camel_case(prop.logical_group)}, + kind: ${prop.logical_mapping_kind(data)} + }), + % else: + None, + % endif + % endfor + ]; + LOGICAL_MAPPING_DATA[self as usize].as_ref() + } + + /// If this is a logical property, return the corresponding physical one in the given + /// writing mode. Otherwise, return unchanged. + #[inline] + pub fn to_physical(self, wm: WritingMode) -> Self { + let Some(data) = self.logical_mapping_data() else { return self }; + data.to_physical(wm) + } + + /// Return the logical group of this longhand property. + pub fn logical_group(self) -> Option<LogicalGroupId> { + const LOGICAL_GROUP_IDS: [Option<LogicalGroupId>; ${len(data.longhands)}] = [ + % for prop in data.longhands: + % if prop.logical_group: + Some(LogicalGroupId::${to_camel_case(prop.logical_group)}), + % else: + None, + % endif + % endfor + ]; + LOGICAL_GROUP_IDS[self as usize] + } + + /// Returns PropertyFlags for given longhand property. + #[inline(always)] + pub fn flags(self) -> PropertyFlags { + // TODO(emilio): This can be simplified further as Rust gains more + // constant expression support. + const FLAGS: [u16; ${len(data.longhands)}] = [ + % for property in data.longhands: + % for flag in property.flags + restriction_flags(property): + PropertyFlags::${flag}.bits() | + % endfor + 0, + % endfor + ]; + PropertyFlags::from_bits_retain(FLAGS[self as usize]) + } +} + +/// An identifier for a given shorthand property. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] +#[repr(u16)] +pub enum ShorthandId { + % for i, property in enumerate(data.shorthands): + /// ${property.name} + ${property.camel_case} = ${i}, + % endfor +} + +impl ShorthandId { + /// Get the longhand ids that form this shorthand. + pub fn longhands(self) -> NonCustomPropertyIterator<LonghandId> { + static MAP: [&'static [LonghandId]; property_counts::SHORTHANDS] = [ + % for property in data.shorthands: + &[ + % for sub in property.sub_properties: + LonghandId::${sub.camel_case}, + % endfor + ], + % endfor + ]; + NonCustomPropertyIterator { + filter: NonCustomPropertyId::from(self).enabled_for_all_content(), + iter: MAP[self as usize].iter(), + } + } + + /// Try to serialize the given declarations as this shorthand. + /// + /// Returns an error if writing to the stream fails, or if the declarations + /// do not map to a shorthand. + pub fn longhands_to_css( + self, + declarations: &[&PropertyDeclaration], + dest: &mut CssStringWriter, + ) -> fmt::Result { + type LonghandsToCssFn = for<'a, 'b> fn(&'a [&'b PropertyDeclaration], &mut CssStringWriter) -> fmt::Result; + fn all_to_css(_: &[&PropertyDeclaration], _: &mut CssStringWriter) -> fmt::Result { + // No need to try to serialize the declarations as the 'all' + // shorthand, since it only accepts CSS-wide keywords (and variable + // references), which will be handled in + // get_shorthand_appendable_value. + Ok(()) + } + + static LONGHANDS_TO_CSS: [LonghandsToCssFn; ${len(data.shorthands)}] = [ + % for shorthand in data.shorthands: + % if shorthand.ident == "all": + all_to_css, + % else: + shorthands::${shorthand.ident}::to_css, + % endif + % endfor + ]; + + LONGHANDS_TO_CSS[self as usize](declarations, dest) + } + + /// Returns PropertyFlags for the given shorthand property. + #[inline] + pub fn flags(self) -> PropertyFlags { + const FLAGS: [u16; ${len(data.shorthands)}] = [ + % for property in data.shorthands: + % for flag in property.flags: + PropertyFlags::${flag}.bits() | + % endfor + 0, + % endfor + ]; + PropertyFlags::from_bits_retain(FLAGS[self as usize]) + } + + /// Returns the order in which this property appears relative to other + /// shorthands in idl-name-sorting order. + #[inline] + pub fn idl_name_sort_order(self) -> u32 { + <% + from data import to_idl_name + ordered = {} + sorted_shorthands = sorted(data.shorthands, key=lambda p: to_idl_name(p.ident)) + for order, shorthand in enumerate(sorted_shorthands): + ordered[shorthand.ident] = order + %> + static IDL_NAME_SORT_ORDER: [u32; ${len(data.shorthands)}] = [ + % for property in data.shorthands: + ${ordered[property.ident]}, + % endfor + ]; + IDL_NAME_SORT_ORDER[self as usize] + } + + pub(super) fn parse_into<'i, 't>( + self, + declarations: &mut SourcePropertyDeclaration, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + type ParseIntoFn = for<'i, 't> fn( + declarations: &mut SourcePropertyDeclaration, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>>; + + fn parse_all<'i, 't>( + _: &mut SourcePropertyDeclaration, + _: &ParserContext, + input: &mut Parser<'i, 't> + ) -> Result<(), ParseError<'i>> { + // 'all' accepts no value other than CSS-wide keywords + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + static PARSE_INTO: [ParseIntoFn; ${len(data.shorthands)}] = [ + % for shorthand in data.shorthands: + % if shorthand.ident == "all": + parse_all, + % else: + shorthands::${shorthand.ident}::parse_into, + % endif + % endfor + ]; + + (PARSE_INTO[self as usize])(declarations, context, input) + } +} + +/// The counted unknown property list which is used for css use counters. +/// +/// FIXME: This should be just #[repr(u8)], but can't be because of ABI issues, +/// see https://bugs.llvm.org/show_bug.cgi?id=44228. +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, PartialEq)] +#[repr(u32)] +pub enum CountedUnknownProperty { + % for prop in data.counted_unknown_properties: + /// ${prop.name} + ${prop.camel_case}, + % endfor +} + +impl CountedUnknownProperty { + /// Parse the counted unknown property, for testing purposes only. + pub fn parse_for_testing(property_name: &str) -> Option<Self> { + ascii_case_insensitive_phf_map! { + unknown_ids -> CountedUnknownProperty = { + % for property in data.counted_unknown_properties: + "${property.name}" => CountedUnknownProperty::${property.camel_case}, + % endfor + } + } + unknown_ids::get(property_name).cloned() + } + + /// Returns the underlying index, used for use counter. + #[inline] + pub fn bit(self) -> usize { + self as usize + } +} + +impl PropertyId { + /// Returns a given property from the given name, _regardless of whether it + /// is enabled or not_, or Err(()) for unknown properties. + pub(super) fn parse_unchecked( + property_name: &str, + use_counters: Option< &UseCounters>, + ) -> Result<Self, ()> { + // A special id for css use counters. ShorthandAlias is not used in the Servo build. + // That's why we need to allow dead_code. + pub enum StaticId { + NonCustom(NonCustomPropertyId), + CountedUnknown(CountedUnknownProperty), + } + ascii_case_insensitive_phf_map! { + static_ids -> StaticId = { + % for i, property in enumerate(data.longhands + data.shorthands + data.all_aliases()): + "${property.name}" => StaticId::NonCustom(NonCustomPropertyId(${i})), + % endfor + % for property in data.counted_unknown_properties: + "${property.name}" => { + StaticId::CountedUnknown(CountedUnknownProperty::${property.camel_case}) + }, + % endfor + } + } + + if let Some(id) = static_ids::get(property_name) { + return Ok(match *id { + StaticId::NonCustom(id) => PropertyId::NonCustom(id), + StaticId::CountedUnknown(unknown_prop) => { + if let Some(counters) = use_counters { + counters.counted_unknown_properties.record(unknown_prop); + } + // Always return Err(()) because these aren't valid custom property names. + return Err(()); + } + }); + } + + let name = crate::custom_properties::parse_name(property_name)?; + Ok(PropertyId::Custom(crate::custom_properties::Name::from(name))) + } +} + +impl PropertyDeclaration { + /// Given a property declaration, return the property declaration id. + #[inline] + pub fn id(&self) -> PropertyDeclarationId { + match *self { + PropertyDeclaration::Custom(ref declaration) => { + return PropertyDeclarationId::Custom(&declaration.name) + } + PropertyDeclaration::CSSWideKeyword(ref declaration) => { + return PropertyDeclarationId::Longhand(declaration.id); + } + PropertyDeclaration::WithVariables(ref declaration) => { + return PropertyDeclarationId::Longhand(declaration.id); + } + _ => {} + } + // This is just fine because PropertyDeclaration and LonghandId + // have corresponding discriminants. + let id = unsafe { *(self as *const _ as *const LonghandId) }; + debug_assert_eq!(id, match *self { + % for property in data.longhands: + PropertyDeclaration::${property.camel_case}(..) => LonghandId::${property.camel_case}, + % endfor + _ => id, + }); + PropertyDeclarationId::Longhand(id) + } + + /// Given a declaration, convert it into a declaration for a corresponding + /// physical property. + #[inline] + pub fn to_physical(&self, wm: WritingMode) -> Self { + match *self { + PropertyDeclaration::WithVariables(VariableDeclaration { + id, + ref value, + }) => { + return PropertyDeclaration::WithVariables(VariableDeclaration { + id: id.to_physical(wm), + value: value.clone(), + }) + } + PropertyDeclaration::CSSWideKeyword(WideKeywordDeclaration { + id, + keyword, + }) => { + return PropertyDeclaration::CSSWideKeyword(WideKeywordDeclaration { + id: id.to_physical(wm), + keyword, + }) + } + PropertyDeclaration::Custom(..) => return self.clone(), + % for prop in data.longhands: + PropertyDeclaration::${prop.camel_case}(..) => {}, + % endfor + } + + let mut ret = self.clone(); + + % for prop in data.longhands: + % for physical_property in prop.all_physical_mapped_properties(data): + % if physical_property.specified_type() != prop.specified_type(): + <% raise "Logical property %s should share specified value with physical property %s" % \ + (prop.name, physical_property.name) %> + % endif + % endfor + % endfor + + unsafe { + let longhand_id = *(&mut ret as *mut _ as *mut LonghandId); + + debug_assert_eq!( + PropertyDeclarationId::Longhand(longhand_id), + ret.id() + ); + + // This is just fine because PropertyDeclaration and LonghandId + // have corresponding discriminants. + *(&mut ret as *mut _ as *mut LonghandId) = longhand_id.to_physical(wm); + + debug_assert_eq!( + PropertyDeclarationId::Longhand(longhand_id.to_physical(wm)), + ret.id() + ); + } + + ret + } + + /// Returns whether or not the property is set by a system font + pub fn get_system(&self) -> Option<SystemFont> { + match *self { + % if engine == "gecko": + % for prop in SYSTEM_FONT_LONGHANDS: + PropertyDeclaration::${to_camel_case(prop)}(ref prop) => { + prop.get_system() + } + % endfor + % endif + _ => None, + } + } +} + +#[cfg(feature = "gecko")] +pub use super::gecko::style_structs; + +/// The module where all the style structs are defined. +#[cfg(feature = "servo")] +pub mod style_structs { + use fxhash::FxHasher; + use super::longhands; + use std::hash::{Hash, Hasher}; + use crate::logical_geometry::WritingMode; + use crate::media_queries::Device; + use crate::values::computed::NonNegativeLength; + + % for style_struct in data.active_style_structs(): + % if style_struct.name == "Font": + #[derive(Clone, Debug, MallocSizeOf)] + #[cfg_attr(feature = "servo", derive(Serialize, Deserialize))] + % else: + #[derive(Clone, Debug, MallocSizeOf, PartialEq)] + % endif + /// The ${style_struct.name} style struct. + pub struct ${style_struct.name} { + % for longhand in style_struct.longhands: + % if not longhand.logical: + /// The ${longhand.name} computed value. + pub ${longhand.ident}: longhands::${longhand.ident}::computed_value::T, + % endif + % endfor + % if style_struct.name == "InheritedText": + /// The "used" text-decorations that apply to this box. + /// + /// FIXME(emilio): This is technically a box-tree concept, and + /// would be nice to move away from style. + pub text_decorations_in_effect: crate::values::computed::text::TextDecorationsInEffect, + % endif + % if style_struct.name == "Font": + /// The font hash, used for font caching. + pub hash: u64, + % endif + % if style_struct.name == "Box": + /// The display value specified by the CSS stylesheets (without any style adjustments), + /// which is needed for hypothetical layout boxes. + pub original_display: longhands::display::computed_value::T, + % endif + } + % if style_struct.name == "Font": + impl PartialEq for Font { + fn eq(&self, other: &Font) -> bool { + self.hash == other.hash + % for longhand in style_struct.longhands: + && self.${longhand.ident} == other.${longhand.ident} + % endfor + } + } + % endif + + impl ${style_struct.name} { + % for longhand in style_struct.longhands: + % if not longhand.logical: + % if longhand.ident == "display": + /// Set `display`. + /// + /// We need to keep track of the original display for hypothetical boxes, + /// so we need to special-case this. + #[allow(non_snake_case)] + #[inline] + pub fn set_display(&mut self, v: longhands::display::computed_value::T) { + self.display = v; + self.original_display = v; + } + % else: + /// Set ${longhand.name}. + #[allow(non_snake_case)] + #[inline] + pub fn set_${longhand.ident}(&mut self, v: longhands::${longhand.ident}::computed_value::T) { + self.${longhand.ident} = v; + } + % endif + % if longhand.ident == "display": + /// Set `display` from other struct. + /// + /// Same as `set_display` above. + /// Thus, we need to special-case this. + #[allow(non_snake_case)] + #[inline] + pub fn copy_display_from(&mut self, other: &Self) { + self.display = other.display.clone(); + self.original_display = other.display.clone(); + } + % else: + /// Set ${longhand.name} from other struct. + #[allow(non_snake_case)] + #[inline] + pub fn copy_${longhand.ident}_from(&mut self, other: &Self) { + self.${longhand.ident} = other.${longhand.ident}.clone(); + } + % endif + /// Reset ${longhand.name} from the initial struct. + #[allow(non_snake_case)] + #[inline] + pub fn reset_${longhand.ident}(&mut self, other: &Self) { + self.copy_${longhand.ident}_from(other) + } + + /// Get the computed value for ${longhand.name}. + #[allow(non_snake_case)] + #[inline] + pub fn clone_${longhand.ident}(&self) -> longhands::${longhand.ident}::computed_value::T { + self.${longhand.ident}.clone() + } + % endif + % if longhand.need_index: + /// If this longhand is indexed, get the number of elements. + #[allow(non_snake_case)] + pub fn ${longhand.ident}_count(&self) -> usize { + self.${longhand.ident}.0.len() + } + + /// If this longhand is indexed, get the element at given + /// index. + #[allow(non_snake_case)] + pub fn ${longhand.ident}_at(&self, index: usize) + -> longhands::${longhand.ident}::computed_value::SingleComputedValue { + self.${longhand.ident}.0[index].clone() + } + % endif + % endfor + % if style_struct.name == "Border": + % for side in ["top", "right", "bottom", "left"]: + /// Whether the border-${side} property has nonzero width. + #[allow(non_snake_case)] + pub fn border_${side}_has_nonzero_width(&self) -> bool { + use crate::Zero; + !self.border_${side}_width.is_zero() + } + % endfor + % elif style_struct.name == "Font": + /// Computes a font hash in order to be able to cache fonts + /// effectively in GFX and layout. + pub fn compute_font_hash(&mut self) { + // Corresponds to the fields in + // `gfx::font_template::FontTemplateDescriptor`. + let mut hasher: FxHasher = Default::default(); + self.font_weight.hash(&mut hasher); + self.font_stretch.hash(&mut hasher); + self.font_style.hash(&mut hasher); + self.font_family.hash(&mut hasher); + self.hash = hasher.finish() + } + + /// (Servo does not handle MathML, so this just calls copy_font_size_from) + pub fn inherit_font_size_from(&mut self, parent: &Self, + _: Option<NonNegativeLength>, + _: &Device) { + self.copy_font_size_from(parent); + } + /// (Servo does not handle MathML, so this just calls set_font_size) + pub fn apply_font_size(&mut self, + v: longhands::font_size::computed_value::T, + _: &Self, + _: &Device) -> Option<NonNegativeLength> { + self.set_font_size(v); + None + } + /// (Servo does not handle MathML, so this does nothing) + pub fn apply_unconstrained_font_size(&mut self, _: NonNegativeLength) { + } + + % elif style_struct.name == "Outline": + /// Whether the outline-width property is non-zero. + #[inline] + pub fn outline_has_nonzero_width(&self) -> bool { + use crate::Zero; + !self.outline_width.is_zero() + } + % elif style_struct.name == "Box": + /// Sets the display property, but without touching original_display, + /// except when the adjustment comes from root or item display fixups. + pub fn set_adjusted_display( + &mut self, + dpy: longhands::display::computed_value::T, + is_item_or_root: bool + ) { + self.display = dpy; + if is_item_or_root { + self.original_display = dpy; + } + } + % endif + } + + % endfor +} + +% for style_struct in data.active_style_structs(): + impl style_structs::${style_struct.name} { + % for longhand in style_struct.longhands: + % if longhand.need_index: + /// Iterate over the values of ${longhand.name}. + #[allow(non_snake_case)] + #[inline] + pub fn ${longhand.ident}_iter(&self) -> ${longhand.camel_case}Iter { + ${longhand.camel_case}Iter { + style_struct: self, + current: 0, + max: self.${longhand.ident}_count(), + } + } + + /// Get a value mod `index` for the property ${longhand.name}. + #[allow(non_snake_case)] + #[inline] + pub fn ${longhand.ident}_mod(&self, index: usize) + -> longhands::${longhand.ident}::computed_value::SingleComputedValue { + self.${longhand.ident}_at(index % self.${longhand.ident}_count()) + } + + /// Clone the computed value for the property. + #[allow(non_snake_case)] + #[inline] + #[cfg(feature = "gecko")] + pub fn clone_${longhand.ident}( + &self, + ) -> longhands::${longhand.ident}::computed_value::T { + longhands::${longhand.ident}::computed_value::List( + self.${longhand.ident}_iter().collect() + ) + } + % endif + % endfor + + % if style_struct.name == "UI": + /// Returns whether there is any animation specified with + /// animation-name other than `none`. + pub fn specifies_animations(&self) -> bool { + self.animation_name_iter().any(|name| !name.is_none()) + } + + /// Returns whether there are any transitions specified. + #[cfg(feature = "servo")] + pub fn specifies_transitions(&self) -> bool { + (0..self.transition_property_count()).any(|index| { + let combined_duration = + self.transition_duration_mod(index).seconds().max(0.) + + self.transition_delay_mod(index).seconds(); + combined_duration > 0. + }) + } + + /// Returns whether there is any named progress timeline specified with + /// scroll-timeline-name other than `none`. + pub fn specifies_scroll_timelines(&self) -> bool { + self.scroll_timeline_name_iter().any(|name| !name.is_none()) + } + + /// Returns whether there is any named progress timeline specified with + /// view-timeline-name other than `none`. + pub fn specifies_view_timelines(&self) -> bool { + self.view_timeline_name_iter().any(|name| !name.is_none()) + } + + /// Returns true if animation properties are equal between styles, but without + /// considering keyframe data and animation-timeline. + #[cfg(feature = "servo")] + pub fn animations_equals(&self, other: &Self) -> bool { + self.animation_name_iter().eq(other.animation_name_iter()) && + self.animation_delay_iter().eq(other.animation_delay_iter()) && + self.animation_direction_iter().eq(other.animation_direction_iter()) && + self.animation_duration_iter().eq(other.animation_duration_iter()) && + self.animation_fill_mode_iter().eq(other.animation_fill_mode_iter()) && + self.animation_iteration_count_iter().eq(other.animation_iteration_count_iter()) && + self.animation_play_state_iter().eq(other.animation_play_state_iter()) && + self.animation_timing_function_iter().eq(other.animation_timing_function_iter()) + } + + % elif style_struct.name == "Column": + /// Whether this is a multicol style. + #[cfg(feature = "servo")] + pub fn is_multicol(&self) -> bool { + !self.column_width.is_auto() || !self.column_count.is_auto() + } + % endif + } + + % for longhand in style_struct.longhands: + % if longhand.need_index: + /// An iterator over the values of the ${longhand.name} properties. + pub struct ${longhand.camel_case}Iter<'a> { + style_struct: &'a style_structs::${style_struct.name}, + current: usize, + max: usize, + } + + impl<'a> Iterator for ${longhand.camel_case}Iter<'a> { + type Item = longhands::${longhand.ident}::computed_value::SingleComputedValue; + + fn next(&mut self) -> Option<Self::Item> { + self.current += 1; + if self.current <= self.max { + Some(self.style_struct.${longhand.ident}_at(self.current - 1)) + } else { + None + } + } + } + % endif + % endfor +% endfor + + +#[cfg(feature = "gecko")] +pub use super::gecko::{ComputedValues, ComputedValuesInner}; + +#[cfg(feature = "servo")] +#[cfg_attr(feature = "servo", derive(Clone, Debug))] +/// Actual data of ComputedValues, to match up with Gecko +pub struct ComputedValuesInner { + % for style_struct in data.active_style_structs(): + ${style_struct.ident}: Arc<style_structs::${style_struct.name}>, + % endfor + custom_properties: crate::custom_properties::ComputedCustomProperties, + + /// The writing mode of this computed values struct. + pub writing_mode: WritingMode, + + /// The effective zoom value. + pub effective_zoom: Zoom, + + /// A set of flags we use to store misc information regarding this style. + pub flags: ComputedValueFlags, + + /// The rule node representing the ordered list of rules matched for this + /// node. Can be None for default values and text nodes. This is + /// essentially an optimization to avoid referencing the root rule node. + pub rules: Option<StrongRuleNode>, + + /// The element's computed values if visited, only computed if there's a + /// relevant link for this element. A element's "relevant link" is the + /// element being matched if it is a link or the nearest ancestor link. + visited_style: Option<Arc<ComputedValues>>, +} + +/// The struct that Servo uses to represent computed values. +/// +/// This struct contains an immutable atomically-reference-counted pointer to +/// every kind of style struct. +/// +/// When needed, the structs may be copied in order to get mutated. +#[cfg(feature = "servo")] +#[cfg_attr(feature = "servo", derive(Clone, Debug))] +pub struct ComputedValues { + /// The actual computed values + /// + /// In Gecko the outer ComputedValues is actually a ComputedStyle, whereas + /// ComputedValuesInner is the core set of computed values. + /// + /// We maintain this distinction in servo to reduce the amount of special + /// casing. + inner: ComputedValuesInner, + + /// The pseudo-element that we're using. + pseudo: Option<PseudoElement>, +} + +impl ComputedValues { + /// Returns the pseudo-element that this style represents. + #[cfg(feature = "servo")] + pub fn pseudo(&self) -> Option<<&PseudoElement> { + self.pseudo.as_ref() + } + + /// Returns true if this is the style for a pseudo-element. + #[cfg(feature = "servo")] + pub fn is_pseudo_style(&self) -> bool { + self.pseudo().is_some() + } + + /// Returns whether this style's display value is equal to contents. + pub fn is_display_contents(&self) -> bool { + self.clone_display().is_contents() + } + + /// Gets a reference to the rule node. Panic if no rule node exists. + pub fn rules(&self) -> &StrongRuleNode { + self.rules.as_ref().unwrap() + } + + /// Returns the visited rules, if applicable. + pub fn visited_rules(&self) -> Option<<&StrongRuleNode> { + self.visited_style().and_then(|s| s.rules.as_ref()) + } + + /// Gets a reference to the custom properties map (if one exists). + pub fn custom_properties(&self) -> &crate::custom_properties::ComputedCustomProperties { + &self.custom_properties + } + + /// Returns whether we have the same custom properties as another style. + pub fn custom_properties_equal(&self, other: &Self) -> bool { + self.custom_properties() == other.custom_properties() + } + +% for prop in data.longhands: +% if not prop.logical: + /// Gets the computed value of a given property. + #[inline(always)] + #[allow(non_snake_case)] + pub fn clone_${prop.ident}( + &self, + ) -> longhands::${prop.ident}::computed_value::T { + self.get_${prop.style_struct.ident.strip("_")}().clone_${prop.ident}() + } +% endif +% endfor + + /// Writes the (resolved or computed) value of the given longhand as a string in `dest`. + /// + /// TODO(emilio): We should move all the special resolution from + /// nsComputedDOMStyle to ToResolvedValue instead. + pub fn computed_or_resolved_value( + &self, + property_id: LonghandId, + context: Option<<&resolved::Context>, + dest: &mut CssStringWriter, + ) -> fmt::Result { + use crate::values::resolved::ToResolvedValue; + let mut dest = CssWriter::new(dest); + let property_id = property_id.to_physical(self.writing_mode); + match property_id { + % for specified_type, props in groupby(data.longhands, key=lambda x: x.specified_type()): + <% props = list(props) %> + ${" |\n".join("LonghandId::{}".format(p.camel_case) for p in props)} => { + let value = match property_id { + % for prop in props: + % if not prop.logical: + LonghandId::${prop.camel_case} => self.clone_${prop.ident}(), + % endif + % endfor + _ => unsafe { debug_unreachable!() }, + }; + if let Some(c) = context { + value.to_resolved_value(c).to_css(&mut dest) + } else { + value.to_css(&mut dest) + } + } + % endfor + } + } + + /// Returns the given longhand's resolved value as a property declaration. + pub fn computed_or_resolved_declaration( + &self, + property_id: LonghandId, + context: Option<<&resolved::Context>, + ) -> PropertyDeclaration { + use crate::values::resolved::ToResolvedValue; + use crate::values::computed::ToComputedValue; + let physical_property_id = property_id.to_physical(self.writing_mode); + match physical_property_id { + % for specified_type, props in groupby(data.longhands, key=lambda x: x.specified_type()): + <% props = list(props) %> + ${" |\n".join("LonghandId::{}".format(p.camel_case) for p in props)} => { + let mut computed_value = match physical_property_id { + % for prop in props: + % if not prop.logical: + LonghandId::${prop.camel_case} => self.clone_${prop.ident}(), + % endif + % endfor + _ => unsafe { debug_unreachable!() }, + }; + if let Some(c) = context { + let resolved = computed_value.to_resolved_value(c); + computed_value = ToResolvedValue::from_resolved_value(resolved); + } + let specified = ToComputedValue::from_computed_value(&computed_value); + % if props[0].boxed: + let specified = Box::new(specified); + % endif + % if len(props) == 1: + PropertyDeclaration::${props[0].camel_case}(specified) + % else: + unsafe { + let mut out = mem::MaybeUninit::uninit(); + ptr::write( + out.as_mut_ptr() as *mut PropertyDeclarationVariantRepr<${specified_type}>, + PropertyDeclarationVariantRepr { + tag: property_id as u16, + value: specified, + }, + ); + out.assume_init() + } + % endif + } + % endfor + } + } + + /// Resolves the currentColor keyword. + /// + /// Any color value from computed values (except for the 'color' property + /// itself) should go through this method. + /// + /// Usage example: + /// let top_color = + /// style.resolve_color(style.get_border().clone_border_top_color()); + #[inline] + pub fn resolve_color(&self, color: computed::Color) -> crate::color::AbsoluteColor { + let current_color = self.get_inherited_text().clone_color(); + color.resolve_to_absolute(¤t_color) + } + + /// Returns which longhand properties have different values in the two + /// ComputedValues. + #[cfg(feature = "gecko_debug")] + pub fn differing_properties(&self, other: &ComputedValues) -> LonghandIdSet { + let mut set = LonghandIdSet::new(); + % for prop in data.longhands: + % if not prop.logical: + if self.clone_${prop.ident}() != other.clone_${prop.ident}() { + set.insert(LonghandId::${prop.camel_case}); + } + % endif + % endfor + set + } + + /// Create a `TransitionPropertyIterator` for this styles transition properties. + pub fn transition_properties<'a>( + &'a self + ) -> animated_properties::TransitionPropertyIterator<'a> { + animated_properties::TransitionPropertyIterator::from_style(self) + } +} + +#[cfg(feature = "servo")] +impl ComputedValues { + /// Create a new refcounted `ComputedValues` + pub fn new( + pseudo: Option<<&PseudoElement>, + custom_properties: crate::custom_properties::ComputedCustomProperties, + writing_mode: WritingMode, + effective_zoom: computed::Zoom, + flags: ComputedValueFlags, + rules: Option<StrongRuleNode>, + visited_style: Option<Arc<ComputedValues>>, + % for style_struct in data.active_style_structs(): + ${style_struct.ident}: Arc<style_structs::${style_struct.name}>, + % endfor + ) -> Arc<Self> { + Arc::new(Self { + inner: ComputedValuesInner { + custom_properties, + writing_mode, + rules, + visited_style, + effective_zoom, + flags, + % for style_struct in data.active_style_structs(): + ${style_struct.ident}, + % endfor + }, + pseudo: pseudo.cloned(), + }) + } + + /// Get the initial computed values. + pub fn initial_values() -> &'static Self { &*INITIAL_SERVO_VALUES } + + /// Serializes the computed value of this property as a string. + pub fn computed_value_to_string(&self, property: PropertyDeclarationId) -> String { + match property { + PropertyDeclarationId::Longhand(id) => { + let mut s = String::new(); + self.get_longhand_property_value( + id, + &mut CssWriter::new(&mut s) + ).unwrap(); + s + } + PropertyDeclarationId::Custom(name) => { + // FIXME(bug 1869476): This should use a stylist to determine + // whether the name corresponds to an inherited custom property + // and then choose the inherited/non_inherited map accordingly. + let p = &self.custom_properties; + let value = p + .inherited + .as_ref() + .and_then(|map| map.get(name)) + .or_else(|| p.non_inherited.as_ref().and_then(|map| map.get(name))); + value.map_or(String::new(), |value| value.to_css_string()) + } + } + } +} + +#[cfg(feature = "servo")] +impl ops::Deref for ComputedValues { + type Target = ComputedValuesInner; + fn deref(&self) -> &ComputedValuesInner { + &self.inner + } +} + +#[cfg(feature = "servo")] +impl ops::DerefMut for ComputedValues { + fn deref_mut(&mut self) -> &mut ComputedValuesInner { + &mut self.inner + } +} + +#[cfg(feature = "servo")] +impl ComputedValuesInner { + /// Returns the visited style, if any. + pub fn visited_style(&self) -> Option<<&ComputedValues> { + self.visited_style.as_deref() + } + + % for style_struct in data.active_style_structs(): + /// Clone the ${style_struct.name} struct. + #[inline] + pub fn clone_${style_struct.name_lower}(&self) -> Arc<style_structs::${style_struct.name}> { + self.${style_struct.ident}.clone() + } + + /// Get a immutable reference to the ${style_struct.name} struct. + #[inline] + pub fn get_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} { + &self.${style_struct.ident} + } + + /// Get a mutable reference to the ${style_struct.name} struct. + #[inline] + pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} { + Arc::make_mut(&mut self.${style_struct.ident}) + } + % endfor + + /// Gets a reference to the rule node. Panic if no rule node exists. + pub fn rules(&self) -> &StrongRuleNode { + self.rules.as_ref().unwrap() + } + + #[inline] + /// Returns whether the "content" property for the given style is completely + /// ineffective, and would yield an empty `::before` or `::after` + /// pseudo-element. + pub fn ineffective_content_property(&self) -> bool { + use crate::values::generics::counters::Content; + match self.get_counters().content { + Content::Normal | Content::None => true, + Content::Items(ref items) => items.is_empty(), + } + } + + /// Whether the current style or any of its ancestors is multicolumn. + #[inline] + pub fn can_be_fragmented(&self) -> bool { + self.flags.contains(ComputedValueFlags::CAN_BE_FRAGMENTED) + } + + /// Whether the current style is multicolumn. + #[inline] + pub fn is_multicol(&self) -> bool { + self.get_column().is_multicol() + } + + /// Get the logical computed inline size. + #[inline] + pub fn content_inline_size(&self) -> &computed::Size { + let position_style = self.get_position(); + if self.writing_mode.is_vertical() { + &position_style.height + } else { + &position_style.width + } + } + + /// Get the logical computed block size. + #[inline] + pub fn content_block_size(&self) -> &computed::Size { + let position_style = self.get_position(); + if self.writing_mode.is_vertical() { &position_style.width } else { &position_style.height } + } + + /// Get the logical computed min inline size. + #[inline] + pub fn min_inline_size(&self) -> &computed::Size { + let position_style = self.get_position(); + if self.writing_mode.is_vertical() { &position_style.min_height } else { &position_style.min_width } + } + + /// Get the logical computed min block size. + #[inline] + pub fn min_block_size(&self) -> &computed::Size { + let position_style = self.get_position(); + if self.writing_mode.is_vertical() { &position_style.min_width } else { &position_style.min_height } + } + + /// Get the logical computed max inline size. + #[inline] + pub fn max_inline_size(&self) -> &computed::MaxSize { + let position_style = self.get_position(); + if self.writing_mode.is_vertical() { &position_style.max_height } else { &position_style.max_width } + } + + /// Get the logical computed max block size. + #[inline] + pub fn max_block_size(&self) -> &computed::MaxSize { + let position_style = self.get_position(); + if self.writing_mode.is_vertical() { &position_style.max_width } else { &position_style.max_height } + } + + /// Get the logical computed padding for this writing mode. + #[inline] + pub fn logical_padding(&self) -> LogicalMargin<<&computed::LengthPercentage> { + let padding_style = self.get_padding(); + LogicalMargin::from_physical(self.writing_mode, SideOffsets2D::new( + &padding_style.padding_top.0, + &padding_style.padding_right.0, + &padding_style.padding_bottom.0, + &padding_style.padding_left.0, + )) + } + + /// Get the logical border width + #[inline] + pub fn border_width_for_writing_mode(&self, writing_mode: WritingMode) -> LogicalMargin<Au> { + let border_style = self.get_border(); + LogicalMargin::from_physical(writing_mode, SideOffsets2D::new( + Au::from(border_style.border_top_width), + Au::from(border_style.border_right_width), + Au::from(border_style.border_bottom_width), + Au::from(border_style.border_left_width), + )) + } + + /// Gets the logical computed border widths for this style. + #[inline] + pub fn logical_border_width(&self) -> LogicalMargin<Au> { + self.border_width_for_writing_mode(self.writing_mode) + } + + /// Gets the logical computed margin from this style. + #[inline] + pub fn logical_margin(&self) -> LogicalMargin<<&computed::LengthPercentageOrAuto> { + let margin_style = self.get_margin(); + LogicalMargin::from_physical(self.writing_mode, SideOffsets2D::new( + &margin_style.margin_top, + &margin_style.margin_right, + &margin_style.margin_bottom, + &margin_style.margin_left, + )) + } + + /// Gets the logical position from this style. + #[inline] + pub fn logical_position(&self) -> LogicalMargin<<&computed::LengthPercentageOrAuto> { + // FIXME(SimonSapin): should be the writing mode of the containing block, maybe? + let position_style = self.get_position(); + LogicalMargin::from_physical(self.writing_mode, SideOffsets2D::new( + &position_style.top, + &position_style.right, + &position_style.bottom, + &position_style.left, + )) + } + + /// Return true if the effects force the transform style to be Flat + pub fn overrides_transform_style(&self) -> bool { + use crate::computed_values::mix_blend_mode::T as MixBlendMode; + + let effects = self.get_effects(); + // TODO(gw): Add clip-path, isolation, mask-image, mask-border-source when supported. + effects.opacity < 1.0 || + !effects.filter.0.is_empty() || + !effects.clip.is_auto() || + effects.mix_blend_mode != MixBlendMode::Normal + } + + /// <https://drafts.csswg.org/css-transforms/#grouping-property-values> + pub fn get_used_transform_style(&self) -> computed_values::transform_style::T { + use crate::computed_values::transform_style::T as TransformStyle; + + let box_ = self.get_box(); + + if self.overrides_transform_style() { + TransformStyle::Flat + } else { + // Return the computed value if not overridden by the above exceptions + box_.transform_style + } + } + + /// Whether given this transform value, the compositor would require a + /// layer. + pub fn transform_requires_layer(&self) -> bool { + use crate::values::generics::transform::TransformOperation; + // Check if the transform matrix is 2D or 3D + for transform in &*self.get_box().transform.0 { + match *transform { + TransformOperation::Perspective(..) => { + return true; + } + TransformOperation::Matrix3D(m) => { + // See http://dev.w3.org/csswg/css-transforms/#2d-matrix + if m.m31 != 0.0 || m.m32 != 0.0 || + m.m13 != 0.0 || m.m23 != 0.0 || + m.m43 != 0.0 || m.m14 != 0.0 || + m.m24 != 0.0 || m.m34 != 0.0 || + m.m33 != 1.0 || m.m44 != 1.0 { + return true; + } + } + TransformOperation::Translate3D(_, _, z) | + TransformOperation::TranslateZ(z) => { + if z.px() != 0. { + return true; + } + } + _ => {} + } + } + + // Neither perspective nor transform present + false + } +} + +/// A reference to a style struct of the parent, or our own style struct. +pub enum StyleStructRef<'a, T: 'static> { + /// A borrowed struct from the parent, for example, for inheriting style. + Borrowed(&'a T), + /// An owned struct, that we've already mutated. + Owned(UniqueArc<T>), + /// Temporarily vacated, will panic if accessed + Vacated, +} + +impl<'a, T: 'a> StyleStructRef<'a, T> +where + T: Clone, +{ + /// Ensure a mutable reference of this value exists, either cloning the + /// borrowed value, or returning the owned one. + pub fn mutate(&mut self) -> &mut T { + if let StyleStructRef::Borrowed(v) = *self { + *self = StyleStructRef::Owned(UniqueArc::new(v.clone())); + } + + match *self { + StyleStructRef::Owned(ref mut v) => v, + StyleStructRef::Borrowed(..) => unreachable!(), + StyleStructRef::Vacated => panic!("Accessed vacated style struct") + } + } + + /// Whether this is pointer-equal to the struct we're going to copy the + /// value from. + /// + /// This is used to avoid allocations when people write stuff like `font: + /// inherit` or such `all: initial`. + #[inline] + pub fn ptr_eq(&self, struct_to_copy_from: &T) -> bool { + match *self { + StyleStructRef::Owned(..) => false, + StyleStructRef::Borrowed(s) => { + s as *const T == struct_to_copy_from as *const T + } + StyleStructRef::Vacated => panic!("Accessed vacated style struct") + } + } + + /// Extract a unique Arc from this struct, vacating it. + /// + /// The vacated state is a transient one, please put the Arc back + /// when done via `put()`. This function is to be used to separate + /// the struct being mutated from the computed context + pub fn take(&mut self) -> UniqueArc<T> { + use std::mem::replace; + let inner = replace(self, StyleStructRef::Vacated); + + match inner { + StyleStructRef::Owned(arc) => arc, + StyleStructRef::Borrowed(s) => UniqueArc::new(s.clone()), + StyleStructRef::Vacated => panic!("Accessed vacated style struct"), + } + } + + /// Replace vacated ref with an arc + pub fn put(&mut self, arc: UniqueArc<T>) { + debug_assert!(matches!(*self, StyleStructRef::Vacated)); + *self = StyleStructRef::Owned(arc); + } + + /// Get a mutable reference to the owned struct, or `None` if the struct + /// hasn't been mutated. + pub fn get_if_mutated(&mut self) -> Option<<&mut T> { + match *self { + StyleStructRef::Owned(ref mut v) => Some(v), + StyleStructRef::Borrowed(..) => None, + StyleStructRef::Vacated => panic!("Accessed vacated style struct") + } + } + + /// Returns an `Arc` to the internal struct, constructing one if + /// appropriate. + pub fn build(self) -> Arc<T> { + match self { + StyleStructRef::Owned(v) => v.shareable(), + // SAFETY: We know all style structs are arc-allocated. + StyleStructRef::Borrowed(v) => unsafe { Arc::from_raw_addrefed(v) }, + StyleStructRef::Vacated => panic!("Accessed vacated style struct") + } + } +} + +impl<'a, T: 'a> ops::Deref for StyleStructRef<'a, T> { + type Target = T; + + fn deref(&self) -> &T { + match *self { + StyleStructRef::Owned(ref v) => &**v, + StyleStructRef::Borrowed(v) => v, + StyleStructRef::Vacated => panic!("Accessed vacated style struct") + } + } +} + +/// A type used to compute a struct with minimal overhead. +/// +/// This allows holding references to the parent/default computed values without +/// actually cloning them, until we either build the style, or mutate the +/// inherited value. +pub struct StyleBuilder<'a> { + /// The device we're using to compute style. + /// + /// This provides access to viewport unit ratios, etc. + pub device: &'a Device, + + /// The stylist we're using to compute style except for media queries. + /// device is used in media queries instead. + pub stylist: Option<<&'a Stylist>, + + /// The style we're inheriting from. + /// + /// This is effectively + /// `parent_style.unwrap_or(device.default_computed_values())`. + inherited_style: &'a ComputedValues, + + /// The style we're getting reset structs from. + reset_style: &'a ComputedValues, + + /// The rule node representing the ordered list of rules matched for this + /// node. + pub rules: Option<StrongRuleNode>, + + /// The computed custom properties. + pub custom_properties: crate::custom_properties::ComputedCustomProperties, + + /// Non-custom properties that are considered invalid at compute time + /// due to cyclic dependencies with custom properties. + /// e.g. `--foo: 1em; font-size: var(--foo)` where `--foo` is registered. + pub invalid_non_custom_properties: LonghandIdSet, + + /// The pseudo-element this style will represent. + pub pseudo: Option<<&'a PseudoElement>, + + /// Whether we have mutated any reset structs since the the last time + /// `clear_modified_reset` was called. This is used to tell whether the + /// `StyleAdjuster` did any work. + modified_reset: bool, + + /// Whether this is the style for the root element. + pub is_root_element: bool, + + /// The writing mode flags. + /// + /// TODO(emilio): Make private. + pub writing_mode: WritingMode, + + /// The effective zoom. + pub effective_zoom: computed::Zoom, + + /// Flags for the computed value. + pub flags: Cell<ComputedValueFlags>, + + /// The element's style if visited, only computed if there's a relevant link + /// for this element. A element's "relevant link" is the element being + /// matched if it is a link or the nearest ancestor link. + pub visited_style: Option<Arc<ComputedValues>>, + % for style_struct in data.active_style_structs(): + ${style_struct.ident}: StyleStructRef<'a, style_structs::${style_struct.name}>, + % endfor +} + +impl<'a> StyleBuilder<'a> { + /// Trivially construct a `StyleBuilder`. + pub fn new( + device: &'a Device, + stylist: Option<<&'a Stylist>, + parent_style: Option<<&'a ComputedValues>, + pseudo: Option<<&'a PseudoElement>, + rules: Option<StrongRuleNode>, + is_root_element: bool, + ) -> Self { + let reset_style = device.default_computed_values(); + let inherited_style = parent_style.unwrap_or(reset_style); + + let flags = inherited_style.flags.inherited(); + StyleBuilder { + device, + stylist, + inherited_style, + reset_style, + pseudo, + rules, + modified_reset: false, + is_root_element, + custom_properties: crate::custom_properties::ComputedCustomProperties::default(), + invalid_non_custom_properties: LonghandIdSet::default(), + writing_mode: inherited_style.writing_mode, + effective_zoom: inherited_style.effective_zoom, + flags: Cell::new(flags), + visited_style: None, + % for style_struct in data.active_style_structs(): + % if style_struct.inherited: + ${style_struct.ident}: StyleStructRef::Borrowed(inherited_style.get_${style_struct.name_lower}()), + % else: + ${style_struct.ident}: StyleStructRef::Borrowed(reset_style.get_${style_struct.name_lower}()), + % endif + % endfor + } + } + + /// NOTE(emilio): This is done so we can compute relative units with respect + /// to the parent style, but all the early properties / writing-mode / etc + /// are already set to the right ones on the kid. + /// + /// Do _not_ actually call this to construct a style, this should mostly be + /// used for animations. + pub fn for_animation( + device: &'a Device, + stylist: Option<<&'a Stylist>, + style_to_derive_from: &'a ComputedValues, + parent_style: Option<<&'a ComputedValues>, + ) -> Self { + let reset_style = device.default_computed_values(); + let inherited_style = parent_style.unwrap_or(reset_style); + StyleBuilder { + device, + stylist, + inherited_style, + reset_style, + pseudo: None, + modified_reset: false, + is_root_element: false, + rules: None, + custom_properties: style_to_derive_from.custom_properties().clone(), + invalid_non_custom_properties: LonghandIdSet::default(), + writing_mode: style_to_derive_from.writing_mode, + effective_zoom: style_to_derive_from.effective_zoom, + flags: Cell::new(style_to_derive_from.flags), + visited_style: None, + % for style_struct in data.active_style_structs(): + ${style_struct.ident}: StyleStructRef::Borrowed( + style_to_derive_from.get_${style_struct.name_lower}() + ), + % endfor + } + } + + /// Copy the reset properties from `style`. + pub fn copy_reset_from(&mut self, style: &'a ComputedValues) { + % for style_struct in data.active_style_structs(): + % if not style_struct.inherited: + self.${style_struct.ident} = + StyleStructRef::Borrowed(style.get_${style_struct.name_lower}()); + % endif + % endfor + } + + % for property in data.longhands: + % if not property.logical: + % if not property.style_struct.inherited: + /// Inherit `${property.ident}` from our parent style. + #[allow(non_snake_case)] + pub fn inherit_${property.ident}(&mut self) { + let inherited_struct = + self.inherited_style.get_${property.style_struct.name_lower}(); + + self.modified_reset = true; + self.add_flags(ComputedValueFlags::INHERITS_RESET_STYLE); + + % if property.ident == "content": + self.add_flags(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE); + % endif + + % if property.ident == "display": + self.add_flags(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE); + % endif + + if self.${property.style_struct.ident}.ptr_eq(inherited_struct) { + return; + } + + self.${property.style_struct.ident}.mutate() + .copy_${property.ident}_from(inherited_struct); + } + % else: + /// Reset `${property.ident}` to the initial value. + #[allow(non_snake_case)] + pub fn reset_${property.ident}(&mut self) { + let reset_struct = + self.reset_style.get_${property.style_struct.name_lower}(); + + if self.${property.style_struct.ident}.ptr_eq(reset_struct) { + return; + } + + self.${property.style_struct.ident}.mutate() + .reset_${property.ident}(reset_struct); + } + % endif + + % if not property.is_vector or property.simple_vector_bindings or engine in ["servo-2013", "servo-2020"]: + /// Set the `${property.ident}` to the computed value `value`. + #[allow(non_snake_case)] + pub fn set_${property.ident}( + &mut self, + value: longhands::${property.ident}::computed_value::T + ) { + % if not property.style_struct.inherited: + self.modified_reset = true; + % endif + + self.${property.style_struct.ident}.mutate() + .set_${property.ident}( + value, + % if property.logical: + self.writing_mode, + % endif + ); + } + % endif + % endif + % endfor + <% del property %> + + /// Inherits style from the parent element, accounting for the default + /// computed values that need to be provided as well. + pub fn for_inheritance( + device: &'a Device, + stylist: Option<<&'a Stylist>, + parent: Option<<&'a ComputedValues>, + pseudo: Option<<&'a PseudoElement>, + ) -> Self { + // Rebuild the visited style from the parent, ensuring that it will also + // not have rules. This matches the unvisited style that will be + // produced by this builder. This assumes that the caller doesn't need + // to adjust or process visited style, so we can just build visited + // style here for simplicity. + let visited_style = parent.and_then(|parent| { + parent.visited_style().map(|style| { + Self::for_inheritance( + device, + stylist, + Some(style), + pseudo, + ).build() + }) + }); + let custom_properties = if let Some(p) = parent { p.custom_properties().clone() } else { crate::custom_properties::ComputedCustomProperties::default() }; + let mut ret = Self::new( + device, + stylist, + parent, + pseudo, + /* rules = */ None, + /* is_root_element = */ false, + ); + ret.custom_properties = custom_properties; + ret.visited_style = visited_style; + ret + } + + /// Returns whether we have a visited style. + pub fn has_visited_style(&self) -> bool { + self.visited_style.is_some() + } + + /// Returns whether we're a pseudo-elements style. + pub fn is_pseudo_element(&self) -> bool { + self.pseudo.map_or(false, |p| !p.is_anon_box()) + } + + /// Returns the style we're getting reset properties from. + pub fn default_style(&self) -> &'a ComputedValues { + self.reset_style + } + + % for style_struct in data.active_style_structs(): + /// Gets an immutable view of the current `${style_struct.name}` style. + pub fn get_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} { + &self.${style_struct.ident} + } + + /// Gets a mutable view of the current `${style_struct.name}` style. + pub fn mutate_${style_struct.name_lower}(&mut self) -> &mut style_structs::${style_struct.name} { + % if not style_struct.inherited: + self.modified_reset = true; + % endif + self.${style_struct.ident}.mutate() + } + + /// Gets a mutable view of the current `${style_struct.name}` style. + pub fn take_${style_struct.name_lower}(&mut self) -> UniqueArc<style_structs::${style_struct.name}> { + % if not style_struct.inherited: + self.modified_reset = true; + % endif + self.${style_struct.ident}.take() + } + + /// Gets a mutable view of the current `${style_struct.name}` style. + pub fn put_${style_struct.name_lower}(&mut self, s: UniqueArc<style_structs::${style_struct.name}>) { + self.${style_struct.ident}.put(s) + } + + /// Gets a mutable view of the current `${style_struct.name}` style, + /// only if it's been mutated before. + pub fn get_${style_struct.name_lower}_if_mutated(&mut self) + -> Option<<&mut style_structs::${style_struct.name}> { + self.${style_struct.ident}.get_if_mutated() + } + + /// Reset the current `${style_struct.name}` style to its default value. + pub fn reset_${style_struct.name_lower}_struct(&mut self) { + self.${style_struct.ident} = + StyleStructRef::Borrowed(self.reset_style.get_${style_struct.name_lower}()); + } + % endfor + <% del style_struct %> + + /// Returns whether this computed style represents a floated object. + pub fn is_floating(&self) -> bool { + self.get_box().clone_float().is_floating() + } + + /// Returns whether this computed style represents an absolutely-positioned + /// object. + pub fn is_absolutely_positioned(&self) -> bool { + self.get_box().clone_position().is_absolutely_positioned() + } + + /// Whether this style has a top-layer style. + #[cfg(feature = "servo")] + pub fn in_top_layer(&self) -> bool { + matches!(self.get_box().clone__servo_top_layer(), + longhands::_servo_top_layer::computed_value::T::Top) + } + + /// Whether this style has a top-layer style. + #[cfg(feature = "gecko")] + pub fn in_top_layer(&self) -> bool { + matches!(self.get_box().clone__moz_top_layer(), + longhands::_moz_top_layer::computed_value::T::Top) + } + + /// Clears the "have any reset structs been modified" flag. + pub fn clear_modified_reset(&mut self) { + self.modified_reset = false; + } + + /// Returns whether we have mutated any reset structs since the the last + /// time `clear_modified_reset` was called. + pub fn modified_reset(&self) -> bool { + self.modified_reset + } + + /// Return the current flags. + #[inline] + pub fn flags(&self) -> ComputedValueFlags { + self.flags.get() + } + + /// Add a flag to the current builder. + #[inline] + pub fn add_flags(&self, flag: ComputedValueFlags) { + let flags = self.flags() | flag; + self.flags.set(flags); + } + + /// Removes a flag to the current builder. + #[inline] + pub fn remove_flags(&self, flag: ComputedValueFlags) { + let flags = self.flags() & !flag; + self.flags.set(flags); + } + + /// Turns this `StyleBuilder` into a proper `ComputedValues` instance. + pub fn build(self) -> Arc<ComputedValues> { + ComputedValues::new( + self.pseudo, + self.custom_properties, + self.writing_mode, + self.effective_zoom, + self.flags.get(), + self.rules, + self.visited_style, + % for style_struct in data.active_style_structs(): + self.${style_struct.ident}.build(), + % endfor + ) + } + + /// Get the custom properties map if necessary. + pub fn custom_properties(&self) -> &crate::custom_properties::ComputedCustomProperties { + &self.custom_properties + } + + + /// Get the inherited custom properties map. + pub fn inherited_custom_properties(&self) -> &crate::custom_properties::ComputedCustomProperties { + &self.inherited_style.custom_properties + } + + /// Access to various information about our inherited styles. We don't + /// expose an inherited ComputedValues directly, because in the + /// ::first-line case some of the inherited information needs to come from + /// one ComputedValues instance and some from a different one. + + /// Inherited writing-mode. + pub fn inherited_writing_mode(&self) -> &WritingMode { + &self.inherited_style.writing_mode + } + + /// The effective zoom value that we should multiply absolute lengths by. + pub fn effective_zoom(&self) -> computed::Zoom { + self.effective_zoom + } + + /// The zoom specified on this element. + pub fn specified_zoom(&self) -> computed::Zoom { + self.get_box().clone_zoom() + } + + /// Inherited zoom. + pub fn inherited_effective_zoom(&self) -> computed::Zoom { + self.inherited_style.effective_zoom + } + + /// The computed value flags of our parent. + #[inline] + pub fn get_parent_flags(&self) -> ComputedValueFlags { + self.inherited_style.flags + } + + /// Calculate the line height, given the currently resolved line-height and font. + pub fn calc_line_height( + &self, + device: &Device, + line_height_base: LineHeightBase, + writing_mode: WritingMode, + ) -> computed::NonNegativeLength { + use crate::computed_value_flags::ComputedValueFlags; + let (font, flag) = match line_height_base { + LineHeightBase::CurrentStyle => ( + self.get_font(), + ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS, + ), + LineHeightBase::InheritedStyle => ( + self.get_parent_font(), + ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS, + ), + }; + let line_height = font.clone_line_height(); + if matches!(line_height, computed::LineHeight::Normal) { + self.add_flags(flag); + } + device.calc_line_height(&font, writing_mode, None) + } + + /// And access to inherited style structs. + % for style_struct in data.active_style_structs(): + /// Gets our inherited `${style_struct.name}`. We don't name these + /// accessors `inherited_${style_struct.name_lower}` because we already + /// have things like "box" vs "inherited_box" as struct names. Do the + /// next-best thing and call them `parent_${style_struct.name_lower}` + /// instead. + pub fn get_parent_${style_struct.name_lower}(&self) -> &style_structs::${style_struct.name} { + self.inherited_style.get_${style_struct.name_lower}() + } + % endfor +} + +#[cfg(feature = "servo")] +pub use self::lazy_static_module::INITIAL_SERVO_VALUES; + +// Use a module to work around #[cfg] on lazy_static! not being applied to every generated item. +#[cfg(feature = "servo")] +#[allow(missing_docs)] +mod lazy_static_module { + use crate::logical_geometry::WritingMode; + use crate::computed_value_flags::ComputedValueFlags; + use servo_arc::Arc; + use super::{ComputedValues, ComputedValuesInner, longhands, style_structs}; + + lazy_static! { + /// The initial values for all style structs as defined by the specification. + pub static ref INITIAL_SERVO_VALUES: ComputedValues = ComputedValues { + inner: ComputedValuesInner { + % for style_struct in data.active_style_structs(): + ${style_struct.ident}: Arc::new(style_structs::${style_struct.name} { + % for longhand in style_struct.longhands: + % if not longhand.logical: + ${longhand.ident}: longhands::${longhand.ident}::get_initial_value(), + % endif + % endfor + % if style_struct.name == "InheritedText": + text_decorations_in_effect: + crate::values::computed::text::TextDecorationsInEffect::default(), + % endif + % if style_struct.name == "Font": + hash: 0, + % endif + % if style_struct.name == "Box": + original_display: longhands::display::get_initial_value(), + % endif + }), + % endfor + custom_properties, + writing_mode: WritingMode::empty(), + rules: None, + visited_style: None, + flags: ComputedValueFlags::empty(), + }, + pseudo: None, + }; + } +} + +/// A per-longhand function that performs the CSS cascade for that longhand. +pub type CascadePropertyFn = + unsafe extern "Rust" fn( + declaration: &PropertyDeclaration, + context: &mut computed::Context, + ); + +/// A per-longhand array of functions to perform the CSS cascade on each of +/// them, effectively doing virtual dispatch. +pub static CASCADE_PROPERTY: [CascadePropertyFn; ${len(data.longhands)}] = [ + % for property in data.longhands: + longhands::${property.ident}::cascade_property, + % endfor +]; + +/// See StyleAdjuster::adjust_for_border_width. +pub fn adjust_border_width(style: &mut StyleBuilder) { + % for side in ["top", "right", "bottom", "left"]: + // Like calling to_computed_value, which wouldn't type check. + if style.get_border().clone_border_${side}_style().none_or_hidden() && + style.get_border().border_${side}_has_nonzero_width() { + style.set_border_${side}_width(Au(0)); + } + % endfor +} + +/// An identifier for a given alias property. +#[derive(Clone, Copy, Eq, PartialEq, MallocSizeOf)] +#[repr(u16)] +pub enum AliasId { + % for i, property in enumerate(data.all_aliases()): + /// ${property.name} + ${property.camel_case} = ${i}, + % endfor +} + +impl fmt::Debug for AliasId { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + let name = NonCustomPropertyId::from(*self).name(); + formatter.write_str(name) + } +} + +impl AliasId { + /// Returns the property we're aliasing, as a longhand or a shorthand. + #[inline] + pub fn aliased_property(self) -> NonCustomPropertyId { + static MAP: [NonCustomPropertyId; ${len(data.all_aliases())}] = [ + % for alias in data.all_aliases(): + % if alias.original.type() == "longhand": + NonCustomPropertyId::from_longhand(LonghandId::${alias.original.camel_case}), + % else: + <% assert alias.original.type() == "shorthand" %> + NonCustomPropertyId::from_shorthand(ShorthandId::${alias.original.camel_case}), + % endif + % endfor + ]; + MAP[self as usize] + } +} + +/// Call the given macro with tokens like this for each longhand and shorthand properties +/// that is enabled in content: +/// +/// ``` +/// [CamelCaseName, SetCamelCaseName, PropertyId::Longhand(LonghandId::CamelCaseName)], +/// ``` +/// +/// NOTE(emilio): Callers are responsible to deal with prefs. +#[macro_export] +macro_rules! css_properties_accessors { + ($macro_name: ident) => { + $macro_name! { + % for kind, props in [("Longhand", data.longhands), ("Shorthand", data.shorthands)]: + % for property in props: + % if property.enabled_in_content(): + % for prop in [property] + property.aliases: + % if '-' in prop.name: + [${prop.ident.capitalize()}, Set${prop.ident.capitalize()}, + PropertyId::${kind}(${kind}Id::${property.camel_case})], + % endif + [${prop.camel_case}, Set${prop.camel_case}, + PropertyId::${kind}(${kind}Id::${property.camel_case})], + % endfor + % endif + % endfor + % endfor + } + } +} + +/// Call the given macro with tokens like this for each longhand properties: +/// +/// ``` +/// { snake_case_ident } +/// ``` +#[macro_export] +macro_rules! longhand_properties_idents { + ($macro_name: ident) => { + $macro_name! { + % for property in data.longhands: + { ${property.ident} } + % endfor + } + } +} + +// Large pages generate tens of thousands of ComputedValues. +size_of_test!(ComputedValues, 240); +// FFI relies on this. +size_of_test!(Option<Arc<ComputedValues>>, 8); + +// There are two reasons for this test to fail: +// +// * Your changes made a specified value type for a given property go +// over the threshold. In that case, you should try to shrink it again +// or, if not possible, mark the property as boxed in the property +// definition. +// +// * Your changes made a specified value type smaller, so that it no +// longer needs to be boxed. In this case you just need to remove +// boxed=True from the property definition. Nice job! +#[cfg(target_pointer_width = "64")] +#[allow(dead_code)] // https://github.com/rust-lang/rust/issues/96952 +const BOX_THRESHOLD: usize = 24; +% for longhand in data.longhands: +#[cfg(target_pointer_width = "64")] +% if longhand.boxed: +const_assert!(std::mem::size_of::<longhands::${longhand.ident}::SpecifiedValue>() > BOX_THRESHOLD); +% else: +const_assert!(std::mem::size_of::<longhands::${longhand.ident}::SpecifiedValue>() <= BOX_THRESHOLD); +% endif +% endfor + +% if engine in ["servo-2013", "servo-2020"]: +% for effect_name in ["repaint", "reflow_out_of_flow", "reflow", "rebuild_and_reflow_inline", "rebuild_and_reflow"]: + macro_rules! restyle_damage_${effect_name} { + ($old: ident, $new: ident, $damage: ident, [ $($effect:expr),* ]) => ({ + if + % for style_struct in data.active_style_structs(): + % for longhand in style_struct.longhands: + % if effect_name in longhand.servo_restyle_damage.split() and not longhand.logical: + $old.get_${style_struct.name_lower}().${longhand.ident} != + $new.get_${style_struct.name_lower}().${longhand.ident} || + % endif + % endfor + % endfor + + false { + $damage.insert($($effect)|*); + true + } else { + false + } + }) + } +% endfor +% endif diff --git a/servo/components/style/properties/shorthands/background.mako.rs b/servo/components/style/properties/shorthands/background.mako.rs new file mode 100644 index 0000000000..08838233f6 --- /dev/null +++ b/servo/components/style/properties/shorthands/background.mako.rs @@ -0,0 +1,289 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +// TODO: other background-* properties +<%helpers:shorthand name="background" + engines="gecko servo-2013 servo-2020" + sub_properties="background-color background-position-x background-position-y background-repeat + background-attachment background-image background-size background-origin + background-clip" + spec="https://drafts.csswg.org/css-backgrounds/#the-background"> + use crate::properties::longhands::{background_position_x, background_position_y, background_repeat}; + use crate::properties::longhands::{background_attachment, background_color, background_image, background_size, background_origin}; + use crate::properties::longhands::background_clip; + use crate::properties::longhands::background_clip::single_value::computed_value::T as Clip; + use crate::properties::longhands::background_origin::single_value::computed_value::T as Origin; + use crate::values::specified::{AllowQuirks, Color, Position, PositionComponent}; + use crate::parser::Parse; + + // FIXME(emilio): Should be the same type! + impl From<background_origin::single_value::SpecifiedValue> for background_clip::single_value::SpecifiedValue { + fn from(origin: background_origin::single_value::SpecifiedValue) -> + background_clip::single_value::SpecifiedValue { + match origin { + background_origin::single_value::SpecifiedValue::ContentBox => + background_clip::single_value::SpecifiedValue::ContentBox, + background_origin::single_value::SpecifiedValue::PaddingBox => + background_clip::single_value::SpecifiedValue::PaddingBox, + background_origin::single_value::SpecifiedValue::BorderBox => + background_clip::single_value::SpecifiedValue::BorderBox, + } + } + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut background_color = None; + + % for name in "image position_x position_y repeat size attachment origin clip".split(): + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut background_${name} = Vec::with_capacity(1); + % endfor + input.parse_comma_separated(|input| { + // background-color can only be in the last element, so if it + // is parsed anywhere before, the value is invalid. + if background_color.is_some() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + % for name in "image position repeat size attachment origin clip".split(): + let mut ${name} = None; + % endfor + loop { + if background_color.is_none() { + if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) { + background_color = Some(value); + continue + } + } + if position.is_none() { + if let Ok(value) = input.try_parse(|input| { + Position::parse_three_value_quirky(context, input, AllowQuirks::No) + }) { + position = Some(value); + + // Parse background size, if applicable. + size = input.try_parse(|input| { + input.expect_delim('/')?; + background_size::single_value::parse(context, input) + }).ok(); + + continue + } + } + % for name in "image repeat attachment origin clip".split(): + if ${name}.is_none() { + if let Ok(value) = input.try_parse(|input| background_${name}::single_value + ::parse(context, input)) { + ${name} = Some(value); + continue + } + } + % endfor + break + } + if clip.is_none() { + if let Some(origin) = origin { + clip = Some(background_clip::single_value::SpecifiedValue::from(origin)); + } + } + let mut any = false; + % for name in "image position repeat size attachment origin clip".split(): + any = any || ${name}.is_some(); + % endfor + any = any || background_color.is_some(); + if any { + if let Some(position) = position { + background_position_x.push(position.horizontal); + background_position_y.push(position.vertical); + } else { + background_position_x.push(PositionComponent::zero()); + background_position_y.push(PositionComponent::zero()); + } + % for name in "image repeat size attachment origin clip".split(): + if let Some(bg_${name}) = ${name} { + background_${name}.push(bg_${name}); + } else { + background_${name}.push(background_${name}::single_value + ::get_initial_specified_value()); + } + % endfor + Ok(()) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + })?; + + Ok(expanded! { + background_color: background_color.unwrap_or(Color::transparent()), + % for name in "image position_x position_y repeat size attachment origin clip".split(): + background_${name}: background_${name}::SpecifiedValue(background_${name}.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.background_image.0.len(); + // There should be at least one declared value + if len == 0 { + return Ok(()); + } + + // If a value list length is differs then we don't do a shorthand serialization. + // The exceptions to this is color which appears once only and is serialized + // with the last item. + % for name in "image position_x position_y size repeat origin clip attachment".split(): + if len != self.background_${name}.0.len() { + return Ok(()); + } + % endfor + + for i in 0..len { + % for name in "image position_x position_y repeat size attachment origin clip".split(): + let ${name} = &self.background_${name}.0[i]; + % endfor + + if i != 0 { + dest.write_str(", ")?; + } + + let mut wrote_value = false; + + if i == len - 1 { + if *self.background_color != background_color::get_initial_specified_value() { + self.background_color.to_css(dest)?; + wrote_value = true; + } + } + + if *image != background_image::single_value::get_initial_specified_value() { + if wrote_value { + dest.write_char(' ')?; + } + image.to_css(dest)?; + wrote_value = true; + } + + // Size is only valid after a position so when there is a + // non-initial size we must also serialize position + if *position_x != PositionComponent::zero() || + *position_y != PositionComponent::zero() || + *size != background_size::single_value::get_initial_specified_value() + { + if wrote_value { + dest.write_char(' ')?; + } + + Position { + horizontal: position_x.clone(), + vertical: position_y.clone() + }.to_css(dest)?; + + wrote_value = true; + + if *size != background_size::single_value::get_initial_specified_value() { + dest.write_str(" / ")?; + size.to_css(dest)?; + } + } + + % for name in "repeat attachment".split(): + if *${name} != background_${name}::single_value::get_initial_specified_value() { + if wrote_value { + dest.write_char(' ')?; + } + ${name}.to_css(dest)?; + wrote_value = true; + } + % endfor + + if *origin != Origin::PaddingBox || *clip != Clip::BorderBox { + if wrote_value { + dest.write_char(' ')?; + } + origin.to_css(dest)?; + if *clip != From::from(*origin) { + dest.write_char(' ')?; + clip.to_css(dest)?; + } + + wrote_value = true; + } + + if !wrote_value { + image.to_css(dest)?; + } + } + + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="background-position" + engines="gecko servo-2013 servo-2020" + flags="SHORTHAND_IN_GETCS" + sub_properties="background-position-x background-position-y" + spec="https://drafts.csswg.org/css-backgrounds-4/#the-background-position"> + use crate::properties::longhands::{background_position_x, background_position_y}; + use crate::values::specified::AllowQuirks; + use crate::values::specified::position::Position; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut position_x = Vec::with_capacity(1); + let mut position_y = Vec::with_capacity(1); + let mut any = false; + + input.parse_comma_separated(|input| { + let value = Position::parse_three_value_quirky(context, input, AllowQuirks::Yes)?; + position_x.push(value.horizontal); + position_y.push(value.vertical); + any = true; + Ok(()) + })?; + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(expanded! { + background_position_x: background_position_x::SpecifiedValue(position_x.into()), + background_position_y: background_position_y::SpecifiedValue(position_y.into()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.background_position_x.0.len(); + if len == 0 || len != self.background_position_y.0.len() { + return Ok(()); + } + for i in 0..len { + Position { + horizontal: self.background_position_x.0[i].clone(), + vertical: self.background_position_y.0[i].clone() + }.to_css(dest)?; + + if i < len - 1 { + dest.write_str(", ")?; + } + } + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/border.mako.rs b/servo/components/style/properties/shorthands/border.mako.rs new file mode 100644 index 0000000000..c6a87f3197 --- /dev/null +++ b/servo/components/style/properties/shorthands/border.mako.rs @@ -0,0 +1,491 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import to_rust_ident, ALL_SIDES, PHYSICAL_SIDES, maybe_moz_logical_alias %> + +${helpers.four_sides_shorthand( + "border-color", + "border-%s-color", + "specified::Color::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-backgrounds/#border-color", + allow_quirks="Yes", +)} + +${helpers.four_sides_shorthand( + "border-style", + "border-%s-style", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-backgrounds/#border-style", +)} + +<%helpers:shorthand + name="border-width" + engines="gecko servo-2013 servo-2020" + sub_properties="${ + ' '.join('border-%s-width' % side + for side in PHYSICAL_SIDES)}" + spec="https://drafts.csswg.org/css-backgrounds/#border-width"> + use crate::values::generics::rect::Rect; + use crate::values::specified::{AllowQuirks, BorderSideWidth}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let rect = Rect::parse_with(context, input, |_, i| { + BorderSideWidth::parse_quirky(context, i, AllowQuirks::Yes) + })?; + Ok(expanded! { + border_top_width: rect.0, + border_right_width: rect.1, + border_bottom_width: rect.2, + border_left_width: rect.3, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + % for side in PHYSICAL_SIDES: + let ${side} = &self.border_${side}_width; + % endfor + Rect::new(top, right, bottom, left).to_css(dest) + } + } +</%helpers:shorthand> + + +pub fn parse_border<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<(specified::Color, specified::BorderStyle, specified::BorderSideWidth), ParseError<'i>> { + use crate::values::specified::{Color, BorderStyle, BorderSideWidth}; + let _unused = context; + let mut color = None; + let mut style = None; + let mut width = None; + let mut any = false; + loop { + if width.is_none() { + if let Ok(value) = input.try_parse(|i| BorderSideWidth::parse(context, i)) { + width = Some(value); + any = true; + } + } + if style.is_none() { + if let Ok(value) = input.try_parse(BorderStyle::parse) { + style = Some(value); + any = true; + continue + } + } + if color.is_none() { + if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) { + color = Some(value); + any = true; + continue + } + } + break + } + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + Ok((color.unwrap_or(Color::CurrentColor), style.unwrap_or(BorderStyle::None), width.unwrap_or(BorderSideWidth::medium()))) +} + +% for side, logical in ALL_SIDES: + <% + spec = "https://drafts.csswg.org/css-backgrounds/#border-%s" % side + if logical: + spec = "https://drafts.csswg.org/css-logical-props/#propdef-border-%s" % side + %> + <%helpers:shorthand + name="border-${side}" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join( + 'border-%s-%s' % (side, prop) + for prop in ['width', 'style', 'color'] + )}" + aliases="${maybe_moz_logical_alias(engine, (side, logical), '-moz-border-%s')}" + spec="${spec}"> + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let (color, style, width) = super::parse_border(context, input)?; + Ok(expanded! { + border_${to_rust_ident(side)}_color: color, + border_${to_rust_ident(side)}_style: style, + border_${to_rust_ident(side)}_width: width + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + crate::values::specified::border::serialize_directional_border( + dest, + self.border_${to_rust_ident(side)}_width, + self.border_${to_rust_ident(side)}_style, + self.border_${to_rust_ident(side)}_color + ) + } + } + + </%helpers:shorthand> +% endfor + +<%helpers:shorthand name="border" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join('border-%s-%s' % (side, prop) + for side in PHYSICAL_SIDES for prop in ['width', 'style', 'color'] + )} + ${' '.join('border-image-%s' % name + for name in ['outset', 'repeat', 'slice', 'source', 'width'])}" + derive_value_info="False" + spec="https://drafts.csswg.org/css-backgrounds/#border"> + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::properties::longhands::{border_image_outset, border_image_repeat, border_image_slice}; + use crate::properties::longhands::{border_image_source, border_image_width}; + + let (color, style, width) = super::parse_border(context, input)?; + Ok(expanded! { + % for side in PHYSICAL_SIDES: + border_${side}_color: color.clone(), + border_${side}_style: style, + border_${side}_width: width.clone(), + % endfor + + // The ‘border’ shorthand resets ‘border-image’ to its initial value. + // See https://drafts.csswg.org/css-backgrounds-3/#the-border-shorthands + % for name in "outset repeat slice source width".split(): + border_image_${name}: border_image_${name}::get_initial_specified_value(), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::properties::longhands; + + // If any of the border-image longhands differ from their initial specified values we should not + // invoke serialize_directional_border(), so there is no point in continuing on to compute all_equal. + % for name in "outset repeat slice source width".split(): + if *self.border_image_${name} != longhands::border_image_${name}::get_initial_specified_value() { + return Ok(()); + } + % endfor + + let all_equal = { + % for side in PHYSICAL_SIDES: + let border_${side}_width = self.border_${side}_width; + let border_${side}_style = self.border_${side}_style; + let border_${side}_color = self.border_${side}_color; + % endfor + + border_top_width == border_right_width && + border_right_width == border_bottom_width && + border_bottom_width == border_left_width && + + border_top_style == border_right_style && + border_right_style == border_bottom_style && + border_bottom_style == border_left_style && + + border_top_color == border_right_color && + border_right_color == border_bottom_color && + border_bottom_color == border_left_color + }; + + // If all longhands are all present, then all sides should be the same, + // so we can just one set of color/style/width + if !all_equal { + return Ok(()) + } + crate::values::specified::border::serialize_directional_border( + dest, + self.border_${side}_width, + self.border_${side}_style, + self.border_${side}_color + ) + } + } + + // Just use the same as border-left. The border shorthand can't accept + // any value that the sub-shorthand couldn't. + <% + border_left = "<crate::properties::shorthands::border_left::Longhands as SpecifiedValueInfo>" + %> + impl SpecifiedValueInfo for Longhands { + const SUPPORTED_TYPES: u8 = ${border_left}::SUPPORTED_TYPES; + fn collect_completion_keywords(f: KeywordsCollectFn) { + ${border_left}::collect_completion_keywords(f); + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="border-radius" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join( + 'border-%s-radius' % (corner) + for corner in ['top-left', 'top-right', 'bottom-right', 'bottom-left'] + )}" + extra_prefixes="webkit" + spec="https://drafts.csswg.org/css-backgrounds/#border-radius" +> + use crate::values::generics::rect::Rect; + use crate::values::generics::border::BorderCornerRadius; + use crate::values::specified::border::BorderRadius; + use crate::parser::Parse; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let radii = BorderRadius::parse(context, input)?; + Ok(expanded! { + border_top_left_radius: radii.top_left, + border_top_right_radius: radii.top_right, + border_bottom_right_radius: radii.bottom_right, + border_bottom_left_radius: radii.bottom_left, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let LonghandsToSerialize { + border_top_left_radius: &BorderCornerRadius(ref tl), + border_top_right_radius: &BorderCornerRadius(ref tr), + border_bottom_right_radius: &BorderCornerRadius(ref br), + border_bottom_left_radius: &BorderCornerRadius(ref bl), + } = *self; + + + let widths = Rect::new(tl.width(), tr.width(), br.width(), bl.width()); + let heights = Rect::new(tl.height(), tr.height(), br.height(), bl.height()); + + BorderRadius::serialize_rects(widths, heights, dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="border-image" + engines="gecko servo-2013" + sub_properties="border-image-outset + border-image-repeat border-image-slice border-image-source border-image-width" + extra_prefixes="moz:layout.css.prefixes.border-image webkit" + spec="https://drafts.csswg.org/css-backgrounds-3/#border-image" +> + use crate::properties::longhands::{border_image_outset, border_image_repeat, border_image_slice}; + use crate::properties::longhands::{border_image_source, border_image_width}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for name in "outset repeat slice source width".split(): + let mut ${name} = border_image_${name}::get_initial_specified_value(); + % endfor + let mut any = false; + let mut parsed_slice = false; + let mut parsed_source = false; + let mut parsed_repeat = false; + loop { + if !parsed_slice { + if let Ok(value) = input.try_parse(|input| border_image_slice::parse(context, input)) { + parsed_slice = true; + any = true; + slice = value; + // Parse border image width and outset, if applicable. + let maybe_width_outset: Result<_, ParseError> = input.try_parse(|input| { + input.expect_delim('/')?; + + // Parse border image width, if applicable. + let w = input.try_parse(|input| border_image_width::parse(context, input)).ok(); + + // Parse border image outset if applicable. + let o = input.try_parse(|input| { + input.expect_delim('/')?; + border_image_outset::parse(context, input) + }).ok(); + if w.is_none() && o.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + Ok((w, o)) + }); + if let Ok((w, o)) = maybe_width_outset { + if let Some(w) = w { + width = w; + } + if let Some(o) = o { + outset = o; + } + } + continue; + } + } + % for name in "source repeat".split(): + if !parsed_${name} { + if let Ok(value) = input.try_parse(|input| border_image_${name}::parse(context, input)) { + ${name} = value; + parsed_${name} = true; + any = true; + continue + } + } + % endfor + break + } + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + Ok(expanded! { + % for name in "outset repeat slice source width".split(): + border_image_${name}: ${name}, + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let mut has_any = false; + % for name in "source slice outset width repeat".split(): + let has_${name} = *self.border_image_${name} != border_image_${name}::get_initial_specified_value(); + has_any = has_any || has_${name}; + % endfor + if has_source || !has_any { + self.border_image_source.to_css(dest)?; + if !has_any { + return Ok(()); + } + } + let needs_slice = has_slice || has_width || has_outset; + if needs_slice { + if has_source { + dest.write_char(' ')?; + } + self.border_image_slice.to_css(dest)?; + if has_width || has_outset { + dest.write_str(" /")?; + if has_width { + dest.write_char(' ')?; + self.border_image_width.to_css(dest)?; + } + if has_outset { + dest.write_str(" / ")?; + self.border_image_outset.to_css(dest)?; + } + } + } + if has_repeat { + if has_source || needs_slice { + dest.write_char(' ')?; + } + self.border_image_repeat.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +% for axis in ["block", "inline"]: + % for prop in ["width", "style", "color"]: + <% + spec = "https://drafts.csswg.org/css-logical/#propdef-border-%s-%s" % (axis, prop) + %> + <%helpers:shorthand + engines="gecko servo-2013 servo-2020" + name="border-${axis}-${prop}" + sub_properties="${' '.join( + 'border-%s-%s-%s' % (axis, side, prop) + for side in ['start', 'end'] + )}" + spec="${spec}"> + + use crate::properties::longhands::border_${axis}_start_${prop}; + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let start_value = border_${axis}_start_${prop}::parse(context, input)?; + let end_value = + input.try_parse(|input| border_${axis}_start_${prop}::parse(context, input)) + .unwrap_or_else(|_| start_value.clone()); + + Ok(expanded! { + border_${axis}_start_${prop}: start_value, + border_${axis}_end_${prop}: end_value, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.border_${axis}_start_${prop}.to_css(dest)?; + + if self.border_${axis}_end_${prop} != self.border_${axis}_start_${prop} { + dest.write_char(' ')?; + self.border_${axis}_end_${prop}.to_css(dest)?; + } + + Ok(()) + } + } + </%helpers:shorthand> + % endfor +% endfor + +% for axis in ["block", "inline"]: + <% + spec = "https://drafts.csswg.org/css-logical/#propdef-border-%s" % (axis) + %> + <%helpers:shorthand + name="border-${axis}" + engines="gecko servo-2013 servo-2020" + sub_properties="${' '.join( + 'border-%s-%s-width' % (axis, side) + for side in ['start', 'end'] + )} ${' '.join( + 'border-%s-%s-style' % (axis, side) + for side in ['start', 'end'] + )} ${' '.join( + 'border-%s-%s-color' % (axis, side) + for side in ['start', 'end'] + )}" + spec="${spec}"> + + use crate::properties::shorthands::border_${axis}_start; + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let start_value = border_${axis}_start::parse_value(context, input)?; + Ok(expanded! { + border_${axis}_start_width: start_value.border_${axis}_start_width.clone(), + border_${axis}_end_width: start_value.border_${axis}_start_width, + border_${axis}_start_style: start_value.border_${axis}_start_style.clone(), + border_${axis}_end_style: start_value.border_${axis}_start_style, + border_${axis}_start_color: start_value.border_${axis}_start_color.clone(), + border_${axis}_end_color: start_value.border_${axis}_start_color, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + crate::values::specified::border::serialize_directional_border( + dest, + self.border_${axis}_start_width, + self.border_${axis}_start_style, + self.border_${axis}_start_color + ) + } + } + </%helpers:shorthand> +% endfor diff --git a/servo/components/style/properties/shorthands/box.mako.rs b/servo/components/style/properties/shorthands/box.mako.rs new file mode 100644 index 0000000000..f644687dc0 --- /dev/null +++ b/servo/components/style/properties/shorthands/box.mako.rs @@ -0,0 +1,253 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +${helpers.two_properties_shorthand( + "overflow", + "overflow-x", + "overflow-y", + engines="gecko servo-2013 servo-2020", + flags="SHORTHAND_IN_GETCS", + spec="https://drafts.csswg.org/css-overflow/#propdef-overflow", +)} + +${helpers.two_properties_shorthand( + "overflow-clip-box", + "overflow-clip-box-block", + "overflow-clip-box-inline", + engines="gecko", + enabled_in="ua", + gecko_pref="layout.css.overflow-clip-box.enabled", + spec="Internal, may be standardized in the future " + "(https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-clip-box)", +)} + +${helpers.two_properties_shorthand( + "overscroll-behavior", + "overscroll-behavior-x", + "overscroll-behavior-y", + engines="gecko", + gecko_pref="layout.css.overscroll-behavior.enabled", + spec="https://wicg.github.io/overscroll-behavior/#overscroll-behavior-properties", +)} + +<%helpers:shorthand + engines="gecko" + name="container" + sub_properties="container-name container-type" + gecko_pref="layout.css.container-queries.enabled" + enabled_in="ua" + spec="https://drafts.csswg.org/css-contain-3/#container-shorthand" +> + use crate::values::specified::box_::{ContainerName, ContainerType}; + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::parser::Parse; + // See https://github.com/w3c/csswg-drafts/issues/7180 for why we don't + // match the spec. + let container_name = ContainerName::parse(context, input)?; + let container_type = if input.try_parse(|input| input.expect_delim('/')).is_ok() { + ContainerType::parse(input)? + } else { + ContainerType::Normal + }; + Ok(expanded! { + container_name: container_name, + container_type: container_type, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.container_name.to_css(dest)?; + if !self.container_type.is_normal() { + dest.write_str(" / ")?; + self.container_type.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="page-break-before" + flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND" + sub_properties="break-before" + spec="https://drafts.csswg.org/css-break-3/#page-break-properties" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::values::specified::box_::BreakBetween; + Ok(expanded! { + break_before: BreakBetween::parse_legacy(context, input)?, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.break_before.to_css_legacy(dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="page-break-after" + flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND" + sub_properties="break-after" + spec="https://drafts.csswg.org/css-break-3/#page-break-properties" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::values::specified::box_::BreakBetween; + Ok(expanded! { + break_after: BreakBetween::parse_legacy(context, input)?, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.break_after.to_css_legacy(dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="page-break-inside" + flags="SHORTHAND_IN_GETCS IS_LEGACY_SHORTHAND" + sub_properties="break-inside" + spec="https://drafts.csswg.org/css-break-3/#page-break-properties" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::values::specified::box_::BreakWithin; + Ok(expanded! { + break_inside: BreakWithin::parse_legacy(context, input)?, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.break_inside.to_css_legacy(dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="offset" + engines="gecko" + sub_properties="offset-path offset-distance offset-rotate offset-anchor + offset-position" + spec="https://drafts.fxtf.org/motion-1/#offset-shorthand"> + use crate::parser::Parse; + use crate::values::specified::motion::{OffsetPath, OffsetPosition, OffsetRotate}; + use crate::values::specified::{LengthPercentage, PositionOrAuto}; + use crate::Zero; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let offset_position = + if static_prefs::pref!("layout.css.motion-path-offset-position.enabled") { + input.try_parse(|i| OffsetPosition::parse(context, i)).ok() + } else { + None + }; + + let offset_path = input.try_parse(|i| OffsetPath::parse(context, i)).ok(); + + // Must have one of [offset-position, offset-path]. + // FIXME: The syntax is out-of-date after the update of the spec. + // https://github.com/w3c/fxtf-drafts/issues/515 + if offset_position.is_none() && offset_path.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut offset_distance = None; + let mut offset_rotate = None; + // offset-distance and offset-rotate are grouped with offset-path. + if offset_path.is_some() { + loop { + if offset_distance.is_none() { + if let Ok(value) = input.try_parse(|i| LengthPercentage::parse(context, i)) { + offset_distance = Some(value); + } + } + + if offset_rotate.is_none() { + if let Ok(value) = input.try_parse(|i| OffsetRotate::parse(context, i)) { + offset_rotate = Some(value); + continue; + } + } + break; + } + } + + let offset_anchor = input.try_parse(|i| { + i.expect_delim('/')?; + PositionOrAuto::parse(context, i) + }).ok(); + + Ok(expanded! { + offset_position: offset_position.unwrap_or(OffsetPosition::normal()), + offset_path: offset_path.unwrap_or(OffsetPath::none()), + offset_distance: offset_distance.unwrap_or(LengthPercentage::zero()), + offset_rotate: offset_rotate.unwrap_or(OffsetRotate::auto()), + offset_anchor: offset_anchor.unwrap_or(PositionOrAuto::auto()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if let Some(offset_position) = self.offset_position { + // The basic concept is: we must serialize offset-position or offset-path group. + // offset-path group means "offset-path offset-distance offset-rotate". + let must_serialize_path = *self.offset_path != OffsetPath::None + || (!self.offset_distance.is_zero() || !self.offset_rotate.is_auto()); + let position_is_default = matches!(offset_position, OffsetPosition::Normal); + if !position_is_default || !must_serialize_path { + offset_position.to_css(dest)?; + } + + if must_serialize_path { + if !position_is_default { + dest.write_char(' ')?; + } + self.offset_path.to_css(dest)?; + } + } else { + // If the pref is off, we always show offset-path. + self.offset_path.to_css(dest)?; + } + + if !self.offset_distance.is_zero() { + dest.write_char(' ')?; + self.offset_distance.to_css(dest)?; + } + + if !self.offset_rotate.is_auto() { + dest.write_char(' ')?; + self.offset_rotate.to_css(dest)?; + } + + if *self.offset_anchor != PositionOrAuto::auto() { + dest.write_str(" / ")?; + self.offset_anchor.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/column.mako.rs b/servo/components/style/properties/shorthands/column.mako.rs new file mode 100644 index 0000000000..3740775a7e --- /dev/null +++ b/servo/components/style/properties/shorthands/column.mako.rs @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="columns" + engines="gecko servo-2013" + sub_properties="column-width column-count" + servo_2013_pref="layout.columns.enabled", + spec="https://drafts.csswg.org/css-multicol/#propdef-columns"> + use crate::properties::longhands::{column_count, column_width}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut column_count = None; + let mut column_width = None; + let mut autos = 0; + + loop { + if input.try_parse(|input| input.expect_ident_matching("auto")).is_ok() { + // Leave the options to None, 'auto' is the initial value. + autos += 1; + continue + } + + if column_count.is_none() { + if let Ok(value) = input.try_parse(|input| column_count::parse(context, input)) { + column_count = Some(value); + continue + } + } + + if column_width.is_none() { + if let Ok(value) = input.try_parse(|input| column_width::parse(context, input)) { + column_width = Some(value); + continue + } + } + + break + } + + let values = autos + column_count.iter().len() + column_width.iter().len(); + if values == 0 || values > 2 { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(expanded! { + column_count: unwrap_or_initial!(column_count), + column_width: unwrap_or_initial!(column_width), + }) + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.column_width.is_auto() { + return self.column_count.to_css(dest) + } + self.column_width.to_css(dest)?; + if !self.column_count.is_auto() { + dest.write_char(' ')?; + self.column_count.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="column-rule" + engines="gecko" + sub_properties="column-rule-width column-rule-style column-rule-color" + derive_serialize="True" + spec="https://drafts.csswg.org/css-multicol/#propdef-column-rule" +> + use crate::properties::longhands::{column_rule_width, column_rule_style}; + use crate::properties::longhands::column_rule_color; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for name in "width style color".split(): + let mut column_rule_${name} = None; + % endfor + let mut any = false; + + loop { + % for name in "width style color".split(): + if column_rule_${name}.is_none() { + if let Ok(value) = input.try_parse(|input| + column_rule_${name}::parse(context, input)) { + column_rule_${name} = Some(value); + any = true; + continue + } + } + % endfor + + break + } + if any { + Ok(expanded! { + column_rule_width: unwrap_or_initial!(column_rule_width), + column_rule_style: unwrap_or_initial!(column_rule_style), + column_rule_color: unwrap_or_initial!(column_rule_color), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/font.mako.rs b/servo/components/style/properties/shorthands/font.mako.rs new file mode 100644 index 0000000000..17dcf9d926 --- /dev/null +++ b/servo/components/style/properties/shorthands/font.mako.rs @@ -0,0 +1,542 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import SYSTEM_FONT_LONGHANDS %> + +<%helpers:shorthand + name="font" + engines="gecko servo-2013 servo-2020" + sub_properties=" + font-style + font-variant-caps + font-weight + font-stretch + font-size + line-height + font-family + ${'font-size-adjust' if engine == 'gecko' else ''} + ${'font-kerning' if engine == 'gecko' else ''} + ${'font-optical-sizing' if engine == 'gecko' else ''} + ${'font-variant-alternates' if engine == 'gecko' else ''} + ${'font-variant-east-asian' if engine == 'gecko' else ''} + ${'font-variant-emoji' if engine == 'gecko' else ''} + ${'font-variant-ligatures' if engine == 'gecko' else ''} + ${'font-variant-numeric' if engine == 'gecko' else ''} + ${'font-variant-position' if engine == 'gecko' else ''} + ${'font-language-override' if engine == 'gecko' else ''} + ${'font-feature-settings' if engine == 'gecko' else ''} + ${'font-variation-settings' if engine == 'gecko' else ''} + " + derive_value_info="False" + spec="https://drafts.csswg.org/css-fonts-3/#propdef-font" +> + use crate::computed_values::font_variant_caps::T::SmallCaps; + use crate::parser::Parse; + use crate::properties::longhands::{font_family, font_style, font_size, font_weight, font_stretch}; + use crate::properties::longhands::font_variant_caps; + use crate::values::specified::font::LineHeight; + use crate::values::specified::{FontSize, FontWeight}; + use crate::values::specified::font::{FontStretch, FontStretchKeyword}; + #[cfg(feature = "gecko")] + use crate::values::specified::font::SystemFont; + + <% + gecko_sub_properties = "kerning language_override size_adjust \ + variant_alternates variant_east_asian \ + variant_emoji variant_ligatures \ + variant_numeric variant_position \ + feature_settings variation_settings \ + optical_sizing".split() + %> + % if engine == "gecko": + % for prop in gecko_sub_properties: + use crate::properties::longhands::font_${prop}; + % endfor + % endif + use self::font_family::SpecifiedValue as FontFamily; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut nb_normals = 0; + let mut style = None; + let mut variant_caps = None; + let mut weight = None; + let mut stretch = None; + let size; + % if engine == "gecko": + if let Ok(sys) = input.try_parse(|i| SystemFont::parse(context, i)) { + return Ok(expanded! { + % for name in SYSTEM_FONT_LONGHANDS: + ${name}: ${name}::SpecifiedValue::system_font(sys), + % endfor + line_height: LineHeight::normal(), + % for name in gecko_sub_properties + ["variant_caps"]: + font_${name}: font_${name}::get_initial_specified_value(), + % endfor + }) + } + % endif + loop { + // Special-case 'normal' because it is valid in each of + // font-style, font-weight, font-variant and font-stretch. + // Leaves the values to None, 'normal' is the initial value for each of them. + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { + nb_normals += 1; + continue; + } + if style.is_none() { + if let Ok(value) = input.try_parse(|input| font_style::parse(context, input)) { + style = Some(value); + continue + } + } + if weight.is_none() { + if let Ok(value) = input.try_parse(|input| font_weight::parse(context, input)) { + weight = Some(value); + continue + } + } + if variant_caps.is_none() { + // The only variant-caps value allowed is small-caps (from CSS2); the added values + // defined by CSS Fonts 3 and later are not accepted. + // https://www.w3.org/TR/css-fonts-4/#font-prop + if input.try_parse(|input| input.expect_ident_matching("small-caps")).is_ok() { + variant_caps = Some(SmallCaps); + continue + } + } + if stretch.is_none() { + if let Ok(value) = input.try_parse(FontStretchKeyword::parse) { + stretch = Some(FontStretch::Keyword(value)); + continue + } + } + size = Some(FontSize::parse(context, input)?); + break + } + + let size = match size { + Some(s) => s, + None => { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + }; + + let line_height = if input.try_parse(|input| input.expect_delim('/')).is_ok() { + Some(LineHeight::parse(context, input)?) + } else { + None + }; + + #[inline] + fn count<T>(opt: &Option<T>) -> u8 { + if opt.is_some() { 1 } else { 0 } + } + + if (count(&style) + count(&weight) + count(&variant_caps) + count(&stretch) + nb_normals) > 4 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + let family = FontFamily::parse(context, input)?; + Ok(expanded! { + % for name in "style weight stretch variant_caps".split(): + font_${name}: unwrap_or_initial!(font_${name}, ${name}), + % endfor + font_size: size, + line_height: line_height.unwrap_or(LineHeight::normal()), + font_family: family, + % if engine == "gecko": + % for name in gecko_sub_properties: + font_${name}: font_${name}::get_initial_specified_value(), + % endfor + % endif + }) + } + + % if engine == "gecko": + enum CheckSystemResult { + AllSystem(SystemFont), + SomeSystem, + None + } + % endif + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + % if engine == "gecko": + match self.check_system() { + CheckSystemResult::AllSystem(sys) => return sys.to_css(dest), + CheckSystemResult::SomeSystem => return Ok(()), + CheckSystemResult::None => {} + } + % endif + + % if engine == "gecko": + if let Some(v) = self.font_optical_sizing { + if v != &font_optical_sizing::get_initial_specified_value() { + return Ok(()); + } + } + if let Some(v) = self.font_variation_settings { + if v != &font_variation_settings::get_initial_specified_value() { + return Ok(()); + } + } + if let Some(v) = self.font_variant_emoji { + if v != &font_variant_emoji::get_initial_specified_value() { + return Ok(()); + } + } + + % for name in gecko_sub_properties: + % if name != "optical_sizing" and name != "variation_settings" and name != "variant_emoji": + if self.font_${name} != &font_${name}::get_initial_specified_value() { + return Ok(()); + } + % endif + % endfor + % endif + + // Only font-stretch keywords are allowed as part as the font + // shorthand. + let font_stretch = match *self.font_stretch { + FontStretch::Keyword(kw) => kw, + FontStretch::Stretch(percentage) => { + match FontStretchKeyword::from_percentage(percentage.0.get()) { + Some(kw) => kw, + None => return Ok(()), + } + } + FontStretch::System(..) => return Ok(()), + }; + + // The only variant-caps value allowed in the shorthand is small-caps (from CSS2); + // the added values defined by CSS Fonts 3 and later are not supported. + // https://www.w3.org/TR/css-fonts-4/#font-prop + if self.font_variant_caps != &font_variant_caps::get_initial_specified_value() && + *self.font_variant_caps != SmallCaps { + return Ok(()); + } + + % for name in "style variant_caps".split(): + if self.font_${name} != &font_${name}::get_initial_specified_value() { + self.font_${name}.to_css(dest)?; + dest.write_char(' ')?; + } + % endfor + + // The initial specified font-weight value of 'normal' computes as a number (400), + // not to the keyword, so we need to check for that as well in order to properly + // serialize the computed style. + if self.font_weight != &FontWeight::normal() && + self.font_weight != &FontWeight::from_gecko_keyword(400) { + self.font_weight.to_css(dest)?; + dest.write_char(' ')?; + } + + if font_stretch != FontStretchKeyword::Normal { + font_stretch.to_css(dest)?; + dest.write_char(' ')?; + } + + self.font_size.to_css(dest)?; + + if *self.line_height != LineHeight::normal() { + dest.write_str(" / ")?; + self.line_height.to_css(dest)?; + } + + dest.write_char(' ')?; + self.font_family.to_css(dest)?; + + Ok(()) + } + } + + impl<'a> LonghandsToSerialize<'a> { + % if engine == "gecko": + /// Check if some or all members are system fonts + fn check_system(&self) -> CheckSystemResult { + let mut sys = None; + let mut all = true; + + % for prop in SYSTEM_FONT_LONGHANDS: + % if prop == "font_optical_sizing" or prop == "font_variation_settings": + if let Some(value) = self.${prop} { + % else: + { + let value = self.${prop}; + % endif + match value.get_system() { + Some(s) => { + debug_assert!(sys.is_none() || s == sys.unwrap()); + sys = Some(s); + } + None => { + all = false; + } + } + } + % endfor + if self.line_height != &LineHeight::normal() { + all = false + } + if all { + CheckSystemResult::AllSystem(sys.unwrap()) + } else if sys.is_some() { + CheckSystemResult::SomeSystem + } else { + CheckSystemResult::None + } + } + % endif + } + + <% + subprops_for_value_info = ["font_style", "font_weight", "font_stretch", + "font_variant_caps", "font_size", "font_family"] + subprops_for_value_info = [ + "<longhands::{}::SpecifiedValue as SpecifiedValueInfo>".format(p) + for p in subprops_for_value_info + ] + %> + impl SpecifiedValueInfo for Longhands { + const SUPPORTED_TYPES: u8 = 0 + % for p in subprops_for_value_info: + | ${p}::SUPPORTED_TYPES + % endfor + ; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + % for p in subprops_for_value_info: + ${p}::collect_completion_keywords(f); + % endfor + <SystemFont as SpecifiedValueInfo>::collect_completion_keywords(f); + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="font-variant" + engines="gecko servo-2013" + flags="SHORTHAND_IN_GETCS" + sub_properties="font-variant-caps + ${'font-variant-alternates' if engine == 'gecko' else ''} + ${'font-variant-east-asian' if engine == 'gecko' else ''} + ${'font-variant-emoji' if engine == 'gecko' else ''} + ${'font-variant-ligatures' if engine == 'gecko' else ''} + ${'font-variant-numeric' if engine == 'gecko' else ''} + ${'font-variant-position' if engine == 'gecko' else ''}" + spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-variant"> +% if engine == 'gecko': + <% sub_properties = "ligatures caps alternates numeric east_asian position emoji".split() %> +% else: + <% sub_properties = ["caps"] %> +% endif + +% for prop in sub_properties: + use crate::properties::longhands::font_variant_${prop}; +% endfor + #[allow(unused_imports)] + use crate::values::specified::FontVariantLigatures; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for prop in sub_properties: + let mut ${prop} = None; + % endfor + + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() { + // Leave the values to None, 'normal' is the initial value for all the sub properties. + } else if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + // The 'none' value sets 'font-variant-ligatures' to 'none' and resets all other sub properties + // to their initial value. + % if engine == "gecko": + ligatures = Some(FontVariantLigatures::NONE); + % endif + } else { + let mut has_custom_value: bool = false; + loop { + if input.try_parse(|input| input.expect_ident_matching("normal")).is_ok() || + input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + % for prop in sub_properties: + if ${prop}.is_none() { + if let Ok(value) = input.try_parse(|i| font_variant_${prop}::parse(context, i)) { + has_custom_value = true; + ${prop} = Some(value); + continue + } + } + % endfor + + break + } + + if !has_custom_value { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + Ok(expanded! { + % for prop in sub_properties: + font_variant_${prop}: unwrap_or_initial!(font_variant_${prop}, ${prop}), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + #[allow(unused_assignments)] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + + let has_none_ligatures = + % if engine == "gecko": + self.font_variant_ligatures == &FontVariantLigatures::NONE; + % else: + false; + % endif + + const TOTAL_SUBPROPS: usize = ${len(sub_properties)}; + let mut nb_normals = 0; + % for prop in sub_properties: + % if prop == "emoji": + if let Some(value) = self.font_variant_${prop} { + % else: + { + let value = self.font_variant_${prop}; + % endif + if value == &font_variant_${prop}::get_initial_specified_value() { + nb_normals += 1; + } + } + % if prop == "emoji": + else { + // The property was disabled, so we count it as 'normal' for the purpose + // of deciding how the shorthand can be serialized. + nb_normals += 1; + } + % endif + % endfor + + + if nb_normals > 0 && nb_normals == TOTAL_SUBPROPS { + dest.write_str("normal")?; + } else if has_none_ligatures { + if nb_normals == TOTAL_SUBPROPS - 1 { + // Serialize to 'none' if 'font-variant-ligatures' is set to 'none' and all other + // font feature properties are reset to their initial value. + dest.write_str("none")?; + } else { + return Ok(()) + } + } else { + let mut has_any = false; + % for prop in sub_properties: + % if prop == "emoji": + if let Some(value) = self.font_variant_${prop} { + % else: + { + let value = self.font_variant_${prop}; + % endif + if value != &font_variant_${prop}::get_initial_specified_value() { + if has_any { + dest.write_char(' ')?; + } + has_any = true; + value.to_css(dest)?; + } + } + % endfor + } + + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="font-synthesis" + engines="gecko" + flags="SHORTHAND_IN_GETCS" + sub_properties="font-synthesis-weight font-synthesis-style font-synthesis-small-caps font-synthesis-position" + derive_value_info="False" + spec="https://drafts.csswg.org/css-fonts-3/#propdef-font-variant"> + <% sub_properties = ["weight", "style", "small_caps", "position"] %> + + use crate::values::specified::FontSynthesis; + + pub fn parse_value<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for prop in sub_properties: + let mut ${prop} = FontSynthesis::None; + % endfor + + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + // Leave all the individual values as None + } else { + let mut has_custom_value = false; + while !input.is_exhausted() { + try_match_ident_ignore_ascii_case! { input, + % for prop in sub_properties: + "${prop.replace('_', '-')}" if ${prop} == FontSynthesis::None => { + has_custom_value = true; + ${prop} = FontSynthesis::Auto; + continue; + }, + % endfor + } + } + if !has_custom_value { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + Ok(expanded! { + % for prop in sub_properties: + font_synthesis_${prop}: ${prop}, + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let mut has_any = false; + + % for prop in sub_properties: + if self.font_synthesis_${prop} == &FontSynthesis::Auto { + if has_any { + dest.write_char(' ')?; + } + has_any = true; + dest.write_str("${prop.replace('_', '-')}")?; + } + % endfor + + if !has_any { + dest.write_str("none")?; + } + + Ok(()) + } + } + + // The shorthand takes the sub-property names of the longhands, and not the + // 'auto' keyword like they do, so we can't automatically derive this. + impl SpecifiedValueInfo for Longhands { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&[ + "none", + % for prop in sub_properties: + "${prop.replace('_', '-')}", + % endfor + ]); + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/inherited_svg.mako.rs b/servo/components/style/properties/shorthands/inherited_svg.mako.rs new file mode 100644 index 0000000000..f29e78a69f --- /dev/null +++ b/servo/components/style/properties/shorthands/inherited_svg.mako.rs @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand + name="marker" + engines="gecko" + sub_properties="marker-start marker-end marker-mid" + spec="https://svgwg.org/svg2-draft/painting.html#MarkerShorthand" +> + use crate::values::specified::url::UrlOrNone; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::parser::Parse; + let url = UrlOrNone::parse(context, input)?; + + Ok(expanded! { + marker_start: url.clone(), + marker_mid: url.clone(), + marker_end: url, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.marker_start == self.marker_mid && self.marker_mid == self.marker_end { + self.marker_start.to_css(dest) + } else { + Ok(()) + } + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/inherited_text.mako.rs b/servo/components/style/properties/shorthands/inherited_text.mako.rs new file mode 100644 index 0000000000..d470553e42 --- /dev/null +++ b/servo/components/style/properties/shorthands/inherited_text.mako.rs @@ -0,0 +1,254 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand + name="text-emphasis" + engines="gecko" + sub_properties="text-emphasis-style text-emphasis-color" + derive_serialize="True" + spec="https://drafts.csswg.org/css-text-decor-3/#text-emphasis-property" +> + use crate::properties::longhands::{text_emphasis_color, text_emphasis_style}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut color = None; + let mut style = None; + + loop { + if color.is_none() { + if let Ok(value) = input.try_parse(|input| text_emphasis_color::parse(context, input)) { + color = Some(value); + continue + } + } + if style.is_none() { + if let Ok(value) = input.try_parse(|input| text_emphasis_style::parse(context, input)) { + style = Some(value); + continue + } + } + break + } + if color.is_some() || style.is_some() { + Ok(expanded! { + text_emphasis_color: unwrap_or_initial!(text_emphasis_color, color), + text_emphasis_style: unwrap_or_initial!(text_emphasis_style, style), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="text-wrap" + engines="gecko" + sub_properties="text-wrap-mode text-wrap-style" + spec="https://www.w3.org/TR/css-text-4/#text-wrap" +> + use crate::properties::longhands::{text_wrap_mode, text_wrap_style}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut mode = None; + let mut style = None; + + loop { + if mode.is_none() { + if let Ok(value) = input.try_parse(|input| text_wrap_mode::parse(context, input)) { + mode = Some(value); + continue + } + } + if style.is_none() { + if let Ok(value) = input.try_parse(|input| text_wrap_style::parse(context, input)) { + style = Some(value); + continue + } + } + break + } + if mode.is_some() || style.is_some() { + Ok(expanded! { + text_wrap_mode: unwrap_or_initial!(text_wrap_mode, mode), + text_wrap_style: unwrap_or_initial!(text_wrap_style, style), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use text_wrap_mode::computed_value::T as Mode; + use text_wrap_style::computed_value::T as Style; + + if matches!(self.text_wrap_style, None | Some(&Style::Auto)) { + return self.text_wrap_mode.to_css(dest); + } + + if *self.text_wrap_mode != Mode::Wrap { + self.text_wrap_mode.to_css(dest)?; + dest.write_char(' ')?; + } + + self.text_wrap_style.to_css(dest) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="white-space" + engines="gecko" + sub_properties="text-wrap-mode white-space-collapse" + spec="https://www.w3.org/TR/css-text-4/#white-space-property" +> + use crate::properties::longhands::{text_wrap_mode, white_space_collapse}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + use white_space_collapse::computed_value::T as Collapse; + use text_wrap_mode::computed_value::T as Wrap; + + fn parse_special_shorthands<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Longhands, ParseError<'i>> { + let (mode, collapse) = try_match_ident_ignore_ascii_case! { input, + "normal" => (Wrap::Wrap, Collapse::Collapse), + "pre" => (Wrap::Nowrap, Collapse::Preserve), + "pre-wrap" => (Wrap::Wrap, Collapse::Preserve), + "pre-line" => (Wrap::Wrap, Collapse::PreserveBreaks), + // TODO: deprecate/remove -moz-pre-space; the white-space-collapse: preserve-spaces value + // should serve this purpose? + "-moz-pre-space" => (Wrap::Wrap, Collapse::PreserveSpaces), + }; + Ok(expanded! { + text_wrap_mode: mode, + white_space_collapse: collapse, + }) + } + + if let Ok(result) = input.try_parse(parse_special_shorthands) { + return Ok(result); + } + + let mut wrap = None; + let mut collapse = None; + + loop { + if wrap.is_none() { + if let Ok(value) = input.try_parse(|input| text_wrap_mode::parse(context, input)) { + wrap = Some(value); + continue + } + } + if collapse.is_none() { + if let Ok(value) = input.try_parse(|input| white_space_collapse::parse(context, input)) { + collapse = Some(value); + continue + } + } + break + } + + if wrap.is_some() || collapse.is_some() { + Ok(expanded! { + text_wrap_mode: unwrap_or_initial!(text_wrap_mode, wrap), + white_space_collapse: unwrap_or_initial!(white_space_collapse, collapse), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use white_space_collapse::computed_value::T as Collapse; + use text_wrap_mode::computed_value::T as Wrap; + + match *self.text_wrap_mode { + Wrap::Wrap => { + match *self.white_space_collapse { + Collapse::Collapse => return dest.write_str("normal"), + Collapse::Preserve => return dest.write_str("pre-wrap"), + Collapse::PreserveBreaks => return dest.write_str("pre-line"), + Collapse::PreserveSpaces => return dest.write_str("-moz-pre-space"), + _ => (), + } + }, + Wrap::Nowrap => { + if let Collapse::Preserve = *self.white_space_collapse { + return dest.write_str("pre"); + } + }, + } + + let mut has_value = false; + if *self.white_space_collapse != Collapse::Collapse { + self.white_space_collapse.to_css(dest)?; + has_value = true; + } + + if *self.text_wrap_mode != Wrap::Wrap { + if has_value { + dest.write_char(' ')?; + } + self.text_wrap_mode.to_css(dest)?; + } + + Ok(()) + } + } +</%helpers:shorthand> + +// CSS Compatibility +// https://compat.spec.whatwg.org/ +<%helpers:shorthand name="-webkit-text-stroke" + engines="gecko" + sub_properties="-webkit-text-stroke-width + -webkit-text-stroke-color" + derive_serialize="True" + spec="https://compat.spec.whatwg.org/#the-webkit-text-stroke"> + use crate::properties::longhands::{_webkit_text_stroke_color, _webkit_text_stroke_width}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut color = None; + let mut width = None; + loop { + if color.is_none() { + if let Ok(value) = input.try_parse(|input| _webkit_text_stroke_color::parse(context, input)) { + color = Some(value); + continue + } + } + + if width.is_none() { + if let Ok(value) = input.try_parse(|input| _webkit_text_stroke_width::parse(context, input)) { + width = Some(value); + continue + } + } + break + } + + if color.is_some() || width.is_some() { + Ok(expanded! { + _webkit_text_stroke_color: unwrap_or_initial!(_webkit_text_stroke_color, color), + _webkit_text_stroke_width: unwrap_or_initial!(_webkit_text_stroke_width, width), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/list.mako.rs b/servo/components/style/properties/shorthands/list.mako.rs new file mode 100644 index 0000000000..2e234e3d8f --- /dev/null +++ b/servo/components/style/properties/shorthands/list.mako.rs @@ -0,0 +1,137 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="list-style" + engines="gecko servo-2013 servo-2020" + sub_properties="list-style-position list-style-image list-style-type" + spec="https://drafts.csswg.org/css-lists/#propdef-list-style"> + use crate::properties::longhands::{list_style_image, list_style_position, list_style_type}; + use crate::values::specified::Image; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + // `none` is ambiguous until we've finished parsing the shorthands, so we count the number + // of times we see it. + let mut nones = 0u8; + let (mut image, mut position, mut list_style_type, mut any) = (None, None, None, false); + loop { + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + nones = nones + 1; + if nones > 2 { + return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("none".into()))) + } + any = true; + continue + } + + if image.is_none() { + if let Ok(value) = input.try_parse(|input| list_style_image::parse(context, input)) { + image = Some(value); + any = true; + continue + } + } + + if position.is_none() { + if let Ok(value) = input.try_parse(|input| list_style_position::parse(context, input)) { + position = Some(value); + any = true; + continue + } + } + + // list-style-type must be checked the last, because it accepts + // arbitrary identifier for custom counter style, and thus may + // affect values of list-style-position. + if list_style_type.is_none() { + if let Ok(value) = input.try_parse(|input| list_style_type::parse(context, input)) { + list_style_type = Some(value); + any = true; + continue + } + } + break + } + + let position = unwrap_or_initial!(list_style_position, position); + + // If there are two `none`s, then we can't have a type or image; if there is one `none`, + // then we can't have both a type *and* an image; if there is no `none` then we're fine as + // long as we parsed something. + use self::list_style_type::SpecifiedValue as ListStyleType; + match (any, nones, list_style_type, image) { + (true, 2, None, None) => { + Ok(expanded! { + list_style_position: position, + list_style_image: Image::None, + list_style_type: ListStyleType::None, + }) + } + (true, 1, None, Some(image)) => { + Ok(expanded! { + list_style_position: position, + list_style_image: image, + list_style_type: ListStyleType::None, + }) + } + (true, 1, Some(list_style_type), None) => { + Ok(expanded! { + list_style_position: position, + list_style_image: Image::None, + list_style_type: list_style_type, + }) + } + (true, 1, None, None) => { + Ok(expanded! { + list_style_position: position, + list_style_image: Image::None, + list_style_type: ListStyleType::None, + }) + } + (true, 0, list_style_type, image) => { + Ok(expanded! { + list_style_position: position, + list_style_image: unwrap_or_initial!(list_style_image, image), + list_style_type: unwrap_or_initial!(list_style_type), + }) + } + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use longhands::list_style_position::SpecifiedValue as ListStylePosition; + use longhands::list_style_type::SpecifiedValue as ListStyleType; + use longhands::list_style_image::SpecifiedValue as ListStyleImage; + let mut have_one_non_initial_value = false; + if self.list_style_position != &ListStylePosition::Outside { + self.list_style_position.to_css(dest)?; + have_one_non_initial_value = true; + } + if self.list_style_image != &ListStyleImage::None { + if have_one_non_initial_value { + dest.write_char(' ')?; + } + self.list_style_image.to_css(dest)?; + have_one_non_initial_value = true; + } + if self.list_style_type != &ListStyleType::disc() { + if have_one_non_initial_value { + dest.write_char(' ')?; + } + self.list_style_type.to_css(dest)?; + have_one_non_initial_value = true; + } + if !have_one_non_initial_value { + self.list_style_position.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/margin.mako.rs b/servo/components/style/properties/shorthands/margin.mako.rs new file mode 100644 index 0000000000..6b5bf7e467 --- /dev/null +++ b/servo/components/style/properties/shorthands/margin.mako.rs @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> +<% from data import DEFAULT_RULES_AND_PAGE %> + +${helpers.four_sides_shorthand( + "margin", + "margin-%s", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-box/#propdef-margin", + rule_types_allowed=DEFAULT_RULES_AND_PAGE, + allow_quirks="Yes", +)} + +${helpers.two_properties_shorthand( + "margin-block", + "margin-block-start", + "margin-block-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-margin-block" +)} + +${helpers.two_properties_shorthand( + "margin-inline", + "margin-inline-start", + "margin-inline-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-margin-inline" +)} + +${helpers.four_sides_shorthand( + "scroll-margin", + "scroll-margin-%s", + "specified::Length::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin", +)} + +${helpers.two_properties_shorthand( + "scroll-margin-block", + "scroll-margin-block-start", + "scroll-margin-block-end", + "specified::Length::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-block", +)} + +${helpers.two_properties_shorthand( + "scroll-margin-inline", + "scroll-margin-inline-start", + "scroll-margin-inline-end", + "specified::Length::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-margin-inline", +)} diff --git a/servo/components/style/properties/shorthands/outline.mako.rs b/servo/components/style/properties/shorthands/outline.mako.rs new file mode 100644 index 0000000000..6ee8ed22c9 --- /dev/null +++ b/servo/components/style/properties/shorthands/outline.mako.rs @@ -0,0 +1,80 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="outline" + engines="gecko servo-2013" + sub_properties="outline-color outline-style outline-width" + spec="https://drafts.csswg.org/css-ui/#propdef-outline"> + use crate::properties::longhands::{outline_color, outline_width, outline_style}; + use crate::values::specified; + use crate::parser::Parse; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let _unused = context; + let mut color = None; + let mut style = None; + let mut width = None; + let mut any = false; + loop { + if color.is_none() { + if let Ok(value) = input.try_parse(|i| specified::Color::parse(context, i)) { + color = Some(value); + any = true; + continue + } + } + if style.is_none() { + if let Ok(value) = input.try_parse(|input| outline_style::parse(context, input)) { + style = Some(value); + any = true; + continue + } + } + if width.is_none() { + if let Ok(value) = input.try_parse(|input| outline_width::parse(context, input)) { + width = Some(value); + any = true; + continue + } + } + break + } + if any { + Ok(expanded! { + outline_color: unwrap_or_initial!(outline_color, color), + outline_style: unwrap_or_initial!(outline_style, style), + outline_width: unwrap_or_initial!(outline_width, width), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let mut wrote_value = false; + + % for name in "color style width".split(): + if *self.outline_${name} != outline_${name}::get_initial_specified_value() { + if wrote_value { + dest.write_char(' ')?; + } + self.outline_${name}.to_css(dest)?; + wrote_value = true; + } + % endfor + + if !wrote_value { + self.outline_style.to_css(dest)?; + } + + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/padding.mako.rs b/servo/components/style/properties/shorthands/padding.mako.rs new file mode 100644 index 0000000000..11ddfed3b1 --- /dev/null +++ b/servo/components/style/properties/shorthands/padding.mako.rs @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +${helpers.four_sides_shorthand( + "padding", + "padding-%s", + "specified::NonNegativeLengthPercentage::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-box-3/#propdef-padding", + allow_quirks="Yes", +)} + +${helpers.two_properties_shorthand( + "padding-block", + "padding-block-start", + "padding-block-end", + "specified::NonNegativeLengthPercentage::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-padding-block" +)} + +${helpers.two_properties_shorthand( + "padding-inline", + "padding-inline-start", + "padding-inline-end", + "specified::NonNegativeLengthPercentage::parse", + engines="gecko servo-2013 servo-2020", + spec="https://drafts.csswg.org/css-logical/#propdef-padding-inline" +)} + +${helpers.four_sides_shorthand( + "scroll-padding", + "scroll-padding-%s", + "specified::NonNegativeLengthPercentageOrAuto::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding" +)} + +${helpers.two_properties_shorthand( + "scroll-padding-block", + "scroll-padding-block-start", + "scroll-padding-block-end", + "specified::NonNegativeLengthPercentageOrAuto::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-block" +)} + +${helpers.two_properties_shorthand( + "scroll-padding-inline", + "scroll-padding-inline-start", + "scroll-padding-inline-end", + "specified::NonNegativeLengthPercentageOrAuto::parse", + engines="gecko", + spec="https://drafts.csswg.org/css-scroll-snap-1/#propdef-scroll-padding-inline" +)} diff --git a/servo/components/style/properties/shorthands/position.mako.rs b/servo/components/style/properties/shorthands/position.mako.rs new file mode 100644 index 0000000000..ed7df5e27a --- /dev/null +++ b/servo/components/style/properties/shorthands/position.mako.rs @@ -0,0 +1,891 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="flex-flow" + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + sub_properties="flex-direction flex-wrap" + extra_prefixes="webkit" + spec="https://drafts.csswg.org/css-flexbox/#flex-flow-property"> + use crate::properties::longhands::{flex_direction, flex_wrap}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut direction = None; + let mut wrap = None; + loop { + if direction.is_none() { + if let Ok(value) = input.try_parse(|input| flex_direction::parse(context, input)) { + direction = Some(value); + continue + } + } + if wrap.is_none() { + if let Ok(value) = input.try_parse(|input| flex_wrap::parse(context, input)) { + wrap = Some(value); + continue + } + } + break + } + + if direction.is_none() && wrap.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + Ok(expanded! { + flex_direction: unwrap_or_initial!(flex_direction, direction), + flex_wrap: unwrap_or_initial!(flex_wrap, wrap), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if *self.flex_direction == flex_direction::get_initial_specified_value() && + *self.flex_wrap != flex_wrap::get_initial_specified_value() { + return self.flex_wrap.to_css(dest) + } + self.flex_direction.to_css(dest)?; + if *self.flex_wrap != flex_wrap::get_initial_specified_value() { + dest.write_char(' ')?; + self.flex_wrap.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="flex" + engines="gecko servo-2013 servo-2020", + servo_2020_pref="layout.flexbox.enabled", + sub_properties="flex-grow flex-shrink flex-basis" + extra_prefixes="webkit" + derive_serialize="True" + spec="https://drafts.csswg.org/css-flexbox/#flex-property"> + use crate::parser::Parse; + use crate::values::specified::NonNegativeNumber; + use crate::properties::longhands::flex_basis::SpecifiedValue as FlexBasis; + + fn parse_flexibility<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(NonNegativeNumber, Option<NonNegativeNumber>),ParseError<'i>> { + let grow = NonNegativeNumber::parse(context, input)?; + let shrink = input.try_parse(|i| NonNegativeNumber::parse(context, i)).ok(); + Ok((grow, shrink)) + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut grow = None; + let mut shrink = None; + let mut basis = None; + + if input.try_parse(|input| input.expect_ident_matching("none")).is_ok() { + return Ok(expanded! { + flex_grow: NonNegativeNumber::new(0.0), + flex_shrink: NonNegativeNumber::new(0.0), + flex_basis: FlexBasis::auto(), + }) + } + loop { + if grow.is_none() { + if let Ok((flex_grow, flex_shrink)) = input.try_parse(|i| parse_flexibility(context, i)) { + grow = Some(flex_grow); + shrink = flex_shrink; + continue + } + } + if basis.is_none() { + if let Ok(value) = input.try_parse(|input| FlexBasis::parse(context, input)) { + basis = Some(value); + continue + } + } + break + } + + if grow.is_none() && basis.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + Ok(expanded! { + flex_grow: grow.unwrap_or(NonNegativeNumber::new(1.0)), + flex_shrink: shrink.unwrap_or(NonNegativeNumber::new(1.0)), + // Per spec, this should be SpecifiedValue::zero(), but all + // browsers currently agree on using `0%`. This is a spec + // change which hasn't been adopted by browsers: + // https://github.com/w3c/csswg-drafts/commit/2c446befdf0f686217905bdd7c92409f6bd3921b + flex_basis: basis.unwrap_or(FlexBasis::zero_percent()), + }) + } +</%helpers:shorthand> + +<%helpers:shorthand + name="gap" + engines="gecko" + aliases="grid-gap" + sub_properties="row-gap column-gap" + spec="https://drafts.csswg.org/css-align-3/#gap-shorthand" +> + use crate::properties::longhands::{row_gap, column_gap}; + + pub fn parse_value<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) + -> Result<Longhands, ParseError<'i>> { + let r_gap = row_gap::parse(context, input)?; + let c_gap = input.try_parse(|input| column_gap::parse(context, input)).unwrap_or(r_gap.clone()); + + Ok(expanded! { + row_gap: r_gap, + column_gap: c_gap, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.row_gap == self.column_gap { + self.row_gap.to_css(dest) + } else { + self.row_gap.to_css(dest)?; + dest.write_char(' ')?; + self.column_gap.to_css(dest) + } + } + } + +</%helpers:shorthand> + +% for kind in ["row", "column"]: +<%helpers:shorthand + name="grid-${kind}" + sub_properties="grid-${kind}-start grid-${kind}-end" + engines="gecko", + spec="https://drafts.csswg.org/css-grid/#propdef-grid-${kind}" +> + use crate::values::specified::GridLine; + use crate::parser::Parse; + use crate::Zero; + + // NOTE: Since both the shorthands have the same code, we should (re-)use code from one to implement + // the other. This might not be a big deal for now, but we should consider looking into this in the future + // to limit the amount of code generated. + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let start = input.try_parse(|i| GridLine::parse(context, i))?; + let end = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + GridLine::parse(context, input)? + } else { + let mut line = GridLine::auto(); + if start.line_num.is_zero() && !start.is_span { + line.ident = start.ident.clone(); // ident from start value should be taken + } + + line + }; + + Ok(expanded! { + grid_${kind}_start: start, + grid_${kind}_end: end, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + // Return the shortest possible serialization of the `grid-${kind}-[start/end]` values. + // This function exploits the opportunities to omit the end value per this spec text: + // + // https://drafts.csswg.org/css-grid/#propdef-grid-column + // "When the second value is omitted, if the first value is a <custom-ident>, + // the grid-row-end/grid-column-end longhand is also set to that <custom-ident>; + // otherwise, it is set to auto." + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.grid_${kind}_start.to_css(dest)?; + if self.grid_${kind}_start.can_omit(self.grid_${kind}_end) { + return Ok(()); // the end value is redundant + } + dest.write_str(" / ")?; + self.grid_${kind}_end.to_css(dest) + } + } +</%helpers:shorthand> +% endfor + +<%helpers:shorthand + name="grid-area" + engines="gecko" + sub_properties="grid-row-start grid-row-end grid-column-start grid-column-end" + spec="https://drafts.csswg.org/css-grid/#propdef-grid-area" +> + use crate::values::specified::GridLine; + use crate::parser::Parse; + use crate::Zero; + + // The code is the same as `grid-{row,column}` except that this can have four values at most. + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + fn line_with_ident_from(other: &GridLine) -> GridLine { + let mut this = GridLine::auto(); + if other.line_num.is_zero() && !other.is_span { + this.ident = other.ident.clone(); + } + + this + } + + let row_start = input.try_parse(|i| GridLine::parse(context, i))?; + let (column_start, row_end, column_end) = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + let column_start = GridLine::parse(context, input)?; + let (row_end, column_end) = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + let row_end = GridLine::parse(context, input)?; + let column_end = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + GridLine::parse(context, input)? + } else { // grid-column-end has not been given + line_with_ident_from(&column_start) + }; + + (row_end, column_end) + } else { // grid-row-start and grid-column-start has been given + let row_end = line_with_ident_from(&row_start); + let column_end = line_with_ident_from(&column_start); + (row_end, column_end) + }; + + (column_start, row_end, column_end) + } else { // only grid-row-start is given + let line = line_with_ident_from(&row_start); + (line.clone(), line.clone(), line) + }; + + Ok(expanded! { + grid_row_start: row_start, + grid_row_end: row_end, + grid_column_start: column_start, + grid_column_end: column_end, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + // Return the shortest possible serialization of the `grid-[column/row]-[start/end]` values. + // This function exploits the opportunities to omit trailing values per this spec text: + // + // https://drafts.csswg.org/css-grid/#propdef-grid-area + // "If four <grid-line> values are specified, grid-row-start is set to the first value, + // grid-column-start is set to the second value, grid-row-end is set to the third value, + // and grid-column-end is set to the fourth value. + // + // When grid-column-end is omitted, if grid-column-start is a <custom-ident>, + // grid-column-end is set to that <custom-ident>; otherwise, it is set to auto. + // + // When grid-row-end is omitted, if grid-row-start is a <custom-ident>, grid-row-end is + // set to that <custom-ident>; otherwise, it is set to auto. + // + // When grid-column-start is omitted, if grid-row-start is a <custom-ident>, all four + // longhands are set to that value. Otherwise, it is set to auto." + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.grid_row_start.to_css(dest)?; + let mut trailing_values = 3; + if self.grid_column_start.can_omit(self.grid_column_end) { + trailing_values -= 1; + if self.grid_row_start.can_omit(self.grid_row_end) { + trailing_values -= 1; + if self.grid_row_start.can_omit(self.grid_column_start) { + trailing_values -= 1; + } + } + } + let values = [&self.grid_column_start, &self.grid_row_end, &self.grid_column_end]; + for value in values.iter().take(trailing_values) { + dest.write_str(" / ")?; + value.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="grid-template" + engines="gecko" + sub_properties="grid-template-rows grid-template-columns grid-template-areas" + spec="https://drafts.csswg.org/css-grid/#propdef-grid-template" +> + use crate::parser::Parse; + use servo_arc::Arc; + use crate::values::generics::grid::{TrackSize, TrackList}; + use crate::values::generics::grid::{TrackListValue, concat_serialize_idents}; + use crate::values::specified::{GridTemplateComponent, GenericGridTemplateComponent}; + use crate::values::specified::grid::parse_line_names; + use crate::values::specified::position::{GridTemplateAreas, TemplateAreasParser, TemplateAreasArc}; + + /// Parsing for `<grid-template>` shorthand (also used by `grid` shorthand). + pub fn parse_grid_template<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(GridTemplateComponent, GridTemplateComponent, GridTemplateAreas), ParseError<'i>> { + // Other shorthand sub properties also parse the `none` keyword and this shorthand + // should know after this keyword there is nothing to parse. Otherwise it gets + // confused and rejects the sub properties that contains `none`. + <% keywords = { + "none": "GenericGridTemplateComponent::None", + } + %> + % for keyword, rust_type in keywords.items(): + if let Ok(x) = input.try_parse(|i| { + if i.try_parse(|i| i.expect_ident_matching("${keyword}")).is_ok() { + if !i.is_exhausted() { + return Err(()); + } + return Ok((${rust_type}, ${rust_type}, GridTemplateAreas::None)); + } + Err(()) + }) { + return Ok(x); + } + % endfor + + let first_line_names = input.try_parse(parse_line_names).unwrap_or_default(); + let mut areas_parser = TemplateAreasParser::default(); + if areas_parser.try_parse_string(input).is_ok() { + let mut values = vec![]; + let mut line_names = vec![]; + line_names.push(first_line_names); + loop { + let size = input.try_parse(|i| TrackSize::parse(context, i)).unwrap_or_default(); + values.push(TrackListValue::TrackSize(size)); + let mut names = input.try_parse(parse_line_names).unwrap_or_default(); + let more_names = input.try_parse(parse_line_names); + + match areas_parser.try_parse_string(input) { + Ok(()) => { + if let Ok(v) = more_names { + // We got `[names] [more_names] "string"` - merge the two name lists. + let mut names_vec = names.into_vec(); + names_vec.extend(v.into_iter()); + names = names_vec.into(); + } + line_names.push(names); + }, + Err(e) => { + if more_names.is_ok() { + // We've parsed `"string" [names] [more_names]` but then failed to parse another `"string"`. + // The grammar doesn't allow two trailing `<line-names>` so this is an invalid value. + return Err(e); + } + // only the named area determines whether we should bail out + line_names.push(names); + break + }, + }; + } + + if line_names.len() == values.len() { + // should be one longer than track sizes + line_names.push(Default::default()); + } + + let template_areas = areas_parser.finish() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?; + let template_rows = TrackList { + values: values.into(), + line_names: line_names.into(), + auto_repeat_index: std::usize::MAX, + }; + + let template_cols = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + let value = GridTemplateComponent::parse_without_none(context, input)?; + if let GenericGridTemplateComponent::TrackList(ref list) = value { + if !list.is_explicit() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + value + } else { + GridTemplateComponent::default() + }; + + Ok(( + GenericGridTemplateComponent::TrackList(Box::new(template_rows)), + template_cols, + GridTemplateAreas::Areas(TemplateAreasArc(Arc::new(template_areas))) + )) + } else { + let mut template_rows = GridTemplateComponent::parse(context, input)?; + if let GenericGridTemplateComponent::TrackList(ref mut list) = template_rows { + // Fist line names are parsed already and it shouldn't be parsed again. + // If line names are not empty, that means given property value is not acceptable + if list.line_names[0].is_empty() { + list.line_names[0] = first_line_names; // won't panic + } else { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + input.expect_delim('/')?; + Ok((template_rows, GridTemplateComponent::parse(context, input)?, GridTemplateAreas::None)) + } + } + + #[inline] + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let (rows, columns, areas) = parse_grid_template(context, input)?; + Ok(expanded! { + grid_template_rows: rows, + grid_template_columns: columns, + grid_template_areas: areas, + }) + } + + /// Serialization for `<grid-template>` shorthand (also used by `grid` shorthand). + pub fn serialize_grid_template<W>( + template_rows: &GridTemplateComponent, + template_columns: &GridTemplateComponent, + template_areas: &GridTemplateAreas, + dest: &mut CssWriter<W>, + ) -> fmt::Result + where + W: Write { + match *template_areas { + GridTemplateAreas::None => { + if template_rows.is_initial() && template_columns.is_initial() { + return GridTemplateComponent::default().to_css(dest); + } + template_rows.to_css(dest)?; + dest.write_str(" / ")?; + template_columns.to_css(dest) + }, + GridTemplateAreas::Areas(ref areas) => { + // The length of template-area and template-rows values should be equal. + if areas.0.strings.len() != template_rows.track_list_len() { + return Ok(()); + } + + let track_list = match *template_rows { + GenericGridTemplateComponent::TrackList(ref list) => { + // We should fail if there is a `repeat` function. + // `grid` and `grid-template` shorthands doesn't accept + // that. Only longhand accepts. + if !list.is_explicit() { + return Ok(()); + } + list + }, + // Others template components shouldn't exist with normal shorthand values. + // But if we need to serialize a group of longhand sub-properties for + // the shorthand, we should be able to return empty string instead of crashing. + _ => return Ok(()), + }; + + // We need to check some values that longhand accepts but shorthands don't. + match *template_columns { + // We should fail if there is a `repeat` function. `grid` and + // `grid-template` shorthands doesn't accept that. Only longhand accepts that. + GenericGridTemplateComponent::TrackList(ref list) => { + if !list.is_explicit() { + return Ok(()); + } + }, + // Also the shorthands don't accept subgrids unlike longhand. + // We should fail without an error here. + GenericGridTemplateComponent::Subgrid(_) => { + return Ok(()); + }, + _ => {}, + } + + let mut names_iter = track_list.line_names.iter(); + for (((i, string), names), value) in areas.0.strings.iter().enumerate() + .zip(&mut names_iter) + .zip(track_list.values.iter()) { + if i > 0 { + dest.write_char(' ')?; + } + + if !names.is_empty() { + concat_serialize_idents("[", "] ", names, " ", dest)?; + } + + string.to_css(dest)?; + + // If the track size is the initial value then it's redundant here. + if !value.is_initial() { + dest.write_char(' ')?; + value.to_css(dest)?; + } + } + + if let Some(names) = names_iter.next() { + concat_serialize_idents(" [", "]", names, " ", dest)?; + } + + if let GenericGridTemplateComponent::TrackList(ref list) = *template_columns { + dest.write_str(" / ")?; + list.to_css(dest)?; + } + + Ok(()) + }, + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + serialize_grid_template( + self.grid_template_rows, + self.grid_template_columns, + self.grid_template_areas, + dest + ) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="grid" + engines="gecko" + sub_properties="grid-template-rows grid-template-columns grid-template-areas + grid-auto-rows grid-auto-columns grid-auto-flow" + spec="https://drafts.csswg.org/css-grid/#propdef-grid" +> + use crate::parser::Parse; + use crate::properties::longhands::{grid_auto_columns, grid_auto_rows, grid_auto_flow}; + use crate::values::generics::grid::GridTemplateComponent; + use crate::values::specified::{GenericGridTemplateComponent, ImplicitGridTracks}; + use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas}; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let mut temp_rows = GridTemplateComponent::default(); + let mut temp_cols = GridTemplateComponent::default(); + let mut temp_areas = GridTemplateAreas::None; + let mut auto_rows = ImplicitGridTracks::default(); + let mut auto_cols = ImplicitGridTracks::default(); + let mut flow = grid_auto_flow::get_initial_value(); + + fn parse_auto_flow<'i, 't>( + input: &mut Parser<'i, 't>, + is_row: bool, + ) -> Result<GridAutoFlow, ParseError<'i>> { + let mut track = None; + let mut dense = GridAutoFlow::empty(); + + for _ in 0..2 { + if input.try_parse(|i| i.expect_ident_matching("auto-flow")).is_ok() { + track = if is_row { + Some(GridAutoFlow::ROW) + } else { + Some(GridAutoFlow::COLUMN) + }; + } else if input.try_parse(|i| i.expect_ident_matching("dense")).is_ok() { + dense = GridAutoFlow::DENSE + } else { + break + } + } + + if track.is_some() { + Ok(track.unwrap() | dense) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + if let Ok((rows, cols, areas)) = input.try_parse(|i| super::grid_template::parse_grid_template(context, i)) { + temp_rows = rows; + temp_cols = cols; + temp_areas = areas; + } else if let Ok(rows) = input.try_parse(|i| GridTemplateComponent::parse(context, i)) { + temp_rows = rows; + input.expect_delim('/')?; + flow = parse_auto_flow(input, false)?; + auto_cols = input.try_parse(|i| grid_auto_columns::parse(context, i)).unwrap_or_default(); + } else { + flow = parse_auto_flow(input, true)?; + auto_rows = input.try_parse(|i| grid_auto_rows::parse(context, i)).unwrap_or_default(); + input.expect_delim('/')?; + temp_cols = GridTemplateComponent::parse(context, input)?; + } + + Ok(expanded! { + grid_template_rows: temp_rows, + grid_template_columns: temp_cols, + grid_template_areas: temp_areas, + grid_auto_rows: auto_rows, + grid_auto_columns: auto_cols, + grid_auto_flow: flow, + }) + } + + impl<'a> LonghandsToSerialize<'a> { + /// Returns true if other sub properties except template-{rows,columns} are initial. + fn is_grid_template(&self) -> bool { + self.grid_auto_rows.is_initial() && + self.grid_auto_columns.is_initial() && + *self.grid_auto_flow == grid_auto_flow::get_initial_value() + } + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + if self.is_grid_template() { + return super::grid_template::serialize_grid_template( + self.grid_template_rows, + self.grid_template_columns, + self.grid_template_areas, + dest + ); + } + + if *self.grid_template_areas != GridTemplateAreas::None { + // No other syntax can set the template areas, so fail to + // serialize. + return Ok(()); + } + + if self.grid_auto_flow.contains(GridAutoFlow::COLUMN) { + // It should fail to serialize if other branch of the if condition's values are set. + if !self.grid_auto_rows.is_initial() || + !self.grid_template_columns.is_initial() { + return Ok(()); + } + + // It should fail to serialize if template-rows value is not Explicit. + if let GenericGridTemplateComponent::TrackList(ref list) = *self.grid_template_rows { + if !list.is_explicit() { + return Ok(()); + } + } + + self.grid_template_rows.to_css(dest)?; + dest.write_str(" / auto-flow")?; + if self.grid_auto_flow.contains(GridAutoFlow::DENSE) { + dest.write_str(" dense")?; + } + + if !self.grid_auto_columns.is_initial() { + dest.write_char(' ')?; + self.grid_auto_columns.to_css(dest)?; + } + + return Ok(()); + } + + // It should fail to serialize if other branch of the if condition's values are set. + if !self.grid_auto_columns.is_initial() || + !self.grid_template_rows.is_initial() { + return Ok(()); + } + + // It should fail to serialize if template-column value is not Explicit. + if let GenericGridTemplateComponent::TrackList(ref list) = *self.grid_template_columns { + if !list.is_explicit() { + return Ok(()); + } + } + + dest.write_str("auto-flow")?; + if self.grid_auto_flow.contains(GridAutoFlow::DENSE) { + dest.write_str(" dense")?; + } + + if !self.grid_auto_rows.is_initial() { + dest.write_char(' ')?; + self.grid_auto_rows.to_css(dest)?; + } + + dest.write_str(" / ")?; + self.grid_template_columns.to_css(dest)?; + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="place-content" + engines="gecko" + sub_properties="align-content justify-content" + spec="https://drafts.csswg.org/css-align/#propdef-place-content" +> + use crate::values::specified::align::{AlignContent, JustifyContent, ContentDistribution, AxisDirection}; + + pub fn parse_value<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let align_content = + ContentDistribution::parse(input, AxisDirection::Block)?; + + let justify_content = input.try_parse(|input| { + ContentDistribution::parse(input, AxisDirection::Inline) + }); + + let justify_content = match justify_content { + Ok(v) => v, + Err(..) => { + // https://drafts.csswg.org/css-align-3/#place-content: + // + // The second value is assigned to justify-content; if + // omitted, it is copied from the first value, unless that + // value is a <baseline-position> in which case it is + // defaulted to start. + // + if !align_content.is_baseline_position() { + align_content + } else { + ContentDistribution::start() + } + } + }; + + Ok(expanded! { + align_content: AlignContent(align_content), + justify_content: JustifyContent(justify_content), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.align_content.to_css(dest)?; + if self.align_content.0 != self.justify_content.0 { + dest.write_char(' ')?; + self.justify_content.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="place-self" + engines="gecko" + sub_properties="align-self justify-self" + spec="https://drafts.csswg.org/css-align/#place-self-property" +> + use crate::values::specified::align::{AlignSelf, JustifySelf, SelfAlignment, AxisDirection}; + + pub fn parse_value<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let align = SelfAlignment::parse(input, AxisDirection::Block)?; + let justify = input.try_parse(|input| SelfAlignment::parse(input, AxisDirection::Inline)); + + let justify = match justify { + Ok(v) => v, + Err(..) => { + debug_assert!(align.is_valid_on_both_axes()); + align + } + }; + + Ok(expanded! { + align_self: AlignSelf(align), + justify_self: JustifySelf(justify), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.align_self.to_css(dest)?; + if self.align_self.0 != self.justify_self.0 { + dest.write_char(' ')?; + self.justify_self.to_css(dest)?; + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + name="place-items" + engines="gecko" + sub_properties="align-items justify-items" + spec="https://drafts.csswg.org/css-align/#place-items-property" +> + use crate::values::specified::align::{AlignItems, JustifyItems}; + use crate::parser::Parse; + + impl From<AlignItems> for JustifyItems { + fn from(align: AlignItems) -> JustifyItems { + JustifyItems(align.0) + } + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + let align = AlignItems::parse(context, input)?; + let justify = + input.try_parse(|input| JustifyItems::parse(context, input)) + .unwrap_or_else(|_| JustifyItems::from(align)); + + Ok(expanded! { + align_items: align, + justify_items: justify, + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + self.align_items.to_css(dest)?; + if self.align_items.0 != self.justify_items.0 { + dest.write_char(' ')?; + self.justify_items.to_css(dest)?; + } + + Ok(()) + } + } +</%helpers:shorthand> + +// See https://github.com/w3c/csswg-drafts/issues/3525 for the quirks stuff. +${helpers.four_sides_shorthand( + "inset", + "%s", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013", + spec="https://drafts.csswg.org/css-logical/#propdef-inset", + allow_quirks="No", +)} + +${helpers.two_properties_shorthand( + "inset-block", + "inset-block-start", + "inset-block-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013", + spec="https://drafts.csswg.org/css-logical/#propdef-inset-block" +)} + +${helpers.two_properties_shorthand( + "inset-inline", + "inset-inline-start", + "inset-inline-end", + "specified::LengthPercentageOrAuto::parse", + engines="gecko servo-2013", + spec="https://drafts.csswg.org/css-logical/#propdef-inset-inline" +)} + +${helpers.two_properties_shorthand( + "contain-intrinsic-size", + "contain-intrinsic-width", + "contain-intrinsic-height", + engines="gecko", + gecko_pref="layout.css.contain-intrinsic-size.enabled", + spec="https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override", +)} diff --git a/servo/components/style/properties/shorthands/svg.mako.rs b/servo/components/style/properties/shorthands/svg.mako.rs new file mode 100644 index 0000000000..cf34b116ee --- /dev/null +++ b/servo/components/style/properties/shorthands/svg.mako.rs @@ -0,0 +1,287 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="mask" engines="gecko" extra_prefixes="webkit" + flags="SHORTHAND_IN_GETCS" + sub_properties="mask-mode mask-repeat mask-clip mask-origin mask-composite mask-position-x + mask-position-y mask-size mask-image" + spec="https://drafts.fxtf.org/css-masking/#propdef-mask"> + use crate::properties::longhands::{mask_mode, mask_repeat, mask_clip, mask_origin, mask_composite, mask_position_x, + mask_position_y}; + use crate::properties::longhands::{mask_size, mask_image}; + use crate::values::specified::{Position, PositionComponent}; + use crate::parser::Parse; + + // FIXME(emilio): These two mask types should be the same! + impl From<mask_origin::single_value::SpecifiedValue> for mask_clip::single_value::SpecifiedValue { + fn from(origin: mask_origin::single_value::SpecifiedValue) -> mask_clip::single_value::SpecifiedValue { + match origin { + mask_origin::single_value::SpecifiedValue::ContentBox => + mask_clip::single_value::SpecifiedValue::ContentBox, + mask_origin::single_value::SpecifiedValue::PaddingBox => + mask_clip::single_value::SpecifiedValue::PaddingBox , + mask_origin::single_value::SpecifiedValue::BorderBox => + mask_clip::single_value::SpecifiedValue::BorderBox, + % if engine == "gecko": + mask_origin::single_value::SpecifiedValue::FillBox => + mask_clip::single_value::SpecifiedValue::FillBox , + mask_origin::single_value::SpecifiedValue::StrokeBox => + mask_clip::single_value::SpecifiedValue::StrokeBox, + mask_origin::single_value::SpecifiedValue::ViewBox=> + mask_clip::single_value::SpecifiedValue::ViewBox, + % endif + } + } + } + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % for name in "image mode position_x position_y size repeat origin clip composite".split(): + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut mask_${name} = Vec::with_capacity(1); + % endfor + + input.parse_comma_separated(|input| { + % for name in "image mode position size repeat origin clip composite".split(): + let mut ${name} = None; + % endfor + loop { + if image.is_none() { + if let Ok(value) = input.try_parse(|input| mask_image::single_value + ::parse(context, input)) { + image = Some(value); + continue + } + } + if position.is_none() { + if let Ok(value) = input.try_parse(|input| Position::parse(context, input)) { + position = Some(value); + + // Parse mask size, if applicable. + size = input.try_parse(|input| { + input.expect_delim('/')?; + mask_size::single_value::parse(context, input) + }).ok(); + + continue + } + } + % for name in "repeat origin clip composite mode".split(): + if ${name}.is_none() { + if let Ok(value) = input.try_parse(|input| mask_${name}::single_value + ::parse(context, input)) { + ${name} = Some(value); + continue + } + } + % endfor + break + } + if clip.is_none() { + if let Some(origin) = origin { + clip = Some(mask_clip::single_value::SpecifiedValue::from(origin)); + } + } + let mut any = false; + % for name in "image mode position size repeat origin clip composite".split(): + any = any || ${name}.is_some(); + % endfor + if any { + if let Some(position) = position { + mask_position_x.push(position.horizontal); + mask_position_y.push(position.vertical); + } else { + mask_position_x.push(PositionComponent::zero()); + mask_position_y.push(PositionComponent::zero()); + } + % for name in "image mode size repeat origin clip composite".split(): + if let Some(m_${name}) = ${name} { + mask_${name}.push(m_${name}); + } else { + mask_${name}.push(mask_${name}::single_value + ::get_initial_specified_value()); + } + % endfor + Ok(()) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + })?; + + Ok(expanded! { + % for name in "image mode position_x position_y size repeat origin clip composite".split(): + mask_${name}: mask_${name}::SpecifiedValue(mask_${name}.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::properties::longhands::mask_origin::single_value::computed_value::T as Origin; + use crate::properties::longhands::mask_clip::single_value::computed_value::T as Clip; + use style_traits::values::SequenceWriter; + + let len = self.mask_image.0.len(); + if len == 0 { + return Ok(()); + } + % for name in "mode position_x position_y size repeat origin clip composite".split(): + if self.mask_${name}.0.len() != len { + return Ok(()); + } + % endfor + + // For each <mask-layer>, we serialize it according to the following order: + // <mask-layer> = + // <mask-reference> || + // <position> [ / <bg-size> ]? || + // <repeat-style> || + // <geometry-box> || + // [ <geometry-box> | no-clip ] || + // <compositing-operator> || + // <masking-mode> + // https://drafts.fxtf.org/css-masking-1/#the-mask + for i in 0..len { + if i > 0 { + dest.write_str(", ")?; + } + + % for name in "image mode position_x position_y size repeat origin clip composite".split(): + let ${name} = &self.mask_${name}.0[i]; + % endfor + + let mut has_other = false; + % for name in "image mode size repeat composite".split(): + let has_${name} = + *${name} != mask_${name}::single_value::get_initial_specified_value(); + has_other |= has_${name}; + % endfor + let has_position = *position_x != PositionComponent::zero() + || *position_y != PositionComponent::zero(); + let has_origin = *origin != Origin::BorderBox; + let has_clip = *clip != Clip::BorderBox; + + // If all are initial values, we serialize mask-image. + if !has_other && !has_position && !has_origin && !has_clip { + return image.to_css(dest); + } + + let mut writer = SequenceWriter::new(dest, " "); + // <mask-reference> + if has_image { + writer.item(image)?; + } + + // <position> [ / <bg-size> ]? + if has_position || has_size { + writer.item(&Position { + horizontal: position_x.clone(), + vertical: position_y.clone() + })?; + + if has_size { + writer.raw_item("/")?; + writer.item(size)?; + } + } + + // <repeat-style> + if has_repeat { + writer.item(repeat)?; + } + + // <geometry-box> + if has_origin { + writer.item(origin)?; + } + + // [ <geometry-box> | no-clip ] + if has_clip && *clip != From::from(*origin) { + writer.item(clip)?; + } + + // <compositing-operator> + if has_composite { + writer.item(composite)?; + } + + // <masking-mode> + if has_mode { + writer.item(mode)?; + } + } + + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="mask-position" engines="gecko" extra_prefixes="webkit" + flags="SHORTHAND_IN_GETCS" + sub_properties="mask-position-x mask-position-y" + spec="https://drafts.csswg.org/css-masks-4/#the-mask-position"> + use crate::properties::longhands::{mask_position_x,mask_position_y}; + use crate::values::specified::position::Position; + use crate::parser::Parse; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + // Vec grows from 0 to 4 by default on first push(). So allocate with + // capacity 1, so in the common case of only one item we don't way + // overallocate, then shrink. Note that we always push at least one + // item if parsing succeeds. + let mut position_x = Vec::with_capacity(1); + let mut position_y = Vec::with_capacity(1); + let mut any = false; + + input.parse_comma_separated(|input| { + let value = Position::parse(context, input)?; + position_x.push(value.horizontal); + position_y.push(value.vertical); + any = true; + Ok(()) + })?; + + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + + Ok(expanded! { + mask_position_x: mask_position_x::SpecifiedValue(position_x.into()), + mask_position_y: mask_position_y::SpecifiedValue(position_y.into()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.mask_position_x.0.len(); + if len == 0 || self.mask_position_y.0.len() != len { + return Ok(()); + } + + for i in 0..len { + Position { + horizontal: self.mask_position_x.0[i].clone(), + vertical: self.mask_position_y.0[i].clone() + }.to_css(dest)?; + + if i < len - 1 { + dest.write_str(", ")?; + } + } + + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/text.mako.rs b/servo/components/style/properties/shorthands/text.mako.rs new file mode 100644 index 0000000000..5b071be2c4 --- /dev/null +++ b/servo/components/style/properties/shorthands/text.mako.rs @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +<%helpers:shorthand name="text-decoration" + engines="gecko servo-2013 servo-2020" + flags="SHORTHAND_IN_GETCS" + sub_properties="text-decoration-line + ${' text-decoration-style text-decoration-color text-decoration-thickness' if engine == 'gecko' else ''}" + spec="https://drafts.csswg.org/css-text-decor/#propdef-text-decoration"> + % if engine == "gecko": + use crate::values::specified; + use crate::properties::longhands::{text_decoration_style, text_decoration_color, text_decoration_thickness}; + % endif + use crate::properties::longhands::text_decoration_line; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + % if engine == "gecko": + let (mut line, mut style, mut color, mut thickness, mut any) = (None, None, None, None, false); + % else: + let (mut line, mut any) = (None, false); + % endif + + loop { + macro_rules! parse_component { + ($value:ident, $module:ident) => ( + if $value.is_none() { + if let Ok(value) = input.try_parse(|input| $module::parse(context, input)) { + $value = Some(value); + any = true; + continue; + } + } + ) + } + + parse_component!(line, text_decoration_line); + + % if engine == "gecko": + parse_component!(style, text_decoration_style); + parse_component!(color, text_decoration_color); + parse_component!(thickness, text_decoration_thickness); + % endif + + break; + } + + if !any { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(expanded! { + text_decoration_line: unwrap_or_initial!(text_decoration_line, line), + + % if engine == "gecko": + text_decoration_style: unwrap_or_initial!(text_decoration_style, style), + text_decoration_color: unwrap_or_initial!(text_decoration_color, color), + text_decoration_thickness: unwrap_or_initial!(text_decoration_thickness, thickness), + % endif + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + #[allow(unused)] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::values::specified::TextDecorationLine; + + let (is_solid_style, is_current_color, is_auto_thickness) = + ( + % if engine == "gecko": + *self.text_decoration_style == text_decoration_style::SpecifiedValue::Solid, + *self.text_decoration_color == specified::Color::CurrentColor, + self.text_decoration_thickness.is_auto() + % else: + true, true, true + % endif + ); + + let mut has_value = false; + let is_none = *self.text_decoration_line == TextDecorationLine::none(); + if (is_solid_style && is_current_color && is_auto_thickness) || !is_none { + self.text_decoration_line.to_css(dest)?; + has_value = true; + } + + if !is_auto_thickness { + if has_value { + dest.write_char(' ')?; + } + self.text_decoration_thickness.to_css(dest)?; + has_value = true; + } + + % if engine == "gecko": + if !is_solid_style { + if has_value { + dest.write_char(' ')?; + } + self.text_decoration_style.to_css(dest)?; + has_value = true; + } + + if !is_current_color { + if has_value { + dest.write_char(' ')?; + } + self.text_decoration_color.to_css(dest)?; + has_value = true; + } + % endif + + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties/shorthands/ui.mako.rs b/servo/components/style/properties/shorthands/ui.mako.rs new file mode 100644 index 0000000000..1fdb5965fc --- /dev/null +++ b/servo/components/style/properties/shorthands/ui.mako.rs @@ -0,0 +1,444 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +<%namespace name="helpers" file="/helpers.mako.rs" /> + +macro_rules! try_parse_one { + ($context: expr, $input: expr, $var: ident, $prop_module: ident) => { + if $var.is_none() { + if let Ok(value) = $input.try_parse(|i| { + $prop_module::single_value::parse($context, i) + }) { + $var = Some(value); + continue; + } + } + }; +} + +<%helpers:shorthand name="transition" + engines="gecko servo-2013 servo-2020" + extra_prefixes="moz:layout.css.prefixes.transitions webkit" + sub_properties="transition-property transition-duration + transition-timing-function + transition-delay" + spec="https://drafts.csswg.org/css-transitions/#propdef-transition"> + use crate::parser::Parse; + % for prop in "delay duration property timing_function".split(): + use crate::properties::longhands::transition_${prop}; + % endfor + use crate::values::specified::TransitionProperty; + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + struct SingleTransition { + % for prop in "property duration timing_function delay".split(): + transition_${prop}: transition_${prop}::SingleSpecifiedValue, + % endfor + } + + fn parse_one_transition<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + first: bool, + ) -> Result<SingleTransition,ParseError<'i>> { + % for prop in "property duration timing_function delay".split(): + let mut ${prop} = None; + % endfor + + let mut parsed = 0; + loop { + parsed += 1; + + try_parse_one!(context, input, duration, transition_duration); + try_parse_one!(context, input, timing_function, transition_timing_function); + try_parse_one!(context, input, delay, transition_delay); + // Must check 'transition-property' after 'transition-timing-function' since + // 'transition-property' accepts any keyword. + if property.is_none() { + if let Ok(value) = input.try_parse(|i| TransitionProperty::parse(context, i)) { + property = Some(value); + continue; + } + + // 'none' is not a valid value for <single-transition-property>, + // so it's only acceptable as the first item. + if first && input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + property = Some(TransitionProperty::none()); + continue; + } + } + + parsed -= 1; + break + } + + if parsed != 0 { + Ok(SingleTransition { + % for prop in "property duration timing_function delay".split(): + transition_${prop}: ${prop}.unwrap_or_else(transition_${prop}::single_value + ::get_initial_specified_value), + % endfor + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } + + % for prop in "property duration timing_function delay".split(): + let mut ${prop}s = Vec::new(); + % endfor + + let mut first = true; + let mut has_transition_property_none = false; + let results = input.parse_comma_separated(|i| { + if has_transition_property_none { + // If you specify transition-property: none, multiple items are invalid. + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + let transition = parse_one_transition(context, i, first)?; + first = false; + has_transition_property_none = transition.transition_property.is_none(); + Ok(transition) + })?; + for result in results { + % for prop in "property duration timing_function delay".split(): + ${prop}s.push(result.transition_${prop}); + % endfor + } + + Ok(expanded! { + % for prop in "property duration timing_function delay".split(): + transition_${prop}: transition_${prop}::SpecifiedValue(${prop}s.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + use crate::Zero; + use style_traits::values::SequenceWriter; + + let property_len = self.transition_property.0.len(); + + // There are two cases that we can do shorthand serialization: + // * when all value lists have the same length, or + // * when transition-property is none, and other value lists have exactly one item. + if property_len == 0 { + % for name in "duration delay timing_function".split(): + if self.transition_${name}.0.len() != 1 { + return Ok(()); + } + % endfor + } else { + % for name in "duration delay timing_function".split(): + if self.transition_${name}.0.len() != property_len { + return Ok(()); + } + % endfor + } + + // Representative length. + let len = self.transition_duration.0.len(); + + for i in 0..len { + if i != 0 { + dest.write_str(", ")?; + } + + let has_duration = !self.transition_duration.0[i].is_zero(); + let has_timing = !self.transition_timing_function.0[i].is_ease(); + let has_delay = !self.transition_delay.0[i].is_zero(); + let has_any = has_duration || has_timing || has_delay; + + let mut writer = SequenceWriter::new(dest, " "); + + if property_len == 0 { + writer.raw_item("none")?; + } else if !self.transition_property.0[i].is_all() || !has_any { + writer.item(&self.transition_property.0[i])?; + } + + // In order to avoid ambiguity, we have to serialize duration if we have delay. + if has_duration || has_delay { + writer.item(&self.transition_duration.0[i])?; + } + + if has_timing { + writer.item(&self.transition_timing_function.0[i])?; + } + + if has_delay { + writer.item(&self.transition_delay.0[i])?; + } + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand name="animation" + engines="gecko servo-2013 servo-2020" + extra_prefixes="moz:layout.css.prefixes.animations webkit" + sub_properties="animation-name animation-duration + animation-timing-function animation-delay + animation-iteration-count animation-direction + animation-fill-mode animation-play-state animation-timeline" + rule_types_allowed="Style" + spec="https://drafts.csswg.org/css-animations/#propdef-animation"> + <% + props = "name timeline duration timing_function delay iteration_count \ + direction fill_mode play_state".split() + %> + % for prop in props: + use crate::properties::longhands::animation_${prop}; + % endfor + + pub fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Longhands, ParseError<'i>> { + struct SingleAnimation { + % for prop in props: + animation_${prop}: animation_${prop}::SingleSpecifiedValue, + % endfor + } + + fn parse_one_animation<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SingleAnimation, ParseError<'i>> { + % for prop in props: + let mut ${prop} = None; + % endfor + + let mut parsed = 0; + // NB: Name must be the last one here so that keywords valid for other + // longhands are not interpreted as names. + // + // Also, duration must be before delay, see + // https://drafts.csswg.org/css-animations/#typedef-single-animation + loop { + parsed += 1; + try_parse_one!(context, input, duration, animation_duration); + try_parse_one!(context, input, timing_function, animation_timing_function); + try_parse_one!(context, input, delay, animation_delay); + try_parse_one!(context, input, iteration_count, animation_iteration_count); + try_parse_one!(context, input, direction, animation_direction); + try_parse_one!(context, input, fill_mode, animation_fill_mode); + try_parse_one!(context, input, play_state, animation_play_state); + try_parse_one!(context, input, name, animation_name); + if static_prefs::pref!("layout.css.scroll-driven-animations.enabled") { + try_parse_one!(context, input, timeline, animation_timeline); + } + + parsed -= 1; + break + } + + // If nothing is parsed, this is an invalid entry. + if parsed == 0 { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(SingleAnimation { + % for prop in props: + animation_${prop}: ${prop}.unwrap_or_else(animation_${prop}::single_value + ::get_initial_specified_value), + % endfor + }) + } + } + + % for prop in props: + let mut ${prop}s = vec![]; + % endfor + + let results = input.parse_comma_separated(|i| parse_one_animation(context, i))?; + for result in results.into_iter() { + % for prop in props: + ${prop}s.push(result.animation_${prop}); + % endfor + } + + Ok(expanded! { + % for prop in props: + animation_${prop}: animation_${prop}::SpecifiedValue(${prop}s.into()), + % endfor + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + let len = self.animation_name.0.len(); + // There should be at least one declared value + if len == 0 { + return Ok(()); + } + + // If any value list length is differs then we don't do a shorthand serialization + // either. + % for name in props[2:]: + if len != self.animation_${name}.0.len() { + return Ok(()) + } + % endfor + + // If the preference of animation-timeline is disabled, `self.animation_timeline` is + // None. + if self.animation_timeline.map_or(false, |v| len != v.0.len()) { + return Ok(()); + } + + for i in 0..len { + if i != 0 { + dest.write_str(", ")?; + } + + % for name in props[2:]: + self.animation_${name}.0[i].to_css(dest)?; + dest.write_char(' ')?; + % endfor + + self.animation_name.0[i].to_css(dest)?; + + // Based on the spec, the default values of other properties must be output in at + // least the cases necessary to distinguish an animation-name. The serialization + // order of animation-timeline is always later than animation-name, so it's fine + // to not serialize it if it is the default value. It's still possible to + // distinguish them (because we always serialize animation-name). + // https://drafts.csswg.org/css-animations-1/#animation + // https://drafts.csswg.org/css-animations-2/#typedef-single-animation + // + // Note: it's also fine to always serialize this. However, it seems Blink + // doesn't serialize default animation-timeline now, so we follow the same rule. + if let Some(ref timeline) = self.animation_timeline { + if !timeline.0[i].is_auto() { + dest.write_char(' ')?; + timeline.0[i].to_css(dest)?; + } + } + } + Ok(()) + } + } +</%helpers:shorthand> + +<%helpers:shorthand + engines="gecko" + name="scroll-timeline" + sub_properties="scroll-timeline-name scroll-timeline-axis" + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-shorthand" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::properties::longhands::{scroll_timeline_axis, scroll_timeline_name}; + + let mut names = Vec::with_capacity(1); + let mut axes = Vec::with_capacity(1); + input.parse_comma_separated(|input| { + let name = scroll_timeline_name::single_value::parse(context, input)?; + let axis = input.try_parse(|i| scroll_timeline_axis::single_value::parse(context, i)); + + names.push(name); + axes.push(axis.unwrap_or_default()); + + Ok(()) + })?; + + Ok(expanded! { + scroll_timeline_name: scroll_timeline_name::SpecifiedValue(names.into()), + scroll_timeline_axis: scroll_timeline_axis::SpecifiedValue(axes.into()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + // If any value list length is differs then we don't do a shorthand serialization + // either. + let len = self.scroll_timeline_name.0.len(); + if len != self.scroll_timeline_axis.0.len() { + return Ok(()); + } + + for i in 0..len { + if i != 0 { + dest.write_str(", ")?; + } + + self.scroll_timeline_name.0[i].to_css(dest)?; + + if self.scroll_timeline_axis.0[i] != Default::default() { + dest.write_char(' ')?; + self.scroll_timeline_axis.0[i].to_css(dest)?; + } + + } + Ok(()) + } + } +</%helpers:shorthand> + +// Note: view-timeline shorthand doesn't take view-timeline-inset into account. +<%helpers:shorthand + engines="gecko" + name="view-timeline" + sub_properties="view-timeline-name view-timeline-axis" + gecko_pref="layout.css.scroll-driven-animations.enabled", + spec="https://drafts.csswg.org/scroll-animations-1/#view-timeline-shorthand" +> + pub fn parse_value<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Longhands, ParseError<'i>> { + use crate::properties::longhands::{view_timeline_axis, view_timeline_name}; + + let mut names = Vec::with_capacity(1); + let mut axes = Vec::with_capacity(1); + input.parse_comma_separated(|input| { + let name = view_timeline_name::single_value::parse(context, input)?; + let axis = input.try_parse(|i| view_timeline_axis::single_value::parse(context, i)); + + names.push(name); + axes.push(axis.unwrap_or_default()); + + Ok(()) + })?; + + Ok(expanded! { + view_timeline_name: view_timeline_name::SpecifiedValue(names.into()), + view_timeline_axis: view_timeline_axis::SpecifiedValue(axes.into()), + }) + } + + impl<'a> ToCss for LonghandsToSerialize<'a> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result where W: fmt::Write { + // If any value list length is differs then we don't do a shorthand serialization + // either. + let len = self.view_timeline_name.0.len(); + if len != self.view_timeline_axis.0.len() { + return Ok(()); + } + + for i in 0..len { + if i != 0 { + dest.write_str(", ")?; + } + + self.view_timeline_name.0[i].to_css(dest)?; + + if self.view_timeline_axis.0[i] != Default::default() { + dest.write_char(' ')?; + self.view_timeline_axis.0[i].to_css(dest)?; + } + + } + Ok(()) + } + } +</%helpers:shorthand> diff --git a/servo/components/style/properties_and_values/mod.rs b/servo/components/style/properties_and_values/mod.rs new file mode 100644 index 0000000000..5b5e219d59 --- /dev/null +++ b/servo/components/style/properties_and_values/mod.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Properties and Values +//! +//! https://drafts.css-houdini.org/css-properties-values-api-1/ + +pub mod registry; +pub mod rule; +pub mod syntax; +pub mod value; diff --git a/servo/components/style/properties_and_values/registry.rs b/servo/components/style/properties_and_values/registry.rs new file mode 100644 index 0000000000..e3cd552c9c --- /dev/null +++ b/servo/components/style/properties_and_values/registry.rs @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Registered custom properties. + +use super::rule::{Inherits, InitialValue, PropertyRuleName}; +use super::syntax::Descriptor; +use crate::selector_map::PrecomputedHashMap; +use crate::stylesheets::UrlExtraData; +use crate::Atom; +use cssparser::SourceLocation; + +/// The metadata of a custom property registration that we need to do the cascade properly. +#[derive(Debug, Clone, MallocSizeOf)] +pub struct PropertyRegistrationData { + /// The syntax of the property. + pub syntax: Descriptor, + /// Whether the property inherits. + pub inherits: Inherits, + /// The initial value. Only missing for universal syntax. + #[ignore_malloc_size_of = "Arc"] + pub initial_value: Option<InitialValue>, +} + +static UNREGISTERED: PropertyRegistrationData = PropertyRegistrationData { + syntax: Descriptor::universal(), + inherits: Inherits::True, + initial_value: None, +}; + +impl PropertyRegistrationData { + /// The data for an unregistered property. + pub fn unregistered() -> &'static Self { + &UNREGISTERED + } + + /// Returns whether this property inherits. + #[inline] + pub fn inherits(&self) -> bool { + self.inherits == Inherits::True + } +} + +/// A computed, already-validated property registration. +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#custom-property-registration> +#[derive(Debug, Clone, MallocSizeOf)] +pub struct PropertyRegistration { + /// The custom property name. + pub name: PropertyRuleName, + /// The actual information about the property. + pub data: PropertyRegistrationData, + /// The url data that is used to parse and compute the registration's initial value. Note that + /// it's not the url data that should be used to parse other values. Other values should use + /// the data of the style sheet where they came from. + pub url_data: UrlExtraData, + /// The source location of this registration, if it comes from a CSS rule. + pub source_location: SourceLocation, +} + +impl PropertyRegistration { + /// Returns whether this property inherits. + #[inline] + pub fn inherits(&self) -> bool { + self.data.inherits == Inherits::True + } +} + +/// The script registry of custom properties. +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#dom-window-registeredpropertyset-slot> +#[derive(Default)] +pub struct ScriptRegistry { + properties: PrecomputedHashMap<Atom, PropertyRegistration>, +} + +impl ScriptRegistry { + /// Gets an already-registered custom property via script. + #[inline] + pub fn get(&self, name: &Atom) -> Option<&PropertyRegistration> { + self.properties.get(name) + } + + /// Gets already-registered custom properties via script. + #[inline] + pub fn properties(&self) -> &PrecomputedHashMap<Atom, PropertyRegistration> { + &self.properties + } + + /// Register a given property. As per + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#the-registerproperty-function> + /// we don't allow overriding the registration. + #[inline] + pub fn register(&mut self, registration: PropertyRegistration) { + let name = registration.name.0.clone(); + let old = self.properties.insert(name, registration); + debug_assert!(old.is_none(), "Already registered? Should be an error"); + } + + /// Returns the properties hashmap. + #[inline] + pub fn get_all(&self) -> &PrecomputedHashMap<Atom, PropertyRegistration> { + &self.properties + } +} diff --git a/servo/components/style/properties_and_values/rule.rs b/servo/components/style/properties_and_values/rule.rs new file mode 100644 index 0000000000..96617eccce --- /dev/null +++ b/servo/components/style/properties_and_values/rule.rs @@ -0,0 +1,348 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@property`] at-rule. +//! +//! https://drafts.css-houdini.org/css-properties-values-api-1/#at-property-rule + +use super::{ + registry::{PropertyRegistration, PropertyRegistrationData}, + syntax::Descriptor, + value::{AllowComputationallyDependent, SpecifiedValue as SpecifiedRegisteredValue}, +}; +use crate::custom_properties::{Name as CustomPropertyName, SpecifiedValue}; +use crate::error_reporting::ContextualParseError; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::{computed, serialize_atom_name}; +use cssparser::{ + AtRuleParser, BasicParseErrorKind, CowRcStr, DeclarationParser, ParseErrorKind, Parser, + ParserInput, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, +}; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use selectors::parser::SelectorParseErrorKind; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use to_shmem::{SharedMemoryBuilder, ToShmem}; + +/// Parse the block inside a `@property` rule. +/// +/// Valid `@property` rules result in a registered custom property, as if `registerProperty()` had +/// been called with equivalent parameters. +pub fn parse_property_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + name: PropertyRuleName, + source_location: SourceLocation, +) -> Result<PropertyRegistration, ParseError<'i>> { + let mut descriptors = PropertyDescriptors::default(); + let mut parser = PropertyRuleParser { + context, + descriptors: &mut descriptors, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if !context.error_reporting_enabled() { + continue; + } + if let Err((error, slice)) = declaration { + let location = error.location; + let error = if matches!( + error.kind, + ParseErrorKind::Custom(StyleParseErrorKind::PropertySyntaxField(_)) + ) { + // If the provided string is not a valid syntax string (if it + // returns failure when consume a syntax definition is called on + // it), the descriptor is invalid and must be ignored. + ContextualParseError::UnsupportedValue(slice, error) + } else { + // Unknown descriptors are invalid and ignored, but do not + // invalidate the @property rule. + ContextualParseError::UnsupportedPropertyDescriptor(slice, error) + }; + context.log_css_error(location, error); + } + } + + // https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor: + // + // The syntax descriptor is required for the @property rule to be valid; if it’s + // missing, the @property rule is invalid. + let Some(syntax) = descriptors.syntax else { + return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)); + }; + + // https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor: + // + // The inherits descriptor is required for the @property rule to be valid; if it’s + // missing, the @property rule is invalid. + let Some(inherits) = descriptors.inherits else { + return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)); + }; + + if PropertyRegistration::validate_initial_value(&syntax, descriptors.initial_value.as_deref()) + .is_err() + { + return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)); + } + + Ok(PropertyRegistration { + name, + data: PropertyRegistrationData { + syntax, + inherits, + initial_value: descriptors.initial_value, + }, + url_data: context.url_data.clone(), + source_location, + }) +} + +struct PropertyRuleParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + descriptors: &'a mut PropertyDescriptors, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for PropertyRuleParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for PropertyRuleParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for PropertyRuleParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} + +macro_rules! property_descriptors { + ( + $( #[$doc: meta] $name: tt $ident: ident: $ty: ty, )* + ) => { + /// Data inside a `@property` rule. + /// + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#at-property-rule> + #[derive(Clone, Debug, Default, PartialEq)] + struct PropertyDescriptors { + $( + #[$doc] + $ident: Option<$ty>, + )* + } + + impl PropertyRegistration { + fn decl_to_css(&self, dest: &mut CssStringWriter) -> fmt::Result { + $( + let $ident = Option::<&$ty>::from(&self.data.$ident); + if let Some(ref value) = $ident { + dest.write_str(concat!($name, ": "))?; + value.to_css(&mut CssWriter::new(dest))?; + dest.write_str("; ")?; + } + )* + Ok(()) + } + } + + impl<'a, 'b, 'i> DeclarationParser<'i> for PropertyRuleParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name => { + // DeclarationParser also calls parse_entirely so we’d normally not need + // to, but in this case we do because we set the value as a side effect + // rather than returning it. + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.descriptors.$ident = Some(value) + }, + )* + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } + } + } +} + +property_descriptors! { + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#the-syntax-descriptor> + "syntax" syntax: Descriptor, + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor> + "inherits" inherits: Inherits, + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#initial-value-descriptor> + "initial-value" initial_value: InitialValue, +} + +/// Errors that can happen when registering a property. +#[allow(missing_docs)] +pub enum PropertyRegistrationError { + NoInitialValue, + InvalidInitialValue, + InitialValueNotComputationallyIndependent, +} + +impl PropertyRegistration { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, _: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + MallocSizeOf::size_of(self, ops) + } + + /// Computes the value of the computationally independent initial value. + pub fn compute_initial_value( + &self, + computed_context: &computed::Context, + ) -> Result<InitialValue, ()> { + let Some(ref initial) = self.data.initial_value else { + return Err(()); + }; + + if self.data.syntax.is_universal() { + return Ok(Arc::clone(initial)); + } + + let mut input = ParserInput::new(initial.css_text()); + let mut input = Parser::new(&mut input); + input.skip_whitespace(); + + match SpecifiedRegisteredValue::compute( + &mut input, + &self.data, + &self.url_data, + computed_context, + AllowComputationallyDependent::No, + ) { + Ok(computed) => Ok(Arc::new(computed)), + Err(_) => Err(()), + } + } + + /// Performs syntax validation as per the initial value descriptor. + /// https://drafts.css-houdini.org/css-properties-values-api-1/#initial-value-descriptor + pub fn validate_initial_value( + syntax: &Descriptor, + initial_value: Option<&SpecifiedValue>, + ) -> Result<(), PropertyRegistrationError> { + use crate::properties::CSSWideKeyword; + // If the value of the syntax descriptor is the universal syntax definition, then the + // initial-value descriptor is optional. If omitted, the initial value of the property is + // the guaranteed-invalid value. + if syntax.is_universal() && initial_value.is_none() { + return Ok(()); + } + + // Otherwise, if the value of the syntax descriptor is not the universal syntax definition, + // the following conditions must be met for the @property rule to be valid: + + // The initial-value descriptor must be present. + let Some(initial) = initial_value else { + return Err(PropertyRegistrationError::NoInitialValue); + }; + + // A value that references the environment or other variables is not computationally + // independent. + if initial.has_references() { + return Err(PropertyRegistrationError::InitialValueNotComputationallyIndependent); + } + + let mut input = ParserInput::new(initial.css_text()); + let mut input = Parser::new(&mut input); + input.skip_whitespace(); + + // The initial-value cannot include CSS-wide keywords. + if input.try_parse(CSSWideKeyword::parse).is_ok() { + return Err(PropertyRegistrationError::InitialValueNotComputationallyIndependent); + } + + match SpecifiedRegisteredValue::parse( + &mut input, + syntax, + &initial.url_data, + AllowComputationallyDependent::No, + ) { + Ok(_) => {}, + Err(_) => return Err(PropertyRegistrationError::InvalidInitialValue), + } + + Ok(()) + } +} + +impl ToCssWithGuard for PropertyRegistration { + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#serialize-a-csspropertyrule> + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@property ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + self.decl_to_css(dest)?; + dest.write_char('}') + } +} + +impl ToShmem for PropertyRegistration { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + Err(String::from( + "ToShmem failed for PropertyRule: cannot handle @property rules", + )) + } +} + +/// A custom property name wrapper that includes the `--` prefix in its serialization +#[derive(Clone, Debug, PartialEq, MallocSizeOf)] +pub struct PropertyRuleName(pub CustomPropertyName); + +impl ToCss for PropertyRuleName { + fn to_css<W: Write>(&self, dest: &mut CssWriter<W>) -> fmt::Result { + dest.write_str("--")?; + serialize_atom_name(&self.0, dest) + } +} + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#inherits-descriptor> +#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, ToCss)] +pub enum Inherits { + /// `true` value for the `inherits` descriptor + True, + /// `false` value for the `inherits` descriptor + False, +} + +/// Specifies the initial value of the custom property registration represented by the @property +/// rule, controlling the property’s initial value. +/// +/// The SpecifiedValue is wrapped in an Arc to avoid copying when using it. +pub type InitialValue = Arc<SpecifiedValue>; + +impl Parse for InitialValue { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.skip_whitespace(); + Ok(Arc::new(SpecifiedValue::parse(input, &context.url_data)?)) + } +} diff --git a/servo/components/style/properties_and_values/syntax/ascii.rs b/servo/components/style/properties_and_values/syntax/ascii.rs new file mode 100644 index 0000000000..e1a1b08535 --- /dev/null +++ b/servo/components/style/properties_and_values/syntax/ascii.rs @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +/// Trims ASCII whitespace characters from a slice, and returns the trimmed input. +pub fn trim_ascii_whitespace(input: &str) -> &str { + if input.is_empty() { + return input; + } + + let mut start = 0; + { + let mut iter = input.as_bytes().iter(); + loop { + let byte = match iter.next() { + Some(b) => b, + None => return "", + }; + + if !byte.is_ascii_whitespace() { + break; + } + start += 1; + } + } + + let mut end = input.len(); + assert!(start < end); + { + let mut iter = input.as_bytes()[start..].iter().rev(); + loop { + let byte = match iter.next() { + Some(b) => b, + None => { + debug_assert!(false, "We should have caught this in the loop above!"); + return ""; + }, + }; + + if !byte.is_ascii_whitespace() { + break; + } + end -= 1; + } + } + + &input[start..end] +} + +#[test] +fn trim_ascii_whitespace_test() { + fn test(i: &str, o: &str) { + assert_eq!(trim_ascii_whitespace(i), o) + } + + test("", ""); + test(" ", ""); + test(" a b c ", "a b c"); + test(" \t \t \ta b c \t \t \t \t", "a b c"); +} diff --git a/servo/components/style/properties_and_values/syntax/data_type.rs b/servo/components/style/properties_and_values/syntax/data_type.rs new file mode 100644 index 0000000000..be331e2222 --- /dev/null +++ b/servo/components/style/properties_and_values/syntax/data_type.rs @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Used for parsing and serializing component names from the syntax string. + +use super::{Component, ComponentName, Multiplier}; +use std::fmt::{self, Debug, Write}; +use style_traits::{CssWriter, ToCss}; + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#supported-names> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq)] +pub enum DataType { + /// Any valid `<length>` value + Length, + /// `<number>` values + Number, + /// Any valid <percentage> value + Percentage, + /// Any valid `<length>` or `<percentage>` value, any valid `<calc()>` expression combining + /// `<length>` and `<percentage>` components. + LengthPercentage, + /// Any valid `<color>` value + Color, + /// Any valid `<image>` value + Image, + /// Any valid `<url>` value + Url, + /// Any valid `<integer>` value + Integer, + /// Any valid `<angle>` value + Angle, + /// Any valid `<time>` value + Time, + /// Any valid `<resolution>` value + Resolution, + /// Any valid `<transform-function>` value + TransformFunction, + /// Any valid `<custom-ident>` value + CustomIdent, + /// A list of valid `<transform-function>` values. Note that "<transform-list>" is a pre-multiplied + /// data type name equivalent to "<transform-function>+" + TransformList, + /// Any valid `<string>` value + /// + /// <https://github.com/w3c/css-houdini-drafts/issues/1103> + String, +} + +impl DataType { + /// Converts a component name from a pre-multiplied data type to its un-pre-multiplied equivalent. + /// + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#pre-multiplied-data-type-name> + pub fn unpremultiply(&self) -> Option<Component> { + match *self { + DataType::TransformList => Some(Component { + name: ComponentName::DataType(DataType::TransformFunction), + multiplier: Some(Multiplier::Space), + }), + _ => None, + } + } + + /// Parses a syntax component name. + pub fn from_str(ty: &str) -> Option<Self> { + Some(match ty.as_bytes() { + b"length" => DataType::Length, + b"number" => DataType::Number, + b"percentage" => DataType::Percentage, + b"length-percentage" => DataType::LengthPercentage, + b"color" => DataType::Color, + b"image" => DataType::Image, + b"url" => DataType::Url, + b"integer" => DataType::Integer, + b"angle" => DataType::Angle, + b"time" => DataType::Time, + b"resolution" => DataType::Resolution, + b"transform-function" => DataType::TransformFunction, + b"custom-ident" => DataType::CustomIdent, + b"transform-list" => DataType::TransformList, + b"string" => DataType::String, + _ => return None, + }) + } + + /// Returns true if this data type requires deferring computation to properly + /// resolve font-dependent lengths. + pub fn may_reference_font_relative_length(&self) -> bool { + match self { + DataType::Length | + DataType::LengthPercentage | + DataType::TransformFunction | + DataType::TransformList => true, + DataType::Number | + DataType::Percentage | + DataType::Color | + DataType::Image | + DataType::Url | + DataType::Integer | + DataType::Angle | + DataType::Time | + DataType::Resolution | + DataType::CustomIdent | + DataType::String => false, + } + } +} + +impl ToCss for DataType { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_char('<')?; + dest.write_str(match *self { + DataType::Length => "length", + DataType::Number => "number", + DataType::Percentage => "percentage", + DataType::LengthPercentage => "length-percentage", + DataType::Color => "color", + DataType::Image => "image", + DataType::Url => "url", + DataType::Integer => "integer", + DataType::Angle => "angle", + DataType::Time => "time", + DataType::Resolution => "resolution", + DataType::TransformFunction => "transform-function", + DataType::CustomIdent => "custom-ident", + DataType::TransformList => "transform-list", + DataType::String => "string", + })?; + dest.write_char('>') + } +} diff --git a/servo/components/style/properties_and_values/syntax/mod.rs b/servo/components/style/properties_and_values/syntax/mod.rs new file mode 100644 index 0000000000..404c8caa7b --- /dev/null +++ b/servo/components/style/properties_and_values/syntax/mod.rs @@ -0,0 +1,392 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Used for parsing and serializing the [`@property`] syntax string. +//! +//! <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax> + +use std::fmt::{self, Debug}; +use std::{borrow::Cow, fmt::Write}; + +use crate::parser::{Parse, ParserContext}; +use crate::values::CustomIdent; +use cssparser::{Parser as CSSParser, ParserInput as CSSParserInput}; +use style_traits::{ + CssWriter, ParseError as StyleParseError, PropertySyntaxParseError as ParseError, + StyleParseErrorKind, ToCss, +}; + +use self::data_type::DataType; + +mod ascii; +pub mod data_type; + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#parsing-syntax> +#[derive(Debug, Clone, Default, MallocSizeOf, PartialEq)] +pub struct Descriptor { + /// The parsed components, if any. + /// TODO: Could be a Box<[]> if that supported const construction. + pub components: Vec<Component>, + /// The specified css syntax, if any. + specified: Option<Box<str>>, +} + +impl Descriptor { + /// Returns the universal descriptor. + pub const fn universal() -> Self { + Self { + components: Vec::new(), + specified: None, + } + } + + /// Returns whether this is the universal syntax descriptor. + #[inline] + pub fn is_universal(&self) -> bool { + self.components.is_empty() + } + + /// Returns the specified string, if any. + #[inline] + pub fn specified_string(&self) -> Option<&str> { + self.specified.as_deref() + } + + /// Parse a syntax descriptor. + /// https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-definition + pub fn from_str(css: &str, save_specified: bool) -> Result<Self, ParseError> { + // 1. Strip leading and trailing ASCII whitespace from string. + let input = ascii::trim_ascii_whitespace(css); + + // 2. If string's length is 0, return failure. + if input.is_empty() { + return Err(ParseError::EmptyInput); + } + + let specified = if save_specified { + Some(Box::from(css)) + } else { + None + }; + + // 3. If string's length is 1, and the only code point in string is U+002A + // ASTERISK (*), return the universal syntax descriptor. + if input.len() == 1 && input.as_bytes()[0] == b'*' { + return Ok(Self { + components: Default::default(), + specified, + }); + } + + // 4. Let stream be an input stream created from the code points of string, + // preprocessed as specified in [css-syntax-3]. Let descriptor be an + // initially empty list of syntax components. + // + // NOTE(emilio): Instead of preprocessing we cheat and treat new-lines and + // nulls in the parser specially. + let mut components = vec![]; + { + let mut parser = Parser::new(input, &mut components); + // 5. Repeatedly consume the next input code point from stream. + parser.parse()?; + } + Ok(Self { components, specified }) + } + + /// Returns true if the syntax permits the value to be computed as a length. + pub fn may_compute_length(&self) -> bool { + for component in self.components.iter() { + match &component.name { + ComponentName::DataType(ref t) => { + if matches!(t, DataType::Length | DataType::LengthPercentage) { + return true; + } + }, + ComponentName::Ident(_) => (), + }; + } + false + } + + /// Returns true if the syntax requires deferring computation to properly + /// resolve font-dependent lengths. + pub fn may_reference_font_relative_length(&self) -> bool { + for component in self.components.iter() { + match &component.name { + ComponentName::DataType(ref t) => { + if t.may_reference_font_relative_length() { + return true; + } + }, + ComponentName::Ident(_) => (), + }; + } + false + } +} + +impl ToCss for Descriptor { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if let Some(ref specified) = self.specified { + return specified.to_css(dest); + } + + if self.is_universal() { + return dest.write_char('*'); + } + + let mut first = true; + for component in &*self.components { + if !first { + dest.write_str(" | ")?; + } + component.to_css(dest)?; + first = false; + } + + Ok(()) + } +} + +impl Parse for Descriptor { + /// Parse a syntax descriptor. + fn parse<'i>( + _: &ParserContext, + parser: &mut CSSParser<'i, '_>, + ) -> Result<Self, StyleParseError<'i>> { + let input = parser.expect_string()?; + Descriptor::from_str(input.as_ref(), /* save_specified = */ true) + .map_err(|err| parser.new_custom_error(StyleParseErrorKind::PropertySyntaxField(err))) + } +} + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#multipliers> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue)] +pub enum Multiplier { + /// Indicates a space-separated list. + Space, + /// Indicates a comma-separated list. + Comma, +} + +impl ToCss for Multiplier { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_char(match *self { + Multiplier::Space => '+', + Multiplier::Comma => '#', + }) + } +} + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component> +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct Component { + name: ComponentName, + multiplier: Option<Multiplier>, +} + +impl Component { + /// Returns the component's name. + #[inline] + pub fn name(&self) -> &ComponentName { + &self.name + } + + /// Returns the component's multiplier, if one exists. + #[inline] + pub fn multiplier(&self) -> Option<Multiplier> { + self.multiplier + } + + /// If the component is premultiplied, return the un-premultiplied component. + #[inline] + pub fn unpremultiplied(&self) -> Cow<Self> { + match self.name.unpremultiply() { + Some(component) => { + debug_assert!( + self.multiplier.is_none(), + "Shouldn't have parsed a multiplier for a pre-multiplied data type name", + ); + Cow::Owned(component) + }, + None => Cow::Borrowed(self), + } + } +} + +impl ToCss for Component { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.name().to_css(dest)?; + self.multiplier().to_css(dest) + } +} + +/// <https://drafts.css-houdini.org/css-properties-values-api-1/#syntax-component-name> +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss)] +pub enum ComponentName { + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#data-type-name> + DataType(DataType), + /// <https://drafts.csswg.org/css-values-4/#custom-idents> + Ident(CustomIdent), +} + +impl ComponentName { + fn unpremultiply(&self) -> Option<Component> { + match *self { + ComponentName::DataType(ref t) => t.unpremultiply(), + ComponentName::Ident(..) => None, + } + } + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#pre-multiplied-data-type-name> + fn is_pre_multiplied(&self) -> bool { + self.unpremultiply().is_some() + } +} + +struct Parser<'a> { + input: &'a str, + position: usize, + output: &'a mut Vec<Component>, +} + +/// <https://drafts.csswg.org/css-syntax-3/#letter> +fn is_letter(byte: u8) -> bool { + match byte { + b'A'..=b'Z' | b'a'..=b'z' => true, + _ => false, + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#non-ascii-code-point> +fn is_non_ascii(byte: u8) -> bool { + byte >= 0x80 +} + +/// <https://drafts.csswg.org/css-syntax-3/#name-start-code-point> +fn is_name_start(byte: u8) -> bool { + is_letter(byte) || is_non_ascii(byte) || byte == b'_' +} + +impl<'a> Parser<'a> { + fn new(input: &'a str, output: &'a mut Vec<Component>) -> Self { + Self { + input, + position: 0, + output, + } + } + + fn peek(&self) -> Option<u8> { + self.input.as_bytes().get(self.position).cloned() + } + + fn parse(&mut self) -> Result<(), ParseError> { + // 5. Repeatedly consume the next input code point from stream: + loop { + let component = self.parse_component()?; + self.output.push(component); + self.skip_whitespace(); + + let byte = match self.peek() { + None => return Ok(()), + Some(b) => b, + }; + + if byte != b'|' { + return Err(ParseError::ExpectedPipeBetweenComponents); + } + + self.position += 1; + } + } + + fn skip_whitespace(&mut self) { + loop { + match self.peek() { + Some(c) if c.is_ascii_whitespace() => self.position += 1, + _ => return, + } + } + } + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-data-type-name> + fn parse_data_type_name(&mut self) -> Result<DataType, ParseError> { + let start = self.position; + loop { + let byte = match self.peek() { + Some(b) => b, + None => return Err(ParseError::UnclosedDataTypeName), + }; + if byte != b'>' { + self.position += 1; + continue; + } + let ty = match DataType::from_str(&self.input[start..self.position]) { + Some(ty) => ty, + None => return Err(ParseError::UnknownDataTypeName), + }; + self.position += 1; + return Ok(ty); + } + } + + fn parse_name(&mut self) -> Result<ComponentName, ParseError> { + let b = match self.peek() { + Some(b) => b, + None => return Err(ParseError::UnexpectedEOF), + }; + + if b == b'<' { + self.position += 1; + return Ok(ComponentName::DataType(self.parse_data_type_name()?)); + } + + if b != b'\\' && !is_name_start(b) { + return Err(ParseError::InvalidNameStart); + } + + let input = &self.input[self.position..]; + let mut input = CSSParserInput::new(input); + let mut input = CSSParser::new(&mut input); + let name = match CustomIdent::parse(&mut input, &[]) { + Ok(name) => name, + Err(_) => return Err(ParseError::InvalidName), + }; + self.position += input.position().byte_index(); + return Ok(ComponentName::Ident(name)); + } + + fn parse_multiplier(&mut self) -> Option<Multiplier> { + let multiplier = match self.peek()? { + b'+' => Multiplier::Space, + b'#' => Multiplier::Comma, + _ => return None, + }; + self.position += 1; + Some(multiplier) + } + + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-a-syntax-component> + fn parse_component(&mut self) -> Result<Component, ParseError> { + // Consume as much whitespace as possible from stream. + self.skip_whitespace(); + let name = self.parse_name()?; + let multiplier = if name.is_pre_multiplied() { + None + } else { + self.parse_multiplier() + }; + Ok(Component { name, multiplier }) + } +} diff --git a/servo/components/style/properties_and_values/value.rs b/servo/components/style/properties_and_values/value.rs new file mode 100644 index 0000000000..8e9d78b8cc --- /dev/null +++ b/servo/components/style/properties_and_values/value.rs @@ -0,0 +1,626 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsing for registered custom properties. + +use std::fmt::{self, Write}; + +use super::{ + registry::PropertyRegistrationData, + syntax::{ + data_type::DataType, Component as SyntaxComponent, ComponentName, Descriptor, Multiplier, + }, +}; +use crate::custom_properties::ComputedValue as ComputedPropertyValue; +use crate::parser::{Parse, ParserContext}; +use crate::properties; +use crate::stylesheets::{CssRuleType, Origin, UrlExtraData}; +use crate::values::{ + animated::{self, Animate, Procedure}, + computed::{self, ToComputedValue}, + specified, CustomIdent, +}; +use cssparser::{BasicParseErrorKind, ParseErrorKind, Parser as CSSParser, TokenSerializationType}; +use selectors::matching::QuirksMode; +use servo_arc::Arc; +use smallvec::SmallVec; +use style_traits::{ + owned_str::OwnedStr, CssWriter, ParseError as StyleParseError, ParsingMode, + PropertySyntaxParseError, StyleParseErrorKind, ToCss, +}; + +/// A single component of the computed value. +pub type ComputedValueComponent = GenericValueComponent< + computed::Length, + computed::Number, + computed::Percentage, + computed::LengthPercentage, + computed::Color, + computed::Image, + computed::url::ComputedUrl, + computed::Integer, + computed::Angle, + computed::Time, + computed::Resolution, + computed::Transform, +>; + +/// A single component of the specified value. +pub type SpecifiedValueComponent = GenericValueComponent< + specified::Length, + specified::Number, + specified::Percentage, + specified::LengthPercentage, + specified::Color, + specified::Image, + specified::url::SpecifiedUrl, + specified::Integer, + specified::Angle, + specified::Time, + specified::Resolution, + specified::Transform, +>; + +impl<L, N, P, LP, C, Image, U, Integer, A, T, R, Transform> + GenericValueComponent<L, N, P, LP, C, Image, U, Integer, A, T, R, Transform> +{ + fn serialization_types(&self) -> (TokenSerializationType, TokenSerializationType) { + let first_token_type = match self { + Self::Length(_) | Self::Angle(_) | Self::Time(_) | Self::Resolution(_) => { + TokenSerializationType::Dimension + }, + Self::Number(_) | Self::Integer(_) => TokenSerializationType::Number, + Self::Percentage(_) | Self::LengthPercentage(_) => TokenSerializationType::Percentage, + Self::Color(_) | + Self::Image(_) | + Self::Url(_) | + Self::TransformFunction(_) | + Self::TransformList(_) => TokenSerializationType::Function, + Self::CustomIdent(_) => TokenSerializationType::Ident, + Self::String(_) => TokenSerializationType::Other, + }; + let last_token_type = if first_token_type == TokenSerializationType::Function { + TokenSerializationType::Other + } else { + first_token_type + }; + (first_token_type, last_token_type) + } +} + +/// A generic enum used for both specified value components and computed value components. +#[derive(Animate, Clone, ToCss, ToComputedValue, Debug, MallocSizeOf, PartialEq)] +#[animation(no_bound(Image, Url))] +pub enum GenericValueComponent< + Length, + Number, + Percentage, + LengthPercentage, + Color, + Image, + Url, + Integer, + Angle, + Time, + Resolution, + TransformFunction, +> { + /// A <length> value + Length(Length), + /// A <number> value + Number(Number), + /// A <percentage> value + Percentage(Percentage), + /// A <length-percentage> value + LengthPercentage(LengthPercentage), + /// A <color> value + Color(Color), + /// An <image> value + #[animation(error)] + Image(Image), + /// A <url> value + #[animation(error)] + Url(Url), + /// An <integer> value + Integer(Integer), + /// An <angle> value + Angle(Angle), + /// A <time> value + Time(Time), + /// A <resolution> value + Resolution(Resolution), + /// A <transform-function> value + TransformFunction(TransformFunction), + /// A <custom-ident> value + #[animation(error)] + CustomIdent(CustomIdent), + /// A <transform-list> value, equivalent to <transform-function>+ + TransformList(ComponentList<Self>), + /// A <string> value + #[animation(error)] + String(OwnedStr), +} + +/// A list of component values, including the list's multiplier. +#[derive(Clone, ToComputedValue, Debug, MallocSizeOf, PartialEq)] +pub struct ComponentList<Component> { + /// Multiplier + pub multiplier: Multiplier, + /// The list of components contained. + pub components: crate::OwnedSlice<Component>, +} + +impl<Component: Animate> Animate for ComponentList<Component> { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.multiplier != other.multiplier { + return Err(()); + } + let components = animated::lists::by_computed_value::animate(&self.components, &other.components, procedure)?; + Ok(Self { + multiplier: self.multiplier, + components, + }) + } +} + +impl<Component: ToCss> ToCss for ComponentList<Component> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let mut iter = self.components.iter(); + let Some(first) = iter.next() else { + return Ok(()); + }; + first.to_css(dest)?; + + // The separator implied by the multiplier for this list. + let separator = match self.multiplier { + // <https://drafts.csswg.org/cssom-1/#serialize-a-whitespace-separated-list> + Multiplier::Space => " ", + // <https://drafts.csswg.org/cssom-1/#serialize-a-comma-separated-list> + Multiplier::Comma => ", ", + }; + for component in iter { + dest.write_str(separator)?; + component.to_css(dest)?; + } + Ok(()) + } +} + +/// A specified registered custom property value. +#[derive(Animate, ToComputedValue, ToCss, Clone, Debug, MallocSizeOf, PartialEq)] +pub enum Value<Component> { + /// A single specified component value whose syntax descriptor component did not have a + /// multiplier. + Component(Component), + /// A specified value whose syntax descriptor was the universal syntax definition. + #[animation(error)] + Universal(#[ignore_malloc_size_of = "Arc"] Arc<ComputedPropertyValue>), + /// A list of specified component values whose syntax descriptor component had a multiplier. + List(#[animation(field_bound)] ComponentList<Component>), +} + +/// Specified custom property value. +pub type SpecifiedValue = Value<SpecifiedValueComponent>; + +/// Computed custom property value. +pub type ComputedValue = Value<ComputedValueComponent>; + +impl SpecifiedValue { + /// Convert a Computed custom property value to a VariableValue. + pub fn compute<'i, 't>( + input: &mut CSSParser<'i, 't>, + registration: &PropertyRegistrationData, + url_data: &UrlExtraData, + context: &computed::Context, + allow_computationally_dependent: AllowComputationallyDependent, + ) -> Result<ComputedPropertyValue, ()> { + let value = Self::get_computed_value( + input, + registration, + url_data, + context, + allow_computationally_dependent, + )?; + Ok(value.to_variable_value(url_data)) + } + + /// Convert a registered custom property to a Computed custom property value, given input and a + /// property registration. + fn get_computed_value<'i, 't>( + input: &mut CSSParser<'i, 't>, + registration: &PropertyRegistrationData, + url_data: &UrlExtraData, + context: &computed::Context, + allow_computationally_dependent: AllowComputationallyDependent, + ) -> Result<ComputedValue, ()> { + debug_assert!(!registration.syntax.is_universal(), "Shouldn't be needed"); + let Ok(value) = Self::parse( + input, + ®istration.syntax, + url_data, + allow_computationally_dependent, + ) else { + return Err(()); + }; + + Ok(value.to_computed_value(context)) + } + + /// Parse and validate a registered custom property value according to its syntax descriptor, + /// and check for computational independence. + pub fn parse<'i, 't>( + mut input: &mut CSSParser<'i, 't>, + syntax: &Descriptor, + url_data: &UrlExtraData, + allow_computationally_dependent: AllowComputationallyDependent, + ) -> Result<Self, StyleParseError<'i>> { + if syntax.is_universal() { + return Ok(Self::Universal(Arc::new(ComputedPropertyValue::parse( + &mut input, url_data, + )?))); + } + + let mut values = SmallComponentVec::new(); + let mut multiplier = None; + { + let mut parser = Parser::new(syntax, &mut values, &mut multiplier); + parser.parse(&mut input, url_data, allow_computationally_dependent)?; + } + let computed_value = if let Some(multiplier) = multiplier { + Self::List(ComponentList { + multiplier, + components: values.to_vec().into(), + }) + } else { + Self::Component(values[0].clone()) + }; + Ok(computed_value) + } +} + +impl ComputedValue { + fn serialization_types(&self) -> (TokenSerializationType, TokenSerializationType) { + match self { + Self::Component(component) => component.serialization_types(), + Self::Universal(_) => unreachable!(), + Self::List(list) => list + .components + .first() + .map_or(Default::default(), |f| f.serialization_types()), + } + } + + fn to_declared_value(&self, url_data: &UrlExtraData) -> Arc<ComputedPropertyValue> { + if let Self::Universal(var) = self { + return Arc::clone(var); + } + Arc::new(self.to_variable_value(url_data)) + } + + fn to_variable_value(&self, url_data: &UrlExtraData) -> ComputedPropertyValue { + debug_assert!(!matches!(self, Self::Universal(..)), "Shouldn't be needed"); + // TODO(zrhoffman, 1864736): Preserve the computed type instead of converting back to a + // string. + let serialization_types = self.serialization_types(); + ComputedPropertyValue::new( + self.to_css_string(), + url_data, + serialization_types.0, + serialization_types.1, + ) + } +} + +/// Whether the computed value parsing should allow computationaly dependent values like 3em or +/// var(-foo). +/// +/// https://drafts.css-houdini.org/css-properties-values-api-1/#computationally-independent +pub enum AllowComputationallyDependent { + /// Only computationally independent values are allowed. + No, + /// Computationally independent and dependent values are allowed. + Yes, +} + +type SmallComponentVec = SmallVec<[SpecifiedValueComponent; 1]>; + +struct Parser<'a> { + syntax: &'a Descriptor, + output: &'a mut SmallComponentVec, + output_multiplier: &'a mut Option<Multiplier>, +} + +impl<'a> Parser<'a> { + fn new( + syntax: &'a Descriptor, + output: &'a mut SmallComponentVec, + output_multiplier: &'a mut Option<Multiplier>, + ) -> Self { + Self { + syntax, + output, + output_multiplier, + } + } + + fn parse<'i, 't>( + &mut self, + input: &mut CSSParser<'i, 't>, + url_data: &UrlExtraData, + allow_computationally_dependent: AllowComputationallyDependent, + ) -> Result<(), StyleParseError<'i>> { + use self::AllowComputationallyDependent::*; + let parsing_mode = match allow_computationally_dependent { + No => ParsingMode::DISALLOW_FONT_RELATIVE, + Yes => ParsingMode::DEFAULT, + }; + let ref context = ParserContext::new( + Origin::Author, + url_data, + Some(CssRuleType::Style), + parsing_mode, + QuirksMode::NoQuirks, + /* namespaces = */ Default::default(), + None, + None, + ); + for component in self.syntax.components.iter() { + let result = input.try_parse(|input| { + input.parse_entirely(|input| { + Self::parse_value(context, input, &component.unpremultiplied()) + }) + }); + let Ok(values) = result else { continue }; + self.output.extend(values); + *self.output_multiplier = component.multiplier(); + break; + } + if self.output.is_empty() { + return Err(input.new_error(BasicParseErrorKind::EndOfInput)); + } + Ok(()) + } + + fn parse_value<'i, 't>( + context: &ParserContext, + input: &mut CSSParser<'i, 't>, + component: &SyntaxComponent, + ) -> Result<SmallComponentVec, StyleParseError<'i>> { + let mut values = SmallComponentVec::new(); + values.push(Self::parse_component_without_multiplier( + context, input, component, + )?); + + if let Some(multiplier) = component.multiplier() { + loop { + let result = Self::expect_multiplier(input, &multiplier); + if Self::expect_multiplier_yielded_eof_error(&result) { + break; + } + result?; + values.push(Self::parse_component_without_multiplier( + context, input, component, + )?); + } + } + Ok(values) + } + + fn parse_component_without_multiplier<'i, 't>( + context: &ParserContext, + input: &mut CSSParser<'i, 't>, + component: &SyntaxComponent, + ) -> Result<SpecifiedValueComponent, StyleParseError<'i>> { + let data_type = match component.name() { + ComponentName::DataType(ty) => ty, + ComponentName::Ident(ref name) => { + let ident = CustomIdent::parse(input, &[])?; + if ident != *name { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(SpecifiedValueComponent::CustomIdent(ident)); + }, + }; + + let value = match data_type { + DataType::Length => { + SpecifiedValueComponent::Length(specified::Length::parse(context, input)?) + }, + DataType::Number => { + SpecifiedValueComponent::Number(specified::Number::parse(context, input)?) + }, + DataType::Percentage => { + SpecifiedValueComponent::Percentage(specified::Percentage::parse(context, input)?) + }, + DataType::LengthPercentage => SpecifiedValueComponent::LengthPercentage( + specified::LengthPercentage::parse(context, input)?, + ), + DataType::Color => { + SpecifiedValueComponent::Color(specified::Color::parse(context, input)?) + }, + DataType::Image => { + SpecifiedValueComponent::Image(specified::Image::parse(context, input)?) + }, + DataType::Url => { + SpecifiedValueComponent::Url(specified::url::SpecifiedUrl::parse(context, input)?) + }, + DataType::Integer => { + SpecifiedValueComponent::Integer(specified::Integer::parse(context, input)?) + }, + DataType::Angle => { + SpecifiedValueComponent::Angle(specified::Angle::parse(context, input)?) + }, + DataType::Time => { + SpecifiedValueComponent::Time(specified::Time::parse(context, input)?) + }, + DataType::Resolution => { + SpecifiedValueComponent::Resolution(specified::Resolution::parse(context, input)?) + }, + DataType::TransformFunction => SpecifiedValueComponent::TransformFunction( + specified::Transform::parse(context, input)?, + ), + DataType::CustomIdent => { + let name = CustomIdent::parse(input, &[])?; + SpecifiedValueComponent::CustomIdent(name) + }, + DataType::TransformList => { + let mut values = vec![]; + let Some(multiplier) = component.unpremultiplied().multiplier() else { + debug_assert!(false, "Unpremultiplied <transform-list> had no multiplier?"); + return Err( + input.new_custom_error(StyleParseErrorKind::PropertySyntaxField( + PropertySyntaxParseError::UnexpectedEOF, + )), + ); + }; + debug_assert_matches!(multiplier, Multiplier::Space); + loop { + values.push(SpecifiedValueComponent::TransformFunction( + specified::Transform::parse(context, input)?, + )); + let result = Self::expect_multiplier(input, &multiplier); + if Self::expect_multiplier_yielded_eof_error(&result) { + break; + } + result?; + } + let list = ComponentList { + multiplier, + components: values.into(), + }; + SpecifiedValueComponent::TransformList(list) + }, + DataType::String => { + let string = input.expect_string()?; + SpecifiedValueComponent::String(string.as_ref().to_owned().into()) + }, + }; + Ok(value) + } + + fn expect_multiplier_yielded_eof_error<'i>(result: &Result<(), StyleParseError<'i>>) -> bool { + matches!( + result, + Err(StyleParseError { + kind: ParseErrorKind::Basic(BasicParseErrorKind::EndOfInput), + .. + }) + ) + } + + fn expect_multiplier<'i, 't>( + input: &mut CSSParser<'i, 't>, + multiplier: &Multiplier, + ) -> Result<(), StyleParseError<'i>> { + match multiplier { + Multiplier::Space => { + input.expect_whitespace()?; + if input.is_exhausted() { + // If there was trailing whitespace, do not interpret it as a multiplier + return Err(input.new_error(BasicParseErrorKind::EndOfInput)); + } + Ok(()) + }, + Multiplier::Comma => Ok(input.expect_comma()?), + } + } +} + + +/// An animated value for custom property. +#[derive(Clone, Debug, MallocSizeOf, PartialEq)] +pub struct CustomAnimatedValue { + /// The name of the custom property. + pub(crate) name: crate::custom_properties::Name, + /// The computed value of the custom property. + value: ComputedValue, + /// The url data where the value came from. + /// FIXME: This seems like it should not be needed: registered properties don't need it, and + /// unregistered properties animate discretely. But we need it so far because the computed + /// value representation isn't typed. + url_data: UrlExtraData, +} + +impl Animate for CustomAnimatedValue { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.name != other.name { + return Err(()) + } + let value = self.value.animate(&other.value, procedure)?; + Ok(Self { + name: self.name.clone(), + value, + // NOTE: This is sketchy AF, but it's ~fine, since values that can animate (non-universal) + // don't need it. + url_data: self.url_data.clone(), + }) + } +} + +impl CustomAnimatedValue { + pub(crate) fn from_computed( + name: &crate::custom_properties::Name, + value: &Arc<ComputedPropertyValue>, + ) -> Self { + Self { + name: name.clone(), + // FIXME: Should probably preserve type-ness in ComputedPropertyValue. + value: ComputedValue::Universal(value.clone()), + url_data: value.url_data.clone(), + } + } + + pub(crate) fn from_declaration( + declaration: &properties::CustomDeclaration, + context: &mut computed::Context, + _initial: &properties::ComputedValues, + ) -> Option<Self> { + let value = match declaration.value { + properties::CustomDeclarationValue::Value(ref v) => v, + // FIXME: This should be made to work to the extent possible like for non-custom + // properties (using `initial` at least to handle unset / inherit). + properties::CustomDeclarationValue::CSSWideKeyword(..) => return None, + }; + + debug_assert!( + context.builder.stylist.is_some(), + "Need a Stylist to get property registration!" + ); + let registration = + context.builder.stylist.unwrap().get_custom_property_registration(&declaration.name); + + // FIXME: Do we need to perform substitution here somehow? + let computed_value = if registration.syntax.is_universal() { + None + } else { + let mut input = cssparser::ParserInput::new(&value.css); + let mut input = CSSParser::new(&mut input); + SpecifiedValue::get_computed_value( + &mut input, + registration, + &value.url_data, + context, + AllowComputationallyDependent::Yes, + ).ok() + }; + + let url_data = value.url_data.clone(); + let value = computed_value.unwrap_or_else(|| ComputedValue::Universal(Arc::clone(value))); + Some(Self { + name: declaration.name.clone(), + url_data, + value, + }) + } + + pub(crate) fn to_declaration(&self) -> properties::PropertyDeclaration { + properties::PropertyDeclaration::Custom(properties::CustomDeclaration { + name: self.name.clone(), + value: properties::CustomDeclarationValue::Value(self.value.to_declared_value(&self.url_data)), + }) + } +} diff --git a/servo/components/style/queries/condition.rs b/servo/components/style/queries/condition.rs new file mode 100644 index 0000000000..e17e6abd2e --- /dev/null +++ b/servo/components/style/queries/condition.rs @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A query condition: +//! +//! https://drafts.csswg.org/mediaqueries-4/#typedef-media-condition +//! https://drafts.csswg.org/css-contain-3/#typedef-container-condition + +use super::{FeatureFlags, FeatureType, QueryFeatureExpression}; +use crate::values::computed; +use crate::{error_reporting::ContextualParseError, parser::ParserContext}; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A binary `and` or `or` operator. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum Operator { + And, + Or, +} + +/// Whether to allow an `or` condition or not during parsing. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)] +enum AllowOr { + Yes, + No, +} + +/// https://en.wikipedia.org/wiki/Three-valued_logic#Kleene_and_Priest_logics +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss)] +pub enum KleeneValue { + /// False + False = 0, + /// True + True = 1, + /// Either true or false, but we’re not sure which yet. + Unknown, +} + +impl From<bool> for KleeneValue { + fn from(b: bool) -> Self { + if b { + Self::True + } else { + Self::False + } + } +} + +impl KleeneValue { + /// Turns this Kleene value to a bool, taking the unknown value as an + /// argument. + pub fn to_bool(self, unknown: bool) -> bool { + match self { + Self::True => true, + Self::False => false, + Self::Unknown => unknown, + } + } +} + +impl std::ops::Not for KleeneValue { + type Output = Self; + + fn not(self) -> Self { + match self { + Self::True => Self::False, + Self::False => Self::True, + Self::Unknown => Self::Unknown, + } + } +} + +// Implements the logical and operation. +impl std::ops::BitAnd for KleeneValue { + type Output = Self; + + fn bitand(self, other: Self) -> Self { + if self == Self::False || other == Self::False { + return Self::False; + } + if self == Self::Unknown || other == Self::Unknown { + return Self::Unknown; + } + Self::True + } +} + +// Implements the logical or operation. +impl std::ops::BitOr for KleeneValue { + type Output = Self; + + fn bitor(self, other: Self) -> Self { + if self == Self::True || other == Self::True { + return Self::True; + } + if self == Self::Unknown || other == Self::Unknown { + return Self::Unknown; + } + Self::False + } +} + +impl std::ops::BitOrAssign for KleeneValue { + fn bitor_assign(&mut self, other: Self) { + *self = *self | other; + } +} + +impl std::ops::BitAndAssign for KleeneValue { + fn bitand_assign(&mut self, other: Self) { + *self = *self & other; + } +} + +/// Represents a condition. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum QueryCondition { + /// A simple feature expression, implicitly parenthesized. + Feature(QueryFeatureExpression), + /// A negation of a condition. + Not(Box<QueryCondition>), + /// A set of joint operations. + Operation(Box<[QueryCondition]>, Operator), + /// A condition wrapped in parenthesis. + InParens(Box<QueryCondition>), + /// [ <function-token> <any-value>? ) ] | [ ( <any-value>? ) ] + GeneralEnclosed(String), +} + +impl ToCss for QueryCondition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + match *self { + // NOTE(emilio): QueryFeatureExpression already includes the + // parenthesis. + QueryCondition::Feature(ref f) => f.to_css(dest), + QueryCondition::Not(ref c) => { + dest.write_str("not ")?; + c.to_css(dest) + }, + QueryCondition::InParens(ref c) => { + dest.write_char('(')?; + c.to_css(dest)?; + dest.write_char(')') + }, + QueryCondition::Operation(ref list, op) => { + let mut iter = list.iter(); + iter.next().unwrap().to_css(dest)?; + for item in iter { + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + item.to_css(dest)?; + } + Ok(()) + }, + QueryCondition::GeneralEnclosed(ref s) => dest.write_str(&s), + } + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value> +fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + input.expect_no_error_token().map_err(Into::into) +} + +impl QueryCondition { + /// Parse a single condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, feature_type, AllowOr::Yes) + } + + fn visit<F>(&self, visitor: &mut F) + where + F: FnMut(&Self), + { + visitor(self); + match *self { + Self::Feature(..) => {}, + Self::GeneralEnclosed(..) => {}, + Self::Not(ref cond) => cond.visit(visitor), + Self::Operation(ref conds, _op) => { + for cond in conds.iter() { + cond.visit(visitor); + } + }, + Self::InParens(ref cond) => cond.visit(visitor), + } + } + + /// Returns the union of all flags in the expression. This is useful for + /// container queries. + pub fn cumulative_flags(&self) -> FeatureFlags { + let mut result = FeatureFlags::empty(); + self.visit(&mut |condition| { + if let Self::Feature(ref f) = condition { + result.insert(f.feature_flags()) + } + }); + result + } + + /// Parse a single condition, disallowing `or` expressions. + /// + /// To be used from the legacy query syntax. + pub fn parse_disallow_or<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, feature_type, AllowOr::No) + } + + /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition or + /// https://drafts.csswg.org/mediaqueries-5/#typedef-media-condition-without-or + /// (depending on `allow_or`). + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + allow_or: AllowOr, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() { + let inner_condition = Self::parse_in_parens(context, input, feature_type)?; + return Ok(QueryCondition::Not(Box::new(inner_condition))); + } + + let first_condition = Self::parse_in_parens(context, input, feature_type)?; + let operator = match input.try_parse(Operator::parse) { + Ok(op) => op, + Err(..) => return Ok(first_condition), + }; + + if allow_or == AllowOr::No && operator == Operator::Or { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut conditions = vec![]; + conditions.push(first_condition); + conditions.push(Self::parse_in_parens(context, input, feature_type)?); + + let delim = match operator { + Operator::And => "and", + Operator::Or => "or", + }; + + loop { + if input.try_parse(|i| i.expect_ident_matching(delim)).is_err() { + return Ok(QueryCondition::Operation( + conditions.into_boxed_slice(), + operator, + )); + } + + conditions.push(Self::parse_in_parens(context, input, feature_type)?); + } + } + + fn parse_in_parenthesis_block<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + // Base case. Make sure to preserve this error as it's more generally + // relevant. + let feature_error = match input.try_parse(|input| { + QueryFeatureExpression::parse_in_parenthesis_block(context, input, feature_type) + }) { + Ok(expr) => return Ok(Self::Feature(expr)), + Err(e) => e, + }; + if let Ok(inner) = Self::parse(context, input, feature_type) { + return Ok(Self::InParens(Box::new(inner))); + } + Err(feature_error) + } + + /// Parse a condition in parentheses, or `<general-enclosed>`. + /// + /// https://drafts.csswg.org/mediaqueries/#typedef-media-in-parens + pub fn parse_in_parens<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + input.skip_whitespace(); + let start = input.position(); + let start_location = input.current_source_location(); + match *input.next()? { + Token::ParenthesisBlock => { + let nested = input.try_parse(|input| { + input.parse_nested_block(|input| { + Self::parse_in_parenthesis_block(context, input, feature_type) + }) + }); + match nested { + Ok(nested) => return Ok(nested), + Err(e) => { + // We're about to swallow the error in a `<general-enclosed>` + // condition, so report it while we can. + let loc = e.location; + let error = + ContextualParseError::InvalidMediaRule(input.slice_from(start), e); + context.log_css_error(loc, error); + }, + } + }, + Token::Function(..) => { + // TODO: handle `style()` queries, etc. + }, + ref t => return Err(start_location.new_unexpected_token_error(t.clone())), + } + input.parse_nested_block(consume_any_value)?; + Ok(Self::GeneralEnclosed(input.slice_from(start).to_owned())) + } + + /// Whether this condition matches the device and quirks mode. + /// https://drafts.csswg.org/mediaqueries/#evaluating + /// https://drafts.csswg.org/mediaqueries/#typedef-general-enclosed + /// Kleene 3-valued logic is adopted here due to the introduction of + /// <general-enclosed>. + pub fn matches(&self, context: &computed::Context) -> KleeneValue { + match *self { + QueryCondition::Feature(ref f) => f.matches(context), + QueryCondition::GeneralEnclosed(_) => KleeneValue::Unknown, + QueryCondition::InParens(ref c) => c.matches(context), + QueryCondition::Not(ref c) => !c.matches(context), + QueryCondition::Operation(ref conditions, op) => { + debug_assert!(!conditions.is_empty(), "We never create an empty op"); + match op { + Operator::And => { + let mut result = KleeneValue::True; + for c in conditions.iter() { + result &= c.matches(context); + if result == KleeneValue::False { + break; + } + } + result + }, + Operator::Or => { + let mut result = KleeneValue::False; + for c in conditions.iter() { + result |= c.matches(context); + if result == KleeneValue::True { + break; + } + } + result + }, + } + }, + } + } +} diff --git a/servo/components/style/queries/feature.rs b/servo/components/style/queries/feature.rs new file mode 100644 index 0000000000..83ff7e7522 --- /dev/null +++ b/servo/components/style/queries/feature.rs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Query features. + +use super::condition::KleeneValue; +use crate::parser::ParserContext; +use crate::values::computed::{self, CSSPixelLength, Ratio, Resolution}; +use crate::values::AtomString; +use crate::Atom; +use cssparser::Parser; +use std::fmt; +use style_traits::ParseError; + +/// A generic discriminant for an enum value. +pub type KeywordDiscriminant = u8; + +type QueryFeatureGetter<T> = fn(device: &computed::Context) -> T; + +/// Serializes a given discriminant. +/// +/// FIXME(emilio): we could prevent this allocation if the ToCss code would +/// generate a method for keywords to get the static string or something. +pub type KeywordSerializer = fn(KeywordDiscriminant) -> String; + +/// Parses a given identifier. +pub type KeywordParser = for<'a, 'i, 't> fn( + context: &'a ParserContext, + input: &'a mut Parser<'i, 't>, +) -> Result<KeywordDiscriminant, ParseError<'i>>; + +/// An evaluator for a given feature. +/// +/// This determines the kind of values that get parsed, too. +#[allow(missing_docs)] +pub enum Evaluator { + Length(QueryFeatureGetter<CSSPixelLength>), + OptionalLength(QueryFeatureGetter<Option<CSSPixelLength>>), + Integer(QueryFeatureGetter<i32>), + Float(QueryFeatureGetter<f32>), + BoolInteger(QueryFeatureGetter<bool>), + /// A non-negative number ratio, such as the one from device-pixel-ratio. + NumberRatio(QueryFeatureGetter<Ratio>), + OptionalNumberRatio(QueryFeatureGetter<Option<Ratio>>), + /// A resolution. + Resolution(QueryFeatureGetter<Resolution>), + String(fn(&computed::Context, Option<&AtomString>) -> KleeneValue), + /// A keyword value. + Enumerated { + /// The parser to get a discriminant given a string. + parser: KeywordParser, + /// The serializer to get a string from a discriminant. + /// + /// This is guaranteed to be called with a keyword that `parser` has + /// produced. + serializer: KeywordSerializer, + /// The evaluator itself. This is guaranteed to be called with a + /// keyword that `parser` has produced. + evaluator: fn(&computed::Context, Option<KeywordDiscriminant>) -> KleeneValue, + }, +} + +/// A simple helper macro to create a keyword evaluator. +/// +/// This assumes that keyword feature expressions don't accept ranges, and +/// asserts if that's not true. As of today there's nothing like that (does that +/// even make sense?). +macro_rules! keyword_evaluator { + ($actual_evaluator:ident, $keyword_type:ty) => {{ + fn __parse<'i, 't>( + context: &$crate::parser::ParserContext, + input: &mut $crate::cssparser::Parser<'i, 't>, + ) -> Result<$crate::queries::feature::KeywordDiscriminant, ::style_traits::ParseError<'i>> + { + let kw = <$keyword_type as $crate::parser::Parse>::parse(context, input)?; + Ok(kw as $crate::queries::feature::KeywordDiscriminant) + } + + fn __serialize(kw: $crate::queries::feature::KeywordDiscriminant) -> String { + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: $keyword_type = ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap(); + <$keyword_type as ::style_traits::ToCss>::to_css_string(&value) + } + + fn __evaluate( + context: &$crate::values::computed::Context, + value: Option<$crate::queries::feature::KeywordDiscriminant>, + ) -> $crate::queries::condition::KleeneValue { + // This unwrap is ok because the only discriminants that get + // back to us is the ones that `parse` produces. + let value: Option<$keyword_type> = + value.map(|kw| ::num_traits::cast::FromPrimitive::from_u8(kw).unwrap()); + $crate::queries::condition::KleeneValue::from($actual_evaluator(context, value)) + } + + $crate::queries::feature::Evaluator::Enumerated { + parser: __parse, + serializer: __serialize, + evaluator: __evaluate, + } + }}; +} + +/// Different flags or toggles that change how a expression is parsed or +/// evaluated. +#[derive(Clone, Copy, Debug, ToShmem)] +pub struct FeatureFlags(u8); +bitflags! { + impl FeatureFlags : u8 { + /// The feature should only be parsed in chrome and ua sheets. + const CHROME_AND_UA_ONLY = 1 << 0; + /// The feature requires a -webkit- prefix. + const WEBKIT_PREFIX = 1 << 1; + /// The feature requires the inline-axis containment. + const CONTAINER_REQUIRES_INLINE_AXIS = 1 << 2; + /// The feature requires the block-axis containment. + const CONTAINER_REQUIRES_BLOCK_AXIS = 1 << 3; + /// The feature requires containment in the physical width axis. + const CONTAINER_REQUIRES_WIDTH_AXIS = 1 << 4; + /// The feature requires containment in the physical height axis. + const CONTAINER_REQUIRES_HEIGHT_AXIS = 1 << 5; + /// The feature evaluation depends on the viewport size. + const VIEWPORT_DEPENDENT = 1 << 6; + } +} + +impl FeatureFlags { + /// Returns parsing requirement flags. + pub fn parsing_requirements(self) -> Self { + self.intersection(Self::CHROME_AND_UA_ONLY | Self::WEBKIT_PREFIX) + } + + /// Returns all the container axis flags. + pub fn all_container_axes() -> Self { + Self::CONTAINER_REQUIRES_INLINE_AXIS | + Self::CONTAINER_REQUIRES_BLOCK_AXIS | + Self::CONTAINER_REQUIRES_WIDTH_AXIS | + Self::CONTAINER_REQUIRES_HEIGHT_AXIS + } + + /// Returns our subset of container axis flags. + pub fn container_axes(self) -> Self { + self.intersection(Self::all_container_axes()) + } +} + +/// Whether a feature allows ranges or not. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum AllowsRanges { + Yes, + No, +} + +/// A description of a feature. +pub struct QueryFeatureDescription { + /// The feature name, in ascii lowercase. + pub name: Atom, + /// Whether min- / max- prefixes are allowed or not. + pub allows_ranges: AllowsRanges, + /// The evaluator, which we also use to determine which kind of value to + /// parse. + pub evaluator: Evaluator, + /// Different feature-specific flags. + pub flags: FeatureFlags, +} + +impl QueryFeatureDescription { + /// Whether this feature allows ranges. + #[inline] + pub fn allows_ranges(&self) -> bool { + self.allows_ranges == AllowsRanges::Yes + } +} + +/// A simple helper to construct a `QueryFeatureDescription`. +macro_rules! feature { + ($name:expr, $allows_ranges:expr, $evaluator:expr, $flags:expr,) => { + $crate::queries::feature::QueryFeatureDescription { + name: $name, + allows_ranges: $allows_ranges, + evaluator: $evaluator, + flags: $flags, + } + }; +} + +impl fmt::Debug for QueryFeatureDescription { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("QueryFeatureDescription") + .field("name", &self.name) + .field("allows_ranges", &self.allows_ranges) + .field("flags", &self.flags) + .finish() + } +} diff --git a/servo/components/style/queries/feature_expression.rs b/servo/components/style/queries/feature_expression.rs new file mode 100644 index 0000000000..c0171c2058 --- /dev/null +++ b/servo/components/style/queries/feature_expression.rs @@ -0,0 +1,764 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsing for query feature expressions, like `(foo: bar)` or +//! `(width >= 400px)`. + +use super::feature::{Evaluator, QueryFeatureDescription}; +use super::feature::{FeatureFlags, KeywordDiscriminant}; +use crate::parser::{Parse, ParserContext}; +use crate::queries::condition::KleeneValue; +use crate::str::{starts_with_ignore_ascii_case, string_as_ascii_lowercase}; +use crate::values::computed::{self, Ratio, ToComputedValue}; +use crate::values::specified::{Integer, Length, Number, Resolution}; +use crate::values::{AtomString, CSSFloat}; +use crate::{Atom, Zero}; +use cssparser::{Parser, Token}; +use std::cmp::{Ordering, PartialOrd}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// Whether we're parsing a media or container query feature. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum FeatureType { + /// We're parsing a media feature. + Media, + /// We're parsing a container feature. + Container, +} + +impl FeatureType { + fn features(&self) -> &'static [QueryFeatureDescription] { + #[cfg(feature = "gecko")] + use crate::gecko::media_features::MEDIA_FEATURES; + #[cfg(feature = "servo")] + use crate::servo::media_queries::MEDIA_FEATURES; + + use crate::stylesheets::container_rule::CONTAINER_FEATURES; + + match *self { + FeatureType::Media => &MEDIA_FEATURES, + FeatureType::Container => &CONTAINER_FEATURES, + } + } + + fn find_feature(&self, name: &Atom) -> Option<(usize, &'static QueryFeatureDescription)> { + self.features() + .iter() + .enumerate() + .find(|(_, f)| f.name == *name) + } +} + +/// The kind of matching that should be performed on a feature value. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +enum LegacyRange { + /// At least the specified value. + Min, + /// At most the specified value. + Max, +} + +/// The operator that was specified in this feature. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +enum Operator { + /// = + Equal, + /// > + GreaterThan, + /// >= + GreaterThanEqual, + /// < + LessThan, + /// <= + LessThanEqual, +} + +impl ToCss for Operator { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + dest.write_str(match *self { + Self::Equal => "=", + Self::LessThan => "<", + Self::LessThanEqual => "<=", + Self::GreaterThan => ">", + Self::GreaterThanEqual => ">=", + }) + } +} + +impl Operator { + fn is_compatible_with(self, right_op: Self) -> bool { + // Some operators are not compatible with each other in multi-range + // context. + match self { + Self::Equal => false, + Self::GreaterThan | Self::GreaterThanEqual => { + matches!(right_op, Self::GreaterThan | Self::GreaterThanEqual) + }, + Self::LessThan | Self::LessThanEqual => { + matches!(right_op, Self::LessThan | Self::LessThanEqual) + }, + } + } + + fn evaluate(&self, cmp: Ordering) -> bool { + match *self { + Self::Equal => cmp == Ordering::Equal, + Self::GreaterThan => cmp == Ordering::Greater, + Self::GreaterThanEqual => cmp == Ordering::Equal || cmp == Ordering::Greater, + Self::LessThan => cmp == Ordering::Less, + Self::LessThanEqual => cmp == Ordering::Equal || cmp == Ordering::Less, + } + } + + fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let operator = match *input.next()? { + Token::Delim('=') => return Ok(Operator::Equal), + Token::Delim('>') => Operator::GreaterThan, + Token::Delim('<') => Operator::LessThan, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + + // https://drafts.csswg.org/mediaqueries-4/#mq-syntax: + // + // No whitespace is allowed between the “<” or “>” + // <delim-token>s and the following “=” <delim-token>, if it’s + // present. + // + // TODO(emilio): Maybe we should ignore comments as well? + // https://github.com/w3c/csswg-drafts/issues/6248 + let parsed_equal = input + .try_parse(|i| { + let t = i.next_including_whitespace().map_err(|_| ())?; + if !matches!(t, Token::Delim('=')) { + return Err(()); + } + Ok(()) + }) + .is_ok(); + + if !parsed_equal { + return Ok(operator); + } + + Ok(match operator { + Operator::GreaterThan => Operator::GreaterThanEqual, + Operator::LessThan => Operator::LessThanEqual, + _ => unreachable!(), + }) + } +} + +#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)] +enum QueryFeatureExpressionKind { + /// Just the media feature name. + Empty, + + /// A single value. + Single(QueryExpressionValue), + + /// Legacy range syntax (min-*: value) or so. + LegacyRange(LegacyRange, QueryExpressionValue), + + /// Modern range context syntax: + /// https://drafts.csswg.org/mediaqueries-5/#mq-range-context + Range { + left: Option<(Operator, QueryExpressionValue)>, + right: Option<(Operator, QueryExpressionValue)>, + }, +} + +impl QueryFeatureExpressionKind { + /// Evaluate a given range given an optional query value and a value from + /// the browser. + fn evaluate<T>( + &self, + context_value: T, + mut compute: impl FnMut(&QueryExpressionValue) -> T, + ) -> bool + where + T: PartialOrd + Zero, + { + match *self { + Self::Empty => return !context_value.is_zero(), + Self::Single(ref value) => { + let value = compute(value); + let cmp = match context_value.partial_cmp(&value) { + Some(c) => c, + None => return false, + }; + cmp == Ordering::Equal + }, + Self::LegacyRange(ref range, ref value) => { + let value = compute(value); + let cmp = match context_value.partial_cmp(&value) { + Some(c) => c, + None => return false, + }; + cmp == Ordering::Equal || + match range { + LegacyRange::Min => cmp == Ordering::Greater, + LegacyRange::Max => cmp == Ordering::Less, + } + }, + Self::Range { + ref left, + ref right, + } => { + debug_assert!(left.is_some() || right.is_some()); + if let Some((ref op, ref value)) = left { + let value = compute(value); + let cmp = match value.partial_cmp(&context_value) { + Some(c) => c, + None => return false, + }; + if !op.evaluate(cmp) { + return false; + } + } + if let Some((ref op, ref value)) = right { + let value = compute(value); + let cmp = match context_value.partial_cmp(&value) { + Some(c) => c, + None => return false, + }; + if !op.evaluate(cmp) { + return false; + } + } + true + }, + } + } + + /// Non-ranged features only need to compare to one value at most. + fn non_ranged_value(&self) -> Option<&QueryExpressionValue> { + match *self { + Self::Empty => None, + Self::Single(ref v) => Some(v), + Self::LegacyRange(..) | Self::Range { .. } => { + debug_assert!(false, "Unexpected ranged value in non-ranged feature!"); + None + }, + } + } +} + +/// A feature expression contains a reference to the feature, the value the +/// query contained, and the range to evaluate. +#[derive(Clone, Debug, MallocSizeOf, ToShmem, PartialEq)] +pub struct QueryFeatureExpression { + feature_type: FeatureType, + feature_index: usize, + kind: QueryFeatureExpressionKind, +} + +impl ToCss for QueryFeatureExpression { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + dest.write_char('(')?; + + match self.kind { + QueryFeatureExpressionKind::Empty => self.write_name(dest)?, + QueryFeatureExpressionKind::Single(ref v) | + QueryFeatureExpressionKind::LegacyRange(_, ref v) => { + self.write_name(dest)?; + dest.write_str(": ")?; + v.to_css(dest, self)?; + }, + QueryFeatureExpressionKind::Range { + ref left, + ref right, + } => { + if let Some((ref op, ref val)) = left { + val.to_css(dest, self)?; + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + } + self.write_name(dest)?; + if let Some((ref op, ref val)) = right { + dest.write_char(' ')?; + op.to_css(dest)?; + dest.write_char(' ')?; + val.to_css(dest, self)?; + } + }, + } + dest.write_char(')') + } +} + +fn consume_operation_or_colon<'i>( + input: &mut Parser<'i, '_>, +) -> Result<Option<Operator>, ParseError<'i>> { + if input.try_parse(|input| input.expect_colon()).is_ok() { + return Ok(None); + } + Operator::parse(input).map(|op| Some(op)) +} + +#[allow(unused_variables)] +fn disabled_by_pref(feature: &Atom, context: &ParserContext) -> bool { + #[cfg(feature = "gecko")] + { + if *feature == atom!("forced-colors") { + // forced-colors is always enabled in the ua and chrome. On + // the web it is hidden behind a preference, which is defaulted + // to 'true' as of bug 1659511. + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.forced-colors.enabled"); + } + // prefers-contrast is always enabled in the ua and chrome. On + // the web it is hidden behind a preference. + if *feature == atom!("prefers-contrast") { + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.prefers-contrast.enabled"); + } + + // prefers-reduced-transparency is always enabled in the ua and chrome. On + // the web it is hidden behind a preference (see Bug 1822176). + if *feature == atom!("prefers-reduced-transparency") { + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.prefers-reduced-transparency.enabled"); + } + + // inverted-colors is always enabled in the ua and chrome. On + // the web it is hidden behind a preferenc. + if *feature == atom!("inverted-colors") { + return !context.chrome_rules_enabled() && + !static_prefs::pref!("layout.css.inverted-colors.enabled"); + } + } + false +} + +impl QueryFeatureExpression { + fn new( + feature_type: FeatureType, + feature_index: usize, + kind: QueryFeatureExpressionKind, + ) -> Self { + debug_assert!(feature_index < feature_type.features().len()); + Self { + feature_type, + feature_index, + kind, + } + } + + fn write_name<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + let feature = self.feature(); + if feature.flags.contains(FeatureFlags::WEBKIT_PREFIX) { + dest.write_str("-webkit-")?; + } + + if let QueryFeatureExpressionKind::LegacyRange(range, _) = self.kind { + match range { + LegacyRange::Min => dest.write_str("min-")?, + LegacyRange::Max => dest.write_str("max-")?, + } + } + + // NB: CssStringWriter not needed, feature names are under control. + write!(dest, "{}", feature.name)?; + + Ok(()) + } + + fn feature(&self) -> &'static QueryFeatureDescription { + &self.feature_type.features()[self.feature_index] + } + + /// Returns the feature flags for our feature. + pub fn feature_flags(&self) -> FeatureFlags { + self.feature().flags + } + + /// Parse a feature expression of the form: + /// + /// ``` + /// (media-feature: media-value) + /// ``` + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + input.expect_parenthesis_block()?; + input.parse_nested_block(|input| { + Self::parse_in_parenthesis_block(context, input, feature_type) + }) + } + + fn parse_feature_name<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<(usize, Option<LegacyRange>), ParseError<'i>> { + let mut flags = FeatureFlags::empty(); + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + if context.chrome_rules_enabled() { + flags.insert(FeatureFlags::CHROME_AND_UA_ONLY); + } + + let mut feature_name = &**ident; + if starts_with_ignore_ascii_case(feature_name, "-webkit-") { + feature_name = &feature_name[8..]; + flags.insert(FeatureFlags::WEBKIT_PREFIX); + } + + let range = if starts_with_ignore_ascii_case(feature_name, "min-") { + feature_name = &feature_name[4..]; + Some(LegacyRange::Min) + } else if starts_with_ignore_ascii_case(feature_name, "max-") { + feature_name = &feature_name[4..]; + Some(LegacyRange::Max) + } else { + None + }; + + let atom = Atom::from(string_as_ascii_lowercase(feature_name)); + let (feature_index, feature) = match feature_type.find_feature(&atom) { + Some((i, f)) => (i, f), + None => { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )) + }, + }; + + if disabled_by_pref(&feature.name, context) || + !flags.contains(feature.flags.parsing_requirements()) || + (range.is_some() && !feature.allows_ranges()) + { + return Err(location.new_custom_error( + StyleParseErrorKind::MediaQueryExpectedFeatureName(ident.clone()), + )); + } + + Ok((feature_index, range)) + } + + /// Parses the following range syntax: + /// + /// (feature-value <operator> feature-name) + /// (feature-value <operator> feature-name <operator> feature-value) + fn parse_multi_range_syntax<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + let start = input.state(); + + // To parse the values, we first need to find the feature name. We rely + // on feature values for ranged features not being able to be top-level + // <ident>s, which holds. + let feature_index = loop { + // NOTE: parse_feature_name advances the input. + if let Ok((index, range)) = Self::parse_feature_name(context, input, feature_type) { + if range.is_some() { + // Ranged names are not allowed here. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + break index; + } + if input.is_exhausted() { + return Err(start + .source_location() + .new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }; + + input.reset(&start); + + let feature = &feature_type.features()[feature_index]; + let left_val = QueryExpressionValue::parse(feature, context, input)?; + let left_op = Operator::parse(input)?; + + { + let (parsed_index, _) = Self::parse_feature_name(context, input, feature_type)?; + debug_assert_eq!( + parsed_index, feature_index, + "How did we find a different feature?" + ); + } + + let right_op = input.try_parse(Operator::parse).ok(); + let right = match right_op { + Some(op) => { + if !left_op.is_compatible_with(op) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Some((op, QueryExpressionValue::parse(feature, context, input)?)) + }, + None => None, + }; + Ok(Self::new( + feature_type, + feature_index, + QueryFeatureExpressionKind::Range { + left: Some((left_op, left_val)), + right, + }, + )) + } + + /// Parse a feature expression where we've already consumed the parenthesis. + pub fn parse_in_parenthesis_block<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + feature_type: FeatureType, + ) -> Result<Self, ParseError<'i>> { + let (feature_index, range) = + match input.try_parse(|input| Self::parse_feature_name(context, input, feature_type)) { + Ok(v) => v, + Err(e) => { + if let Ok(expr) = Self::parse_multi_range_syntax(context, input, feature_type) { + return Ok(expr); + } + return Err(e); + }, + }; + let operator = input.try_parse(consume_operation_or_colon); + let operator = match operator { + Err(..) => { + // If there's no colon, this is a query of the form + // '(<feature>)', that is, there's no value specified. + // + // Gecko doesn't allow ranged expressions without a + // value, so just reject them here too. + if range.is_some() { + return Err( + input.new_custom_error(StyleParseErrorKind::RangedExpressionWithNoValue) + ); + } + + return Ok(Self::new( + feature_type, + feature_index, + QueryFeatureExpressionKind::Empty, + )); + }, + Ok(operator) => operator, + }; + + let feature = &feature_type.features()[feature_index]; + + let value = QueryExpressionValue::parse(feature, context, input).map_err(|err| { + err.location + .new_custom_error(StyleParseErrorKind::MediaQueryExpectedFeatureValue) + })?; + + let kind = match range { + Some(range) => { + if operator.is_some() { + return Err( + input.new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator) + ); + } + QueryFeatureExpressionKind::LegacyRange(range, value) + }, + None => match operator { + Some(operator) => { + if !feature.allows_ranges() { + return Err(input + .new_custom_error(StyleParseErrorKind::MediaQueryUnexpectedOperator)); + } + QueryFeatureExpressionKind::Range { + left: None, + right: Some((operator, value)), + } + }, + None => QueryFeatureExpressionKind::Single(value), + }, + }; + + Ok(Self::new(feature_type, feature_index, kind)) + } + + /// Returns whether this query evaluates to true for the given device. + pub fn matches(&self, context: &computed::Context) -> KleeneValue { + macro_rules! expect { + ($variant:ident, $v:expr) => { + match *$v { + QueryExpressionValue::$variant(ref v) => v, + _ => unreachable!("Unexpected QueryExpressionValue"), + } + }; + } + + KleeneValue::from(match self.feature().evaluator { + Evaluator::Length(eval) => { + let v = eval(context); + self.kind + .evaluate(v, |v| expect!(Length, v).to_computed_value(context)) + }, + Evaluator::OptionalLength(eval) => { + let v = match eval(context) { + Some(v) => v, + None => return KleeneValue::Unknown, + }; + self.kind + .evaluate(v, |v| expect!(Length, v).to_computed_value(context)) + }, + Evaluator::Integer(eval) => { + let v = eval(context); + self.kind.evaluate(v, |v| *expect!(Integer, v)) + }, + Evaluator::Float(eval) => { + let v = eval(context); + self.kind.evaluate(v, |v| *expect!(Float, v)) + }, + Evaluator::NumberRatio(eval) => { + let ratio = eval(context); + // A ratio of 0/0 behaves as the ratio 1/0, so we need to call used_value() + // to convert it if necessary. + // FIXME: we may need to update here once + // https://github.com/w3c/csswg-drafts/issues/4954 got resolved. + self.kind + .evaluate(ratio, |v| expect!(NumberRatio, v).used_value()) + }, + Evaluator::OptionalNumberRatio(eval) => { + let ratio = match eval(context) { + Some(v) => v, + None => return KleeneValue::Unknown, + }; + // See above for subtleties here. + self.kind + .evaluate(ratio, |v| expect!(NumberRatio, v).used_value()) + }, + Evaluator::Resolution(eval) => { + let v = eval(context).dppx(); + self.kind.evaluate(v, |v| { + expect!(Resolution, v).to_computed_value(context).dppx() + }) + }, + Evaluator::Enumerated { evaluator, .. } => { + let computed = self + .kind + .non_ranged_value() + .map(|v| *expect!(Enumerated, v)); + return evaluator(context, computed); + }, + Evaluator::String(evaluator) => { + let string = self.kind.non_ranged_value().map(|v| expect!(String, v)); + return evaluator(context, string); + }, + Evaluator::BoolInteger(eval) => { + let computed = self + .kind + .non_ranged_value() + .map(|v| *expect!(BoolInteger, v)); + let boolean = eval(context); + computed.map_or(boolean, |v| v == boolean) + }, + }) + } +} + +/// A value found or expected in a expression. +/// +/// FIXME(emilio): How should calc() serialize in the Number / Integer / +/// BoolInteger / NumberRatio case, as computed or as specified value? +/// +/// If the first, this would need to store the relevant values. +/// +/// See: https://github.com/w3c/csswg-drafts/issues/1968 +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum QueryExpressionValue { + /// A length. + Length(Length), + /// An integer. + Integer(i32), + /// A floating point value. + Float(CSSFloat), + /// A boolean value, specified as an integer (i.e., either 0 or 1). + BoolInteger(bool), + /// A single non-negative number or two non-negative numbers separated by '/', + /// with optional whitespace on either side of the '/'. + NumberRatio(Ratio), + /// A resolution. + Resolution(Resolution), + /// An enumerated value, defined by the variant keyword table in the + /// feature's `mData` member. + Enumerated(KeywordDiscriminant), + /// An arbitrary ident value. + String(AtomString), +} + +impl QueryExpressionValue { + fn to_css<W>(&self, dest: &mut CssWriter<W>, for_expr: &QueryFeatureExpression) -> fmt::Result + where + W: fmt::Write, + { + match *self { + QueryExpressionValue::Length(ref l) => l.to_css(dest), + QueryExpressionValue::Integer(v) => v.to_css(dest), + QueryExpressionValue::Float(v) => v.to_css(dest), + QueryExpressionValue::BoolInteger(v) => dest.write_str(if v { "1" } else { "0" }), + QueryExpressionValue::NumberRatio(ratio) => ratio.to_css(dest), + QueryExpressionValue::Resolution(ref r) => r.to_css(dest), + QueryExpressionValue::Enumerated(value) => match for_expr.feature().evaluator { + Evaluator::Enumerated { serializer, .. } => dest.write_str(&*serializer(value)), + _ => unreachable!(), + }, + QueryExpressionValue::String(ref s) => s.to_css(dest), + } + } + + fn parse<'i, 't>( + for_feature: &QueryFeatureDescription, + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<QueryExpressionValue, ParseError<'i>> { + Ok(match for_feature.evaluator { + Evaluator::OptionalLength(..) | Evaluator::Length(..) => { + let length = Length::parse(context, input)?; + QueryExpressionValue::Length(length) + }, + Evaluator::Integer(..) => { + let integer = Integer::parse(context, input)?; + QueryExpressionValue::Integer(integer.value()) + }, + Evaluator::BoolInteger(..) => { + let integer = Integer::parse_non_negative(context, input)?; + let value = integer.value(); + if value > 1 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + QueryExpressionValue::BoolInteger(value == 1) + }, + Evaluator::Float(..) => { + let number = Number::parse(context, input)?; + QueryExpressionValue::Float(number.get()) + }, + Evaluator::OptionalNumberRatio(..) | Evaluator::NumberRatio(..) => { + use crate::values::specified::Ratio as SpecifiedRatio; + let ratio = SpecifiedRatio::parse(context, input)?; + QueryExpressionValue::NumberRatio(Ratio::new(ratio.0.get(), ratio.1.get())) + }, + Evaluator::Resolution(..) => { + QueryExpressionValue::Resolution(Resolution::parse(context, input)?) + }, + Evaluator::String(..) => { + QueryExpressionValue::String(input.expect_string()?.as_ref().into()) + }, + Evaluator::Enumerated { parser, .. } => { + QueryExpressionValue::Enumerated(parser(context, input)?) + }, + }) + } +} diff --git a/servo/components/style/queries/mod.rs b/servo/components/style/queries/mod.rs new file mode 100644 index 0000000000..ec11ab3721 --- /dev/null +++ b/servo/components/style/queries/mod.rs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code shared between [media queries][mq] and [container queries][cq]. +//! +//! [mq]: https://drafts.csswg.org/mediaqueries/ +//! [cq]: https://drafts.csswg.org/css-contain-3/#container-rule + +pub mod condition; + +#[macro_use] +pub mod feature; +pub mod feature_expression; +pub mod values; + +pub use self::condition::QueryCondition; +pub use self::feature::FeatureFlags; +pub use self::feature_expression::{FeatureType, QueryFeatureExpression}; diff --git a/servo/components/style/queries/values.rs b/servo/components/style/queries/values.rs new file mode 100644 index 0000000000..f4934408c4 --- /dev/null +++ b/servo/components/style/queries/values.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common feature values between media and container features. + +use app_units::Au; +use euclid::default::Size2D; + +/// The orientation media / container feature. +/// https://drafts.csswg.org/mediaqueries-5/#orientation +/// https://drafts.csswg.org/css-contain-3/#orientation +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum Orientation { + Portrait, + Landscape, +} + +impl Orientation { + /// A helper to evaluate a orientation query given a generic size getter. + pub fn eval(size: Size2D<Au>, value: Option<Self>) -> bool { + let query_orientation = match value { + Some(v) => v, + None => return true, + }; + + // Per spec, square viewports should be 'portrait' + let is_landscape = size.width > size.height; + match query_orientation { + Self::Landscape => is_landscape, + Self::Portrait => !is_landscape, + } + } +} diff --git a/servo/components/style/rule_cache.rs b/servo/components/style/rule_cache.rs new file mode 100644 index 0000000000..70c5b79731 --- /dev/null +++ b/servo/components/style/rule_cache.rs @@ -0,0 +1,219 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A cache from rule node to computed values, in order to cache reset +//! properties. + +use crate::logical_geometry::WritingMode; +use crate::properties::{ComputedValues, StyleBuilder}; +use crate::rule_tree::StrongRuleNode; +use crate::selector_parser::PseudoElement; +use crate::shared_lock::StylesheetGuards; +use crate::values::computed::{NonNegativeLength, Zoom}; +use fxhash::FxHashMap; +use servo_arc::Arc; +use smallvec::SmallVec; + +/// The conditions for caching and matching a style in the rule cache. +#[derive(Clone, Debug, Default)] +pub struct RuleCacheConditions { + uncacheable: bool, + font_size: Option<NonNegativeLength>, + line_height: Option<NonNegativeLength>, + writing_mode: Option<WritingMode>, +} + +impl RuleCacheConditions { + /// Sets the style as depending in the font-size value. + pub fn set_font_size_dependency(&mut self, font_size: NonNegativeLength) { + debug_assert!(self.font_size.map_or(true, |f| f == font_size)); + self.font_size = Some(font_size); + } + + /// Sets the style as depending in the line-height value. + pub fn set_line_height_dependency(&mut self, line_height: NonNegativeLength) { + debug_assert!(self.line_height.map_or(true, |l| l == line_height)); + self.line_height = Some(line_height); + } + + /// Sets the style as uncacheable. + pub fn set_uncacheable(&mut self) { + self.uncacheable = true; + } + + /// Sets the style as depending in the writing-mode value `writing_mode`. + pub fn set_writing_mode_dependency(&mut self, writing_mode: WritingMode) { + debug_assert!(self.writing_mode.map_or(true, |wm| wm == writing_mode)); + self.writing_mode = Some(writing_mode); + } + + /// Returns whether the current style's reset properties are cacheable. + fn cacheable(&self) -> bool { + !self.uncacheable + } +} + +#[derive(Debug)] +struct CachedConditions { + font_size: Option<NonNegativeLength>, + line_height: Option<NonNegativeLength>, + writing_mode: Option<WritingMode>, + zoom: Zoom, +} + +impl CachedConditions { + /// Returns whether `style` matches the conditions. + fn matches(&self, style: &StyleBuilder) -> bool { + if style.effective_zoom != self.zoom { + return false; + } + + if let Some(fs) = self.font_size { + if style.get_font().clone_font_size().computed_size != fs { + return false; + } + } + + if let Some(lh) = self.line_height { + let new_line_height = + style + .device + .calc_line_height(&style.get_font(), style.writing_mode, None); + if new_line_height != lh { + return false; + } + } + + if let Some(wm) = self.writing_mode { + if style.writing_mode != wm { + return false; + } + } + + true + } +} + +/// A TLS cache from rules matched to computed values. +pub struct RuleCache { + // FIXME(emilio): Consider using LRUCache or something like that? + map: FxHashMap<StrongRuleNode, SmallVec<[(CachedConditions, Arc<ComputedValues>); 1]>>, +} + +impl RuleCache { + /// Creates an empty `RuleCache`. + pub fn new() -> Self { + Self { + map: FxHashMap::default(), + } + } + + /// Walk the rule tree and return a rule node for using as the key + /// for rule cache. + /// + /// It currently skips a rule node when it is neither from a style + /// rule, nor containing any declaration of reset property. We don't + /// skip style rule so that we don't need to walk a long way in the + /// worst case. Skipping declarations rule nodes should be enough + /// to address common cases that rule cache would fail to share + /// when using the rule node directly, like preshint, style attrs, + /// and animations. + fn get_rule_node_for_cache<'r>( + guards: &StylesheetGuards, + mut rule_node: Option<&'r StrongRuleNode>, + ) -> Option<&'r StrongRuleNode> { + while let Some(node) = rule_node { + match node.style_source() { + Some(s) => match s.as_declarations() { + Some(decls) => { + let cascade_level = node.cascade_level(); + let decls = decls.read_with(cascade_level.guard(guards)); + if decls.contains_any_reset() { + break; + } + }, + None => break, + }, + None => {}, + } + rule_node = node.parent(); + } + rule_node + } + + /// Finds a node in the properties matched cache. + /// + /// This needs to receive a `StyleBuilder` with the `early` properties + /// already applied. + pub fn find( + &self, + guards: &StylesheetGuards, + builder_with_early_props: &StyleBuilder, + ) -> Option<&ComputedValues> { + // A pseudo-element with property restrictions can result in different + // computed values if it's also used for a non-pseudo. + if builder_with_early_props + .pseudo + .and_then(|p| p.property_restriction()) + .is_some() + { + return None; + } + + let rules = builder_with_early_props.rules.as_ref(); + let rules = Self::get_rule_node_for_cache(guards, rules)?; + let cached_values = self.map.get(rules)?; + + for &(ref conditions, ref values) in cached_values.iter() { + if conditions.matches(builder_with_early_props) { + debug!("Using cached reset style with conditions {:?}", conditions); + return Some(&**values); + } + } + None + } + + /// Inserts a node into the rules cache if possible. + /// + /// Returns whether the style was inserted into the cache. + pub fn insert_if_possible( + &mut self, + guards: &StylesheetGuards, + style: &Arc<ComputedValues>, + pseudo: Option<&PseudoElement>, + conditions: &RuleCacheConditions, + ) -> bool { + if !conditions.cacheable() { + return false; + } + + // A pseudo-element with property restrictions can result in different + // computed values if it's also used for a non-pseudo. + if pseudo.and_then(|p| p.property_restriction()).is_some() { + return false; + } + + let rules = style.rules.as_ref(); + let rules = match Self::get_rule_node_for_cache(guards, rules) { + Some(r) => r.clone(), + None => return false, + }; + + debug!( + "Inserting cached reset style with conditions {:?}", + conditions + ); + let cached_conditions = CachedConditions { + writing_mode: conditions.writing_mode, + font_size: conditions.font_size, + line_height: conditions.line_height, + zoom: style.effective_zoom, + }; + self.map + .entry(rules) + .or_default() + .push((cached_conditions, style.clone())); + true + } +} diff --git a/servo/components/style/rule_collector.rs b/servo/components/style/rule_collector.rs new file mode 100644 index 0000000000..058d682317 --- /dev/null +++ b/servo/components/style/rule_collector.rs @@ -0,0 +1,505 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Collects a series of applicable rules for a given element. + +use crate::applicable_declarations::{ApplicableDeclarationBlock, ApplicableDeclarationList}; +use crate::dom::{TElement, TNode, TShadowRoot}; +use crate::properties::{AnimationDeclarations, PropertyDeclarationBlock}; +use crate::rule_tree::{CascadeLevel, ShadowCascadeOrder}; +use crate::selector_map::SelectorMap; +use crate::selector_parser::PseudoElement; +use crate::shared_lock::Locked; +use crate::stylesheets::{layer_rule::LayerOrder, Origin}; +use crate::stylist::{AuthorStylesEnabled, CascadeData, Rule, RuleInclusion, Stylist}; +use selectors::matching::{MatchingContext, MatchingMode}; +use servo_arc::ArcBorrow; +use smallvec::SmallVec; + +/// This is a bit of a hack so <svg:use> matches the rules of the enclosing +/// tree. +/// +/// This function returns the containing shadow host ignoring <svg:use> shadow +/// trees, since those match the enclosing tree's rules. +/// +/// Only a handful of places need to really care about this. This is not a +/// problem for invalidation and that kind of stuff because they still don't +/// match rules based on elements outside of the shadow tree, and because the +/// <svg:use> subtrees are immutable and recreated each time the source tree +/// changes. +/// +/// We historically allow cross-document <svg:use> to have these rules applied, +/// but I think that's not great. Gecko is the only engine supporting that. +/// +/// See https://github.com/w3c/svgwg/issues/504 for the relevant spec +/// discussion. +#[inline] +pub fn containing_shadow_ignoring_svg_use<E: TElement>( + element: E, +) -> Option<<E::ConcreteNode as TNode>::ConcreteShadowRoot> { + let mut shadow = element.containing_shadow()?; + loop { + let host = shadow.host(); + let host_is_svg_use_element = + host.is_svg_element() && host.local_name() == &**local_name!("use"); + if !host_is_svg_use_element { + return Some(shadow); + } + debug_assert!( + shadow.style_data().is_none(), + "We allow no stylesheets in <svg:use> subtrees" + ); + shadow = host.containing_shadow()?; + } +} + +/// An object that we use with all the intermediate state needed for the +/// cascade. +/// +/// This is done basically to be able to organize the cascade in smaller +/// functions, and be able to reason about it easily. +pub struct RuleCollector<'a, 'b: 'a, E> +where + E: TElement, +{ + element: E, + rule_hash_target: E, + stylist: &'a Stylist, + pseudo_element: Option<&'a PseudoElement>, + style_attribute: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>, + smil_override: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>, + animation_declarations: AnimationDeclarations, + rule_inclusion: RuleInclusion, + rules: &'a mut ApplicableDeclarationList, + context: &'a mut MatchingContext<'b, E::Impl>, + matches_user_and_content_rules: bool, + matches_document_author_rules: bool, + in_sort_scope: bool, +} + +impl<'a, 'b: 'a, E> RuleCollector<'a, 'b, E> +where + E: TElement, +{ + /// Trivially construct a new collector. + pub fn new( + stylist: &'a Stylist, + element: E, + pseudo_element: Option<&'a PseudoElement>, + style_attribute: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>, + smil_override: Option<ArcBorrow<'a, Locked<PropertyDeclarationBlock>>>, + animation_declarations: AnimationDeclarations, + rule_inclusion: RuleInclusion, + rules: &'a mut ApplicableDeclarationList, + context: &'a mut MatchingContext<'b, E::Impl>, + ) -> Self { + // When we're matching with matching_mode = + // `ForStatelessPseudoeElement`, the "target" for the rule hash is the + // element itself, since it's what's generating the pseudo-element. + let rule_hash_target = match context.matching_mode() { + MatchingMode::ForStatelessPseudoElement => element, + MatchingMode::Normal => element.rule_hash_target(), + }; + + let matches_user_and_content_rules = rule_hash_target.matches_user_and_content_rules(); + + // Gecko definitely has pseudo-elements with style attributes, like + // ::-moz-color-swatch. + debug_assert!( + cfg!(feature = "gecko") || style_attribute.is_none() || pseudo_element.is_none(), + "Style attributes do not apply to pseudo-elements" + ); + debug_assert!(pseudo_element.map_or(true, |p| !p.is_precomputed())); + + Self { + element, + rule_hash_target, + stylist, + pseudo_element, + style_attribute, + smil_override, + animation_declarations, + rule_inclusion, + context, + rules, + matches_user_and_content_rules, + matches_document_author_rules: matches_user_and_content_rules, + in_sort_scope: false, + } + } + + /// Sets up the state necessary to collect rules from a given DOM tree + /// (either the document tree, or a shadow tree). + /// + /// All rules in the same tree need to be matched together, and this + /// function takes care of sorting them by specificity and source order. + #[inline] + fn in_tree(&mut self, host: Option<E>, f: impl FnOnce(&mut Self)) { + debug_assert!(!self.in_sort_scope, "Nested sorting makes no sense"); + let start = self.rules.len(); + self.in_sort_scope = true; + let old_host = self.context.current_host.take(); + self.context.current_host = host.map(|e| e.opaque()); + f(self); + if start != self.rules.len() { + self.rules[start..].sort_unstable_by_key(|block| { + (block.layer_order(), block.specificity, block.source_order()) + }); + } + self.context.current_host = old_host; + self.in_sort_scope = false; + } + + #[inline] + fn in_shadow_tree(&mut self, host: E, f: impl FnOnce(&mut Self)) { + self.in_tree(Some(host), f); + } + + fn collect_stylist_rules(&mut self, origin: Origin) { + let cascade_level = match origin { + Origin::UserAgent => CascadeLevel::UANormal, + Origin::User => CascadeLevel::UserNormal, + Origin::Author => CascadeLevel::same_tree_author_normal(), + }; + + let cascade_data = self.stylist.cascade_data().borrow_for_origin(origin); + let map = match cascade_data.normal_rules(self.pseudo_element) { + Some(m) => m, + None => return, + }; + + self.in_tree(None, |collector| { + collector.collect_rules_in_map(map, cascade_level, cascade_data); + }); + } + + fn collect_user_agent_rules(&mut self) { + self.collect_stylist_rules(Origin::UserAgent); + } + + fn collect_user_rules(&mut self) { + if !self.matches_user_and_content_rules { + return; + } + + self.collect_stylist_rules(Origin::User); + } + + /// Presentational hints. + /// + /// These go before author rules, but after user rules, see: + /// https://drafts.csswg.org/css-cascade/#preshint + fn collect_presentational_hints(&mut self) { + if self.pseudo_element.is_some() { + return; + } + + let length_before_preshints = self.rules.len(); + self.element + .synthesize_presentational_hints_for_legacy_attributes( + self.context.visited_handling(), + self.rules, + ); + if cfg!(debug_assertions) { + if self.rules.len() != length_before_preshints { + for declaration in &self.rules[length_before_preshints..] { + assert_eq!(declaration.level(), CascadeLevel::PresHints); + } + } + } + } + + #[inline] + fn collect_rules_in_list( + &mut self, + part_rules: &[Rule], + cascade_level: CascadeLevel, + cascade_data: &CascadeData, + ) { + debug_assert!(self.in_sort_scope, "Rules gotta be sorted"); + SelectorMap::get_matching_rules( + self.element, + part_rules, + &mut self.rules, + &mut self.context, + cascade_level, + cascade_data, + &self.stylist, + ); + } + + #[inline] + fn collect_rules_in_map( + &mut self, + map: &SelectorMap<Rule>, + cascade_level: CascadeLevel, + cascade_data: &CascadeData, + ) { + debug_assert!(self.in_sort_scope, "Rules gotta be sorted"); + map.get_all_matching_rules( + self.element, + self.rule_hash_target, + &mut self.rules, + &mut self.context, + cascade_level, + cascade_data, + &self.stylist, + ); + } + + /// Collects the rules for the ::slotted pseudo-element and the :host + /// pseudo-class. + fn collect_host_and_slotted_rules(&mut self) { + let mut slots = SmallVec::<[_; 3]>::new(); + let mut current = self.rule_hash_target.assigned_slot(); + let mut shadow_cascade_order = ShadowCascadeOrder::for_outermost_shadow_tree(); + + while let Some(slot) = current { + debug_assert!( + self.matches_user_and_content_rules, + "We should not slot NAC anywhere" + ); + slots.push(slot); + current = slot.assigned_slot(); + shadow_cascade_order.dec(); + } + + self.collect_host_rules(shadow_cascade_order); + + // Match slotted rules in reverse order, so that the outer slotted rules + // come before the inner rules (and thus have less priority). + for slot in slots.iter().rev() { + shadow_cascade_order.inc(); + + let shadow = slot.containing_shadow().unwrap(); + let data = match shadow.style_data() { + Some(d) => d, + None => continue, + }; + let slotted_rules = match data.slotted_rules(self.pseudo_element) { + Some(r) => r, + None => continue, + }; + + self.in_shadow_tree(shadow.host(), |collector| { + let cascade_level = CascadeLevel::AuthorNormal { + shadow_cascade_order, + }; + collector.collect_rules_in_map(slotted_rules, cascade_level, data); + }); + } + } + + fn collect_rules_from_containing_shadow_tree(&mut self) { + if !self.matches_user_and_content_rules { + return; + } + + let containing_shadow = containing_shadow_ignoring_svg_use(self.rule_hash_target); + let containing_shadow = match containing_shadow { + Some(s) => s, + None => return, + }; + + self.matches_document_author_rules = false; + + let cascade_data = match containing_shadow.style_data() { + Some(c) => c, + None => return, + }; + + let cascade_level = CascadeLevel::same_tree_author_normal(); + self.in_shadow_tree(containing_shadow.host(), |collector| { + if let Some(map) = cascade_data.normal_rules(collector.pseudo_element) { + collector.collect_rules_in_map(map, cascade_level, cascade_data); + } + + // Collect rules from :host::part() and such + let hash_target = collector.rule_hash_target; + if !hash_target.has_part_attr() { + return; + } + + let part_rules = match cascade_data.part_rules(collector.pseudo_element) { + Some(p) => p, + None => return, + }; + + hash_target.each_part(|part| { + if let Some(part_rules) = part_rules.get(&part.0) { + collector.collect_rules_in_list(part_rules, cascade_level, cascade_data); + } + }); + }); + } + + /// Collects the rules for the :host pseudo-class. + fn collect_host_rules(&mut self, shadow_cascade_order: ShadowCascadeOrder) { + let shadow = match self.rule_hash_target.shadow_root() { + Some(s) => s, + None => return, + }; + + let style_data = match shadow.style_data() { + Some(d) => d, + None => return, + }; + + let host_rules = match style_data.host_rules(self.pseudo_element) { + Some(rules) => rules, + None => return, + }; + + let rule_hash_target = self.rule_hash_target; + self.in_shadow_tree(rule_hash_target, |collector| { + let cascade_level = CascadeLevel::AuthorNormal { + shadow_cascade_order, + }; + collector.collect_rules_in_map(host_rules, cascade_level, style_data); + }); + } + + fn collect_document_author_rules(&mut self) { + if !self.matches_document_author_rules { + return; + } + + self.collect_stylist_rules(Origin::Author); + } + + fn collect_part_rules_from_outer_trees(&mut self) { + if !self.rule_hash_target.has_part_attr() { + return; + } + + let mut inner_shadow = match self.rule_hash_target.containing_shadow() { + Some(s) => s, + None => return, + }; + + let mut shadow_cascade_order = ShadowCascadeOrder::for_innermost_containing_tree(); + + let mut parts = SmallVec::<[_; 3]>::new(); + self.rule_hash_target.each_part(|p| parts.push(p.clone())); + + loop { + if parts.is_empty() { + return; + } + + let inner_shadow_host = inner_shadow.host(); + let outer_shadow = inner_shadow_host.containing_shadow(); + let cascade_data = match outer_shadow { + Some(shadow) => shadow.style_data(), + None => Some( + self.stylist + .cascade_data() + .borrow_for_origin(Origin::Author), + ), + }; + + if let Some(cascade_data) = cascade_data { + if let Some(part_rules) = cascade_data.part_rules(self.pseudo_element) { + let containing_host = outer_shadow.map(|s| s.host()); + let cascade_level = CascadeLevel::AuthorNormal { + shadow_cascade_order, + }; + self.in_tree(containing_host, |collector| { + for p in &parts { + if let Some(part_rules) = part_rules.get(&p.0) { + collector.collect_rules_in_list( + part_rules, + cascade_level, + cascade_data, + ); + } + } + }); + shadow_cascade_order.inc(); + } + } + + inner_shadow = match outer_shadow { + Some(s) => s, + None => break, // Nowhere to export to. + }; + + let mut new_parts = SmallVec::new(); + for part in &parts { + inner_shadow_host.each_exported_part(part, |exported_part| { + new_parts.push(exported_part.clone()); + }); + } + parts = new_parts; + } + } + + fn collect_style_attribute(&mut self) { + if let Some(sa) = self.style_attribute { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + sa.clone_arc(), + CascadeLevel::same_tree_author_normal(), + LayerOrder::style_attribute(), + )); + } + } + + fn collect_animation_rules(&mut self) { + if let Some(so) = self.smil_override { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + so.clone_arc(), + CascadeLevel::SMILOverride, + LayerOrder::root(), + )); + } + + // The animations sheet (CSS animations, script-generated + // animations, and CSS transitions that are no longer tied to CSS + // markup). + if let Some(anim) = self.animation_declarations.animations.take() { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + anim, + CascadeLevel::Animations, + LayerOrder::root(), + )); + } + + // The transitions sheet (CSS transitions that are tied to CSS + // markup). + if let Some(anim) = self.animation_declarations.transitions.take() { + self.rules + .push(ApplicableDeclarationBlock::from_declarations( + anim, + CascadeLevel::Transitions, + LayerOrder::root(), + )); + } + } + + /// Collects all the rules, leaving the result in `self.rules`. + /// + /// Note that `!important` rules are handled during rule tree insertion. + pub fn collect_all(mut self) { + self.collect_user_agent_rules(); + self.collect_user_rules(); + if self.rule_inclusion == RuleInclusion::DefaultOnly { + return; + } + self.collect_presentational_hints(); + // FIXME(emilio): Should the author styles enabled stuff avoid the + // presentational hints from getting pushed? See bug 1505770. + if self.stylist.author_styles_enabled() == AuthorStylesEnabled::No { + return; + } + self.collect_host_and_slotted_rules(); + self.collect_rules_from_containing_shadow_tree(); + self.collect_document_author_rules(); + self.collect_style_attribute(); + self.collect_part_rules_from_outer_trees(); + self.collect_animation_rules(); + } +} diff --git a/servo/components/style/rule_tree/core.rs b/servo/components/style/rule_tree/core.rs new file mode 100644 index 0000000000..85584a0e22 --- /dev/null +++ b/servo/components/style/rule_tree/core.rs @@ -0,0 +1,772 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +use crate::applicable_declarations::CascadePriority; +use crate::shared_lock::StylesheetGuards; +use crate::stylesheets::layer_rule::LayerOrder; +use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps}; +use parking_lot::RwLock; +use smallvec::SmallVec; +use std::fmt; +use std::hash; +use std::io::Write; +use std::mem; +use std::ptr; +use std::sync::atomic::{self, AtomicPtr, AtomicUsize, Ordering}; + +use super::map::{Entry, Map}; +use super::unsafe_box::UnsafeBox; +use super::{CascadeLevel, StyleSource}; + +/// The rule tree, the structure servo uses to preserve the results of selector +/// matching. +/// +/// This is organized as a tree of rules. When a node matches a set of rules, +/// they're inserted in order in the tree, starting with the less specific one. +/// +/// When a rule is inserted in the tree, other elements may share the path up to +/// a given rule. If that's the case, we don't duplicate child nodes, but share +/// them. +/// +/// When the rule node refcount drops to zero, it doesn't get freed. It gets +/// instead put into a free list, and it is potentially GC'd after a while. +/// +/// That way, a rule node that represents a likely-to-match-again rule (like a +/// :hover rule) can be reused if we haven't GC'd it yet. +#[derive(Debug)] +pub struct RuleTree { + root: StrongRuleNode, +} + +impl Drop for RuleTree { + fn drop(&mut self) { + unsafe { self.swap_free_list_and_gc(ptr::null_mut()) } + } +} + +impl MallocSizeOf for RuleTree { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + let mut stack = SmallVec::<[_; 32]>::new(); + stack.push(self.root.clone()); + + while let Some(node) = stack.pop() { + n += unsafe { ops.malloc_size_of(&*node.p) }; + let children = node.p.children.read(); + children.shallow_size_of(ops); + for c in &*children { + stack.push(unsafe { c.upgrade() }); + } + } + + n + } +} + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +struct ChildKey(CascadePriority, ptr::NonNull<()>); +unsafe impl Send for ChildKey {} +unsafe impl Sync for ChildKey {} + +impl RuleTree { + /// Construct a new rule tree. + pub fn new() -> Self { + RuleTree { + root: StrongRuleNode::new(Box::new(RuleNode::root())), + } + } + + /// Get the root rule node. + pub fn root(&self) -> &StrongRuleNode { + &self.root + } + + /// This can only be called when no other threads is accessing this tree. + pub fn gc(&self) { + unsafe { self.swap_free_list_and_gc(RuleNode::DANGLING_PTR) } + } + + /// This can only be called when no other threads is accessing this tree. + pub fn maybe_gc(&self) { + #[cfg(debug_assertions)] + self.maybe_dump_stats(); + + if self.root.p.approximate_free_count.load(Ordering::Relaxed) > RULE_TREE_GC_INTERVAL { + self.gc(); + } + } + + #[cfg(debug_assertions)] + fn maybe_dump_stats(&self) { + use itertools::Itertools; + use std::cell::Cell; + use std::time::{Duration, Instant}; + + if !log_enabled!(log::Level::Trace) { + return; + } + + const RULE_TREE_STATS_INTERVAL: Duration = Duration::from_secs(2); + + thread_local! { + pub static LAST_STATS: Cell<Instant> = Cell::new(Instant::now()); + }; + + let should_dump = LAST_STATS.with(|s| { + let now = Instant::now(); + if now.duration_since(s.get()) < RULE_TREE_STATS_INTERVAL { + return false; + } + s.set(now); + true + }); + + if !should_dump { + return; + } + + let mut children_count = fxhash::FxHashMap::default(); + + let mut stack = SmallVec::<[_; 32]>::new(); + stack.push(self.root.clone()); + while let Some(node) = stack.pop() { + let children = node.p.children.read(); + *children_count.entry(children.len()).or_insert(0) += 1; + for c in &*children { + stack.push(unsafe { c.upgrade() }); + } + } + + trace!("Rule tree stats:"); + let counts = children_count.keys().sorted(); + for count in counts { + trace!(" {} - {}", count, children_count[count]); + } + } + + /// Steals the free list and drops its contents. + unsafe fn swap_free_list_and_gc(&self, ptr: *mut RuleNode) { + let root = &self.root.p; + + debug_assert!(!root.next_free.load(Ordering::Relaxed).is_null()); + + // Reset the approximate free count to zero, as we are going to steal + // the free list. + root.approximate_free_count.store(0, Ordering::Relaxed); + + // Steal the free list head. Memory loads on nodes while iterating it + // must observe any prior changes that occured so this requires + // acquire ordering, but there are no writes that need to be kept + // before this swap so there is no need for release. + let mut head = root.next_free.swap(ptr, Ordering::Acquire); + + while head != RuleNode::DANGLING_PTR { + debug_assert!(!head.is_null()); + + let mut node = UnsafeBox::from_raw(head); + + // The root node cannot go on the free list. + debug_assert!(node.root.is_some()); + + // The refcount of nodes on the free list never goes below 1. + debug_assert!(node.refcount.load(Ordering::Relaxed) > 0); + + // No one else is currently writing to that field. Get the address + // of the next node in the free list and replace it with null, + // other threads will now consider that this node is not on the + // free list. + head = node.next_free.swap(ptr::null_mut(), Ordering::Relaxed); + + // This release write synchronises with the acquire fence in + // `WeakRuleNode::upgrade`, making sure that if `upgrade` observes + // decrements the refcount to 0, it will also observe the + // `node.next_free` swap to null above. + if node.refcount.fetch_sub(1, Ordering::Release) == 1 { + // And given it observed the null swap above, it will need + // `pretend_to_be_on_free_list` to finish its job, writing + // `RuleNode::DANGLING_PTR` in `node.next_free`. + RuleNode::pretend_to_be_on_free_list(&node); + + // Drop this node now that we just observed its refcount going + // down to zero. + RuleNode::drop_without_free_list(&mut node); + } + } + } +} + +/// The number of RuleNodes added to the free list before we will consider +/// doing a GC when calling maybe_gc(). (The value is copied from Gecko, +/// where it likely did not result from a rigorous performance analysis.) +const RULE_TREE_GC_INTERVAL: usize = 300; + +/// A node in the rule tree. +struct RuleNode { + /// The root node. Only the root has no root pointer, for obvious reasons. + root: Option<WeakRuleNode>, + + /// The parent rule node. Only the root has no parent. + parent: Option<StrongRuleNode>, + + /// The actual style source, either coming from a selector in a StyleRule, + /// or a raw property declaration block (like the style attribute). + /// + /// None for the root node. + source: Option<StyleSource>, + + /// The cascade level + layer order this rule is positioned at. + cascade_priority: CascadePriority, + + /// The refcount of this node. + /// + /// Starts at one. Incremented in `StrongRuleNode::clone` and + /// `WeakRuleNode::upgrade`. Decremented in `StrongRuleNode::drop` + /// and `RuleTree::swap_free_list_and_gc`. + /// + /// If a non-root node's refcount reaches zero, it is incremented back to at + /// least one in `RuleNode::pretend_to_be_on_free_list` until the caller who + /// observed it dropping to zero had a chance to try to remove it from its + /// parent's children list. + /// + /// The refcount should never be decremented to zero if the value in + /// `next_free` is not null. + refcount: AtomicUsize, + + /// Only used for the root, stores the number of free rule nodes that are + /// around. + approximate_free_count: AtomicUsize, + + /// The children of a given rule node. Children remove themselves from here + /// when they go away. + children: RwLock<Map<ChildKey, WeakRuleNode>>, + + /// This field has two different meanings depending on whether this is the + /// root node or not. + /// + /// If it is the root, it represents the head of the free list. It may be + /// null, which means the free list is gone because the tree was dropped, + /// and it may be `RuleNode::DANGLING_PTR`, which means the free list is + /// empty. + /// + /// If it is not the root node, this field is either null if the node is + /// not on the free list, `RuleNode::DANGLING_PTR` if it is the last item + /// on the free list or the node is pretending to be on the free list, or + /// any valid non-null pointer representing the next item on the free list + /// after this one. + /// + /// See `RuleNode::push_on_free_list`, `swap_free_list_and_gc`, and + /// `WeakRuleNode::upgrade`. + /// + /// Two threads should never attempt to put the same node on the free list + /// both at the same time. + next_free: AtomicPtr<RuleNode>, +} + +// On Gecko builds, hook into the leak checking machinery. +#[cfg(feature = "gecko_refcount_logging")] +mod gecko_leak_checking { + use super::RuleNode; + use std::mem::size_of; + use std::os::raw::{c_char, c_void}; + + extern "C" { + fn NS_LogCtor(aPtr: *mut c_void, aTypeName: *const c_char, aSize: u32); + fn NS_LogDtor(aPtr: *mut c_void, aTypeName: *const c_char, aSize: u32); + } + static NAME: &'static [u8] = b"RuleNode\0"; + + /// Logs the creation of a heap-allocated object to Gecko's leak-checking machinery. + pub(super) fn log_ctor(ptr: *const RuleNode) { + let s = NAME as *const [u8] as *const u8 as *const c_char; + unsafe { + NS_LogCtor(ptr as *mut c_void, s, size_of::<RuleNode>() as u32); + } + } + + /// Logs the destruction of a heap-allocated object to Gecko's leak-checking machinery. + pub(super) fn log_dtor(ptr: *const RuleNode) { + let s = NAME as *const [u8] as *const u8 as *const c_char; + unsafe { + NS_LogDtor(ptr as *mut c_void, s, size_of::<RuleNode>() as u32); + } + } +} + +#[inline(always)] +fn log_new(_ptr: *const RuleNode) { + #[cfg(feature = "gecko_refcount_logging")] + gecko_leak_checking::log_ctor(_ptr); +} + +#[inline(always)] +fn log_drop(_ptr: *const RuleNode) { + #[cfg(feature = "gecko_refcount_logging")] + gecko_leak_checking::log_dtor(_ptr); +} + +impl RuleNode { + const DANGLING_PTR: *mut Self = ptr::NonNull::dangling().as_ptr(); + + unsafe fn new( + root: WeakRuleNode, + parent: StrongRuleNode, + source: StyleSource, + cascade_priority: CascadePriority, + ) -> Self { + debug_assert!(root.p.parent.is_none()); + RuleNode { + root: Some(root), + parent: Some(parent), + source: Some(source), + cascade_priority, + refcount: AtomicUsize::new(1), + children: Default::default(), + approximate_free_count: AtomicUsize::new(0), + next_free: AtomicPtr::new(ptr::null_mut()), + } + } + + fn root() -> Self { + RuleNode { + root: None, + parent: None, + source: None, + cascade_priority: CascadePriority::new(CascadeLevel::UANormal, LayerOrder::root()), + refcount: AtomicUsize::new(1), + approximate_free_count: AtomicUsize::new(0), + children: Default::default(), + next_free: AtomicPtr::new(RuleNode::DANGLING_PTR), + } + } + + fn key(&self) -> ChildKey { + ChildKey( + self.cascade_priority, + self.source + .as_ref() + .expect("Called key() on the root node") + .key(), + ) + } + + /// Drops a node without ever putting it on the free list. + /// + /// Note that the node may not be dropped if we observe that its refcount + /// isn't zero anymore when we write-lock its parent's children map to + /// remove it. + /// + /// This loops over parents of dropped nodes if their own refcount reaches + /// zero to avoid recursion when dropping deep hierarchies of nodes. + /// + /// For non-root nodes, this should always be preceded by a call of + /// `RuleNode::pretend_to_be_on_free_list`. + unsafe fn drop_without_free_list(this: &mut UnsafeBox<Self>) { + // We clone the box and shadow the original one to be able to loop + // over its ancestors if they also need to be dropped. + let mut this = UnsafeBox::clone(this); + loop { + // If the node has a parent, we need to remove it from its parent's + // children list. + if let Some(parent) = this.parent.as_ref() { + debug_assert!(!this.next_free.load(Ordering::Relaxed).is_null()); + + // We lock the parent's children list, which means no other + // thread will have any more opportunity to resurrect the node + // anymore. + let mut children = parent.p.children.write(); + + this.next_free.store(ptr::null_mut(), Ordering::Relaxed); + + // We decrement the counter to remove the "pretend to be + // on the free list" reference. + let old_refcount = this.refcount.fetch_sub(1, Ordering::Release); + debug_assert!(old_refcount != 0); + if old_refcount != 1 { + // Other threads resurrected this node and those references + // are still alive, we have nothing to do anymore. + return; + } + + // We finally remove the node from its parent's children list, + // there are now no other references to it and it cannot + // be resurrected anymore even after we unlock the list. + debug!( + "Remove from child list: {:?}, parent: {:?}", + this.as_mut_ptr(), + this.parent.as_ref().map(|p| p.p.as_mut_ptr()) + ); + let weak = children.remove(&this.key(), |node| node.p.key()).unwrap(); + assert_eq!(weak.p.as_mut_ptr(), this.as_mut_ptr()); + } else { + debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut()); + debug_assert_eq!(this.refcount.load(Ordering::Relaxed), 0); + } + + // We are going to drop this node for good this time, as per the + // usual refcounting protocol we need an acquire fence here before + // we run the destructor. + // + // See https://github.com/rust-lang/rust/pull/41714#issuecomment-298996916 + // for why it doesn't matter whether this is a load or a fence. + atomic::fence(Ordering::Acquire); + + // Remove the parent reference from the child to avoid + // recursively dropping it and putting it on the free list. + let parent = UnsafeBox::deref_mut(&mut this).parent.take(); + + // We now drop the actual box and its contents, no one should + // access the current value in `this` anymore. + log_drop(&*this); + UnsafeBox::drop(&mut this); + + if let Some(parent) = parent { + // We will attempt to drop the node's parent without the free + // list, so we clone the inner unsafe box and forget the + // original parent to avoid running its `StrongRuleNode` + // destructor which would attempt to use the free list if it + // still exists. + this = UnsafeBox::clone(&parent.p); + mem::forget(parent); + if this.refcount.fetch_sub(1, Ordering::Release) == 1 { + debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut()); + if this.root.is_some() { + RuleNode::pretend_to_be_on_free_list(&this); + } + // Parent also reached refcount zero, we loop to drop it. + continue; + } + } + + return; + } + } + + /// Pushes this node on the tree's free list. Returns false if the free list + /// is gone. Should only be called after we decremented a node's refcount + /// to zero and pretended to be on the free list. + unsafe fn push_on_free_list(this: &UnsafeBox<Self>) -> bool { + let root = &this.root.as_ref().unwrap().p; + + debug_assert!(this.refcount.load(Ordering::Relaxed) > 0); + debug_assert_eq!(this.next_free.load(Ordering::Relaxed), Self::DANGLING_PTR); + + // Increment the approximate free count by one. + root.approximate_free_count.fetch_add(1, Ordering::Relaxed); + + // If the compare-exchange operation fails in the loop, we will retry + // with the new head value, so this can be a relaxed load. + let mut head = root.next_free.load(Ordering::Relaxed); + + while !head.is_null() { + // Two threads can never attempt to push the same node on the free + // list both at the same time, so whoever else pushed a node on the + // free list cannot have done so with this node. + debug_assert_ne!(head, this.as_mut_ptr()); + + // Store the current head of the free list in this node. + this.next_free.store(head, Ordering::Relaxed); + + // Any thread acquiring the free list must observe the previous + // next_free changes that occured, hence the release ordering + // on success. + match root.next_free.compare_exchange_weak( + head, + this.as_mut_ptr(), + Ordering::Release, + Ordering::Relaxed, + ) { + Ok(_) => { + // This node is now on the free list, caller should not use + // the node anymore. + return true; + }, + Err(new_head) => head = new_head, + } + } + + // Tree was dropped and free list has been destroyed. We did not push + // this node on the free list but we still pretend to be on the free + // list to be ready to call `drop_without_free_list`. + false + } + + /// Makes the node pretend to be on the free list. This will increment the + /// refcount by 1 and store `Self::DANGLING_PTR` in `next_free`. This + /// method should only be called after caller decremented the refcount to + /// zero, with the null pointer stored in `next_free`. + unsafe fn pretend_to_be_on_free_list(this: &UnsafeBox<Self>) { + debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut()); + this.refcount.fetch_add(1, Ordering::Relaxed); + this.next_free.store(Self::DANGLING_PTR, Ordering::Release); + } + + fn as_mut_ptr(&self) -> *mut RuleNode { + self as *const RuleNode as *mut RuleNode + } +} + +pub(crate) struct WeakRuleNode { + p: UnsafeBox<RuleNode>, +} + +/// A strong reference to a rule node. +pub struct StrongRuleNode { + p: UnsafeBox<RuleNode>, +} + +#[cfg(feature = "servo")] +malloc_size_of_is_0!(StrongRuleNode); + +impl StrongRuleNode { + fn new(n: Box<RuleNode>) -> Self { + debug_assert_eq!(n.parent.is_none(), !n.source.is_some()); + + log_new(&*n); + + debug!("Creating rule node: {:p}", &*n); + + Self { + p: UnsafeBox::from_box(n), + } + } + + unsafe fn from_unsafe_box(p: UnsafeBox<RuleNode>) -> Self { + Self { p } + } + + unsafe fn downgrade(&self) -> WeakRuleNode { + WeakRuleNode { + p: UnsafeBox::clone(&self.p), + } + } + + /// Get the parent rule node of this rule node. + pub fn parent(&self) -> Option<&StrongRuleNode> { + self.p.parent.as_ref() + } + + pub(super) fn ensure_child( + &self, + root: &StrongRuleNode, + source: StyleSource, + cascade_priority: CascadePriority, + ) -> StrongRuleNode { + use parking_lot::RwLockUpgradableReadGuard; + + debug_assert!( + self.p.cascade_priority <= cascade_priority, + "Should be ordered (instead {:?} > {:?}), from {:?} and {:?}", + self.p.cascade_priority, + cascade_priority, + self.p.source, + source, + ); + + let key = ChildKey(cascade_priority, source.key()); + let children = self.p.children.upgradable_read(); + if let Some(child) = children.get(&key, |node| node.p.key()) { + // Sound to call because we read-locked the parent's children. + return unsafe { child.upgrade() }; + } + let mut children = RwLockUpgradableReadGuard::upgrade(children); + match children.entry(key, |node| node.p.key()) { + Entry::Occupied(child) => { + // Sound to call because we write-locked the parent's children. + unsafe { child.upgrade() } + }, + Entry::Vacant(entry) => unsafe { + let node = StrongRuleNode::new(Box::new(RuleNode::new( + root.downgrade(), + self.clone(), + source, + cascade_priority, + ))); + // Sound to call because we still own a strong reference to + // this node, through the `node` variable itself that we are + // going to return to the caller. + entry.insert(node.downgrade()); + node + }, + } + } + + /// Get the style source corresponding to this rule node. May return `None` + /// if it's the root node, which means that the node hasn't matched any + /// rules. + pub fn style_source(&self) -> Option<&StyleSource> { + self.p.source.as_ref() + } + + /// The cascade priority. + #[inline] + pub fn cascade_priority(&self) -> CascadePriority { + self.p.cascade_priority + } + + /// The cascade level. + #[inline] + pub fn cascade_level(&self) -> CascadeLevel { + self.cascade_priority().cascade_level() + } + + /// The importance. + #[inline] + pub fn importance(&self) -> crate::properties::Importance { + self.cascade_level().importance() + } + + /// Returns whether this node has any child, only intended for testing + /// purposes. + pub unsafe fn has_children_for_testing(&self) -> bool { + !self.p.children.read().is_empty() + } + + pub(super) fn dump<W: Write>(&self, guards: &StylesheetGuards, writer: &mut W, indent: usize) { + const INDENT_INCREMENT: usize = 4; + + for _ in 0..indent { + let _ = write!(writer, " "); + } + + let _ = writeln!( + writer, + " - {:p} (ref: {:?}, parent: {:?})", + &*self.p, + self.p.refcount.load(Ordering::Relaxed), + self.parent().map(|p| &*p.p as *const RuleNode) + ); + + for _ in 0..indent { + let _ = write!(writer, " "); + } + + if let Some(source) = self.style_source() { + source.dump(self.cascade_level().guard(guards), writer); + } else { + if indent != 0 { + warn!("How has this happened?"); + } + let _ = write!(writer, "(root)"); + } + + let _ = write!(writer, "\n"); + for child in &*self.p.children.read() { + unsafe { + child + .upgrade() + .dump(guards, writer, indent + INDENT_INCREMENT); + } + } + } +} + +impl Clone for StrongRuleNode { + fn clone(&self) -> Self { + debug!( + "{:p}: {:?}+", + &*self.p, + self.p.refcount.load(Ordering::Relaxed) + ); + debug_assert!(self.p.refcount.load(Ordering::Relaxed) > 0); + self.p.refcount.fetch_add(1, Ordering::Relaxed); + unsafe { StrongRuleNode::from_unsafe_box(UnsafeBox::clone(&self.p)) } + } +} + +impl Drop for StrongRuleNode { + #[cfg_attr(feature = "servo", allow(unused_mut))] + fn drop(&mut self) { + let node = &*self.p; + debug!("{:p}: {:?}-", node, node.refcount.load(Ordering::Relaxed)); + debug!( + "Dropping node: {:p}, root: {:?}, parent: {:?}", + node, + node.root.as_ref().map(|r| &*r.p as *const RuleNode), + node.parent.as_ref().map(|p| &*p.p as *const RuleNode) + ); + + let should_drop = { + debug_assert!(node.refcount.load(Ordering::Relaxed) > 0); + node.refcount.fetch_sub(1, Ordering::Release) == 1 + }; + + if !should_drop { + // The refcount didn't even drop zero yet, there is nothing for us + // to do anymore. + return; + } + + unsafe { + if node.root.is_some() { + // This is a non-root node and we just observed the refcount + // dropping to zero, we need to pretend to be on the free list + // to unstuck any thread who tried to resurrect this node first + // through `WeakRuleNode::upgrade`. + RuleNode::pretend_to_be_on_free_list(&self.p); + + // Attempt to push the node on the free list. This may fail + // if the free list is gone. + if RuleNode::push_on_free_list(&self.p) { + return; + } + } + + // Either this was the last reference of the root node, or the + // tree rule is gone and there is no free list anymore. Drop the + // node. + RuleNode::drop_without_free_list(&mut self.p); + } + } +} + +impl WeakRuleNode { + /// Upgrades this weak node reference, returning a strong one. + /// + /// Must be called with items stored in a node's children list. The children + /// list must at least be read-locked when this is called. + unsafe fn upgrade(&self) -> StrongRuleNode { + debug!("Upgrading weak node: {:p}", &*self.p); + + if self.p.refcount.fetch_add(1, Ordering::Relaxed) == 0 { + // We observed a refcount of 0, we need to wait for this node to + // be put on the free list. Resetting the `next_free` pointer to + // null is only done in `RuleNode::drop_without_free_list`, just + // before a release refcount decrement, so this acquire fence here + // makes sure that we observed the write to null before we loop + // until there is a non-null value. + atomic::fence(Ordering::Acquire); + while self.p.next_free.load(Ordering::Relaxed).is_null() {} + } + StrongRuleNode::from_unsafe_box(UnsafeBox::clone(&self.p)) + } +} + +impl fmt::Debug for StrongRuleNode { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + (&*self.p as *const RuleNode).fmt(f) + } +} + +impl Eq for StrongRuleNode {} +impl PartialEq for StrongRuleNode { + fn eq(&self, other: &Self) -> bool { + &*self.p as *const RuleNode == &*other.p + } +} + +impl hash::Hash for StrongRuleNode { + fn hash<H>(&self, state: &mut H) + where + H: hash::Hasher, + { + (&*self.p as *const RuleNode).hash(state) + } +} + +// Large pages generate thousands of RuleNode objects. +size_of_test!(RuleNode, 80); +// StrongRuleNode should be pointer-sized even inside an option. +size_of_test!(Option<StrongRuleNode>, 8); diff --git a/servo/components/style/rule_tree/level.rs b/servo/components/style/rule_tree/level.rs new file mode 100644 index 0000000000..b8cbe55ed9 --- /dev/null +++ b/servo/components/style/rule_tree/level.rs @@ -0,0 +1,249 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![forbid(unsafe_code)] + +use crate::properties::Importance; +use crate::shared_lock::{SharedRwLockReadGuard, StylesheetGuards}; +use crate::stylesheets::Origin; + +/// The cascade level these rules are relevant at, as per[1][2][3]. +/// +/// Presentational hints for SVG and HTML are in the "author-level +/// zero-specificity" level, that is, right after user rules, and before author +/// rules. +/// +/// The order of variants declared here is significant, and must be in +/// _ascending_ order of precedence. +/// +/// See also [4] for the Shadow DOM bits. We rely on the invariant that rules +/// from outside the tree the element is in can't affect the element. +/// +/// The opposite is not true (i.e., :host and ::slotted) from an "inner" shadow +/// tree may affect an element connected to the document or an "outer" shadow +/// tree. +/// +/// [1]: https://drafts.csswg.org/css-cascade/#cascade-origin +/// [2]: https://drafts.csswg.org/css-cascade/#preshint +/// [3]: https://html.spec.whatwg.org/multipage/#presentational-hints +/// [4]: https://drafts.csswg.org/css-scoping/#shadow-cascading +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Ord, PartialEq, PartialOrd)] +pub enum CascadeLevel { + /// Normal User-Agent rules. + UANormal, + /// User normal rules. + UserNormal, + /// Presentational hints. + PresHints, + /// Shadow DOM styles from author styles. + AuthorNormal { + /// The order in the shadow tree hierarchy. This number is relative to + /// the tree of the element, and thus the only invariants that need to + /// be preserved is: + /// + /// * Zero is the same tree as the element that matched the rule. This + /// is important so that we can optimize style attribute insertions. + /// + /// * The levels are ordered in accordance with + /// https://drafts.csswg.org/css-scoping/#shadow-cascading + shadow_cascade_order: ShadowCascadeOrder, + }, + /// SVG SMIL animations. + SMILOverride, + /// CSS animations and script-generated animations. + Animations, + /// Author-supplied important rules. + AuthorImportant { + /// The order in the shadow tree hierarchy, inverted, so that PartialOrd + /// does the right thing. + shadow_cascade_order: ShadowCascadeOrder, + }, + /// User important rules. + UserImportant, + /// User-agent important rules. + UAImportant, + /// Transitions + Transitions, +} + +impl CascadeLevel { + /// Convert this level from "unimportant" to "important". + pub fn important(&self) -> Self { + match *self { + Self::UANormal => Self::UAImportant, + Self::UserNormal => Self::UserImportant, + Self::AuthorNormal { + shadow_cascade_order, + } => Self::AuthorImportant { + shadow_cascade_order: -shadow_cascade_order, + }, + Self::PresHints | + Self::SMILOverride | + Self::Animations | + Self::AuthorImportant { .. } | + Self::UserImportant | + Self::UAImportant | + Self::Transitions => *self, + } + } + + /// Convert this level from "important" to "non-important". + pub fn unimportant(&self) -> Self { + match *self { + Self::UAImportant => Self::UANormal, + Self::UserImportant => Self::UserNormal, + Self::AuthorImportant { + shadow_cascade_order, + } => Self::AuthorNormal { + shadow_cascade_order: -shadow_cascade_order, + }, + Self::PresHints | + Self::SMILOverride | + Self::Animations | + Self::AuthorNormal { .. } | + Self::UserNormal | + Self::UANormal | + Self::Transitions => *self, + } + } + + /// Select a lock guard for this level + pub fn guard<'a>(&self, guards: &'a StylesheetGuards<'a>) -> &'a SharedRwLockReadGuard<'a> { + match *self { + Self::UANormal | Self::UserNormal | Self::UserImportant | Self::UAImportant => { + guards.ua_or_user + }, + _ => guards.author, + } + } + + /// Returns the cascade level for author important declarations from the + /// same tree as the element. + #[inline] + pub fn same_tree_author_important() -> Self { + Self::AuthorImportant { + shadow_cascade_order: ShadowCascadeOrder::for_same_tree(), + } + } + + /// Returns the cascade level for author normal declarations from the same + /// tree as the element. + #[inline] + pub fn same_tree_author_normal() -> Self { + Self::AuthorNormal { + shadow_cascade_order: ShadowCascadeOrder::for_same_tree(), + } + } + + /// Returns whether this cascade level represents important rules of some + /// sort. + #[inline] + pub fn is_important(&self) -> bool { + match *self { + Self::AuthorImportant { .. } | Self::UserImportant | Self::UAImportant => true, + _ => false, + } + } + + /// Returns the importance relevant for this rule. Pretty similar to + /// `is_important`. + #[inline] + pub fn importance(&self) -> Importance { + if self.is_important() { + Importance::Important + } else { + Importance::Normal + } + } + + /// Returns the cascade origin of the rule. + #[inline] + pub fn origin(&self) -> Origin { + match *self { + Self::UAImportant | Self::UANormal => Origin::UserAgent, + Self::UserImportant | Self::UserNormal => Origin::User, + Self::PresHints | + Self::AuthorNormal { .. } | + Self::AuthorImportant { .. } | + Self::SMILOverride | + Self::Animations | + Self::Transitions => Origin::Author, + } + } + + /// Returns whether this cascade level represents an animation rules. + #[inline] + pub fn is_animation(&self) -> bool { + match *self { + Self::SMILOverride | Self::Animations | Self::Transitions => true, + _ => false, + } + } +} + +/// A counter to track how many shadow root rules deep we are. This is used to +/// handle: +/// +/// https://drafts.csswg.org/css-scoping/#shadow-cascading +/// +/// See the static functions for the meaning of different values. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Ord, PartialEq, PartialOrd)] +pub struct ShadowCascadeOrder(i8); + +impl ShadowCascadeOrder { + /// We keep a maximum of 3 bits of order as a limit so that we can pack + /// CascadeLevel in one byte by using half of it for the order, if that ends + /// up being necessary. + const MAX: i8 = 0b111; + const MIN: i8 = -Self::MAX; + + /// A level for the outermost shadow tree (the shadow tree we own, and the + /// ones from the slots we're slotted in). + #[inline] + pub fn for_outermost_shadow_tree() -> Self { + Self(-1) + } + + /// A level for the element's tree. + #[inline] + fn for_same_tree() -> Self { + Self(0) + } + + /// A level for the innermost containing tree (the one closest to the + /// element). + #[inline] + pub fn for_innermost_containing_tree() -> Self { + Self(1) + } + + /// Decrement the level, moving inwards. We should only move inwards if + /// we're traversing slots. + #[inline] + pub fn dec(&mut self) { + debug_assert!(self.0 < 0); + if self.0 != Self::MIN { + self.0 -= 1; + } + } + + /// The level, moving inwards. We should only move inwards if we're + /// traversing slots. + #[inline] + pub fn inc(&mut self) { + debug_assert_ne!(self.0, -1); + if self.0 != Self::MAX { + self.0 += 1; + } + } +} + +impl std::ops::Neg for ShadowCascadeOrder { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self(self.0.neg()) + } +} diff --git a/servo/components/style/rule_tree/map.rs b/servo/components/style/rule_tree/map.rs new file mode 100644 index 0000000000..33c470e9c1 --- /dev/null +++ b/servo/components/style/rule_tree/map.rs @@ -0,0 +1,201 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![forbid(unsafe_code)] + +use fxhash::FxHashMap; +use malloc_size_of::{MallocShallowSizeOf, MallocSizeOfOps}; +use std::collections::hash_map; +use std::hash::Hash; +use std::mem; + +pub(super) struct Map<K, V> { + inner: MapInner<K, V>, +} + +enum MapInner<K, V> { + Empty, + One(V), + Map(Box<FxHashMap<K, V>>), +} + +pub(super) struct MapIter<'a, K, V> { + inner: MapIterInner<'a, K, V>, +} + +enum MapIterInner<'a, K, V> { + One(std::option::IntoIter<&'a V>), + Map(std::collections::hash_map::Values<'a, K, V>), +} + +pub(super) enum Entry<'a, K, V> { + Occupied(&'a mut V), + Vacant(VacantEntry<'a, K, V>), +} + +pub(super) struct VacantEntry<'a, K, V> { + inner: VacantEntryInner<'a, K, V>, +} + +enum VacantEntryInner<'a, K, V> { + One(&'a mut MapInner<K, V>), + Map(hash_map::VacantEntry<'a, K, V>), +} + +impl<K, V> Default for Map<K, V> { + fn default() -> Self { + Map { + inner: MapInner::Empty, + } + } +} + +impl<'a, K, V> IntoIterator for &'a Map<K, V> { + type Item = &'a V; + type IntoIter = MapIter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + MapIter { + inner: match &self.inner { + MapInner::Empty => MapIterInner::One(None.into_iter()), + MapInner::One(one) => MapIterInner::One(Some(one).into_iter()), + MapInner::Map(map) => MapIterInner::Map(map.values()), + }, + } + } +} + +impl<'a, K, V> Iterator for MapIter<'a, K, V> { + type Item = &'a V; + + fn next(&mut self) -> Option<Self::Item> { + match &mut self.inner { + MapIterInner::One(one_iter) => one_iter.next(), + MapIterInner::Map(map_iter) => map_iter.next(), + } + } +} + +impl<K, V> Map<K, V> +where + K: Eq + Hash, +{ + pub(super) fn is_empty(&self) -> bool { + match &self.inner { + MapInner::Empty => true, + MapInner::One(_) => false, + MapInner::Map(map) => map.is_empty(), + } + } + + #[cfg(debug_assertions)] + pub(super) fn len(&self) -> usize { + match &self.inner { + MapInner::Empty => 0, + MapInner::One(_) => 1, + MapInner::Map(map) => map.len(), + } + } + + pub(super) fn get(&self, key: &K, key_from_value: impl FnOnce(&V) -> K) -> Option<&V> { + match &self.inner { + MapInner::One(one) if *key == key_from_value(one) => Some(one), + MapInner::Map(map) => map.get(key), + MapInner::Empty | MapInner::One(_) => None, + } + } + + pub(super) fn entry( + &mut self, + key: K, + key_from_value: impl FnOnce(&V) -> K, + ) -> Entry<'_, K, V> { + match self.inner { + ref mut inner @ MapInner::Empty => Entry::Vacant(VacantEntry { + inner: VacantEntryInner::One(inner), + }), + MapInner::One(_) => { + let one = match mem::replace(&mut self.inner, MapInner::Empty) { + MapInner::One(one) => one, + _ => unreachable!(), + }; + // If this panics, the child `one` will be lost. + let one_key = key_from_value(&one); + // Same for the equality test. + if key == one_key { + self.inner = MapInner::One(one); + let one = match &mut self.inner { + MapInner::One(one) => one, + _ => unreachable!(), + }; + return Entry::Occupied(one); + } + self.inner = MapInner::Map(Box::new(FxHashMap::with_capacity_and_hasher( + 2, + Default::default(), + ))); + let map = match &mut self.inner { + MapInner::Map(map) => map, + _ => unreachable!(), + }; + map.insert(one_key, one); + match map.entry(key) { + hash_map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry { + inner: VacantEntryInner::Map(entry), + }), + _ => unreachable!(), + } + }, + MapInner::Map(ref mut map) => match map.entry(key) { + hash_map::Entry::Occupied(entry) => Entry::Occupied(entry.into_mut()), + hash_map::Entry::Vacant(entry) => Entry::Vacant(VacantEntry { + inner: VacantEntryInner::Map(entry), + }), + }, + } + } + + pub(super) fn remove(&mut self, key: &K, key_from_value: impl FnOnce(&V) -> K) -> Option<V> { + match &mut self.inner { + MapInner::One(one) if *key == key_from_value(one) => { + match mem::replace(&mut self.inner, MapInner::Empty) { + MapInner::One(one) => Some(one), + _ => unreachable!(), + } + }, + MapInner::Map(map) => map.remove(key), + MapInner::Empty | MapInner::One(_) => None, + } + } +} + +impl<'a, K, V> VacantEntry<'a, K, V> { + pub(super) fn insert(self, value: V) -> &'a mut V { + match self.inner { + VacantEntryInner::One(map) => { + *map = MapInner::One(value); + match map { + MapInner::One(one) => one, + _ => unreachable!(), + } + }, + VacantEntryInner::Map(entry) => entry.insert(value), + } + } +} + +impl<K, V> MallocShallowSizeOf for Map<K, V> +where + K: Eq + Hash, +{ + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match &self.inner { + MapInner::Map(m) => { + // We want to account for both the box and the hashmap. + m.shallow_size_of(ops) + (**m).shallow_size_of(ops) + }, + MapInner::One(_) | MapInner::Empty => 0, + } + } +} diff --git a/servo/components/style/rule_tree/mod.rs b/servo/components/style/rule_tree/mod.rs new file mode 100644 index 0000000000..18ee018d64 --- /dev/null +++ b/servo/components/style/rule_tree/mod.rs @@ -0,0 +1,403 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![deny(unsafe_code)] + +//! The rule tree. + +use crate::applicable_declarations::{ApplicableDeclarationList, CascadePriority}; +use crate::properties::{LonghandIdSet, PropertyDeclarationBlock}; +use crate::shared_lock::{Locked, StylesheetGuards}; +use crate::stylesheets::layer_rule::LayerOrder; +use servo_arc::ArcBorrow; +use smallvec::SmallVec; +use std::io::{self, Write}; + +mod core; +mod level; +mod map; +mod source; +mod unsafe_box; + +pub use self::core::{RuleTree, StrongRuleNode}; +pub use self::level::{CascadeLevel, ShadowCascadeOrder}; +pub use self::source::StyleSource; + +impl RuleTree { + fn dump<W: Write>(&self, guards: &StylesheetGuards, writer: &mut W) { + let _ = writeln!(writer, " + RuleTree"); + self.root().dump(guards, writer, 0); + } + + /// Dump the rule tree to stdout. + pub fn dump_stdout(&self, guards: &StylesheetGuards) { + let mut stdout = io::stdout(); + self.dump(guards, &mut stdout); + } + + /// Inserts the given rules, that must be in proper order by specifity, and + /// returns the corresponding rule node representing the last inserted one. + /// + /// !important rules are detected and inserted into the appropriate position + /// in the rule tree. This allows selector matching to ignore importance, + /// while still maintaining the appropriate cascade order in the rule tree. + pub fn insert_ordered_rules_with_important<'a, I>( + &self, + iter: I, + guards: &StylesheetGuards, + ) -> StrongRuleNode + where + I: Iterator<Item = (StyleSource, CascadePriority)>, + { + use self::CascadeLevel::*; + let mut current = self.root().clone(); + + let mut found_important = false; + + let mut important_author = SmallVec::<[(StyleSource, CascadePriority); 4]>::new(); + let mut important_user = SmallVec::<[(StyleSource, CascadePriority); 4]>::new(); + let mut important_ua = SmallVec::<[(StyleSource, CascadePriority); 4]>::new(); + let mut transition = None; + + for (source, priority) in iter { + let level = priority.cascade_level(); + debug_assert!(!level.is_important(), "Important levels handled internally"); + + let any_important = { + let pdb = source.read(level.guard(guards)); + pdb.any_important() + }; + + if any_important { + found_important = true; + match level { + AuthorNormal { .. } => { + important_author.push((source.clone(), priority.important())) + }, + UANormal => important_ua.push((source.clone(), priority.important())), + UserNormal => important_user.push((source.clone(), priority.important())), + _ => {}, + }; + } + + // We don't optimize out empty rules, even though we could. + // + // Inspector relies on every rule being inserted in the normal level + // at least once, in order to return the rules with the correct + // specificity order. + // + // TODO(emilio): If we want to apply these optimizations without + // breaking inspector's expectations, we'd need to run + // selector-matching again at the inspector's request. That may or + // may not be a better trade-off. + if matches!(level, Transitions) && found_important { + // There can be at most one transition, and it will come at + // the end of the iterator. Stash it and apply it after + // !important rules. + debug_assert!(transition.is_none()); + transition = Some(source); + } else { + current = current.ensure_child(self.root(), source, priority); + } + } + + // Early-return in the common case of no !important declarations. + if !found_important { + return current; + } + + // Insert important declarations, in order of increasing importance, + // followed by any transition rule. + // + // Important rules are sorted differently from unimportant ones by + // shadow order and cascade order. + if !important_author.is_empty() && + important_author.first().unwrap().1 != important_author.last().unwrap().1 + { + // We only need to sort if the important rules come from + // different trees, but we need this sort to be stable. + // + // FIXME(emilio): This could maybe be smarter, probably by chunking + // the important rules while inserting, and iterating the outer + // chunks in reverse order. + // + // That is, if we have rules with levels like: -1 -1 -1 0 0 0 1 1 1, + // we're really only sorting the chunks, while keeping elements + // inside the same chunk already sorted. Seems like we could try to + // keep a SmallVec-of-SmallVecs with the chunks and just iterate the + // outer in reverse. + important_author.sort_by_key(|&(_, priority)| priority); + } + + for (source, priority) in important_author.drain(..) { + current = current.ensure_child(self.root(), source, priority); + } + + for (source, priority) in important_user.drain(..) { + current = current.ensure_child(self.root(), source, priority); + } + + for (source, priority) in important_ua.drain(..) { + current = current.ensure_child(self.root(), source, priority); + } + + if let Some(source) = transition { + current = current.ensure_child( + self.root(), + source, + CascadePriority::new(Transitions, LayerOrder::root()), + ); + } + + current + } + + /// Given a list of applicable declarations, insert the rules and return the + /// corresponding rule node. + pub fn compute_rule_node( + &self, + applicable_declarations: &mut ApplicableDeclarationList, + guards: &StylesheetGuards, + ) -> StrongRuleNode { + self.insert_ordered_rules_with_important( + applicable_declarations.drain(..).map(|d| d.for_rule_tree()), + guards, + ) + } + + /// Insert the given rules, that must be in proper order by specifity, and + /// return the corresponding rule node representing the last inserted one. + pub fn insert_ordered_rules<'a, I>(&self, iter: I) -> StrongRuleNode + where + I: Iterator<Item = (StyleSource, CascadePriority)>, + { + self.insert_ordered_rules_from(self.root().clone(), iter) + } + + fn insert_ordered_rules_from<'a, I>(&self, from: StrongRuleNode, iter: I) -> StrongRuleNode + where + I: Iterator<Item = (StyleSource, CascadePriority)>, + { + let mut current = from; + for (source, priority) in iter { + current = current.ensure_child(self.root(), source, priority); + } + current + } + + /// Replaces a rule in a given level (if present) for another rule. + /// + /// Returns the resulting node that represents the new path, or None if + /// the old path is still valid. + pub fn update_rule_at_level( + &self, + level: CascadeLevel, + layer_order: LayerOrder, + pdb: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>, + path: &StrongRuleNode, + guards: &StylesheetGuards, + important_rules_changed: &mut bool, + ) -> Option<StrongRuleNode> { + // TODO(emilio): Being smarter with lifetimes we could avoid a bit of + // the refcount churn. + let mut current = path.clone(); + *important_rules_changed = false; + + // First walk up until the first less-or-equally specific rule. + let mut children = SmallVec::<[_; 10]>::new(); + while current.cascade_priority().cascade_level() > level { + children.push(( + current.style_source().unwrap().clone(), + current.cascade_priority(), + )); + current = current.parent().unwrap().clone(); + } + + // Then remove the one at the level we want to replace, if any. + // + // NOTE: Here we assume that only one rule can be at the level we're + // replacing. + // + // This is certainly true for HTML style attribute rules, animations and + // transitions, but could not be so for SMIL animations, which we'd need + // to special-case (isn't hard, it's just about removing the `if` and + // special cases, and replacing them for a `while` loop, avoiding the + // optimizations). + if current.cascade_priority().cascade_level() == level { + *important_rules_changed |= level.is_important(); + + let current_decls = current.style_source().unwrap().as_declarations(); + + // If the only rule at the level we're replacing is exactly the + // same as `pdb`, we're done, and `path` is still valid. + if let (Some(ref pdb), Some(ref current_decls)) = (pdb, current_decls) { + // If the only rule at the level we're replacing is exactly the + // same as `pdb`, we're done, and `path` is still valid. + // + // TODO(emilio): Another potential optimization is the one where + // we can just replace the rule at that level for `pdb`, and + // then we don't need to re-create the children, and `path` is + // also equally valid. This is less likely, and would require an + // in-place mutation of the source, which is, at best, fiddly, + // so let's skip it for now. + let is_here_already = ArcBorrow::ptr_eq(pdb, current_decls); + if is_here_already { + debug!("Picking the fast path in rule replacement"); + return None; + } + } + + if current_decls.is_some() { + current = current.parent().unwrap().clone(); + } + } + + // Insert the rule if it's relevant at this level in the cascade. + // + // These optimizations are likely to be important, because the levels + // where replacements apply (style and animations) tend to trigger + // pretty bad styling cases already. + if let Some(pdb) = pdb { + if level.is_important() { + if pdb.read_with(level.guard(guards)).any_important() { + current = current.ensure_child( + self.root(), + StyleSource::from_declarations(pdb.clone_arc()), + CascadePriority::new(level, layer_order), + ); + *important_rules_changed = true; + } + } else { + if pdb.read_with(level.guard(guards)).any_normal() { + current = current.ensure_child( + self.root(), + StyleSource::from_declarations(pdb.clone_arc()), + CascadePriority::new(level, layer_order), + ); + } + } + } + + // Now the rule is in the relevant place, push the children as + // necessary. + let rule = self.insert_ordered_rules_from(current, children.drain(..).rev()); + Some(rule) + } + + /// Returns new rule nodes without Transitions level rule. + pub fn remove_transition_rule_if_applicable(&self, path: &StrongRuleNode) -> StrongRuleNode { + // Return a clone if there is no transition level. + if path.cascade_level() != CascadeLevel::Transitions { + return path.clone(); + } + + path.parent().unwrap().clone() + } + + /// Returns new rule node without rules from declarative animations. + pub fn remove_animation_rules(&self, path: &StrongRuleNode) -> StrongRuleNode { + // Return a clone if there are no animation rules. + if !path.has_animation_or_transition_rules() { + return path.clone(); + } + + let iter = path + .self_and_ancestors() + .take_while(|node| node.cascade_level() >= CascadeLevel::SMILOverride); + let mut last = path; + let mut children = SmallVec::<[_; 10]>::new(); + for node in iter { + if !node.cascade_level().is_animation() { + children.push(( + node.style_source().unwrap().clone(), + node.cascade_priority(), + )); + } + last = node; + } + + let rule = self + .insert_ordered_rules_from(last.parent().unwrap().clone(), children.drain(..).rev()); + rule + } +} + +impl StrongRuleNode { + /// Get an iterator for this rule node and its ancestors. + pub fn self_and_ancestors(&self) -> SelfAndAncestors { + SelfAndAncestors { + current: Some(self), + } + } + + /// Returns true if there is either animation or transition level rule. + pub fn has_animation_or_transition_rules(&self) -> bool { + self.self_and_ancestors() + .take_while(|node| node.cascade_level() >= CascadeLevel::SMILOverride) + .any(|node| node.cascade_level().is_animation()) + } + + /// Get a set of properties whose CascadeLevel are higher than Animations + /// but not equal to Transitions. + /// + /// If there are any custom properties, we set the boolean value of the + /// returned tuple to true. + pub fn get_properties_overriding_animations( + &self, + guards: &StylesheetGuards, + ) -> (LonghandIdSet, bool) { + use crate::properties::PropertyDeclarationId; + + // We want to iterate over cascade levels that override the animations + // level, i.e. !important levels and the transitions level. + // + // However, we actually want to skip the transitions level because + // although it is higher in the cascade than animations, when both + // transitions and animations are present for a given element and + // property, transitions are suppressed so that they don't actually + // override animations. + let iter = self + .self_and_ancestors() + .skip_while(|node| node.cascade_level() == CascadeLevel::Transitions) + .take_while(|node| node.cascade_level() > CascadeLevel::Animations); + let mut result = (LonghandIdSet::new(), false); + for node in iter { + let style = node.style_source().unwrap(); + for (decl, important) in style + .read(node.cascade_level().guard(guards)) + .declaration_importance_iter() + { + // Although we are only iterating over cascade levels that + // override animations, in a given property declaration block we + // can have a mixture of !important and non-!important + // declarations but only the !important declarations actually + // override animations. + if important.important() { + match decl.id() { + PropertyDeclarationId::Longhand(id) => result.0.insert(id), + PropertyDeclarationId::Custom(_) => result.1 = true, + } + } + } + } + result + } +} + +/// An iterator over a rule node and its ancestors. +#[derive(Clone)] +pub struct SelfAndAncestors<'a> { + current: Option<&'a StrongRuleNode>, +} + +impl<'a> Iterator for SelfAndAncestors<'a> { + type Item = &'a StrongRuleNode; + + fn next(&mut self) -> Option<Self::Item> { + self.current.map(|node| { + self.current = node.parent(); + node + }) + } +} diff --git a/servo/components/style/rule_tree/source.rs b/servo/components/style/rule_tree/source.rs new file mode 100644 index 0000000000..76443692d7 --- /dev/null +++ b/servo/components/style/rule_tree/source.rs @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![forbid(unsafe_code)] + +use crate::properties::PropertyDeclarationBlock; +use crate::shared_lock::{Locked, SharedRwLockReadGuard}; +use crate::stylesheets::StyleRule; +use servo_arc::{Arc, ArcBorrow, ArcUnion, ArcUnionBorrow}; +use std::io::Write; +use std::ptr; + +/// A style source for the rule node. It can either be a CSS style rule or a +/// declaration block. +/// +/// Note that, even though the declaration block from inside the style rule +/// could be enough to implement the rule tree, keeping the whole rule provides +/// more debuggability, and also the ability of show those selectors to +/// devtools. +#[derive(Clone, Debug)] +pub struct StyleSource(ArcUnion<Locked<StyleRule>, Locked<PropertyDeclarationBlock>>); + +impl PartialEq for StyleSource { + fn eq(&self, other: &Self) -> bool { + ArcUnion::ptr_eq(&self.0, &other.0) + } +} + +impl StyleSource { + /// Creates a StyleSource from a StyleRule. + pub fn from_rule(rule: Arc<Locked<StyleRule>>) -> Self { + StyleSource(ArcUnion::from_first(rule)) + } + + #[inline] + pub(super) fn key(&self) -> ptr::NonNull<()> { + self.0.ptr() + } + + /// Creates a StyleSource from a PropertyDeclarationBlock. + pub fn from_declarations(decls: Arc<Locked<PropertyDeclarationBlock>>) -> Self { + StyleSource(ArcUnion::from_second(decls)) + } + + pub(super) fn dump<W: Write>(&self, guard: &SharedRwLockReadGuard, writer: &mut W) { + if let Some(ref rule) = self.0.as_first() { + let rule = rule.read_with(guard); + let _ = write!(writer, "{:?}", rule.selectors); + } + + let _ = write!(writer, " -> {:?}", self.read(guard).declarations()); + } + + /// Read the style source guard, and obtain thus read access to the + /// underlying property declaration block. + #[inline] + pub fn read<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a PropertyDeclarationBlock { + let block: &Locked<PropertyDeclarationBlock> = match self.0.borrow() { + ArcUnionBorrow::First(ref rule) => &rule.get().read_with(guard).block, + ArcUnionBorrow::Second(ref block) => block.get(), + }; + block.read_with(guard) + } + + /// Returns the style rule if applicable, otherwise None. + pub fn as_rule(&self) -> Option<ArcBorrow<Locked<StyleRule>>> { + self.0.as_first() + } + + /// Returns the declaration block if applicable, otherwise None. + pub fn as_declarations(&self) -> Option<ArcBorrow<Locked<PropertyDeclarationBlock>>> { + self.0.as_second() + } +} diff --git a/servo/components/style/rule_tree/unsafe_box.rs b/servo/components/style/rule_tree/unsafe_box.rs new file mode 100644 index 0000000000..eaa441d7b2 --- /dev/null +++ b/servo/components/style/rule_tree/unsafe_box.rs @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +use std::mem::ManuallyDrop; +use std::ops::Deref; +use std::ptr; + +/// An unsafe box, derefs to `T`. +pub(super) struct UnsafeBox<T> { + inner: ManuallyDrop<Box<T>>, +} + +impl<T> UnsafeBox<T> { + /// Creates a new unsafe box. + pub(super) fn from_box(value: Box<T>) -> Self { + Self { + inner: ManuallyDrop::new(value), + } + } + + /// Creates a new box from a pointer. + /// + /// # Safety + /// + /// The input should point to a valid `T`. + pub(super) unsafe fn from_raw(ptr: *mut T) -> Self { + Self { + inner: ManuallyDrop::new(Box::from_raw(ptr)), + } + } + + /// Creates a new unsafe box from an existing one. + /// + /// # Safety + /// + /// There is no refcounting or whatever else in an unsafe box, so this + /// operation can lead to double frees. + pub(super) unsafe fn clone(this: &Self) -> Self { + Self { + inner: ptr::read(&this.inner), + } + } + + /// Returns a mutable reference to the inner value of this unsafe box. + /// + /// # Safety + /// + /// Given `Self::clone`, nothing prevents anyone from creating + /// multiple mutable references to the inner value, which is completely UB. + pub(crate) unsafe fn deref_mut(this: &mut Self) -> &mut T { + &mut this.inner + } + + /// Drops the inner value of this unsafe box. + /// + /// # Safety + /// + /// Given this doesn't consume the unsafe box itself, this has the same + /// safety caveats as `ManuallyDrop::drop`. + pub(super) unsafe fn drop(this: &mut Self) { + ManuallyDrop::drop(&mut this.inner) + } +} + +impl<T> Deref for UnsafeBox<T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} diff --git a/servo/components/style/scoped_tls.rs b/servo/components/style/scoped_tls.rs new file mode 100644 index 0000000000..0d3267397a --- /dev/null +++ b/servo/components/style/scoped_tls.rs @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Stack-scoped thread-local storage for rayon thread pools. + +#![allow(unsafe_code)] +#![deny(missing_docs)] + +use crate::global_style_data::STYLO_MAX_THREADS; +use rayon; +use std::cell::{Ref, RefCell, RefMut}; +use std::ops::DerefMut; + +/// A scoped TLS set, that is alive during the `'scope` lifetime. +/// +/// We use this on Servo to construct thread-local contexts, but clear them once +/// we're done with restyling. +/// +/// Note that the cleanup is done on the thread that owns the scoped TLS, thus +/// the Send bound. +pub struct ScopedTLS<'scope, T: Send> { + pool: Option<&'scope rayon::ThreadPool>, + slots: [RefCell<Option<T>>; STYLO_MAX_THREADS], +} + +/// The scoped TLS is `Sync` because no more than one worker thread can access a +/// given slot. +unsafe impl<'scope, T: Send> Sync for ScopedTLS<'scope, T> {} + +impl<'scope, T: Send> ScopedTLS<'scope, T> { + /// Create a new scoped TLS that will last as long as this rayon threadpool + /// reference. + pub fn new(pool: Option<&'scope rayon::ThreadPool>) -> Self { + debug_assert!(pool.map_or(true, |p| p.current_num_threads() <= STYLO_MAX_THREADS)); + ScopedTLS { + pool, + slots: Default::default(), + } + } + + /// Returns the index corresponding to the calling thread in the thread pool. + #[inline] + pub fn current_thread_index(&self) -> usize { + self.pool.map_or(0, |p| p.current_thread_index().unwrap()) + } + + /// Return an immutable reference to the `Option<T>` that this thread owns. + pub fn borrow(&self) -> Ref<Option<T>> { + let idx = self.current_thread_index(); + self.slots[idx].borrow() + } + + /// Return a mutable reference to the `Option<T>` that this thread owns. + pub fn borrow_mut(&self) -> RefMut<Option<T>> { + let idx = self.current_thread_index(); + self.slots[idx].borrow_mut() + } + + /// Ensure that the current data this thread owns is initialized, or + /// initialize it using `f`. We want ensure() to be fast and inline, and we + /// want to inline the memmove that initializes the Option<T>. But we don't + /// want to inline space for the entire large T struct in our stack frame. + /// That's why we hand `f` a mutable borrow to write to instead of just + /// having it return a T. + #[inline(always)] + pub fn ensure<F: FnOnce(&mut Option<T>)>(&self, f: F) -> RefMut<T> { + let mut opt = self.borrow_mut(); + if opt.is_none() { + f(opt.deref_mut()); + } + + RefMut::map(opt, |x| x.as_mut().unwrap()) + } + + /// Returns the slots. Safe because if we have a mut reference the tls can't be referenced by + /// any other thread. + pub fn slots(&mut self) -> &mut [RefCell<Option<T>>] { + &mut self.slots + } +} diff --git a/servo/components/style/selector_map.rs b/servo/components/style/selector_map.rs new file mode 100644 index 0000000000..2b8d6add55 --- /dev/null +++ b/servo/components/style/selector_map.rs @@ -0,0 +1,870 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A data structure to efficiently index structs containing selectors by local +//! name, ids and hash. + +use crate::applicable_declarations::ApplicableDeclarationList; +use crate::context::QuirksMode; +use crate::dom::TElement; +use crate::rule_tree::CascadeLevel; +use crate::selector_parser::SelectorImpl; +use crate::stylist::{CascadeData, ContainerConditionId, Rule, Stylist}; +use crate::AllocErr; +use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded, WeakAtom}; +use dom::ElementState; +use precomputed_hash::PrecomputedHash; +use selectors::matching::{matches_selector, MatchingContext}; +use selectors::parser::{Combinator, Component, SelectorIter}; +use smallvec::SmallVec; +use std::collections::hash_map; +use std::collections::{HashMap, HashSet}; +use std::hash::{BuildHasherDefault, Hash, Hasher}; + +/// A hasher implementation that doesn't hash anything, because it expects its +/// input to be a suitable u32 hash. +pub struct PrecomputedHasher { + hash: Option<u32>, +} + +impl Default for PrecomputedHasher { + fn default() -> Self { + Self { hash: None } + } +} + +/// A vector of relevant attributes, that can be useful for revalidation. +pub type RelevantAttributes = thin_vec::ThinVec<LocalName>; + +/// This is a set of pseudo-classes that are both relatively-rare (they don't +/// affect most elements by default) and likely or known to have global rules +/// (in e.g., the UA sheets). +/// +/// We can avoid selector-matching those global rules for all elements without +/// these pseudo-class states. +const RARE_PSEUDO_CLASS_STATES: ElementState = ElementState::from_bits_retain( + ElementState::FULLSCREEN.bits() | + ElementState::VISITED_OR_UNVISITED.bits() | + ElementState::URLTARGET.bits() | + ElementState::INERT.bits() | + ElementState::FOCUS.bits() | + ElementState::FOCUSRING.bits() | + ElementState::TOPMOST_MODAL.bits(), +); + +/// A simple alias for a hashmap using PrecomputedHasher. +pub type PrecomputedHashMap<K, V> = HashMap<K, V, BuildHasherDefault<PrecomputedHasher>>; + +/// A simple alias for a hashset using PrecomputedHasher. +pub type PrecomputedHashSet<K> = HashSet<K, BuildHasherDefault<PrecomputedHasher>>; + +impl Hasher for PrecomputedHasher { + #[inline] + fn write(&mut self, _: &[u8]) { + unreachable!( + "Called into PrecomputedHasher with something that isn't \ + a u32" + ) + } + + #[inline] + fn write_u32(&mut self, i: u32) { + debug_assert!(self.hash.is_none()); + self.hash = Some(i); + } + + #[inline] + fn finish(&self) -> u64 { + self.hash.expect("PrecomputedHasher wasn't fed?") as u64 + } +} + +/// A trait to abstract over a given selector map entry. +pub trait SelectorMapEntry: Sized + Clone { + /// Gets the selector we should use to index in the selector map. + fn selector(&self) -> SelectorIter<SelectorImpl>; +} + +/// Map element data to selector-providing objects for which the last simple +/// selector starts with them. +/// +/// e.g., +/// "p > img" would go into the set of selectors corresponding to the +/// element "img" +/// "a .foo .bar.baz" would go into the set of selectors corresponding to +/// the class "bar" +/// +/// Because we match selectors right-to-left (i.e., moving up the tree +/// from an element), we need to compare the last simple selector in the +/// selector with the element. +/// +/// So, if an element has ID "id1" and classes "foo" and "bar", then all +/// the rules it matches will have their last simple selector starting +/// either with "#id1" or with ".foo" or with ".bar". +/// +/// Hence, the union of the rules keyed on each of element's classes, ID, +/// element name, etc. will contain the Selectors that actually match that +/// element. +/// +/// We use a 1-entry SmallVec to avoid a separate heap allocation in the case +/// where we only have one entry, which is quite common. See measurements in: +/// * https://bugzilla.mozilla.org/show_bug.cgi?id=1363789#c5 +/// * https://bugzilla.mozilla.org/show_bug.cgi?id=681755 +/// +/// TODO: Tune the initial capacity of the HashMap +#[derive(Clone, Debug, MallocSizeOf)] +pub struct SelectorMap<T: 'static> { + /// Rules that have `:root` selectors. + pub root: SmallVec<[T; 1]>, + /// A hash from an ID to rules which contain that ID selector. + pub id_hash: MaybeCaseInsensitiveHashMap<Atom, SmallVec<[T; 1]>>, + /// A hash from a class name to rules which contain that class selector. + pub class_hash: MaybeCaseInsensitiveHashMap<Atom, SmallVec<[T; 1]>>, + /// A hash from local name to rules which contain that local name selector. + pub local_name_hash: PrecomputedHashMap<LocalName, SmallVec<[T; 1]>>, + /// A hash from attributes to rules which contain that attribute selector. + pub attribute_hash: PrecomputedHashMap<LocalName, SmallVec<[T; 1]>>, + /// A hash from namespace to rules which contain that namespace selector. + pub namespace_hash: PrecomputedHashMap<Namespace, SmallVec<[T; 1]>>, + /// Rules for pseudo-states that are rare but have global selectors. + pub rare_pseudo_classes: SmallVec<[T; 1]>, + /// All other rules. + pub other: SmallVec<[T; 1]>, + /// The number of entries in this map. + pub count: usize, +} + +impl<T: 'static> Default for SelectorMap<T> { + #[inline] + fn default() -> Self { + Self::new() + } +} + +impl<T> SelectorMap<T> { + /// Trivially constructs an empty `SelectorMap`. + pub fn new() -> Self { + SelectorMap { + root: SmallVec::new(), + id_hash: MaybeCaseInsensitiveHashMap::new(), + class_hash: MaybeCaseInsensitiveHashMap::new(), + attribute_hash: HashMap::default(), + local_name_hash: HashMap::default(), + namespace_hash: HashMap::default(), + rare_pseudo_classes: SmallVec::new(), + other: SmallVec::new(), + count: 0, + } + } + + /// Shrink the capacity of the map if needed. + pub fn shrink_if_needed(&mut self) { + self.id_hash.shrink_if_needed(); + self.class_hash.shrink_if_needed(); + self.attribute_hash.shrink_if_needed(); + self.local_name_hash.shrink_if_needed(); + self.namespace_hash.shrink_if_needed(); + } + + /// Clears the hashmap retaining storage. + pub fn clear(&mut self) { + self.root.clear(); + self.id_hash.clear(); + self.class_hash.clear(); + self.attribute_hash.clear(); + self.local_name_hash.clear(); + self.namespace_hash.clear(); + self.rare_pseudo_classes.clear(); + self.other.clear(); + self.count = 0; + } + + /// Returns whether there are any entries in the map. + pub fn is_empty(&self) -> bool { + self.count == 0 + } + + /// Returns the number of entries. + pub fn len(&self) -> usize { + self.count + } +} + +impl SelectorMap<Rule> { + /// Append to `rule_list` all Rules in `self` that match element. + /// + /// Extract matching rules as per element's ID, classes, tag name, etc.. + /// Sort the Rules at the end to maintain cascading order. + pub fn get_all_matching_rules<E>( + &self, + element: E, + rule_hash_target: E, + matching_rules_list: &mut ApplicableDeclarationList, + matching_context: &mut MatchingContext<E::Impl>, + cascade_level: CascadeLevel, + cascade_data: &CascadeData, + stylist: &Stylist, + ) where + E: TElement, + { + if self.is_empty() { + return; + } + + let quirks_mode = matching_context.quirks_mode(); + + if rule_hash_target.is_root() { + SelectorMap::get_matching_rules( + element, + &self.root, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ); + } + + if let Some(id) = rule_hash_target.id() { + if let Some(rules) = self.id_hash.get(id, quirks_mode) { + SelectorMap::get_matching_rules( + element, + rules, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ) + } + } + + rule_hash_target.each_class(|class| { + if let Some(rules) = self.class_hash.get(&class, quirks_mode) { + SelectorMap::get_matching_rules( + element, + rules, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ) + } + }); + + rule_hash_target.each_attr_name(|name| { + if let Some(rules) = self.attribute_hash.get(name) { + SelectorMap::get_matching_rules( + element, + rules, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ) + } + }); + + if let Some(rules) = self.local_name_hash.get(rule_hash_target.local_name()) { + SelectorMap::get_matching_rules( + element, + rules, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ) + } + + if rule_hash_target + .state() + .intersects(RARE_PSEUDO_CLASS_STATES) + { + SelectorMap::get_matching_rules( + element, + &self.rare_pseudo_classes, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ); + } + + if let Some(rules) = self.namespace_hash.get(rule_hash_target.namespace()) { + SelectorMap::get_matching_rules( + element, + rules, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ) + } + + SelectorMap::get_matching_rules( + element, + &self.other, + matching_rules_list, + matching_context, + cascade_level, + cascade_data, + stylist, + ); + } + + /// Adds rules in `rules` that match `element` to the `matching_rules` list. + pub(crate) fn get_matching_rules<E>( + element: E, + rules: &[Rule], + matching_rules: &mut ApplicableDeclarationList, + matching_context: &mut MatchingContext<E::Impl>, + cascade_level: CascadeLevel, + cascade_data: &CascadeData, + stylist: &Stylist, + ) where + E: TElement, + { + for rule in rules { + if !matches_selector( + &rule.selector, + 0, + Some(&rule.hashes), + &element, + matching_context, + ) { + continue; + } + + if rule.container_condition_id != ContainerConditionId::none() { + if !cascade_data.container_condition_matches( + rule.container_condition_id, + stylist, + element, + matching_context, + ) { + continue; + } + } + + matching_rules.push(rule.to_applicable_declaration_block(cascade_level, cascade_data)); + } + } +} + +impl<T: SelectorMapEntry> SelectorMap<T> { + /// Inserts an entry into the correct bucket(s). + pub fn insert(&mut self, entry: T, quirks_mode: QuirksMode) -> Result<(), AllocErr> { + self.count += 1; + + // NOTE(emilio): It'd be nice for this to be a separate function, but + // then the compiler can't reason about the lifetime dependency between + // `entry` and `bucket`, and would force us to clone the rule in the + // common path. + macro_rules! insert_into_bucket { + ($entry:ident, $bucket:expr) => {{ + let vec = match $bucket { + Bucket::Root => &mut self.root, + Bucket::ID(id) => self + .id_hash + .try_entry(id.clone(), quirks_mode)? + .or_default(), + Bucket::Class(class) => self + .class_hash + .try_entry(class.clone(), quirks_mode)? + .or_default(), + Bucket::Attribute { name, lower_name } | + Bucket::LocalName { name, lower_name } => { + // If the local name in the selector isn't lowercase, + // insert it into the rule hash twice. This means that, + // during lookup, we can always find the rules based on + // the local name of the element, regardless of whether + // it's an html element in an html document (in which + // case we match against lower_name) or not (in which + // case we match against name). + // + // In the case of a non-html-element-in-html-document + // with a lowercase localname and a non-lowercase + // selector, the rulehash lookup may produce superfluous + // selectors, but the subsequent selector matching work + // will filter them out. + let is_attribute = matches!($bucket, Bucket::Attribute { .. }); + let hash = if is_attribute { + &mut self.attribute_hash + } else { + &mut self.local_name_hash + }; + if name != lower_name { + hash.try_reserve(1)?; + let vec = hash.entry(lower_name.clone()).or_default(); + vec.try_reserve(1)?; + vec.push($entry.clone()); + } + hash.try_reserve(1)?; + hash.entry(name.clone()).or_default() + }, + Bucket::Namespace(url) => { + self.namespace_hash.try_reserve(1)?; + self.namespace_hash.entry(url.clone()).or_default() + }, + Bucket::RarePseudoClasses => &mut self.rare_pseudo_classes, + Bucket::Universal => &mut self.other, + }; + vec.try_reserve(1)?; + vec.push($entry); + }}; + } + + let bucket = { + let mut disjoint_buckets = SmallVec::new(); + let bucket = find_bucket( + entry.selector(), + &mut disjoint_buckets, + ); + + // See if inserting this selector in multiple entries in the + // selector map would be worth it. Consider a case like: + // + // .foo:where(div, #bar) + // + // There, `bucket` would be `Class(foo)`, and disjoint_buckets would + // be `[LocalName { div }, ID(bar)]`. + // + // Here we choose to insert the selector in the `.foo` bucket in + // such a case, as it's likely more worth it than inserting it in + // both `div` and `#bar`. + // + // This is specially true if there's any universal selector in the + // `disjoint_selectors` set, at which point we'd just be doing + // wasted work. + if !disjoint_buckets.is_empty() && + disjoint_buckets + .iter() + .all(|b| b.more_specific_than(&bucket)) + { + for bucket in &disjoint_buckets { + let entry = entry.clone(); + insert_into_bucket!(entry, *bucket); + } + return Ok(()); + } + bucket + }; + + insert_into_bucket!(entry, bucket); + Ok(()) + } + + /// Looks up entries by id, class, local name, namespace, and other (in + /// order). + /// + /// Each entry is passed to the callback, which returns true to continue + /// iterating entries, or false to terminate the lookup. + /// + /// Returns false if the callback ever returns false. + /// + /// FIXME(bholley) This overlaps with SelectorMap<Rule>::get_all_matching_rules, + /// but that function is extremely hot and I'd rather not rearrange it. + pub fn lookup<'a, E, F>( + &'a self, + element: E, + quirks_mode: QuirksMode, + relevant_attributes: Option<&mut RelevantAttributes>, + f: F, + ) -> bool + where + E: TElement, + F: FnMut(&'a T) -> bool, + { + self.lookup_with_state( + element, + element.state(), + quirks_mode, + relevant_attributes, + f, + ) + } + + #[inline] + fn lookup_with_state<'a, E, F>( + &'a self, + element: E, + element_state: ElementState, + quirks_mode: QuirksMode, + mut relevant_attributes: Option<&mut RelevantAttributes>, + mut f: F, + ) -> bool + where + E: TElement, + F: FnMut(&'a T) -> bool, + { + if element.is_root() { + for entry in self.root.iter() { + if !f(&entry) { + return false; + } + } + } + + if let Some(id) = element.id() { + if let Some(v) = self.id_hash.get(id, quirks_mode) { + for entry in v.iter() { + if !f(&entry) { + return false; + } + } + } + } + + let mut done = false; + element.each_class(|class| { + if done { + return; + } + if let Some(v) = self.class_hash.get(class, quirks_mode) { + for entry in v.iter() { + if !f(&entry) { + done = true; + return; + } + } + } + }); + + if done { + return false; + } + + element.each_attr_name(|name| { + if done { + return; + } + if let Some(v) = self.attribute_hash.get(name) { + if let Some(ref mut relevant_attributes) = relevant_attributes { + relevant_attributes.push(name.clone()); + } + for entry in v.iter() { + if !f(&entry) { + done = true; + return; + } + } + } + }); + + if done { + return false; + } + + if let Some(v) = self.local_name_hash.get(element.local_name()) { + for entry in v.iter() { + if !f(&entry) { + return false; + } + } + } + + if let Some(v) = self.namespace_hash.get(element.namespace()) { + for entry in v.iter() { + if !f(&entry) { + return false; + } + } + } + + if element_state.intersects(RARE_PSEUDO_CLASS_STATES) { + for entry in self.rare_pseudo_classes.iter() { + if !f(&entry) { + return false; + } + } + } + + for entry in self.other.iter() { + if !f(&entry) { + return false; + } + } + + true + } + + /// Performs a normal lookup, and also looks up entries for the passed-in + /// id and classes. + /// + /// Each entry is passed to the callback, which returns true to continue + /// iterating entries, or false to terminate the lookup. + /// + /// Returns false if the callback ever returns false. + #[inline] + pub fn lookup_with_additional<'a, E, F>( + &'a self, + element: E, + quirks_mode: QuirksMode, + additional_id: Option<&WeakAtom>, + additional_classes: &[Atom], + additional_states: ElementState, + mut f: F, + ) -> bool + where + E: TElement, + F: FnMut(&'a T) -> bool, + { + // Do the normal lookup. + if !self.lookup_with_state( + element, + element.state() | additional_states, + quirks_mode, + /* relevant_attributes = */ None, + |entry| f(entry), + ) { + return false; + } + + // Check the additional id. + if let Some(id) = additional_id { + if let Some(v) = self.id_hash.get(id, quirks_mode) { + for entry in v.iter() { + if !f(&entry) { + return false; + } + } + } + } + + // Check the additional classes. + for class in additional_classes { + if let Some(v) = self.class_hash.get(class, quirks_mode) { + for entry in v.iter() { + if !f(&entry) { + return false; + } + } + } + } + + true + } +} + +enum Bucket<'a> { + Universal, + Namespace(&'a Namespace), + RarePseudoClasses, + LocalName { + name: &'a LocalName, + lower_name: &'a LocalName, + }, + Attribute { + name: &'a LocalName, + lower_name: &'a LocalName, + }, + Class(&'a Atom), + ID(&'a Atom), + Root, +} + +impl<'a> Bucket<'a> { + /// root > id > class > local name > namespace > pseudo-classes > universal. + #[inline] + fn specificity(&self) -> usize { + match *self { + Bucket::Universal => 0, + Bucket::Namespace(..) => 1, + Bucket::RarePseudoClasses => 2, + Bucket::LocalName { .. } => 3, + Bucket::Attribute { .. } => 4, + Bucket::Class(..) => 5, + Bucket::ID(..) => 6, + Bucket::Root => 7, + } + } + + #[inline] + fn more_or_equally_specific_than(&self, other: &Self) -> bool { + self.specificity() >= other.specificity() + } + + #[inline] + fn more_specific_than(&self, other: &Self) -> bool { + self.specificity() > other.specificity() + } +} + +type DisjointBuckets<'a> = SmallVec<[Bucket<'a>; 5]>; + +fn specific_bucket_for<'a>( + component: &'a Component<SelectorImpl>, + disjoint_buckets: &mut DisjointBuckets<'a>, +) -> Bucket<'a> { + match *component { + Component::Root => Bucket::Root, + Component::ID(ref id) => Bucket::ID(id), + Component::Class(ref class) => Bucket::Class(class), + Component::AttributeInNoNamespace { ref local_name, .. } => { + Bucket::Attribute { + name: local_name, + lower_name: local_name, + } + }, + Component::AttributeInNoNamespaceExists { + ref local_name, + ref local_name_lower, + } => Bucket::Attribute { + name: local_name, + lower_name: local_name_lower, + }, + Component::AttributeOther(ref selector) => Bucket::Attribute { + name: &selector.local_name, + lower_name: &selector.local_name_lower, + }, + Component::LocalName(ref selector) => Bucket::LocalName { + name: &selector.name, + lower_name: &selector.lower_name, + }, + Component::Namespace(_, ref url) | Component::DefaultNamespace(ref url) => { + Bucket::Namespace(url) + }, + // ::slotted(..) isn't a normal pseudo-element, so we can insert it on + // the rule hash normally without much problem. For example, in a + // selector like: + // + // div::slotted(span)::before + // + // It looks like: + // + // [ + // LocalName(div), + // Combinator(SlotAssignment), + // Slotted(span), + // Combinator::PseudoElement, + // PseudoElement(::before), + // ] + // + // So inserting `span` in the rule hash makes sense since we want to + // match the slotted <span>. + Component::Slotted(ref selector) => { + find_bucket(selector.iter(), disjoint_buckets) + }, + Component::Host(Some(ref selector)) => { + find_bucket(selector.iter(), disjoint_buckets) + }, + Component::Is(ref list) | Component::Where(ref list) => { + if list.len() == 1 { + find_bucket(list.slice()[0].iter(), disjoint_buckets) + } else { + for selector in list.slice() { + let bucket = find_bucket(selector.iter(), disjoint_buckets); + disjoint_buckets.push(bucket); + } + Bucket::Universal + } + }, + Component::NonTSPseudoClass(ref pseudo_class) + if pseudo_class + .state_flag() + .intersects(RARE_PSEUDO_CLASS_STATES) => + { + Bucket::RarePseudoClasses + }, + _ => Bucket::Universal, + } +} + +/// Searches a compound selector from left to right, and returns the appropriate +/// bucket for it. +/// +/// It also populates disjoint_buckets with dependencies from nested selectors +/// with any semantics like :is() and :where(). +#[inline(always)] +fn find_bucket<'a>( + mut iter: SelectorIter<'a, SelectorImpl>, + disjoint_buckets: &mut DisjointBuckets<'a>, +) -> Bucket<'a> { + let mut current_bucket = Bucket::Universal; + + loop { + for ss in &mut iter { + let new_bucket = specific_bucket_for(ss, disjoint_buckets); + // NOTE: When presented with the choice of multiple specific selectors, use the + // rightmost, on the assumption that that's less common, see bug 1829540. + if new_bucket.more_or_equally_specific_than(¤t_bucket) { + current_bucket = new_bucket; + } + } + + // Effectively, pseudo-elements are ignored, given only state + // pseudo-classes may appear before them. + if iter.next_sequence() != Some(Combinator::PseudoElement) { + break; + } + } + + current_bucket +} + +/// Wrapper for PrecomputedHashMap that does ASCII-case-insensitive lookup in quirks mode. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct MaybeCaseInsensitiveHashMap<K: PrecomputedHash + Hash + Eq, V>(PrecomputedHashMap<K, V>); + +impl<V> Default for MaybeCaseInsensitiveHashMap<Atom, V> { + #[inline] + fn default() -> Self { + MaybeCaseInsensitiveHashMap(PrecomputedHashMap::default()) + } +} + +impl<V> MaybeCaseInsensitiveHashMap<Atom, V> { + /// Empty map + pub fn new() -> Self { + Self::default() + } + + /// Shrink the capacity of the map if needed. + pub fn shrink_if_needed(&mut self) { + self.0.shrink_if_needed() + } + + /// HashMap::try_entry + pub fn try_entry( + &mut self, + mut key: Atom, + quirks_mode: QuirksMode, + ) -> Result<hash_map::Entry<Atom, V>, AllocErr> { + if quirks_mode == QuirksMode::Quirks { + key = key.to_ascii_lowercase() + } + self.0.try_reserve(1)?; + Ok(self.0.entry(key)) + } + + /// HashMap::is_empty + #[inline] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// HashMap::iter + pub fn iter(&self) -> hash_map::Iter<Atom, V> { + self.0.iter() + } + + /// HashMap::clear + pub fn clear(&mut self) { + self.0.clear() + } + + /// HashMap::get + pub fn get(&self, key: &WeakAtom, quirks_mode: QuirksMode) -> Option<&V> { + if quirks_mode == QuirksMode::Quirks { + self.0.get(&key.to_ascii_lowercase()) + } else { + self.0.get(key) + } + } +} diff --git a/servo/components/style/selector_parser.rs b/servo/components/style/selector_parser.rs new file mode 100644 index 0000000000..4eef26f1b7 --- /dev/null +++ b/servo/components/style/selector_parser.rs @@ -0,0 +1,240 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The pseudo-classes and pseudo-elements supported by the style system. + +#![deny(missing_docs)] + +use crate::stylesheets::{Namespaces, Origin, UrlExtraData}; +use crate::values::serialize_atom_identifier; +use crate::Atom; +use cssparser::{Parser as CssParser, ParserInput}; +use dom::ElementState; +use selectors::parser::{ParseRelative, SelectorList}; +use std::fmt::{self, Debug, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A convenient alias for the type that represents an attribute value used for +/// selector parser implementation. +pub type AttrValue = <SelectorImpl as ::selectors::SelectorImpl>::AttrValue; + +#[cfg(feature = "servo")] +pub use crate::servo::selector_parser::*; + +#[cfg(feature = "gecko")] +pub use crate::gecko::selector_parser::*; + +#[cfg(feature = "servo")] +pub use crate::servo::selector_parser::ServoElementSnapshot as Snapshot; + +#[cfg(feature = "gecko")] +pub use crate::gecko::snapshot::GeckoElementSnapshot as Snapshot; + +#[cfg(feature = "servo")] +pub use crate::servo::restyle_damage::ServoRestyleDamage as RestyleDamage; + +#[cfg(feature = "gecko")] +pub use crate::gecko::restyle_damage::GeckoRestyleDamage as RestyleDamage; + +/// Servo's selector parser. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct SelectorParser<'a> { + /// The origin of the stylesheet we're parsing. + pub stylesheet_origin: Origin, + /// The namespace set of the stylesheet. + pub namespaces: &'a Namespaces, + /// The extra URL data of the stylesheet, which is used to look up + /// whether we are parsing a chrome:// URL style sheet. + pub url_data: &'a UrlExtraData, + /// Whether we're parsing selectors for `@supports` + pub for_supports_rule: bool, +} + +impl<'a> SelectorParser<'a> { + /// Parse a selector list with an author origin and without taking into + /// account namespaces. + /// + /// This is used for some DOM APIs like `querySelector`. + pub fn parse_author_origin_no_namespace<'i>( + input: &'i str, + url_data: &UrlExtraData, + ) -> Result<SelectorList<SelectorImpl>, ParseError<'i>> { + let namespaces = Namespaces::default(); + let parser = SelectorParser { + stylesheet_origin: Origin::Author, + namespaces: &namespaces, + url_data, + for_supports_rule: false, + }; + let mut input = ParserInput::new(input); + SelectorList::parse(&parser, &mut CssParser::new(&mut input), ParseRelative::No) + } + + /// Whether we're parsing selectors in a user-agent stylesheet. + pub fn in_user_agent_stylesheet(&self) -> bool { + matches!(self.stylesheet_origin, Origin::UserAgent) + } + + /// Whether we're parsing selectors in a stylesheet that has chrome + /// privilege. + pub fn chrome_rules_enabled(&self) -> bool { + self.url_data.chrome_rules_enabled() || self.stylesheet_origin == Origin::User + } +} + +/// This enumeration determines if a pseudo-element is eagerly cascaded or not. +/// +/// If you're implementing a public selector for `Servo` that the end-user might +/// customize, then you probably need to make it eager. +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PseudoElementCascadeType { + /// Eagerly cascaded pseudo-elements are "normal" pseudo-elements (i.e. + /// `::before` and `::after`). They inherit styles normally as another + /// selector would do, and they're computed as part of the cascade. + Eager, + /// Lazy pseudo-elements are affected by selector matching, but they're only + /// computed when needed, and not before. They're useful for general + /// pseudo-elements that are not very common. + /// + /// Note that in Servo lazy pseudo-elements are restricted to a subset of + /// selectors, so you can't use it for public pseudo-elements. This is not + /// the case with Gecko though. + Lazy, + /// Precomputed pseudo-elements skip the cascade process entirely, mostly as + /// an optimisation since they are private pseudo-elements (like + /// `::-servo-details-content`). + /// + /// This pseudo-elements are resolved on the fly using *only* global rules + /// (rules of the form `*|*`), and applying them to the parent style. + Precomputed, +} + +/// A per-pseudo map, from a given pseudo to a `T`. +#[derive(Clone, MallocSizeOf)] +pub struct PerPseudoElementMap<T> { + entries: [Option<T>; PSEUDO_COUNT], +} + +impl<T> Default for PerPseudoElementMap<T> { + fn default() -> Self { + Self { + entries: PseudoElement::pseudo_none_array(), + } + } +} + +impl<T> Debug for PerPseudoElementMap<T> +where + T: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_char('[')?; + let mut first = true; + for entry in self.entries.iter() { + if !first { + f.write_str(", ")?; + } + first = false; + entry.fmt(f)?; + } + f.write_char(']') + } +} + +impl<T> PerPseudoElementMap<T> { + /// Get an entry in the map. + pub fn get(&self, pseudo: &PseudoElement) -> Option<&T> { + self.entries[pseudo.index()].as_ref() + } + + /// Clear this enumerated array. + pub fn clear(&mut self) { + *self = Self::default(); + } + + /// Set an entry value. + /// + /// Returns an error if the element is not a simple pseudo. + pub fn set(&mut self, pseudo: &PseudoElement, value: T) { + self.entries[pseudo.index()] = Some(value); + } + + /// Get an entry for `pseudo`, or create it with calling `f`. + pub fn get_or_insert_with<F>(&mut self, pseudo: &PseudoElement, f: F) -> &mut T + where + F: FnOnce() -> T, + { + let index = pseudo.index(); + if self.entries[index].is_none() { + self.entries[index] = Some(f()); + } + self.entries[index].as_mut().unwrap() + } + + /// Get an iterator for the entries. + pub fn iter(&self) -> std::slice::Iter<Option<T>> { + self.entries.iter() + } + + /// Get a mutable iterator for the entries. + pub fn iter_mut(&mut self) -> std::slice::IterMut<Option<T>> { + self.entries.iter_mut() + } +} + +/// Values for the :dir() pseudo class +/// +/// "ltr" and "rtl" values are normalized to lowercase. +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub struct Direction(pub Atom); + +/// Horizontal values for the :dir() pseudo class +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum HorizontalDirection { + /// :dir(ltr) + Ltr, + /// :dir(rtl) + Rtl, +} + +impl Direction { + /// Parse a direction value. + pub fn parse<'i, 't>(parser: &mut CssParser<'i, 't>) -> Result<Self, ParseError<'i>> { + let ident = parser.expect_ident()?; + Ok(Direction(match_ignore_ascii_case! { &ident, + "rtl" => atom!("rtl"), + "ltr" => atom!("ltr"), + _ => Atom::from(ident.as_ref()), + })) + } + + /// Convert this Direction into a HorizontalDirection, if applicable + pub fn as_horizontal_direction(&self) -> Option<HorizontalDirection> { + if self.0 == atom!("ltr") { + Some(HorizontalDirection::Ltr) + } else if self.0 == atom!("rtl") { + Some(HorizontalDirection::Rtl) + } else { + None + } + } + + /// Gets the element state relevant to this :dir() selector. + pub fn element_state(&self) -> ElementState { + match self.as_horizontal_direction() { + Some(HorizontalDirection::Ltr) => ElementState::LTR, + Some(HorizontalDirection::Rtl) => ElementState::RTL, + None => ElementState::empty(), + } + } +} + +impl ToCss for Direction { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} diff --git a/servo/components/style/servo/media_queries.rs b/servo/components/style/servo/media_queries.rs new file mode 100644 index 0000000000..83b12f5ff2 --- /dev/null +++ b/servo/components/style/servo/media_queries.rs @@ -0,0 +1,226 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Servo's media-query device and expression representation. + +use crate::context::QuirksMode; +use crate::custom_properties::CssEnvironment; +use crate::media_queries::media_feature::{AllowsRanges, ParsingRequirements}; +use crate::media_queries::media_feature::{Evaluator, MediaFeatureDescription}; +use crate::media_queries::media_feature_expression::RangeOrOperator; +use crate::media_queries::MediaType; +use crate::properties::ComputedValues; +use crate::values::computed::CSSPixelLength; +use crate::values::specified::font::FONT_MEDIUM_PX; +use crate::values::KeyframesName; +use app_units::Au; +use euclid::default::Size2D as UntypedSize2D; +use euclid::{Scale, SideOffsets2D, Size2D}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use style_traits::{CSSPixel, DevicePixel}; + +/// A device is a structure that represents the current media a given document +/// is displayed in. +/// +/// This is the struct against which media queries are evaluated. +#[derive(Debug, MallocSizeOf)] +pub struct Device { + /// The current media type used by de device. + media_type: MediaType, + /// The current viewport size, in CSS pixels. + viewport_size: Size2D<f32, CSSPixel>, + /// The current device pixel ratio, from CSS pixels to device pixels. + device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>, + /// The current quirks mode. + #[ignore_malloc_size_of = "Pure stack type"] + quirks_mode: QuirksMode, + + /// The font size of the root element + /// This is set when computing the style of the root + /// element, and used for rem units in other elements + /// + /// When computing the style of the root element, there can't be any + /// other style being computed at the same time, given we need the style of + /// the parent to compute everything else. So it is correct to just use + /// a relaxed atomic here. + #[ignore_malloc_size_of = "Pure stack type"] + root_font_size: AtomicU32, + /// Whether any styles computed in the document relied on the root font-size + /// by using rem units. + #[ignore_malloc_size_of = "Pure stack type"] + used_root_font_size: AtomicBool, + /// Whether any styles computed in the document relied on the viewport size. + #[ignore_malloc_size_of = "Pure stack type"] + used_viewport_units: AtomicBool, + /// The CssEnvironment object responsible of getting CSS environment + /// variables. + environment: CssEnvironment, +} + +impl Device { + /// Trivially construct a new `Device`. + pub fn new( + media_type: MediaType, + quirks_mode: QuirksMode, + viewport_size: Size2D<f32, CSSPixel>, + device_pixel_ratio: Scale<f32, CSSPixel, DevicePixel>, + ) -> Device { + Device { + media_type, + viewport_size, + device_pixel_ratio, + quirks_mode, + // FIXME(bz): Seems dubious? + root_font_size: AtomicU32::new(FONT_MEDIUM_PX.to_bits()), + used_root_font_size: AtomicBool::new(false), + used_viewport_units: AtomicBool::new(false), + environment: CssEnvironment, + } + } + + /// Get the relevant environment to resolve `env()` functions. + #[inline] + pub fn environment(&self) -> &CssEnvironment { + &self.environment + } + + /// Return the default computed values for this device. + pub fn default_computed_values(&self) -> &ComputedValues { + // FIXME(bz): This isn't really right, but it's no more wrong + // than what we used to do. See + // https://github.com/servo/servo/issues/14773 for fixing it properly. + ComputedValues::initial_values() + } + + /// Get the font size of the root element (for rem) + pub fn root_font_size(&self) -> CSSPixelLength { + self.used_root_font_size.store(true, Ordering::Relaxed); + CSSPixelLength::new(f32::from_bits(self.root_font_size.load(Ordering::Relaxed))) + } + + /// Set the font size of the root element (for rem) + pub fn set_root_font_size(&self, size: CSSPixelLength) { + self.root_font_size + .store(size.px().to_bits(), Ordering::Relaxed) + } + + /// Get the quirks mode of the current device. + pub fn quirks_mode(&self) -> QuirksMode { + self.quirks_mode + } + + /// Sets the body text color for the "inherit color from body" quirk. + /// + /// <https://quirks.spec.whatwg.org/#the-tables-inherit-color-from-body-quirk> + pub fn set_body_text_color(&self, _color: RGBA) { + // Servo doesn't implement this quirk (yet) + } + + /// Whether a given animation name may be referenced from style. + pub fn animation_name_may_be_referenced(&self, _: &KeyframesName) -> bool { + // Assume it is, since we don't have any good way to prove it's not. + true + } + + /// Returns whether we ever looked up the root font size of the Device. + pub fn used_root_font_size(&self) -> bool { + self.used_root_font_size.load(Ordering::Relaxed) + } + + /// Returns the viewport size of the current device in app units, needed, + /// among other things, to resolve viewport units. + #[inline] + pub fn au_viewport_size(&self) -> UntypedSize2D<Au> { + Size2D::new( + Au::from_f32_px(self.viewport_size.width), + Au::from_f32_px(self.viewport_size.height), + ) + } + + /// Like the above, but records that we've used viewport units. + pub fn au_viewport_size_for_viewport_unit_resolution(&self) -> UntypedSize2D<Au> { + self.used_viewport_units.store(true, Ordering::Relaxed); + self.au_viewport_size() + } + + /// Whether viewport units were used since the last device change. + pub fn used_viewport_units(&self) -> bool { + self.used_viewport_units.load(Ordering::Relaxed) + } + + /// Returns the device pixel ratio. + pub fn device_pixel_ratio(&self) -> Scale<f32, CSSPixel, DevicePixel> { + self.device_pixel_ratio + } + + /// Return the media type of the current device. + pub fn media_type(&self) -> MediaType { + self.media_type.clone() + } + + /// Returns whether document colors are enabled. + pub fn use_document_colors(&self) -> bool { + true + } + + /// Returns the default background color. + pub fn default_background_color(&self) -> RGBA { + RGBA::new(255, 255, 255, 255) + } + + /// Returns the default color color. + pub fn default_color(&self) -> RGBA { + RGBA::new(0, 0, 0, 255) + } + + /// Returns safe area insets + pub fn safe_area_insets(&self) -> SideOffsets2D<f32, CSSPixel> { + SideOffsets2D::zero() + } +} + +/// https://drafts.csswg.org/mediaqueries-4/#width +fn eval_width( + device: &Device, + value: Option<CSSPixelLength>, + range_or_operator: Option<RangeOrOperator>, +) -> bool { + RangeOrOperator::evaluate( + range_or_operator, + value.map(Au::from), + device.au_viewport_size().width, + ) +} + +#[derive(Clone, Copy, Debug, FromPrimitive, Parse, ToCss)] +#[repr(u8)] +enum Scan { + Progressive, + Interlace, +} + +/// https://drafts.csswg.org/mediaqueries-4/#scan +fn eval_scan(_: &Device, _: Option<Scan>) -> bool { + // Since we doesn't support the 'tv' media type, the 'scan' feature never + // matches. + false +} + +lazy_static! { + /// A list with all the media features that Servo supports. + pub static ref MEDIA_FEATURES: [MediaFeatureDescription; 2] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::Length(eval_width), + ParsingRequirements::empty(), + ), + feature!( + atom!("scan"), + AllowsRanges::No, + keyword_evaluator!(eval_scan, Scan), + ParsingRequirements::empty(), + ), + ]; +} diff --git a/servo/components/style/servo/mod.rs b/servo/components/style/servo/mod.rs new file mode 100644 index 0000000000..6502d28727 --- /dev/null +++ b/servo/components/style/servo/mod.rs @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Servo-specific bits of the style system. +//! +//! These get compiled out on a Gecko build. + +pub mod media_queries; +pub mod restyle_damage; +pub mod selector_parser; +pub mod url; diff --git a/servo/components/style/servo/restyle_damage.rs b/servo/components/style/servo/restyle_damage.rs new file mode 100644 index 0000000000..fe17fa6198 --- /dev/null +++ b/servo/components/style/servo/restyle_damage.rs @@ -0,0 +1,268 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The restyle damage is a hint that tells layout which kind of operations may +//! be needed in presence of incremental style changes. + +use crate::computed_values::display::T as Display; +use crate::matching::{StyleChange, StyleDifference}; +use crate::properties::ComputedValues; +use std::fmt; + +bitflags! { + /// Individual layout actions that may be necessary after restyling. + pub struct ServoRestyleDamage: u8 { + /// Repaint the node itself. + /// + /// Currently unused; need to decide how this propagates. + const REPAINT = 0x01; + + /// The stacking-context-relative position of this node or its + /// descendants has changed. + /// + /// Propagates both up and down the flow tree. + const REPOSITION = 0x02; + + /// Recompute the overflow regions (bounding box of object and all descendants). + /// + /// Propagates down the flow tree because the computation is bottom-up. + const STORE_OVERFLOW = 0x04; + + /// Recompute intrinsic inline_sizes (minimum and preferred). + /// + /// Propagates down the flow tree because the computation is. + /// bottom-up. + const BUBBLE_ISIZES = 0x08; + + /// Recompute actual inline-sizes and block-sizes, only taking + /// out-of-flow children into account. + /// + /// Propagates up the flow tree because the computation is top-down. + const REFLOW_OUT_OF_FLOW = 0x10; + + /// Recompute actual inline_sizes and block_sizes. + /// + /// Propagates up the flow tree because the computation is top-down. + const REFLOW = 0x20; + + /// Re-resolve generated content. + /// + /// Propagates up the flow tree because the computation is inorder. + const RESOLVE_GENERATED_CONTENT = 0x40; + + /// The entire flow needs to be reconstructed. + const RECONSTRUCT_FLOW = 0x80; + } +} + +malloc_size_of_is_0!(ServoRestyleDamage); + +impl ServoRestyleDamage { + /// Compute the `StyleDifference` (including the appropriate restyle damage) + /// for a given style change between `old` and `new`. + pub fn compute_style_difference(old: &ComputedValues, new: &ComputedValues) -> StyleDifference { + let damage = compute_damage(old, new); + let change = if damage.is_empty() { + StyleChange::Unchanged + } else { + // FIXME(emilio): Differentiate between reset and inherited + // properties here, and set `reset_only` appropriately so the + // optimization to skip the cascade in those cases applies. + StyleChange::Changed { reset_only: false } + }; + StyleDifference { damage, change } + } + + /// Returns a bitmask that represents a flow that needs to be rebuilt and + /// reflowed. + /// + /// FIXME(bholley): Do we ever actually need this? Shouldn't + /// RECONSTRUCT_FLOW imply everything else? + pub fn rebuild_and_reflow() -> ServoRestyleDamage { + ServoRestyleDamage::REPAINT | + ServoRestyleDamage::REPOSITION | + ServoRestyleDamage::STORE_OVERFLOW | + ServoRestyleDamage::BUBBLE_ISIZES | + ServoRestyleDamage::REFLOW_OUT_OF_FLOW | + ServoRestyleDamage::REFLOW | + ServoRestyleDamage::RECONSTRUCT_FLOW + } + + /// Returns a bitmask indicating that the frame needs to be reconstructed. + pub fn reconstruct() -> ServoRestyleDamage { + ServoRestyleDamage::RECONSTRUCT_FLOW + } + + /// Supposing a flow has the given `position` property and this damage, + /// returns the damage that we should add to the *parent* of this flow. + pub fn damage_for_parent(self, child_is_absolutely_positioned: bool) -> ServoRestyleDamage { + if child_is_absolutely_positioned { + self & (ServoRestyleDamage::REPAINT | + ServoRestyleDamage::REPOSITION | + ServoRestyleDamage::STORE_OVERFLOW | + ServoRestyleDamage::REFLOW_OUT_OF_FLOW | + ServoRestyleDamage::RESOLVE_GENERATED_CONTENT) + } else { + self & (ServoRestyleDamage::REPAINT | + ServoRestyleDamage::REPOSITION | + ServoRestyleDamage::STORE_OVERFLOW | + ServoRestyleDamage::REFLOW | + ServoRestyleDamage::REFLOW_OUT_OF_FLOW | + ServoRestyleDamage::RESOLVE_GENERATED_CONTENT) + } + } + + /// Supposing the *parent* of a flow with the given `position` property has + /// this damage, returns the damage that we should add to this flow. + pub fn damage_for_child( + self, + parent_is_absolutely_positioned: bool, + child_is_absolutely_positioned: bool, + ) -> ServoRestyleDamage { + match ( + parent_is_absolutely_positioned, + child_is_absolutely_positioned, + ) { + (false, true) => { + // Absolute children are out-of-flow and therefore insulated from changes. + // + // FIXME(pcwalton): Au contraire, if the containing block dimensions change! + self & (ServoRestyleDamage::REPAINT | ServoRestyleDamage::REPOSITION) + }, + (true, false) => { + // Changing the position of an absolutely-positioned block requires us to reflow + // its kids. + if self.contains(ServoRestyleDamage::REFLOW_OUT_OF_FLOW) { + self | ServoRestyleDamage::REFLOW + } else { + self + } + }, + _ => { + // TODO(pcwalton): Take floatedness into account. + self & (ServoRestyleDamage::REPAINT | + ServoRestyleDamage::REPOSITION | + ServoRestyleDamage::REFLOW) + }, + } + } +} + +impl Default for ServoRestyleDamage { + fn default() -> Self { + Self::empty() + } +} + +impl fmt::Display for ServoRestyleDamage { + fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { + let mut first_elem = true; + + let to_iter = [ + (ServoRestyleDamage::REPAINT, "Repaint"), + (ServoRestyleDamage::REPOSITION, "Reposition"), + (ServoRestyleDamage::STORE_OVERFLOW, "StoreOverflow"), + (ServoRestyleDamage::BUBBLE_ISIZES, "BubbleISizes"), + (ServoRestyleDamage::REFLOW_OUT_OF_FLOW, "ReflowOutOfFlow"), + (ServoRestyleDamage::REFLOW, "Reflow"), + ( + ServoRestyleDamage::RESOLVE_GENERATED_CONTENT, + "ResolveGeneratedContent", + ), + (ServoRestyleDamage::RECONSTRUCT_FLOW, "ReconstructFlow"), + ]; + + for &(damage, damage_str) in &to_iter { + if self.contains(damage) { + if !first_elem { + write!(f, " | ")?; + } + write!(f, "{}", damage_str)?; + first_elem = false; + } + } + + if first_elem { + write!(f, "NoDamage")?; + } + + Ok(()) + } +} + +fn compute_damage(old: &ComputedValues, new: &ComputedValues) -> ServoRestyleDamage { + let mut damage = ServoRestyleDamage::empty(); + + // This should check every CSS property, as enumerated in the fields of + // https://doc.servo.org/style/properties/struct.ComputedValues.html + + // This uses short-circuiting boolean OR for its side effects and ignores the result. + let _ = restyle_damage_rebuild_and_reflow!( + old, + new, + damage, + [ + ServoRestyleDamage::REPAINT, + ServoRestyleDamage::REPOSITION, + ServoRestyleDamage::STORE_OVERFLOW, + ServoRestyleDamage::BUBBLE_ISIZES, + ServoRestyleDamage::REFLOW_OUT_OF_FLOW, + ServoRestyleDamage::REFLOW, + ServoRestyleDamage::RECONSTRUCT_FLOW + ] + ) || (new.get_box().display == Display::Inline && + restyle_damage_rebuild_and_reflow_inline!( + old, + new, + damage, + [ + ServoRestyleDamage::REPAINT, + ServoRestyleDamage::REPOSITION, + ServoRestyleDamage::STORE_OVERFLOW, + ServoRestyleDamage::BUBBLE_ISIZES, + ServoRestyleDamage::REFLOW_OUT_OF_FLOW, + ServoRestyleDamage::REFLOW, + ServoRestyleDamage::RECONSTRUCT_FLOW + ] + )) || + restyle_damage_reflow!( + old, + new, + damage, + [ + ServoRestyleDamage::REPAINT, + ServoRestyleDamage::REPOSITION, + ServoRestyleDamage::STORE_OVERFLOW, + ServoRestyleDamage::BUBBLE_ISIZES, + ServoRestyleDamage::REFLOW_OUT_OF_FLOW, + ServoRestyleDamage::REFLOW + ] + ) || + restyle_damage_reflow_out_of_flow!( + old, + new, + damage, + [ + ServoRestyleDamage::REPAINT, + ServoRestyleDamage::REPOSITION, + ServoRestyleDamage::STORE_OVERFLOW, + ServoRestyleDamage::REFLOW_OUT_OF_FLOW + ] + ) || + restyle_damage_repaint!(old, new, damage, [ServoRestyleDamage::REPAINT]); + + // Paint worklets may depend on custom properties, + // so if they have changed we should repaint. + if !old.custom_properties_equal(new) { + damage.insert(ServoRestyleDamage::REPAINT); + } + + // If the layer requirements of this flow have changed due to the value + // of the transform, then reflow is required to rebuild the layers. + if old.transform_requires_layer() != new.transform_requires_layer() { + damage.insert(ServoRestyleDamage::rebuild_and_reflow()); + } + + damage +} diff --git a/servo/components/style/servo/selector_parser.rs b/servo/components/style/servo/selector_parser.rs new file mode 100644 index 0000000000..b20f1754a0 --- /dev/null +++ b/servo/components/style/servo/selector_parser.rs @@ -0,0 +1,806 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![deny(missing_docs)] + +//! Servo's selector parser. + +use crate::attr::{AttrIdentifier, AttrValue}; +use crate::dom::{OpaqueNode, TElement, TNode}; +use crate::invalidation::element::document_state::InvalidationMatchingData; +use crate::invalidation::element::element_wrapper::ElementSnapshot; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::{ComputedValues, PropertyFlags}; +use crate::selector_parser::AttrValue as SelectorAttrValue; +use crate::selector_parser::{PseudoElementCascadeType, SelectorParser}; +use crate::values::{AtomIdent, AtomString}; +use crate::{Atom, CaseSensitivityExt, LocalName, Namespace, Prefix}; +use cssparser::{serialize_identifier, CowRcStr, Parser as CssParser, SourceLocation, ToCss}; +use dom::{DocumentState, ElementState}; +use fxhash::FxHashMap; +use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint}; +use selectors::parser::SelectorParseErrorKind; +use selectors::visitor::SelectorVisitor; +use std::fmt; +use std::mem; +use std::ops::{Deref, DerefMut}; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A pseudo-element, both public and private. +/// +/// NB: If you add to this list, be sure to update `each_simple_pseudo_element` too. +#[derive( + Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize, ToShmem, +)] +#[allow(missing_docs)] +#[repr(usize)] +pub enum PseudoElement { + // Eager pseudos. Keep these first so that eager_index() works. + After = 0, + Before, + Selection, + // If/when :first-letter is added, update is_first_letter accordingly. + + // If/when :first-line is added, update is_first_line accordingly. + + // If/when ::first-letter, ::first-line, or ::placeholder are added, adjust + // our property_restriction implementation to do property filtering for + // them. Also, make sure the UA sheet has the !important rules some of the + // APPLIES_TO_PLACEHOLDER properties expect! + + // Non-eager pseudos. + DetailsSummary, + DetailsContent, + ServoText, + ServoInputText, + ServoTableWrapper, + ServoAnonymousTableWrapper, + ServoAnonymousTable, + ServoAnonymousTableRow, + ServoAnonymousTableCell, + ServoAnonymousBlock, + ServoInlineBlockWrapper, + ServoInlineAbsolute, +} + +/// The count of all pseudo-elements. +pub const PSEUDO_COUNT: usize = PseudoElement::ServoInlineAbsolute as usize + 1; + +impl ::selectors::parser::PseudoElement for PseudoElement { + type Impl = SelectorImpl; +} + +impl ToCss for PseudoElement { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + use self::PseudoElement::*; + dest.write_str(match *self { + After => "::after", + Before => "::before", + Selection => "::selection", + DetailsSummary => "::-servo-details-summary", + DetailsContent => "::-servo-details-content", + ServoText => "::-servo-text", + ServoInputText => "::-servo-input-text", + ServoTableWrapper => "::-servo-table-wrapper", + ServoAnonymousTableWrapper => "::-servo-anonymous-table-wrapper", + ServoAnonymousTable => "::-servo-anonymous-table", + ServoAnonymousTableRow => "::-servo-anonymous-table-row", + ServoAnonymousTableCell => "::-servo-anonymous-table-cell", + ServoAnonymousBlock => "::-servo-anonymous-block", + ServoInlineBlockWrapper => "::-servo-inline-block-wrapper", + ServoInlineAbsolute => "::-servo-inline-absolute", + }) + } +} + +/// The number of eager pseudo-elements. Keep this in sync with cascade_type. +pub const EAGER_PSEUDO_COUNT: usize = 3; + +impl PseudoElement { + /// Gets the canonical index of this eagerly-cascaded pseudo-element. + #[inline] + pub fn eager_index(&self) -> usize { + debug_assert!(self.is_eager()); + self.clone() as usize + } + + /// An index for this pseudo-element to be indexed in an enumerated array. + #[inline] + pub fn index(&self) -> usize { + self.clone() as usize + } + + /// An array of `None`, one per pseudo-element. + pub fn pseudo_none_array<T>() -> [Option<T>; PSEUDO_COUNT] { + Default::default() + } + + /// Creates a pseudo-element from an eager index. + #[inline] + pub fn from_eager_index(i: usize) -> Self { + assert!(i < EAGER_PSEUDO_COUNT); + let result: PseudoElement = unsafe { mem::transmute(i) }; + debug_assert!(result.is_eager()); + result + } + + /// Whether the current pseudo element is ::before or ::after. + #[inline] + pub fn is_before_or_after(&self) -> bool { + self.is_before() || self.is_after() + } + + /// Whether this is an unknown ::-webkit- pseudo-element. + #[inline] + pub fn is_unknown_webkit_pseudo_element(&self) -> bool { + false + } + + /// Whether this pseudo-element is the ::marker pseudo. + #[inline] + pub fn is_marker(&self) -> bool { + false + } + + /// Whether this pseudo-element is the ::selection pseudo. + #[inline] + pub fn is_selection(&self) -> bool { + *self == PseudoElement::Selection + } + + /// Whether this pseudo-element is the ::before pseudo. + #[inline] + pub fn is_before(&self) -> bool { + *self == PseudoElement::Before + } + + /// Whether this pseudo-element is the ::after pseudo. + #[inline] + pub fn is_after(&self) -> bool { + *self == PseudoElement::After + } + + /// Whether the current pseudo element is :first-letter + #[inline] + pub fn is_first_letter(&self) -> bool { + false + } + + /// Whether the current pseudo element is :first-line + #[inline] + pub fn is_first_line(&self) -> bool { + false + } + + /// Whether this pseudo-element is the ::-moz-color-swatch pseudo. + #[inline] + pub fn is_color_swatch(&self) -> bool { + false + } + + /// Whether this pseudo-element is eagerly-cascaded. + #[inline] + pub fn is_eager(&self) -> bool { + self.cascade_type() == PseudoElementCascadeType::Eager + } + + /// Whether this pseudo-element is lazily-cascaded. + #[inline] + pub fn is_lazy(&self) -> bool { + self.cascade_type() == PseudoElementCascadeType::Lazy + } + + /// Whether this pseudo-element is for an anonymous box. + pub fn is_anon_box(&self) -> bool { + self.is_precomputed() + } + + /// Whether this pseudo-element skips flex/grid container display-based + /// fixup. + #[inline] + pub fn skip_item_display_fixup(&self) -> bool { + !self.is_before_or_after() + } + + /// Whether this pseudo-element is precomputed. + #[inline] + pub fn is_precomputed(&self) -> bool { + self.cascade_type() == PseudoElementCascadeType::Precomputed + } + + /// Returns which kind of cascade type has this pseudo. + /// + /// For more info on cascade types, see docs/components/style.md + /// + /// Note: Keep this in sync with EAGER_PSEUDO_COUNT. + #[inline] + pub fn cascade_type(&self) -> PseudoElementCascadeType { + match *self { + PseudoElement::After | PseudoElement::Before | PseudoElement::Selection => { + PseudoElementCascadeType::Eager + }, + PseudoElement::DetailsSummary => PseudoElementCascadeType::Lazy, + PseudoElement::DetailsContent | + PseudoElement::ServoText | + PseudoElement::ServoInputText | + PseudoElement::ServoTableWrapper | + PseudoElement::ServoAnonymousTableWrapper | + PseudoElement::ServoAnonymousTable | + PseudoElement::ServoAnonymousTableRow | + PseudoElement::ServoAnonymousTableCell | + PseudoElement::ServoAnonymousBlock | + PseudoElement::ServoInlineBlockWrapper | + PseudoElement::ServoInlineAbsolute => PseudoElementCascadeType::Precomputed, + } + } + + /// Covert non-canonical pseudo-element to canonical one, and keep a + /// canonical one as it is. + pub fn canonical(&self) -> PseudoElement { + self.clone() + } + + /// Stub, only Gecko needs this + pub fn pseudo_info(&self) { + () + } + + /// Property flag that properties must have to apply to this pseudo-element. + #[inline] + pub fn property_restriction(&self) -> Option<PropertyFlags> { + None + } + + /// Whether this pseudo-element should actually exist if it has + /// the given styles. + pub fn should_exist(&self, style: &ComputedValues) -> bool { + let display = style.get_box().clone_display(); + if display == Display::None { + return false; + } + if self.is_before_or_after() && style.ineffective_content_property() { + return false; + } + + true + } +} + +/// The type used for storing `:lang` arguments. +pub type Lang = Box<str>; + +/// A non tree-structural pseudo-class. +/// See https://drafts.csswg.org/selectors-4/#structural-pseudos +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] +#[allow(missing_docs)] +pub enum NonTSPseudoClass { + Active, + AnyLink, + Checked, + Defined, + Disabled, + Enabled, + Focus, + Fullscreen, + Hover, + Indeterminate, + Lang(Lang), + Link, + PlaceholderShown, + ReadWrite, + ReadOnly, + ServoNonZeroBorder, + Target, + Visited, +} + +impl ::selectors::parser::NonTSPseudoClass for NonTSPseudoClass { + type Impl = SelectorImpl; + + #[inline] + fn is_active_or_hover(&self) -> bool { + matches!(*self, NonTSPseudoClass::Active | NonTSPseudoClass::Hover) + } + + #[inline] + fn is_user_action_state(&self) -> bool { + matches!( + *self, + NonTSPseudoClass::Active | NonTSPseudoClass::Hover | NonTSPseudoClass::Focus + ) + } + + fn visit<V>(&self, _: &mut V) -> bool + where + V: SelectorVisitor<Impl = Self::Impl>, + { + true + } +} + +impl ToCss for NonTSPseudoClass { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: fmt::Write, + { + use self::NonTSPseudoClass::*; + if let Lang(ref lang) = *self { + dest.write_str(":lang(")?; + serialize_identifier(lang, dest)?; + return dest.write_char(')'); + } + + dest.write_str(match *self { + Active => ":active", + AnyLink => ":any-link", + Checked => ":checked", + Defined => ":defined", + Disabled => ":disabled", + Enabled => ":enabled", + Focus => ":focus", + Fullscreen => ":fullscreen", + Hover => ":hover", + Indeterminate => ":indeterminate", + Link => ":link", + PlaceholderShown => ":placeholder-shown", + ReadWrite => ":read-write", + ReadOnly => ":read-only", + ServoNonZeroBorder => ":-servo-nonzero-border", + Target => ":target", + Visited => ":visited", + Lang(_) => unreachable!(), + }) + } +} + +impl NonTSPseudoClass { + /// Gets a given state flag for this pseudo-class. This is used to do + /// selector matching, and it's set from the DOM. + pub fn state_flag(&self) -> ElementState { + use self::NonTSPseudoClass::*; + match *self { + Active => ElementState::IN_ACTIVE_STATE, + Focus => ElementState::IN_FOCUS_STATE, + Fullscreen => ElementState::IN_FULLSCREEN_STATE, + Hover => ElementState::IN_HOVER_STATE, + Defined => ElementState::IN_DEFINED_STATE, + Enabled => ElementState::IN_ENABLED_STATE, + Disabled => ElementState::IN_DISABLED_STATE, + Checked => ElementState::IN_CHECKED_STATE, + Indeterminate => ElementState::IN_INDETERMINATE_STATE, + ReadOnly | ReadWrite => ElementState::IN_READWRITE_STATE, + PlaceholderShown => ElementState::IN_PLACEHOLDER_SHOWN_STATE, + Target => ElementState::IN_TARGET_STATE, + + AnyLink | Lang(_) | Link | Visited | ServoNonZeroBorder => ElementState::empty(), + } + } + + /// Get the document state flag associated with a pseudo-class, if any. + pub fn document_state_flag(&self) -> DocumentState { + DocumentState::empty() + } + + /// Returns true if the given pseudoclass should trigger style sharing cache revalidation. + pub fn needs_cache_revalidation(&self) -> bool { + self.state_flag().is_empty() + } +} + +/// The abstract struct we implement the selector parser implementation on top +/// of. +#[derive(Clone, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct SelectorImpl; + +impl ::selectors::SelectorImpl for SelectorImpl { + type PseudoElement = PseudoElement; + type NonTSPseudoClass = NonTSPseudoClass; + + type ExtraMatchingData = InvalidationMatchingData; + type AttrValue = String; + type Identifier = Atom; + type ClassName = Atom; + type PartName = Atom; + type LocalName = LocalName; + type NamespacePrefix = Prefix; + type NamespaceUrl = Namespace; + type BorrowedLocalName = LocalName; + type BorrowedNamespaceUrl = Namespace; +} + +impl<'a, 'i> ::selectors::Parser<'i> for SelectorParser<'a> { + type Impl = SelectorImpl; + type Error = StyleParseErrorKind<'i>; + + fn parse_non_ts_pseudo_class( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<NonTSPseudoClass, ParseError<'i>> { + use self::NonTSPseudoClass::*; + let pseudo_class = match_ignore_ascii_case! { &name, + "active" => Active, + "any-link" => AnyLink, + "checked" => Checked, + "defined" => Defined, + "disabled" => Disabled, + "enabled" => Enabled, + "focus" => Focus, + "fullscreen" => Fullscreen, + "hover" => Hover, + "indeterminate" => Indeterminate, + "-moz-inert" => MozInert, + "link" => Link, + "placeholder-shown" => PlaceholderShown, + "read-write" => ReadWrite, + "read-only" => ReadOnly, + "target" => Target, + "visited" => Visited, + "-servo-nonzero-border" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent("-servo-nonzero-border".into()) + )) + } + ServoNonZeroBorder + }, + _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + }; + + Ok(pseudo_class) + } + + fn parse_non_ts_functional_pseudo_class<'t>( + &self, + name: CowRcStr<'i>, + parser: &mut CssParser<'i, 't>, + ) -> Result<NonTSPseudoClass, ParseError<'i>> { + use self::NonTSPseudoClass::*; + let pseudo_class = match_ignore_ascii_case! { &name, + "lang" => { + Lang(parser.expect_ident_or_string()?.as_ref().into()) + }, + _ => return Err(parser.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + }; + + Ok(pseudo_class) + } + + fn parse_pseudo_element( + &self, + location: SourceLocation, + name: CowRcStr<'i>, + ) -> Result<PseudoElement, ParseError<'i>> { + use self::PseudoElement::*; + let pseudo_element = match_ignore_ascii_case! { &name, + "before" => Before, + "after" => After, + "selection" => Selection, + "-servo-details-summary" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + DetailsSummary + }, + "-servo-details-content" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + DetailsContent + }, + "-servo-text" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoText + }, + "-servo-input-text" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoInputText + }, + "-servo-table-wrapper" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoTableWrapper + }, + "-servo-anonymous-table-wrapper" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoAnonymousTableWrapper + }, + "-servo-anonymous-table" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoAnonymousTable + }, + "-servo-anonymous-table-row" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoAnonymousTableRow + }, + "-servo-anonymous-table-cell" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoAnonymousTableCell + }, + "-servo-anonymous-block" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoAnonymousBlock + }, + "-servo-inline-block-wrapper" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoInlineBlockWrapper + }, + "-servo-inline-absolute" => { + if !self.in_user_agent_stylesheet() { + return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + } + ServoInlineAbsolute + }, + _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))) + + }; + + Ok(pseudo_element) + } + + fn default_namespace(&self) -> Option<Namespace> { + self.namespaces.default.as_ref().map(|ns| ns.clone()) + } + + fn namespace_for_prefix(&self, prefix: &Prefix) -> Option<Namespace> { + self.namespaces.prefixes.get(prefix).cloned() + } +} + +impl SelectorImpl { + /// A helper to traverse each eagerly cascaded pseudo-element, executing + /// `fun` on it. + #[inline] + pub fn each_eagerly_cascaded_pseudo_element<F>(mut fun: F) + where + F: FnMut(PseudoElement), + { + for i in 0..EAGER_PSEUDO_COUNT { + fun(PseudoElement::from_eager_index(i)); + } + } +} + +/// A map from elements to snapshots for the Servo style back-end. +#[derive(Debug)] +pub struct SnapshotMap(FxHashMap<OpaqueNode, ServoElementSnapshot>); + +impl SnapshotMap { + /// Create a new empty `SnapshotMap`. + pub fn new() -> Self { + SnapshotMap(FxHashMap::default()) + } + + /// Get a snapshot given an element. + pub fn get<T: TElement>(&self, el: &T) -> Option<&ServoElementSnapshot> { + self.0.get(&el.as_node().opaque()) + } +} + +impl Deref for SnapshotMap { + type Target = FxHashMap<OpaqueNode, ServoElementSnapshot>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for SnapshotMap { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// Servo's version of an element snapshot. +#[derive(Debug, Default, MallocSizeOf)] +pub struct ServoElementSnapshot { + /// The stored state of the element. + pub state: Option<ElementState>, + /// The set of stored attributes and its values. + pub attrs: Option<Vec<(AttrIdentifier, AttrValue)>>, + /// The set of changed attributes and its values. + pub changed_attrs: Vec<LocalName>, + /// Whether the class attribute changed or not. + pub class_changed: bool, + /// Whether the id attribute changed or not. + pub id_changed: bool, + /// Whether other attributes other than id or class changed or not. + pub other_attributes_changed: bool, +} + +impl ServoElementSnapshot { + /// Create an empty element snapshot. + pub fn new() -> Self { + Self::default() + } + + /// Returns whether the id attribute changed or not. + pub fn id_changed(&self) -> bool { + self.id_changed + } + + /// Returns whether the class attribute changed or not. + pub fn class_changed(&self) -> bool { + self.class_changed + } + + /// Returns whether other attributes other than id or class changed or not. + pub fn other_attr_changed(&self) -> bool { + self.other_attributes_changed + } + + fn get_attr(&self, namespace: &Namespace, name: &LocalName) -> Option<&AttrValue> { + self.attrs + .as_ref() + .unwrap() + .iter() + .find(|&&(ref ident, _)| ident.local_name == *name && ident.namespace == *namespace) + .map(|&(_, ref v)| v) + } + + /// Executes the callback once for each attribute that changed. + #[inline] + pub fn each_attr_changed<F>(&self, mut callback: F) + where + F: FnMut(&LocalName), + { + for name in &self.changed_attrs { + callback(name) + } + } + + fn any_attr_ignore_ns<F>(&self, name: &LocalName, mut f: F) -> bool + where + F: FnMut(&AttrValue) -> bool, + { + self.attrs + .as_ref() + .unwrap() + .iter() + .any(|&(ref ident, ref v)| ident.local_name == *name && f(v)) + } +} + +impl ElementSnapshot for ServoElementSnapshot { + fn state(&self) -> Option<ElementState> { + self.state.clone() + } + + fn has_attrs(&self) -> bool { + self.attrs.is_some() + } + + fn id_attr(&self) -> Option<&Atom> { + self.get_attr(&ns!(), &local_name!("id")) + .map(|v| v.as_atom()) + } + + fn is_part(&self, _name: &AtomIdent) -> bool { + false + } + + fn imported_part(&self, _: &AtomIdent) -> Option<AtomIdent> { + None + } + + fn has_class(&self, name: &AtomIdent, case_sensitivity: CaseSensitivity) -> bool { + self.get_attr(&ns!(), &local_name!("class")) + .map_or(false, |v| { + v.as_tokens() + .iter() + .any(|atom| case_sensitivity.eq_atom(atom, name)) + }) + } + + fn each_class<F>(&self, mut callback: F) + where + F: FnMut(&AtomIdent), + { + if let Some(v) = self.get_attr(&ns!(), &local_name!("class")) { + for class in v.as_tokens() { + callback(AtomIdent::cast(class)); + } + } + } + + fn lang_attr(&self) -> Option<SelectorAttrValue> { + self.get_attr(&ns!(xml), &local_name!("lang")) + .or_else(|| self.get_attr(&ns!(), &local_name!("lang"))) + .map(|v| SelectorAttrValue::from(v as &str)) + } +} + +impl ServoElementSnapshot { + /// selectors::Element::attr_matches + pub fn attr_matches( + &self, + ns: &NamespaceConstraint<&Namespace>, + local_name: &LocalName, + operation: &AttrSelectorOperation<&AtomString>, + ) -> bool { + match *ns { + NamespaceConstraint::Specific(ref ns) => self + .get_attr(ns, local_name) + .map_or(false, |value| value.eval_selector(operation)), + NamespaceConstraint::Any => { + self.any_attr_ignore_ns(local_name, |value| value.eval_selector(operation)) + }, + } + } +} + +/// Returns whether the language is matched, as defined by +/// [RFC 4647](https://tools.ietf.org/html/rfc4647#section-3.3.2). +pub fn extended_filtering(tag: &str, range: &str) -> bool { + range.split(',').any(|lang_range| { + // step 1 + let mut range_subtags = lang_range.split('\x2d'); + let mut tag_subtags = tag.split('\x2d'); + + // step 2 + // Note: [Level-4 spec](https://drafts.csswg.org/selectors/#lang-pseudo) check for wild card + if let (Some(range_subtag), Some(tag_subtag)) = (range_subtags.next(), tag_subtags.next()) { + if !(range_subtag.eq_ignore_ascii_case(tag_subtag) || + range_subtag.eq_ignore_ascii_case("*")) + { + return false; + } + } + + let mut current_tag_subtag = tag_subtags.next(); + + // step 3 + for range_subtag in range_subtags { + // step 3a + if range_subtag == "*" { + continue; + } + match current_tag_subtag.clone() { + Some(tag_subtag) => { + // step 3c + if range_subtag.eq_ignore_ascii_case(tag_subtag) { + current_tag_subtag = tag_subtags.next(); + continue; + } + // step 3d + if tag_subtag.len() == 1 { + return false; + } + // else step 3e - continue with loop + current_tag_subtag = tag_subtags.next(); + if current_tag_subtag.is_none() { + return false; + } + }, + // step 3b + None => { + return false; + }, + } + } + // step 4 + true + }) +} diff --git a/servo/components/style/servo/url.rs b/servo/components/style/servo/url.rs new file mode 100644 index 0000000000..2186be7aab --- /dev/null +++ b/servo/components/style/servo/url.rs @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common handling for the specified value CSS url() values. + +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::CorsMode; +use crate::values::computed::{Context, ToComputedValue}; +use cssparser::Parser; +use servo_arc::Arc; +use servo_url::ServoUrl; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A CSS url() value for servo. +/// +/// Servo eagerly resolves SpecifiedUrls, which it can then take advantage of +/// when computing values. In contrast, Gecko uses a different URL backend, so +/// eagerly resolving with rust-url would be duplicated work. +/// +/// However, this approach is still not necessarily optimal: See +/// <https://bugzilla.mozilla.org/show_bug.cgi?id=1347435#c6> +/// +/// TODO(emilio): This should be shrunk by making CssUrl a wrapper type of an +/// arc, and keep the serialization in that Arc. See gecko/url.rs for example. +#[derive(Clone, Debug, Deserialize, MallocSizeOf, Serialize, SpecifiedValueInfo, ToShmem)] +pub struct CssUrl { + /// The original URI. This might be optional since we may insert computed + /// values of images into the cascade directly, and we don't bother to + /// convert their serialization. + /// + /// Refcounted since cloning this should be cheap and data: uris can be + /// really large. + #[ignore_malloc_size_of = "Arc"] + original: Option<Arc<String>>, + + /// The resolved value for the url, if valid. + resolved: Option<ServoUrl>, +} + +impl CssUrl { + /// Try to parse a URL from a string value that is a valid CSS token for a + /// URL. + /// + /// FIXME(emilio): Should honor CorsMode. + pub fn parse_from_string(url: String, context: &ParserContext, _: CorsMode) -> Self { + let serialization = Arc::new(url); + let resolved = context.url_data.join(&serialization).ok(); + CssUrl { + original: Some(serialization), + resolved: resolved, + } + } + + /// Returns true if the URL is definitely invalid. For Servo URLs, we can + /// use its |resolved| status. + pub fn is_invalid(&self) -> bool { + self.resolved.is_none() + } + + /// Returns true if this URL looks like a fragment. + /// See https://drafts.csswg.org/css-values/#local-urls + /// + /// Since Servo currently stores resolved URLs, this is hard to implement. We + /// either need to change servo to lazily resolve (like Gecko), or note this + /// information in the tokenizer. + pub fn is_fragment(&self) -> bool { + error!("Can't determine whether the url is a fragment."); + false + } + + /// Returns the resolved url if it was valid. + pub fn url(&self) -> Option<&ServoUrl> { + self.resolved.as_ref() + } + + /// Return the resolved url as string, or the empty string if it's invalid. + /// + /// TODO(emilio): Should we return the original one if needed? + pub fn as_str(&self) -> &str { + match self.resolved { + Some(ref url) => url.as_str(), + None => "", + } + } + + /// Creates an already specified url value from an already resolved URL + /// for insertion in the cascade. + pub fn for_cascade(url: ServoUrl) -> Self { + CssUrl { + original: None, + resolved: Some(url), + } + } + + /// Gets a new url from a string for unit tests. + pub fn new_for_testing(url: &str) -> Self { + CssUrl { + original: Some(Arc::new(url.into())), + resolved: ServoUrl::parse(url).ok(), + } + } + + /// Parses a URL request and records that the corresponding request needs to + /// be CORS-enabled. + /// + /// This is only for shape images and masks in Gecko, thus unimplemented for + /// now so somebody notices when trying to do so. + pub fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + let url = input.expect_url()?; + Ok(Self::parse_from_string( + url.as_ref().to_owned(), + context, + cors_mode, + )) + } +} + +impl Parse for CssUrl { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_cors_mode(context, input, CorsMode::None) + } +} + +impl PartialEq for CssUrl { + fn eq(&self, other: &Self) -> bool { + // TODO(emilio): maybe we care about equality of the specified values if + // present? Seems not. + self.resolved == other.resolved + } +} + +impl Eq for CssUrl {} + +impl ToCss for CssUrl { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let string = match self.original { + Some(ref original) => &**original, + None => match self.resolved { + Some(ref url) => url.as_str(), + // This can only happen if the url wasn't specified by the + // user *and* it's an invalid url that has been transformed + // back to specified value via the "uncompute" functionality. + None => "about:invalid", + }, + }; + + dest.write_str("url(")?; + string.to_css(dest)?; + dest.write_char(')') + } +} + +/// A specified url() value for servo. +pub type SpecifiedUrl = CssUrl; + +impl ToComputedValue for SpecifiedUrl { + type ComputedValue = ComputedUrl; + + // If we can't resolve the URL from the specified one, we fall back to the original + // but still return it as a ComputedUrl::Invalid + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + match self.resolved { + Some(ref url) => ComputedUrl::Valid(url.clone()), + None => match self.original { + Some(ref url) => ComputedUrl::Invalid(url.clone()), + None => { + unreachable!("Found specified url with neither resolved or original URI!"); + }, + }, + } + } + + fn from_computed_value(computed: &ComputedUrl) -> Self { + match *computed { + ComputedUrl::Valid(ref url) => SpecifiedUrl { + original: None, + resolved: Some(url.clone()), + }, + ComputedUrl::Invalid(ref url) => SpecifiedUrl { + original: Some(url.clone()), + resolved: None, + }, + } + } +} + +/// A specified image url() value for servo. +pub type SpecifiedImageUrl = CssUrl; + +/// The computed value of a CSS `url()`, resolved relative to the stylesheet URL. +#[derive(Clone, Debug, Deserialize, MallocSizeOf, PartialEq, Serialize)] +pub enum ComputedUrl { + /// The `url()` was invalid or it wasn't specified by the user. + Invalid(#[ignore_malloc_size_of = "Arc"] Arc<String>), + /// The resolved `url()` relative to the stylesheet URL. + Valid(ServoUrl), +} + +impl ComputedUrl { + /// Returns the resolved url if it was valid. + pub fn url(&self) -> Option<&ServoUrl> { + match *self { + ComputedUrl::Valid(ref url) => Some(url), + _ => None, + } + } +} + +impl ToCss for ComputedUrl { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let string = match *self { + ComputedUrl::Valid(ref url) => url.as_str(), + ComputedUrl::Invalid(ref invalid_string) => invalid_string, + }; + + dest.write_str("url(")?; + string.to_css(dest)?; + dest.write_char(')') + } +} + +/// The computed value of a CSS `url()` for image. +pub type ComputedImageUrl = ComputedUrl; diff --git a/servo/components/style/shared_lock.rs b/servo/components/style/shared_lock.rs new file mode 100644 index 0000000000..55708a9f7b --- /dev/null +++ b/servo/components/style/shared_lock.rs @@ -0,0 +1,374 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Different objects protected by the same lock + +use crate::str::{CssString, CssStringWriter}; +use crate::stylesheets::Origin; +#[cfg(feature = "gecko")] +use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut}; +#[cfg(feature = "servo")] +use parking_lot::RwLock; +use servo_arc::Arc; +use std::cell::UnsafeCell; +use std::fmt; +#[cfg(feature = "servo")] +use std::mem; +#[cfg(feature = "gecko")] +use std::ptr; +use to_shmem::{SharedMemoryBuilder, ToShmem}; + +/// A shared read/write lock that can protect multiple objects. +/// +/// In Gecko builds, we don't need the blocking behavior, just the safety. As +/// such we implement this with an AtomicRefCell instead in Gecko builds, +/// which is ~2x as fast, and panics (rather than deadlocking) when things go +/// wrong (which is much easier to debug on CI). +/// +/// Servo needs the blocking behavior for its unsynchronized animation setup, +/// but that may not be web-compatible and may need to be changed (at which +/// point Servo could use AtomicRefCell too). +/// +/// Gecko also needs the ability to have "read only" SharedRwLocks, which are +/// used for objects stored in (read only) shared memory. Attempting to acquire +/// write access to objects protected by a read only SharedRwLock will panic. +#[derive(Clone)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct SharedRwLock { + #[cfg(feature = "servo")] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + arc: Arc<RwLock<()>>, + + #[cfg(feature = "gecko")] + cell: Option<Arc<AtomicRefCell<SomethingZeroSizedButTyped>>>, +} + +#[cfg(feature = "gecko")] +struct SomethingZeroSizedButTyped; + +impl fmt::Debug for SharedRwLock { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("SharedRwLock") + } +} + +impl SharedRwLock { + /// Create a new shared lock (servo). + #[cfg(feature = "servo")] + pub fn new() -> Self { + SharedRwLock { + arc: Arc::new(RwLock::new(())), + } + } + + /// Create a new shared lock (gecko). + #[cfg(feature = "gecko")] + pub fn new() -> Self { + SharedRwLock { + cell: Some(Arc::new(AtomicRefCell::new(SomethingZeroSizedButTyped))), + } + } + + /// Create a new global shared lock (servo). + #[cfg(feature = "servo")] + pub fn new_leaked() -> Self { + SharedRwLock { + arc: Arc::new_leaked(RwLock::new(())), + } + } + + /// Create a new global shared lock (gecko). + #[cfg(feature = "gecko")] + pub fn new_leaked() -> Self { + SharedRwLock { + cell: Some(Arc::new_leaked(AtomicRefCell::new( + SomethingZeroSizedButTyped, + ))), + } + } + + /// Create a new read-only shared lock (gecko). + #[cfg(feature = "gecko")] + pub fn read_only() -> Self { + SharedRwLock { cell: None } + } + + #[cfg(feature = "gecko")] + #[inline] + fn ptr(&self) -> *const SomethingZeroSizedButTyped { + self.cell + .as_ref() + .map(|cell| cell.as_ptr() as *const _) + .unwrap_or(ptr::null()) + } + + /// Wrap the given data to make its access protected by this lock. + pub fn wrap<T>(&self, data: T) -> Locked<T> { + Locked { + shared_lock: self.clone(), + data: UnsafeCell::new(data), + } + } + + /// Obtain the lock for reading (servo). + #[cfg(feature = "servo")] + pub fn read(&self) -> SharedRwLockReadGuard { + mem::forget(self.arc.read()); + SharedRwLockReadGuard(self) + } + + /// Obtain the lock for reading (gecko). + #[cfg(feature = "gecko")] + pub fn read(&self) -> SharedRwLockReadGuard { + SharedRwLockReadGuard(self.cell.as_ref().map(|cell| cell.borrow())) + } + + /// Obtain the lock for writing (servo). + #[cfg(feature = "servo")] + pub fn write(&self) -> SharedRwLockWriteGuard { + mem::forget(self.arc.write()); + SharedRwLockWriteGuard(self) + } + + /// Obtain the lock for writing (gecko). + #[cfg(feature = "gecko")] + pub fn write(&self) -> SharedRwLockWriteGuard { + SharedRwLockWriteGuard(self.cell.as_ref().unwrap().borrow_mut()) + } +} + +/// Proof that a shared lock was obtained for reading (servo). +#[cfg(feature = "servo")] +pub struct SharedRwLockReadGuard<'a>(&'a SharedRwLock); +/// Proof that a shared lock was obtained for reading (gecko). +#[cfg(feature = "gecko")] +pub struct SharedRwLockReadGuard<'a>(Option<AtomicRef<'a, SomethingZeroSizedButTyped>>); +#[cfg(feature = "servo")] +impl<'a> Drop for SharedRwLockReadGuard<'a> { + fn drop(&mut self) { + // Unsafe: self.lock is private to this module, only ever set after `read()`, + // and never copied or cloned (see `compile_time_assert` below). + unsafe { self.0.arc.force_unlock_read() } + } +} + +impl<'a> SharedRwLockReadGuard<'a> { + #[inline] + #[cfg(feature = "gecko")] + fn ptr(&self) -> *const SomethingZeroSizedButTyped { + self.0 + .as_ref() + .map(|r| &**r as *const _) + .unwrap_or(ptr::null()) + } +} + +/// Proof that a shared lock was obtained for writing (servo). +#[cfg(feature = "servo")] +pub struct SharedRwLockWriteGuard<'a>(&'a SharedRwLock); +/// Proof that a shared lock was obtained for writing (gecko). +#[cfg(feature = "gecko")] +pub struct SharedRwLockWriteGuard<'a>(AtomicRefMut<'a, SomethingZeroSizedButTyped>); +#[cfg(feature = "servo")] +impl<'a> Drop for SharedRwLockWriteGuard<'a> { + fn drop(&mut self) { + // Unsafe: self.lock is private to this module, only ever set after `write()`, + // and never copied or cloned (see `compile_time_assert` below). + unsafe { self.0.arc.force_unlock_write() } + } +} + +/// Data protect by a shared lock. +pub struct Locked<T> { + shared_lock: SharedRwLock, + data: UnsafeCell<T>, +} + +// Unsafe: the data inside `UnsafeCell` is only accessed in `read_with` and `write_with`, +// where guards ensure synchronization. +unsafe impl<T: Send> Send for Locked<T> {} +unsafe impl<T: Send + Sync> Sync for Locked<T> {} + +impl<T: fmt::Debug> fmt::Debug for Locked<T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let guard = self.shared_lock.read(); + self.read_with(&guard).fmt(f) + } +} + +impl<T> Locked<T> { + #[cfg(feature = "gecko")] + #[inline] + fn is_read_only_lock(&self) -> bool { + self.shared_lock.cell.is_none() + } + + #[cfg(feature = "servo")] + fn same_lock_as(&self, lock: &SharedRwLock) -> bool { + Arc::ptr_eq(&self.shared_lock.arc, &lock.arc) + } + + #[cfg(feature = "gecko")] + fn same_lock_as(&self, ptr: *const SomethingZeroSizedButTyped) -> bool { + ptr::eq(self.shared_lock.ptr(), ptr) + } + + /// Access the data for reading. + pub fn read_with<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a T { + #[cfg(feature = "gecko")] + assert!( + self.is_read_only_lock() || self.same_lock_as(guard.ptr()), + "Locked::read_with called with a guard from an unrelated SharedRwLock: {:?} vs. {:?}", + self.shared_lock.ptr(), + guard.ptr(), + ); + #[cfg(not(feature = "gecko"))] + assert!(self.same_lock_as(&guard.0)); + + let ptr = self.data.get(); + + // Unsafe: + // + // * The guard guarantees that the lock is taken for reading, + // and we’ve checked that it’s the correct lock. + // * The returned reference borrows *both* the data and the guard, + // so that it can outlive neither. + unsafe { &*ptr } + } + + /// Access the data for reading without verifying the lock. Use with caution. + #[cfg(feature = "gecko")] + pub unsafe fn read_unchecked<'a>(&'a self) -> &'a T { + let ptr = self.data.get(); + &*ptr + } + + /// Access the data for writing. + pub fn write_with<'a>(&'a self, guard: &'a mut SharedRwLockWriteGuard) -> &'a mut T { + #[cfg(feature = "gecko")] + assert!( + !self.is_read_only_lock() && self.same_lock_as(&*guard.0), + "Locked::write_with called with a guard from a read only or unrelated SharedRwLock" + ); + #[cfg(not(feature = "gecko"))] + assert!(self.same_lock_as(&guard.0)); + + let ptr = self.data.get(); + + // Unsafe: + // + // * The guard guarantees that the lock is taken for writing, + // and we’ve checked that it’s the correct lock. + // * The returned reference borrows *both* the data and the guard, + // so that it can outlive neither. + // * We require a mutable borrow of the guard, + // so that one write guard can only be used once at a time. + unsafe { &mut *ptr } + } +} + +#[cfg(feature = "gecko")] +impl<T: ToShmem> ToShmem for Locked<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + use std::mem::ManuallyDrop; + + let guard = self.shared_lock.read(); + Ok(ManuallyDrop::new(Locked { + shared_lock: SharedRwLock::read_only(), + data: UnsafeCell::new(ManuallyDrop::into_inner( + self.read_with(&guard).to_shmem(builder)?, + )), + })) + } +} + +#[cfg(feature = "servo")] +impl<T: ToShmem> ToShmem for Locked<T> { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + panic!("ToShmem not supported in Servo currently") + } +} + +#[allow(dead_code)] +mod compile_time_assert { + use super::{SharedRwLockReadGuard, SharedRwLockWriteGuard}; + + trait Marker1 {} + impl<T: Clone> Marker1 for T {} + impl<'a> Marker1 for SharedRwLockReadGuard<'a> {} // Assert SharedRwLockReadGuard: !Clone + impl<'a> Marker1 for SharedRwLockWriteGuard<'a> {} // Assert SharedRwLockWriteGuard: !Clone + + trait Marker2 {} + impl<T: Copy> Marker2 for T {} + impl<'a> Marker2 for SharedRwLockReadGuard<'a> {} // Assert SharedRwLockReadGuard: !Copy + impl<'a> Marker2 for SharedRwLockWriteGuard<'a> {} // Assert SharedRwLockWriteGuard: !Copy +} + +/// Like ToCss, but with a lock guard given by the caller, and with the writer specified +/// concretely rather than with a parameter. +pub trait ToCssWithGuard { + /// Serialize `self` in CSS syntax, writing to `dest`, using the given lock guard. + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result; + + /// Serialize `self` in CSS syntax using the given lock guard and return a string. + /// + /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) + #[inline] + fn to_css_string(&self, guard: &SharedRwLockReadGuard) -> CssString { + let mut s = CssString::new(); + self.to_css(guard, &mut s).unwrap(); + s + } +} + +/// Parameters needed for deep clones. +#[cfg(feature = "gecko")] +pub struct DeepCloneParams { + /// The new sheet we're cloning rules into. + pub reference_sheet: *const crate::gecko_bindings::structs::StyleSheet, +} + +/// Parameters needed for deep clones. +#[cfg(feature = "servo")] +pub struct DeepCloneParams; + +/// A trait to do a deep clone of a given CSS type. Gets a lock and a read +/// guard, in order to be able to read and clone nested structures. +pub trait DeepCloneWithLock: Sized { + /// Deep clones this object. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self; +} + +/// Guards for a document +#[derive(Clone)] +pub struct StylesheetGuards<'a> { + /// For author-origin stylesheets. + pub author: &'a SharedRwLockReadGuard<'a>, + + /// For user-agent-origin and user-origin stylesheets + pub ua_or_user: &'a SharedRwLockReadGuard<'a>, +} + +impl<'a> StylesheetGuards<'a> { + /// Get the guard for a given stylesheet origin. + pub fn for_origin(&self, origin: Origin) -> &SharedRwLockReadGuard<'a> { + match origin { + Origin::Author => &self.author, + _ => &self.ua_or_user, + } + } + + /// Same guard for all origins + pub fn same(guard: &'a SharedRwLockReadGuard<'a>) -> Self { + StylesheetGuards { + author: guard, + ua_or_user: guard, + } + } +} diff --git a/servo/components/style/sharing/checks.rs b/servo/components/style/sharing/checks.rs new file mode 100644 index 0000000000..dfb7ab7b97 --- /dev/null +++ b/servo/components/style/sharing/checks.rs @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Different checks done during the style sharing process in order to determine +//! quickly whether it's worth to share style, and whether two different +//! elements can indeed share the same style. + +use crate::bloom::StyleBloom; +use crate::context::SharedStyleContext; +use crate::dom::TElement; +use crate::sharing::{StyleSharingCandidate, StyleSharingTarget}; +use selectors::matching::SelectorCaches; + +/// Determines whether a target and a candidate have compatible parents for +/// sharing. +pub fn parents_allow_sharing<E>( + target: &mut StyleSharingTarget<E>, + candidate: &mut StyleSharingCandidate<E>, +) -> bool +where + E: TElement, +{ + // If the identity of the parent style isn't equal, we can't share. We check + // this first, because the result is cached. + if target.parent_style_identity() != candidate.parent_style_identity() { + return false; + } + + // Siblings can always share. + let parent = target.inheritance_parent().unwrap(); + let candidate_parent = candidate.element.inheritance_parent().unwrap(); + if parent == candidate_parent { + return true; + } + + // If a parent element was already styled and we traversed past it without + // restyling it, that may be because our clever invalidation logic was able + // to prove that the styles of that element would remain unchanged despite + // changes to the id or class attributes. However, style sharing relies in + // the strong guarantee that all the classes and ids up the respective parent + // chains are identical. As such, if we skipped styling for one (or both) of + // the parents on this traversal, we can't share styles across cousins. + // + // This is a somewhat conservative check. We could tighten it by having the + // invalidation logic explicitly flag elements for which it ellided styling. + let parent_data = parent.borrow_data().unwrap(); + let candidate_parent_data = candidate_parent.borrow_data().unwrap(); + if !parent_data.safe_for_cousin_sharing() || !candidate_parent_data.safe_for_cousin_sharing() { + return false; + } + + true +} + +/// Whether two elements have the same same style attribute (by pointer identity). +pub fn have_same_style_attribute<E>( + target: &mut StyleSharingTarget<E>, + candidate: &mut StyleSharingCandidate<E>, +) -> bool +where + E: TElement, +{ + match (target.style_attribute(), candidate.style_attribute()) { + (None, None) => true, + (Some(_), None) | (None, Some(_)) => false, + (Some(a), Some(b)) => &*a as *const _ == &*b as *const _, + } +} + +/// Whether two elements have the same same presentational attributes. +pub fn have_same_presentational_hints<E>( + target: &mut StyleSharingTarget<E>, + candidate: &mut StyleSharingCandidate<E>, +) -> bool +where + E: TElement, +{ + target.pres_hints() == candidate.pres_hints() +} + +/// Whether a given element has the same class attribute as a given candidate. +/// +/// We don't try to share style across elements with different class attributes. +pub fn have_same_class<E>( + target: &mut StyleSharingTarget<E>, + candidate: &mut StyleSharingCandidate<E>, +) -> bool +where + E: TElement, +{ + target.class_list() == candidate.class_list() +} + +/// Whether a given element has the same part attribute as a given candidate. +/// +/// We don't try to share style across elements with different part attributes. +pub fn have_same_parts<E>( + target: &mut StyleSharingTarget<E>, + candidate: &mut StyleSharingCandidate<E>, +) -> bool +where + E: TElement, +{ + target.part_list() == candidate.part_list() +} + +/// Whether a given element and a candidate match the same set of "revalidation" +/// selectors. +/// +/// Revalidation selectors are those that depend on the DOM structure, like +/// :first-child, etc, or on attributes that we don't check off-hand (pretty +/// much every attribute selector except `id` and `class`. +#[inline] +pub fn revalidate<E>( + target: &mut StyleSharingTarget<E>, + candidate: &mut StyleSharingCandidate<E>, + shared_context: &SharedStyleContext, + bloom: &StyleBloom<E>, + selector_caches: &mut SelectorCaches, +) -> bool +where + E: TElement, +{ + let stylist = &shared_context.stylist; + + let for_element = target.revalidation_match_results(stylist, bloom, selector_caches); + + let for_candidate = candidate.revalidation_match_results(stylist, bloom, selector_caches); + + for_element == for_candidate +} + +/// Checks whether we might have rules for either of the two ids. +#[inline] +pub fn may_match_different_id_rules<E>( + shared_context: &SharedStyleContext, + element: E, + candidate: E, +) -> bool +where + E: TElement, +{ + let element_id = element.id(); + let candidate_id = candidate.id(); + + if element_id == candidate_id { + return false; + } + + let stylist = &shared_context.stylist; + + let may_have_rules_for_element = match element_id { + Some(id) => stylist.may_have_rules_for_id(id, element), + None => false, + }; + + if may_have_rules_for_element { + return true; + } + + match candidate_id { + Some(id) => stylist.may_have_rules_for_id(id, candidate), + None => false, + } +} diff --git a/servo/components/style/sharing/mod.rs b/servo/components/style/sharing/mod.rs new file mode 100644 index 0000000000..eeea135c06 --- /dev/null +++ b/servo/components/style/sharing/mod.rs @@ -0,0 +1,923 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Code related to the style sharing cache, an optimization that allows similar +//! nodes to share style without having to run selector matching twice. +//! +//! The basic setup is as follows. We have an LRU cache of style sharing +//! candidates. When we try to style a target element, we first check whether +//! we can quickly determine that styles match something in this cache, and if +//! so we just use the cached style information. This check is done with a +//! StyleBloom filter set up for the target element, which may not be a correct +//! state for the cached candidate element if they're cousins instead of +//! siblings. +//! +//! The complicated part is determining that styles match. This is subject to +//! the following constraints: +//! +//! 1) The target and candidate must be inheriting the same styles. +//! 2) The target and candidate must have exactly the same rules matching them. +//! 3) The target and candidate must have exactly the same non-selector-based +//! style information (inline styles, presentation hints). +//! 4) The target and candidate must have exactly the same rules matching their +//! pseudo-elements, because an element's style data points to the style +//! data for its pseudo-elements. +//! +//! These constraints are satisfied in the following ways: +//! +//! * We check that the parents of the target and the candidate have the same +//! computed style. This addresses constraint 1. +//! +//! * We check that the target and candidate have the same inline style and +//! presentation hint declarations. This addresses constraint 3. +//! +//! * We ensure that a target matches a candidate only if they have the same +//! matching result for all selectors that target either elements or the +//! originating elements of pseudo-elements. This addresses constraint 4 +//! (because it prevents a target that has pseudo-element styles from matching +//! a candidate that has different pseudo-element styles) as well as +//! constraint 2. +//! +//! The actual checks that ensure that elements match the same rules are +//! conceptually split up into two pieces. First, we do various checks on +//! elements that make sure that the set of possible rules in all selector maps +//! in the stylist (for normal styling and for pseudo-elements) that might match +//! the two elements is the same. For example, we enforce that the target and +//! candidate must have the same localname and namespace. Second, we have a +//! selector map of "revalidation selectors" that the stylist maintains that we +//! actually match against the target and candidate and then check whether the +//! two sets of results were the same. Due to the up-front selector map checks, +//! we know that the target and candidate will be matched against the same exact +//! set of revalidation selectors, so the match result arrays can be compared +//! directly. +//! +//! It's very important that a selector be added to the set of revalidation +//! selectors any time there are two elements that could pass all the up-front +//! checks but match differently against some ComplexSelector in the selector. +//! If that happens, then they can have descendants that might themselves pass +//! the up-front checks but would have different matching results for the +//! selector in question. In this case, "descendants" includes pseudo-elements, +//! so there is a single selector map of revalidation selectors that includes +//! both selectors targeting elements and selectors targeting pseudo-element +//! originating elements. We ensure that the pseudo-element parts of all these +//! selectors are effectively stripped off, so that matching them all against +//! elements makes sense. + +use crate::applicable_declarations::ApplicableDeclarationBlock; +use crate::bloom::StyleBloom; +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{SharedStyleContext, StyleContext}; +use crate::dom::{SendElement, TElement}; +use crate::properties::ComputedValues; +use crate::rule_tree::StrongRuleNode; +use crate::selector_map::RelevantAttributes; +use crate::style_resolver::{PrimaryStyle, ResolvedElementStyles}; +use crate::stylist::Stylist; +use crate::values::AtomIdent; +use atomic_refcell::{AtomicRefCell, AtomicRefMut}; +use owning_ref::OwningHandle; +use selectors::matching::{NeedsSelectorFlags, SelectorCaches, VisitedHandlingMode}; +use servo_arc::Arc; +use smallbitvec::SmallBitVec; +use smallvec::SmallVec; +use std::marker::PhantomData; +use std::mem::{self, ManuallyDrop}; +use std::ops::Deref; +use std::ptr::NonNull; +use uluru::LRUCache; + +mod checks; + +/// The amount of nodes that the style sharing candidate cache should hold at +/// most. +/// +/// The cache size was chosen by measuring style sharing and resulting +/// performance on a few pages; sizes up to about 32 were giving good sharing +/// improvements (e.g. 3x fewer styles having to be resolved than at size 8) and +/// slight performance improvements. Sizes larger than 32 haven't really been +/// tested. +pub const SHARING_CACHE_SIZE: usize = 32; + +/// Opaque pointer type to compare ComputedValues identities. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OpaqueComputedValues(NonNull<()>); + +unsafe impl Send for OpaqueComputedValues {} +unsafe impl Sync for OpaqueComputedValues {} + +impl OpaqueComputedValues { + fn from(cv: &ComputedValues) -> Self { + let p = + unsafe { NonNull::new_unchecked(cv as *const ComputedValues as *const () as *mut ()) }; + OpaqueComputedValues(p) + } + + fn eq(&self, cv: &ComputedValues) -> bool { + Self::from(cv) == *self + } +} + +/// The results from the revalidation step. +/// +/// Rather than either: +/// +/// * Plainly rejecting sharing for elements with different attributes (which would be unfortunate +/// because a lot of elements have different attributes yet those attributes are not +/// style-relevant). +/// +/// * Having to give up on per-attribute bucketing, which would be unfortunate because it +/// increases the cost of revalidation for pages with lots of global attribute selectors (see +/// bug 1868316). +/// +/// * We also store the style-relevant attributes for these elements, in order to guarantee that +/// we end up looking at the same selectors. +/// +#[derive(Debug, Default)] +pub struct RevalidationResult { + /// A bit for each selector matched. This is sound because we guarantee we look up into the + /// same buckets via the pre-revalidation checks and relevant_attributes. + pub selectors_matched: SmallBitVec, + /// The set of attributes of this element that were relevant for its style. + pub relevant_attributes: RelevantAttributes, +} + +impl PartialEq for RevalidationResult { + fn eq(&self, other: &Self) -> bool { + if self.relevant_attributes != other.relevant_attributes { + return false; + } + + // This assert "ensures", to some extent, that the two candidates have matched the + // same rulehash buckets, and as such, that the bits we're comparing represent the + // same set of selectors. + debug_assert_eq!(self.selectors_matched.len(), other.selectors_matched.len()); + self.selectors_matched == other.selectors_matched + } +} + +/// Some data we want to avoid recomputing all the time while trying to share +/// style. +#[derive(Debug, Default)] +pub struct ValidationData { + /// The class list of this element. + /// + /// TODO(emilio): Maybe check whether rules for these classes apply to the + /// element? + class_list: Option<SmallVec<[AtomIdent; 5]>>, + + /// The part list of this element. + /// + /// TODO(emilio): Maybe check whether rules with these part names apply to + /// the element? + part_list: Option<SmallVec<[AtomIdent; 5]>>, + + /// The list of presentational attributes of the element. + pres_hints: Option<SmallVec<[ApplicableDeclarationBlock; 5]>>, + + /// The pointer identity of the parent ComputedValues. + parent_style_identity: Option<OpaqueComputedValues>, + + /// The cached result of matching this entry against the revalidation + /// selectors. + revalidation_match_results: Option<RevalidationResult>, +} + +impl ValidationData { + /// Move the cached data to a new instance, and return it. + pub fn take(&mut self) -> Self { + mem::replace(self, Self::default()) + } + + /// Get or compute the list of presentational attributes associated with + /// this element. + pub fn pres_hints<E>(&mut self, element: E) -> &[ApplicableDeclarationBlock] + where + E: TElement, + { + self.pres_hints.get_or_insert_with(|| { + let mut pres_hints = SmallVec::new(); + element.synthesize_presentational_hints_for_legacy_attributes( + VisitedHandlingMode::AllLinksUnvisited, + &mut pres_hints, + ); + pres_hints + }) + } + + /// Get or compute the part-list associated with this element. + pub fn part_list<E>(&mut self, element: E) -> &[AtomIdent] + where + E: TElement, + { + if !element.has_part_attr() { + return &[]; + } + self.part_list.get_or_insert_with(|| { + let mut list = SmallVec::<[_; 5]>::new(); + element.each_part(|p| list.push(p.clone())); + // See below for the reasoning. + if !list.spilled() { + list.sort_unstable_by_key(|a| a.get_hash()); + } + list + }) + } + + /// Get or compute the class-list associated with this element. + pub fn class_list<E>(&mut self, element: E) -> &[AtomIdent] + where + E: TElement, + { + self.class_list.get_or_insert_with(|| { + let mut list = SmallVec::<[_; 5]>::new(); + element.each_class(|c| list.push(c.clone())); + // Assuming there are a reasonable number of classes (we use the + // inline capacity as "reasonable number"), sort them to so that + // we don't mistakenly reject sharing candidates when one element + // has "foo bar" and the other has "bar foo". + if !list.spilled() { + list.sort_unstable_by_key(|a| a.get_hash()); + } + list + }) + } + + /// Get or compute the parent style identity. + pub fn parent_style_identity<E>(&mut self, el: E) -> OpaqueComputedValues + where + E: TElement, + { + self.parent_style_identity + .get_or_insert_with(|| { + let parent = el.inheritance_parent().unwrap(); + let values = + OpaqueComputedValues::from(parent.borrow_data().unwrap().styles.primary()); + values + }) + .clone() + } + + /// Computes the revalidation results if needed, and returns it. + /// Inline so we know at compile time what bloom_known_valid is. + #[inline] + fn revalidation_match_results<E>( + &mut self, + element: E, + stylist: &Stylist, + bloom: &StyleBloom<E>, + selector_caches: &mut SelectorCaches, + bloom_known_valid: bool, + needs_selector_flags: NeedsSelectorFlags, + ) -> &RevalidationResult + where + E: TElement, + { + self.revalidation_match_results.get_or_insert_with(|| { + // The bloom filter may already be set up for our element. + // If it is, use it. If not, we must be in a candidate + // (i.e. something in the cache), and the element is one + // of our cousins, not a sibling. In that case, we'll + // just do revalidation selector matching without a bloom + // filter, to avoid thrashing the filter. + let bloom_to_use = if bloom_known_valid { + debug_assert_eq!(bloom.current_parent(), element.traversal_parent()); + Some(bloom.filter()) + } else { + if bloom.current_parent() == element.traversal_parent() { + Some(bloom.filter()) + } else { + None + } + }; + stylist.match_revalidation_selectors( + element, + bloom_to_use, + selector_caches, + needs_selector_flags, + ) + }) + } +} + +/// Information regarding a style sharing candidate, that is, an entry in the +/// style sharing cache. +/// +/// Note that this information is stored in TLS and cleared after the traversal, +/// and once here, the style information of the element is immutable, so it's +/// safe to access. +/// +/// Important: If you change the members/layout here, You need to do the same for +/// FakeCandidate below. +#[derive(Debug)] +pub struct StyleSharingCandidate<E: TElement> { + /// The element. + element: E, + validation_data: ValidationData, +} + +struct FakeCandidate { + _element: usize, + _validation_data: ValidationData, +} + +impl<E: TElement> Deref for StyleSharingCandidate<E> { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.element + } +} + +impl<E: TElement> StyleSharingCandidate<E> { + /// Get the classlist of this candidate. + fn class_list(&mut self) -> &[AtomIdent] { + self.validation_data.class_list(self.element) + } + + /// Get the part list of this candidate. + fn part_list(&mut self) -> &[AtomIdent] { + self.validation_data.part_list(self.element) + } + + /// Get the pres hints of this candidate. + fn pres_hints(&mut self) -> &[ApplicableDeclarationBlock] { + self.validation_data.pres_hints(self.element) + } + + /// Get the parent style identity. + fn parent_style_identity(&mut self) -> OpaqueComputedValues { + self.validation_data.parent_style_identity(self.element) + } + + /// Compute the bit vector of revalidation selector match results + /// for this candidate. + fn revalidation_match_results( + &mut self, + stylist: &Stylist, + bloom: &StyleBloom<E>, + selector_caches: &mut SelectorCaches, + ) -> &RevalidationResult { + self.validation_data.revalidation_match_results( + self.element, + stylist, + bloom, + selector_caches, + /* bloom_known_valid = */ false, + // The candidate must already have the right bits already, if + // needed. + NeedsSelectorFlags::No, + ) + } +} + +impl<E: TElement> PartialEq<StyleSharingCandidate<E>> for StyleSharingCandidate<E> { + fn eq(&self, other: &Self) -> bool { + self.element == other.element + } +} + +/// An element we want to test against the style sharing cache. +pub struct StyleSharingTarget<E: TElement> { + element: E, + validation_data: ValidationData, +} + +impl<E: TElement> Deref for StyleSharingTarget<E> { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.element + } +} + +impl<E: TElement> StyleSharingTarget<E> { + /// Trivially construct a new StyleSharingTarget to test against the cache. + pub fn new(element: E) -> Self { + Self { + element: element, + validation_data: ValidationData::default(), + } + } + + fn class_list(&mut self) -> &[AtomIdent] { + self.validation_data.class_list(self.element) + } + + fn part_list(&mut self) -> &[AtomIdent] { + self.validation_data.part_list(self.element) + } + + /// Get the pres hints of this candidate. + fn pres_hints(&mut self) -> &[ApplicableDeclarationBlock] { + self.validation_data.pres_hints(self.element) + } + + /// Get the parent style identity. + fn parent_style_identity(&mut self) -> OpaqueComputedValues { + self.validation_data.parent_style_identity(self.element) + } + + fn revalidation_match_results( + &mut self, + stylist: &Stylist, + bloom: &StyleBloom<E>, + selector_caches: &mut SelectorCaches, + ) -> &RevalidationResult { + // It's important to set the selector flags. Otherwise, if we succeed in + // sharing the style, we may not set the slow selector flags for the + // right elements (which may not necessarily be |element|), causing + // missed restyles after future DOM mutations. + // + // Gecko's test_bug534804.html exercises this. A minimal testcase is: + // <style> #e:empty + span { ... } </style> + // <span id="e"> + // <span></span> + // </span> + // <span></span> + // + // The style sharing cache will get a hit for the second span. When the + // child span is subsequently removed from the DOM, missing selector + // flags would cause us to miss the restyle on the second span. + self.validation_data.revalidation_match_results( + self.element, + stylist, + bloom, + selector_caches, + /* bloom_known_valid = */ true, + NeedsSelectorFlags::Yes, + ) + } + + /// Attempts to share a style with another node. + pub fn share_style_if_possible( + &mut self, + context: &mut StyleContext<E>, + ) -> Option<ResolvedElementStyles> { + let cache = &mut context.thread_local.sharing_cache; + let shared_context = &context.shared; + let bloom_filter = &context.thread_local.bloom_filter; + let selector_caches = &mut context.thread_local.selector_caches; + + if cache.dom_depth != bloom_filter.matching_depth() { + debug!( + "Can't share style, because DOM depth changed from {:?} to {:?}, element: {:?}", + cache.dom_depth, + bloom_filter.matching_depth(), + self.element + ); + return None; + } + debug_assert_eq!( + bloom_filter.current_parent(), + self.element.traversal_parent() + ); + + cache.share_style_if_possible(shared_context, bloom_filter, selector_caches, self) + } + + /// Gets the validation data used to match against this target, if any. + pub fn take_validation_data(&mut self) -> ValidationData { + self.validation_data.take() + } +} + +struct SharingCacheBase<Candidate> { + entries: LRUCache<Candidate, SHARING_CACHE_SIZE>, +} + +impl<Candidate> Default for SharingCacheBase<Candidate> { + fn default() -> Self { + Self { + entries: LRUCache::default(), + } + } +} + +impl<Candidate> SharingCacheBase<Candidate> { + fn clear(&mut self) { + self.entries.clear(); + } + + fn is_empty(&self) -> bool { + self.entries.len() == 0 + } +} + +impl<E: TElement> SharingCache<E> { + fn insert( + &mut self, + element: E, + validation_data_holder: Option<&mut StyleSharingTarget<E>>, + ) { + let validation_data = match validation_data_holder { + Some(v) => v.take_validation_data(), + None => ValidationData::default(), + }; + self.entries.insert(StyleSharingCandidate { + element, + validation_data, + }); + } +} + +/// Style sharing caches are are large allocations, so we store them in thread-local +/// storage such that they can be reused across style traversals. Ideally, we'd just +/// stack-allocate these buffers with uninitialized memory, but right now rustc can't +/// avoid memmoving the entire cache during setup, which gets very expensive. See +/// issues like [1] and [2]. +/// +/// Given that the cache stores entries of type TElement, we transmute to usize +/// before storing in TLS. This is safe as long as we make sure to empty the cache +/// before we let it go. +/// +/// [1] https://github.com/rust-lang/rust/issues/42763 +/// [2] https://github.com/rust-lang/rust/issues/13707 +type SharingCache<E> = SharingCacheBase<StyleSharingCandidate<E>>; +type TypelessSharingCache = SharingCacheBase<FakeCandidate>; +type StoredSharingCache = Arc<AtomicRefCell<TypelessSharingCache>>; + +thread_local! { + // See the comment on bloom.rs about why do we leak this. + static SHARING_CACHE_KEY: ManuallyDrop<StoredSharingCache> = + ManuallyDrop::new(Arc::new_leaked(Default::default())); +} + +/// An LRU cache of the last few nodes seen, so that we can aggressively try to +/// reuse their styles. +/// +/// Note that this cache is flushed every time we steal work from the queue, so +/// storing nodes here temporarily is safe. +pub struct StyleSharingCache<E: TElement> { + /// The LRU cache, with the type cast away to allow persisting the allocation. + cache_typeless: OwningHandle<StoredSharingCache, AtomicRefMut<'static, TypelessSharingCache>>, + /// Bind this structure to the lifetime of E, since that's what we effectively store. + marker: PhantomData<SendElement<E>>, + /// The DOM depth we're currently at. This is used as an optimization to + /// clear the cache when we change depths, since we know at that point + /// nothing in the cache will match. + dom_depth: usize, +} + +impl<E: TElement> Drop for StyleSharingCache<E> { + fn drop(&mut self) { + self.clear(); + } +} + +impl<E: TElement> StyleSharingCache<E> { + #[allow(dead_code)] + fn cache(&self) -> &SharingCache<E> { + let base: &TypelessSharingCache = &*self.cache_typeless; + unsafe { mem::transmute(base) } + } + + fn cache_mut(&mut self) -> &mut SharingCache<E> { + let base: &mut TypelessSharingCache = &mut *self.cache_typeless; + unsafe { mem::transmute(base) } + } + + /// Create a new style sharing candidate cache. + + // Forced out of line to limit stack frame sizes after extra inlining from + // https://github.com/rust-lang/rust/pull/43931 + // + // See https://github.com/servo/servo/pull/18420#issuecomment-328769322 + #[inline(never)] + pub fn new() -> Self { + assert_eq!( + mem::size_of::<SharingCache<E>>(), + mem::size_of::<TypelessSharingCache>() + ); + assert_eq!( + mem::align_of::<SharingCache<E>>(), + mem::align_of::<TypelessSharingCache>() + ); + let cache_arc = SHARING_CACHE_KEY.with(|c| Arc::clone(&*c)); + let cache = + OwningHandle::new_with_fn(cache_arc, |x| unsafe { x.as_ref() }.unwrap().borrow_mut()); + debug_assert!(cache.is_empty()); + + StyleSharingCache { + cache_typeless: cache, + marker: PhantomData, + dom_depth: 0, + } + } + + /// Tries to insert an element in the style sharing cache. + /// + /// Fails if we know it should never be in the cache. + /// + /// NB: We pass a source for the validation data, rather than the data itself, + /// to avoid memmoving at each function call. See rust issue #42763. + pub fn insert_if_possible( + &mut self, + element: &E, + style: &PrimaryStyle, + validation_data_holder: Option<&mut StyleSharingTarget<E>>, + dom_depth: usize, + shared_context: &SharedStyleContext, + ) { + let parent = match element.traversal_parent() { + Some(element) => element, + None => { + debug!("Failing to insert to the cache: no parent element"); + return; + }, + }; + + if !element.matches_user_and_content_rules() { + debug!("Failing to insert into the cache: no tree rules:"); + return; + } + + // We can't share style across shadow hosts right now, because they may + // match different :host rules. + // + // TODO(emilio): We could share across the ones that don't have :host + // rules or have the same. + if element.shadow_root().is_some() { + debug!("Failing to insert into the cache: Shadow Host"); + return; + } + + // If the element has running animations, we can't share style. + // + // This is distinct from the specifies_{animations,transitions} check below, + // because: + // * Animations can be triggered directly via the Web Animations API. + // * Our computed style can still be affected by animations after we no + // longer match any animation rules, since removing animations involves + // a sequential task and an additional traversal. + if element.has_animations(shared_context) { + debug!("Failing to insert to the cache: running animations"); + return; + } + + // In addition to the above running animations check, we also need to + // check CSS animation and transition styles since it's possible that + // we are about to create CSS animations/transitions. + // + // These are things we don't check in the candidate match because they + // are either uncommon or expensive. + let ui_style = style.style().get_ui(); + if ui_style.specifies_transitions() { + debug!("Failing to insert to the cache: transitions"); + return; + } + + if ui_style.specifies_animations() { + debug!("Failing to insert to the cache: animations"); + return; + } + + debug!( + "Inserting into cache: {:?} with parent {:?}", + element, parent + ); + + if self.dom_depth != dom_depth { + debug!( + "Clearing cache because depth changed from {:?} to {:?}, element: {:?}", + self.dom_depth, dom_depth, element + ); + self.clear(); + self.dom_depth = dom_depth; + } + self.cache_mut().insert( + *element, + validation_data_holder, + ); + } + + /// Clear the style sharing candidate cache. + pub fn clear(&mut self) { + self.cache_mut().clear(); + } + + /// Attempts to share a style with another node. + fn share_style_if_possible( + &mut self, + shared_context: &SharedStyleContext, + bloom_filter: &StyleBloom<E>, + selector_caches: &mut SelectorCaches, + target: &mut StyleSharingTarget<E>, + ) -> Option<ResolvedElementStyles> { + if shared_context.options.disable_style_sharing_cache { + debug!( + "{:?} Cannot share style: style sharing cache disabled", + target.element + ); + return None; + } + + if target.inheritance_parent().is_none() { + debug!( + "{:?} Cannot share style: element has no parent", + target.element + ); + return None; + } + + if !target.matches_user_and_content_rules() { + debug!("{:?} Cannot share style: content rules", target.element); + return None; + } + + self.cache_mut().entries.lookup(|candidate| { + Self::test_candidate( + target, + candidate, + &shared_context, + bloom_filter, + selector_caches, + shared_context, + ) + }) + } + + fn test_candidate( + target: &mut StyleSharingTarget<E>, + candidate: &mut StyleSharingCandidate<E>, + shared: &SharedStyleContext, + bloom: &StyleBloom<E>, + selector_caches: &mut SelectorCaches, + shared_context: &SharedStyleContext, + ) -> Option<ResolvedElementStyles> { + debug_assert!(target.matches_user_and_content_rules()); + + // Check that we have the same parent, or at least that the parents + // share styles and permit sharing across their children. The latter + // check allows us to share style between cousins if the parents + // shared style. + if !checks::parents_allow_sharing(target, candidate) { + trace!("Miss: Parent"); + return None; + } + + if target.local_name() != candidate.element.local_name() { + trace!("Miss: Local Name"); + return None; + } + + if target.namespace() != candidate.element.namespace() { + trace!("Miss: Namespace"); + return None; + } + + // We do not ignore visited state here, because Gecko needs to store + // extra bits on visited styles, so these contexts cannot be shared. + if target.element.state() != candidate.state() { + trace!("Miss: User and Author State"); + return None; + } + + if target.is_link() != candidate.element.is_link() { + trace!("Miss: Link"); + return None; + } + + // If two elements belong to different shadow trees, different rules may + // apply to them, from the respective trees. + if target.element.containing_shadow() != candidate.element.containing_shadow() { + trace!("Miss: Different containing shadow roots"); + return None; + } + + // If the elements are not assigned to the same slot they could match + // different ::slotted() rules in the slot scope. + // + // If two elements are assigned to different slots, even within the same + // shadow root, they could match different rules, due to the slot being + // assigned to yet another slot in another shadow root. + if target.element.assigned_slot() != candidate.element.assigned_slot() { + // TODO(emilio): We could have a look at whether the shadow roots + // actually have slotted rules and such. + trace!("Miss: Different assigned slots"); + return None; + } + + if target.element.shadow_root().is_some() { + trace!("Miss: Shadow host"); + return None; + } + + if target.element.has_animations(shared_context) { + trace!("Miss: Has Animations"); + return None; + } + + if target.matches_user_and_content_rules() != + candidate.element.matches_user_and_content_rules() + { + trace!("Miss: User and Author Rules"); + return None; + } + + // It's possible that there are no styles for either id. + if checks::may_match_different_id_rules(shared, target.element, candidate.element) { + trace!("Miss: ID Attr"); + return None; + } + + if !checks::have_same_style_attribute(target, candidate) { + trace!("Miss: Style Attr"); + return None; + } + + if !checks::have_same_class(target, candidate) { + trace!("Miss: Class"); + return None; + } + + if !checks::have_same_presentational_hints(target, candidate) { + trace!("Miss: Pres Hints"); + return None; + } + + if !checks::have_same_parts(target, candidate) { + trace!("Miss: Shadow parts"); + return None; + } + + if !checks::revalidate(target, candidate, shared, bloom, selector_caches) { + trace!("Miss: Revalidation"); + return None; + } + + debug!( + "Sharing allowed between {:?} and {:?}", + target.element, candidate.element + ); + Some(candidate.element.borrow_data().unwrap().share_styles()) + } + + /// Attempts to find an element in the cache with the given primary rule + /// node and parent. + /// + /// FIXME(emilio): re-measure this optimization, and remove if it's not very + /// useful... It's probably not worth the complexity / obscure bugs. + pub fn lookup_by_rules( + &mut self, + shared_context: &SharedStyleContext, + inherited: &ComputedValues, + rules: &StrongRuleNode, + visited_rules: Option<&StrongRuleNode>, + target: E, + ) -> Option<PrimaryStyle> { + if shared_context.options.disable_style_sharing_cache { + return None; + } + + self.cache_mut().entries.lookup(|candidate| { + debug_assert_ne!(candidate.element, target); + if !candidate.parent_style_identity().eq(inherited) { + return None; + } + let data = candidate.element.borrow_data().unwrap(); + let style = data.styles.primary(); + if style.rules.as_ref() != Some(&rules) { + return None; + } + if style.visited_rules() != visited_rules { + return None; + } + // NOTE(emilio): We only need to check name / namespace because we + // do name-dependent style adjustments, like the display: contents + // to display: none adjustment. + if target.namespace() != candidate.element.namespace() || + target.local_name() != candidate.element.local_name() + { + return None; + } + // When using container units, inherited style + rules matched aren't enough to + // determine whether the style is the same. We could actually do a full container + // lookup but for now we just check that our actual traversal parent matches. + if data + .styles + .primary() + .flags + .intersects(ComputedValueFlags::USES_CONTAINER_UNITS) && + candidate.element.traversal_parent() != target.traversal_parent() + { + return None; + } + // Rule nodes and styles are computed independent of the element's actual visitedness, + // but at the end of the cascade (in `adjust_for_visited`) we do store the + // RELEVANT_LINK_VISITED flag, so we can't share by rule node between visited and + // unvisited styles. We don't check for visitedness and just refuse to share for links + // entirely, so that visitedness doesn't affect timing. + debug_assert_eq!( + target.is_link(), + candidate.element.is_link(), + "Linkness mismatch" + ); + if target.is_link() { + return None; + } + + Some(data.share_primary_style()) + }) + } +} diff --git a/servo/components/style/str.rs b/servo/components/style/str.rs new file mode 100644 index 0000000000..9badcdf413 --- /dev/null +++ b/servo/components/style/str.rs @@ -0,0 +1,181 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! String utils for attributes and similar stuff. + +#![deny(missing_docs)] + +use num_traits::ToPrimitive; +use std::borrow::Cow; +use std::convert::AsRef; +use std::iter::{Filter, Peekable}; +use std::str::Split; + +/// A static slice of characters. +pub type StaticCharVec = &'static [char]; + +/// A static slice of `str`s. +pub type StaticStringVec = &'static [&'static str]; + +/// A "space character" according to: +/// +/// <https://html.spec.whatwg.org/multipage/#space-character> +pub static HTML_SPACE_CHARACTERS: StaticCharVec = + &['\u{0020}', '\u{0009}', '\u{000a}', '\u{000c}', '\u{000d}']; + +/// Whether a character is a HTML whitespace character. +#[inline] +pub fn char_is_whitespace(c: char) -> bool { + HTML_SPACE_CHARACTERS.contains(&c) +} + +/// Whether all the string is HTML whitespace. +#[inline] +pub fn is_whitespace(s: &str) -> bool { + s.chars().all(char_is_whitespace) +} + +#[inline] +fn not_empty(&split: &&str) -> bool { + !split.is_empty() +} + +/// Split a string on HTML whitespace. +#[inline] +pub fn split_html_space_chars<'a>( + s: &'a str, +) -> Filter<Split<'a, StaticCharVec>, fn(&&str) -> bool> { + s.split(HTML_SPACE_CHARACTERS) + .filter(not_empty as fn(&&str) -> bool) +} + +/// Split a string on commas. +#[inline] +pub fn split_commas<'a>(s: &'a str) -> Filter<Split<'a, char>, fn(&&str) -> bool> { + s.split(',').filter(not_empty as fn(&&str) -> bool) +} + +/// Character is ascii digit +pub fn is_ascii_digit(c: &char) -> bool { + match *c { + '0'..='9' => true, + _ => false, + } +} + +fn is_decimal_point(c: char) -> bool { + c == '.' +} + +fn is_exponent_char(c: char) -> bool { + match c { + 'e' | 'E' => true, + _ => false, + } +} + +/// Read a set of ascii digits and read them into a number. +pub fn read_numbers<I: Iterator<Item = char>>(mut iter: Peekable<I>) -> (Option<i64>, usize) { + match iter.peek() { + Some(c) if is_ascii_digit(c) => (), + _ => return (None, 0), + } + + iter.take_while(is_ascii_digit) + .map(|d| d as i64 - '0' as i64) + .fold((Some(0i64), 0), |accumulator, d| { + let digits = accumulator + .0 + .and_then(|accumulator| accumulator.checked_mul(10)) + .and_then(|accumulator| accumulator.checked_add(d)); + (digits, accumulator.1 + 1) + }) +} + +/// Read a decimal fraction. +pub fn read_fraction<I: Iterator<Item = char>>( + mut iter: Peekable<I>, + mut divisor: f64, + value: f64, +) -> (f64, usize) { + match iter.peek() { + Some(c) if is_decimal_point(*c) => (), + _ => return (value, 0), + } + iter.next(); + + iter.take_while(is_ascii_digit) + .map(|d| d as i64 - '0' as i64) + .fold((value, 1), |accumulator, d| { + divisor *= 10f64; + (accumulator.0 + d as f64 / divisor, accumulator.1 + 1) + }) +} + +/// Reads an exponent from an iterator over chars, for example `e100`. +pub fn read_exponent<I: Iterator<Item = char>>(mut iter: Peekable<I>) -> Option<i32> { + match iter.peek() { + Some(c) if is_exponent_char(*c) => (), + _ => return None, + } + iter.next(); + + match iter.peek() { + None => None, + Some(&'-') => { + iter.next(); + read_numbers(iter).0.map(|exp| -exp.to_i32().unwrap_or(0)) + }, + Some(&'+') => { + iter.next(); + read_numbers(iter).0.map(|exp| exp.to_i32().unwrap_or(0)) + }, + Some(_) => read_numbers(iter).0.map(|exp| exp.to_i32().unwrap_or(0)), + } +} + +/// Join a set of strings with a given delimiter `join`. +pub fn str_join<I, T>(strs: I, join: &str) -> String +where + I: IntoIterator<Item = T>, + T: AsRef<str>, +{ + strs.into_iter() + .enumerate() + .fold(String::new(), |mut acc, (i, s)| { + if i > 0 { + acc.push_str(join); + } + acc.push_str(s.as_ref()); + acc + }) +} + +/// Returns true if a given string has a given prefix with case-insensitive match. +pub fn starts_with_ignore_ascii_case(string: &str, prefix: &str) -> bool { + string.len() >= prefix.len() && + string.as_bytes()[0..prefix.len()].eq_ignore_ascii_case(prefix.as_bytes()) +} + +/// Returns an ascii lowercase version of a string, only allocating if needed. +pub fn string_as_ascii_lowercase<'a>(input: &'a str) -> Cow<'a, str> { + if input.bytes().any(|c| matches!(c, b'A'..=b'Z')) { + input.to_ascii_lowercase().into() + } else { + // Already ascii lowercase. + Cow::Borrowed(input) + } +} + +/// To avoid accidentally instantiating multiple monomorphizations of large +/// serialization routines, we define explicit concrete types and require +/// them in those routines. This avoids accidental mixing of String and +/// nsACString arguments in Gecko, which would cause code size to blow up. +#[cfg(feature = "gecko")] +pub type CssStringWriter = ::nsstring::nsACString; + +/// String type that coerces to CssStringWriter, used when serialization code +/// needs to allocate a temporary string. +#[cfg(feature = "gecko")] +pub type CssString = ::nsstring::nsCString; diff --git a/servo/components/style/style_adjuster.rs b/servo/components/style/style_adjuster.rs new file mode 100644 index 0000000000..a993d79d6a --- /dev/null +++ b/servo/components/style/style_adjuster.rs @@ -0,0 +1,1009 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A struct to encapsulate all the style fixups and flags propagations +//! a computed style needs in order for it to adhere to the CSS spec. + +use crate::computed_value_flags::ComputedValueFlags; +use crate::dom::TElement; +use crate::properties::longhands::contain::computed_value::T as Contain; +use crate::properties::longhands::container_type::computed_value::T as ContainerType; +use crate::properties::longhands::content_visibility::computed_value::T as ContentVisibility; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::longhands::float::computed_value::T as Float; +use crate::properties::longhands::overflow_x::computed_value::T as Overflow; +use crate::properties::longhands::position::computed_value::T as Position; +use crate::properties::{self, ComputedValues, StyleBuilder}; + +/// A struct that implements all the adjustment methods. +/// +/// NOTE(emilio): If new adjustments are introduced that depend on reset +/// properties of the parent, you may need tweaking the +/// `ChildCascadeRequirement` code in `matching.rs`. +/// +/// NOTE(emilio): Also, if new adjustments are introduced that break the +/// following invariant: +/// +/// Given same tag name, namespace, rules and parent style, two elements would +/// end up with exactly the same style. +/// +/// Then you need to adjust the lookup_by_rules conditions in the sharing cache. +pub struct StyleAdjuster<'a, 'b: 'a> { + style: &'a mut StyleBuilder<'b>, +} + +#[cfg(feature = "gecko")] +fn is_topmost_svg_svg_element<E>(e: E) -> bool +where + E: TElement, +{ + debug_assert!(e.is_svg_element()); + if e.local_name() != &*atom!("svg") { + return false; + } + + let parent = match e.traversal_parent() { + Some(n) => n, + None => return true, + }; + + if !parent.is_svg_element() { + return true; + } + + parent.local_name() == &*atom!("foreignObject") +} + +// https://drafts.csswg.org/css-display/#unbox +#[cfg(feature = "gecko")] +fn is_effective_display_none_for_display_contents<E>(element: E) -> bool +where + E: TElement, +{ + use crate::Atom; + + const SPECIAL_HTML_ELEMENTS: [Atom; 16] = [ + atom!("br"), + atom!("wbr"), + atom!("meter"), + atom!("progress"), + atom!("canvas"), + atom!("embed"), + atom!("object"), + atom!("audio"), + atom!("iframe"), + atom!("img"), + atom!("video"), + atom!("frame"), + atom!("frameset"), + atom!("input"), + atom!("textarea"), + atom!("select"), + ]; + + // https://drafts.csswg.org/css-display/#unbox-svg + // + // There's a note about "Unknown elements", but there's not a good way to + // know what that means, or to get that information from here, and no other + // UA implements this either. + const SPECIAL_SVG_ELEMENTS: [Atom; 6] = [ + atom!("svg"), + atom!("a"), + atom!("g"), + atom!("use"), + atom!("tspan"), + atom!("textPath"), + ]; + + // https://drafts.csswg.org/css-display/#unbox-html + if element.is_html_element() { + let local_name = element.local_name(); + return SPECIAL_HTML_ELEMENTS + .iter() + .any(|name| &**name == local_name); + } + + // https://drafts.csswg.org/css-display/#unbox-svg + if element.is_svg_element() { + if is_topmost_svg_svg_element(element) { + return true; + } + let local_name = element.local_name(); + return !SPECIAL_SVG_ELEMENTS + .iter() + .any(|name| &**name == local_name); + } + + // https://drafts.csswg.org/css-display/#unbox-mathml + // + // We always treat XUL as display: none. We don't use display: + // contents in XUL anyway, so should be fine to be consistent with + // MathML unless there's a use case for it. + if element.is_mathml_element() || element.is_xul_element() { + return true; + } + + false +} + +impl<'a, 'b: 'a> StyleAdjuster<'a, 'b> { + /// Trivially constructs a new StyleAdjuster. + #[inline] + pub fn new(style: &'a mut StyleBuilder<'b>) -> Self { + StyleAdjuster { style } + } + + /// <https://fullscreen.spec.whatwg.org/#new-stacking-layer> + /// + /// Any position value other than 'absolute' and 'fixed' are + /// computed to 'absolute' if the element is in a top layer. + /// + fn adjust_for_top_layer(&mut self) { + if !self.style.in_top_layer() { + return; + } + if !self.style.is_absolutely_positioned() { + self.style.mutate_box().set_position(Position::Absolute); + } + if self.style.get_box().clone_display().is_contents() { + self.style.mutate_box().set_display(Display::Block); + } + } + + /// -webkit-box with line-clamp and vertical orientation gets turned into + /// flow-root at computed-value time. + /// + /// This makes the element not be a flex container, with all that it + /// implies, but it should be safe. It matches blink, see + /// https://bugzilla.mozilla.org/show_bug.cgi?id=1786147#c10 + fn adjust_for_webkit_line_clamp(&mut self) { + use crate::properties::longhands::_moz_box_orient::computed_value::T as BoxOrient; + use crate::values::specified::box_::{DisplayInside, DisplayOutside}; + let box_style = self.style.get_box(); + if box_style.clone__webkit_line_clamp().is_none() { + return; + } + let disp = box_style.clone_display(); + if disp.inside() != DisplayInside::WebkitBox { + return; + } + if self.style.get_xul().clone__moz_box_orient() != BoxOrient::Vertical { + return; + } + let new_display = if disp.outside() == DisplayOutside::Block { + Display::FlowRoot + } else { + debug_assert_eq!(disp.outside(), DisplayOutside::Inline); + Display::InlineBlock + }; + self.style + .mutate_box() + .set_adjusted_display(new_display, false); + } + + /// CSS 2.1 section 9.7: + /// + /// If 'position' has the value 'absolute' or 'fixed', [...] the computed + /// value of 'float' is 'none'. + /// + fn adjust_for_position(&mut self) { + if self.style.is_absolutely_positioned() && self.style.is_floating() { + self.style.mutate_box().set_float(Float::None); + } + } + + /// Whether we should skip any item-based display property blockification on + /// this element. + fn skip_item_display_fixup<E>(&self, element: Option<E>) -> bool + where + E: TElement, + { + if let Some(pseudo) = self.style.pseudo { + return pseudo.skip_item_display_fixup(); + } + + element.map_or(false, |e| e.skip_item_display_fixup()) + } + + /// Apply the blockification rules based on the table in CSS 2.2 section 9.7. + /// <https://drafts.csswg.org/css2/visuren.html#dis-pos-flo> + /// A ::marker pseudo-element with 'list-style-position:outside' needs to + /// have its 'display' blockified, unless the ::marker is for an inline + /// list-item (for which 'list-style-position:outside' behaves as 'inside'). + /// https://drafts.csswg.org/css-lists-3/#list-style-position-property + fn blockify_if_necessary<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>) + where + E: TElement, + { + let mut blockify = false; + macro_rules! blockify_if { + ($if_what:expr) => { + if !blockify { + blockify = $if_what; + } + }; + } + + blockify_if!(self.style.is_root_element); + if !self.skip_item_display_fixup(element) { + let parent_display = layout_parent_style.get_box().clone_display(); + blockify_if!(parent_display.is_item_container()); + } + + let is_item_or_root = blockify; + + blockify_if!(self.style.is_floating()); + blockify_if!(self.style.is_absolutely_positioned()); + + if !blockify { + return; + } + + let display = self.style.get_box().clone_display(); + let blockified_display = display.equivalent_block_display(self.style.is_root_element); + if display != blockified_display { + self.style + .mutate_box() + .set_adjusted_display(blockified_display, is_item_or_root); + } + } + + /// Compute a few common flags for both text and element's style. + fn set_bits(&mut self) { + let box_style = self.style.get_box(); + let display = box_style.clone_display(); + + if !display.is_contents() { + if !self + .style + .get_text() + .clone_text_decoration_line() + .is_empty() + { + self.style + .add_flags(ComputedValueFlags::HAS_TEXT_DECORATION_LINES); + } + + if self.style.get_effects().clone_opacity() == 0. { + self.style + .add_flags(ComputedValueFlags::IS_IN_OPACITY_ZERO_SUBTREE); + } + } + + if self.style.is_pseudo_element() { + self.style + .add_flags(ComputedValueFlags::IS_IN_PSEUDO_ELEMENT_SUBTREE); + } + + if self.style.is_root_element { + self.style + .add_flags(ComputedValueFlags::IS_ROOT_ELEMENT_STYLE); + } + + if box_style + .clone_effective_containment() + .contains(Contain::STYLE) + { + self.style + .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_CONTAIN_STYLE); + } + + if box_style.clone_container_type().is_size_container_type() { + self.style + .add_flags(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE); + } + + #[cfg(feature = "servo-layout-2013")] + { + if self.style.get_parent_column().is_multicol() { + self.style.add_flags(ComputedValueFlags::CAN_BE_FRAGMENTED); + } + } + } + + /// Adjust the style for text style. + /// + /// The adjustments here are a subset of the adjustments generally, because + /// text only inherits properties. + /// + /// Note that this, for Gecko, comes through Servo_ComputedValues_Inherit. + #[cfg(feature = "gecko")] + pub fn adjust_for_text(&mut self) { + debug_assert!(!self.style.is_root_element); + self.adjust_for_text_combine_upright(); + self.adjust_for_text_in_ruby(); + self.set_bits(); + } + + /// Change writing mode of the text frame for text-combine-upright. + /// + /// It is safe to look at our own style because we are looking at inherited + /// properties, and text is just plain inheritance. + /// + /// TODO(emilio): we should (Gecko too) revise these adjustments in presence + /// of display: contents. + /// + /// FIXME(emilio): How does this play with logical properties? Doesn't + /// mutating writing-mode change the potential physical sides chosen? + #[cfg(feature = "gecko")] + fn adjust_for_text_combine_upright(&mut self) { + use crate::computed_values::text_combine_upright::T as TextCombineUpright; + use crate::computed_values::writing_mode::T as WritingMode; + use crate::logical_geometry; + + let writing_mode = self.style.get_inherited_box().clone_writing_mode(); + let text_combine_upright = self.style.get_inherited_text().clone_text_combine_upright(); + + if matches!( + writing_mode, + WritingMode::VerticalRl | WritingMode::VerticalLr + ) && text_combine_upright == TextCombineUpright::All + { + self.style.add_flags(ComputedValueFlags::IS_TEXT_COMBINED); + self.style + .mutate_inherited_box() + .set_writing_mode(WritingMode::HorizontalTb); + self.style.writing_mode = + logical_geometry::WritingMode::new(self.style.get_inherited_box()); + } + } + + /// Unconditionally propagates the line break suppression flag to text, and + /// additionally it applies it if it is in any ruby box. + /// + /// This is necessary because its parent may not itself have the flag set + /// (e.g. ruby or ruby containers), thus we may not inherit the flag from + /// them. + #[cfg(feature = "gecko")] + fn adjust_for_text_in_ruby(&mut self) { + let parent_display = self.style.get_parent_box().clone_display(); + if parent_display.is_ruby_type() || + self.style + .get_parent_flags() + .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK) + { + self.style + .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK); + } + } + + /// <https://drafts.csswg.org/css-writing-modes-3/#block-flow:> + /// + /// If a box has a different writing-mode value than its containing + /// block: + /// + /// - If the box has a specified display of inline, its display + /// computes to inline-block. [CSS21] + /// + /// This matches the adjustment that Gecko does, not exactly following + /// the spec. See also: + /// + /// <https://lists.w3.org/Archives/Public/www-style/2017Mar/0045.html> + /// <https://github.com/servo/servo/issues/15754> + fn adjust_for_writing_mode(&mut self, layout_parent_style: &ComputedValues) { + let our_writing_mode = self.style.get_inherited_box().clone_writing_mode(); + let parent_writing_mode = layout_parent_style.get_inherited_box().clone_writing_mode(); + + if our_writing_mode != parent_writing_mode && + self.style.get_box().clone_display() == Display::Inline + { + // TODO(emilio): Figure out if we can just set the adjusted display + // on Gecko too and unify this code path. + if cfg!(feature = "servo") { + self.style + .mutate_box() + .set_adjusted_display(Display::InlineBlock, false); + } else { + self.style.mutate_box().set_display(Display::InlineBlock); + } + } + } + + /// This implements an out-of-date spec. The new spec moves the handling of + /// this to layout, which Gecko implements but Servo doesn't. + /// + /// See https://github.com/servo/servo/issues/15229 + #[cfg(feature = "servo")] + fn adjust_for_alignment(&mut self, layout_parent_style: &ComputedValues) { + use crate::computed_values::align_items::T as AlignItems; + use crate::computed_values::align_self::T as AlignSelf; + + if self.style.get_position().clone_align_self() == AlignSelf::Auto && + !self.style.is_absolutely_positioned() + { + let self_align = match layout_parent_style.get_position().clone_align_items() { + AlignItems::Stretch => AlignSelf::Stretch, + AlignItems::Baseline => AlignSelf::Baseline, + AlignItems::FlexStart => AlignSelf::FlexStart, + AlignItems::FlexEnd => AlignSelf::FlexEnd, + AlignItems::Center => AlignSelf::Center, + }; + self.style.mutate_position().set_align_self(self_align); + } + } + + /// The initial value of border-*-width may be changed at computed value + /// time. + /// + /// This is moved to properties.rs for convenience. + fn adjust_for_border_width(&mut self) { + properties::adjust_border_width(self.style); + } + + /// column-rule-style: none causes a computed column-rule-width of zero + /// at computed value time. + fn adjust_for_column_rule_width(&mut self) { + let column_style = self.style.get_column(); + if !column_style.clone_column_rule_style().none_or_hidden() { + return; + } + if !column_style.column_rule_has_nonzero_width() { + return; + } + self.style + .mutate_column() + .set_column_rule_width(crate::Zero::zero()); + } + + /// outline-style: none causes a computed outline-width of zero at computed + /// value time. + fn adjust_for_outline_width(&mut self) { + let outline = self.style.get_outline(); + if !outline.clone_outline_style().none_or_hidden() { + return; + } + if !outline.outline_has_nonzero_width() { + return; + } + self.style + .mutate_outline() + .set_outline_width(crate::Zero::zero()); + } + + /// CSS overflow-x and overflow-y require some fixup as well in some cases. + /// https://drafts.csswg.org/css-overflow-3/#overflow-properties + /// "Computed value: as specified, except with `visible`/`clip` computing to + /// `auto`/`hidden` (respectively) if one of `overflow-x` or `overflow-y` is + /// neither `visible` nor `clip`." + fn adjust_for_overflow(&mut self) { + let overflow_x = self.style.get_box().clone_overflow_x(); + let overflow_y = self.style.get_box().clone_overflow_y(); + if overflow_x == overflow_y { + return; // optimization for the common case + } + + if overflow_x.is_scrollable() != overflow_y.is_scrollable() { + let box_style = self.style.mutate_box(); + box_style.set_overflow_x(overflow_x.to_scrollable()); + box_style.set_overflow_y(overflow_y.to_scrollable()); + } + } + + fn adjust_for_contain(&mut self) { + let box_style = self.style.get_box(); + let container_type = box_style.clone_container_type(); + let content_visibility = box_style.clone_content_visibility(); + if container_type == ContainerType::Normal && + content_visibility == ContentVisibility::Visible + { + debug_assert_eq!( + box_style.clone_contain(), + box_style.clone_effective_containment() + ); + return; + } + let old_contain = box_style.clone_contain(); + let mut new_contain = old_contain; + match content_visibility { + ContentVisibility::Visible => {}, + // `content-visibility:auto` also applies size containment when content + // is not relevant (and therefore skipped). This is checked in + // nsIFrame::GetContainSizeAxes. + ContentVisibility::Auto => { + new_contain.insert(Contain::LAYOUT | Contain::PAINT | Contain::STYLE) + }, + ContentVisibility::Hidden => new_contain + .insert(Contain::LAYOUT | Contain::PAINT | Contain::SIZE | Contain::STYLE), + } + match container_type { + ContainerType::Normal => {}, + // https://drafts.csswg.org/css-contain-3/#valdef-container-type-inline-size: + // Applies layout containment, style containment, and inline-size + // containment to the principal box. + ContainerType::InlineSize => { + new_contain.insert(Contain::LAYOUT | Contain::STYLE | Contain::INLINE_SIZE) + }, + // https://drafts.csswg.org/css-contain-3/#valdef-container-type-size: + // Applies layout containment, style containment, and size + // containment to the principal box. + ContainerType::Size => { + new_contain.insert(Contain::LAYOUT | Contain::STYLE | Contain::SIZE) + }, + } + if new_contain == old_contain { + debug_assert_eq!( + box_style.clone_contain(), + box_style.clone_effective_containment() + ); + return; + } + self.style + .mutate_box() + .set_effective_containment(new_contain); + } + + /// content-visibility: auto should force contain-intrinsic-size to gain + /// an auto value + /// + /// <https://github.com/w3c/csswg-drafts/issues/8407> + fn adjust_for_contain_intrinsic_size(&mut self) { + let content_visibility = self.style.get_box().clone_content_visibility(); + if content_visibility != ContentVisibility::Auto { + return; + } + + let pos = self.style.get_position(); + let new_width = pos.clone_contain_intrinsic_width().add_auto_if_needed(); + let new_height = pos.clone_contain_intrinsic_height().add_auto_if_needed(); + if new_width.is_none() && new_height.is_none() { + return; + } + + let pos = self.style.mutate_position(); + if let Some(width) = new_width { + pos.set_contain_intrinsic_width(width); + } + if let Some(height) = new_height { + pos.set_contain_intrinsic_height(height); + } + } + + /// Handles the relevant sections in: + /// + /// https://drafts.csswg.org/css-display/#unbox-html + /// + /// And forbidding display: contents in pseudo-elements, at least for now. + #[cfg(feature = "gecko")] + fn adjust_for_prohibited_display_contents<E>(&mut self, element: Option<E>) + where + E: TElement, + { + if self.style.get_box().clone_display() != Display::Contents { + return; + } + + // FIXME(emilio): ::before and ::after should support display: contents, + // see bug 1418138. + if self.style.pseudo.is_some() { + self.style.mutate_box().set_display(Display::Inline); + return; + } + + let element = match element { + Some(e) => e, + None => return, + }; + + if is_effective_display_none_for_display_contents(element) { + self.style.mutate_box().set_display(Display::None); + } + } + + /// <textarea>'s editor root needs to inherit the overflow value from its + /// parent, but we need to make sure it's still scrollable. + #[cfg(feature = "gecko")] + fn adjust_for_text_control_editing_root(&mut self) { + use crate::selector_parser::PseudoElement; + + if self.style.pseudo != Some(&PseudoElement::MozTextControlEditingRoot) { + return; + } + + let box_style = self.style.get_box(); + let overflow_x = box_style.clone_overflow_x(); + let overflow_y = box_style.clone_overflow_y(); + + // If at least one is scrollable we'll adjust the other one in + // adjust_for_overflow if needed. + if overflow_x.is_scrollable() || overflow_y.is_scrollable() { + return; + } + + let box_style = self.style.mutate_box(); + box_style.set_overflow_x(Overflow::Auto); + box_style.set_overflow_y(Overflow::Auto); + } + + /// If a <fieldset> has grid/flex display type, we need to inherit + /// this type into its ::-moz-fieldset-content anonymous box. + /// + /// NOTE(emilio): We don't need to handle the display change for this case + /// in matching.rs because anonymous box restyling works separately to the + /// normal cascading process. + #[cfg(feature = "gecko")] + fn adjust_for_fieldset_content(&mut self, layout_parent_style: &ComputedValues) { + use crate::selector_parser::PseudoElement; + + if self.style.pseudo != Some(&PseudoElement::FieldsetContent) { + return; + } + + // TODO We actually want style from parent rather than layout + // parent, so that this fixup doesn't happen incorrectly when + // when <fieldset> has "display: contents". + let parent_display = layout_parent_style.get_box().clone_display(); + let new_display = match parent_display { + Display::Flex | Display::InlineFlex => Some(Display::Flex), + Display::Grid | Display::InlineGrid => Some(Display::Grid), + _ => None, + }; + if let Some(new_display) = new_display { + self.style.mutate_box().set_display(new_display); + } + } + + /// -moz-center, -moz-left and -moz-right are used for HTML's alignment. + /// + /// This is covering the <div align="right"><table>...</table></div> case. + /// + /// In this case, we don't want to inherit the text alignment into the + /// table. + #[cfg(feature = "gecko")] + fn adjust_for_table_text_align(&mut self) { + use crate::properties::longhands::text_align::computed_value::T as TextAlign; + if self.style.get_box().clone_display() != Display::Table { + return; + } + + match self.style.get_inherited_text().clone_text_align() { + TextAlign::MozLeft | TextAlign::MozCenter | TextAlign::MozRight => {}, + _ => return, + } + + self.style + .mutate_inherited_text() + .set_text_align(TextAlign::Start) + } + + /// Computes the used text decoration for Servo. + /// + /// FIXME(emilio): This is a layout tree concept, should move away from + /// style, since otherwise we're going to have the same subtle bugs WebKit + /// and Blink have with this very same thing. + #[cfg(feature = "servo")] + fn adjust_for_text_decorations_in_effect(&mut self) { + use crate::values::computed::text::TextDecorationsInEffect; + + let decorations_in_effect = TextDecorationsInEffect::from_style(&self.style); + if self.style.get_inherited_text().text_decorations_in_effect != decorations_in_effect { + self.style + .mutate_inherited_text() + .text_decorations_in_effect = decorations_in_effect; + } + } + + #[cfg(feature = "gecko")] + fn should_suppress_linebreak<E>( + &self, + layout_parent_style: &ComputedValues, + element: Option<E>, + ) -> bool + where + E: TElement, + { + // Line break suppression should only be propagated to in-flow children. + if self.style.is_floating() || self.style.is_absolutely_positioned() { + return false; + } + let parent_display = layout_parent_style.get_box().clone_display(); + if layout_parent_style + .flags + .contains(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK) + { + // Line break suppression is propagated to any children of + // line participants. + if parent_display.is_line_participant() { + return true; + } + } + match self.style.get_box().clone_display() { + // Ruby base and text are always non-breakable. + Display::RubyBase | Display::RubyText => true, + // Ruby base container and text container are breakable. + // Non-HTML elements may not form ruby base / text container because + // they may not respect ruby-internal display values, so we can't + // make them escaped from line break suppression. + // Note that, when certain HTML tags, e.g. form controls, have ruby + // level container display type, they could also escape from the + // line break suppression flag while they shouldn't. However, it is + // generally fine as far as they can't break the line inside them. + Display::RubyBaseContainer | Display::RubyTextContainer + if element.map_or(true, |e| e.is_html_element()) => + { + false + }, + // Anything else is non-breakable if and only if its layout parent + // has a ruby display type, because any of the ruby boxes can be + // anonymous. + _ => parent_display.is_ruby_type(), + } + } + + /// Do ruby-related style adjustments, which include: + /// * propagate the line break suppression flag, + /// * inlinify block descendants, + /// * suppress border and padding for ruby level containers, + /// * correct unicode-bidi. + #[cfg(feature = "gecko")] + fn adjust_for_ruby<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>) + where + E: TElement, + { + use crate::properties::longhands::unicode_bidi::computed_value::T as UnicodeBidi; + + let self_display = self.style.get_box().clone_display(); + // Check whether line break should be suppressed for this element. + if self.should_suppress_linebreak(layout_parent_style, element) { + self.style + .add_flags(ComputedValueFlags::SHOULD_SUPPRESS_LINEBREAK); + // Inlinify the display type if allowed. + if !self.skip_item_display_fixup(element) { + let inline_display = self_display.inlinify(); + if self_display != inline_display { + self.style + .mutate_box() + .set_adjusted_display(inline_display, false); + } + } + } + // Suppress border and padding for ruby level containers. + // This is actually not part of the spec. It is currently unspecified + // how border and padding should be handled for ruby level container, + // and suppressing them here make it easier for layout to handle. + if self_display.is_ruby_level_container() { + self.style.reset_border_struct(); + self.style.reset_padding_struct(); + } + + // Force bidi isolation on all internal ruby boxes and ruby container + // per spec https://drafts.csswg.org/css-ruby-1/#bidi + if self_display.is_ruby_type() { + let new_value = match self.style.get_text().clone_unicode_bidi() { + UnicodeBidi::Normal | UnicodeBidi::Embed => Some(UnicodeBidi::Isolate), + UnicodeBidi::BidiOverride => Some(UnicodeBidi::IsolateOverride), + _ => None, + }; + if let Some(new_value) = new_value { + self.style.mutate_text().set_unicode_bidi(new_value); + } + } + } + + /// Computes the RELEVANT_LINK_VISITED flag based on the parent style and on + /// whether we're a relevant link. + /// + /// NOTE(emilio): We don't do this for text styles, which is... dubious, but + /// Gecko doesn't seem to do it either. It's extremely easy to do if needed + /// though. + /// + /// FIXME(emilio): This isn't technically a style adjustment thingie, could + /// it move somewhere else? + fn adjust_for_visited<E>(&mut self, element: Option<E>) + where + E: TElement, + { + if !self.style.has_visited_style() { + return; + } + + let is_link_element = self.style.pseudo.is_none() && element.map_or(false, |e| e.is_link()); + + if !is_link_element { + return; + } + + if element.unwrap().is_visited_link() { + self.style + .add_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED); + } else { + // Need to remove to handle unvisited link inside visited. + self.style + .remove_flags(ComputedValueFlags::IS_RELEVANT_LINK_VISITED); + } + } + + /// Resolves "justify-items: legacy" based on the inherited style if needed + /// to comply with: + /// + /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy> + #[cfg(feature = "gecko")] + fn adjust_for_justify_items(&mut self) { + use crate::values::specified::align; + let justify_items = self.style.get_position().clone_justify_items(); + if justify_items.specified.0 != align::AlignFlags::LEGACY { + return; + } + + let parent_justify_items = self.style.get_parent_position().clone_justify_items(); + + if !parent_justify_items + .computed + .0 + .contains(align::AlignFlags::LEGACY) + { + return; + } + + if parent_justify_items.computed == justify_items.computed { + return; + } + + self.style + .mutate_position() + .set_computed_justify_items(parent_justify_items.computed); + } + + /// If '-webkit-appearance' is 'menulist' on a <select> element then + /// the computed value of 'line-height' is 'normal'. + /// + /// https://github.com/w3c/csswg-drafts/issues/3257 + #[cfg(feature = "gecko")] + fn adjust_for_appearance<E>(&mut self, element: Option<E>) + where + E: TElement, + { + use crate::properties::longhands::appearance::computed_value::T as Appearance; + use crate::properties::longhands::line_height::computed_value::T as LineHeight; + + let box_ = self.style.get_box(); + let appearance = match box_.clone_appearance() { + Appearance::Auto => box_.clone__moz_default_appearance(), + a => a, + }; + + if appearance == Appearance::Menulist { + if self.style.get_font().clone_line_height() == LineHeight::normal() { + return; + } + if self.style.pseudo.is_some() { + return; + } + let is_html_select_element = element.map_or(false, |e| { + e.is_html_element() && e.local_name() == &*atom!("select") + }); + if !is_html_select_element { + return; + } + self.style + .mutate_font() + .set_line_height(LineHeight::normal()); + } + } + + /// A legacy ::marker (i.e. no 'content') without an author-specified 'font-family' + /// and 'list-style-type:disc|circle|square|disclosure-closed|disclosure-open' + /// is assigned 'font-family:-moz-bullet-font'. (This is for <ul><li> etc.) + /// We don't want synthesized italic/bold for this font, so turn that off too. + /// Likewise for 'letter/word-spacing' -- unless the author specified it then reset + /// them to their initial value because traditionally we never added such spacing + /// between a legacy bullet and the list item's content, so we keep that behavior + /// for web-compat reasons. + /// We intentionally don't check 'list-style-image' below since we want it to use + /// the same font as its fallback ('list-style-type') in case it fails to load. + #[cfg(feature = "gecko")] + fn adjust_for_marker_pseudo(&mut self) { + use crate::values::computed::counters::Content; + use crate::values::computed::font::{FontFamily, FontSynthesis}; + use crate::values::computed::text::{LetterSpacing, WordSpacing}; + + let is_legacy_marker = self.style.pseudo.map_or(false, |p| p.is_marker()) && + self.style.get_list().clone_list_style_type().is_bullet() && + self.style.get_counters().clone_content() == Content::Normal; + if !is_legacy_marker { + return; + } + let flags = self.style.flags.get(); + if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_FAMILY) { + self.style + .mutate_font() + .set_font_family(FontFamily::moz_bullet().clone()); + + // FIXME(mats): We can remove this if support for font-synthesis is added to @font-face rules. + // Then we can add it to the @font-face rule in html.css instead. + // https://github.com/w3c/csswg-drafts/issues/6081 + if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_WEIGHT) { + self.style + .mutate_font() + .set_font_synthesis_weight(FontSynthesis::None); + } + if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_FONT_SYNTHESIS_STYLE) { + self.style + .mutate_font() + .set_font_synthesis_style(FontSynthesis::None); + } + } + if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_LETTER_SPACING) { + self.style + .mutate_inherited_text() + .set_letter_spacing(LetterSpacing::normal()); + } + if !flags.contains(ComputedValueFlags::HAS_AUTHOR_SPECIFIED_WORD_SPACING) { + self.style + .mutate_inherited_text() + .set_word_spacing(WordSpacing::normal()); + } + } + + /// Adjusts the style to account for various fixups that don't fit naturally + /// into the cascade. + /// + /// When comparing to Gecko, this is similar to the work done by + /// `ComputedStyle::ApplyStyleFixups`, plus some parts of + /// `nsStyleSet::GetContext`. + pub fn adjust<E>(&mut self, layout_parent_style: &ComputedValues, element: Option<E>) + where + E: TElement, + { + if cfg!(debug_assertions) { + if element.map_or(false, |e| e.is_pseudo_element()) { + // It'd be nice to assert `self.style.pseudo == Some(&pseudo)`, + // but we do resolve ::-moz-list pseudos on ::before / ::after + // content, sigh. + debug_assert!(self.style.pseudo.is_some(), "Someone really messed up"); + } + } + // FIXME(emilio): The apply_declarations callsite in Servo's + // animation, and the font stuff for Gecko + // (Stylist::compute_for_declarations) should pass an element to + // cascade(), then we can make this assertion hold everywhere. + // debug_assert!( + // element.is_some() || self.style.pseudo.is_some(), + // "Should always have an element around for non-pseudo styles" + // ); + + self.adjust_for_visited(element); + #[cfg(feature = "gecko")] + { + self.adjust_for_prohibited_display_contents(element); + self.adjust_for_fieldset_content(layout_parent_style); + // NOTE: It's important that this happens before + // adjust_for_overflow. + self.adjust_for_text_control_editing_root(); + } + self.adjust_for_top_layer(); + self.blockify_if_necessary(layout_parent_style, element); + self.adjust_for_webkit_line_clamp(); + self.adjust_for_position(); + self.adjust_for_overflow(); + self.adjust_for_contain(); + self.adjust_for_contain_intrinsic_size(); + #[cfg(feature = "gecko")] + { + self.adjust_for_table_text_align(); + self.adjust_for_justify_items(); + } + #[cfg(feature = "servo")] + { + self.adjust_for_alignment(layout_parent_style); + } + self.adjust_for_border_width(); + self.adjust_for_column_rule_width(); + self.adjust_for_outline_width(); + self.adjust_for_writing_mode(layout_parent_style); + #[cfg(feature = "gecko")] + { + self.adjust_for_ruby(layout_parent_style, element); + } + #[cfg(feature = "servo")] + { + self.adjust_for_text_decorations_in_effect(); + } + #[cfg(feature = "gecko")] + { + self.adjust_for_appearance(element); + self.adjust_for_marker_pseudo(); + } + self.set_bits(); + } +} diff --git a/servo/components/style/style_resolver.rs b/servo/components/style/style_resolver.rs new file mode 100644 index 0000000000..5c940ad2be --- /dev/null +++ b/servo/components/style/style_resolver.rs @@ -0,0 +1,585 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Style resolution for a given element or pseudo-element. + +use crate::applicable_declarations::ApplicableDeclarationList; +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{CascadeInputs, ElementCascadeInputs, StyleContext}; +use crate::data::{EagerPseudoStyles, ElementStyles}; +use crate::dom::TElement; +use crate::matching::MatchMethods; +use crate::properties::longhands::display::computed_value::T as Display; +use crate::properties::{ComputedValues, FirstLineReparenting}; +use crate::rule_tree::StrongRuleNode; +use crate::selector_parser::{PseudoElement, SelectorImpl}; +use crate::stylist::RuleInclusion; +use log::Level::Trace; +use selectors::matching::{ + MatchingContext, MatchingForInvalidation, MatchingMode, NeedsSelectorFlags, VisitedHandlingMode, +}; +use servo_arc::Arc; + +/// Whether pseudo-elements should be resolved or not. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PseudoElementResolution { + /// Only resolve pseudo-styles if possibly applicable. + IfApplicable, + /// Force pseudo-element resolution. + Force, +} + +/// A struct that takes care of resolving the style of a given element. +pub struct StyleResolverForElement<'a, 'ctx, 'le, E> +where + 'ctx: 'a, + 'le: 'ctx, + E: TElement + MatchMethods + 'le, +{ + element: E, + context: &'a mut StyleContext<'ctx, E>, + rule_inclusion: RuleInclusion, + pseudo_resolution: PseudoElementResolution, + _marker: ::std::marker::PhantomData<&'le E>, +} + +struct MatchingResults { + rule_node: StrongRuleNode, + flags: ComputedValueFlags, +} + +/// A style returned from the resolver machinery. +pub struct ResolvedStyle(pub Arc<ComputedValues>); + +/// The primary style of an element or an element-backed pseudo-element. +pub struct PrimaryStyle { + /// The style itself. + pub style: ResolvedStyle, + /// Whether the style was reused from another element via the rule node (see + /// `StyleSharingCache::lookup_by_rules`). + pub reused_via_rule_node: bool, +} + +/// A set of style returned from the resolver machinery. +pub struct ResolvedElementStyles { + /// Primary style. + pub primary: PrimaryStyle, + /// Pseudo styles. + pub pseudos: EagerPseudoStyles, +} + +impl ResolvedElementStyles { + /// Convenience accessor for the primary style. + pub fn primary_style(&self) -> &Arc<ComputedValues> { + &self.primary.style.0 + } + + /// Convenience mutable accessor for the style. + pub fn primary_style_mut(&mut self) -> &mut Arc<ComputedValues> { + &mut self.primary.style.0 + } +} + +impl PrimaryStyle { + /// Convenience accessor for the style. + pub fn style(&self) -> &ComputedValues { + &*self.style.0 + } +} + +impl From<ResolvedElementStyles> for ElementStyles { + fn from(r: ResolvedElementStyles) -> ElementStyles { + ElementStyles { + primary: Some(r.primary.style.0), + pseudos: r.pseudos, + } + } +} + +fn with_default_parent_styles<E, F, R>(element: E, f: F) -> R +where + E: TElement, + F: FnOnce(Option<&ComputedValues>, Option<&ComputedValues>) -> R, +{ + let parent_el = element.inheritance_parent(); + let parent_data = parent_el.as_ref().and_then(|e| e.borrow_data()); + let parent_style = parent_data.as_ref().map(|d| d.styles.primary()); + + let mut layout_parent_el = parent_el.clone(); + let layout_parent_data; + let mut layout_parent_style = parent_style; + if parent_style.map_or(false, |s| s.is_display_contents()) { + layout_parent_el = Some(layout_parent_el.unwrap().layout_parent()); + layout_parent_data = layout_parent_el.as_ref().unwrap().borrow_data().unwrap(); + layout_parent_style = Some(layout_parent_data.styles.primary()); + } + + f( + parent_style.map(|x| &**x), + layout_parent_style.map(|s| &**s), + ) +} + +fn layout_parent_style_for_pseudo<'a>( + primary_style: &'a PrimaryStyle, + layout_parent_style: Option<&'a ComputedValues>, +) -> Option<&'a ComputedValues> { + if primary_style.style().is_display_contents() { + layout_parent_style + } else { + Some(primary_style.style()) + } +} + +fn eager_pseudo_is_definitely_not_generated( + pseudo: &PseudoElement, + style: &ComputedValues, +) -> bool { + if !pseudo.is_before_or_after() { + return false; + } + + if !style + .flags + .intersects(ComputedValueFlags::DISPLAY_DEPENDS_ON_INHERITED_STYLE) && + style.get_box().clone_display() == Display::None + { + return true; + } + + if !style + .flags + .intersects(ComputedValueFlags::CONTENT_DEPENDS_ON_INHERITED_STYLE) && + style.ineffective_content_property() + { + return true; + } + + false +} + +impl<'a, 'ctx, 'le, E> StyleResolverForElement<'a, 'ctx, 'le, E> +where + 'ctx: 'a, + 'le: 'ctx, + E: TElement + MatchMethods + 'le, +{ + /// Trivially construct a new StyleResolverForElement. + pub fn new( + element: E, + context: &'a mut StyleContext<'ctx, E>, + rule_inclusion: RuleInclusion, + pseudo_resolution: PseudoElementResolution, + ) -> Self { + Self { + element, + context, + rule_inclusion, + pseudo_resolution, + _marker: ::std::marker::PhantomData, + } + } + + /// Resolve just the style of a given element. + pub fn resolve_primary_style( + &mut self, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + ) -> PrimaryStyle { + let primary_results = self.match_primary(VisitedHandlingMode::AllLinksUnvisited); + + let inside_link = parent_style.map_or(false, |s| s.visited_style().is_some()); + + let visited_rules = if self.context.shared.visited_styles_enabled && + (inside_link || self.element.is_link()) + { + let visited_matching_results = + self.match_primary(VisitedHandlingMode::RelevantLinkVisited); + Some(visited_matching_results.rule_node) + } else { + None + }; + + self.cascade_primary_style( + CascadeInputs { + rules: Some(primary_results.rule_node), + visited_rules, + flags: primary_results.flags, + }, + parent_style, + layout_parent_style, + ) + } + + fn cascade_primary_style( + &mut self, + inputs: CascadeInputs, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + ) -> PrimaryStyle { + // Before doing the cascade, check the sharing cache and see if we can + // reuse the style via rule node identity. + let may_reuse = self.element.matches_user_and_content_rules() && + parent_style.is_some() && + inputs.rules.is_some() && + // If this style was considered in any way for relative selector matching, + // we do not want to lose that fact by sharing a style with something that + // did not. + !inputs.flags.contains(ComputedValueFlags::CONSIDERED_RELATIVE_SELECTOR); + + if may_reuse { + let cached = self.context.thread_local.sharing_cache.lookup_by_rules( + self.context.shared, + parent_style.unwrap(), + inputs.rules.as_ref().unwrap(), + inputs.visited_rules.as_ref(), + self.element, + ); + if let Some(mut primary_style) = cached { + self.context.thread_local.statistics.styles_reused += 1; + primary_style.reused_via_rule_node |= true; + return primary_style; + } + } + + // No style to reuse. Cascade the style, starting with visited style + // if necessary. + PrimaryStyle { + style: self.cascade_style_and_visited( + inputs, + parent_style, + layout_parent_style, + /* pseudo = */ None, + ), + reused_via_rule_node: false, + } + } + + /// Resolve the style of a given element, and all its eager pseudo-elements. + pub fn resolve_style( + &mut self, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + ) -> ResolvedElementStyles { + let primary_style = self.resolve_primary_style(parent_style, layout_parent_style); + + let mut pseudo_styles = EagerPseudoStyles::default(); + + if !self.element.is_pseudo_element() { + let layout_parent_style_for_pseudo = + layout_parent_style_for_pseudo(&primary_style, layout_parent_style); + SelectorImpl::each_eagerly_cascaded_pseudo_element(|pseudo| { + let pseudo_style = self.resolve_pseudo_style( + &pseudo, + &primary_style, + layout_parent_style_for_pseudo, + ); + + if let Some(style) = pseudo_style { + if !matches!(self.pseudo_resolution, PseudoElementResolution::Force) && + eager_pseudo_is_definitely_not_generated(&pseudo, &style.0) + { + return; + } + pseudo_styles.set(&pseudo, style.0); + } + }) + } + + ResolvedElementStyles { + primary: primary_style, + pseudos: pseudo_styles, + } + } + + /// Resolve an element's styles with the default inheritance parent/layout + /// parents. + pub fn resolve_style_with_default_parents(&mut self) -> ResolvedElementStyles { + with_default_parent_styles(self.element, |parent_style, layout_parent_style| { + self.resolve_style(parent_style, layout_parent_style) + }) + } + + /// Cascade a set of rules, using the default parent for inheritance. + pub fn cascade_style_and_visited_with_default_parents( + &mut self, + inputs: CascadeInputs, + ) -> ResolvedStyle { + with_default_parent_styles(self.element, |parent_style, layout_parent_style| { + self.cascade_style_and_visited( + inputs, + parent_style, + layout_parent_style, + /* pseudo = */ None, + ) + }) + } + + /// Cascade a set of rules for pseudo element, using the default parent for inheritance. + pub fn cascade_style_and_visited_for_pseudo_with_default_parents( + &mut self, + inputs: CascadeInputs, + pseudo: &PseudoElement, + primary_style: &PrimaryStyle, + ) -> ResolvedStyle { + with_default_parent_styles(self.element, |_, layout_parent_style| { + let layout_parent_style_for_pseudo = + layout_parent_style_for_pseudo(primary_style, layout_parent_style); + + self.cascade_style_and_visited( + inputs, + Some(primary_style.style()), + layout_parent_style_for_pseudo, + Some(pseudo), + ) + }) + } + + fn cascade_style_and_visited( + &mut self, + inputs: CascadeInputs, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + pseudo: Option<&PseudoElement>, + ) -> ResolvedStyle { + debug_assert!(pseudo.map_or(true, |p| p.is_eager())); + + let implemented_pseudo = self.element.implemented_pseudo_element(); + let pseudo = pseudo.or(implemented_pseudo.as_ref()); + + let mut conditions = Default::default(); + let values = self.context.shared.stylist.cascade_style_and_visited( + Some(self.element), + pseudo, + inputs, + &self.context.shared.guards, + parent_style, + layout_parent_style, + FirstLineReparenting::No, + Some(&self.context.thread_local.rule_cache), + &mut conditions, + ); + + self.context.thread_local.rule_cache.insert_if_possible( + &self.context.shared.guards, + &values, + pseudo, + &conditions, + ); + + ResolvedStyle(values) + } + + /// Cascade the element and pseudo-element styles with the default parents. + pub fn cascade_styles_with_default_parents( + &mut self, + inputs: ElementCascadeInputs, + ) -> ResolvedElementStyles { + with_default_parent_styles(self.element, move |parent_style, layout_parent_style| { + let primary_style = + self.cascade_primary_style(inputs.primary, parent_style, layout_parent_style); + + let mut pseudo_styles = EagerPseudoStyles::default(); + if let Some(mut pseudo_array) = inputs.pseudos.into_array() { + let layout_parent_style_for_pseudo = if primary_style.style().is_display_contents() + { + layout_parent_style + } else { + Some(primary_style.style()) + }; + + for (i, inputs) in pseudo_array.iter_mut().enumerate() { + if let Some(inputs) = inputs.take() { + let pseudo = PseudoElement::from_eager_index(i); + + let style = self.cascade_style_and_visited( + inputs, + Some(primary_style.style()), + layout_parent_style_for_pseudo, + Some(&pseudo), + ); + + if !matches!(self.pseudo_resolution, PseudoElementResolution::Force) && + eager_pseudo_is_definitely_not_generated(&pseudo, &style.0) + { + continue; + } + + pseudo_styles.set(&pseudo, style.0); + } + } + } + + ResolvedElementStyles { + primary: primary_style, + pseudos: pseudo_styles, + } + }) + } + + fn resolve_pseudo_style( + &mut self, + pseudo: &PseudoElement, + originating_element_style: &PrimaryStyle, + layout_parent_style: Option<&ComputedValues>, + ) -> Option<ResolvedStyle> { + let MatchingResults { + rule_node, + mut flags, + } = self.match_pseudo( + &originating_element_style.style.0, + pseudo, + VisitedHandlingMode::AllLinksUnvisited, + )?; + + let mut visited_rules = None; + if originating_element_style.style().visited_style().is_some() { + visited_rules = self + .match_pseudo( + &originating_element_style.style.0, + pseudo, + VisitedHandlingMode::RelevantLinkVisited, + ) + .map(|results| { + flags |= results.flags; + results.rule_node + }); + } + + Some(self.cascade_style_and_visited( + CascadeInputs { + rules: Some(rule_node), + visited_rules, + flags, + }, + Some(originating_element_style.style()), + layout_parent_style, + Some(pseudo), + )) + } + + fn match_primary(&mut self, visited_handling: VisitedHandlingMode) -> MatchingResults { + debug!( + "Match primary for {:?}, visited: {:?}", + self.element, visited_handling + ); + let mut applicable_declarations = ApplicableDeclarationList::new(); + + let bloom_filter = self.context.thread_local.bloom_filter.filter(); + let selector_caches = &mut self.context.thread_local.selector_caches; + let mut matching_context = MatchingContext::new_for_visited( + MatchingMode::Normal, + Some(bloom_filter), + selector_caches, + visited_handling, + self.context.shared.quirks_mode(), + NeedsSelectorFlags::Yes, + MatchingForInvalidation::No, + ); + + let stylist = &self.context.shared.stylist; + let implemented_pseudo = self.element.implemented_pseudo_element(); + // Compute the primary rule node. + stylist.push_applicable_declarations( + self.element, + implemented_pseudo.as_ref(), + self.element.style_attribute(), + self.element.smil_override(), + self.element.animation_declarations(self.context.shared), + self.rule_inclusion, + &mut applicable_declarations, + &mut matching_context, + ); + + // FIXME(emilio): This is a hack for animations, and should go away. + self.element.unset_dirty_style_attribute(); + + let rule_node = stylist + .rule_tree() + .compute_rule_node(&mut applicable_declarations, &self.context.shared.guards); + + if log_enabled!(Trace) { + trace!("Matched rules for {:?}:", self.element); + for rn in rule_node.self_and_ancestors() { + let source = rn.style_source(); + if source.is_some() { + trace!(" > {:?}", source); + } + } + } + + MatchingResults { + rule_node, + flags: matching_context.extra_data.cascade_input_flags, + } + } + + fn match_pseudo( + &mut self, + originating_element_style: &ComputedValues, + pseudo_element: &PseudoElement, + visited_handling: VisitedHandlingMode, + ) -> Option<MatchingResults> { + debug!( + "Match pseudo {:?} for {:?}, visited: {:?}", + self.element, pseudo_element, visited_handling + ); + debug_assert!(pseudo_element.is_eager()); + debug_assert!( + !self.element.is_pseudo_element(), + "Element pseudos can't have any other eager pseudo." + ); + + let mut applicable_declarations = ApplicableDeclarationList::new(); + + let stylist = &self.context.shared.stylist; + + if !self + .element + .may_generate_pseudo(pseudo_element, originating_element_style) + { + return None; + } + + let bloom_filter = self.context.thread_local.bloom_filter.filter(); + let selector_caches = &mut self.context.thread_local.selector_caches; + + let mut matching_context = MatchingContext::<'_, E::Impl>::new_for_visited( + MatchingMode::ForStatelessPseudoElement, + Some(bloom_filter), + selector_caches, + visited_handling, + self.context.shared.quirks_mode(), + NeedsSelectorFlags::Yes, + MatchingForInvalidation::No, + ); + matching_context.extra_data.originating_element_style = Some(originating_element_style); + + // NB: We handle animation rules for ::before and ::after when + // traversing them. + stylist.push_applicable_declarations( + self.element, + Some(pseudo_element), + None, + None, + /* animation_declarations = */ Default::default(), + self.rule_inclusion, + &mut applicable_declarations, + &mut matching_context, + ); + + if applicable_declarations.is_empty() { + return None; + } + + let rule_node = stylist + .rule_tree() + .compute_rule_node(&mut applicable_declarations, &self.context.shared.guards); + + Some(MatchingResults { + rule_node, + flags: matching_context.extra_data.cascade_input_flags, + }) + } +} diff --git a/servo/components/style/stylesheet_set.rs b/servo/components/style/stylesheet_set.rs new file mode 100644 index 0000000000..b93bbfe2fd --- /dev/null +++ b/servo/components/style/stylesheet_set.rs @@ -0,0 +1,705 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A centralized set of stylesheets for a document. + +use crate::dom::TElement; +use crate::invalidation::stylesheets::{RuleChangeKind, StylesheetInvalidationSet}; +use crate::media_queries::Device; +use crate::selector_parser::SnapshotMap; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{ + CssRule, Origin, OriginSet, OriginSetIterator, PerOrigin, StylesheetInDocument, +}; +use std::{mem, slice}; + +/// Entry for a StylesheetSet. +#[derive(MallocSizeOf)] +struct StylesheetSetEntry<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The sheet. + sheet: S, + + /// Whether this sheet has been part of at least one flush. + committed: bool, +} + +impl<S> StylesheetSetEntry<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + fn new(sheet: S) -> Self { + Self { + sheet, + committed: false, + } + } +} + +/// A iterator over the stylesheets of a list of entries in the StylesheetSet. +pub struct StylesheetCollectionIterator<'a, S>(slice::Iter<'a, StylesheetSetEntry<S>>) +where + S: StylesheetInDocument + PartialEq + 'static; + +impl<'a, S> Clone for StylesheetCollectionIterator<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + fn clone(&self) -> Self { + StylesheetCollectionIterator(self.0.clone()) + } +} + +impl<'a, S> Iterator for StylesheetCollectionIterator<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + type Item = &'a S; + + fn next(&mut self) -> Option<Self::Item> { + self.0.next().map(|entry| &entry.sheet) + } + + fn size_hint(&self) -> (usize, Option<usize>) { + self.0.size_hint() + } +} + +/// An iterator over the flattened view of the stylesheet collections. +#[derive(Clone)] +pub struct StylesheetIterator<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + origins: OriginSetIterator, + collections: &'a PerOrigin<SheetCollection<S>>, + current: Option<(Origin, StylesheetCollectionIterator<'a, S>)>, +} + +impl<'a, S> Iterator for StylesheetIterator<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + type Item = (&'a S, Origin); + + fn next(&mut self) -> Option<Self::Item> { + loop { + if self.current.is_none() { + let next_origin = self.origins.next()?; + + self.current = Some(( + next_origin, + self.collections.borrow_for_origin(&next_origin).iter(), + )); + } + + { + let (origin, ref mut iter) = *self.current.as_mut().unwrap(); + if let Some(s) = iter.next() { + return Some((s, origin)); + } + } + + self.current = None; + } + } +} + +/// The validity of the data in a given cascade origin. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, Ord, PartialEq, PartialOrd)] +pub enum DataValidity { + /// The origin is clean, all the data already there is valid, though we may + /// have new sheets at the end. + Valid = 0, + + /// The cascade data is invalid, but not the invalidation data (which is + /// order-independent), and thus only the cascade data should be inserted. + CascadeInvalid = 1, + + /// Everything needs to be rebuilt. + FullyInvalid = 2, +} + +impl Default for DataValidity { + fn default() -> Self { + DataValidity::Valid + } +} + +/// A struct to iterate over the different stylesheets to be flushed. +pub struct DocumentStylesheetFlusher<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + collections: &'a mut PerOrigin<SheetCollection<S>>, + had_invalidations: bool, +} + +/// The type of rebuild that we need to do for a given stylesheet. +#[derive(Clone, Copy, Debug)] +pub enum SheetRebuildKind { + /// A full rebuild, of both cascade data and invalidation data. + Full, + /// A partial rebuild, of only the cascade data. + CascadeOnly, +} + +impl SheetRebuildKind { + /// Whether the stylesheet invalidation data should be rebuilt. + pub fn should_rebuild_invalidation(&self) -> bool { + matches!(*self, SheetRebuildKind::Full) + } +} + +impl<'a, S> DocumentStylesheetFlusher<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Returns a flusher for `origin`. + pub fn flush_origin(&mut self, origin: Origin) -> SheetCollectionFlusher<S> { + self.collections.borrow_mut_for_origin(&origin).flush() + } + + /// Returns the list of stylesheets for `origin`. + /// + /// Only used for UA sheets. + pub fn origin_sheets(&mut self, origin: Origin) -> StylesheetCollectionIterator<S> { + self.collections.borrow_mut_for_origin(&origin).iter() + } + + /// Returns whether any DOM invalidations were processed as a result of the + /// stylesheet flush. + #[inline] + pub fn had_invalidations(&self) -> bool { + self.had_invalidations + } +} + +/// A flusher struct for a given collection, that takes care of returning the +/// appropriate stylesheets that need work. +pub struct SheetCollectionFlusher<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + // TODO: This can be made an iterator again once + // https://github.com/rust-lang/rust/pull/82771 lands on stable. + entries: &'a mut [StylesheetSetEntry<S>], + validity: DataValidity, + dirty: bool, +} + +impl<'a, S> SheetCollectionFlusher<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Whether the collection was originally dirty. + #[inline] + pub fn dirty(&self) -> bool { + self.dirty + } + + /// What the state of the sheet data is. + #[inline] + pub fn data_validity(&self) -> DataValidity { + self.validity + } + + /// Returns an iterator over the remaining list of sheets to consume. + pub fn sheets<'b>(&'b self) -> impl Iterator<Item = &'b S> { + self.entries.iter().map(|entry| &entry.sheet) + } +} + +impl<'a, S> SheetCollectionFlusher<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Iterates over all sheets and values that we have to invalidate. + /// + /// TODO(emilio): This would be nicer as an iterator but we can't do that + /// until https://github.com/rust-lang/rust/pull/82771 stabilizes. + /// + /// Since we don't have a good use-case for partial iteration, this does the + /// trick for now. + pub fn each(self, mut callback: impl FnMut(&S, SheetRebuildKind) -> bool) { + for potential_sheet in self.entries.iter_mut() { + let committed = mem::replace(&mut potential_sheet.committed, true); + let rebuild_kind = if !committed { + // If the sheet was uncommitted, we need to do a full rebuild + // anyway. + SheetRebuildKind::Full + } else { + match self.validity { + DataValidity::Valid => continue, + DataValidity::CascadeInvalid => SheetRebuildKind::CascadeOnly, + DataValidity::FullyInvalid => SheetRebuildKind::Full, + } + }; + + if !callback(&potential_sheet.sheet, rebuild_kind) { + return; + } + } + } +} + +#[derive(MallocSizeOf)] +struct SheetCollection<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The actual list of stylesheets. + /// + /// This is only a list of top-level stylesheets, and as such it doesn't + /// include recursive `@import` rules. + entries: Vec<StylesheetSetEntry<S>>, + + /// The validity of the data that was already there for a given origin. + /// + /// Note that an origin may appear on `origins_dirty`, but still have + /// `DataValidity::Valid`, if only sheets have been appended into it (in + /// which case the existing data is valid, but the origin needs to be + /// rebuilt). + data_validity: DataValidity, + + /// Whether anything in the collection has changed. Note that this is + /// different from `data_validity`, in the sense that after a sheet append, + /// the data validity is still `Valid`, but we need to be marked as dirty. + dirty: bool, +} + +impl<S> Default for SheetCollection<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + fn default() -> Self { + Self { + entries: vec![], + data_validity: DataValidity::Valid, + dirty: false, + } + } +} + +impl<S> SheetCollection<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Returns the number of stylesheets in the set. + fn len(&self) -> usize { + self.entries.len() + } + + /// Returns the `index`th stylesheet in the set if present. + fn get(&self, index: usize) -> Option<&S> { + self.entries.get(index).map(|e| &e.sheet) + } + + fn remove(&mut self, sheet: &S) { + let index = self.entries.iter().position(|entry| entry.sheet == *sheet); + if cfg!(feature = "gecko") && index.is_none() { + // FIXME(emilio): Make Gecko's PresShell::AddUserSheet not suck. + return; + } + let sheet = self.entries.remove(index.unwrap()); + // Removing sheets makes us tear down the whole cascade and invalidation + // data, but only if the sheet has been involved in at least one flush. + // Checking whether the sheet has been committed allows us to avoid + // rebuilding the world when sites quickly append and remove a + // stylesheet. + // + // See bug 1434756. + if sheet.committed { + self.set_data_validity_at_least(DataValidity::FullyInvalid); + } else { + self.dirty = true; + } + } + + fn contains(&self, sheet: &S) -> bool { + self.entries.iter().any(|e| e.sheet == *sheet) + } + + /// Appends a given sheet into the collection. + fn append(&mut self, sheet: S) { + debug_assert!(!self.contains(&sheet)); + self.entries.push(StylesheetSetEntry::new(sheet)); + // Appending sheets doesn't alter the validity of the existing data, so + // we don't need to change `data_validity` here. + // + // But we need to be marked as dirty, otherwise we'll never add the new + // sheet! + self.dirty = true; + } + + fn insert_before(&mut self, sheet: S, before_sheet: &S) { + debug_assert!(!self.contains(&sheet)); + + let index = self + .entries + .iter() + .position(|entry| entry.sheet == *before_sheet) + .expect("`before_sheet` stylesheet not found"); + + // Inserting stylesheets somewhere but at the end changes the validity + // of the cascade data, but not the invalidation data. + self.set_data_validity_at_least(DataValidity::CascadeInvalid); + self.entries.insert(index, StylesheetSetEntry::new(sheet)); + } + + fn set_data_validity_at_least(&mut self, validity: DataValidity) { + use std::cmp; + + debug_assert_ne!(validity, DataValidity::Valid); + + self.dirty = true; + self.data_validity = cmp::max(validity, self.data_validity); + } + + /// Returns an iterator over the current list of stylesheets. + fn iter(&self) -> StylesheetCollectionIterator<S> { + StylesheetCollectionIterator(self.entries.iter()) + } + + fn flush(&mut self) -> SheetCollectionFlusher<S> { + let dirty = mem::replace(&mut self.dirty, false); + let validity = mem::replace(&mut self.data_validity, DataValidity::Valid); + + SheetCollectionFlusher { + entries: &mut self.entries, + dirty, + validity, + } + } +} + +/// The set of stylesheets effective for a given document. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct DocumentStylesheetSet<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The collections of sheets per each origin. + collections: PerOrigin<SheetCollection<S>>, + + /// The invalidations for stylesheets added or removed from this document. + invalidations: StylesheetInvalidationSet, +} + +/// This macro defines methods common to DocumentStylesheetSet and +/// AuthorStylesheetSet. +/// +/// We could simplify the setup moving invalidations to SheetCollection, but +/// that would imply not sharing invalidations across origins of the same +/// documents, which is slightly annoying. +macro_rules! sheet_set_methods { + ($set_name:expr) => { + fn collect_invalidations_for( + &mut self, + device: Option<&Device>, + sheet: &S, + guard: &SharedRwLockReadGuard, + ) { + if let Some(device) = device { + self.invalidations + .collect_invalidations_for(device, sheet, guard); + } + } + + /// Appends a new stylesheet to the current set. + /// + /// No device implies not computing invalidations. + pub fn append_stylesheet( + &mut self, + device: Option<&Device>, + sheet: S, + guard: &SharedRwLockReadGuard, + ) { + debug!(concat!($set_name, "::append_stylesheet")); + self.collect_invalidations_for(device, &sheet, guard); + let collection = self.collection_for(&sheet); + collection.append(sheet); + } + + /// Insert a given stylesheet before another stylesheet in the document. + pub fn insert_stylesheet_before( + &mut self, + device: Option<&Device>, + sheet: S, + before_sheet: S, + guard: &SharedRwLockReadGuard, + ) { + debug!(concat!($set_name, "::insert_stylesheet_before")); + self.collect_invalidations_for(device, &sheet, guard); + + let collection = self.collection_for(&sheet); + collection.insert_before(sheet, &before_sheet); + } + + /// Remove a given stylesheet from the set. + pub fn remove_stylesheet( + &mut self, + device: Option<&Device>, + sheet: S, + guard: &SharedRwLockReadGuard, + ) { + debug!(concat!($set_name, "::remove_stylesheet")); + self.collect_invalidations_for(device, &sheet, guard); + + let collection = self.collection_for(&sheet); + collection.remove(&sheet) + } + + /// Notify the set that a rule from a given stylesheet has changed + /// somehow. + pub fn rule_changed( + &mut self, + device: Option<&Device>, + sheet: &S, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + change_kind: RuleChangeKind, + ) { + if let Some(device) = device { + let quirks_mode = device.quirks_mode(); + self.invalidations.rule_changed( + sheet, + rule, + guard, + device, + quirks_mode, + change_kind, + ); + } + + let validity = match change_kind { + // Insertion / Removals need to rebuild both the cascade and + // invalidation data. For generic changes this is conservative, + // could be optimized on a per-case basis. + RuleChangeKind::Generic | RuleChangeKind::Insertion | RuleChangeKind::Removal => { + DataValidity::FullyInvalid + }, + // TODO(emilio): This, in theory, doesn't need to invalidate + // style data, if the rule we're modifying is actually in the + // CascadeData already. + // + // But this is actually a bit tricky to prove, because when we + // copy-on-write a stylesheet we don't bother doing a rebuild, + // so we may still have rules from the original stylesheet + // instead of the cloned one that we're modifying. So don't + // bother for now and unconditionally rebuild, it's no worse + // than what we were already doing anyway. + // + // Maybe we could record whether we saw a clone in this flush, + // and if so do the conservative thing, otherwise just + // early-return. + RuleChangeKind::StyleRuleDeclarations => DataValidity::FullyInvalid, + }; + + let collection = self.collection_for(&sheet); + collection.set_data_validity_at_least(validity); + } + }; +} + +impl<S> DocumentStylesheetSet<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Create a new empty DocumentStylesheetSet. + pub fn new() -> Self { + Self { + collections: Default::default(), + invalidations: StylesheetInvalidationSet::new(), + } + } + + fn collection_for(&mut self, sheet: &S) -> &mut SheetCollection<S> { + let origin = sheet.contents().origin; + self.collections.borrow_mut_for_origin(&origin) + } + + sheet_set_methods!("DocumentStylesheetSet"); + + /// Returns the number of stylesheets in the set. + pub fn len(&self) -> usize { + self.collections + .iter_origins() + .fold(0, |s, (item, _)| s + item.len()) + } + + /// Returns the count of stylesheets for a given origin. + #[inline] + pub fn sheet_count(&self, origin: Origin) -> usize { + self.collections.borrow_for_origin(&origin).len() + } + + /// Returns the `index`th stylesheet in the set for the given origin. + #[inline] + pub fn get(&self, origin: Origin, index: usize) -> Option<&S> { + self.collections.borrow_for_origin(&origin).get(index) + } + + /// Returns whether the given set has changed from the last flush. + pub fn has_changed(&self) -> bool { + !self.invalidations.is_empty() || + self.collections + .iter_origins() + .any(|(collection, _)| collection.dirty) + } + + /// Flush the current set, unmarking it as dirty, and returns a + /// `DocumentStylesheetFlusher` in order to rebuild the stylist. + pub fn flush<E>( + &mut self, + document_element: Option<E>, + snapshots: Option<&SnapshotMap>, + ) -> DocumentStylesheetFlusher<S> + where + E: TElement, + { + debug!("DocumentStylesheetSet::flush"); + + let had_invalidations = self.invalidations.flush(document_element, snapshots); + + DocumentStylesheetFlusher { + collections: &mut self.collections, + had_invalidations, + } + } + + /// Flush stylesheets, but without running any of the invalidation passes. + #[cfg(feature = "servo")] + pub fn flush_without_invalidation(&mut self) -> OriginSet { + debug!("DocumentStylesheetSet::flush_without_invalidation"); + + let mut origins = OriginSet::empty(); + self.invalidations.clear(); + + for (collection, origin) in self.collections.iter_mut_origins() { + if collection.flush().dirty() { + origins |= origin; + } + } + + origins + } + + /// Return an iterator over the flattened view of all the stylesheets. + pub fn iter(&self) -> StylesheetIterator<S> { + StylesheetIterator { + origins: OriginSet::all().iter_origins(), + collections: &self.collections, + current: None, + } + } + + /// Mark the stylesheets for the specified origin as dirty, because + /// something external may have invalidated it. + pub fn force_dirty(&mut self, origins: OriginSet) { + self.invalidations.invalidate_fully(); + for origin in origins.iter_origins() { + // We don't know what happened, assume the worse. + self.collections + .borrow_mut_for_origin(&origin) + .set_data_validity_at_least(DataValidity::FullyInvalid); + } + } +} + +/// The set of stylesheets effective for a given Shadow Root. +#[derive(MallocSizeOf)] +pub struct AuthorStylesheetSet<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The actual style sheets. + collection: SheetCollection<S>, + /// The set of invalidations scheduled for this collection. + invalidations: StylesheetInvalidationSet, +} + +/// A struct to flush an author style sheet collection. +pub struct AuthorStylesheetFlusher<'a, S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// The actual flusher for the collection. + pub sheets: SheetCollectionFlusher<'a, S>, + /// Whether any sheet invalidation matched. + pub had_invalidations: bool, +} + +impl<S> AuthorStylesheetSet<S> +where + S: StylesheetInDocument + PartialEq + 'static, +{ + /// Create a new empty AuthorStylesheetSet. + #[inline] + pub fn new() -> Self { + Self { + collection: Default::default(), + invalidations: StylesheetInvalidationSet::new(), + } + } + + /// Whether anything has changed since the last time this was flushed. + pub fn dirty(&self) -> bool { + self.collection.dirty + } + + /// Whether the collection is empty. + pub fn is_empty(&self) -> bool { + self.collection.len() == 0 + } + + /// Returns the `index`th stylesheet in the collection of author styles if present. + pub fn get(&self, index: usize) -> Option<&S> { + self.collection.get(index) + } + + /// Returns the number of author stylesheets. + pub fn len(&self) -> usize { + self.collection.len() + } + + fn collection_for(&mut self, _sheet: &S) -> &mut SheetCollection<S> { + &mut self.collection + } + + sheet_set_methods!("AuthorStylesheetSet"); + + /// Iterate over the list of stylesheets. + pub fn iter(&self) -> StylesheetCollectionIterator<S> { + self.collection.iter() + } + + /// Mark the sheet set dirty, as appropriate. + pub fn force_dirty(&mut self) { + self.invalidations.invalidate_fully(); + self.collection + .set_data_validity_at_least(DataValidity::FullyInvalid); + } + + /// Flush the stylesheets for this author set. + /// + /// `host` is the root of the affected subtree, like the shadow host, for + /// example. + pub fn flush<E>( + &mut self, + host: Option<E>, + snapshots: Option<&SnapshotMap>, + ) -> AuthorStylesheetFlusher<S> + where + E: TElement, + { + let had_invalidations = self.invalidations.flush(host, snapshots); + AuthorStylesheetFlusher { + sheets: self.collection.flush(), + had_invalidations, + } + } +} diff --git a/servo/components/style/stylesheets/container_rule.rs b/servo/components/style/stylesheets/container_rule.rs new file mode 100644 index 0000000000..28c387fde2 --- /dev/null +++ b/servo/components/style/stylesheets/container_rule.rs @@ -0,0 +1,642 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A [`@container`][container] rule. +//! +//! [container]: https://drafts.csswg.org/css-contain-3/#container-rule + +use crate::computed_value_flags::ComputedValueFlags; +use crate::dom::TElement; +use crate::logical_geometry::{LogicalSize, WritingMode}; +use crate::parser::ParserContext; +use crate::properties::ComputedValues; +use crate::queries::condition::KleeneValue; +use crate::queries::feature::{AllowsRanges, Evaluator, FeatureFlags, QueryFeatureDescription}; +use crate::queries::values::Orientation; +use crate::queries::{FeatureType, QueryCondition}; +use crate::shared_lock::{ + DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, +}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use crate::stylist::Stylist; +use crate::values::computed::{CSSPixelLength, ContainerType, Context, Ratio}; +use crate::values::specified::ContainerName; +use app_units::Au; +use cssparser::{Parser, SourceLocation}; +use euclid::default::Size2D; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A container rule. +#[derive(Debug, ToShmem)] +pub struct ContainerRule { + /// The container query and name. + pub condition: Arc<ContainerCondition>, + /// The nested rules inside the block. + pub rules: Arc<Locked<CssRules>>, + /// The source position where this rule was found. + pub source_location: SourceLocation, +} + +impl ContainerRule { + /// Returns the query condition. + pub fn query_condition(&self) -> &QueryCondition { + &self.condition.condition + } + + /// Returns the query name filter. + pub fn container_name(&self) -> &ContainerName { + &self.condition.name + } + + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + } +} + +impl DeepCloneWithLock for ContainerRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(guard); + Self { + condition: self.condition.clone(), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + source_location: self.source_location.clone(), + } + } +} + +impl ToCssWithGuard for ContainerRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@container ")?; + { + let mut writer = CssWriter::new(dest); + if !self.condition.name.is_none() { + self.condition.name.to_css(&mut writer)?; + writer.write_char(' ')?; + } + self.condition.condition.to_css(&mut writer)?; + } + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +/// A container condition and filter, combined. +#[derive(Debug, ToShmem, ToCss)] +pub struct ContainerCondition { + #[css(skip_if = "ContainerName::is_none")] + name: ContainerName, + condition: QueryCondition, + #[css(skip)] + flags: FeatureFlags, +} + +/// The result of a successful container query lookup. +pub struct ContainerLookupResult<E> { + /// The relevant container. + pub element: E, + /// The sizing / writing-mode information of the container. + pub info: ContainerInfo, + /// The style of the element. + pub style: Arc<ComputedValues>, +} + +fn container_type_axes(ty_: ContainerType, wm: WritingMode) -> FeatureFlags { + match ty_ { + ContainerType::Size => FeatureFlags::all_container_axes(), + ContainerType::InlineSize => { + let physical_axis = if wm.is_vertical() { + FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS + } else { + FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS + }; + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS | physical_axis + }, + ContainerType::Normal => FeatureFlags::empty(), + } +} + +enum TraversalResult<T> { + InProgress, + StopTraversal, + Done(T), +} + +fn traverse_container<E, F, R>( + mut e: E, + originating_element_style: Option<&ComputedValues>, + evaluator: F, +) -> Option<(E, R)> +where + E: TElement, + F: Fn(E, Option<&ComputedValues>) -> TraversalResult<R>, +{ + if originating_element_style.is_some() { + match evaluator(e, originating_element_style) { + TraversalResult::InProgress => {}, + TraversalResult::StopTraversal => return None, + TraversalResult::Done(result) => return Some((e, result)), + } + } + while let Some(element) = e.traversal_parent() { + match evaluator(element, None) { + TraversalResult::InProgress => {}, + TraversalResult::StopTraversal => return None, + TraversalResult::Done(result) => return Some((element, result)), + } + e = element; + } + + None +} + +impl ContainerCondition { + /// Parse a container condition. + pub fn parse<'a>( + context: &ParserContext, + input: &mut Parser<'a, '_>, + ) -> Result<Self, ParseError<'a>> { + let name = input + .try_parse(|input| ContainerName::parse_for_query(context, input)) + .ok() + .unwrap_or_else(ContainerName::none); + let condition = QueryCondition::parse(context, input, FeatureType::Container)?; + let flags = condition.cumulative_flags(); + Ok(Self { + name, + condition, + flags, + }) + } + + fn valid_container_info<E>( + &self, + potential_container: E, + originating_element_style: Option<&ComputedValues>, + ) -> TraversalResult<ContainerLookupResult<E>> + where + E: TElement, + { + let data; + let style = match originating_element_style { + Some(s) => s, + None => { + data = match potential_container.borrow_data() { + Some(d) => d, + None => return TraversalResult::InProgress, + }; + &**data.styles.primary() + }, + }; + let wm = style.writing_mode; + let box_style = style.get_box(); + + // Filter by container-type. + let container_type = box_style.clone_container_type(); + let available_axes = container_type_axes(container_type, wm); + if !available_axes.contains(self.flags.container_axes()) { + return TraversalResult::InProgress; + } + + // Filter by container-name. + let container_name = box_style.clone_container_name(); + for filter_name in self.name.0.iter() { + if !container_name.0.contains(filter_name) { + return TraversalResult::InProgress; + } + } + + let size = potential_container.query_container_size(&box_style.clone_display()); + let style = style.to_arc(); + TraversalResult::Done(ContainerLookupResult { + element: potential_container, + info: ContainerInfo { size, wm }, + style, + }) + } + + /// Performs container lookup for a given element. + pub fn find_container<E>( + &self, + e: E, + originating_element_style: Option<&ComputedValues>, + ) -> Option<ContainerLookupResult<E>> + where + E: TElement, + { + match traverse_container( + e, + originating_element_style, + |element, originating_element_style| { + self.valid_container_info(element, originating_element_style) + }, + ) { + Some((_, result)) => Some(result), + None => None, + } + } + + /// Tries to match a container query condition for a given element. + pub(crate) fn matches<E>( + &self, + stylist: &Stylist, + element: E, + originating_element_style: Option<&ComputedValues>, + invalidation_flags: &mut ComputedValueFlags, + ) -> KleeneValue + where + E: TElement, + { + let result = self.find_container(element, originating_element_style); + let (container, info) = match result { + Some(r) => (Some(r.element), Some((r.info, r.style))), + None => (None, None), + }; + // Set up the lookup for the container in question, as the condition may be using container + // query lengths. + let size_query_container_lookup = ContainerSizeQuery::for_option_element( + container, /* known_parent_style = */ None, /* is_pseudo = */ false, + ); + Context::for_container_query_evaluation( + stylist.device(), + Some(stylist), + info, + size_query_container_lookup, + |context| { + let matches = self.condition.matches(context); + if context + .style() + .flags() + .contains(ComputedValueFlags::USES_VIEWPORT_UNITS) + { + // TODO(emilio): Might need something similar to improve + // invalidation of font relative container-query lengths. + invalidation_flags + .insert(ComputedValueFlags::USES_VIEWPORT_UNITS_ON_CONTAINER_QUERIES); + } + matches + }, + ) + } +} + +/// Information needed to evaluate an individual container query. +#[derive(Copy, Clone)] +pub struct ContainerInfo { + size: Size2D<Option<Au>>, + wm: WritingMode, +} + +impl ContainerInfo { + fn size(&self) -> Option<Size2D<Au>> { + Some(Size2D::new(self.size.width?, self.size.height?)) + } +} + +fn eval_width(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new(info.size.width?.to_f32_px())) +} + +fn eval_height(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new(info.size.height?.to_f32_px())) +} + +fn eval_inline_size(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new( + LogicalSize::from_physical(info.wm, info.size) + .inline? + .to_f32_px(), + )) +} + +fn eval_block_size(context: &Context) -> Option<CSSPixelLength> { + let info = context.container_info.as_ref()?; + Some(CSSPixelLength::new( + LogicalSize::from_physical(info.wm, info.size) + .block? + .to_f32_px(), + )) +} + +fn eval_aspect_ratio(context: &Context) -> Option<Ratio> { + let info = context.container_info.as_ref()?; + Some(Ratio::new( + info.size.width?.0 as f32, + info.size.height?.0 as f32, + )) +} + +fn eval_orientation(context: &Context, value: Option<Orientation>) -> KleeneValue { + let size = match context.container_info.as_ref().and_then(|info| info.size()) { + Some(size) => size, + None => return KleeneValue::Unknown, + }; + KleeneValue::from(Orientation::eval(size, value)) +} + +/// https://drafts.csswg.org/css-contain-3/#container-features +/// +/// TODO: Support style queries, perhaps. +pub static CONTAINER_FEATURES: [QueryFeatureDescription; 6] = [ + feature!( + atom!("width"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_width), + FeatureFlags::CONTAINER_REQUIRES_WIDTH_AXIS, + ), + feature!( + atom!("height"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_height), + FeatureFlags::CONTAINER_REQUIRES_HEIGHT_AXIS, + ), + feature!( + atom!("inline-size"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_inline_size), + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS, + ), + feature!( + atom!("block-size"), + AllowsRanges::Yes, + Evaluator::OptionalLength(eval_block_size), + FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS, + ), + feature!( + atom!("aspect-ratio"), + AllowsRanges::Yes, + Evaluator::OptionalNumberRatio(eval_aspect_ratio), + // XXX from_bits_truncate is const, but the pipe operator isn't, so this + // works around it. + FeatureFlags::from_bits_truncate( + FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() | + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits() + ), + ), + feature!( + atom!("orientation"), + AllowsRanges::No, + keyword_evaluator!(eval_orientation, Orientation), + FeatureFlags::from_bits_truncate( + FeatureFlags::CONTAINER_REQUIRES_BLOCK_AXIS.bits() | + FeatureFlags::CONTAINER_REQUIRES_INLINE_AXIS.bits() + ), + ), +]; + +/// Result of a container size query, signifying the hypothetical containment boundary in terms of physical axes. +/// Defined by up to two size containers. Queries on logical axes are resolved with respect to the querying +/// element's writing mode. +#[derive(Copy, Clone, Default)] +pub struct ContainerSizeQueryResult { + width: Option<Au>, + height: Option<Au>, +} + +impl ContainerSizeQueryResult { + fn get_viewport_size(context: &Context) -> Size2D<Au> { + use crate::values::specified::ViewportVariant; + context.viewport_size_for_viewport_unit_resolution(ViewportVariant::Small) + } + + fn get_logical_viewport_size(context: &Context) -> LogicalSize<Au> { + LogicalSize::from_physical( + context.builder.writing_mode, + Self::get_viewport_size(context), + ) + } + + /// Get the inline-size of the query container. + pub fn get_container_inline_size(&self, context: &Context) -> Au { + if context.builder.writing_mode.is_horizontal() { + if let Some(w) = self.width { + return w; + } + } else { + if let Some(h) = self.height { + return h; + } + } + Self::get_logical_viewport_size(context).inline + } + + /// Get the block-size of the query container. + pub fn get_container_block_size(&self, context: &Context) -> Au { + if context.builder.writing_mode.is_horizontal() { + self.get_container_height(context) + } else { + self.get_container_width(context) + } + } + + /// Get the width of the query container. + pub fn get_container_width(&self, context: &Context) -> Au { + if let Some(w) = self.width { + return w; + } + Self::get_viewport_size(context).width + } + + /// Get the height of the query container. + pub fn get_container_height(&self, context: &Context) -> Au { + if let Some(h) = self.height { + return h; + } + Self::get_viewport_size(context).height + } + + // Merge the result of a subsequent lookup, preferring the initial result. + fn merge(self, new_result: Self) -> Self { + let mut result = self; + if let Some(width) = new_result.width { + result.width.get_or_insert(width); + } + if let Some(height) = new_result.height { + result.height.get_or_insert(height); + } + result + } + + fn is_complete(&self) -> bool { + self.width.is_some() && self.height.is_some() + } +} + +/// Unevaluated lazy container size query. +pub enum ContainerSizeQuery<'a> { + /// Query prior to evaluation. + NotEvaluated(Box<dyn Fn() -> ContainerSizeQueryResult + 'a>), + /// Cached evaluated result. + Evaluated(ContainerSizeQueryResult), +} + +impl<'a> ContainerSizeQuery<'a> { + fn evaluate_potential_size_container<E>( + e: E, + originating_element_style: Option<&ComputedValues>, + ) -> TraversalResult<ContainerSizeQueryResult> + where + E: TElement, + { + let data; + let style = match originating_element_style { + Some(s) => s, + None => { + data = match e.borrow_data() { + Some(d) => d, + None => return TraversalResult::InProgress, + }; + &**data.styles.primary() + }, + }; + if !style + .flags + .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE) + { + // We know we won't find a size container. + return TraversalResult::StopTraversal; + } + + let wm = style.writing_mode; + let box_style = style.get_box(); + + let container_type = box_style.clone_container_type(); + let size = e.query_container_size(&box_style.clone_display()); + match container_type { + ContainerType::Size => TraversalResult::Done(ContainerSizeQueryResult { + width: size.width, + height: size.height, + }), + ContainerType::InlineSize => { + if wm.is_horizontal() { + TraversalResult::Done(ContainerSizeQueryResult { + width: size.width, + height: None, + }) + } else { + TraversalResult::Done(ContainerSizeQueryResult { + width: None, + height: size.height, + }) + } + }, + ContainerType::Normal => TraversalResult::InProgress, + } + } + + /// Find the query container size for a given element. Meant to be used as a callback for new(). + fn lookup<E>( + element: E, + originating_element_style: Option<&ComputedValues>, + ) -> ContainerSizeQueryResult + where + E: TElement + 'a, + { + match traverse_container( + element, + originating_element_style, + |e, originating_element_style| { + Self::evaluate_potential_size_container(e, originating_element_style) + }, + ) { + Some((container, result)) => { + if result.is_complete() { + result + } else { + // Traverse up from the found size container to see if we can get a complete containment. + result.merge(Self::lookup(container, None)) + } + }, + None => ContainerSizeQueryResult::default(), + } + } + + /// Create a new instance of the container size query for given element, with a deferred lookup callback. + pub fn for_element<E>( + element: E, + known_parent_style: Option<&'a ComputedValues>, + is_pseudo: bool, + ) -> Self + where + E: TElement + 'a, + { + let parent; + let data; + let parent_style = match known_parent_style { + Some(s) => Some(s), + None => { + // No need to bother if we're the top element. + parent = match element.traversal_parent() { + Some(parent) => parent, + None => return Self::none(), + }; + data = parent.borrow_data(); + data.as_ref().map(|data| &**data.styles.primary()) + }, + }; + + // If there's no style, such as being `display: none` or so, we still want to show a + // correct computed value, so give it a try. + let should_traverse = parent_style.map_or(true, |s| { + s.flags + .contains(ComputedValueFlags::SELF_OR_ANCESTOR_HAS_SIZE_CONTAINER_TYPE) + }); + if !should_traverse { + return Self::none(); + } + return Self::NotEvaluated(Box::new(move || { + Self::lookup(element, if is_pseudo { known_parent_style } else { None }) + })); + } + + /// Create a new instance, but with optional element. + pub fn for_option_element<E>( + element: Option<E>, + known_parent_style: Option<&'a ComputedValues>, + is_pseudo: bool, + ) -> Self + where + E: TElement + 'a, + { + if let Some(e) = element { + Self::for_element(e, known_parent_style, is_pseudo) + } else { + Self::none() + } + } + + /// Create a query that evaluates to empty, for cases where container size query is not required. + pub fn none() -> Self { + ContainerSizeQuery::Evaluated(ContainerSizeQueryResult::default()) + } + + /// Get the result of the container size query, doing the lookup if called for the first time. + pub fn get(&mut self) -> ContainerSizeQueryResult { + match self { + Self::NotEvaluated(lookup) => { + *self = Self::Evaluated((lookup)()); + match self { + Self::Evaluated(info) => *info, + _ => unreachable!("Just evaluated but not set?"), + } + }, + Self::Evaluated(info) => *info, + } + } +} diff --git a/servo/components/style/stylesheets/counter_style_rule.rs b/servo/components/style/stylesheets/counter_style_rule.rs new file mode 100644 index 0000000000..974b76b806 --- /dev/null +++ b/servo/components/style/stylesheets/counter_style_rule.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(missing_docs)] + +pub use crate::counter_style::CounterStyleRuleData as CounterStyleRule; diff --git a/servo/components/style/stylesheets/document_rule.rs b/servo/components/style/stylesheets/document_rule.rs new file mode 100644 index 0000000000..d043691e4c --- /dev/null +++ b/servo/components/style/stylesheets/document_rule.rs @@ -0,0 +1,299 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [@document rules](https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document) +//! initially in CSS Conditional Rules Module Level 3, @document has been postponed to the level 4. +//! We implement the prefixed `@-moz-document`. + +use crate::media_queries::Device; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use crate::values::CssUrl; +use cssparser::{BasicParseErrorKind, Parser, SourceLocation}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +#[derive(Debug, ToShmem)] +/// A @-moz-document rule +pub struct DocumentRule { + /// The parsed condition + pub condition: DocumentCondition, + /// Child rules + pub rules: Arc<Locked<CssRules>>, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl DocumentRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + } +} + +impl ToCssWithGuard for DocumentRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@-moz-document ")?; + self.condition.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" {")?; + for rule in self.rules.read_with(guard).0.iter() { + dest.write_char(' ')?; + rule.to_css(guard, dest)?; + } + dest.write_str(" }") + } +} + +impl DeepCloneWithLock for DocumentRule { + /// Deep clones this DocumentRule. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(guard); + DocumentRule { + condition: self.condition.clone(), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + source_location: self.source_location.clone(), + } + } +} + +/// The kind of media document that the rule will match. +#[derive(Clone, Copy, Debug, Parse, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum MediaDocumentKind { + All, + Plugin, + Image, + Video, +} + +/// A matching function for a `@document` rule's condition. +#[derive(Clone, Debug, ToCss, ToShmem)] +pub enum DocumentMatchingFunction { + /// Exact URL matching function. It evaluates to true whenever the + /// URL of the document being styled is exactly the URL given. + Url(CssUrl), + /// URL prefix matching function. It evaluates to true whenever the + /// URL of the document being styled has the argument to the + /// function as an initial substring (which is true when the two + /// strings are equal). When the argument is the empty string, + /// it evaluates to true for all documents. + #[css(function)] + UrlPrefix(String), + /// Domain matching function. It evaluates to true whenever the URL + /// of the document being styled has a host subcomponent and that + /// host subcomponent is exactly the argument to the ‘domain()’ + /// function or a final substring of the host component is a + /// period (U+002E) immediately followed by the argument to the + /// ‘domain()’ function. + #[css(function)] + Domain(String), + /// Regular expression matching function. It evaluates to true + /// whenever the regular expression matches the entirety of the URL + /// of the document being styled. + #[css(function)] + Regexp(String), + /// Matching function for a media document. + #[css(function)] + MediaDocument(MediaDocumentKind), + /// Matching function for a plain-text document. + #[css(function)] + PlainTextDocument(()), + /// Matching function for a document that can be observed by other content + /// documents. + #[css(function)] + UnobservableDocument(()), +} + +macro_rules! parse_quoted_or_unquoted_string { + ($input:ident, $url_matching_function:expr) => { + $input.parse_nested_block(|input| { + let start = input.position(); + input + .parse_entirely(|input| { + let string = input.expect_string()?; + Ok($url_matching_function(string.as_ref().to_owned())) + }) + .or_else(|_: ParseError| { + while let Ok(_) = input.next() {} + Ok($url_matching_function(input.slice_from(start).to_string())) + }) + }) + }; +} + +impl DocumentMatchingFunction { + /// Parse a URL matching function for a`@document` rule's condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(url) = input.try_parse(|input| CssUrl::parse(context, input)) { + return Ok(DocumentMatchingFunction::Url(url)); + } + + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + match_ignore_ascii_case! { &function, + "url-prefix" => { + parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::UrlPrefix) + }, + "domain" => { + parse_quoted_or_unquoted_string!(input, DocumentMatchingFunction::Domain) + }, + "regexp" => { + input.parse_nested_block(|input| { + Ok(DocumentMatchingFunction::Regexp( + input.expect_string()?.as_ref().to_owned(), + )) + }) + }, + "media-document" => { + input.parse_nested_block(|input| { + let kind = MediaDocumentKind::parse(input)?; + Ok(DocumentMatchingFunction::MediaDocument(kind)) + }) + }, + + "plain-text-document" => { + input.parse_nested_block(|input| { + input.expect_exhausted()?; + Ok(DocumentMatchingFunction::PlainTextDocument(())) + }) + }, + + "unobservable-document" => { + input.parse_nested_block(|input| { + input.expect_exhausted()?; + Ok(DocumentMatchingFunction::UnobservableDocument(())) + }) + }, + + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + } + + #[cfg(feature = "gecko")] + /// Evaluate a URL matching function. + pub fn evaluate(&self, device: &Device) -> bool { + use crate::gecko_bindings::bindings::Gecko_DocumentRule_UseForPresentation; + use crate::gecko_bindings::structs::DocumentMatchingFunction as GeckoDocumentMatchingFunction; + use nsstring::nsCStr; + + let func = match *self { + DocumentMatchingFunction::Url(_) => GeckoDocumentMatchingFunction::URL, + DocumentMatchingFunction::UrlPrefix(_) => GeckoDocumentMatchingFunction::URLPrefix, + DocumentMatchingFunction::Domain(_) => GeckoDocumentMatchingFunction::Domain, + DocumentMatchingFunction::Regexp(_) => GeckoDocumentMatchingFunction::RegExp, + DocumentMatchingFunction::MediaDocument(_) => { + GeckoDocumentMatchingFunction::MediaDocument + }, + DocumentMatchingFunction::PlainTextDocument(..) => { + GeckoDocumentMatchingFunction::PlainTextDocument + }, + DocumentMatchingFunction::UnobservableDocument(..) => { + GeckoDocumentMatchingFunction::UnobservableDocument + }, + }; + + let pattern = nsCStr::from(match *self { + DocumentMatchingFunction::Url(ref url) => url.as_str(), + DocumentMatchingFunction::UrlPrefix(ref pat) | + DocumentMatchingFunction::Domain(ref pat) | + DocumentMatchingFunction::Regexp(ref pat) => pat, + DocumentMatchingFunction::MediaDocument(kind) => match kind { + MediaDocumentKind::All => "all", + MediaDocumentKind::Image => "image", + MediaDocumentKind::Plugin => "plugin", + MediaDocumentKind::Video => "video", + }, + DocumentMatchingFunction::PlainTextDocument(()) | + DocumentMatchingFunction::UnobservableDocument(()) => "", + }); + unsafe { Gecko_DocumentRule_UseForPresentation(device.document(), &*pattern, func) } + } + + #[cfg(not(feature = "gecko"))] + /// Evaluate a URL matching function. + pub fn evaluate(&self, _: &Device) -> bool { + false + } +} + +/// A `@document` rule's condition. +/// +/// <https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#at-document> +/// +/// The `@document` rule's condition is written as a comma-separated list of +/// URL matching functions, and the condition evaluates to true whenever any +/// one of those functions evaluates to true. +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct DocumentCondition(#[css(iterable)] Vec<DocumentMatchingFunction>); + +impl DocumentCondition { + /// Parse a document condition. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let conditions = + input.parse_comma_separated(|input| DocumentMatchingFunction::parse(context, input))?; + + let condition = DocumentCondition(conditions); + if !condition.allowed_in(context) { + return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid("-moz-document".into()))); + } + Ok(condition) + } + + /// Evaluate a document condition. + pub fn evaluate(&self, device: &Device) -> bool { + self.0 + .iter() + .any(|url_matching_function| url_matching_function.evaluate(device)) + } + + #[cfg(feature = "servo")] + fn allowed_in(&self, _: &ParserContext) -> bool { + false + } + + #[cfg(feature = "gecko")] + fn allowed_in(&self, context: &ParserContext) -> bool { + if context.chrome_rules_enabled() { + return true; + } + + // Allow a single url-prefix() for compatibility. + // + // See bug 1446470 and dependencies. + if self.0.len() != 1 { + return false; + } + + // NOTE(emilio): This technically allows url-prefix("") too, but... + match self.0[0] { + DocumentMatchingFunction::UrlPrefix(ref prefix) => prefix.is_empty(), + _ => false, + } + } +} diff --git a/servo/components/style/stylesheets/font_face_rule.rs b/servo/components/style/stylesheets/font_face_rule.rs new file mode 100644 index 0000000000..78f3b338b2 --- /dev/null +++ b/servo/components/style/stylesheets/font_face_rule.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(missing_docs)] + +pub use crate::font_face::FontFaceRuleData as FontFaceRule; diff --git a/servo/components/style/stylesheets/font_feature_values_rule.rs b/servo/components/style/stylesheets/font_feature_values_rule.rs new file mode 100644 index 0000000000..06016ec2bd --- /dev/null +++ b/servo/components/style/stylesheets/font_feature_values_rule.rs @@ -0,0 +1,490 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@font-feature-values`][font-feature-values] at-rule. +//! +//! [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule + +use crate::error_reporting::ContextualParseError; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::bindings::Gecko_AppendFeatureValueHashEntry; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs::{self, gfxFontFeatureValueSet, nsTArray}; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRuleType; +use crate::values::computed::font::FamilyName; +use crate::values::serialize_atom_identifier; +use crate::Atom; +use cssparser::{ + AtRuleParser, BasicParseErrorKind, CowRcStr, DeclarationParser, Parser, ParserState, + QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, Token, +}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A @font-feature-values block declaration. +/// It is `<ident>: <integer>+`. +/// This struct can take 3 value types. +/// - `SingleValue` is to keep just one unsigned integer value. +/// - `PairValues` is to keep one or two unsigned integer values. +/// - `VectorValues` is to keep a list of unsigned integer values. +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FFVDeclaration<T> { + /// An `<ident>` for declaration name. + pub name: Atom, + /// An `<integer>+` for declaration value. + pub value: T, +} + +impl<T: ToCss> ToCss for FFVDeclaration<T> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.name, dest)?; + dest.write_str(": ")?; + self.value.to_css(dest)?; + dest.write_char(';') + } +} + +/// A trait for @font-feature-values rule to gecko values conversion. +#[cfg(feature = "gecko")] +pub trait ToGeckoFontFeatureValues { + /// Sets the equivalent of declaration to gecko `nsTArray<u32>` array. + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>); +} + +/// A @font-feature-values block declaration value that keeps one value. +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub struct SingleValue(pub u32); + +impl Parse for SingleValue { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SingleValue, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { + int_value: Some(v), .. + } if v >= 0 => Ok(SingleValue(v as u32)), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +#[cfg(feature = "gecko")] +impl ToGeckoFontFeatureValues for SingleValue { + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) { + unsafe { + array.set_len_pod(1); + } + array[0] = self.0 as u32; + } +} + +/// A @font-feature-values block declaration value that keeps one or two values. +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub struct PairValues(pub u32, pub Option<u32>); + +impl Parse for PairValues { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<PairValues, ParseError<'i>> { + let location = input.current_source_location(); + let first = match *input.next()? { + Token::Number { + int_value: Some(a), .. + } if a >= 0 => a as u32, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + let location = input.current_source_location(); + match input.next() { + Ok(&Token::Number { + int_value: Some(b), .. + }) if b >= 0 => Ok(PairValues(first, Some(b as u32))), + // It can't be anything other than number. + Ok(t) => Err(location.new_unexpected_token_error(t.clone())), + // It can be just one value. + Err(_) => Ok(PairValues(first, None)), + } + } +} + +#[cfg(feature = "gecko")] +impl ToGeckoFontFeatureValues for PairValues { + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) { + let len = if self.1.is_some() { 2 } else { 1 }; + + unsafe { + array.set_len_pod(len); + } + array[0] = self.0 as u32; + if let Some(second) = self.1 { + array[1] = second as u32; + }; + } +} + +/// A @font-feature-values block declaration value that keeps a list of values. +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub struct VectorValues(#[css(iterable)] pub Vec<u32>); + +impl Parse for VectorValues { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<VectorValues, ParseError<'i>> { + let mut vec = vec![]; + loop { + let location = input.current_source_location(); + match input.next() { + Ok(&Token::Number { + int_value: Some(a), .. + }) if a >= 0 => { + vec.push(a as u32); + }, + // It can't be anything other than number. + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(_) => break, + } + } + + if vec.len() == 0 { + return Err(input.new_error(BasicParseErrorKind::EndOfInput)); + } + + Ok(VectorValues(vec)) + } +} + +#[cfg(feature = "gecko")] +impl ToGeckoFontFeatureValues for VectorValues { + fn to_gecko_font_feature_values(&self, array: &mut nsTArray<u32>) { + array.assign_from_iter_pod(self.0.iter().map(|v| *v)); + } +} + +/// Parses a list of `FamilyName`s. +pub fn parse_family_name_list<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Vec<FamilyName>, ParseError<'i>> { + input + .parse_comma_separated(|i| FamilyName::parse(context, i)) + .map_err(|e| e.into()) +} + +/// @font-feature-values inside block parser. Parses a list of `FFVDeclaration`. +/// (`<ident>: <integer>+`) +struct FFVDeclarationsParser<'a, 'b: 'a, T: 'a> { + context: &'a ParserContext<'b>, + declarations: &'a mut Vec<FFVDeclaration<T>>, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i, T> AtRuleParser<'i> for FFVDeclarationsParser<'a, 'b, T> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i, T> QualifiedRuleParser<'i> for FFVDeclarationsParser<'a, 'b, T> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i, T> DeclarationParser<'i> for FFVDeclarationsParser<'a, 'b, T> +where + T: Parse, +{ + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let value = input.parse_entirely(|i| T::parse(self.context, i))?; + let new = FFVDeclaration { + name: Atom::from(&*name), + value, + }; + update_or_push(&mut self.declarations, new); + Ok(()) + } +} + +impl<'a, 'b, 'i, T> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for FFVDeclarationsParser<'a, 'b, T> +where + T: Parse, +{ + fn parse_declarations(&self) -> bool { + true + } + fn parse_qualified(&self) -> bool { + false + } +} + +macro_rules! font_feature_values_blocks { + ( + blocks = [ + $( #[$doc: meta] $name: tt $ident: ident / $ident_camel: ident / $gecko_enum: ident: $ty: ty, )* + ] + ) => { + /// The [`@font-feature-values`][font-feature-values] at-rule. + /// + /// [font-feature-values]: https://drafts.csswg.org/css-fonts-3/#at-font-feature-values-rule + #[derive(Clone, Debug, PartialEq, ToShmem)] + pub struct FontFeatureValuesRule { + /// Font family list for @font-feature-values rule. + /// Family names cannot contain generic families. FamilyName + /// also accepts only non-generic names. + pub family_names: Vec<FamilyName>, + $( + #[$doc] + pub $ident: Vec<FFVDeclaration<$ty>>, + )* + /// The line and column of the rule's source code. + pub source_location: SourceLocation, + } + + impl FontFeatureValuesRule { + /// Creates an empty FontFeatureValuesRule with given location and family name list. + fn new(family_names: Vec<FamilyName>, location: SourceLocation) -> Self { + FontFeatureValuesRule { + family_names: family_names, + $( + $ident: vec![], + )* + source_location: location, + } + } + + /// Parses a `FontFeatureValuesRule`. + pub fn parse( + context: &ParserContext, + input: &mut Parser, + family_names: Vec<FamilyName>, + location: SourceLocation, + ) -> Self { + let mut rule = FontFeatureValuesRule::new(family_names, location); + let mut parser = FontFeatureValuesRuleParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(result) = iter.next() { + if let Err((error, slice)) = result { + let location = error.location; + let error = ContextualParseError::UnsupportedRule(slice, error); + context.log_css_error(location, error); + } + } + rule + } + + /// Prints inside of `@font-feature-values` block. + pub fn value_to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + $( + if self.$ident.len() > 0 { + dest.write_str(concat!("@", $name, " {\n"))?; + let iter = self.$ident.iter(); + for val in iter { + val.to_css(dest)?; + dest.write_str("\n")? + } + dest.write_str("}\n")? + } + )* + Ok(()) + } + + /// Returns length of all at-rules. + pub fn len(&self) -> usize { + let mut len = 0; + $( + len += self.$ident.len(); + )* + len + } + + /// Convert to Gecko gfxFontFeatureValueSet. + #[cfg(feature = "gecko")] + pub fn set_at_rules(&self, dest: *mut gfxFontFeatureValueSet) { + for ref family in self.family_names.iter() { + let family = family.name.to_ascii_lowercase(); + $( + if self.$ident.len() > 0 { + for val in self.$ident.iter() { + let array = unsafe { + Gecko_AppendFeatureValueHashEntry( + dest, + family.as_ptr(), + structs::$gecko_enum, + val.name.as_ptr() + ) + }; + unsafe { + val.value.to_gecko_font_feature_values(&mut *array); + } + } + } + )* + } + } + } + + impl ToCssWithGuard for FontFeatureValuesRule { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@font-feature-values ")?; + self.family_names.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" {\n")?; + self.value_to_css(&mut CssWriter::new(dest))?; + dest.write_char('}') + } + } + + /// Updates with new value if same `ident` exists, otherwise pushes to the vector. + fn update_or_push<T>(vec: &mut Vec<FFVDeclaration<T>>, element: FFVDeclaration<T>) { + if let Some(item) = vec.iter_mut().find(|item| item.name == element.name) { + item.value = element.value; + } else { + vec.push(element); + } + } + + /// Keeps the information about block type like @swash, @styleset etc. + enum BlockType { + $( + $ident_camel, + )* + } + + /// Parser for `FontFeatureValuesRule`. Parses all blocks + /// <feature-type> { + /// <feature-value-declaration-list> + /// } + /// <feature-type> = @stylistic | @historical-forms | @styleset | + /// @character-variant | @swash | @ornaments | @annotation + struct FontFeatureValuesRuleParser<'a> { + context: &'a ParserContext<'a>, + rule: &'a mut FontFeatureValuesRule, + } + + /// Default methods reject all qualified rules. + impl<'a, 'i> QualifiedRuleParser<'i> for FontFeatureValuesRuleParser<'a> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; + } + + impl<'a, 'i> AtRuleParser<'i> for FontFeatureValuesRuleParser<'a> { + type Prelude = BlockType; + type AtRule = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<BlockType, ParseError<'i>> { + match_ignore_ascii_case! { &*name, + $( + $name => Ok(BlockType::$ident_camel), + )* + _ => Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), + } + } + + fn parse_block<'t>( + &mut self, + prelude: BlockType, + _: &ParserState, + input: &mut Parser<'i, 't> + ) -> Result<Self::AtRule, ParseError<'i>> { + debug_assert!(self.context.rule_types().contains(CssRuleType::FontFeatureValues)); + match prelude { + $( + BlockType::$ident_camel => { + let mut parser = FFVDeclarationsParser { + context: &self.context, + declarations: &mut self.rule.$ident, + }; + + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedKeyframePropertyDeclaration( + slice, error + ); + self.context.log_css_error(location, error); + } + } + }, + )* + } + + Ok(()) + } + } + + impl<'a, 'i> DeclarationParser<'i> for FontFeatureValuesRuleParser<'a> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + } + + impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> for FontFeatureValuesRuleParser<'a> { + fn parse_declarations(&self) -> bool { false } + fn parse_qualified(&self) -> bool { true } + } + } +} + +font_feature_values_blocks! { + blocks = [ + #[doc = "A @swash blocksck. \ + Specifies a feature name that will work with the swash() \ + functional notation of font-variant-alternates."] + "swash" swash / Swash / NS_FONT_VARIANT_ALTERNATES_SWASH: SingleValue, + + #[doc = "A @stylistic block. \ + Specifies a feature name that will work with the annotation() \ + functional notation of font-variant-alternates."] + "stylistic" stylistic / Stylistic / NS_FONT_VARIANT_ALTERNATES_STYLISTIC: SingleValue, + + #[doc = "A @ornaments block. \ + Specifies a feature name that will work with the ornaments() ] \ + functional notation of font-variant-alternates."] + "ornaments" ornaments / Ornaments / NS_FONT_VARIANT_ALTERNATES_ORNAMENTS: SingleValue, + + #[doc = "A @annotation block. \ + Specifies a feature name that will work with the stylistic() \ + functional notation of font-variant-alternates."] + "annotation" annotation / Annotation / NS_FONT_VARIANT_ALTERNATES_ANNOTATION: SingleValue, + + #[doc = "A @character-variant block. \ + Specifies a feature name that will work with the styleset() \ + functional notation of font-variant-alternates. The value can be a pair."] + "character-variant" character_variant / CharacterVariant / NS_FONT_VARIANT_ALTERNATES_CHARACTER_VARIANT: + PairValues, + + #[doc = "A @styleset block. \ + Specifies a feature name that will work with the character-variant() \ + functional notation of font-variant-alternates. The value can be a list."] + "styleset" styleset / Styleset / NS_FONT_VARIANT_ALTERNATES_STYLESET: VectorValues, + ] +} diff --git a/servo/components/style/stylesheets/font_palette_values_rule.rs b/servo/components/style/stylesheets/font_palette_values_rule.rs new file mode 100644 index 0000000000..400d348215 --- /dev/null +++ b/servo/components/style/stylesheets/font_palette_values_rule.rs @@ -0,0 +1,264 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@font-palette-values`][font-palette-values] at-rule. +//! +//! [font-palette-values]: https://drafts.csswg.org/css-fonts/#font-palette-values + +use crate::error_reporting::ContextualParseError; +use crate::gecko_bindings::bindings::Gecko_AppendPaletteValueHashEntry; +use crate::gecko_bindings::bindings::{Gecko_SetFontPaletteBase, Gecko_SetFontPaletteOverride}; +use crate::gecko_bindings::structs::gfx::FontPaletteValueSet; +use crate::gecko_bindings::structs::gfx::FontPaletteValueSet_PaletteValues_kDark; +use crate::gecko_bindings::structs::gfx::FontPaletteValueSet_PaletteValues_kLight; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::font_feature_values_rule::parse_family_name_list; +use crate::values::computed::font::FamilyName; +use crate::values::specified::Color as SpecifiedColor; +use crate::values::specified::NonNegativeInteger; +use crate::values::DashedIdent; +use cssparser::{ + AtRuleParser, CowRcStr, DeclarationParser, Parser, QualifiedRuleParser, RuleBodyItemParser, + RuleBodyParser, SourceLocation, +}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{Comma, OneOrMoreSeparated}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +#[allow(missing_docs)] +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct FontPaletteOverrideColor { + index: NonNegativeInteger, + color: SpecifiedColor, +} + +impl Parse for FontPaletteOverrideColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontPaletteOverrideColor, ParseError<'i>> { + let index = NonNegativeInteger::parse(context, input)?; + let location = input.current_source_location(); + let color = SpecifiedColor::parse(context, input)?; + // Only absolute colors are accepted here. + if let SpecifiedColor::Absolute { .. } = color { + Ok(FontPaletteOverrideColor { index, color }) + } else { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for FontPaletteOverrideColor { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.index.to_css(dest)?; + dest.write_char(' ')?; + self.color.to_css(dest) + } +} + +impl OneOrMoreSeparated for FontPaletteOverrideColor { + type S = Comma; +} + +impl OneOrMoreSeparated for FamilyName { + type S = Comma; +} + +#[allow(missing_docs)] +#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +pub enum FontPaletteBase { + Light, + Dark, + Index(NonNegativeInteger), +} + +/// The [`@font-palette-values`][font-palette-values] at-rule. +/// +/// [font-palette-values]: https://drafts.csswg.org/css-fonts/#font-palette-values +#[derive(Clone, Debug, PartialEq, ToShmem)] +pub struct FontPaletteValuesRule { + /// Palette name. + pub name: DashedIdent, + /// Font family list for @font-palette-values rule. + /// Family names cannot contain generic families. FamilyName + /// also accepts only non-generic names. + pub family_names: Vec<FamilyName>, + /// The base palette. + pub base_palette: Option<FontPaletteBase>, + /// The list of override colors. + pub override_colors: Vec<FontPaletteOverrideColor>, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl FontPaletteValuesRule { + /// Creates an empty FontPaletteValuesRule with given location and name. + fn new(name: DashedIdent, location: SourceLocation) -> Self { + FontPaletteValuesRule { + name, + family_names: vec![], + base_palette: None, + override_colors: vec![], + source_location: location, + } + } + + /// Parses a `FontPaletteValuesRule`. + pub fn parse( + context: &ParserContext, + input: &mut Parser, + name: DashedIdent, + location: SourceLocation, + ) -> Self { + let mut rule = FontPaletteValuesRule::new(name, location); + let mut parser = FontPaletteValuesDeclarationParser { + context, + rule: &mut rule, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = + ContextualParseError::UnsupportedFontPaletteValuesDescriptor(slice, error); + context.log_css_error(location, error); + } + } + rule + } + + /// Prints inside of `@font-palette-values` block. + fn value_to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if !self.family_names.is_empty() { + dest.write_str("font-family: ")?; + self.family_names.to_css(dest)?; + dest.write_str("; ")?; + } + if let Some(base) = &self.base_palette { + dest.write_str("base-palette: ")?; + base.to_css(dest)?; + dest.write_str("; ")?; + } + if !self.override_colors.is_empty() { + dest.write_str("override-colors: ")?; + self.override_colors.to_css(dest)?; + dest.write_str("; ")?; + } + Ok(()) + } + + /// Convert to Gecko FontPaletteValueSet. + pub fn to_gecko_palette_value_set(&self, dest: *mut FontPaletteValueSet) { + for ref family in self.family_names.iter() { + let family = family.name.to_ascii_lowercase(); + let palette_values = unsafe { + Gecko_AppendPaletteValueHashEntry(dest, family.as_ptr(), self.name.0.as_ptr()) + }; + if let Some(base_palette) = &self.base_palette { + unsafe { + Gecko_SetFontPaletteBase( + palette_values, + match &base_palette { + FontPaletteBase::Light => FontPaletteValueSet_PaletteValues_kLight, + FontPaletteBase::Dark => FontPaletteValueSet_PaletteValues_kDark, + FontPaletteBase::Index(i) => i.0.value() as i32, + }, + ); + } + } + for c in &self.override_colors { + if let SpecifiedColor::Absolute(ref absolute) = c.color { + unsafe { + Gecko_SetFontPaletteOverride( + palette_values, + c.index.0.value(), + (&absolute.color) as *const _ as *mut _, + ); + } + } + } + } + } +} + +impl ToCssWithGuard for FontPaletteValuesRule { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@font-palette-values ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + self.value_to_css(&mut CssWriter::new(dest))?; + dest.write_char('}') + } +} + +/// Parser for declarations in `FontPaletteValuesRule`. +struct FontPaletteValuesDeclarationParser<'a> { + context: &'a ParserContext<'a>, + rule: &'a mut FontPaletteValuesRule, +} + +impl<'a, 'i> AtRuleParser<'i> for FontPaletteValuesDeclarationParser<'a> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'i> QualifiedRuleParser<'i> for FontPaletteValuesDeclarationParser<'a> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +fn parse_override_colors<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Vec<FontPaletteOverrideColor>, ParseError<'i>> { + input.parse_comma_separated(|i| FontPaletteOverrideColor::parse(context, i)) +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for FontPaletteValuesDeclarationParser<'a> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + match_ignore_ascii_case! { &*name, + "font-family" => { + self.rule.family_names = parse_family_name_list(self.context, input)? + }, + "base-palette" => { + self.rule.base_palette = Some(input.parse_entirely(|i| FontPaletteBase::parse(self.context, i))?) + }, + "override-colors" => { + self.rule.override_colors = parse_override_colors(self.context, input)? + }, + _ => return Err(input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name.clone()))), + } + Ok(()) + } +} + +impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for FontPaletteValuesDeclarationParser<'a> +{ + fn parse_declarations(&self) -> bool { + true + } + fn parse_qualified(&self) -> bool { + false + } +} diff --git a/servo/components/style/stylesheets/import_rule.rs b/servo/components/style/stylesheets/import_rule.rs new file mode 100644 index 0000000000..e96134b436 --- /dev/null +++ b/servo/components/style/stylesheets/import_rule.rs @@ -0,0 +1,301 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The [`@import`][import] at-rule. +//! +//! [import]: https://drafts.csswg.org/css-cascade-3/#at-import + +use crate::media_queries::MediaList; +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{ + DeepCloneParams, DeepCloneWithLock, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, +}; +use crate::str::CssStringWriter; +use crate::stylesheets::{ + layer_rule::LayerName, supports_rule::SupportsCondition, CssRule, CssRuleType, + StylesheetInDocument, +}; +use crate::values::CssUrl; +use cssparser::{Parser, SourceLocation}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +/// A sheet that is held from an import rule. +#[cfg(feature = "gecko")] +#[derive(Debug)] +pub enum ImportSheet { + /// A bonafide stylesheet. + Sheet(crate::gecko::data::GeckoStyleSheet), + + /// An @import created while parsing off-main-thread, whose Gecko sheet has + /// yet to be created and attached. + Pending, + + /// An @import created with a false <supports-condition>, so will never be fetched. + Refused, +} + +#[cfg(feature = "gecko")] +impl ImportSheet { + /// Creates a new ImportSheet from a GeckoStyleSheet. + pub fn new(sheet: crate::gecko::data::GeckoStyleSheet) -> Self { + ImportSheet::Sheet(sheet) + } + + /// Creates a pending ImportSheet for a load that has not started yet. + pub fn new_pending() -> Self { + ImportSheet::Pending + } + + /// Creates a refused ImportSheet for a load that will not happen. + pub fn new_refused() -> Self { + ImportSheet::Refused + } + + /// Returns a reference to the GeckoStyleSheet in this ImportSheet, if it + /// exists. + pub fn as_sheet(&self) -> Option<&crate::gecko::data::GeckoStyleSheet> { + match *self { + ImportSheet::Sheet(ref s) => { + debug_assert!(!s.hack_is_null()); + if s.hack_is_null() { + return None; + } + Some(s) + }, + ImportSheet::Refused | ImportSheet::Pending => None, + } + } + + /// Returns the media list for this import rule. + pub fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + self.as_sheet().and_then(|s| s.media(guard)) + } + + /// Returns the rule list for this import rule. + pub fn rules<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a [CssRule] { + match self.as_sheet() { + Some(s) => s.rules(guard), + None => &[], + } + } +} + +#[cfg(feature = "gecko")] +impl DeepCloneWithLock for ImportSheet { + fn deep_clone_with_lock( + &self, + _lock: &SharedRwLock, + _guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + use crate::gecko::data::GeckoStyleSheet; + use crate::gecko_bindings::bindings; + match *self { + ImportSheet::Sheet(ref s) => { + let clone = unsafe { + bindings::Gecko_StyleSheet_Clone(s.raw() as *const _, params.reference_sheet) + }; + ImportSheet::Sheet(unsafe { GeckoStyleSheet::from_addrefed(clone) }) + }, + ImportSheet::Pending => ImportSheet::Pending, + ImportSheet::Refused => ImportSheet::Refused, + } + } +} + +/// A sheet that is held from an import rule. +#[cfg(feature = "servo")] +#[derive(Debug)] +pub struct ImportSheet(pub ::servo_arc::Arc<crate::stylesheets::Stylesheet>); + +#[cfg(feature = "servo")] +impl ImportSheet { + /// Returns the media list for this import rule. + pub fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + self.0.media(guard) + } + + /// Returns the rules for this import rule. + pub fn rules<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> &'a [CssRule] { + self.0.rules() + } +} + +#[cfg(feature = "servo")] +impl DeepCloneWithLock for ImportSheet { + fn deep_clone_with_lock( + &self, + _lock: &SharedRwLock, + _guard: &SharedRwLockReadGuard, + _params: &DeepCloneParams, + ) -> Self { + use servo_arc::Arc; + + ImportSheet(Arc::new((&*self.0).clone())) + } +} + +/// The layer specified in an import rule (can be none, anonymous, or named). +#[derive(Debug, Clone)] +pub enum ImportLayer { + /// No layer specified + None, + + /// Anonymous layer (`layer`) + Anonymous, + + /// Named layer (`layer(name)`) + Named(LayerName), +} + +/// The supports condition in an import rule. +#[derive(Debug, Clone)] +pub struct ImportSupportsCondition { + /// The supports condition. + pub condition: SupportsCondition, + + /// If the import is enabled, from the result of the import condition. + pub enabled: bool, +} + +impl ToCss for ImportLayer { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + ImportLayer::None => Ok(()), + ImportLayer::Anonymous => dest.write_str("layer"), + ImportLayer::Named(ref name) => { + dest.write_str("layer(")?; + name.to_css(dest)?; + dest.write_char(')') + }, + } + } +} + +/// The [`@import`][import] at-rule. +/// +/// [import]: https://drafts.csswg.org/css-cascade-3/#at-import +#[derive(Debug)] +pub struct ImportRule { + /// The `<url>` this `@import` rule is loading. + pub url: CssUrl, + + /// The stylesheet is always present. However, in the case of gecko async + /// parsing, we don't actually have a Gecko sheet at first, and so the + /// ImportSheet just has stub behavior until it appears. + pub stylesheet: ImportSheet, + + /// A <supports-condition> for the rule. + pub supports: Option<ImportSupportsCondition>, + + /// A `layer()` function name. + pub layer: ImportLayer, + + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl ImportRule { + /// Parses the layer() / layer / supports() part of the import header, as per + /// https://drafts.csswg.org/css-cascade-5/#at-import: + /// + /// [ layer | layer(<layer-name>) ]? + /// [ supports([ <supports-condition> | <declaration> ]) ]? + /// + /// We do this here so that the import preloader can look at this without having to parse the + /// whole import rule or parse the media query list or what not. + pub fn parse_layer_and_supports<'i, 't>( + input: &mut Parser<'i, 't>, + context: &mut ParserContext, + ) -> (ImportLayer, Option<ImportSupportsCondition>) { + let layer = if input + .try_parse(|input| input.expect_ident_matching("layer")) + .is_ok() + { + ImportLayer::Anonymous + } else { + input + .try_parse(|input| { + input.expect_function_matching("layer")?; + input + .parse_nested_block(|input| LayerName::parse(context, input)) + .map(|name| ImportLayer::Named(name)) + }) + .ok() + .unwrap_or(ImportLayer::None) + }; + + let supports = if !static_prefs::pref!("layout.css.import-supports.enabled") { + None + } else { + input + .try_parse(SupportsCondition::parse_for_import) + .map(|condition| { + let enabled = context + .nest_for_rule(CssRuleType::Style, |context| condition.eval(context)); + ImportSupportsCondition { condition, enabled } + }) + .ok() + }; + + (layer, supports) + } +} + +impl ToShmem for ImportRule { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + Err(String::from( + "ToShmem failed for ImportRule: cannot handle imported style sheets", + )) + } +} + +impl DeepCloneWithLock for ImportRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + ImportRule { + url: self.url.clone(), + stylesheet: self.stylesheet.deep_clone_with_lock(lock, guard, params), + supports: self.supports.clone(), + layer: self.layer.clone(), + source_location: self.source_location.clone(), + } + } +} + +impl ToCssWithGuard for ImportRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@import ")?; + self.url.to_css(&mut CssWriter::new(dest))?; + + if !matches!(self.layer, ImportLayer::None) { + dest.write_char(' ')?; + self.layer.to_css(&mut CssWriter::new(dest))?; + } + + if let Some(ref supports) = self.supports { + dest.write_str(" supports(")?; + supports.condition.to_css(&mut CssWriter::new(dest))?; + dest.write_char(')')?; + } + + if let Some(media) = self.stylesheet.media(guard) { + if !media.is_empty() { + dest.write_char(' ')?; + media.to_css(&mut CssWriter::new(dest))?; + } + } + + dest.write_char(';') + } +} diff --git a/servo/components/style/stylesheets/keyframes_rule.rs b/servo/components/style/stylesheets/keyframes_rule.rs new file mode 100644 index 0000000000..96e916b553 --- /dev/null +++ b/servo/components/style/stylesheets/keyframes_rule.rs @@ -0,0 +1,690 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Keyframes: https://drafts.csswg.org/css-animations/#keyframes + +use crate::error_reporting::ContextualParseError; +use crate::parser::ParserContext; +use crate::properties::{ + longhands::{ + animation_composition::single_value::SpecifiedValue as SpecifiedComposition, + transition_timing_function::single_value::SpecifiedValue as SpecifiedTimingFunction, + }, + Importance, LonghandId, PropertyDeclaration, PropertyDeclarationBlock, PropertyDeclarationId, + PropertyDeclarationIdSet, PropertyId, SourcePropertyDeclaration, +}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, SharedRwLock, SharedRwLockReadGuard}; +use crate::shared_lock::{Locked, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::rule_parser::VendorPrefix; +use crate::stylesheets::{CssRuleType, StylesheetContents}; +use crate::values::{serialize_percentage, KeyframesName}; +use cssparser::{ + parse_one_rule, AtRuleParser, CowRcStr, DeclarationParser, Parser, ParserInput, ParserState, + QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, Token, +}; +use servo_arc::Arc; +use std::borrow::Cow; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ParsingMode, StyleParseErrorKind, ToCss}; + +/// A [`@keyframes`][keyframes] rule. +/// +/// [keyframes]: https://drafts.csswg.org/css-animations/#keyframes +#[derive(Debug, ToShmem)] +pub struct KeyframesRule { + /// The name of the current animation. + pub name: KeyframesName, + /// The keyframes specified for this CSS rule. + pub keyframes: Vec<Arc<Locked<Keyframe>>>, + /// Vendor prefix type the @keyframes has. + pub vendor_prefix: Option<VendorPrefix>, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for KeyframesRule { + // Serialization of KeyframesRule is not specced. + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@keyframes ")?; + self.name.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" {")?; + let iter = self.keyframes.iter(); + for lock in iter { + dest.write_str("\n")?; + let keyframe = lock.read_with(&guard); + keyframe.to_css(guard, dest)?; + } + dest.write_str("\n}") + } +} + +impl KeyframesRule { + /// Returns the index of the last keyframe that matches the given selector. + /// If the selector is not valid, or no keyframe is found, returns None. + /// + /// Related spec: + /// <https://drafts.csswg.org/css-animations-1/#interface-csskeyframesrule-findrule> + pub fn find_rule(&self, guard: &SharedRwLockReadGuard, selector: &str) -> Option<usize> { + let mut input = ParserInput::new(selector); + if let Ok(selector) = Parser::new(&mut input).parse_entirely(KeyframeSelector::parse) { + for (i, keyframe) in self.keyframes.iter().enumerate().rev() { + if keyframe.read_with(guard).selector == selector { + return Some(i); + } + } + } + None + } +} + +impl DeepCloneWithLock for KeyframesRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + KeyframesRule { + name: self.name.clone(), + keyframes: self + .keyframes + .iter() + .map(|x| { + Arc::new( + lock.wrap(x.read_with(guard).deep_clone_with_lock(lock, guard, params)), + ) + }) + .collect(), + vendor_prefix: self.vendor_prefix.clone(), + source_location: self.source_location.clone(), + } + } +} + +/// A number from 0 to 1, indicating the percentage of the animation when this +/// keyframe should run. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToShmem)] +pub struct KeyframePercentage(pub f32); + +impl ::std::cmp::Ord for KeyframePercentage { + #[inline] + fn cmp(&self, other: &Self) -> ::std::cmp::Ordering { + // We know we have a number from 0 to 1, so unwrap() here is safe. + self.0.partial_cmp(&other.0).unwrap() + } +} + +impl ::std::cmp::Eq for KeyframePercentage {} + +impl ToCss for KeyframePercentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_percentage(self.0, dest) + } +} + +impl KeyframePercentage { + /// Trivially constructs a new `KeyframePercentage`. + #[inline] + pub fn new(value: f32) -> KeyframePercentage { + debug_assert!(value >= 0. && value <= 1.); + KeyframePercentage(value) + } + + fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<KeyframePercentage, ParseError<'i>> { + let token = input.next()?.clone(); + match token { + Token::Ident(ref identifier) if identifier.as_ref().eq_ignore_ascii_case("from") => { + Ok(KeyframePercentage::new(0.)) + }, + Token::Ident(ref identifier) if identifier.as_ref().eq_ignore_ascii_case("to") => { + Ok(KeyframePercentage::new(1.)) + }, + Token::Percentage { + unit_value: percentage, + .. + } if percentage >= 0. && percentage <= 1. => Ok(KeyframePercentage::new(percentage)), + _ => Err(input.new_unexpected_token_error(token)), + } + } +} + +/// A keyframes selector is a list of percentages or from/to symbols, which are +/// converted at parse time to percentages. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[css(comma)] +pub struct KeyframeSelector(#[css(iterable)] Vec<KeyframePercentage>); + +impl KeyframeSelector { + /// Return the list of percentages this selector contains. + #[inline] + pub fn percentages(&self) -> &[KeyframePercentage] { + &self.0 + } + + /// A dummy public function so we can write a unit test for this. + pub fn new_for_unit_testing(percentages: Vec<KeyframePercentage>) -> KeyframeSelector { + KeyframeSelector(percentages) + } + + /// Parse a keyframe selector from CSS input. + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + input + .parse_comma_separated(KeyframePercentage::parse) + .map(KeyframeSelector) + } +} + +/// A keyframe. +#[derive(Debug, ToShmem)] +pub struct Keyframe { + /// The selector this keyframe was specified from. + pub selector: KeyframeSelector, + + /// The declaration block that was declared inside this keyframe. + /// + /// Note that `!important` rules in keyframes don't apply, but we keep this + /// `Arc` just for convenience. + pub block: Arc<Locked<PropertyDeclarationBlock>>, + + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for Keyframe { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + self.selector.to_css(&mut CssWriter::new(dest))?; + dest.write_str(" { ")?; + self.block.read_with(guard).to_css(dest)?; + dest.write_str(" }")?; + Ok(()) + } +} + +impl Keyframe { + /// Parse a CSS keyframe. + pub fn parse<'i>( + css: &'i str, + parent_stylesheet_contents: &StylesheetContents, + lock: &SharedRwLock, + ) -> Result<Arc<Locked<Self>>, ParseError<'i>> { + let url_data = parent_stylesheet_contents.url_data.read(); + let namespaces = parent_stylesheet_contents.namespaces.read(); + let mut context = ParserContext::new( + parent_stylesheet_contents.origin, + &url_data, + Some(CssRuleType::Keyframe), + ParsingMode::DEFAULT, + parent_stylesheet_contents.quirks_mode, + Cow::Borrowed(&*namespaces), + None, + None, + ); + let mut input = ParserInput::new(css); + let mut input = Parser::new(&mut input); + + let mut declarations = SourcePropertyDeclaration::default(); + let mut rule_parser = KeyframeListParser { + context: &mut context, + shared_lock: &lock, + declarations: &mut declarations, + }; + parse_one_rule(&mut input, &mut rule_parser) + } +} + +impl DeepCloneWithLock for Keyframe { + /// Deep clones this Keyframe. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + _params: &DeepCloneParams, + ) -> Keyframe { + Keyframe { + selector: self.selector.clone(), + block: Arc::new(lock.wrap(self.block.read_with(guard).clone())), + source_location: self.source_location.clone(), + } + } +} + +/// A keyframes step value. This can be a synthetised keyframes animation, that +/// is, one autogenerated from the current computed values, or a list of +/// declarations to apply. +/// +/// TODO: Find a better name for this? +#[derive(Clone, Debug, MallocSizeOf)] +pub enum KeyframesStepValue { + /// A step formed by a declaration block specified by the CSS. + Declarations { + /// The declaration block per se. + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "XXX: Primary ref, measure if DMD says it's worthwhile" + )] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + block: Arc<Locked<PropertyDeclarationBlock>>, + }, + /// A synthetic step computed from the current computed values at the time + /// of the animation. + ComputedValues, +} + +/// A single step from a keyframe animation. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct KeyframesStep { + /// The percentage of the animation duration when this step starts. + pub start_percentage: KeyframePercentage, + /// Declarations that will determine the final style during the step, or + /// `ComputedValues` if this is an autogenerated step. + pub value: KeyframesStepValue, + /// Whether an animation-timing-function declaration exists in the list of + /// declarations. + /// + /// This is used to know when to override the keyframe animation style. + pub declared_timing_function: bool, + /// Whether an animation-composition declaration exists in the list of + /// declarations. + /// + /// This is used to know when to override the keyframe animation style. + pub declared_composition: bool, +} + +impl KeyframesStep { + #[inline] + fn new( + start_percentage: KeyframePercentage, + value: KeyframesStepValue, + guard: &SharedRwLockReadGuard, + ) -> Self { + let mut declared_timing_function = false; + let mut declared_composition = false; + if let KeyframesStepValue::Declarations { ref block } = value { + for prop_decl in block.read_with(guard).declarations().iter() { + match *prop_decl { + PropertyDeclaration::AnimationTimingFunction(..) => { + declared_timing_function = true; + }, + PropertyDeclaration::AnimationComposition(..) => { + declared_composition = true; + }, + _ => continue, + } + // Don't need to continue the loop if both are found. + if declared_timing_function && declared_composition { + break; + } + } + } + + KeyframesStep { + start_percentage, + value, + declared_timing_function, + declared_composition, + } + } + + /// Return specified PropertyDeclaration. + #[inline] + fn get_declared_property<'a>( + &'a self, + guard: &'a SharedRwLockReadGuard, + property: LonghandId, + ) -> Option<&'a PropertyDeclaration> { + match self.value { + KeyframesStepValue::Declarations { ref block } => { + let guard = block.read_with(guard); + let (declaration, _) = guard + .get(PropertyDeclarationId::Longhand(property)) + .unwrap(); + match *declaration { + PropertyDeclaration::CSSWideKeyword(..) => None, + // FIXME: Bug 1710735: Support css variable in @keyframes rule. + PropertyDeclaration::WithVariables(..) => None, + _ => Some(declaration), + } + }, + KeyframesStepValue::ComputedValues => { + panic!("Shouldn't happen to set this property in missing keyframes") + }, + } + } + + /// Return specified TransitionTimingFunction if this KeyframesSteps has + /// 'animation-timing-function'. + pub fn get_animation_timing_function( + &self, + guard: &SharedRwLockReadGuard, + ) -> Option<SpecifiedTimingFunction> { + if !self.declared_timing_function { + return None; + } + + self.get_declared_property(guard, LonghandId::AnimationTimingFunction) + .map(|decl| { + match *decl { + PropertyDeclaration::AnimationTimingFunction(ref value) => { + // Use the first value + value.0[0].clone() + }, + _ => unreachable!("Unexpected PropertyDeclaration"), + } + }) + } + + /// Return CompositeOperation if this KeyframesSteps has 'animation-composition'. + pub fn get_animation_composition( + &self, + guard: &SharedRwLockReadGuard, + ) -> Option<SpecifiedComposition> { + if !self.declared_composition { + return None; + } + + self.get_declared_property(guard, LonghandId::AnimationComposition) + .map(|decl| { + match *decl { + PropertyDeclaration::AnimationComposition(ref value) => { + // Use the first value + value.0[0].clone() + }, + _ => unreachable!("Unexpected PropertyDeclaration"), + } + }) + } +} + +/// This structure represents a list of animation steps computed from the list +/// of keyframes, in order. +/// +/// It only takes into account animable properties. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct KeyframesAnimation { + /// The difference steps of the animation. + pub steps: Vec<KeyframesStep>, + /// The properties that change in this animation. + pub properties_changed: PropertyDeclarationIdSet, + /// Vendor prefix type the @keyframes has. + pub vendor_prefix: Option<VendorPrefix>, +} + +/// Get all the animated properties in a keyframes animation. +fn get_animated_properties( + keyframes: &[Arc<Locked<Keyframe>>], + guard: &SharedRwLockReadGuard, +) -> PropertyDeclarationIdSet { + let mut ret = PropertyDeclarationIdSet::default(); + // NB: declarations are already deduplicated, so we don't have to check for + // it here. + for keyframe in keyframes { + let keyframe = keyframe.read_with(&guard); + let block = keyframe.block.read_with(guard); + // CSS Animations spec clearly defines that properties with !important + // in keyframe rules are invalid and ignored, but it's still ambiguous + // whether we should drop the !important properties or retain the + // properties when they are set via CSSOM. So we assume there might + // be properties with !important in keyframe rules here. + // See the spec issue https://github.com/w3c/csswg-drafts/issues/1824 + for declaration in block.normal_declaration_iter() { + let declaration_id = declaration.id(); + + if declaration_id == PropertyDeclarationId::Longhand(LonghandId::Display) { + continue; + } + + if !declaration_id.is_animatable() { + continue; + } + + ret.insert(declaration_id); + } + } + + ret +} + +impl KeyframesAnimation { + /// Create a keyframes animation from a given list of keyframes. + /// + /// This will return a keyframe animation with empty steps and + /// properties_changed if the list of keyframes is empty, or there are no + /// animated properties obtained from the keyframes. + /// + /// Otherwise, this will compute and sort the steps used for the animation, + /// and return the animation object. + pub fn from_keyframes( + keyframes: &[Arc<Locked<Keyframe>>], + vendor_prefix: Option<VendorPrefix>, + guard: &SharedRwLockReadGuard, + ) -> Self { + let mut result = KeyframesAnimation { + steps: vec![], + properties_changed: PropertyDeclarationIdSet::default(), + vendor_prefix, + }; + + if keyframes.is_empty() { + return result; + } + + result.properties_changed = get_animated_properties(keyframes, guard); + if result.properties_changed.is_empty() { + return result; + } + + for keyframe in keyframes { + let keyframe = keyframe.read_with(&guard); + for percentage in keyframe.selector.0.iter() { + result.steps.push(KeyframesStep::new( + *percentage, + KeyframesStepValue::Declarations { + block: keyframe.block.clone(), + }, + guard, + )); + } + } + + // Sort by the start percentage, so we can easily find a frame. + result.steps.sort_by_key(|step| step.start_percentage); + + // Prepend autogenerated keyframes if appropriate. + if result.steps[0].start_percentage.0 != 0. { + result.steps.insert( + 0, + KeyframesStep::new( + KeyframePercentage::new(0.), + KeyframesStepValue::ComputedValues, + guard, + ), + ); + } + + if result.steps.last().unwrap().start_percentage.0 != 1. { + result.steps.push(KeyframesStep::new( + KeyframePercentage::new(1.), + KeyframesStepValue::ComputedValues, + guard, + )); + } + + result + } +} + +/// Parses a keyframes list, like: +/// 0%, 50% { +/// width: 50%; +/// } +/// +/// 40%, 60%, 100% { +/// width: 100%; +/// } +struct KeyframeListParser<'a, 'b> { + context: &'a mut ParserContext<'b>, + shared_lock: &'a SharedRwLock, + declarations: &'a mut SourcePropertyDeclaration, +} + +/// Parses a keyframe list from CSS input. +pub fn parse_keyframe_list<'a>( + context: &mut ParserContext<'a>, + input: &mut Parser, + shared_lock: &SharedRwLock, +) -> Vec<Arc<Locked<Keyframe>>> { + let mut declarations = SourcePropertyDeclaration::default(); + let mut parser = KeyframeListParser { + context, + shared_lock, + declarations: &mut declarations, + }; + RuleBodyParser::new(input, &mut parser) + .filter_map(Result::ok) + .collect() +} + +impl<'a, 'b, 'i> AtRuleParser<'i> for KeyframeListParser<'a, 'b> { + type Prelude = (); + type AtRule = Arc<Locked<Keyframe>>; + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for KeyframeListParser<'a, 'b> { + type Declaration = Arc<Locked<Keyframe>>; + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for KeyframeListParser<'a, 'b> { + type Prelude = KeyframeSelector; + type QualifiedRule = Arc<Locked<Keyframe>>; + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + let start_position = input.position(); + KeyframeSelector::parse(input).map_err(|e| { + let location = e.location; + let error = ContextualParseError::InvalidKeyframeRule( + input.slice_from(start_position), + e.clone(), + ); + self.context.log_css_error(location, error); + e + }) + } + + fn parse_block<'t>( + &mut self, + selector: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::QualifiedRule, ParseError<'i>> { + let mut block = PropertyDeclarationBlock::new(); + let declarations = &mut self.declarations; + self.context + .nest_for_rule(CssRuleType::Keyframe, |context| { + let mut parser = KeyframeDeclarationParser { + context: &context, + declarations, + }; + let mut iter = RuleBodyParser::new(input, &mut parser); + while let Some(declaration) = iter.next() { + match declaration { + Ok(()) => { + block.extend(iter.parser.declarations.drain(), Importance::Normal); + }, + Err((error, slice)) => { + iter.parser.declarations.clear(); + let location = error.location; + let error = + ContextualParseError::UnsupportedKeyframePropertyDeclaration( + slice, error, + ); + context.log_css_error(location, error); + }, + } + // `parse_important` is not called here, `!important` is not allowed in keyframe blocks. + } + }); + Ok(Arc::new(self.shared_lock.wrap(Keyframe { + selector, + block: Arc::new(self.shared_lock.wrap(block)), + source_location: start.source_location(), + }))) + } +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, Arc<Locked<Keyframe>>, StyleParseErrorKind<'i>> + for KeyframeListParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + true + } + fn parse_declarations(&self) -> bool { + false + } +} + +struct KeyframeDeclarationParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + declarations: &'a mut SourcePropertyDeclaration, +} + +/// Default methods reject all at rules. +impl<'a, 'b, 'i> AtRuleParser<'i> for KeyframeDeclarationParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> QualifiedRuleParser<'i> for KeyframeDeclarationParser<'a, 'b> { + type Prelude = (); + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for KeyframeDeclarationParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let id = match PropertyId::parse(&name, self.context) { + Ok(id) => id, + Err(()) => { + return Err(input.new_custom_error(StyleParseErrorKind::UnknownProperty(name))); + }, + }; + + // TODO(emilio): Shouldn't this use parse_entirely? + PropertyDeclaration::parse_into(self.declarations, id, self.context, input)?; + + // In case there is still unparsed text in the declaration, we should + // roll back. + input.expect_exhausted()?; + + Ok(()) + } +} + +impl<'a, 'b, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> + for KeyframeDeclarationParser<'a, 'b> +{ + fn parse_qualified(&self) -> bool { + false + } + fn parse_declarations(&self) -> bool { + true + } +} diff --git a/servo/components/style/stylesheets/layer_rule.rs b/servo/components/style/stylesheets/layer_rule.rs new file mode 100644 index 0000000000..3ebe6bb34f --- /dev/null +++ b/servo/components/style/stylesheets/layer_rule.rs @@ -0,0 +1,228 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A [`@layer`][layer] rule. +//! +//! [layer]: https://drafts.csswg.org/css-cascade-5/#layering + +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::values::AtomIdent; + +use super::CssRules; + +use cssparser::{Parser, SourceLocation, Token}; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// The order of a given layer. We use 16 bits so that we can pack LayerOrder +/// and CascadeLevel in a single 32-bit struct. If we need more bits we can go +/// back to packing CascadeLevel in a single byte as we did before. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, PartialOrd, Ord)] +pub struct LayerOrder(u16); + +impl LayerOrder { + /// The order of the root layer. + pub const fn root() -> Self { + Self(std::u16::MAX - 1) + } + + /// The order of the style attribute layer. + pub const fn style_attribute() -> Self { + Self(std::u16::MAX) + } + + /// Returns whether this layer is for the style attribute, which behaves + /// differently in terms of !important, see + /// https://github.com/w3c/csswg-drafts/issues/6872 + /// + /// (This is a bit silly, mind-you, but it's needed so that revert-layer + /// behaves correctly). + #[inline] + pub fn is_style_attribute_layer(&self) -> bool { + *self == Self::style_attribute() + } + + /// The first cascade layer order. + pub const fn first() -> Self { + Self(0) + } + + /// Increment the cascade layer order. + #[inline] + pub fn inc(&mut self) { + if self.0 != std::u16::MAX - 1 { + self.0 += 1; + } + } +} + +/// A `<layer-name>`: https://drafts.csswg.org/css-cascade-5/#typedef-layer-name +#[derive(Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToShmem)] +pub struct LayerName(pub SmallVec<[AtomIdent; 1]>); + +impl LayerName { + /// Returns an empty layer name (which isn't a valid final state, so caller + /// is responsible to fill up the name before use). + pub fn new_empty() -> Self { + Self(Default::default()) + } + + /// Returns a synthesized name for an anonymous layer. + pub fn new_anonymous() -> Self { + use std::sync::atomic::{AtomicUsize, Ordering}; + static NEXT_ANONYMOUS_LAYER_NAME: AtomicUsize = AtomicUsize::new(0); + + let mut name = SmallVec::new(); + let next_id = NEXT_ANONYMOUS_LAYER_NAME.fetch_add(1, Ordering::Relaxed); + // The parens don't _technically_ prevent conflicts with authors, as + // authors could write escaped parens as part of the identifier, I + // think, but highly reduces the possibility. + name.push(AtomIdent::from(&*format!("-moz-anon-layer({})", next_id))); + + LayerName(name) + } + + /// Returns the names of the layers. That is, for a layer like `foo.bar`, + /// it'd return [foo, bar]. + pub fn layer_names(&self) -> &[AtomIdent] { + &self.0 + } +} + +impl Parse for LayerName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = SmallVec::new(); + result.push(AtomIdent::from(&**input.expect_ident()?)); + loop { + let next_name = input.try_parse(|input| -> Result<AtomIdent, ParseError<'i>> { + match input.next_including_whitespace()? { + Token::Delim('.') => {}, + other => { + let t = other.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + } + + let name = match input.next_including_whitespace()? { + Token::Ident(ref ident) => ident, + other => { + let t = other.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + }; + + Ok(AtomIdent::from(&**name)) + }); + + match next_name { + Ok(name) => result.push(name), + Err(..) => break, + } + } + Ok(LayerName(result)) + } +} + +impl ToCss for LayerName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let mut first = true; + for name in self.0.iter() { + if !first { + dest.write_char('.')?; + } + first = false; + name.to_css(dest)?; + } + Ok(()) + } +} + +#[derive(Debug, ToShmem)] +/// A block `@layer <name>? { ... }` +/// https://drafts.csswg.org/css-cascade-5/#layer-block +pub struct LayerBlockRule { + /// The layer name, or `None` if anonymous. + pub name: Option<LayerName>, + /// The nested rules. + pub rules: Arc<Locked<CssRules>>, + /// The source position where this rule was found. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for LayerBlockRule { + fn to_css( + &self, + guard: &SharedRwLockReadGuard, + dest: &mut crate::str::CssStringWriter, + ) -> fmt::Result { + dest.write_str("@layer")?; + if let Some(ref name) = self.name { + dest.write_char(' ')?; + name.to_css(&mut CssWriter::new(dest))?; + } + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +impl DeepCloneWithLock for LayerBlockRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + Self { + name: self.name.clone(), + rules: Arc::new( + lock.wrap( + self.rules + .read_with(guard) + .deep_clone_with_lock(lock, guard, params), + ), + ), + source_location: self.source_location.clone(), + } + } +} + +/// A statement `@layer <name>, <name>, <name>;` +/// +/// https://drafts.csswg.org/css-cascade-5/#layer-empty +#[derive(Clone, Debug, ToShmem)] +pub struct LayerStatementRule { + /// The list of layers to sort. + pub names: Vec<LayerName>, + /// The source position where this rule was found. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for LayerStatementRule { + fn to_css( + &self, + _: &SharedRwLockReadGuard, + dest: &mut crate::str::CssStringWriter, + ) -> fmt::Result { + let mut writer = CssWriter::new(dest); + writer.write_str("@layer ")?; + let mut first = true; + for name in &*self.names { + if !first { + writer.write_str(", ")?; + } + first = false; + name.to_css(&mut writer)?; + } + writer.write_char(';') + } +} diff --git a/servo/components/style/stylesheets/loader.rs b/servo/components/style/stylesheets/loader.rs new file mode 100644 index 0000000000..f987cf9597 --- /dev/null +++ b/servo/components/style/stylesheets/loader.rs @@ -0,0 +1,31 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The stylesheet loader is the abstraction used to trigger network requests +//! for `@import` rules. + +use crate::media_queries::MediaList; +use crate::parser::ParserContext; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::stylesheets::import_rule::{ImportLayer, ImportRule, ImportSupportsCondition}; +use crate::values::CssUrl; +use cssparser::SourceLocation; +use servo_arc::Arc; + +/// The stylesheet loader is the abstraction used to trigger network requests +/// for `@import` rules. +pub trait StylesheetLoader { + /// Request a stylesheet after parsing a given `@import` rule, and return + /// the constructed `@import` rule. + fn request_stylesheet( + &self, + url: CssUrl, + location: SourceLocation, + context: &ParserContext, + lock: &SharedRwLock, + media: Arc<Locked<MediaList>>, + supports: Option<ImportSupportsCondition>, + layer: ImportLayer, + ) -> Arc<Locked<ImportRule>>; +} diff --git a/servo/components/style/stylesheets/margin_rule.rs b/servo/components/style/stylesheets/margin_rule.rs new file mode 100644 index 0000000000..ab46283151 --- /dev/null +++ b/servo/components/style/stylesheets/margin_rule.rs @@ -0,0 +1,167 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A [`@margin`][margin] rule. +//! +//! [margin]: https://drafts.csswg.org/css-page-3/#margin-boxes + +use crate::properties::PropertyDeclarationBlock; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use cssparser::SourceLocation; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; + +macro_rules! margin_rule_types { + ($($(#[$($meta:tt)+])* $id:ident => $val:literal,)+) => { + /// [`@margin`][margin] rule names. + /// + /// https://drafts.csswg.org/css-page-3/#margin-at-rules + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] + #[repr(u8)] + pub enum MarginRuleType { + $($(#[$($meta)+])* $id,)+ + } + + impl MarginRuleType { + #[inline] + fn to_str(&self) -> &'static str { + match *self { + $(MarginRuleType::$id => concat!('@', $val),)+ + } + } + /// Matches the rule type for this name. This does not expect a + /// leading '@'. + pub fn match_name(name: &str) -> Option<Self> { + Some(match_ignore_ascii_case! { name, + $( $val => MarginRuleType::$id, )+ + _ => return None, + }) + } + } + } +} + +margin_rule_types! { + /// [`@top-left-corner`][top-left-corner] margin rule + /// + /// [top-left-corner] https://drafts.csswg.org/css-page-3/#top-left-corner-box-def + TopLeftCorner => "top-left-corner", + /// [`@top-left`][top-left] margin rule + /// + /// [top-left] https://drafts.csswg.org/css-page-3/#top-left-box-def + TopLeft => "top-left", + /// [`@top-center`][top-center] margin rule + /// + /// [top-center] https://drafts.csswg.org/css-page-3/#top-center-box-def + TopCenter => "top-center", + /// [`@top-right`][top-right] margin rule + /// + /// [top-right] https://drafts.csswg.org/css-page-3/#top-right-box-def + TopRight => "top-right", + /// [`@top-right-corner`][top-right-corner] margin rule + /// + /// [top-right-corner] https://drafts.csswg.org/css-page-3/#top-right-corner-box-def + TopRightCorner => "top-right-corner", + /// [`@bottom-left-corner`][bottom-left-corner] margin rule + /// + /// [bottom-left-corner] https://drafts.csswg.org/css-page-3/#bottom-left-corner-box-def + BottomLeftCorner => "bottom-left-corner", + /// [`@bottom-left`][bottom-left] margin rule + /// + /// [bottom-left] https://drafts.csswg.org/css-page-3/#bottom-left-box-def + BottomLeft => "bottom-left", + /// [`@bottom-center`][bottom-center] margin rule + /// + /// [bottom-center] https://drafts.csswg.org/css-page-3/#bottom-center-box-def + BottomCenter => "bottom-center", + /// [`@bottom-right`][bottom-right] margin rule + /// + /// [bottom-right] https://drafts.csswg.org/css-page-3/#bottom-right-box-def + BottomRight => "bottom-right", + /// [`@bottom-right-corner`][bottom-right-corner] margin rule + /// + /// [bottom-right-corner] https://drafts.csswg.org/css-page-3/#bottom-right-corner-box-def + BottomRightCorner => "bottom-right-corner", + /// [`@left-top`][left-top] margin rule + /// + /// [left-top] https://drafts.csswg.org/css-page-3/#left-top-box-def + LeftTop => "left-top", + /// [`@left-middle`][left-middle] margin rule + /// + /// [left-middle] https://drafts.csswg.org/css-page-3/#left-middle-box-def + LeftMiddle => "left-middle", + /// [`@left-bottom`][left-bottom] margin rule + /// + /// [left-bottom] https://drafts.csswg.org/css-page-3/#left-bottom-box-def + LeftBottom => "left-bottom", + /// [`@right-top`][right-top] margin rule + /// + /// [right-top] https://drafts.csswg.org/css-page-3/#right-top-box-def + RightTop => "right-top", + /// [`@right-middle`][right-middle] margin rule + /// + /// [right-middle] https://drafts.csswg.org/css-page-3/#right-middle-box-def + RightMiddle => "right-middle", + /// [`@right-bottom`][right-bottom] margin rule + /// + /// [right-bottom] https://drafts.csswg.org/css-page-3/#right-bottom-box-def + RightBottom => "right-bottom", +} + +/// A [`@margin`][margin] rule. +/// +/// [margin]: https://drafts.csswg.org/css-page-3/#margin-at-rules +#[derive(Clone, Debug, ToShmem)] +pub struct MarginRule { + /// Type of this margin rule. + pub rule_type: MarginRuleType, + /// The declaration block this margin rule contains. + pub block: Arc<Locked<PropertyDeclarationBlock>>, + /// The source position this rule was found at. + pub source_location: SourceLocation, +} + +impl MarginRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + // Measurement of other fields may be added later. + self.block.unconditional_shallow_size_of(ops) + + self.block.read_with(guard).size_of(ops) + } +} + +impl ToCssWithGuard for MarginRule { + /// Serialization of a margin-rule is not specced, this is adapted from how + /// page-rules and style-rules are serialized. + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str(self.rule_type.to_str())?; + dest.write_str(" { ")?; + let declaration_block = self.block.read_with(guard); + declaration_block.to_css(dest)?; + if !declaration_block.declarations().is_empty() { + dest.write_char(' ')?; + } + dest.write_char('}') + } +} + +impl DeepCloneWithLock for MarginRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + _params: &DeepCloneParams, + ) -> Self { + MarginRule { + rule_type: self.rule_type, + block: Arc::new(lock.wrap(self.block.read_with(&guard).clone())), + source_location: self.source_location.clone(), + } + } +} diff --git a/servo/components/style/stylesheets/media_rule.rs b/servo/components/style/stylesheets/media_rule.rs new file mode 100644 index 0000000000..cde60a16bf --- /dev/null +++ b/servo/components/style/stylesheets/media_rule.rs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An [`@media`][media] rule. +//! +//! [media]: https://drafts.csswg.org/css-conditional/#at-ruledef-media + +use crate::media_queries::MediaList; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use cssparser::SourceLocation; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// An [`@media`][media] rule. +/// +/// [media]: https://drafts.csswg.org/css-conditional/#at-ruledef-media +#[derive(Debug, ToShmem)] +pub struct MediaRule { + /// The list of media queries used by this media rule. + pub media_queries: Arc<Locked<MediaList>>, + /// The nested rules to this media rule. + pub rules: Arc<Locked<CssRules>>, + /// The source position where this media rule was found. + pub source_location: SourceLocation, +} + +impl MediaRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + } +} + +impl ToCssWithGuard for MediaRule { + // Serialization of MediaRule is not specced. + // https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSMediaRule + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@media ")?; + self.media_queries + .read_with(guard) + .to_css(&mut CssWriter::new(dest))?; + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +impl DeepCloneWithLock for MediaRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let media_queries = self.media_queries.read_with(guard); + let rules = self.rules.read_with(guard); + MediaRule { + media_queries: Arc::new(lock.wrap(media_queries.clone())), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + source_location: self.source_location.clone(), + } + } +} diff --git a/servo/components/style/stylesheets/mod.rs b/servo/components/style/stylesheets/mod.rs new file mode 100644 index 0000000000..2bf75565de --- /dev/null +++ b/servo/components/style/stylesheets/mod.rs @@ -0,0 +1,597 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Style sheets and their CSS rules. + +pub mod container_rule; +mod counter_style_rule; +mod document_rule; +mod font_face_rule; +pub mod font_feature_values_rule; +pub mod font_palette_values_rule; +pub mod import_rule; +pub mod keyframes_rule; +pub mod layer_rule; +mod loader; +mod media_rule; +mod namespace_rule; +pub mod origin; +mod page_rule; +mod margin_rule; +mod property_rule; +mod rule_list; +mod rule_parser; +mod rules_iterator; +mod style_rule; +mod stylesheet; +pub mod supports_rule; + +#[cfg(feature = "gecko")] +use crate::gecko_bindings::sugar::refptr::RefCounted; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::{bindings, structs}; +use crate::parser::ParserContext; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use cssparser::{parse_one_rule, Parser, ParserInput}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use std::borrow::Cow; +use std::fmt; +#[cfg(feature = "gecko")] +use std::mem::{self, ManuallyDrop}; +use style_traits::ParsingMode; +#[cfg(feature = "gecko")] +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +pub use self::container_rule::ContainerRule; +pub use self::counter_style_rule::CounterStyleRule; +pub use self::document_rule::DocumentRule; +pub use self::font_face_rule::FontFaceRule; +pub use self::font_feature_values_rule::FontFeatureValuesRule; +pub use self::font_palette_values_rule::FontPaletteValuesRule; +pub use self::import_rule::ImportRule; +pub use self::keyframes_rule::KeyframesRule; +pub use self::layer_rule::{LayerBlockRule, LayerStatementRule}; +pub use self::loader::StylesheetLoader; +pub use self::margin_rule::{MarginRule, MarginRuleType}; +pub use self::media_rule::MediaRule; +pub use self::namespace_rule::NamespaceRule; +pub use self::origin::{Origin, OriginSet, OriginSetIterator, PerOrigin, PerOriginIter}; +pub use self::page_rule::{PagePseudoClassFlags, PageRule, PageSelector, PageSelectors}; +pub use self::property_rule::PropertyRule; +pub use self::rule_list::{CssRules, CssRulesHelpers}; +pub use self::rule_parser::{InsertRuleContext, State, TopLevelRuleParser}; +pub use self::rules_iterator::{AllRules, EffectiveRules}; +pub use self::rules_iterator::{ + EffectiveRulesIterator, NestedRuleIterationCondition, RulesIterator, +}; +pub use self::style_rule::StyleRule; +pub use self::stylesheet::{AllowImportRules, SanitizationData, SanitizationKind}; +pub use self::stylesheet::{DocumentStyleSheet, Namespaces, Stylesheet}; +pub use self::stylesheet::{StylesheetContents, StylesheetInDocument, UserAgentStylesheets}; +pub use self::supports_rule::SupportsRule; + +/// The CORS mode used for a CSS load. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, ToShmem)] +pub enum CorsMode { + /// No CORS mode, so cross-origin loads can be done. + None, + /// Anonymous CORS request. + Anonymous, +} + +/// Extra data that the backend may need to resolve url values. +/// +/// If the usize's lowest bit is 0, then this is a strong reference to a +/// structs::URLExtraData object. +/// +/// Otherwise, shifting the usize's bits the right by one gives the +/// UserAgentStyleSheetID value corresponding to the style sheet whose +/// URLExtraData this is, which is stored in URLExtraData_sShared. We don't +/// hold a strong reference to that object from here, but we rely on that +/// array's objects being held alive until shutdown. +/// +/// We use this packed representation rather than an enum so that +/// `from_ptr_ref` can work. +#[cfg(feature = "gecko")] +// Although deriving MallocSizeOf means it always returns 0, that is fine because UrlExtraData +// objects are reference-counted. +#[derive(MallocSizeOf, PartialEq)] +#[repr(C)] +pub struct UrlExtraData(usize); + +/// Extra data that the backend may need to resolve url values. +#[cfg(not(feature = "gecko"))] +pub type UrlExtraData = ::servo_url::ServoUrl; + +#[cfg(feature = "gecko")] +impl Clone for UrlExtraData { + fn clone(&self) -> UrlExtraData { + UrlExtraData::new(self.ptr()) + } +} + +#[cfg(feature = "gecko")] +impl Drop for UrlExtraData { + fn drop(&mut self) { + // No need to release when we have an index into URLExtraData_sShared. + if self.0 & 1 == 0 { + unsafe { + self.as_ref().release(); + } + } + } +} + +#[cfg(feature = "gecko")] +impl ToShmem for UrlExtraData { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + if self.0 & 1 == 0 { + let shared_extra_datas = unsafe { &structs::URLExtraData_sShared }; + let self_ptr = self.as_ref() as *const _ as *mut _; + let sheet_id = shared_extra_datas + .iter() + .position(|r| r.mRawPtr == self_ptr); + let sheet_id = match sheet_id { + Some(id) => id, + None => { + return Err(String::from( + "ToShmem failed for UrlExtraData: expected sheet's URLExtraData to be in \ + URLExtraData::sShared", + )); + }, + }; + Ok(ManuallyDrop::new(UrlExtraData((sheet_id << 1) | 1))) + } else { + Ok(ManuallyDrop::new(UrlExtraData(self.0))) + } + } +} + +#[cfg(feature = "gecko")] +impl UrlExtraData { + /// Create a new UrlExtraData wrapping a pointer to the specified Gecko + /// URLExtraData object. + pub fn new(ptr: *mut structs::URLExtraData) -> UrlExtraData { + unsafe { + (*ptr).addref(); + } + UrlExtraData(ptr as usize) + } + + /// True if this URL scheme is chrome. + #[inline] + pub fn chrome_rules_enabled(&self) -> bool { + self.as_ref().mChromeRulesEnabled + } + + /// Create a reference to this `UrlExtraData` from a reference to pointer. + /// + /// The pointer must be valid and non null. + /// + /// This method doesn't touch refcount. + #[inline] + pub unsafe fn from_ptr_ref(ptr: &*mut structs::URLExtraData) -> &Self { + mem::transmute(ptr) + } + + /// Returns a pointer to the Gecko URLExtraData object. + pub fn ptr(&self) -> *mut structs::URLExtraData { + if self.0 & 1 == 0 { + self.0 as *mut structs::URLExtraData + } else { + unsafe { + let sheet_id = self.0 >> 1; + structs::URLExtraData_sShared[sheet_id].mRawPtr + } + } + } + + fn as_ref(&self) -> &structs::URLExtraData { + unsafe { &*(self.ptr() as *const structs::URLExtraData) } + } +} + +#[cfg(feature = "gecko")] +impl fmt::Debug for UrlExtraData { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + macro_rules! define_debug_struct { + ($struct_name:ident, $gecko_class:ident, $debug_fn:ident) => { + struct $struct_name(*mut structs::$gecko_class); + impl fmt::Debug for $struct_name { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + use nsstring::nsCString; + let mut spec = nsCString::new(); + unsafe { + bindings::$debug_fn(self.0, &mut spec); + } + spec.fmt(formatter) + } + } + }; + } + + define_debug_struct!(DebugURI, nsIURI, Gecko_nsIURI_Debug); + define_debug_struct!( + DebugReferrerInfo, + nsIReferrerInfo, + Gecko_nsIReferrerInfo_Debug + ); + + formatter + .debug_struct("URLExtraData") + .field("chrome_rules_enabled", &self.chrome_rules_enabled()) + .field("base", &DebugURI(self.as_ref().mBaseURI.raw())) + .field( + "referrer", + &DebugReferrerInfo(self.as_ref().mReferrerInfo.raw()), + ) + .finish() + } +} + +// XXX We probably need to figure out whether we should mark Eq here. +// It is currently marked so because properties::UnparsedValue wants Eq. +#[cfg(feature = "gecko")] +impl Eq for UrlExtraData {} + +/// A CSS rule. +/// +/// TODO(emilio): Lots of spec links should be around. +#[derive(Clone, Debug, ToShmem)] +#[allow(missing_docs)] +pub enum CssRule { + Style(Arc<Locked<StyleRule>>), + // No Charset here, CSSCharsetRule has been removed from CSSOM + // https://drafts.csswg.org/cssom/#changes-from-5-december-2013 + Namespace(Arc<NamespaceRule>), + Import(Arc<Locked<ImportRule>>), + Media(Arc<MediaRule>), + Container(Arc<ContainerRule>), + FontFace(Arc<Locked<FontFaceRule>>), + FontFeatureValues(Arc<FontFeatureValuesRule>), + FontPaletteValues(Arc<FontPaletteValuesRule>), + CounterStyle(Arc<Locked<CounterStyleRule>>), + Keyframes(Arc<Locked<KeyframesRule>>), + Margin(Arc<MarginRule>), + Supports(Arc<SupportsRule>), + Page(Arc<Locked<PageRule>>), + Property(Arc<PropertyRule>), + Document(Arc<DocumentRule>), + LayerBlock(Arc<LayerBlockRule>), + LayerStatement(Arc<LayerStatementRule>), +} + +impl CssRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + match *self { + // Not all fields are currently fully measured. Extra measurement + // may be added later. + CssRule::Namespace(_) => 0, + + // We don't need to measure ImportRule::stylesheet because we measure + // it on the C++ side in the child list of the ServoStyleSheet. + CssRule::Import(_) => 0, + + CssRule::Style(ref lock) => { + lock.unconditional_shallow_size_of(ops) + lock.read_with(guard).size_of(guard, ops) + }, + CssRule::Media(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + CssRule::Container(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + CssRule::FontFace(_) => 0, + CssRule::FontFeatureValues(_) => 0, + CssRule::FontPaletteValues(_) => 0, + CssRule::CounterStyle(_) => 0, + CssRule::Keyframes(_) => 0, + CssRule::Margin(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + CssRule::Supports(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + CssRule::Page(ref lock) => { + lock.unconditional_shallow_size_of(ops) + lock.read_with(guard).size_of(guard, ops) + }, + CssRule::Property(ref rule) => { + rule.unconditional_shallow_size_of(ops) + rule.size_of(guard, ops) + }, + CssRule::Document(ref arc) => { + arc.unconditional_shallow_size_of(ops) + arc.size_of(guard, ops) + }, + // TODO(emilio): Add memory reporting for these rules. + CssRule::LayerBlock(_) | CssRule::LayerStatement(_) => 0, + } + } +} + +/// https://drafts.csswg.org/cssom-1/#dom-cssrule-type +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, PartialEq)] +#[repr(u8)] +pub enum CssRuleType { + // https://drafts.csswg.org/cssom/#the-cssrule-interface + Style = 1, + // Charset = 2, // Historical + Import = 3, + Media = 4, + FontFace = 5, + Page = 6, + // https://drafts.csswg.org/css-animations-1/#interface-cssrule-idl + Keyframes = 7, + Keyframe = 8, + // https://drafts.csswg.org/cssom/#the-cssrule-interface + Margin = 9, + Namespace = 10, + // https://drafts.csswg.org/css-counter-styles-3/#extentions-to-cssrule-interface + CounterStyle = 11, + // https://drafts.csswg.org/css-conditional-3/#extentions-to-cssrule-interface + Supports = 12, + // https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#extentions-to-cssrule-interface + Document = 13, + // https://drafts.csswg.org/css-fonts/#om-fontfeaturevalues + FontFeatureValues = 14, + // After viewport, all rules should return 0 from the API, but we still need + // a constant somewhere. + LayerBlock = 16, + LayerStatement = 17, + Container = 18, + FontPaletteValues = 19, + // 20 is an arbitrary number to use for Property. + Property = 20, +} + +impl CssRuleType { + /// Returns a bit that identifies this rule type. + #[inline] + pub const fn bit(self) -> u32 { + 1 << self as u32 + } +} + +/// Set of rule types. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct CssRuleTypes(u32); + +impl From<CssRuleType> for CssRuleTypes { + fn from(ty: CssRuleType) -> Self { + Self(ty.bit()) + } +} + +impl CssRuleTypes { + /// Returns whether the rule is in the current set. + #[inline] + pub fn contains(self, ty: CssRuleType) -> bool { + self.0 & ty.bit() != 0 + } + + /// Returns all the rules specified in the set. + #[inline] + pub fn bits(self) -> u32 { + self.0 + } + + /// Creates a raw CssRuleTypes bitfield. + #[inline] + pub fn from_bits(bits: u32) -> Self { + Self(bits) + } + + /// Returns whether the rule set is empty. + #[inline] + pub fn is_empty(self) -> bool { + self.0 == 0 + } + + /// Inserts a rule type into the set. + #[inline] + pub fn insert(&mut self, ty: CssRuleType) { + self.0 |= ty.bit() + } + + /// Returns whether any of the types intersect. + #[inline] + pub fn intersects(self, other: Self) -> bool { + self.0 & other.0 != 0 + } +} + +#[allow(missing_docs)] +pub enum RulesMutateError { + Syntax, + IndexSize, + HierarchyRequest, + InvalidState, +} + +impl CssRule { + /// Returns the CSSOM rule type of this rule. + pub fn rule_type(&self) -> CssRuleType { + match *self { + CssRule::Style(_) => CssRuleType::Style, + CssRule::Import(_) => CssRuleType::Import, + CssRule::Media(_) => CssRuleType::Media, + CssRule::FontFace(_) => CssRuleType::FontFace, + CssRule::FontFeatureValues(_) => CssRuleType::FontFeatureValues, + CssRule::FontPaletteValues(_) => CssRuleType::FontPaletteValues, + CssRule::CounterStyle(_) => CssRuleType::CounterStyle, + CssRule::Keyframes(_) => CssRuleType::Keyframes, + CssRule::Margin(_) => CssRuleType::Margin, + CssRule::Namespace(_) => CssRuleType::Namespace, + CssRule::Supports(_) => CssRuleType::Supports, + CssRule::Page(_) => CssRuleType::Page, + CssRule::Property(_) => CssRuleType::Property, + CssRule::Document(_) => CssRuleType::Document, + CssRule::LayerBlock(_) => CssRuleType::LayerBlock, + CssRule::LayerStatement(_) => CssRuleType::LayerStatement, + CssRule::Container(_) => CssRuleType::Container, + } + } + + /// Parse a CSS rule. + /// + /// Returns a parsed CSS rule and the final state of the parser. + /// + /// Input state is None for a nested rule + pub fn parse( + css: &str, + insert_rule_context: InsertRuleContext, + parent_stylesheet_contents: &StylesheetContents, + shared_lock: &SharedRwLock, + loader: Option<&dyn StylesheetLoader>, + allow_import_rules: AllowImportRules, + ) -> Result<Self, RulesMutateError> { + let url_data = parent_stylesheet_contents.url_data.read(); + let namespaces = parent_stylesheet_contents.namespaces.read(); + let mut context = ParserContext::new( + parent_stylesheet_contents.origin, + &url_data, + None, + ParsingMode::DEFAULT, + parent_stylesheet_contents.quirks_mode, + Cow::Borrowed(&*namespaces), + None, + None, + ); + context.rule_types = insert_rule_context.containing_rule_types; + + let state = if !insert_rule_context.containing_rule_types.is_empty() { + State::Body + } else if insert_rule_context.index == 0 { + State::Start + } else { + let index = insert_rule_context.index; + insert_rule_context.max_rule_state_at_index(index - 1) + }; + + let mut input = ParserInput::new(css); + let mut input = Parser::new(&mut input); + + // nested rules are in the body state + let mut rule_parser = TopLevelRuleParser { + context, + shared_lock: &shared_lock, + loader, + state, + dom_error: None, + insert_rule_context: Some(insert_rule_context), + allow_import_rules, + declaration_parser_state: Default::default(), + error_reporting_state: Default::default(), + rules: Default::default(), + }; + + match parse_one_rule(&mut input, &mut rule_parser) { + Ok(_) => Ok(rule_parser.rules.pop().unwrap()), + Err(_) => Err(rule_parser.dom_error.unwrap_or(RulesMutateError::Syntax)), + } + } +} + +impl DeepCloneWithLock for CssRule { + /// Deep clones this CssRule. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> CssRule { + match *self { + CssRule::Namespace(ref arc) => CssRule::Namespace(arc.clone()), + CssRule::Import(ref arc) => { + let rule = arc + .read_with(guard) + .deep_clone_with_lock(lock, guard, params); + CssRule::Import(Arc::new(lock.wrap(rule))) + }, + CssRule::Style(ref arc) => { + let rule = arc.read_with(guard); + CssRule::Style(Arc::new( + lock.wrap(rule.deep_clone_with_lock(lock, guard, params)), + )) + }, + CssRule::Container(ref arc) => { + CssRule::Container(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::Media(ref arc) => { + CssRule::Media(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::FontFace(ref arc) => { + let rule = arc.read_with(guard); + CssRule::FontFace(Arc::new(lock.wrap(rule.clone()))) + }, + CssRule::FontFeatureValues(ref arc) => CssRule::FontFeatureValues(arc.clone()), + CssRule::FontPaletteValues(ref arc) => CssRule::FontPaletteValues(arc.clone()), + CssRule::CounterStyle(ref arc) => { + let rule = arc.read_with(guard); + CssRule::CounterStyle(Arc::new(lock.wrap(rule.clone()))) + }, + CssRule::Keyframes(ref arc) => { + let rule = arc.read_with(guard); + CssRule::Keyframes(Arc::new( + lock.wrap(rule.deep_clone_with_lock(lock, guard, params)), + )) + }, + CssRule::Margin(ref arc) => { + CssRule::Margin(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::Supports(ref arc) => { + CssRule::Supports(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::Page(ref arc) => { + let rule = arc.read_with(guard); + CssRule::Page(Arc::new( + lock.wrap(rule.deep_clone_with_lock(lock, guard, params)), + )) + }, + CssRule::Property(ref arc) => { + // @property rules are immutable, so we don't need any of the `Locked` + // shenanigans, actually, and can just share the rule. + CssRule::Property(arc.clone()) + }, + CssRule::Document(ref arc) => { + CssRule::Document(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + CssRule::LayerStatement(ref arc) => CssRule::LayerStatement(arc.clone()), + CssRule::LayerBlock(ref arc) => { + CssRule::LayerBlock(Arc::new(arc.deep_clone_with_lock(lock, guard, params))) + }, + } + } +} + +impl ToCssWithGuard for CssRule { + // https://drafts.csswg.org/cssom/#serialize-a-css-rule + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + match *self { + CssRule::Namespace(ref rule) => rule.to_css(guard, dest), + CssRule::Import(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Style(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::FontFace(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::FontFeatureValues(ref rule) => rule.to_css(guard, dest), + CssRule::FontPaletteValues(ref rule) => rule.to_css(guard, dest), + CssRule::CounterStyle(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Keyframes(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Margin(ref rule) => rule.to_css(guard, dest), + CssRule::Media(ref rule) => rule.to_css(guard, dest), + CssRule::Supports(ref rule) => rule.to_css(guard, dest), + CssRule::Page(ref lock) => lock.read_with(guard).to_css(guard, dest), + CssRule::Property(ref rule) => rule.to_css(guard, dest), + CssRule::Document(ref rule) => rule.to_css(guard, dest), + CssRule::LayerBlock(ref rule) => rule.to_css(guard, dest), + CssRule::LayerStatement(ref rule) => rule.to_css(guard, dest), + CssRule::Container(ref rule) => rule.to_css(guard, dest), + } + } +} diff --git a/servo/components/style/stylesheets/namespace_rule.rs b/servo/components/style/stylesheets/namespace_rule.rs new file mode 100644 index 0000000000..ad980b70a8 --- /dev/null +++ b/servo/components/style/stylesheets/namespace_rule.rs @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! The `@namespace` at-rule. + +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::{Namespace, Prefix}; +use cssparser::SourceLocation; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A `@namespace` rule. +#[derive(Clone, Debug, PartialEq, ToShmem)] +#[allow(missing_docs)] +pub struct NamespaceRule { + /// The namespace prefix, and `None` if it's the default Namespace + pub prefix: Option<Prefix>, + /// The actual namespace url. + pub url: Namespace, + /// The source location this rule was found at. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for NamespaceRule { + // https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSNamespaceRule + fn to_css( + &self, + _guard: &SharedRwLockReadGuard, + dest_str: &mut CssStringWriter, + ) -> fmt::Result { + let mut dest = CssWriter::new(dest_str); + dest.write_str("@namespace ")?; + if let Some(ref prefix) = self.prefix { + prefix.to_css(&mut dest)?; + dest.write_char(' ')?; + } + dest.write_str("url(")?; + self.url.to_string().to_css(&mut dest)?; + dest.write_str(");") + } +} diff --git a/servo/components/style/stylesheets/origin.rs b/servo/components/style/stylesheets/origin.rs new file mode 100644 index 0000000000..76167f6d5c --- /dev/null +++ b/servo/components/style/stylesheets/origin.rs @@ -0,0 +1,248 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [CSS cascade origins](https://drafts.csswg.org/css-cascade/#cascading-origins). + +use std::marker::PhantomData; +use std::ops::BitOrAssign; + +/// Each style rule has an origin, which determines where it enters the cascade. +/// +/// <https://drafts.csswg.org/css-cascade/#cascading-origins> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem, PartialOrd, Ord)] +#[repr(u8)] +pub enum Origin { + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user-agent> + UserAgent = 0x1, + + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user> + User = 0x2, + + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-author> + Author = 0x4, +} + +impl Origin { + /// Returns an origin that goes in order for `index`. + /// + /// This is used for iterating across origins. + fn from_index(index: i8) -> Option<Self> { + Some(match index { + 0 => Origin::Author, + 1 => Origin::User, + 2 => Origin::UserAgent, + _ => return None, + }) + } + + fn to_index(self) -> i8 { + match self { + Origin::Author => 0, + Origin::User => 1, + Origin::UserAgent => 2, + } + } + + /// Returns an iterator from this origin, towards all the less specific + /// origins. So for `UserAgent`, it'd iterate through all origins. + #[inline] + pub fn following_including(self) -> OriginSetIterator { + OriginSetIterator { + set: OriginSet::ORIGIN_USER | OriginSet::ORIGIN_AUTHOR | OriginSet::ORIGIN_USER_AGENT, + cur: self.to_index(), + rev: true, + } + } +} + +/// A set of origins. This is equivalent to Gecko's OriginFlags. +#[derive(Clone, Copy, PartialEq, MallocSizeOf)] +pub struct OriginSet(u8); +bitflags! { + impl OriginSet: u8 { + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user-agent> + const ORIGIN_USER_AGENT = Origin::UserAgent as u8; + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-user> + const ORIGIN_USER = Origin::User as u8; + /// <https://drafts.csswg.org/css-cascade/#cascade-origin-author> + const ORIGIN_AUTHOR = Origin::Author as u8; + } +} + +impl OriginSet { + /// Returns an iterator over the origins present in this `OriginSet`. + /// + /// See the `OriginSet` documentation for information about the order + /// origins are iterated. + pub fn iter_origins(&self) -> OriginSetIterator { + OriginSetIterator { + set: *self, + cur: 0, + rev: false, + } + } +} + +impl From<Origin> for OriginSet { + fn from(origin: Origin) -> Self { + Self::from_bits_retain(origin as u8) + } +} + +impl BitOrAssign<Origin> for OriginSet { + fn bitor_assign(&mut self, origin: Origin) { + *self |= OriginSet::from(origin); + } +} + +/// Iterates over the origins present in an `OriginSet`, in order from +/// highest priority (author) to lower (user agent). +#[derive(Clone)] +pub struct OriginSetIterator { + set: OriginSet, + cur: i8, + rev: bool, +} + +impl Iterator for OriginSetIterator { + type Item = Origin; + + fn next(&mut self) -> Option<Origin> { + loop { + let origin = Origin::from_index(self.cur)?; + + if self.rev { + self.cur -= 1; + } else { + self.cur += 1; + } + + if self.set.contains(origin.into()) { + return Some(origin); + } + } + } +} + +/// An object that stores a `T` for each origin of the CSS cascade. +#[derive(Debug, Default, MallocSizeOf)] +pub struct PerOrigin<T> { + /// Data for `Origin::UserAgent`. + pub user_agent: T, + + /// Data for `Origin::User`. + pub user: T, + + /// Data for `Origin::Author`. + pub author: T, +} + +impl<T> PerOrigin<T> { + /// Returns a reference to the per-origin data for the specified origin. + #[inline] + pub fn borrow_for_origin(&self, origin: &Origin) -> &T { + match *origin { + Origin::UserAgent => &self.user_agent, + Origin::User => &self.user, + Origin::Author => &self.author, + } + } + + /// Returns a mutable reference to the per-origin data for the specified + /// origin. + #[inline] + pub fn borrow_mut_for_origin(&mut self, origin: &Origin) -> &mut T { + match *origin { + Origin::UserAgent => &mut self.user_agent, + Origin::User => &mut self.user, + Origin::Author => &mut self.author, + } + } + + /// Iterates over references to per-origin extra style data, from highest + /// level (author) to lowest (user agent). + pub fn iter_origins(&self) -> PerOriginIter<T> { + PerOriginIter { + data: &self, + cur: 0, + rev: false, + } + } + + /// Iterates over references to per-origin extra style data, from lowest + /// level (user agent) to highest (author). + pub fn iter_origins_rev(&self) -> PerOriginIter<T> { + PerOriginIter { + data: &self, + cur: 2, + rev: true, + } + } + + /// Iterates over mutable references to per-origin extra style data, from + /// highest level (author) to lowest (user agent). + pub fn iter_mut_origins(&mut self) -> PerOriginIterMut<T> { + PerOriginIterMut { + data: self, + cur: 0, + _marker: PhantomData, + } + } +} + +/// Iterator over `PerOrigin<T>`, from highest level (author) to lowest +/// (user agent). +/// +/// We rely on this specific order for correctly looking up @font-face, +/// @counter-style and @keyframes rules. +pub struct PerOriginIter<'a, T: 'a> { + data: &'a PerOrigin<T>, + cur: i8, + rev: bool, +} + +impl<'a, T> Iterator for PerOriginIter<'a, T> +where + T: 'a, +{ + type Item = (&'a T, Origin); + + fn next(&mut self) -> Option<Self::Item> { + let origin = Origin::from_index(self.cur)?; + + self.cur += if self.rev { -1 } else { 1 }; + + Some((self.data.borrow_for_origin(&origin), origin)) + } +} + +/// Like `PerOriginIter<T>`, but iterates over mutable references to the +/// per-origin data. +/// +/// We must use unsafe code here since it's not possible for the borrow +/// checker to know that we are safely returning a different reference +/// each time from `next()`. +pub struct PerOriginIterMut<'a, T: 'a> { + data: *mut PerOrigin<T>, + cur: i8, + _marker: PhantomData<&'a mut PerOrigin<T>>, +} + +impl<'a, T> Iterator for PerOriginIterMut<'a, T> +where + T: 'a, +{ + type Item = (&'a mut T, Origin); + + fn next(&mut self) -> Option<Self::Item> { + let origin = Origin::from_index(self.cur)?; + + self.cur += 1; + + Some(( + unsafe { (*self.data).borrow_mut_for_origin(&origin) }, + origin, + )) + } +} diff --git a/servo/components/style/stylesheets/page_rule.rs b/servo/components/style/stylesheets/page_rule.rs new file mode 100644 index 0000000000..a1618309a3 --- /dev/null +++ b/servo/components/style/stylesheets/page_rule.rs @@ -0,0 +1,366 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A [`@page`][page] rule. +//! +//! [page]: https://drafts.csswg.org/css2/page.html#page-box + +use crate::parser::{Parse, ParserContext}; +use crate::properties::PropertyDeclarationBlock; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::stylesheets::CssRules; +use crate::str::CssStringWriter; +use crate::values::{AtomIdent, CustomIdent}; +use cssparser::{Parser, SourceLocation, Token}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use servo_arc::Arc; +use smallvec::SmallVec; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +macro_rules! page_pseudo_classes { + ($($(#[$($meta:tt)+])* $id:ident => $val:literal,)+) => { + /// [`@page`][page] rule pseudo-classes. + /// + /// https://drafts.csswg.org/css-page-3/#page-selectors + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] + #[repr(u8)] + pub enum PagePseudoClass { + $($(#[$($meta)+])* $id,)+ + } + impl PagePseudoClass { + fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let loc = input.current_source_location(); + let colon = input.next_including_whitespace()?; + if *colon != Token::Colon { + return Err(loc.new_unexpected_token_error(colon.clone())); + } + + let ident = input.next_including_whitespace()?; + if let Token::Ident(s) = ident { + return match_ignore_ascii_case! { &**s, + $($val => Ok(PagePseudoClass::$id),)+ + _ => Err(loc.new_unexpected_token_error(Token::Ident(s.clone()))), + }; + } + Err(loc.new_unexpected_token_error(ident.clone())) + } + #[inline] + fn to_str(&self) -> &'static str { + match *self { + $(PagePseudoClass::$id => concat!(':', $val),)+ + } + } + } + } +} + +page_pseudo_classes! { + /// [`:first`][first] pseudo-class + /// + /// [first] https://drafts.csswg.org/css-page-3/#first-pseudo + First => "first", + /// [`:blank`][blank] pseudo-class + /// + /// [blank] https://drafts.csswg.org/css-page-3/#blank-pseudo + Blank => "blank", + /// [`:left`][left] pseudo-class + /// + /// [left]: https://drafts.csswg.org/css-page-3/#spread-pseudos + Left => "left", + /// [`:right`][right] pseudo-class + /// + /// [right]: https://drafts.csswg.org/css-page-3/#spread-pseudos + Right => "right", +} + +bitflags! { + /// Bit-flags for pseudo-class. This should only be used for querying if a + /// page-rule applies. + /// + /// https://drafts.csswg.org/css-page-3/#page-selectors + #[derive(Clone, Copy)] + #[repr(C)] + pub struct PagePseudoClassFlags : u8 { + /// No pseudo-classes + const NONE = 0; + /// Flag for PagePseudoClass::First + const FIRST = 1 << 0; + /// Flag for PagePseudoClass::Blank + const BLANK = 1 << 1; + /// Flag for PagePseudoClass::Left + const LEFT = 1 << 2; + /// Flag for PagePseudoClass::Right + const RIGHT = 1 << 3; + } +} + +impl PagePseudoClassFlags { + /// Creates a pseudo-class flags object with a single pseudo-class. + #[inline] + pub fn new(other: &PagePseudoClass) -> Self { + match *other { + PagePseudoClass::First => PagePseudoClassFlags::FIRST, + PagePseudoClass::Blank => PagePseudoClassFlags::BLANK, + PagePseudoClass::Left => PagePseudoClassFlags::LEFT, + PagePseudoClass::Right => PagePseudoClassFlags::RIGHT, + } + } + /// Checks if the given pseudo class applies to this set of flags. + #[inline] + pub fn contains_class(self, other: &PagePseudoClass) -> bool { + self.intersects(PagePseudoClassFlags::new(other)) + } +} + +type PagePseudoClasses = SmallVec<[PagePseudoClass; 4]>; + +/// Type of a single [`@page`][page selector] +/// +/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors +#[derive(Clone, Debug, MallocSizeOf, ToShmem)] +pub struct PageSelector { + /// Page name + /// + /// https://drafts.csswg.org/css-page-3/#page-type-selector + pub name: AtomIdent, + /// Pseudo-classes for [`@page`][page-selectors] + /// + /// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors + pub pseudos: PagePseudoClasses, +} + +impl PageSelector { + /// Checks if the ident matches a page-name's ident. + /// + /// This does not take pseudo selectors into account. + #[inline] + pub fn ident_matches(&self, other: &CustomIdent) -> bool { + self.name.0 == other.0 + } + + /// Checks that this selector matches the ident and all pseudo classes are + /// present in the provided flags. + #[inline] + pub fn matches(&self, name: &CustomIdent, flags: PagePseudoClassFlags) -> bool { + self.ident_matches(name) && self.flags_match(flags) + } + + /// Checks that all pseudo classes in this selector are present in the + /// provided flags. + /// + /// Equivalent to, but may be more efficient than: + /// + /// ``` + /// match_specificity(flags).is_some() + /// ``` + pub fn flags_match(&self, flags: PagePseudoClassFlags) -> bool { + self.pseudos.iter().all(|pc| flags.contains_class(pc)) + } + + /// Implements specificity calculation for a page selector given a set of + /// page pseudo-classes to match with. + /// If this selector includes any pseudo-classes that are not in the flags, + /// then this will return None. + /// + /// To fit the specificity calculation into a 32-bit value, this limits the + /// maximum count of :first and :blank to 32767, and the maximum count of + /// :left and :right to 65535. + /// + /// https://drafts.csswg.org/css-page-3/#cascading-and-page-context + pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> { + let mut g: usize = 0; + let mut h: usize = 0; + for pc in self.pseudos.iter() { + if !flags.contains_class(pc) { + return None; + } + match pc { + PagePseudoClass::First | PagePseudoClass::Blank => g += 1, + PagePseudoClass::Left | PagePseudoClass::Right => h += 1, + } + } + let h = h.min(0xFFFF) as u32; + let g = (g.min(0x7FFF) as u32) << 16; + let f = if self.name.0.is_empty() { + 0 + } else { + 0x80000000 + }; + Some(h + g + f) + } +} + +impl ToCss for PageSelector { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.name.to_css(dest)?; + for pc in self.pseudos.iter() { + dest.write_str(pc.to_str())?; + } + Ok(()) + } +} + +fn parse_page_name<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AtomIdent, ParseError<'i>> { + let s = input.expect_ident()?; + Ok(AtomIdent::from(&**s)) +} + +impl Parse for PageSelector { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let name = input + .try_parse(parse_page_name) + .unwrap_or(AtomIdent(atom!(""))); + let mut pseudos = PagePseudoClasses::default(); + while let Ok(pc) = input.try_parse(PagePseudoClass::parse) { + pseudos.push(pc); + } + Ok(PageSelector { name, pseudos }) + } +} + +/// A list of [`@page`][page selectors] +/// +/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors +#[derive(Clone, Debug, Default, MallocSizeOf, ToCss, ToShmem)] +#[css(comma)] +pub struct PageSelectors(#[css(iterable)] pub Box<[PageSelector]>); + +impl PageSelectors { + /// Creates a new PageSelectors from a Vec, as from parse_comma_separated + #[inline] + pub fn new(s: Vec<PageSelector>) -> Self { + PageSelectors(s.into()) + } + /// Returns true iff there are any page selectors + #[inline] + pub fn is_empty(&self) -> bool { + self.as_slice().is_empty() + } + /// Get the underlying PageSelector data as a slice + #[inline] + pub fn as_slice(&self) -> &[PageSelector] { + &*self.0 + } +} + +impl Parse for PageSelectors { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(PageSelectors::new(input.parse_comma_separated(|i| { + PageSelector::parse(context, i) + })?)) + } +} + +/// A [`@page`][page] rule. +/// +/// This implements only a limited subset of the CSS +/// 2.2 syntax. +/// +/// [page]: https://drafts.csswg.org/css2/page.html#page-box +/// [page-selectors]: https://drafts.csswg.org/css2/page.html#page-selectors +#[derive(Clone, Debug, ToShmem)] +pub struct PageRule { + /// Selectors of the page-rule + pub selectors: PageSelectors, + /// Nested rules. + pub rules: Arc<Locked<CssRules>>, + /// The declaration block this page rule contains. + pub block: Arc<Locked<PropertyDeclarationBlock>>, + /// The source position this rule was found at. + pub source_location: SourceLocation, +} + +impl PageRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + + self.block.unconditional_shallow_size_of(ops) + + self.block.read_with(guard).size_of(ops) + + self.selectors.size_of(ops) + } + /// Computes the specificity of this page rule when matched with flags. + /// + /// Computing this value has linear-complexity with the size of the + /// selectors, so the caller should usually call this once and cache the + /// result. + /// + /// Returns None if the flags do not match this page rule. + /// + /// The return type is ordered by page-rule specificity. + pub fn match_specificity(&self, flags: PagePseudoClassFlags) -> Option<u32> { + let mut specificity = None; + for s in self.selectors.0.iter().map(|s| s.match_specificity(flags)) { + specificity = s.max(specificity); + } + specificity + } +} + +impl ToCssWithGuard for PageRule { + /// Serialization of PageRule is not specced, adapted from steps for + /// StyleRule. + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + // https://drafts.csswg.org/cssom/#serialize-a-css-rule + dest.write_str("@page ")?; + if !self.selectors.is_empty() { + self.selectors.to_css(&mut CssWriter::new(dest))?; + dest.write_char(' ')?; + } + dest.write_char('{')?; + + // TODO: share more/most of this with style rules + // https://bugzilla.mozilla.org/1867164 + let declaration_block = self.block.read_with(guard); + let has_declarations = !declaration_block.declarations().is_empty(); + + let rules = self.rules.read_with(guard); + if !rules.is_empty() { + if has_declarations { + dest.write_str("\n ")?; + declaration_block.to_css(dest)?; + } + return rules.to_css_block_without_opening(guard, dest); + } + + if has_declarations { + dest.write_char(' ')?; + declaration_block.to_css(dest)?; + } + dest.write_str(" }") + } +} + +impl DeepCloneWithLock for PageRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(&guard); + PageRule { + selectors: self.selectors.clone(), + block: Arc::new(lock.wrap(self.block.read_with(&guard).clone())), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + source_location: self.source_location.clone(), + } + } +} diff --git a/servo/components/style/stylesheets/property_rule.rs b/servo/components/style/stylesheets/property_rule.rs new file mode 100644 index 0000000000..abe32050bf --- /dev/null +++ b/servo/components/style/stylesheets/property_rule.rs @@ -0,0 +1,5 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +pub use crate::properties_and_values::registry::PropertyRegistration as PropertyRule; diff --git a/servo/components/style/stylesheets/rule_list.rs b/servo/components/style/stylesheets/rule_list.rs new file mode 100644 index 0000000000..1b9f330185 --- /dev/null +++ b/servo/components/style/stylesheets/rule_list.rs @@ -0,0 +1,189 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A list of CSS rules. + +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::loader::StylesheetLoader; +use crate::stylesheets::rule_parser::InsertRuleContext; +use crate::stylesheets::stylesheet::StylesheetContents; +use crate::stylesheets::{AllowImportRules, CssRule, CssRuleTypes, RulesMutateError}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocShallowSizeOf, MallocSizeOfOps}; +use servo_arc::Arc; +use std::fmt::{self, Write}; + +/// A list of CSS rules. +#[derive(Debug, ToShmem)] +pub struct CssRules(pub Vec<CssRule>); + +impl CssRules { + /// Whether this CSS rules is empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} + +impl DeepCloneWithLock for CssRules { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + CssRules( + self.0 + .iter() + .map(|x| x.deep_clone_with_lock(lock, guard, params)) + .collect(), + ) + } +} + +impl CssRules { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + let mut n = self.0.shallow_size_of(ops); + for rule in self.0.iter() { + n += rule.size_of(guard, ops); + } + n + } + + /// Trivially construct a new set of CSS rules. + pub fn new(rules: Vec<CssRule>, shared_lock: &SharedRwLock) -> Arc<Locked<CssRules>> { + Arc::new(shared_lock.wrap(CssRules(rules))) + } + + /// Returns whether all the rules in this list are namespace or import + /// rules. + fn only_ns_or_import(&self) -> bool { + self.0.iter().all(|r| match *r { + CssRule::Namespace(..) | CssRule::Import(..) => true, + _ => false, + }) + } + + /// <https://drafts.csswg.org/cssom/#remove-a-css-rule> + pub fn remove_rule(&mut self, index: usize) -> Result<(), RulesMutateError> { + // Step 1, 2 + if index >= self.0.len() { + return Err(RulesMutateError::IndexSize); + } + + { + // Step 3 + let ref rule = self.0[index]; + + // Step 4 + if let CssRule::Namespace(..) = *rule { + if !self.only_ns_or_import() { + return Err(RulesMutateError::InvalidState); + } + } + } + + // Step 5, 6 + self.0.remove(index); + Ok(()) + } + + /// Serializes this CSSRules to CSS text as a block of rules. + /// + /// This should be speced into CSSOM spec at some point. See + /// <https://github.com/w3c/csswg-drafts/issues/1985> + pub fn to_css_block( + &self, + guard: &SharedRwLockReadGuard, + dest: &mut CssStringWriter, + ) -> fmt::Result { + dest.write_str(" {")?; + self.to_css_block_without_opening(guard, dest) + } + + /// As above, but without the opening curly bracket. That's needed for nesting. + pub fn to_css_block_without_opening( + &self, + guard: &SharedRwLockReadGuard, + dest: &mut CssStringWriter, + ) -> fmt::Result { + for rule in self.0.iter() { + dest.write_str("\n ")?; + rule.to_css(guard, dest)?; + } + dest.write_str("\n}") + } +} + +/// A trait to implement helpers for `Arc<Locked<CssRules>>`. +pub trait CssRulesHelpers { + /// <https://drafts.csswg.org/cssom/#insert-a-css-rule> + /// + /// Written in this funky way because parsing an @import rule may cause us + /// to clone a stylesheet from the same document due to caching in the CSS + /// loader. + /// + /// TODO(emilio): We could also pass the write guard down into the loader + /// instead, but that seems overkill. + fn insert_rule( + &self, + lock: &SharedRwLock, + rule: &str, + parent_stylesheet_contents: &StylesheetContents, + index: usize, + nested: CssRuleTypes, + loader: Option<&dyn StylesheetLoader>, + allow_import_rules: AllowImportRules, + ) -> Result<CssRule, RulesMutateError>; +} + +impl CssRulesHelpers for Locked<CssRules> { + fn insert_rule( + &self, + lock: &SharedRwLock, + rule: &str, + parent_stylesheet_contents: &StylesheetContents, + index: usize, + containing_rule_types: CssRuleTypes, + loader: Option<&dyn StylesheetLoader>, + allow_import_rules: AllowImportRules, + ) -> Result<CssRule, RulesMutateError> { + let new_rule = { + let read_guard = lock.read(); + let rules = self.read_with(&read_guard); + + // Step 1, 2 + if index > rules.0.len() { + return Err(RulesMutateError::IndexSize); + } + + let insert_rule_context = InsertRuleContext { + rule_list: &rules.0, + index, + containing_rule_types, + }; + + // Steps 3, 4, 5, 6 + CssRule::parse( + &rule, + insert_rule_context, + parent_stylesheet_contents, + lock, + loader, + allow_import_rules, + )? + }; + + { + let mut write_guard = lock.write(); + let rules = self.write_with(&mut write_guard); + rules.0.insert(index, new_rule.clone()); + } + + Ok(new_rule) + } +} diff --git a/servo/components/style/stylesheets/rule_parser.rs b/servo/components/style/stylesheets/rule_parser.rs new file mode 100644 index 0000000000..742ad5d250 --- /dev/null +++ b/servo/components/style/stylesheets/rule_parser.rs @@ -0,0 +1,982 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Parsing of the stylesheet contents. + +use crate::counter_style::{parse_counter_style_body, parse_counter_style_name_definition}; +use crate::custom_properties::parse_name as parse_custom_property_name; +use crate::error_reporting::ContextualParseError; +use crate::font_face::parse_font_face_block; +use crate::media_queries::MediaList; +use crate::parser::{Parse, ParserContext}; +use crate::properties::declaration_block::{ + parse_property_declaration_list, DeclarationParserState, PropertyDeclarationBlock, +}; +use crate::properties_and_values::rule::{parse_property_block, PropertyRuleName}; +use crate::selector_parser::{SelectorImpl, SelectorParser}; +use crate::shared_lock::{Locked, SharedRwLock}; +use crate::str::starts_with_ignore_ascii_case; +use crate::stylesheets::container_rule::{ContainerCondition, ContainerRule}; +use crate::stylesheets::document_rule::DocumentCondition; +use crate::stylesheets::font_feature_values_rule::parse_family_name_list; +use crate::stylesheets::import_rule::{ImportLayer, ImportRule, ImportSupportsCondition}; +use crate::stylesheets::keyframes_rule::parse_keyframe_list; +use crate::stylesheets::layer_rule::{LayerBlockRule, LayerName, LayerStatementRule}; +use crate::stylesheets::supports_rule::SupportsCondition; +use crate::stylesheets::{ + AllowImportRules, CorsMode, CssRule, CssRuleType, CssRuleTypes, CssRules, DocumentRule, + FontFeatureValuesRule, FontPaletteValuesRule, KeyframesRule, MarginRule, MarginRuleType, + MediaRule, NamespaceRule, PageRule, PageSelectors, RulesMutateError, StyleRule, + StylesheetLoader, SupportsRule, +}; +use crate::values::computed::font::FamilyName; +use crate::values::{CssUrl, CustomIdent, DashedIdent, KeyframesName}; +use crate::{Atom, Namespace, Prefix}; +use cssparser::{ + AtRuleParser, BasicParseError, BasicParseErrorKind, CowRcStr, DeclarationParser, Parser, + ParserState, QualifiedRuleParser, RuleBodyItemParser, RuleBodyParser, SourceLocation, + SourcePosition, +}; +use selectors::parser::{ParseRelative, SelectorList}; +use servo_arc::Arc; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// The information we need particularly to do CSSOM insertRule stuff. +pub struct InsertRuleContext<'a> { + /// The rule list we're about to insert into. + pub rule_list: &'a [CssRule], + /// The index we're about to get inserted at. + pub index: usize, + /// The containing rule types of our ancestors. + pub containing_rule_types: CssRuleTypes, +} + +impl<'a> InsertRuleContext<'a> { + /// Returns the max rule state allowable for insertion at a given index in + /// the rule list. + pub fn max_rule_state_at_index(&self, index: usize) -> State { + let rule = match self.rule_list.get(index) { + Some(rule) => rule, + None => return State::Body, + }; + match rule { + CssRule::Import(..) => State::Imports, + CssRule::Namespace(..) => State::Namespaces, + CssRule::LayerStatement(..) => { + // If there are @import / @namespace after this layer, then + // we're in the early-layers phase, otherwise we're in the body + // and everything is fair game. + let next_non_layer_statement_rule = self.rule_list[index + 1..] + .iter() + .find(|r| !matches!(*r, CssRule::LayerStatement(..))); + if let Some(non_layer) = next_non_layer_statement_rule { + if matches!(*non_layer, CssRule::Import(..) | CssRule::Namespace(..)) { + return State::EarlyLayers; + } + } + State::Body + }, + _ => State::Body, + } + } +} + +/// The parser for the top-level rules in a stylesheet. +pub struct TopLevelRuleParser<'a, 'i> { + /// A reference to the lock we need to use to create rules. + pub shared_lock: &'a SharedRwLock, + /// A reference to a stylesheet loader if applicable, for `@import` rules. + pub loader: Option<&'a dyn StylesheetLoader>, + /// The top-level parser context. + pub context: ParserContext<'a>, + /// The current state of the parser. + pub state: State, + /// Whether we have tried to parse was invalid due to being in the wrong + /// place (e.g. an @import rule was found while in the `Body` state). Reset + /// to `false` when `take_had_hierarchy_error` is called. + pub dom_error: Option<RulesMutateError>, + /// The info we need insert a rule in a list. + pub insert_rule_context: Option<InsertRuleContext<'a>>, + /// Whether @import rules will be allowed. + pub allow_import_rules: AllowImportRules, + /// Parser state for declaration blocks in either nested rules or style rules. + pub declaration_parser_state: DeclarationParserState<'i>, + /// State we keep around only for error reporting purposes. Right now that contains just the + /// selectors stack for nesting, if any. + /// + /// TODO(emilio): This isn't populated properly for `insertRule()` but... + pub error_reporting_state: Vec<SelectorList<SelectorImpl>>, + /// The rules we've parsed so far. + pub rules: Vec<CssRule>, +} + +impl<'a, 'i> TopLevelRuleParser<'a, 'i> { + #[inline] + fn nested(&mut self) -> &mut NestedRuleParser<'a, 'i> { + // SAFETY: NestedRuleParser is just a repr(transparent) wrapper over TopLevelRuleParser + const_assert!( + std::mem::size_of::<TopLevelRuleParser<'static, 'static>>() == + std::mem::size_of::<NestedRuleParser<'static, 'static>>() + ); + const_assert!( + std::mem::align_of::<TopLevelRuleParser<'static, 'static>>() == + std::mem::align_of::<NestedRuleParser<'static, 'static>>() + ); + unsafe { &mut *(self as *mut _ as *mut NestedRuleParser<'a, 'i>) } + } + + /// Returns the current state of the parser. + #[inline] + pub fn state(&self) -> State { + self.state + } + + /// Checks whether we can parse a rule that would transition us to + /// `new_state`. + /// + /// This is usually a simple branch, but we may need more bookkeeping if + /// doing `insertRule` from CSSOM. + fn check_state(&mut self, new_state: State) -> bool { + if self.state > new_state { + self.dom_error = Some(RulesMutateError::HierarchyRequest); + return false; + } + + let ctx = match self.insert_rule_context { + Some(ref ctx) => ctx, + None => return true, + }; + + let max_rule_state = ctx.max_rule_state_at_index(ctx.index); + if new_state > max_rule_state { + self.dom_error = Some(RulesMutateError::HierarchyRequest); + return false; + } + + // If there's anything that isn't a namespace rule (or import rule, but + // we checked that already at the beginning), reject with a + // StateError. + if new_state == State::Namespaces && + ctx.rule_list[ctx.index..] + .iter() + .any(|r| !matches!(*r, CssRule::Namespace(..))) + { + self.dom_error = Some(RulesMutateError::InvalidState); + return false; + } + + true + } +} + +/// The current state of the parser. +#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +pub enum State { + /// We haven't started parsing rules. + Start = 1, + /// We're parsing early `@layer` statement rules. + EarlyLayers = 2, + /// We're parsing `@import` and early `@layer` statement rules. + Imports = 3, + /// We're parsing `@namespace` rules. + Namespaces = 4, + /// We're parsing the main body of the stylesheet. + Body = 5, +} + +#[derive(Clone, Debug, MallocSizeOf, ToShmem)] +/// Vendor prefix. +pub enum VendorPrefix { + /// -moz prefix. + Moz, + /// -webkit prefix. + WebKit, +} + +/// A rule prelude for at-rule with block. +pub enum AtRulePrelude { + /// A @font-face rule prelude. + FontFace, + /// A @font-feature-values rule prelude, with its FamilyName list. + FontFeatureValues(Vec<FamilyName>), + /// A @font-palette-values rule prelude, with its identifier. + FontPaletteValues(DashedIdent), + /// A @counter-style rule prelude, with its counter style name. + CounterStyle(CustomIdent), + /// A @media rule prelude, with its media queries. + Media(Arc<Locked<MediaList>>), + /// A @container rule prelude. + Container(Arc<ContainerCondition>), + /// An @supports rule, with its conditional + Supports(SupportsCondition), + /// A @keyframes rule, with its animation name and vendor prefix if exists. + Keyframes(KeyframesName, Option<VendorPrefix>), + /// A @page rule prelude, with its page name if it exists. + Page(PageSelectors), + /// A @property rule prelude. + Property(PropertyRuleName), + /// A @document rule, with its conditional. + Document(DocumentCondition), + /// A @import rule prelude. + Import( + CssUrl, + Arc<Locked<MediaList>>, + Option<ImportSupportsCondition>, + ImportLayer, + ), + /// A @margin rule prelude. + Margin(MarginRuleType), + /// A @namespace rule prelude. + Namespace(Option<Prefix>, Namespace), + /// A @layer rule prelude. + Layer(Vec<LayerName>), +} + +impl AtRulePrelude { + fn name(&self) -> &'static str { + match *self { + Self::FontFace => "font-face", + Self::FontFeatureValues(..) => "font-feature-values", + Self::FontPaletteValues(..) => "font-palette-values", + Self::CounterStyle(..) => "counter-style", + Self::Media(..) => "media", + Self::Container(..) => "container", + Self::Supports(..) => "supports", + Self::Keyframes(..) => "keyframes", + Self::Page(..) => "page", + Self::Property(..) => "property", + Self::Document(..) => "-moz-document", + Self::Import(..) => "import", + Self::Margin(..) => "margin", + Self::Namespace(..) => "namespace", + Self::Layer(..) => "layer", + } + } +} + +impl<'a, 'i> AtRuleParser<'i> for TopLevelRuleParser<'a, 'i> { + type Prelude = AtRulePrelude; + type AtRule = SourcePosition; + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<AtRulePrelude, ParseError<'i>> { + match_ignore_ascii_case! { &*name, + "import" => { + if !self.check_state(State::Imports) { + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedImportRule)) + } + + if let AllowImportRules::No = self.allow_import_rules { + return Err(input.new_custom_error(StyleParseErrorKind::DisallowedImportRule)) + } + + // FIXME(emilio): We should always be able to have a loader + // around! See bug 1533783. + if self.loader.is_none() { + error!("Saw @import rule, but no way to trigger the load"); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedImportRule)) + } + + let url_string = input.expect_url_or_string()?.as_ref().to_owned(); + let url = CssUrl::parse_from_string(url_string, &self.context, CorsMode::None); + + let (layer, supports) = ImportRule::parse_layer_and_supports(input, &mut self.context); + + let media = MediaList::parse(&self.context, input); + let media = Arc::new(self.shared_lock.wrap(media)); + + return Ok(AtRulePrelude::Import(url, media, supports, layer)); + }, + "namespace" => { + if !self.check_state(State::Namespaces) { + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedNamespaceRule)) + } + + let prefix = input.try_parse(|i| i.expect_ident_cloned()) + .map(|s| Prefix::from(s.as_ref())).ok(); + let maybe_namespace = match input.expect_url_or_string() { + Ok(url_or_string) => url_or_string, + Err(BasicParseError { kind: BasicParseErrorKind::UnexpectedToken(t), location }) => { + return Err(location.new_custom_error(StyleParseErrorKind::UnexpectedTokenWithinNamespace(t))) + } + Err(e) => return Err(e.into()), + }; + let url = Namespace::from(maybe_namespace.as_ref()); + return Ok(AtRulePrelude::Namespace(prefix, url)); + }, + // @charset is removed by rust-cssparser if it’s the first rule in the stylesheet + // anything left is invalid. + "charset" => { + self.dom_error = Some(RulesMutateError::HierarchyRequest); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedCharsetRule)) + }, + "layer" => { + let state_to_check = if self.state <= State::EarlyLayers { + // The real state depends on whether there's a block or not. + // We don't know that yet, but the parse_block check deals + // with that. + State::EarlyLayers + } else { + State::Body + }; + if !self.check_state(state_to_check) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }, + _ => { + // All other rules have blocks, so we do this check early in + // parse_block instead. + } + } + + AtRuleParser::parse_prelude(self.nested(), name, input) + } + + #[inline] + fn parse_block<'t>( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::AtRule, ParseError<'i>> { + if !self.check_state(State::Body) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + AtRuleParser::parse_block(self.nested(), prelude, start, input)?; + self.state = State::Body; + Ok(start.position()) + } + + #[inline] + fn rule_without_block( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + ) -> Result<Self::AtRule, ()> { + match prelude { + AtRulePrelude::Import(url, media, supports, layer) => { + let loader = self + .loader + .expect("Expected a stylesheet loader for @import"); + + let import_rule = loader.request_stylesheet( + url, + start.source_location(), + &self.context, + &self.shared_lock, + media, + supports, + layer, + ); + + self.state = State::Imports; + self.rules.push(CssRule::Import(import_rule)) + }, + AtRulePrelude::Namespace(prefix, url) => { + let namespaces = self.context.namespaces.to_mut(); + let prefix = if let Some(prefix) = prefix { + namespaces.prefixes.insert(prefix.clone(), url.clone()); + Some(prefix) + } else { + namespaces.default = Some(url.clone()); + None + }; + + self.state = State::Namespaces; + self.rules.push(CssRule::Namespace(Arc::new(NamespaceRule { + prefix, + url, + source_location: start.source_location(), + }))); + }, + AtRulePrelude::Layer(..) => { + AtRuleParser::rule_without_block(self.nested(), prelude, start)?; + if self.state <= State::EarlyLayers { + self.state = State::EarlyLayers; + } else { + self.state = State::Body; + } + }, + _ => AtRuleParser::rule_without_block(self.nested(), prelude, start)?, + }; + + Ok(start.position()) + } +} + +impl<'a, 'i> QualifiedRuleParser<'i> for TopLevelRuleParser<'a, 'i> { + type Prelude = SelectorList<SelectorImpl>; + type QualifiedRule = SourcePosition; + type Error = StyleParseErrorKind<'i>; + + #[inline] + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + if !self.check_state(State::Body) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + QualifiedRuleParser::parse_prelude(self.nested(), input) + } + + #[inline] + fn parse_block<'t>( + &mut self, + prelude: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<Self::QualifiedRule, ParseError<'i>> { + QualifiedRuleParser::parse_block(self.nested(), prelude, start, input)?; + self.state = State::Body; + Ok(start.position()) + } +} + +#[repr(transparent)] +#[derive(Deref, DerefMut)] +struct NestedRuleParser<'a, 'i>(TopLevelRuleParser<'a, 'i>); + +struct NestedParseResult { + rules: Vec<CssRule>, + declarations: PropertyDeclarationBlock, +} + +impl NestedParseResult { + fn into_rules( + mut self, + shared_lock: &SharedRwLock, + source_location: SourceLocation, + ) -> Arc<Locked<CssRules>> { + lazy_static! { + static ref AMPERSAND: SelectorList<SelectorImpl> = { + let list = SelectorList::ampersand(); + list.slice() + .iter() + .for_each(|selector| selector.mark_as_intentionally_leaked()); + list + }; + }; + + if !self.declarations.is_empty() { + self.rules.insert( + 0, + CssRule::Style(Arc::new(shared_lock.wrap(StyleRule { + selectors: AMPERSAND.clone(), + block: Arc::new(shared_lock.wrap(self.declarations)), + rules: None, + source_location, + }))), + ) + } + + CssRules::new(self.rules, shared_lock) + } +} + +impl<'a, 'i> NestedRuleParser<'a, 'i> { + #[inline] + fn in_style_rule(&self) -> bool { + self.context.rule_types.contains(CssRuleType::Style) + } + + #[inline] + fn in_page_rule(&self) -> bool { + self.context.rule_types.contains(CssRuleType::Page) + } + + #[inline] + fn in_style_or_page_rule(&self) -> bool { + let types = CssRuleTypes::from_bits(CssRuleType::Style.bit() | CssRuleType::Page.bit()); + self.context.rule_types.intersects(types) + } + + // https://drafts.csswg.org/css-nesting/#conditionals + // In addition to nested style rules, this specification allows nested group rules inside + // of style rules: any at-rule whose body contains style rules can be nested inside of a + // style rule as well. + fn at_rule_allowed(&self, prelude: &AtRulePrelude) -> bool { + match prelude { + AtRulePrelude::Media(..) | + AtRulePrelude::Supports(..) | + AtRulePrelude::Container(..) | + AtRulePrelude::Document(..) | + AtRulePrelude::Layer(..) => true, + + AtRulePrelude::Namespace(..) | + AtRulePrelude::FontFace | + AtRulePrelude::FontFeatureValues(..) | + AtRulePrelude::FontPaletteValues(..) | + AtRulePrelude::CounterStyle(..) | + AtRulePrelude::Keyframes(..) | + AtRulePrelude::Page(..) | + AtRulePrelude::Property(..) | + AtRulePrelude::Import(..) => !self.in_style_or_page_rule(), + AtRulePrelude::Margin(..) => self.in_page_rule(), + } + } + + fn nest_for_rule<R>(&mut self, rule_type: CssRuleType, cb: impl FnOnce(&mut Self) -> R) -> R { + let old_rule_types = self.context.rule_types; + self.context.rule_types.insert(rule_type); + let r = cb(self); + self.context.rule_types = old_rule_types; + r + } + + fn parse_nested( + &mut self, + input: &mut Parser<'i, '_>, + rule_type: CssRuleType, + ) -> NestedParseResult { + self.nest_for_rule(rule_type, |parser| { + let parse_declarations = parser.parse_declarations(); + let mut old_declaration_state = std::mem::take(&mut parser.declaration_parser_state); + let mut rules = std::mem::take(&mut parser.rules); + let mut iter = RuleBodyParser::new(input, parser); + while let Some(result) = iter.next() { + match result { + Ok(()) => {}, + Err((error, slice)) => { + if parse_declarations { + let top = &mut **iter.parser; + top.declaration_parser_state + .did_error(&top.context, error, slice); + } else { + let location = error.location; + let error = ContextualParseError::InvalidRule(slice, error); + iter.parser.context.log_css_error(location, error); + } + }, + } + } + let declarations = if parse_declarations { + let top = &mut **parser; + top.declaration_parser_state + .report_errors_if_needed(&top.context, &top.error_reporting_state); + parser.declaration_parser_state.take_declarations() + } else { + PropertyDeclarationBlock::default() + }; + debug_assert!( + !parser.declaration_parser_state.has_parsed_declarations(), + "Parsed but didn't consume declarations" + ); + std::mem::swap( + &mut parser.declaration_parser_state, + &mut old_declaration_state, + ); + std::mem::swap(&mut parser.rules, &mut rules); + NestedParseResult { + rules, + declarations, + } + }) + } + + #[inline(never)] + fn handle_error_reporting_selectors_pre( + &mut self, + start: &ParserState, + selectors: &SelectorList<SelectorImpl>, + ) { + use cssparser::ToCss; + debug_assert!(self.context.error_reporting_enabled()); + self.error_reporting_state.push(selectors.clone()); + 'selector_loop: for selector in selectors.slice().iter() { + let mut current = selector.iter(); + loop { + let mut found_host = false; + let mut found_non_host = false; + for component in &mut current { + if component.is_host() { + found_host = true; + } else { + found_non_host = true; + } + if found_host && found_non_host { + self.context.log_css_error( + start.source_location(), + ContextualParseError::NeverMatchingHostSelector( + selector.to_css_string(), + ), + ); + continue 'selector_loop; + } + } + if current.next_sequence().is_none() { + break; + } + } + } + } + + fn handle_error_reporting_selectors_post(&mut self) { + self.error_reporting_state.pop(); + } +} + +impl<'a, 'i> AtRuleParser<'i> for NestedRuleParser<'a, 'i> { + type Prelude = AtRulePrelude; + type AtRule = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + Ok(match_ignore_ascii_case! { &*name, + "media" => { + let media_queries = MediaList::parse(&self.context, input); + let arc = Arc::new(self.shared_lock.wrap(media_queries)); + AtRulePrelude::Media(arc) + }, + "supports" => { + let cond = SupportsCondition::parse(input)?; + AtRulePrelude::Supports(cond) + }, + "font-face" => { + AtRulePrelude::FontFace + }, + "container" if static_prefs::pref!("layout.css.container-queries.enabled") => { + let condition = Arc::new(ContainerCondition::parse(&self.context, input)?); + AtRulePrelude::Container(condition) + }, + "layer" => { + let names = input.try_parse(|input| { + input.parse_comma_separated(|input| { + LayerName::parse(&self.context, input) + }) + }).unwrap_or_default(); + AtRulePrelude::Layer(names) + }, + "font-feature-values" if cfg!(feature = "gecko") => { + let family_names = parse_family_name_list(&self.context, input)?; + AtRulePrelude::FontFeatureValues(family_names) + }, + "font-palette-values" if static_prefs::pref!("layout.css.font-palette.enabled") => { + let name = DashedIdent::parse(&self.context, input)?; + AtRulePrelude::FontPaletteValues(name) + }, + "counter-style" if cfg!(feature = "gecko") => { + let name = parse_counter_style_name_definition(input)?; + AtRulePrelude::CounterStyle(name) + }, + "keyframes" | "-webkit-keyframes" | "-moz-keyframes" => { + let prefix = if starts_with_ignore_ascii_case(&*name, "-webkit-") { + Some(VendorPrefix::WebKit) + } else if starts_with_ignore_ascii_case(&*name, "-moz-") { + Some(VendorPrefix::Moz) + } else { + None + }; + if cfg!(feature = "servo") && + prefix.as_ref().map_or(false, |p| matches!(*p, VendorPrefix::Moz)) { + // Servo should not support @-moz-keyframes. + return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))) + } + let name = KeyframesName::parse(&self.context, input)?; + AtRulePrelude::Keyframes(name, prefix) + }, + "page" if cfg!(feature = "gecko") => { + AtRulePrelude::Page( + input.try_parse(|i| PageSelectors::parse(&self.context, i)).unwrap_or_default() + ) + }, + "property" if static_prefs::pref!("layout.css.properties-and-values.enabled") => { + let name = input.expect_ident_cloned()?; + let name = parse_custom_property_name(&name).map_err(|_| { + input.new_custom_error(StyleParseErrorKind::UnexpectedIdent(name.clone())) + })?; + AtRulePrelude::Property(PropertyRuleName(Atom::from(name))) + }, + "-moz-document" if cfg!(feature = "gecko") => { + let cond = DocumentCondition::parse(&self.context, input)?; + AtRulePrelude::Document(cond) + }, + _ => { + if static_prefs::pref!("layout.css.margin-rules.enabled") { + if let Some(margin_rule_type) = MarginRuleType::match_name(&name) { + return Ok(AtRulePrelude::Margin(margin_rule_type)); + } + } + return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(name.clone()))) + }, + }) + } + + fn parse_block<'t>( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + if !self.at_rule_allowed(&prelude) { + self.dom_error = Some(RulesMutateError::HierarchyRequest); + return Err(input.new_error(BasicParseErrorKind::AtRuleInvalid(prelude.name().into()))); + } + let rule = match prelude { + AtRulePrelude::FontFace => self.nest_for_rule(CssRuleType::FontFace, |p| { + CssRule::FontFace(Arc::new(p.shared_lock.wrap( + parse_font_face_block(&p.context, input, start.source_location()).into(), + ))) + }), + AtRulePrelude::FontFeatureValues(family_names) => { + self.nest_for_rule(CssRuleType::FontFeatureValues, |p| { + CssRule::FontFeatureValues(Arc::new(FontFeatureValuesRule::parse( + &p.context, + input, + family_names, + start.source_location(), + ))) + }) + }, + AtRulePrelude::FontPaletteValues(name) => { + self.nest_for_rule(CssRuleType::FontPaletteValues, |p| { + CssRule::FontPaletteValues(Arc::new(FontPaletteValuesRule::parse( + &p.context, + input, + name, + start.source_location(), + ))) + }) + }, + AtRulePrelude::CounterStyle(name) => { + let body = self.nest_for_rule(CssRuleType::CounterStyle, |p| { + parse_counter_style_body(name, &p.context, input, start.source_location()) + })?; + CssRule::CounterStyle(Arc::new(self.shared_lock.wrap(body))) + }, + AtRulePrelude::Media(media_queries) => { + let source_location = start.source_location(); + CssRule::Media(Arc::new(MediaRule { + media_queries, + rules: self + .parse_nested(input, CssRuleType::Media) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Supports(condition) => { + let enabled = + self.nest_for_rule(CssRuleType::Style, |p| condition.eval(&p.context)); + let source_location = start.source_location(); + CssRule::Supports(Arc::new(SupportsRule { + condition, + rules: self + .parse_nested(input, CssRuleType::Supports) + .into_rules(self.shared_lock, source_location), + enabled, + source_location, + })) + }, + AtRulePrelude::Keyframes(name, vendor_prefix) => { + self.nest_for_rule(CssRuleType::Keyframe, |p| { + let top = &mut **p; + CssRule::Keyframes(Arc::new(top.shared_lock.wrap(KeyframesRule { + name, + keyframes: parse_keyframe_list(&mut top.context, input, top.shared_lock), + vendor_prefix, + source_location: start.source_location(), + }))) + }) + }, + AtRulePrelude::Page(selectors) => { + let source_location = start.source_location(); + let page_rule = if !static_prefs::pref!("layout.css.margin-rules.enabled") { + let declarations = self.nest_for_rule(CssRuleType::Page, |p| { + parse_property_declaration_list(&p.context, input, &[]) + }); + PageRule { + selectors, + rules: CssRules::new(vec![], self.shared_lock), + block: Arc::new(self.shared_lock.wrap(declarations)), + source_location, + } + } else { + let result = self.parse_nested(input, CssRuleType::Page); + PageRule { + selectors, + rules: CssRules::new(result.rules, self.shared_lock), + block: Arc::new(self.shared_lock.wrap(result.declarations)), + source_location, + } + }; + CssRule::Page(Arc::new(self.shared_lock.wrap(page_rule))) + }, + AtRulePrelude::Property(name) => self.nest_for_rule(CssRuleType::Property, |p| { + let rule_data = + parse_property_block(&p.context, input, name, start.source_location())?; + Ok::<CssRule, ParseError<'i>>(CssRule::Property(Arc::new(rule_data))) + })?, + AtRulePrelude::Document(condition) => { + if !cfg!(feature = "gecko") { + unreachable!() + } + let source_location = start.source_location(); + CssRule::Document(Arc::new(DocumentRule { + condition, + rules: self + .parse_nested(input, CssRuleType::Document) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Container(condition) => { + let source_location = start.source_location(); + CssRule::Container(Arc::new(ContainerRule { + condition, + rules: self + .parse_nested(input, CssRuleType::Container) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Layer(names) => { + let name = match names.len() { + 0 | 1 => names.into_iter().next(), + _ => return Err(input.new_error(BasicParseErrorKind::AtRuleBodyInvalid)), + }; + let source_location = start.source_location(); + CssRule::LayerBlock(Arc::new(LayerBlockRule { + name, + rules: self + .parse_nested(input, CssRuleType::LayerBlock) + .into_rules(self.shared_lock, source_location), + source_location, + })) + }, + AtRulePrelude::Margin(rule_type) => { + let declarations = self.nest_for_rule(CssRuleType::Margin, |p| { + parse_property_declaration_list(&p.context, input, &[]) + }); + CssRule::Margin(Arc::new(MarginRule { + rule_type, + block: Arc::new(self.shared_lock.wrap(declarations)), + source_location: start.source_location(), + })) + } + AtRulePrelude::Import(..) | AtRulePrelude::Namespace(..) => { + // These rules don't have blocks. + return Err(input.new_unexpected_token_error(cssparser::Token::CurlyBracketBlock)); + }, + }; + self.rules.push(rule); + Ok(()) + } + + #[inline] + fn rule_without_block( + &mut self, + prelude: AtRulePrelude, + start: &ParserState, + ) -> Result<(), ()> { + if self.in_style_rule() { + return Err(()); + } + let rule = match prelude { + AtRulePrelude::Layer(names) => { + if names.is_empty() { + return Err(()); + } + CssRule::LayerStatement(Arc::new(LayerStatementRule { + names, + source_location: start.source_location(), + })) + }, + _ => return Err(()), + }; + self.rules.push(rule); + Ok(()) + } +} + +impl<'a, 'i> QualifiedRuleParser<'i> for NestedRuleParser<'a, 'i> { + type Prelude = SelectorList<SelectorImpl>; + type QualifiedRule = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_prelude<'t>( + &mut self, + input: &mut Parser<'i, 't>, + ) -> Result<Self::Prelude, ParseError<'i>> { + let selector_parser = SelectorParser { + stylesheet_origin: self.context.stylesheet_origin, + namespaces: &self.context.namespaces, + url_data: self.context.url_data, + for_supports_rule: false, + }; + let parse_relative = if self.in_style_rule() { + ParseRelative::ForNesting + } else { + ParseRelative::No + }; + SelectorList::parse(&selector_parser, input, parse_relative) + } + + fn parse_block<'t>( + &mut self, + selectors: Self::Prelude, + start: &ParserState, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let reporting_errors = self.context.error_reporting_enabled(); + if reporting_errors { + self.handle_error_reporting_selectors_pre(start, &selectors); + } + let result = self.parse_nested(input, CssRuleType::Style); + if reporting_errors { + self.handle_error_reporting_selectors_post(); + } + let block = Arc::new(self.shared_lock.wrap(result.declarations)); + let top = &mut **self; + top.rules + .push(CssRule::Style(Arc::new(top.shared_lock.wrap(StyleRule { + selectors, + block, + rules: if result.rules.is_empty() { + None + } else { + Some(CssRules::new(result.rules, top.shared_lock)) + }, + source_location: start.source_location(), + })))); + Ok(()) + } +} + +impl<'a, 'i> DeclarationParser<'i> for NestedRuleParser<'a, 'i> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + let top = &mut **self; + top.declaration_parser_state + .parse_value(&top.context, name, input) + } +} + +impl<'a, 'i> RuleBodyItemParser<'i, (), StyleParseErrorKind<'i>> for NestedRuleParser<'a, 'i> { + fn parse_qualified(&self) -> bool { + true + } + + /// If nesting is disabled, we can't get there for a non-style-rule. If it's enabled, we parse + /// raw declarations there. + fn parse_declarations(&self) -> bool { + // We also have to check for page rules here because we currently don't + // have a bespoke parser for page rules, and parse them as though they + // are style rules. + self.in_style_or_page_rule() + } +} diff --git a/servo/components/style/stylesheets/rules_iterator.rs b/servo/components/style/stylesheets/rules_iterator.rs new file mode 100644 index 0000000000..76d41c8184 --- /dev/null +++ b/servo/components/style/stylesheets/rules_iterator.rs @@ -0,0 +1,331 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! An iterator over a list of rules. + +use crate::context::QuirksMode; +use crate::media_queries::Device; +use crate::shared_lock::SharedRwLockReadGuard; +use crate::stylesheets::{CssRule, DocumentRule, ImportRule, MediaRule, SupportsRule}; +use smallvec::SmallVec; +use std::slice; + +/// An iterator over a list of rules. +pub struct RulesIterator<'a, 'b, C> +where + 'b: 'a, + C: NestedRuleIterationCondition + 'static, +{ + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'b>, + stack: SmallVec<[slice::Iter<'a, CssRule>; 3]>, + _phantom: ::std::marker::PhantomData<C>, +} + +impl<'a, 'b, C> RulesIterator<'a, 'b, C> +where + 'b: 'a, + C: NestedRuleIterationCondition + 'static, +{ + /// Creates a new `RulesIterator` to iterate over `rules`. + pub fn new( + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'b>, + rules: slice::Iter<'a, CssRule>, + ) -> Self { + let mut stack = SmallVec::new(); + stack.push(rules); + Self { + device, + quirks_mode, + guard, + stack, + _phantom: ::std::marker::PhantomData, + } + } + + /// Skips all the remaining children of the last nested rule processed. + pub fn skip_children(&mut self) { + self.stack.pop(); + } + + /// Returns the children of `rule`, and whether `rule` is effective. + pub fn children( + rule: &'a CssRule, + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'_>, + effective: &mut bool, + ) -> Option<slice::Iter<'a, CssRule>> { + *effective = true; + match *rule { + CssRule::Namespace(_) | + CssRule::FontFace(_) | + CssRule::CounterStyle(_) | + CssRule::Keyframes(_) | + CssRule::Margin(_) | + CssRule::Property(_) | + CssRule::LayerStatement(_) | + CssRule::FontFeatureValues(_) | + CssRule::FontPaletteValues(_) => None, + CssRule::Page(ref page_rule) => { + let page_rule = page_rule.read_with(guard); + let rules = page_rule.rules.read_with(guard); + Some(rules.0.iter()) + }, + CssRule::Style(ref style_rule) => { + let style_rule = style_rule.read_with(guard); + style_rule + .rules + .as_ref() + .map(|r| r.read_with(guard).0.iter()) + }, + CssRule::Import(ref import_rule) => { + let import_rule = import_rule.read_with(guard); + if !C::process_import(guard, device, quirks_mode, import_rule) { + *effective = false; + return None; + } + Some(import_rule.stylesheet.rules(guard).iter()) + }, + CssRule::Document(ref doc_rule) => { + if !C::process_document(guard, device, quirks_mode, doc_rule) { + *effective = false; + return None; + } + Some(doc_rule.rules.read_with(guard).0.iter()) + }, + CssRule::Container(ref container_rule) => { + Some(container_rule.rules.read_with(guard).0.iter()) + }, + CssRule::Media(ref media_rule) => { + if !C::process_media(guard, device, quirks_mode, media_rule) { + *effective = false; + return None; + } + Some(media_rule.rules.read_with(guard).0.iter()) + }, + CssRule::Supports(ref supports_rule) => { + if !C::process_supports(guard, device, quirks_mode, supports_rule) { + *effective = false; + return None; + } + Some(supports_rule.rules.read_with(guard).0.iter()) + }, + CssRule::LayerBlock(ref layer_rule) => Some(layer_rule.rules.read_with(guard).0.iter()), + } + } +} + +impl<'a, 'b, C> Iterator for RulesIterator<'a, 'b, C> +where + 'b: 'a, + C: NestedRuleIterationCondition + 'static, +{ + type Item = &'a CssRule; + + fn next(&mut self) -> Option<Self::Item> { + while !self.stack.is_empty() { + let rule = { + let nested_iter = self.stack.last_mut().unwrap(); + match nested_iter.next() { + Some(r) => r, + None => { + self.stack.pop(); + continue; + }, + } + }; + + let mut effective = true; + let children = Self::children( + rule, + self.device, + self.quirks_mode, + self.guard, + &mut effective, + ); + if !effective { + continue; + } + + if let Some(children) = children { + // NOTE: It's important that `children` gets pushed even if + // empty, so that `skip_children()` works as expected. + self.stack.push(children); + } + + return Some(rule); + } + + None + } +} + +/// RulesIterator. +pub trait NestedRuleIterationCondition { + /// Whether we should process the nested rules in a given `@import` rule. + fn process_import( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &ImportRule, + ) -> bool; + + /// Whether we should process the nested rules in a given `@media` rule. + fn process_media( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &MediaRule, + ) -> bool; + + /// Whether we should process the nested rules in a given `@-moz-document` + /// rule. + fn process_document( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &DocumentRule, + ) -> bool; + + /// Whether we should process the nested rules in a given `@supports` rule. + fn process_supports( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &SupportsRule, + ) -> bool; +} + +/// A struct that represents the condition that a rule applies to the document. +pub struct EffectiveRules; + +impl EffectiveRules { + /// Returns whether a given rule is effective. + pub fn is_effective( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &CssRule, + ) -> bool { + match *rule { + CssRule::Import(ref import_rule) => { + let import_rule = import_rule.read_with(guard); + Self::process_import(guard, device, quirks_mode, import_rule) + }, + CssRule::Document(ref doc_rule) => { + Self::process_document(guard, device, quirks_mode, doc_rule) + }, + CssRule::Media(ref media_rule) => { + Self::process_media(guard, device, quirks_mode, media_rule) + }, + CssRule::Supports(ref supports_rule) => { + Self::process_supports(guard, device, quirks_mode, supports_rule) + }, + _ => true, + } + } +} + +impl NestedRuleIterationCondition for EffectiveRules { + fn process_import( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &ImportRule, + ) -> bool { + match rule.stylesheet.media(guard) { + Some(m) => m.evaluate(device, quirks_mode), + None => true, + } + } + + fn process_media( + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + rule: &MediaRule, + ) -> bool { + rule.media_queries + .read_with(guard) + .evaluate(device, quirks_mode) + } + + fn process_document( + _: &SharedRwLockReadGuard, + device: &Device, + _: QuirksMode, + rule: &DocumentRule, + ) -> bool { + rule.condition.evaluate(device) + } + + fn process_supports( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + rule: &SupportsRule, + ) -> bool { + rule.enabled + } +} + +/// A filter that processes all the rules in a rule list. +pub struct AllRules; + +impl NestedRuleIterationCondition for AllRules { + fn process_import( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &ImportRule, + ) -> bool { + true + } + + fn process_media(_: &SharedRwLockReadGuard, _: &Device, _: QuirksMode, _: &MediaRule) -> bool { + true + } + + fn process_document( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &DocumentRule, + ) -> bool { + true + } + + fn process_supports( + _: &SharedRwLockReadGuard, + _: &Device, + _: QuirksMode, + _: &SupportsRule, + ) -> bool { + true + } +} + +/// An iterator over all the effective rules of a stylesheet. +/// +/// NOTE: This iterator recurses into `@import` rules. +pub type EffectiveRulesIterator<'a, 'b> = RulesIterator<'a, 'b, EffectiveRules>; + +impl<'a, 'b> EffectiveRulesIterator<'a, 'b> { + /// Returns an iterator over the effective children of a rule, even if + /// `rule` itself is not effective. + pub fn effective_children( + device: &'a Device, + quirks_mode: QuirksMode, + guard: &'a SharedRwLockReadGuard<'b>, + rule: &'a CssRule, + ) -> Self { + let children = + RulesIterator::<AllRules>::children(rule, device, quirks_mode, guard, &mut false); + EffectiveRulesIterator::new(device, quirks_mode, guard, children.unwrap_or([].iter())) + } +} diff --git a/servo/components/style/stylesheets/style_rule.rs b/servo/components/style/stylesheets/style_rule.rs new file mode 100644 index 0000000000..8f8b9f4a13 --- /dev/null +++ b/servo/components/style/stylesheets/style_rule.rs @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A style rule. + +use crate::properties::PropertyDeclarationBlock; +use crate::selector_parser::SelectorImpl; +use crate::shared_lock::{ + DeepCloneParams, DeepCloneWithLock, Locked, SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard, +}; +use crate::str::CssStringWriter; +use crate::stylesheets::CssRules; +use cssparser::SourceLocation; +#[cfg(feature = "gecko")] +use malloc_size_of::{ + MallocSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf, MallocUnconditionalSizeOf, +}; +use selectors::SelectorList; +use servo_arc::Arc; +use std::fmt::{self, Write}; + +/// A style rule, with selectors and declarations. +#[derive(Debug, ToShmem)] +pub struct StyleRule { + /// The list of selectors in this rule. + pub selectors: SelectorList<SelectorImpl>, + /// The declaration block with the properties it contains. + pub block: Arc<Locked<PropertyDeclarationBlock>>, + /// The nested rules to this style rule. Only non-`None` when nesting is enabled. + pub rules: Option<Arc<Locked<CssRules>>>, + /// The location in the sheet where it was found. + pub source_location: SourceLocation, +} + +impl DeepCloneWithLock for StyleRule { + /// Deep clones this StyleRule. + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> StyleRule { + StyleRule { + selectors: self.selectors.clone(), + block: Arc::new(lock.wrap(self.block.read_with(guard).clone())), + rules: self.rules.as_ref().map(|rules| { + let rules = rules.read_with(guard); + Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))) + }), + source_location: self.source_location.clone(), + } + } +} + +impl StyleRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + n += self.selectors.unconditional_size_of(ops); + n += self.block.unconditional_shallow_size_of(ops) + + self.block.read_with(guard).size_of(ops); + if let Some(ref rules) = self.rules { + n += rules.unconditional_shallow_size_of(ops) + + rules.read_with(guard).size_of(guard, ops) + } + n + } +} + +impl ToCssWithGuard for StyleRule { + /// https://drafts.csswg.org/cssom/#serialize-a-css-rule CSSStyleRule + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + use cssparser::ToCss; + // Step 1 + self.selectors.to_css(dest)?; + dest.write_str(" {")?; + + // Step 2 + let declaration_block = self.block.read_with(guard); + let has_declarations = !declaration_block.declarations().is_empty(); + + // Step 3 + if let Some(ref rules) = self.rules { + let rules = rules.read_with(guard); + // Step 6 (here because it's more convenient) + if !rules.is_empty() { + if has_declarations { + dest.write_str("\n ")?; + declaration_block.to_css(dest)?; + } + return rules.to_css_block_without_opening(guard, dest); + } + } + + // Steps 4 & 5 + if has_declarations { + dest.write_char(' ')?; + declaration_block.to_css(dest)?; + } + dest.write_str(" }") + } +} diff --git a/servo/components/style/stylesheets/stylesheet.rs b/servo/components/style/stylesheets/stylesheet.rs new file mode 100644 index 0000000000..1604022871 --- /dev/null +++ b/servo/components/style/stylesheets/stylesheet.rs @@ -0,0 +1,566 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::context::QuirksMode; +use crate::error_reporting::{ContextualParseError, ParseErrorReporter}; +use crate::media_queries::{Device, MediaList}; +use crate::parser::ParserContext; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard}; +use crate::stylesheets::loader::StylesheetLoader; +use crate::stylesheets::rule_parser::{State, TopLevelRuleParser}; +use crate::stylesheets::rules_iterator::{EffectiveRules, EffectiveRulesIterator}; +use crate::stylesheets::rules_iterator::{NestedRuleIterationCondition, RulesIterator}; +use crate::stylesheets::{CssRule, CssRules, Origin, UrlExtraData}; +use crate::use_counters::UseCounters; +use crate::{Namespace, Prefix}; +use cssparser::{Parser, ParserInput, StyleSheetParser}; +use fxhash::FxHashMap; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use parking_lot::RwLock; +use servo_arc::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use style_traits::ParsingMode; + +/// This structure holds the user-agent and user stylesheets. +pub struct UserAgentStylesheets { + /// The lock used for user-agent stylesheets. + pub shared_lock: SharedRwLock, + /// The user or user agent stylesheets. + pub user_or_user_agent_stylesheets: Vec<DocumentStyleSheet>, + /// The quirks mode stylesheet. + pub quirks_mode_stylesheet: DocumentStyleSheet, +} + +/// A set of namespaces applying to a given stylesheet. +/// +/// The namespace id is used in gecko +#[derive(Clone, Debug, Default, MallocSizeOf)] +#[allow(missing_docs)] +pub struct Namespaces { + pub default: Option<Namespace>, + pub prefixes: FxHashMap<Prefix, Namespace>, +} + +/// The contents of a given stylesheet. This effectively maps to a +/// StyleSheetInner in Gecko. +#[derive(Debug)] +pub struct StylesheetContents { + /// List of rules in the order they were found (important for + /// cascading order) + pub rules: Arc<Locked<CssRules>>, + /// The origin of this stylesheet. + pub origin: Origin, + /// The url data this stylesheet should use. + pub url_data: RwLock<UrlExtraData>, + /// The namespaces that apply to this stylesheet. + pub namespaces: RwLock<Namespaces>, + /// The quirks mode of this stylesheet. + pub quirks_mode: QuirksMode, + /// This stylesheet's source map URL. + pub source_map_url: RwLock<Option<String>>, + /// This stylesheet's source URL. + pub source_url: RwLock<Option<String>>, + + /// We don't want to allow construction outside of this file, to guarantee + /// that all contents are created with Arc<>. + _forbid_construction: (), +} + +impl StylesheetContents { + /// Parse a given CSS string, with a given url-data, origin, and + /// quirks mode. + pub fn from_str( + css: &str, + url_data: UrlExtraData, + origin: Origin, + shared_lock: &SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + use_counters: Option<&UseCounters>, + allow_import_rules: AllowImportRules, + sanitization_data: Option<&mut SanitizationData>, + ) -> Arc<Self> { + let (namespaces, rules, source_map_url, source_url) = Stylesheet::parse_rules( + css, + &url_data, + origin, + &shared_lock, + stylesheet_loader, + error_reporter, + quirks_mode, + use_counters, + allow_import_rules, + sanitization_data, + ); + + Arc::new(Self { + rules: CssRules::new(rules, &shared_lock), + origin, + url_data: RwLock::new(url_data), + namespaces: RwLock::new(namespaces), + quirks_mode, + source_map_url: RwLock::new(source_map_url), + source_url: RwLock::new(source_url), + _forbid_construction: (), + }) + } + + /// Creates a new StylesheetContents with the specified pre-parsed rules, + /// origin, URL data, and quirks mode. + /// + /// Since the rules have already been parsed, and the intention is that + /// this function is used for read only User Agent style sheets, an empty + /// namespace map is used, and the source map and source URLs are set to + /// None. + /// + /// An empty namespace map should be fine, as it is only used for parsing, + /// not serialization of existing selectors. Since UA sheets are read only, + /// we should never need the namespace map. + pub fn from_shared_data( + rules: Arc<Locked<CssRules>>, + origin: Origin, + url_data: UrlExtraData, + quirks_mode: QuirksMode, + ) -> Arc<Self> { + debug_assert!(rules.is_static()); + Arc::new(Self { + rules, + origin, + url_data: RwLock::new(url_data), + namespaces: RwLock::new(Namespaces::default()), + quirks_mode, + source_map_url: RwLock::new(None), + source_url: RwLock::new(None), + _forbid_construction: (), + }) + } + + /// Returns a reference to the list of rules. + #[inline] + pub fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] { + &self.rules.read_with(guard).0 + } + + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + if self.rules.is_static() { + return 0; + } + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + } +} + +impl DeepCloneWithLock for StylesheetContents { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + // Make a deep clone of the rules, using the new lock. + let rules = self + .rules + .read_with(guard) + .deep_clone_with_lock(lock, guard, params); + + Self { + rules: Arc::new(lock.wrap(rules)), + quirks_mode: self.quirks_mode, + origin: self.origin, + url_data: RwLock::new((*self.url_data.read()).clone()), + namespaces: RwLock::new((*self.namespaces.read()).clone()), + source_map_url: RwLock::new((*self.source_map_url.read()).clone()), + source_url: RwLock::new((*self.source_url.read()).clone()), + _forbid_construction: (), + } + } +} + +/// The structure servo uses to represent a stylesheet. +#[derive(Debug)] +pub struct Stylesheet { + /// The contents of this stylesheet. + pub contents: Arc<StylesheetContents>, + /// The lock used for objects inside this stylesheet + pub shared_lock: SharedRwLock, + /// List of media associated with the Stylesheet. + pub media: Arc<Locked<MediaList>>, + /// Whether this stylesheet should be disabled. + pub disabled: AtomicBool, +} + +/// A trait to represent a given stylesheet in a document. +pub trait StylesheetInDocument: ::std::fmt::Debug { + /// Get whether this stylesheet is enabled. + fn enabled(&self) -> bool; + + /// Get the media associated with this stylesheet. + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList>; + + /// Returns a reference to the list of rules in this stylesheet. + fn rules<'a, 'b: 'a>(&'a self, guard: &'b SharedRwLockReadGuard) -> &'a [CssRule] { + self.contents().rules(guard) + } + + /// Returns a reference to the contents of the stylesheet. + fn contents(&self) -> &StylesheetContents; + + /// Return an iterator using the condition `C`. + #[inline] + fn iter_rules<'a, 'b, C>( + &'a self, + device: &'a Device, + guard: &'a SharedRwLockReadGuard<'b>, + ) -> RulesIterator<'a, 'b, C> + where + C: NestedRuleIterationCondition, + { + let contents = self.contents(); + RulesIterator::new( + device, + contents.quirks_mode, + guard, + contents.rules(guard).iter(), + ) + } + + /// Returns whether the style-sheet applies for the current device. + fn is_effective_for_device(&self, device: &Device, guard: &SharedRwLockReadGuard) -> bool { + match self.media(guard) { + Some(medialist) => medialist.evaluate(device, self.contents().quirks_mode), + None => true, + } + } + + /// Return an iterator over the effective rules within the style-sheet, as + /// according to the supplied `Device`. + #[inline] + fn effective_rules<'a, 'b>( + &'a self, + device: &'a Device, + guard: &'a SharedRwLockReadGuard<'b>, + ) -> EffectiveRulesIterator<'a, 'b> { + self.iter_rules::<EffectiveRules>(device, guard) + } +} + +impl StylesheetInDocument for Stylesheet { + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + Some(self.media.read_with(guard)) + } + + fn enabled(&self) -> bool { + !self.disabled() + } + + #[inline] + fn contents(&self) -> &StylesheetContents { + &self.contents + } +} + +/// A simple wrapper over an `Arc<Stylesheet>`, with pointer comparison, and +/// suitable for its use in a `StylesheetSet`. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct DocumentStyleSheet( + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] pub Arc<Stylesheet>, +); + +impl PartialEq for DocumentStyleSheet { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl StylesheetInDocument for DocumentStyleSheet { + fn media<'a>(&'a self, guard: &'a SharedRwLockReadGuard) -> Option<&'a MediaList> { + self.0.media(guard) + } + + fn enabled(&self) -> bool { + self.0.enabled() + } + + #[inline] + fn contents(&self) -> &StylesheetContents { + self.0.contents() + } +} + +/// The kind of sanitization to use when parsing a stylesheet. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SanitizationKind { + /// Perform no sanitization. + None, + /// Allow only @font-face, style rules, and @namespace. + Standard, + /// Allow everything but conditional rules. + NoConditionalRules, +} + +/// Whether @import rules are allowed. +#[repr(u8)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AllowImportRules { + /// @import rules will be parsed. + Yes, + /// @import rules will not be parsed. + No, +} + +impl SanitizationKind { + fn allows(self, rule: &CssRule) -> bool { + debug_assert_ne!(self, SanitizationKind::None); + // NOTE(emilio): If this becomes more complex (not filtering just by + // top-level rules), we should thread all the data through nested rules + // and such. But this doesn't seem necessary at the moment. + let is_standard = matches!(self, SanitizationKind::Standard); + match *rule { + CssRule::Document(..) | + CssRule::Media(..) | + CssRule::Supports(..) | + CssRule::Import(..) | + CssRule::Container(..) | + // TODO(emilio): Perhaps Layer should not be always sanitized? But + // we sanitize @media and co, so this seems safer for now. + CssRule::LayerStatement(..) | + CssRule::LayerBlock(..) => false, + + CssRule::FontFace(..) | CssRule::Namespace(..) | CssRule::Style(..) => true, + + CssRule::Keyframes(..) | + CssRule::Page(..) | + CssRule::Margin(..) | + CssRule::Property(..) | + CssRule::FontFeatureValues(..) | + CssRule::FontPaletteValues(..) | + CssRule::CounterStyle(..) => !is_standard, + } + } +} + +/// A struct to hold the data relevant to style sheet sanitization. +#[derive(Debug)] +pub struct SanitizationData { + kind: SanitizationKind, + output: String, +} + +impl SanitizationData { + /// Create a new input for sanitization. + #[inline] + pub fn new(kind: SanitizationKind) -> Option<Self> { + if matches!(kind, SanitizationKind::None) { + return None; + } + Some(Self { + kind, + output: String::new(), + }) + } + + /// Take the sanitized output. + #[inline] + pub fn take(self) -> String { + self.output + } +} + +impl Stylesheet { + /// Updates an empty stylesheet from a given string of text. + pub fn update_from_str( + existing: &Stylesheet, + css: &str, + url_data: UrlExtraData, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + allow_import_rules: AllowImportRules, + ) { + // FIXME: Consider adding use counters to Servo? + let (namespaces, rules, source_map_url, source_url) = Self::parse_rules( + css, + &url_data, + existing.contents.origin, + &existing.shared_lock, + stylesheet_loader, + error_reporter, + existing.contents.quirks_mode, + /* use_counters = */ None, + allow_import_rules, + /* sanitization_data = */ None, + ); + + *existing.contents.url_data.write() = url_data; + *existing.contents.namespaces.write() = namespaces; + + // Acquire the lock *after* parsing, to minimize the exclusive section. + let mut guard = existing.shared_lock.write(); + *existing.contents.rules.write_with(&mut guard) = CssRules(rules); + *existing.contents.source_map_url.write() = source_map_url; + *existing.contents.source_url.write() = source_url; + } + + fn parse_rules( + css: &str, + url_data: &UrlExtraData, + origin: Origin, + shared_lock: &SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + use_counters: Option<&UseCounters>, + allow_import_rules: AllowImportRules, + mut sanitization_data: Option<&mut SanitizationData>, + ) -> (Namespaces, Vec<CssRule>, Option<String>, Option<String>) { + let mut input = ParserInput::new(css); + let mut input = Parser::new(&mut input); + + let context = ParserContext::new( + origin, + url_data, + None, + ParsingMode::DEFAULT, + quirks_mode, + /* namespaces = */ Default::default(), + error_reporter, + use_counters, + ); + + let mut rule_parser = TopLevelRuleParser { + shared_lock, + loader: stylesheet_loader, + context, + state: State::Start, + dom_error: None, + insert_rule_context: None, + allow_import_rules, + declaration_parser_state: Default::default(), + error_reporting_state: Default::default(), + rules: Vec::new(), + }; + + { + let mut iter = StyleSheetParser::new(&mut input, &mut rule_parser); + while let Some(result) = iter.next() { + match result { + Ok(rule_start) => { + // TODO(emilio, nesting): sanitize nested CSS rules, probably? + if let Some(ref mut data) = sanitization_data { + if let Some(ref rule) = iter.parser.rules.last() { + if !data.kind.allows(rule) { + iter.parser.rules.pop(); + continue; + } + } + let end = iter.input.position().byte_index(); + data.output.push_str(&css[rule_start.byte_index()..end]); + } + }, + Err((error, slice)) => { + let location = error.location; + let error = ContextualParseError::InvalidRule(slice, error); + iter.parser.context.log_css_error(location, error); + }, + } + } + } + + let source_map_url = input.current_source_map_url().map(String::from); + let source_url = input.current_source_url().map(String::from); + ( + rule_parser.context.namespaces.into_owned(), + rule_parser.rules, + source_map_url, + source_url, + ) + } + + /// Creates an empty stylesheet and parses it with a given base url, origin + /// and media. + /// + /// Effectively creates a new stylesheet and forwards the hard work to + /// `Stylesheet::update_from_str`. + pub fn from_str( + css: &str, + url_data: UrlExtraData, + origin: Origin, + media: Arc<Locked<MediaList>>, + shared_lock: SharedRwLock, + stylesheet_loader: Option<&dyn StylesheetLoader>, + error_reporter: Option<&dyn ParseErrorReporter>, + quirks_mode: QuirksMode, + allow_import_rules: AllowImportRules, + ) -> Self { + // FIXME: Consider adding use counters to Servo? + let contents = StylesheetContents::from_str( + css, + url_data, + origin, + &shared_lock, + stylesheet_loader, + error_reporter, + quirks_mode, + /* use_counters = */ None, + allow_import_rules, + /* sanitized_output = */ None, + ); + + Stylesheet { + contents, + shared_lock, + media, + disabled: AtomicBool::new(false), + } + } + + /// Returns whether the stylesheet has been explicitly disabled through the + /// CSSOM. + pub fn disabled(&self) -> bool { + self.disabled.load(Ordering::SeqCst) + } + + /// Records that the stylesheet has been explicitly disabled through the + /// CSSOM. + /// + /// Returns whether the the call resulted in a change in disabled state. + /// + /// Disabled stylesheets remain in the document, but their rules are not + /// added to the Stylist. + pub fn set_disabled(&self, disabled: bool) -> bool { + self.disabled.swap(disabled, Ordering::SeqCst) != disabled + } +} + +#[cfg(feature = "servo")] +impl Clone for Stylesheet { + fn clone(&self) -> Self { + // Create a new lock for our clone. + let lock = self.shared_lock.clone(); + let guard = self.shared_lock.read(); + + // Make a deep clone of the media, using the new lock. + let media = self.media.read_with(&guard).clone(); + let media = Arc::new(lock.wrap(media)); + let contents = Arc::new(self.contents.deep_clone_with_lock( + &lock, + &guard, + &DeepCloneParams, + )); + + Stylesheet { + contents, + media, + shared_lock: lock, + disabled: AtomicBool::new(self.disabled.load(Ordering::SeqCst)), + } + } +} diff --git a/servo/components/style/stylesheets/supports_rule.rs b/servo/components/style/stylesheets/supports_rule.rs new file mode 100644 index 0000000000..a3ffe5a2f5 --- /dev/null +++ b/servo/components/style/stylesheets/supports_rule.rs @@ -0,0 +1,397 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [@supports rules](https://drafts.csswg.org/css-conditional-3/#at-supports) + +use crate::font_face::{FontFaceSourceFormatKeyword, FontFaceSourceTechFlags}; +use crate::parser::ParserContext; +use crate::properties::{PropertyDeclaration, PropertyId, SourcePropertyDeclaration}; +use crate::selector_parser::{SelectorImpl, SelectorParser}; +use crate::shared_lock::{DeepCloneParams, DeepCloneWithLock, Locked}; +use crate::shared_lock::{SharedRwLock, SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::stylesheets::{CssRuleType, CssRules}; +use cssparser::parse_important; +use cssparser::{Delimiter, Parser, SourceLocation, Token}; +use cssparser::{ParseError as CssParseError, ParserInput}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use selectors::parser::{Selector, SelectorParseErrorKind}; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use std::str; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// An [`@supports`][supports] rule. +/// +/// [supports]: https://drafts.csswg.org/css-conditional-3/#at-supports +#[derive(Debug, ToShmem)] +pub struct SupportsRule { + /// The parsed condition + pub condition: SupportsCondition, + /// Child rules + pub rules: Arc<Locked<CssRules>>, + /// The result of evaluating the condition + pub enabled: bool, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl SupportsRule { + /// Measure heap usage. + #[cfg(feature = "gecko")] + pub fn size_of(&self, guard: &SharedRwLockReadGuard, ops: &mut MallocSizeOfOps) -> usize { + // Measurement of other fields may be added later. + self.rules.unconditional_shallow_size_of(ops) + + self.rules.read_with(guard).size_of(guard, ops) + } +} + +impl ToCssWithGuard for SupportsRule { + fn to_css(&self, guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + dest.write_str("@supports ")?; + self.condition.to_css(&mut CssWriter::new(dest))?; + self.rules.read_with(guard).to_css_block(guard, dest) + } +} + +impl DeepCloneWithLock for SupportsRule { + fn deep_clone_with_lock( + &self, + lock: &SharedRwLock, + guard: &SharedRwLockReadGuard, + params: &DeepCloneParams, + ) -> Self { + let rules = self.rules.read_with(guard); + SupportsRule { + condition: self.condition.clone(), + rules: Arc::new(lock.wrap(rules.deep_clone_with_lock(lock, guard, params))), + enabled: self.enabled, + source_location: self.source_location.clone(), + } + } +} + +/// An @supports condition +/// +/// <https://drafts.csswg.org/css-conditional-3/#at-supports> +#[derive(Clone, Debug, ToShmem)] +pub enum SupportsCondition { + /// `not (condition)` + Not(Box<SupportsCondition>), + /// `(condition)` + Parenthesized(Box<SupportsCondition>), + /// `(condition) and (condition) and (condition) ..` + And(Vec<SupportsCondition>), + /// `(condition) or (condition) or (condition) ..` + Or(Vec<SupportsCondition>), + /// `property-ident: value` (value can be any tokens) + Declaration(Declaration), + /// A `selector()` function. + Selector(RawSelector), + /// `font-format(<font-format>)` + FontFormat(FontFaceSourceFormatKeyword), + /// `font-tech(<font-tech>)` + FontTech(FontFaceSourceTechFlags), + /// `(any tokens)` or `func(any tokens)` + FutureSyntax(String), +} + +impl SupportsCondition { + /// Parse a condition + /// + /// <https://drafts.csswg.org/css-conditional/#supports_condition> + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("not")).is_ok() { + let inner = SupportsCondition::parse_in_parens(input)?; + return Ok(SupportsCondition::Not(Box::new(inner))); + } + + let in_parens = SupportsCondition::parse_in_parens(input)?; + + let location = input.current_source_location(); + let (keyword, wrapper) = match input.next() { + // End of input + Err(..) => return Ok(in_parens), + Ok(&Token::Ident(ref ident)) => { + match_ignore_ascii_case! { &ident, + "and" => ("and", SupportsCondition::And as fn(_) -> _), + "or" => ("or", SupportsCondition::Or as fn(_) -> _), + _ => return Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + } + }, + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + }; + + let mut conditions = Vec::with_capacity(2); + conditions.push(in_parens); + loop { + conditions.push(SupportsCondition::parse_in_parens(input)?); + if input + .try_parse(|input| input.expect_ident_matching(keyword)) + .is_err() + { + // Did not find the expected keyword. + // If we found some other token, it will be rejected by + // `Parser::parse_entirely` somewhere up the stack. + return Ok(wrapper(conditions)); + } + } + } + + /// Parses a functional supports condition. + fn parse_functional<'i, 't>( + function: &str, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + match_ignore_ascii_case! { function, + "selector" => { + let pos = input.position(); + consume_any_value(input)?; + Ok(SupportsCondition::Selector(RawSelector( + input.slice_from(pos).to_owned() + ))) + }, + "font-format" if static_prefs::pref!("layout.css.font-tech.enabled") => { + let kw = FontFaceSourceFormatKeyword::parse(input)?; + Ok(SupportsCondition::FontFormat(kw)) + }, + "font-tech" if static_prefs::pref!("layout.css.font-tech.enabled") => { + let flag = FontFaceSourceTechFlags::parse_one(input)?; + Ok(SupportsCondition::FontTech(flag)) + }, + _ => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + } + + /// Parses an `@import` condition as per + /// https://drafts.csswg.org/css-cascade-5/#typedef-import-conditions + pub fn parse_for_import<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("supports")?; + input.parse_nested_block(parse_condition_or_declaration) + } + + /// <https://drafts.csswg.org/css-conditional-3/#supports_condition_in_parens> + fn parse_in_parens<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + // Whitespace is normally taken care of in `Parser::next`, but we want to not include it in + // `pos` for the SupportsCondition::FutureSyntax cases. + input.skip_whitespace(); + let pos = input.position(); + let location = input.current_source_location(); + match *input.next()? { + Token::ParenthesisBlock => { + let nested = input + .try_parse(|input| input.parse_nested_block(parse_condition_or_declaration)); + if let Ok(nested) = nested { + return Ok(Self::Parenthesized(Box::new(nested))); + } + }, + Token::Function(ref ident) => { + let ident = ident.clone(); + let nested = input.try_parse(|input| { + input.parse_nested_block(|input| { + SupportsCondition::parse_functional(&ident, input) + }) + }); + if nested.is_ok() { + return nested; + } + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + input.parse_nested_block(consume_any_value)?; + Ok(SupportsCondition::FutureSyntax( + input.slice_from(pos).to_owned(), + )) + } + + /// Evaluate a supports condition + pub fn eval(&self, cx: &ParserContext) -> bool { + match *self { + SupportsCondition::Not(ref cond) => !cond.eval(cx), + SupportsCondition::Parenthesized(ref cond) => cond.eval(cx), + SupportsCondition::And(ref vec) => vec.iter().all(|c| c.eval(cx)), + SupportsCondition::Or(ref vec) => vec.iter().any(|c| c.eval(cx)), + SupportsCondition::Declaration(ref decl) => decl.eval(cx), + SupportsCondition::Selector(ref selector) => selector.eval(cx), + SupportsCondition::FontFormat(ref format) => eval_font_format(format), + SupportsCondition::FontTech(ref tech) => eval_font_tech(tech), + SupportsCondition::FutureSyntax(_) => false, + } + } +} + +fn eval_font_format(kw: &FontFaceSourceFormatKeyword) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsFontFormatSupported(*kw) } +} + +fn eval_font_tech(flag: &FontFaceSourceTechFlags) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsFontTechSupported(*flag) } +} + +/// supports_condition | declaration +/// <https://drafts.csswg.org/css-conditional/#dom-css-supports-conditiontext-conditiontext> +pub fn parse_condition_or_declaration<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<SupportsCondition, ParseError<'i>> { + if let Ok(condition) = input.try_parse(SupportsCondition::parse) { + Ok(condition) + } else { + Declaration::parse(input).map(SupportsCondition::Declaration) + } +} + +impl ToCss for SupportsCondition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + SupportsCondition::Not(ref cond) => { + dest.write_str("not ")?; + cond.to_css(dest) + }, + SupportsCondition::Parenthesized(ref cond) => { + dest.write_char('(')?; + cond.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::And(ref vec) => { + let mut first = true; + for cond in vec { + if !first { + dest.write_str(" and ")?; + } + first = false; + cond.to_css(dest)?; + } + Ok(()) + }, + SupportsCondition::Or(ref vec) => { + let mut first = true; + for cond in vec { + if !first { + dest.write_str(" or ")?; + } + first = false; + cond.to_css(dest)?; + } + Ok(()) + }, + SupportsCondition::Declaration(ref decl) => decl.to_css(dest), + SupportsCondition::Selector(ref selector) => { + dest.write_str("selector(")?; + selector.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::FontFormat(ref kw) => { + dest.write_str("font-format(")?; + kw.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::FontTech(ref flag) => { + dest.write_str("font-tech(")?; + flag.to_css(dest)?; + dest.write_char(')') + }, + SupportsCondition::FutureSyntax(ref s) => dest.write_str(&s), + } + } +} + +#[derive(Clone, Debug, ToShmem)] +/// A possibly-invalid CSS selector. +pub struct RawSelector(pub String); + +impl ToCss for RawSelector { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.0) + } +} + +impl RawSelector { + /// Tries to evaluate a `selector()` function. + pub fn eval(&self, context: &ParserContext) -> bool { + let mut input = ParserInput::new(&self.0); + let mut input = Parser::new(&mut input); + input + .parse_entirely(|input| -> Result<(), CssParseError<()>> { + let parser = SelectorParser { + namespaces: &context.namespaces, + stylesheet_origin: context.stylesheet_origin, + url_data: context.url_data, + for_supports_rule: true, + }; + + Selector::<SelectorImpl>::parse(&parser, input) + .map_err(|_| input.new_custom_error(()))?; + + Ok(()) + }) + .is_ok() + } +} + +#[derive(Clone, Debug, ToShmem)] +/// A possibly-invalid property declaration +pub struct Declaration(pub String); + +impl ToCss for Declaration { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(&self.0) + } +} + +/// <https://drafts.csswg.org/css-syntax-3/#typedef-any-value> +fn consume_any_value<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + input.expect_no_error_token().map_err(|err| err.into()) +} + +impl Declaration { + /// Parse a declaration + pub fn parse<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Declaration, ParseError<'i>> { + let pos = input.position(); + input.expect_ident()?; + input.expect_colon()?; + consume_any_value(input)?; + Ok(Declaration(input.slice_from(pos).to_owned())) + } + + /// Determine if a declaration parses + /// + /// <https://drafts.csswg.org/css-conditional-3/#support-definition> + pub fn eval(&self, context: &ParserContext) -> bool { + debug_assert!(context.rule_types().contains(CssRuleType::Style)); + + let mut input = ParserInput::new(&self.0); + let mut input = Parser::new(&mut input); + input + .parse_entirely(|input| -> Result<(), CssParseError<()>> { + let prop = input.expect_ident_cloned().unwrap(); + input.expect_colon().unwrap(); + + let id = + PropertyId::parse(&prop, context).map_err(|_| input.new_custom_error(()))?; + + let mut declarations = SourcePropertyDeclaration::default(); + input.parse_until_before(Delimiter::Bang, |input| { + PropertyDeclaration::parse_into(&mut declarations, id, &context, input) + .map_err(|_| input.new_custom_error(())) + })?; + let _ = input.try_parse(parse_important); + Ok(()) + }) + .is_ok() + } +} diff --git a/servo/components/style/stylist.rs b/servo/components/style/stylist.rs new file mode 100644 index 0000000000..cc1cb75689 --- /dev/null +++ b/servo/components/style/stylist.rs @@ -0,0 +1,3503 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Selector matching. + +use crate::applicable_declarations::{ + ApplicableDeclarationBlock, ApplicableDeclarationList, CascadePriority, +}; +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::{CascadeInputs, QuirksMode}; +use crate::custom_properties::ComputedCustomProperties; +use crate::dom::TElement; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs::{ServoStyleSetSizes, StyleRuleInclusion}; +use crate::invalidation::element::invalidation_map::{ + note_selector_for_invalidation, InvalidationMap, RelativeSelectorInvalidationMap, +}; +use crate::invalidation::media_queries::{ + EffectiveMediaQueryResults, MediaListKey, ToMediaListKey, +}; +use crate::invalidation::stylesheets::RuleChangeKind; +use crate::media_queries::Device; +use crate::properties::{self, CascadeMode, ComputedValues, FirstLineReparenting}; +use crate::properties::{AnimationDeclarations, PropertyDeclarationBlock}; +use crate::properties_and_values::registry::{ + PropertyRegistration, PropertyRegistrationData, ScriptRegistry as CustomPropertyScriptRegistry, +}; +use crate::rule_cache::{RuleCache, RuleCacheConditions}; +use crate::rule_collector::RuleCollector; +use crate::rule_tree::{CascadeLevel, RuleTree, StrongRuleNode, StyleSource}; +use crate::sharing::RevalidationResult; +use crate::selector_map::{PrecomputedHashMap, PrecomputedHashSet, SelectorMap, SelectorMapEntry}; +use crate::selector_parser::{PerPseudoElementMap, PseudoElement, SelectorImpl, SnapshotMap}; +use crate::shared_lock::{Locked, SharedRwLockReadGuard, StylesheetGuards}; +use crate::stylesheet_set::{DataValidity, DocumentStylesheetSet, SheetRebuildKind}; +use crate::stylesheet_set::{DocumentStylesheetFlusher, SheetCollectionFlusher}; +use crate::stylesheets::container_rule::ContainerCondition; +use crate::stylesheets::import_rule::ImportLayer; +use crate::stylesheets::keyframes_rule::KeyframesAnimation; +use crate::stylesheets::layer_rule::{LayerName, LayerOrder}; +#[cfg(feature = "gecko")] +use crate::stylesheets::{ + CounterStyleRule, FontFaceRule, FontFeatureValuesRule, FontPaletteValuesRule, +}; +use crate::stylesheets::{ + CssRule, EffectiveRulesIterator, Origin, OriginSet, PagePseudoClassFlags, PageRule, PerOrigin, + PerOriginIter, +}; +use crate::stylesheets::{StyleRule, StylesheetContents, StylesheetInDocument}; +use crate::values::computed; +use crate::AllocErr; +use crate::{Atom, LocalName, Namespace, ShrinkIfNeeded, WeakAtom}; +use dom::{DocumentState, ElementState}; +use fxhash::FxHashMap; +use malloc_size_of::MallocSizeOf; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocShallowSizeOf, MallocSizeOfOps, MallocUnconditionalShallowSizeOf}; +use selectors::attr::{CaseSensitivity, NamespaceConstraint}; +use selectors::bloom::BloomFilter; +use selectors::matching::{ + matches_selector, MatchingContext, MatchingMode, NeedsSelectorFlags, SelectorCaches, +}; +use selectors::matching::{MatchingForInvalidation, VisitedHandlingMode}; +use selectors::parser::{ + AncestorHashes, Combinator, Component, Selector, SelectorIter, SelectorList, +}; +use selectors::visitor::{SelectorListKind, SelectorVisitor}; +use servo_arc::{Arc, ArcBorrow}; +use smallvec::SmallVec; +use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; +use std::sync::Mutex; +use std::{mem, ops}; + +/// The type of the stylesheets that the stylist contains. +#[cfg(feature = "servo")] +pub type StylistSheet = crate::stylesheets::DocumentStyleSheet; + +/// The type of the stylesheets that the stylist contains. +#[cfg(feature = "gecko")] +pub type StylistSheet = crate::gecko::data::GeckoStyleSheet; + +#[derive(Debug, Clone)] +struct StylesheetContentsPtr(Arc<StylesheetContents>); + +impl PartialEq for StylesheetContentsPtr { + #[inline] + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl Eq for StylesheetContentsPtr {} + +impl Hash for StylesheetContentsPtr { + fn hash<H: Hasher>(&self, state: &mut H) { + let contents: &StylesheetContents = &*self.0; + (contents as *const StylesheetContents).hash(state) + } +} + +type StyleSheetContentList = Vec<StylesheetContentsPtr>; + +/// A key in the cascade data cache. +#[derive(Debug, Hash, Default, PartialEq, Eq)] +struct CascadeDataCacheKey { + media_query_results: Vec<MediaListKey>, + contents: StyleSheetContentList, +} + +unsafe impl Send for CascadeDataCacheKey {} +unsafe impl Sync for CascadeDataCacheKey {} + +trait CascadeDataCacheEntry: Sized { + /// Returns a reference to the cascade data. + fn cascade_data(&self) -> &CascadeData; + /// Rebuilds the cascade data for the new stylesheet collection. The + /// collection is guaranteed to be dirty. + fn rebuild<S>( + device: &Device, + quirks_mode: QuirksMode, + collection: SheetCollectionFlusher<S>, + guard: &SharedRwLockReadGuard, + old_entry: &Self, + ) -> Result<Arc<Self>, AllocErr> + where + S: StylesheetInDocument + PartialEq + 'static; + /// Measures heap memory usage. + #[cfg(feature = "gecko")] + fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes); +} + +struct CascadeDataCache<Entry> { + entries: FxHashMap<CascadeDataCacheKey, Arc<Entry>>, +} + +impl<Entry> CascadeDataCache<Entry> +where + Entry: CascadeDataCacheEntry, +{ + fn new() -> Self { + Self { + entries: Default::default(), + } + } + + fn len(&self) -> usize { + self.entries.len() + } + + // FIXME(emilio): This may need to be keyed on quirks-mode too, though for + // UA sheets there aren't class / id selectors on those sheets, usually, so + // it's probably ok... For the other cache the quirks mode shouldn't differ + // so also should be fine. + fn lookup<'a, S>( + &'a mut self, + device: &Device, + quirks_mode: QuirksMode, + collection: SheetCollectionFlusher<S>, + guard: &SharedRwLockReadGuard, + old_entry: &Entry, + ) -> Result<Option<Arc<Entry>>, AllocErr> + where + S: StylesheetInDocument + PartialEq + 'static, + { + use std::collections::hash_map::Entry as HashMapEntry; + debug!("StyleSheetCache::lookup({})", self.len()); + + if !collection.dirty() { + return Ok(None); + } + + let mut key = CascadeDataCacheKey::default(); + for sheet in collection.sheets() { + CascadeData::collect_applicable_media_query_results_into( + device, + sheet, + guard, + &mut key.media_query_results, + &mut key.contents, + ) + } + + let new_entry; + match self.entries.entry(key) { + HashMapEntry::Vacant(e) => { + debug!("> Picking the slow path (not in the cache)"); + new_entry = Entry::rebuild(device, quirks_mode, collection, guard, old_entry)?; + e.insert(new_entry.clone()); + }, + HashMapEntry::Occupied(mut e) => { + // Avoid reusing our old entry (this can happen if we get + // invalidated due to CSSOM mutations and our old stylesheet + // contents were already unique, for example). + if !std::ptr::eq(&**e.get(), old_entry) { + if log_enabled!(log::Level::Debug) { + debug!("cache hit for:"); + for sheet in collection.sheets() { + debug!(" > {:?}", sheet); + } + } + // The line below ensures the "committed" bit is updated + // properly. + collection.each(|_, _| true); + return Ok(Some(e.get().clone())); + } + + debug!("> Picking the slow path due to same entry as old"); + new_entry = Entry::rebuild(device, quirks_mode, collection, guard, old_entry)?; + e.insert(new_entry.clone()); + }, + } + + Ok(Some(new_entry)) + } + + /// Returns all the cascade datas that are not being used (that is, that are + /// held alive just by this cache). + /// + /// We return them instead of dropping in place because some of them may + /// keep alive some other documents (like the SVG documents kept alive by + /// URL references), and thus we don't want to drop them while locking the + /// cache to not deadlock. + fn take_unused(&mut self) -> SmallVec<[Arc<Entry>; 3]> { + let mut unused = SmallVec::new(); + self.entries.retain(|_key, value| { + // is_unique() returns false for static references, but we never + // have static references to UserAgentCascadeDatas. If we did, it + // may not make sense to put them in the cache in the first place. + if !value.is_unique() { + return true; + } + unused.push(value.clone()); + false + }); + unused + } + + fn take_all(&mut self) -> FxHashMap<CascadeDataCacheKey, Arc<Entry>> { + mem::take(&mut self.entries) + } + + #[cfg(feature = "gecko")] + fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + sizes.mOther += self.entries.shallow_size_of(ops); + for (_key, arc) in self.entries.iter() { + // These are primary Arc references that can be measured + // unconditionally. + sizes.mOther += arc.unconditional_shallow_size_of(ops); + arc.add_size_of(ops, sizes); + } + } +} + +/// Measure heap usage of UA_CASCADE_DATA_CACHE. +#[cfg(feature = "gecko")] +pub fn add_size_of_ua_cache(ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + UA_CASCADE_DATA_CACHE + .lock() + .unwrap() + .add_size_of(ops, sizes); +} + +lazy_static! { + /// A cache of computed user-agent data, to be shared across documents. + static ref UA_CASCADE_DATA_CACHE: Mutex<UserAgentCascadeDataCache> = + Mutex::new(UserAgentCascadeDataCache::new()); +} + +impl CascadeDataCacheEntry for UserAgentCascadeData { + fn cascade_data(&self) -> &CascadeData { + &self.cascade_data + } + + fn rebuild<S>( + device: &Device, + quirks_mode: QuirksMode, + collection: SheetCollectionFlusher<S>, + guard: &SharedRwLockReadGuard, + _old: &Self, + ) -> Result<Arc<Self>, AllocErr> + where + S: StylesheetInDocument + PartialEq + 'static, + { + // TODO: Maybe we should support incremental rebuilds, though they seem + // uncommon and rebuild() doesn't deal with + // precomputed_pseudo_element_decls for now so... + let mut new_data = Self { + cascade_data: CascadeData::new(), + precomputed_pseudo_element_decls: PrecomputedPseudoElementDeclarations::default(), + }; + + for sheet in collection.sheets() { + new_data.cascade_data.add_stylesheet( + device, + quirks_mode, + sheet, + guard, + SheetRebuildKind::Full, + Some(&mut new_data.precomputed_pseudo_element_decls), + )?; + } + + new_data.cascade_data.did_finish_rebuild(); + + Ok(Arc::new(new_data)) + } + + #[cfg(feature = "gecko")] + fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.cascade_data.add_size_of(ops, sizes); + sizes.mPrecomputedPseudos += self.precomputed_pseudo_element_decls.size_of(ops); + } +} + +type UserAgentCascadeDataCache = CascadeDataCache<UserAgentCascadeData>; + +type PrecomputedPseudoElementDeclarations = PerPseudoElementMap<Vec<ApplicableDeclarationBlock>>; + +#[derive(Default)] +struct UserAgentCascadeData { + cascade_data: CascadeData, + + /// Applicable declarations for a given non-eagerly cascaded pseudo-element. + /// + /// These are eagerly computed once, and then used to resolve the new + /// computed values on the fly on layout. + /// + /// These are only filled from UA stylesheets. + precomputed_pseudo_element_decls: PrecomputedPseudoElementDeclarations, +} + +lazy_static! { + /// The empty UA cascade data for un-filled stylists. + static ref EMPTY_UA_CASCADE_DATA: Arc<UserAgentCascadeData> = { + let arc = Arc::new(UserAgentCascadeData::default()); + arc.mark_as_intentionally_leaked(); + arc + }; +} + +/// All the computed information for all the stylesheets that apply to the +/// document. +#[derive(MallocSizeOf)] +pub struct DocumentCascadeData { + #[ignore_malloc_size_of = "Arc, owned by UserAgentCascadeDataCache or empty"] + user_agent: Arc<UserAgentCascadeData>, + user: CascadeData, + author: CascadeData, + per_origin: PerOrigin<()>, +} + +impl Default for DocumentCascadeData { + fn default() -> Self { + Self { + user_agent: EMPTY_UA_CASCADE_DATA.clone(), + user: Default::default(), + author: Default::default(), + per_origin: Default::default(), + } + } +} + +/// An iterator over the cascade data of a given document. +pub struct DocumentCascadeDataIter<'a> { + iter: PerOriginIter<'a, ()>, + cascade_data: &'a DocumentCascadeData, +} + +impl<'a> Iterator for DocumentCascadeDataIter<'a> { + type Item = (&'a CascadeData, Origin); + + fn next(&mut self) -> Option<Self::Item> { + let (_, origin) = self.iter.next()?; + Some((self.cascade_data.borrow_for_origin(origin), origin)) + } +} + +impl DocumentCascadeData { + /// Borrows the cascade data for a given origin. + #[inline] + pub fn borrow_for_origin(&self, origin: Origin) -> &CascadeData { + match origin { + Origin::UserAgent => &self.user_agent.cascade_data, + Origin::Author => &self.author, + Origin::User => &self.user, + } + } + + fn iter_origins(&self) -> DocumentCascadeDataIter { + DocumentCascadeDataIter { + iter: self.per_origin.iter_origins(), + cascade_data: self, + } + } + + fn iter_origins_rev(&self) -> DocumentCascadeDataIter { + DocumentCascadeDataIter { + iter: self.per_origin.iter_origins_rev(), + cascade_data: self, + } + } + + /// Rebuild the cascade data for the given document stylesheets, and + /// optionally with a set of user agent stylesheets. Returns Err(..) + /// to signify OOM. + fn rebuild<'a, S>( + &mut self, + device: &Device, + quirks_mode: QuirksMode, + mut flusher: DocumentStylesheetFlusher<'a, S>, + guards: &StylesheetGuards, + ) -> Result<(), AllocErr> + where + S: StylesheetInDocument + PartialEq + 'static, + { + // First do UA sheets. + { + let origin_flusher = flusher.flush_origin(Origin::UserAgent); + // Dirty check is just a minor optimization (no need to grab the + // lock if nothing has changed). + if origin_flusher.dirty() { + let mut ua_cache = UA_CASCADE_DATA_CACHE.lock().unwrap(); + let new_data = ua_cache.lookup( + device, + quirks_mode, + origin_flusher, + guards.ua_or_user, + &self.user_agent, + )?; + if let Some(new_data) = new_data { + self.user_agent = new_data; + } + let _unused_entries = ua_cache.take_unused(); + // See the comments in take_unused() as for why the following + // line. + std::mem::drop(ua_cache); + } + } + + // Now do the user sheets. + self.user.rebuild( + device, + quirks_mode, + flusher.flush_origin(Origin::User), + guards.ua_or_user, + )?; + + // And now the author sheets. + self.author.rebuild( + device, + quirks_mode, + flusher.flush_origin(Origin::Author), + guards.author, + )?; + + Ok(()) + } + + /// Measures heap usage. + #[cfg(feature = "gecko")] + pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.user.add_size_of(ops, sizes); + self.author.add_size_of(ops, sizes); + } +} + +/// Whether author styles are enabled. +/// +/// This is used to support Gecko. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq)] +pub enum AuthorStylesEnabled { + Yes, + No, +} + +/// A wrapper over a DocumentStylesheetSet that can be `Sync`, since it's only +/// used and exposed via mutable methods in the `Stylist`. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +struct StylistStylesheetSet(DocumentStylesheetSet<StylistSheet>); +// Read above to see why this is fine. +unsafe impl Sync for StylistStylesheetSet {} + +impl StylistStylesheetSet { + fn new() -> Self { + StylistStylesheetSet(DocumentStylesheetSet::new()) + } +} + +impl ops::Deref for StylistStylesheetSet { + type Target = DocumentStylesheetSet<StylistSheet>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl ops::DerefMut for StylistStylesheetSet { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// This structure holds all the selectors and device characteristics +/// for a given document. The selectors are converted into `Rule`s +/// and sorted into `SelectorMap`s keyed off stylesheet origin and +/// pseudo-element (see `CascadeData`). +/// +/// This structure is effectively created once per pipeline, in the +/// LayoutThread corresponding to that pipeline. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Stylist { + /// Device that the stylist is currently evaluating against. + /// + /// This field deserves a bigger comment due to the different use that Gecko + /// and Servo give to it (that we should eventually unify). + /// + /// With Gecko, the device is never changed. Gecko manually tracks whether + /// the device data should be reconstructed, and "resets" the state of the + /// device. + /// + /// On Servo, on the other hand, the device is a really cheap representation + /// that is recreated each time some constraint changes and calling + /// `set_device`. + device: Device, + + /// The list of stylesheets. + stylesheets: StylistStylesheetSet, + + /// A cache of CascadeDatas for AuthorStylesheetSets (i.e., shadow DOM). + author_data_cache: CascadeDataCache<CascadeData>, + + /// If true, the quirks-mode stylesheet is applied. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "defined in selectors")] + quirks_mode: QuirksMode, + + /// Selector maps for all of the style sheets in the stylist, after + /// evalutaing media rules against the current device, split out per + /// cascade level. + cascade_data: DocumentCascadeData, + + /// Whether author styles are enabled. + author_styles_enabled: AuthorStylesEnabled, + + /// The rule tree, that stores the results of selector matching. + rule_tree: RuleTree, + + /// The set of registered custom properties from script. + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#dom-window-registeredpropertyset-slot> + script_custom_properties: CustomPropertyScriptRegistry, + + /// Initial values for registered custom properties. + initial_values_for_custom_properties: ComputedCustomProperties, + + /// Flags set from computing registered custom property initial values. + initial_values_for_custom_properties_flags: ComputedValueFlags, + + /// The total number of times the stylist has been rebuilt. + num_rebuilds: usize, +} + +/// What cascade levels to include when styling elements. +#[derive(Clone, Copy, PartialEq)] +pub enum RuleInclusion { + /// Include rules for style sheets at all cascade levels. This is the + /// normal rule inclusion mode. + All, + /// Only include rules from UA and user level sheets. Used to implement + /// `getDefaultComputedStyle`. + DefaultOnly, +} + +#[cfg(feature = "gecko")] +impl From<StyleRuleInclusion> for RuleInclusion { + fn from(value: StyleRuleInclusion) -> Self { + match value { + StyleRuleInclusion::All => RuleInclusion::All, + StyleRuleInclusion::DefaultOnly => RuleInclusion::DefaultOnly, + } + } +} + +/// A struct containing state from ancestor rules like @layer / @import / +/// @container / nesting. +struct ContainingRuleState { + layer_name: LayerName, + layer_id: LayerId, + container_condition_id: ContainerConditionId, + ancestor_selector_lists: SmallVec<[SelectorList<SelectorImpl>; 2]>, +} + +impl Default for ContainingRuleState { + fn default() -> Self { + Self { + layer_name: LayerName::new_empty(), + layer_id: LayerId::root(), + container_condition_id: ContainerConditionId::none(), + ancestor_selector_lists: Default::default(), + } + } +} + +struct SavedContainingRuleState { + ancestor_selector_lists_len: usize, + layer_name_len: usize, + layer_id: LayerId, + container_condition_id: ContainerConditionId, +} + +impl ContainingRuleState { + fn save(&self) -> SavedContainingRuleState { + SavedContainingRuleState { + ancestor_selector_lists_len: self.ancestor_selector_lists.len(), + layer_name_len: self.layer_name.0.len(), + layer_id: self.layer_id, + container_condition_id: self.container_condition_id, + } + } + + fn restore(&mut self, saved: &SavedContainingRuleState) { + debug_assert!(self.layer_name.0.len() >= saved.layer_name_len); + debug_assert!(self.ancestor_selector_lists.len() >= saved.ancestor_selector_lists_len); + self.ancestor_selector_lists + .truncate(saved.ancestor_selector_lists_len); + self.layer_name.0.truncate(saved.layer_name_len); + self.layer_id = saved.layer_id; + self.container_condition_id = saved.container_condition_id; + } +} + +impl Stylist { + /// Construct a new `Stylist`, using given `Device` and `QuirksMode`. + /// If more members are added here, think about whether they should + /// be reset in clear(). + #[inline] + pub fn new(device: Device, quirks_mode: QuirksMode) -> Self { + Self { + device, + quirks_mode, + stylesheets: StylistStylesheetSet::new(), + author_data_cache: CascadeDataCache::new(), + cascade_data: Default::default(), + author_styles_enabled: AuthorStylesEnabled::Yes, + rule_tree: RuleTree::new(), + script_custom_properties: Default::default(), + initial_values_for_custom_properties: Default::default(), + initial_values_for_custom_properties_flags: Default::default(), + num_rebuilds: 0, + } + } + + /// Returns the document cascade data. + #[inline] + pub fn cascade_data(&self) -> &DocumentCascadeData { + &self.cascade_data + } + + /// Returns whether author styles are enabled or not. + #[inline] + pub fn author_styles_enabled(&self) -> AuthorStylesEnabled { + self.author_styles_enabled + } + + /// Iterate through all the cascade datas from the document. + #[inline] + pub fn iter_origins(&self) -> DocumentCascadeDataIter { + self.cascade_data.iter_origins() + } + + /// Does what the name says, to prevent author_data_cache to grow without + /// bound. + pub fn remove_unique_author_data_cache_entries(&mut self) { + self.author_data_cache.take_unused(); + } + + /// Returns the custom property registration for this property's name. + /// https://drafts.css-houdini.org/css-properties-values-api-1/#determining-registration + pub fn get_custom_property_registration(&self, name: &Atom) -> &PropertyRegistrationData { + if let Some(registration) = self.custom_property_script_registry().get(name) { + return ®istration.data; + } + for (data, _) in self.iter_origins() { + if let Some(registration) = data.custom_property_registrations.get(name) { + return ®istration.data; + } + } + PropertyRegistrationData::unregistered() + } + + /// Returns custom properties with their registered initial values. + pub fn get_custom_property_initial_values(&self) -> &ComputedCustomProperties { + &self.initial_values_for_custom_properties + } + + /// Returns flags set from computing the registered custom property initial values. + pub fn get_custom_property_initial_values_flags(&self) -> ComputedValueFlags { + self.initial_values_for_custom_properties_flags + } + + /// Rebuild custom properties with their registered initial values. + /// https://drafts.css-houdini.org/css-properties-values-api-1/#determining-registration + pub fn rebuild_initial_values_for_custom_properties(&mut self) { + let mut initial_values = ComputedCustomProperties::default(); + let initial_values_flags; + { + let mut seen_names = PrecomputedHashSet::default(); + let mut rule_cache_conditions = RuleCacheConditions::default(); + let context = computed::Context::new_for_initial_at_property_value( + self, + &mut rule_cache_conditions, + ); + + for (k, v) in self.custom_property_script_registry().properties().iter() { + seen_names.insert(k.clone()); + let Ok(value) = v.compute_initial_value(&context) else { + continue; + }; + let map = if v.inherits() { + &mut initial_values.inherited + } else { + &mut initial_values.non_inherited + }; + map.insert(k, value); + } + for (data, _) in self.iter_origins() { + for (k, v) in data.custom_property_registrations.iter() { + if seen_names.insert(k.clone()) { + let last_value = &v.last().unwrap().0; + let Ok(value) = last_value.compute_initial_value(&context) else { + continue; + }; + let map = if last_value.inherits() { + &mut initial_values.inherited + } else { + &mut initial_values.non_inherited + }; + map.insert(k, value); + } + } + } + initial_values_flags = context.builder.flags(); + } + self.initial_values_for_custom_properties_flags = initial_values_flags; + self.initial_values_for_custom_properties = initial_values; + } + + /// Rebuilds (if needed) the CascadeData given a sheet collection. + pub fn rebuild_author_data<S>( + &mut self, + old_data: &CascadeData, + collection: SheetCollectionFlusher<S>, + guard: &SharedRwLockReadGuard, + ) -> Result<Option<Arc<CascadeData>>, AllocErr> + where + S: StylesheetInDocument + PartialEq + 'static, + { + self.author_data_cache + .lookup(&self.device, self.quirks_mode, collection, guard, old_data) + } + + /// Iterate over the extra data in origin order. + #[inline] + pub fn iter_extra_data_origins(&self) -> ExtraStyleDataIterator { + ExtraStyleDataIterator(self.cascade_data.iter_origins()) + } + + /// Iterate over the extra data in reverse origin order. + #[inline] + pub fn iter_extra_data_origins_rev(&self) -> ExtraStyleDataIterator { + ExtraStyleDataIterator(self.cascade_data.iter_origins_rev()) + } + + /// Returns the number of selectors. + pub fn num_selectors(&self) -> usize { + self.cascade_data + .iter_origins() + .map(|(d, _)| d.num_selectors) + .sum() + } + + /// Returns the number of declarations. + pub fn num_declarations(&self) -> usize { + self.cascade_data + .iter_origins() + .map(|(d, _)| d.num_declarations) + .sum() + } + + /// Returns the number of times the stylist has been rebuilt. + pub fn num_rebuilds(&self) -> usize { + self.num_rebuilds + } + + /// Returns the number of revalidation_selectors. + pub fn num_revalidation_selectors(&self) -> usize { + self.cascade_data + .iter_origins() + .map(|(data, _)| data.selectors_for_cache_revalidation.len()) + .sum() + } + + /// Returns the number of entries in invalidation maps. + pub fn num_invalidations(&self) -> usize { + self.cascade_data + .iter_origins() + .map(|(data, _)| { + data.invalidation_map.len() + data.relative_selector_invalidation_map.len() + }) + .sum() + } + + /// Returns whether the given DocumentState bit is relied upon by a selector + /// of some rule. + pub fn has_document_state_dependency(&self, state: DocumentState) -> bool { + self.cascade_data + .iter_origins() + .any(|(d, _)| d.document_state_dependencies.intersects(state)) + } + + /// Flush the list of stylesheets if they changed, ensuring the stylist is + /// up-to-date. + pub fn flush<E>( + &mut self, + guards: &StylesheetGuards, + document_element: Option<E>, + snapshots: Option<&SnapshotMap>, + ) -> bool + where + E: TElement, + { + if !self.stylesheets.has_changed() { + return false; + } + + self.num_rebuilds += 1; + + let flusher = self.stylesheets.flush(document_element, snapshots); + + let had_invalidations = flusher.had_invalidations(); + + self.cascade_data + .rebuild(&self.device, self.quirks_mode, flusher, guards) + .unwrap_or_else(|_| warn!("OOM in Stylist::flush")); + + self.rebuild_initial_values_for_custom_properties(); + + had_invalidations + } + + /// Insert a given stylesheet before another stylesheet in the document. + pub fn insert_stylesheet_before( + &mut self, + sheet: StylistSheet, + before_sheet: StylistSheet, + guard: &SharedRwLockReadGuard, + ) { + self.stylesheets + .insert_stylesheet_before(Some(&self.device), sheet, before_sheet, guard) + } + + /// Marks a given stylesheet origin as dirty, due to, for example, changes + /// in the declarations that affect a given rule. + /// + /// FIXME(emilio): Eventually it'd be nice for this to become more + /// fine-grained. + pub fn force_stylesheet_origins_dirty(&mut self, origins: OriginSet) { + self.stylesheets.force_dirty(origins) + } + + /// Sets whether author style is enabled or not. + pub fn set_author_styles_enabled(&mut self, enabled: AuthorStylesEnabled) { + self.author_styles_enabled = enabled; + } + + /// Returns whether we've recorded any stylesheet change so far. + pub fn stylesheets_have_changed(&self) -> bool { + self.stylesheets.has_changed() + } + + /// Appends a new stylesheet to the current set. + pub fn append_stylesheet(&mut self, sheet: StylistSheet, guard: &SharedRwLockReadGuard) { + self.stylesheets + .append_stylesheet(Some(&self.device), sheet, guard) + } + + /// Remove a given stylesheet to the current set. + pub fn remove_stylesheet(&mut self, sheet: StylistSheet, guard: &SharedRwLockReadGuard) { + self.stylesheets + .remove_stylesheet(Some(&self.device), sheet, guard) + } + + /// Notify of a change of a given rule. + pub fn rule_changed( + &mut self, + sheet: &StylistSheet, + rule: &CssRule, + guard: &SharedRwLockReadGuard, + change_kind: RuleChangeKind, + ) { + self.stylesheets + .rule_changed(Some(&self.device), sheet, rule, guard, change_kind) + } + + /// Appends a new stylesheet to the current set. + #[inline] + pub fn sheet_count(&self, origin: Origin) -> usize { + self.stylesheets.sheet_count(origin) + } + + /// Appends a new stylesheet to the current set. + #[inline] + pub fn sheet_at(&self, origin: Origin, index: usize) -> Option<&StylistSheet> { + self.stylesheets.get(origin, index) + } + + /// Returns whether for any of the applicable style rule data a given + /// condition is true. + pub fn any_applicable_rule_data<E, F>(&self, element: E, mut f: F) -> bool + where + E: TElement, + F: FnMut(&CascadeData) -> bool, + { + if f(&self.cascade_data.user_agent.cascade_data) { + return true; + } + + let mut maybe = false; + + let doc_author_rules_apply = + element.each_applicable_non_document_style_rule_data(|data, _| { + maybe = maybe || f(&*data); + }); + + if maybe || f(&self.cascade_data.user) { + return true; + } + + doc_author_rules_apply && f(&self.cascade_data.author) + } + + /// Execute callback for all applicable style rule data. + pub fn for_each_cascade_data_with_scope<'a, E, F>(&'a self, element: E, mut f: F) + where + E: TElement + 'a, + F: FnMut(&'a CascadeData, Option<E>), + { + f(&self.cascade_data.user_agent.cascade_data, None); + element.each_applicable_non_document_style_rule_data(|data, scope| { + f(data, Some(scope)); + }); + f(&self.cascade_data.user, None); + f(&self.cascade_data.author, None); + } + + /// Computes the style for a given "precomputed" pseudo-element, taking the + /// universal rules and applying them. + pub fn precomputed_values_for_pseudo<E>( + &self, + guards: &StylesheetGuards, + pseudo: &PseudoElement, + parent: Option<&ComputedValues>, + ) -> Arc<ComputedValues> + where + E: TElement, + { + debug_assert!(pseudo.is_precomputed()); + + let rule_node = self.rule_node_for_precomputed_pseudo(guards, pseudo, vec![]); + + self.precomputed_values_for_pseudo_with_rule_node::<E>(guards, pseudo, parent, rule_node) + } + + /// Computes the style for a given "precomputed" pseudo-element with + /// given rule node. + /// + /// TODO(emilio): The type parameter could go away with a void type + /// implementing TElement. + pub fn precomputed_values_for_pseudo_with_rule_node<E>( + &self, + guards: &StylesheetGuards, + pseudo: &PseudoElement, + parent: Option<&ComputedValues>, + rules: StrongRuleNode, + ) -> Arc<ComputedValues> + where + E: TElement, + { + self.compute_pseudo_element_style_with_inputs::<E>( + CascadeInputs { + rules: Some(rules), + visited_rules: None, + flags: Default::default(), + }, + pseudo, + guards, + parent, + /* element */ None, + ) + } + + /// Returns the rule node for a given precomputed pseudo-element. + /// + /// If we want to include extra declarations to this precomputed + /// pseudo-element, we can provide a vector of ApplicableDeclarationBlocks + /// to extra_declarations. This is useful for @page rules. + pub fn rule_node_for_precomputed_pseudo( + &self, + guards: &StylesheetGuards, + pseudo: &PseudoElement, + mut extra_declarations: Vec<ApplicableDeclarationBlock>, + ) -> StrongRuleNode { + let mut declarations_with_extra; + let declarations = match self + .cascade_data + .user_agent + .precomputed_pseudo_element_decls + .get(pseudo) + { + Some(declarations) => { + if !extra_declarations.is_empty() { + declarations_with_extra = declarations.clone(); + declarations_with_extra.append(&mut extra_declarations); + &*declarations_with_extra + } else { + &**declarations + } + }, + None => &[], + }; + + self.rule_tree.insert_ordered_rules_with_important( + declarations.into_iter().map(|a| a.clone().for_rule_tree()), + guards, + ) + } + + /// Returns the style for an anonymous box of the given type. + /// + /// TODO(emilio): The type parameter could go away with a void type + /// implementing TElement. + #[cfg(feature = "servo")] + pub fn style_for_anonymous<E>( + &self, + guards: &StylesheetGuards, + pseudo: &PseudoElement, + parent_style: &ComputedValues, + ) -> Arc<ComputedValues> + where + E: TElement, + { + self.precomputed_values_for_pseudo::<E>(guards, &pseudo, Some(parent_style)) + } + + /// Computes a pseudo-element style lazily during layout. + /// + /// This can only be done for a certain set of pseudo-elements, like + /// :selection. + /// + /// Check the documentation on lazy pseudo-elements in + /// docs/components/style.md + pub fn lazily_compute_pseudo_element_style<E>( + &self, + guards: &StylesheetGuards, + element: E, + pseudo: &PseudoElement, + rule_inclusion: RuleInclusion, + originating_element_style: &ComputedValues, + is_probe: bool, + matching_fn: Option<&dyn Fn(&PseudoElement) -> bool>, + ) -> Option<Arc<ComputedValues>> + where + E: TElement, + { + let cascade_inputs = self.lazy_pseudo_rules( + guards, + element, + originating_element_style, + pseudo, + is_probe, + rule_inclusion, + matching_fn, + )?; + + Some(self.compute_pseudo_element_style_with_inputs( + cascade_inputs, + pseudo, + guards, + Some(originating_element_style), + Some(element), + )) + } + + /// Computes a pseudo-element style lazily using the given CascadeInputs. + /// This can be used for truly lazy pseudo-elements or to avoid redoing + /// selector matching for eager pseudo-elements when we need to recompute + /// their style with a new parent style. + pub fn compute_pseudo_element_style_with_inputs<E>( + &self, + inputs: CascadeInputs, + pseudo: &PseudoElement, + guards: &StylesheetGuards, + parent_style: Option<&ComputedValues>, + element: Option<E>, + ) -> Arc<ComputedValues> + where + E: TElement, + { + // FIXME(emilio): The lack of layout_parent_style here could be + // worrying, but we're probably dropping the display fixup for + // pseudos other than before and after, so it's probably ok. + // + // (Though the flags don't indicate so!) + // + // It'd be fine to assert that this isn't called with a parent style + // where display contents is in effect, but in practice this is hard to + // do for stuff like :-moz-fieldset-content with a + // <fieldset style="display: contents">. That is, the computed value of + // display for the fieldset is "contents", even though it's not the used + // value, so we don't need to adjust in a different way anyway. + self.cascade_style_and_visited( + element, + Some(pseudo), + inputs, + guards, + parent_style, + parent_style, + FirstLineReparenting::No, + /* rule_cache = */ None, + &mut RuleCacheConditions::default(), + ) + } + + /// Computes a style using the given CascadeInputs. This can be used to + /// compute a style any time we know what rules apply and just need to use + /// the given parent styles. + /// + /// parent_style is the style to inherit from for properties affected by + /// first-line ancestors. + /// + /// parent_style_ignoring_first_line is the style to inherit from for + /// properties not affected by first-line ancestors. + /// + /// layout_parent_style is the style used for some property fixups. It's + /// the style of the nearest ancestor with a layout box. + pub fn cascade_style_and_visited<E>( + &self, + element: Option<E>, + pseudo: Option<&PseudoElement>, + inputs: CascadeInputs, + guards: &StylesheetGuards, + parent_style: Option<&ComputedValues>, + layout_parent_style: Option<&ComputedValues>, + first_line_reparenting: FirstLineReparenting, + rule_cache: Option<&RuleCache>, + rule_cache_conditions: &mut RuleCacheConditions, + ) -> Arc<ComputedValues> + where + E: TElement, + { + debug_assert!(pseudo.is_some() || element.is_some(), "Huh?"); + + // We need to compute visited values if we have visited rules or if our + // parent has visited values. + let visited_rules = match inputs.visited_rules.as_ref() { + Some(rules) => Some(rules), + None => { + if parent_style.and_then(|s| s.visited_style()).is_some() { + Some(inputs.rules.as_ref().unwrap_or(self.rule_tree.root())) + } else { + None + } + }, + }; + + // Read the comment on `precomputed_values_for_pseudo` to see why it's + // difficult to assert that display: contents nodes never arrive here + // (tl;dr: It doesn't apply for replaced elements and such, but the + // computed value is still "contents"). + // + // FIXME(emilio): We should assert that it holds if pseudo.is_none()! + properties::cascade::<E>( + &self, + pseudo, + inputs.rules.as_ref().unwrap_or(self.rule_tree.root()), + guards, + parent_style, + layout_parent_style, + first_line_reparenting, + visited_rules, + inputs.flags, + rule_cache, + rule_cache_conditions, + element, + ) + } + + /// Computes the cascade inputs for a lazily-cascaded pseudo-element. + /// + /// See the documentation on lazy pseudo-elements in + /// docs/components/style.md + fn lazy_pseudo_rules<E>( + &self, + guards: &StylesheetGuards, + element: E, + originating_element_style: &ComputedValues, + pseudo: &PseudoElement, + is_probe: bool, + rule_inclusion: RuleInclusion, + matching_fn: Option<&dyn Fn(&PseudoElement) -> bool>, + ) -> Option<CascadeInputs> + where + E: TElement, + { + debug_assert!(pseudo.is_lazy()); + + let mut selector_caches = SelectorCaches::default(); + // No need to bother setting the selector flags when we're computing + // default styles. + let needs_selector_flags = if rule_inclusion == RuleInclusion::DefaultOnly { + NeedsSelectorFlags::No + } else { + NeedsSelectorFlags::Yes + }; + + let mut declarations = ApplicableDeclarationList::new(); + let mut matching_context = MatchingContext::<'_, E::Impl>::new( + MatchingMode::ForStatelessPseudoElement, + None, + &mut selector_caches, + self.quirks_mode, + needs_selector_flags, + MatchingForInvalidation::No, + ); + + matching_context.pseudo_element_matching_fn = matching_fn; + matching_context.extra_data.originating_element_style = Some(originating_element_style); + + self.push_applicable_declarations( + element, + Some(&pseudo), + None, + None, + /* animation_declarations = */ Default::default(), + rule_inclusion, + &mut declarations, + &mut matching_context, + ); + + if declarations.is_empty() && is_probe { + return None; + } + + let rules = self.rule_tree.compute_rule_node(&mut declarations, guards); + + let mut visited_rules = None; + if originating_element_style.visited_style().is_some() { + let mut declarations = ApplicableDeclarationList::new(); + let mut selector_caches = SelectorCaches::default(); + + let mut matching_context = MatchingContext::<'_, E::Impl>::new_for_visited( + MatchingMode::ForStatelessPseudoElement, + None, + &mut selector_caches, + VisitedHandlingMode::RelevantLinkVisited, + self.quirks_mode, + needs_selector_flags, + MatchingForInvalidation::No, + ); + matching_context.pseudo_element_matching_fn = matching_fn; + matching_context.extra_data.originating_element_style = Some(originating_element_style); + + self.push_applicable_declarations( + element, + Some(&pseudo), + None, + None, + /* animation_declarations = */ Default::default(), + rule_inclusion, + &mut declarations, + &mut matching_context, + ); + if !declarations.is_empty() { + let rule_node = self.rule_tree.insert_ordered_rules_with_important( + declarations.drain(..).map(|a| a.for_rule_tree()), + guards, + ); + if rule_node != *self.rule_tree.root() { + visited_rules = Some(rule_node); + } + } + } + + Some(CascadeInputs { + rules: Some(rules), + visited_rules, + flags: matching_context.extra_data.cascade_input_flags, + }) + } + + /// Set a given device, which may change the styles that apply to the + /// document. + /// + /// Returns the sheet origins that were actually affected. + /// + /// This means that we may need to rebuild style data even if the + /// stylesheets haven't changed. + /// + /// Also, the device that arrives here may need to take the viewport rules + /// into account. + pub fn set_device(&mut self, device: Device, guards: &StylesheetGuards) -> OriginSet { + self.device = device; + self.media_features_change_changed_style(guards, &self.device) + } + + /// Returns whether, given a media feature change, any previously-applicable + /// style has become non-applicable, or vice-versa for each origin, using + /// `device`. + pub fn media_features_change_changed_style( + &self, + guards: &StylesheetGuards, + device: &Device, + ) -> OriginSet { + debug!("Stylist::media_features_change_changed_style {:?}", device); + + let mut origins = OriginSet::empty(); + let stylesheets = self.stylesheets.iter(); + + for (stylesheet, origin) in stylesheets { + if origins.contains(origin.into()) { + continue; + } + + let guard = guards.for_origin(origin); + let origin_cascade_data = self.cascade_data.borrow_for_origin(origin); + + let affected_changed = !origin_cascade_data.media_feature_affected_matches( + stylesheet, + guard, + device, + self.quirks_mode, + ); + + if affected_changed { + origins |= origin; + } + } + + origins + } + + /// Returns the Quirks Mode of the document. + pub fn quirks_mode(&self) -> QuirksMode { + self.quirks_mode + } + + /// Sets the quirks mode of the document. + pub fn set_quirks_mode(&mut self, quirks_mode: QuirksMode) { + if self.quirks_mode == quirks_mode { + return; + } + self.quirks_mode = quirks_mode; + self.force_stylesheet_origins_dirty(OriginSet::all()); + } + + /// Returns the applicable CSS declarations for the given element. + pub fn push_applicable_declarations<E>( + &self, + element: E, + pseudo_element: Option<&PseudoElement>, + style_attribute: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>, + smil_override: Option<ArcBorrow<Locked<PropertyDeclarationBlock>>>, + animation_declarations: AnimationDeclarations, + rule_inclusion: RuleInclusion, + applicable_declarations: &mut ApplicableDeclarationList, + context: &mut MatchingContext<E::Impl>, + ) where + E: TElement, + { + RuleCollector::new( + self, + element, + pseudo_element, + style_attribute, + smil_override, + animation_declarations, + rule_inclusion, + applicable_declarations, + context, + ) + .collect_all(); + } + + /// Given an id, returns whether there might be any rules for that id in any + /// of our rule maps. + #[inline] + pub fn may_have_rules_for_id<E>(&self, id: &WeakAtom, element: E) -> bool + where + E: TElement, + { + // If id needs to be compared case-insensitively, the logic below + // wouldn't work. Just conservatively assume it may have such rules. + match self.quirks_mode().classes_and_ids_case_sensitivity() { + CaseSensitivity::AsciiCaseInsensitive => return true, + CaseSensitivity::CaseSensitive => {}, + } + + self.any_applicable_rule_data(element, |data| data.mapped_ids.contains(id)) + } + + /// Returns the registered `@keyframes` animation for the specified name. + #[inline] + pub fn get_animation<'a, E>(&'a self, name: &Atom, element: E) -> Option<&'a KeyframesAnimation> + where + E: TElement + 'a, + { + macro_rules! try_find_in { + ($data:expr) => { + if let Some(animation) = $data.animations.get(name) { + return Some(animation); + } + }; + } + + // NOTE(emilio): This is a best-effort thing, the right fix is a bit TBD because it + // involves "recording" which tree the name came from, see [1][2]. + // + // [1]: https://github.com/w3c/csswg-drafts/issues/1995 + // [2]: https://bugzil.la/1458189 + let mut animation = None; + let doc_rules_apply = + element.each_applicable_non_document_style_rule_data(|data, _host| { + if animation.is_none() { + animation = data.animations.get(name); + } + }); + + if animation.is_some() { + return animation; + } + + if doc_rules_apply { + try_find_in!(self.cascade_data.author); + } + try_find_in!(self.cascade_data.user); + try_find_in!(self.cascade_data.user_agent.cascade_data); + + None + } + + /// Computes the match results of a given element against the set of + /// revalidation selectors. + pub fn match_revalidation_selectors<E>( + &self, + element: E, + bloom: Option<&BloomFilter>, + selector_caches: &mut SelectorCaches, + needs_selector_flags: NeedsSelectorFlags, + ) -> RevalidationResult + where + E: TElement, + { + // NB: `MatchingMode` doesn't really matter, given we don't share style + // between pseudos. + let mut matching_context = MatchingContext::new( + MatchingMode::Normal, + bloom, + selector_caches, + self.quirks_mode, + needs_selector_flags, + MatchingForInvalidation::No, + ); + + // Note that, by the time we're revalidating, we're guaranteed that the + // candidate and the entry have the same id, classes, and local name. + // This means we're guaranteed to get the same rulehash buckets for all + // the lookups, which means that the bitvecs are comparable. We verify + // this in the caller by asserting that the bitvecs are same-length. + let mut result = RevalidationResult::default(); + let mut relevant_attributes = &mut result.relevant_attributes; + let selectors_matched = &mut result.selectors_matched; + + let matches_document_rules = + element.each_applicable_non_document_style_rule_data(|data, host| { + matching_context.with_shadow_host(Some(host), |matching_context| { + data.selectors_for_cache_revalidation.lookup( + element, + self.quirks_mode, + Some(&mut relevant_attributes), + |selector_and_hashes| { + selectors_matched.push(matches_selector( + &selector_and_hashes.selector, + selector_and_hashes.selector_offset, + Some(&selector_and_hashes.hashes), + &element, + matching_context, + )); + true + }, + ); + }) + }); + + for (data, origin) in self.cascade_data.iter_origins() { + if origin == Origin::Author && !matches_document_rules { + continue; + } + + data.selectors_for_cache_revalidation.lookup( + element, + self.quirks_mode, + Some(&mut relevant_attributes), + |selector_and_hashes| { + selectors_matched.push(matches_selector( + &selector_and_hashes.selector, + selector_and_hashes.selector_offset, + Some(&selector_and_hashes.hashes), + &element, + &mut matching_context, + )); + true + }, + ); + } + + result + } + + /// Computes styles for a given declaration with parent_style. + /// + /// FIXME(emilio): the lack of pseudo / cascade flags look quite dubious, + /// hopefully this is only used for some canvas font stuff. + /// + /// TODO(emilio): The type parameter can go away when + /// https://github.com/rust-lang/rust/issues/35121 is fixed. + pub fn compute_for_declarations<E>( + &self, + guards: &StylesheetGuards, + parent_style: &ComputedValues, + declarations: Arc<Locked<PropertyDeclarationBlock>>, + ) -> Arc<ComputedValues> + where + E: TElement, + { + let block = declarations.read_with(guards.author); + + // We don't bother inserting these declarations in the rule tree, since + // it'd be quite useless and slow. + // + // TODO(emilio): Now that we fixed bug 1493420, we should consider + // reversing this as it shouldn't be slow anymore, and should avoid + // generating two instantiations of apply_declarations. + properties::apply_declarations::<E, _>( + &self, + /* pseudo = */ None, + self.rule_tree.root(), + guards, + block.declaration_importance_iter().map(|(declaration, _)| { + ( + declaration, + CascadePriority::new( + CascadeLevel::same_tree_author_normal(), + LayerOrder::root(), + ), + ) + }), + Some(parent_style), + Some(parent_style), + FirstLineReparenting::No, + CascadeMode::Unvisited { + visited_rules: None, + }, + Default::default(), + /* rule_cache = */ None, + &mut Default::default(), + /* element = */ None, + ) + } + + /// Accessor for a shared reference to the device. + #[inline] + pub fn device(&self) -> &Device { + &self.device + } + + /// Accessor for a mutable reference to the device. + #[inline] + pub fn device_mut(&mut self) -> &mut Device { + &mut self.device + } + + /// Accessor for a shared reference to the rule tree. + #[inline] + pub fn rule_tree(&self) -> &RuleTree { + &self.rule_tree + } + + /// Returns the script-registered custom property registry. + #[inline] + pub fn custom_property_script_registry(&self) -> &CustomPropertyScriptRegistry { + &self.script_custom_properties + } + + /// Returns the script-registered custom property registry, as a mutable ref. + #[inline] + pub fn custom_property_script_registry_mut(&mut self) -> &mut CustomPropertyScriptRegistry { + &mut self.script_custom_properties + } + + /// Measures heap usage. + #[cfg(feature = "gecko")] + pub fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.cascade_data.add_size_of(ops, sizes); + self.author_data_cache.add_size_of(ops, sizes); + sizes.mRuleTree += self.rule_tree.size_of(ops); + + // We may measure other fields in the future if DMD says it's worth it. + } + + /// Shutdown the static data that this module stores. + pub fn shutdown() { + let _entries = UA_CASCADE_DATA_CACHE.lock().unwrap().take_all(); + } +} + +/// A vector that is sorted in layer order. +#[derive(Clone, Debug, Deref, MallocSizeOf)] +pub struct LayerOrderedVec<T>(Vec<(T, LayerId)>); +impl<T> Default for LayerOrderedVec<T> { + fn default() -> Self { + Self(Default::default()) + } +} + +/// A map that is sorted in layer order. +#[derive(Clone, Debug, Deref, MallocSizeOf)] +pub struct LayerOrderedMap<T>(PrecomputedHashMap<Atom, SmallVec<[(T, LayerId); 1]>>); +impl<T> Default for LayerOrderedMap<T> { + fn default() -> Self { + Self(Default::default()) + } +} + +impl<T: 'static> LayerOrderedVec<T> { + fn clear(&mut self) { + self.0.clear(); + } + fn push(&mut self, v: T, id: LayerId) { + self.0.push((v, id)); + } + fn sort(&mut self, layers: &[CascadeLayer]) { + self.0 + .sort_by_key(|&(_, ref id)| layers[id.0 as usize].order) + } +} + +impl<T: 'static> LayerOrderedMap<T> { + fn shrink_if_needed(&mut self) { + self.0.shrink_if_needed(); + } + fn clear(&mut self) { + self.0.clear(); + } + fn try_insert(&mut self, name: Atom, v: T, id: LayerId) -> Result<(), AllocErr> { + self.try_insert_with(name, v, id, |_, _| Ordering::Equal) + } + fn try_insert_with( + &mut self, + name: Atom, + v: T, + id: LayerId, + cmp: impl Fn(&T, &T) -> Ordering, + ) -> Result<(), AllocErr> { + self.0.try_reserve(1)?; + let vec = self.0.entry(name).or_default(); + if let Some(&mut (ref mut val, ref last_id)) = vec.last_mut() { + if *last_id == id { + if cmp(&val, &v) != Ordering::Greater { + *val = v; + } + return Ok(()); + } + } + vec.push((v, id)); + Ok(()) + } + fn sort(&mut self, layers: &[CascadeLayer]) { + self.sort_with(layers, |_, _| Ordering::Equal) + } + fn sort_with(&mut self, layers: &[CascadeLayer], cmp: impl Fn(&T, &T) -> Ordering) { + for (_, v) in self.0.iter_mut() { + v.sort_by(|&(ref v1, ref id1), &(ref v2, ref id2)| { + let order1 = layers[id1.0 as usize].order; + let order2 = layers[id2.0 as usize].order; + order1.cmp(&order2).then_with(|| cmp(v1, v2)) + }) + } + } + /// Get an entry on the LayerOrderedMap by name. + pub fn get(&self, name: &Atom) -> Option<&T> { + let vec = self.0.get(name)?; + Some(&vec.last()?.0) + } +} + +/// Wrapper to allow better tracking of memory usage by page rule lists. +/// +/// This includes the layer ID for use with the named page table. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct PageRuleData { + /// Layer ID for sorting page rules after matching. + pub layer: LayerId, + /// Page rule + #[ignore_malloc_size_of = "Arc, stylesheet measures as primary ref"] + pub rule: Arc<Locked<PageRule>>, +} + +/// Stores page rules indexed by page names. +#[derive(Clone, Debug, Default, MallocSizeOf)] +pub struct PageRuleMap { + /// Page rules, indexed by page name. An empty atom indicates no page name. + pub rules: PrecomputedHashMap<Atom, SmallVec<[PageRuleData; 1]>>, +} + +impl PageRuleMap { + #[inline] + fn clear(&mut self) { + self.rules.clear(); + } + + /// Uses page-name and pseudo-classes to match all applicable + /// page-rules and append them to the matched_rules vec. + /// This will ensure correct rule order for cascading. + pub fn match_and_append_rules( + &self, + matched_rules: &mut Vec<ApplicableDeclarationBlock>, + origin: Origin, + guards: &StylesheetGuards, + cascade_data: &DocumentCascadeData, + name: &Option<Atom>, + pseudos: PagePseudoClassFlags, + ) { + let level = match origin { + Origin::UserAgent => CascadeLevel::UANormal, + Origin::User => CascadeLevel::UserNormal, + Origin::Author => CascadeLevel::same_tree_author_normal(), + }; + let cascade_data = cascade_data.borrow_for_origin(origin); + let start = matched_rules.len(); + + self.match_and_add_rules( + matched_rules, + level, + guards, + cascade_data, + &atom!(""), + pseudos, + ); + if let Some(name) = name { + self.match_and_add_rules(matched_rules, level, guards, cascade_data, name, pseudos); + } + + // Because page-rules do not have source location information stored, + // use stable sort to ensure source locations are preserved. + matched_rules[start..] + .sort_by_key(|block| (block.layer_order(), block.specificity, block.source_order())); + } + + fn match_and_add_rules( + &self, + extra_declarations: &mut Vec<ApplicableDeclarationBlock>, + level: CascadeLevel, + guards: &StylesheetGuards, + cascade_data: &CascadeData, + name: &Atom, + pseudos: PagePseudoClassFlags, + ) { + let rules = match self.rules.get(name) { + Some(rules) => rules, + None => return, + }; + for data in rules.iter() { + let rule = data.rule.read_with(level.guard(&guards)); + let specificity = match rule.match_specificity(pseudos) { + Some(specificity) => specificity, + None => continue, + }; + let block = rule.block.clone(); + extra_declarations.push(ApplicableDeclarationBlock::new( + StyleSource::from_declarations(block), + 0, + level, + specificity, + cascade_data.layer_order_for(data.layer), + )); + } + } +} + +impl MallocShallowSizeOf for PageRuleMap { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.rules.shallow_size_of(ops) + } +} + +/// This struct holds data which users of Stylist may want to extract +/// from stylesheets which can be done at the same time as updating. +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct ExtraStyleData { + /// A list of effective font-face rules and their origin. + #[cfg(feature = "gecko")] + pub font_faces: LayerOrderedVec<Arc<Locked<FontFaceRule>>>, + + /// A list of effective font-feature-values rules. + #[cfg(feature = "gecko")] + pub font_feature_values: LayerOrderedVec<Arc<FontFeatureValuesRule>>, + + /// A list of effective font-palette-values rules. + #[cfg(feature = "gecko")] + pub font_palette_values: LayerOrderedVec<Arc<FontPaletteValuesRule>>, + + /// A map of effective counter-style rules. + #[cfg(feature = "gecko")] + pub counter_styles: LayerOrderedMap<Arc<Locked<CounterStyleRule>>>, + + /// A map of effective page rules. + #[cfg(feature = "gecko")] + pub pages: PageRuleMap, +} + +#[cfg(feature = "gecko")] +impl ExtraStyleData { + /// Add the given @font-face rule. + fn add_font_face(&mut self, rule: &Arc<Locked<FontFaceRule>>, layer: LayerId) { + self.font_faces.push(rule.clone(), layer); + } + + /// Add the given @font-feature-values rule. + fn add_font_feature_values(&mut self, rule: &Arc<FontFeatureValuesRule>, layer: LayerId) { + self.font_feature_values.push(rule.clone(), layer); + } + + /// Add the given @font-palette-values rule. + fn add_font_palette_values(&mut self, rule: &Arc<FontPaletteValuesRule>, layer: LayerId) { + self.font_palette_values.push(rule.clone(), layer); + } + + /// Add the given @counter-style rule. + fn add_counter_style( + &mut self, + guard: &SharedRwLockReadGuard, + rule: &Arc<Locked<CounterStyleRule>>, + layer: LayerId, + ) -> Result<(), AllocErr> { + let name = rule.read_with(guard).name().0.clone(); + self.counter_styles.try_insert(name, rule.clone(), layer) + } + + /// Add the given @page rule. + fn add_page( + &mut self, + guard: &SharedRwLockReadGuard, + rule: &Arc<Locked<PageRule>>, + layer: LayerId, + ) -> Result<(), AllocErr> { + let page_rule = rule.read_with(guard); + let mut add_rule = |name| { + let vec = self.pages.rules.entry(name).or_default(); + vec.push(PageRuleData { + layer, + rule: rule.clone(), + }); + }; + if page_rule.selectors.0.is_empty() { + add_rule(atom!("")); + } else { + for selector in page_rule.selectors.as_slice() { + add_rule(selector.name.0.clone()); + } + } + Ok(()) + } + + fn sort_by_layer(&mut self, layers: &[CascadeLayer]) { + self.font_faces.sort(layers); + self.font_feature_values.sort(layers); + self.font_palette_values.sort(layers); + self.counter_styles.sort(layers); + } + + fn clear(&mut self) { + #[cfg(feature = "gecko")] + { + self.font_faces.clear(); + self.font_feature_values.clear(); + self.font_palette_values.clear(); + self.counter_styles.clear(); + self.pages.clear(); + } + } +} + +// Don't let a prefixed keyframes animation override +// a non-prefixed one. +fn compare_keyframes_in_same_layer(v1: &KeyframesAnimation, v2: &KeyframesAnimation) -> Ordering { + if v1.vendor_prefix.is_some() == v2.vendor_prefix.is_some() { + Ordering::Equal + } else if v2.vendor_prefix.is_some() { + Ordering::Greater + } else { + Ordering::Less + } +} + +/// An iterator over the different ExtraStyleData. +pub struct ExtraStyleDataIterator<'a>(DocumentCascadeDataIter<'a>); + +impl<'a> Iterator for ExtraStyleDataIterator<'a> { + type Item = (&'a ExtraStyleData, Origin); + + fn next(&mut self) -> Option<Self::Item> { + self.0.next().map(|d| (&d.0.extra_data, d.1)) + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for ExtraStyleData { + /// Measure heap usage. + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut n = 0; + n += self.font_faces.shallow_size_of(ops); + n += self.font_feature_values.shallow_size_of(ops); + n += self.font_palette_values.shallow_size_of(ops); + n += self.counter_styles.shallow_size_of(ops); + n += self.pages.shallow_size_of(ops); + n + } +} + +/// SelectorMapEntry implementation for use in our revalidation selector map. +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Debug)] +struct RevalidationSelectorAndHashes { + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "CssRules have primary refs, we measure there" + )] + selector: Selector<SelectorImpl>, + selector_offset: usize, + hashes: AncestorHashes, +} + +impl RevalidationSelectorAndHashes { + fn new(selector: Selector<SelectorImpl>, hashes: AncestorHashes) -> Self { + let selector_offset = { + // We basically want to check whether the first combinator is a + // pseudo-element combinator. If it is, we want to use the offset + // one past it. Otherwise, our offset is 0. + let mut index = 0; + let mut iter = selector.iter(); + + // First skip over the first ComplexSelector. + // + // We can't check what sort of what combinator we have until we do + // that. + for _ in &mut iter { + index += 1; // Simple selector + } + + match iter.next_sequence() { + Some(Combinator::PseudoElement) => index + 1, // +1 for the combinator + _ => 0, + } + }; + + RevalidationSelectorAndHashes { + selector, + selector_offset, + hashes, + } + } +} + +impl SelectorMapEntry for RevalidationSelectorAndHashes { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.selector.iter_from(self.selector_offset) + } +} + +/// A selector visitor implementation that collects all the state the Stylist +/// cares about a selector. +struct StylistSelectorVisitor<'a> { + /// Whether we've past the rightmost compound selector, not counting + /// pseudo-elements. + passed_rightmost_selector: bool, + + /// Whether the selector needs revalidation for the style sharing cache. + needs_revalidation: &'a mut bool, + + /// Flags for which selector list-containing components the visitor is + /// inside of, if any + in_selector_list_of: SelectorListKind, + + /// The filter with all the id's getting referenced from rightmost + /// selectors. + mapped_ids: &'a mut PrecomputedHashSet<Atom>, + + /// The filter with the IDs getting referenced from the selector list of + /// :nth-child(... of <selector list>) selectors. + nth_of_mapped_ids: &'a mut PrecomputedHashSet<Atom>, + + /// The filter with the local names of attributes there are selectors for. + attribute_dependencies: &'a mut PrecomputedHashSet<LocalName>, + + /// The filter with the classes getting referenced from the selector list of + /// :nth-child(... of <selector list>) selectors. + nth_of_class_dependencies: &'a mut PrecomputedHashSet<Atom>, + + /// The filter with the local names of attributes there are selectors for + /// within the selector list of :nth-child(... of <selector list>) + /// selectors. + nth_of_attribute_dependencies: &'a mut PrecomputedHashSet<LocalName>, + + /// All the states selectors in the page reference. + state_dependencies: &'a mut ElementState, + + /// All the state selectors in the page reference within the selector list + /// of :nth-child(... of <selector list>) selectors. + nth_of_state_dependencies: &'a mut ElementState, + + /// All the document states selectors in the page reference. + document_state_dependencies: &'a mut DocumentState, +} + +fn component_needs_revalidation( + c: &Component<SelectorImpl>, + passed_rightmost_selector: bool, +) -> bool { + match *c { + Component::ID(_) => { + // TODO(emilio): This could also check that the ID is not already in + // the rule hash. In that case, we could avoid making this a + // revalidation selector too. + // + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1369611 + passed_rightmost_selector + }, + Component::AttributeInNoNamespaceExists { .. } | + Component::AttributeInNoNamespace { .. } | + Component::AttributeOther(_) | + Component::Empty | + Component::Nth(_) | + Component::NthOf(_) | + Component::Has(_) => true, + Component::NonTSPseudoClass(ref p) => p.needs_cache_revalidation(), + _ => false, + } +} + +impl<'a> StylistSelectorVisitor<'a> { + fn visit_nested_selector( + &mut self, + in_selector_list_of: SelectorListKind, + selector: &Selector<SelectorImpl>, + ) { + let old_passed_rightmost_selector = self.passed_rightmost_selector; + let old_in_selector_list_of = self.in_selector_list_of; + + self.passed_rightmost_selector = false; + self.in_selector_list_of = in_selector_list_of; + let _ret = selector.visit(self); + debug_assert!(_ret, "We never return false"); + + self.passed_rightmost_selector = old_passed_rightmost_selector; + self.in_selector_list_of = old_in_selector_list_of; + } +} + +impl<'a> SelectorVisitor for StylistSelectorVisitor<'a> { + type Impl = SelectorImpl; + + fn visit_complex_selector(&mut self, combinator: Option<Combinator>) -> bool { + *self.needs_revalidation = + *self.needs_revalidation || combinator.map_or(false, |c| c.is_sibling()); + + // NOTE(emilio): this call happens before we visit any of the simple + // selectors in the next ComplexSelector, so we can use this to skip + // looking at them. + self.passed_rightmost_selector = self.passed_rightmost_selector || + !matches!(combinator, None | Some(Combinator::PseudoElement)); + + true + } + + fn visit_selector_list( + &mut self, + list_kind: SelectorListKind, + list: &[Selector<Self::Impl>], + ) -> bool { + let in_selector_list_of = self.in_selector_list_of | list_kind; + for selector in list { + self.visit_nested_selector(in_selector_list_of, selector); + } + true + } + + fn visit_relative_selector_list( + &mut self, + list: &[selectors::parser::RelativeSelector<Self::Impl>], + ) -> bool { + let in_selector_list_of = self.in_selector_list_of | SelectorListKind::HAS; + for selector in list { + self.visit_nested_selector(in_selector_list_of, &selector.selector); + } + true + } + + fn visit_attribute_selector( + &mut self, + _ns: &NamespaceConstraint<&Namespace>, + name: &LocalName, + lower_name: &LocalName, + ) -> bool { + if self.in_selector_list_of.relevant_to_nth_of_dependencies() { + self.nth_of_attribute_dependencies.insert(name.clone()); + if name != lower_name { + self.nth_of_attribute_dependencies + .insert(lower_name.clone()); + } + } + + self.attribute_dependencies.insert(name.clone()); + if name != lower_name { + self.attribute_dependencies.insert(lower_name.clone()); + } + + true + } + + fn visit_simple_selector(&mut self, s: &Component<SelectorImpl>) -> bool { + *self.needs_revalidation = *self.needs_revalidation || + component_needs_revalidation(s, self.passed_rightmost_selector); + + match *s { + Component::NonTSPseudoClass(ref p) => { + self.state_dependencies.insert(p.state_flag()); + self.document_state_dependencies + .insert(p.document_state_flag()); + + if self.in_selector_list_of.relevant_to_nth_of_dependencies() { + self.nth_of_state_dependencies.insert(p.state_flag()); + } + }, + Component::ID(ref id) => { + // We want to stop storing mapped ids as soon as we've moved off + // the rightmost ComplexSelector that is not a pseudo-element. + // + // That can be detected by a visit_complex_selector call with a + // combinator other than None and PseudoElement. + // + // Importantly, this call happens before we visit any of the + // simple selectors in that ComplexSelector. + // + // NOTE(emilio): See the comment regarding on when this may + // break in visit_complex_selector. + if !self.passed_rightmost_selector { + self.mapped_ids.insert(id.0.clone()); + } + + if self.in_selector_list_of.relevant_to_nth_of_dependencies() { + self.nth_of_mapped_ids.insert(id.0.clone()); + } + }, + Component::Class(ref class) + if self.in_selector_list_of.relevant_to_nth_of_dependencies() => + { + self.nth_of_class_dependencies.insert(class.0.clone()); + }, + _ => {}, + } + + true + } +} + +/// A set of rules for element and pseudo-elements. +#[derive(Clone, Debug, Default, MallocSizeOf)] +struct GenericElementAndPseudoRules<Map> { + /// Rules from stylesheets at this `CascadeData`'s origin. + element_map: Map, + + /// Rules from stylesheets at this `CascadeData`'s origin that correspond + /// to a given pseudo-element. + /// + /// FIXME(emilio): There are a bunch of wasted entries here in practice. + /// Figure out a good way to do a `PerNonAnonBox` and `PerAnonBox` (for + /// `precomputed_values_for_pseudo`) without duplicating a lot of code. + pseudos_map: PerPseudoElementMap<Box<Map>>, +} + +impl<Map: Default + MallocSizeOf> GenericElementAndPseudoRules<Map> { + #[inline(always)] + fn for_insertion(&mut self, pseudo_element: Option<&PseudoElement>) -> &mut Map { + debug_assert!( + pseudo_element.map_or(true, |pseudo| { + !pseudo.is_precomputed() && !pseudo.is_unknown_webkit_pseudo_element() + }), + "Precomputed pseudos should end up in precomputed_pseudo_element_decls, \ + and unknown webkit pseudos should be discarded before getting here" + ); + + match pseudo_element { + None => &mut self.element_map, + Some(pseudo) => self + .pseudos_map + .get_or_insert_with(pseudo, || Box::new(Default::default())), + } + } + + #[inline] + fn rules(&self, pseudo: Option<&PseudoElement>) -> Option<&Map> { + match pseudo { + Some(pseudo) => self.pseudos_map.get(pseudo).map(|p| &**p), + None => Some(&self.element_map), + } + } + + /// Measures heap usage. + #[cfg(feature = "gecko")] + fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + sizes.mElementAndPseudosMaps += self.element_map.size_of(ops); + + for elem in self.pseudos_map.iter() { + if let Some(ref elem) = *elem { + sizes.mElementAndPseudosMaps += <Box<_> as MallocSizeOf>::size_of(elem, ops); + } + } + } +} + +type ElementAndPseudoRules = GenericElementAndPseudoRules<SelectorMap<Rule>>; +type PartMap = PrecomputedHashMap<Atom, SmallVec<[Rule; 1]>>; +type PartElementAndPseudoRules = GenericElementAndPseudoRules<PartMap>; + +impl ElementAndPseudoRules { + // TODO(emilio): Should we retain storage of these? + fn clear(&mut self) { + self.element_map.clear(); + self.pseudos_map.clear(); + } + + fn shrink_if_needed(&mut self) { + self.element_map.shrink_if_needed(); + for pseudo in self.pseudos_map.iter_mut() { + if let Some(ref mut pseudo) = pseudo { + pseudo.shrink_if_needed(); + } + } + } +} + +impl PartElementAndPseudoRules { + // TODO(emilio): Should we retain storage of these? + fn clear(&mut self) { + self.element_map.clear(); + self.pseudos_map.clear(); + } +} + +/// The id of a given layer, a sequentially-increasing identifier. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord)] +pub struct LayerId(u16); + +impl LayerId { + /// The id of the root layer. + pub const fn root() -> Self { + Self(0) + } +} + +#[derive(Clone, Debug, MallocSizeOf)] +struct CascadeLayer { + id: LayerId, + order: LayerOrder, + children: Vec<LayerId>, +} + +impl CascadeLayer { + const fn root() -> Self { + Self { + id: LayerId::root(), + order: LayerOrder::root(), + children: vec![], + } + } +} + +/// The id of a given container condition, a sequentially-increasing identifier +/// for a given style set. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, Ord)] +pub struct ContainerConditionId(u16); + +impl ContainerConditionId { + /// A special id that represents no container rule all. + pub const fn none() -> Self { + Self(0) + } +} + +#[derive(Clone, Debug, MallocSizeOf)] +struct ContainerConditionReference { + parent: ContainerConditionId, + #[ignore_malloc_size_of = "Arc"] + condition: Option<Arc<ContainerCondition>>, +} + +impl ContainerConditionReference { + const fn none() -> Self { + Self { + parent: ContainerConditionId::none(), + condition: None, + } + } +} + +/// Data resulting from performing the CSS cascade that is specific to a given +/// origin. +/// +/// FIXME(emilio): Consider renaming and splitting in `CascadeData` and +/// `InvalidationData`? That'd make `clear_cascade_data()` clearer. +#[derive(Debug, Clone, MallocSizeOf)] +pub struct CascadeData { + /// The data coming from normal style rules that apply to elements at this + /// cascade level. + normal_rules: ElementAndPseudoRules, + + /// The `:host` pseudo rules that are the rightmost selector (without + /// accounting for pseudo-elements). + host_rules: Option<Box<ElementAndPseudoRules>>, + + /// The data coming from ::slotted() pseudo-element rules. + /// + /// We need to store them separately because an element needs to match + /// ::slotted() pseudo-element rules in different shadow roots. + /// + /// In particular, we need to go through all the style data in all the + /// containing style scopes starting from the closest assigned slot. + slotted_rules: Option<Box<ElementAndPseudoRules>>, + + /// The data coming from ::part() pseudo-element rules. + /// + /// We need to store them separately because an element needs to match + /// ::part() pseudo-element rules in different shadow roots. + part_rules: Option<Box<PartElementAndPseudoRules>>, + + /// The invalidation map for these rules. + invalidation_map: InvalidationMap, + + /// The relative selector equivalent of the invalidation map. + relative_selector_invalidation_map: RelativeSelectorInvalidationMap, + + /// The attribute local names that appear in attribute selectors. Used + /// to avoid taking element snapshots when an irrelevant attribute changes. + /// (We don't bother storing the namespace, since namespaced attributes are + /// rare.) + attribute_dependencies: PrecomputedHashSet<LocalName>, + + /// The classes that appear in the selector list of + /// :nth-child(... of <selector list>). Used to avoid restyling siblings of + /// an element when an irrelevant class changes. + nth_of_class_dependencies: PrecomputedHashSet<Atom>, + + /// The attributes that appear in the selector list of + /// :nth-child(... of <selector list>). Used to avoid restyling siblings of + /// an element when an irrelevant attribute changes. + nth_of_attribute_dependencies: PrecomputedHashSet<LocalName>, + + /// The element state bits that are relied on by selectors. Like + /// `attribute_dependencies`, this is used to avoid taking element snapshots + /// when an irrelevant element state bit changes. + state_dependencies: ElementState, + + /// The element state bits that are relied on by selectors that appear in + /// the selector list of :nth-child(... of <selector list>). + nth_of_state_dependencies: ElementState, + + /// The document state bits that are relied on by selectors. This is used + /// to tell whether we need to restyle the entire document when a document + /// state bit changes. + document_state_dependencies: DocumentState, + + /// The ids that appear in the rightmost complex selector of selectors (and + /// hence in our selector maps). Used to determine when sharing styles is + /// safe: we disallow style sharing for elements whose id matches this + /// filter, and hence might be in one of our selector maps. + mapped_ids: PrecomputedHashSet<Atom>, + + /// The IDs that appear in the selector list of + /// :nth-child(... of <selector list>). Used to avoid restyling siblings + /// of an element when an irrelevant ID changes. + nth_of_mapped_ids: PrecomputedHashSet<Atom>, + + /// Selectors that require explicit cache revalidation (i.e. which depend + /// on state that is not otherwise visible to the cache, like attributes or + /// tree-structural state like child index and pseudos). + #[ignore_malloc_size_of = "Arc"] + selectors_for_cache_revalidation: SelectorMap<RevalidationSelectorAndHashes>, + + /// A map with all the animations at this `CascadeData`'s origin, indexed + /// by name. + animations: LayerOrderedMap<KeyframesAnimation>, + + /// A map with all the layer-ordered registrations from style at this `CascadeData`'s origin, + /// indexed by name. + #[ignore_malloc_size_of = "Arc"] + custom_property_registrations: LayerOrderedMap<Arc<PropertyRegistration>>, + + /// A map from cascade layer name to layer order. + layer_id: FxHashMap<LayerName, LayerId>, + + /// The list of cascade layers, indexed by their layer id. + layers: SmallVec<[CascadeLayer; 1]>, + + /// The list of container conditions, indexed by their id. + container_conditions: SmallVec<[ContainerConditionReference; 1]>, + + /// Effective media query results cached from the last rebuild. + effective_media_query_results: EffectiveMediaQueryResults, + + /// Extra data, like different kinds of rules, etc. + extra_data: ExtraStyleData, + + /// A monotonically increasing counter to represent the order on which a + /// style rule appears in a stylesheet, needed to sort them by source order. + rules_source_order: u32, + + /// The total number of selectors. + num_selectors: usize, + + /// The total number of declarations. + num_declarations: usize, +} + +impl CascadeData { + /// Creates an empty `CascadeData`. + pub fn new() -> Self { + Self { + normal_rules: ElementAndPseudoRules::default(), + host_rules: None, + slotted_rules: None, + part_rules: None, + invalidation_map: InvalidationMap::new(), + relative_selector_invalidation_map: RelativeSelectorInvalidationMap::new(), + nth_of_mapped_ids: PrecomputedHashSet::default(), + nth_of_class_dependencies: PrecomputedHashSet::default(), + nth_of_attribute_dependencies: PrecomputedHashSet::default(), + nth_of_state_dependencies: ElementState::empty(), + attribute_dependencies: PrecomputedHashSet::default(), + state_dependencies: ElementState::empty(), + document_state_dependencies: DocumentState::empty(), + mapped_ids: PrecomputedHashSet::default(), + selectors_for_cache_revalidation: SelectorMap::new(), + animations: Default::default(), + custom_property_registrations: Default::default(), + layer_id: Default::default(), + layers: smallvec::smallvec![CascadeLayer::root()], + container_conditions: smallvec::smallvec![ContainerConditionReference::none()], + extra_data: ExtraStyleData::default(), + effective_media_query_results: EffectiveMediaQueryResults::new(), + rules_source_order: 0, + num_selectors: 0, + num_declarations: 0, + } + } + + /// Rebuild the cascade data from a given SheetCollection, incrementally if + /// possible. + pub fn rebuild<'a, S>( + &mut self, + device: &Device, + quirks_mode: QuirksMode, + collection: SheetCollectionFlusher<S>, + guard: &SharedRwLockReadGuard, + ) -> Result<(), AllocErr> + where + S: StylesheetInDocument + PartialEq + 'static, + { + if !collection.dirty() { + return Ok(()); + } + + let validity = collection.data_validity(); + + match validity { + DataValidity::Valid => {}, + DataValidity::CascadeInvalid => self.clear_cascade_data(), + DataValidity::FullyInvalid => self.clear(), + } + + let mut result = Ok(()); + + collection.each(|stylesheet, rebuild_kind| { + result = self.add_stylesheet( + device, + quirks_mode, + stylesheet, + guard, + rebuild_kind, + /* precomputed_pseudo_element_decls = */ None, + ); + result.is_ok() + }); + + self.did_finish_rebuild(); + + result + } + + /// Returns the invalidation map. + pub fn invalidation_map(&self) -> &InvalidationMap { + &self.invalidation_map + } + + /// Returns the relative selector invalidation map. + pub fn relative_selector_invalidation_map(&self) -> &RelativeSelectorInvalidationMap { + &self.relative_selector_invalidation_map + } + + /// Returns whether the given ElementState bit is relied upon by a selector + /// of some rule. + #[inline] + pub fn has_state_dependency(&self, state: ElementState) -> bool { + self.state_dependencies.intersects(state) + } + + /// Returns whether the given ElementState bit is relied upon by a selector + /// of some rule in the selector list of :nth-child(... of <selector list>). + #[inline] + pub fn has_nth_of_state_dependency(&self, state: ElementState) -> bool { + self.nth_of_state_dependencies.intersects(state) + } + + /// Returns whether the given attribute might appear in an attribute + /// selector of some rule. + #[inline] + pub fn might_have_attribute_dependency(&self, local_name: &LocalName) -> bool { + self.attribute_dependencies.contains(local_name) + } + + /// Returns whether the given ID might appear in an ID selector in the + /// selector list of :nth-child(... of <selector list>). + #[inline] + pub fn might_have_nth_of_id_dependency(&self, id: &Atom) -> bool { + self.nth_of_mapped_ids.contains(id) + } + + /// Returns whether the given class might appear in a class selector in the + /// selector list of :nth-child(... of <selector list>). + #[inline] + pub fn might_have_nth_of_class_dependency(&self, class: &Atom) -> bool { + self.nth_of_class_dependencies.contains(class) + } + + /// Returns whether the given attribute might appear in an attribute + /// selector in the selector list of :nth-child(... of <selector list>). + #[inline] + pub fn might_have_nth_of_attribute_dependency(&self, local_name: &LocalName) -> bool { + self.nth_of_attribute_dependencies.contains(local_name) + } + + /// Returns the normal rule map for a given pseudo-element. + #[inline] + pub fn normal_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap<Rule>> { + self.normal_rules.rules(pseudo) + } + + /// Returns the host pseudo rule map for a given pseudo-element. + #[inline] + pub fn host_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap<Rule>> { + self.host_rules.as_ref().and_then(|d| d.rules(pseudo)) + } + + /// Whether there's any host rule that could match in this scope. + pub fn any_host_rules(&self) -> bool { + self.host_rules.is_some() + } + + /// Returns the slotted rule map for a given pseudo-element. + #[inline] + pub fn slotted_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&SelectorMap<Rule>> { + self.slotted_rules.as_ref().and_then(|d| d.rules(pseudo)) + } + + /// Whether there's any ::slotted rule that could match in this scope. + pub fn any_slotted_rule(&self) -> bool { + self.slotted_rules.is_some() + } + + /// Returns the parts rule map for a given pseudo-element. + #[inline] + pub fn part_rules(&self, pseudo: Option<&PseudoElement>) -> Option<&PartMap> { + self.part_rules.as_ref().and_then(|d| d.rules(pseudo)) + } + + /// Whether there's any ::part rule that could match in this scope. + pub fn any_part_rule(&self) -> bool { + self.part_rules.is_some() + } + + #[inline] + fn layer_order_for(&self, id: LayerId) -> LayerOrder { + self.layers[id.0 as usize].order + } + + pub(crate) fn container_condition_matches<E>( + &self, + mut id: ContainerConditionId, + stylist: &Stylist, + element: E, + context: &mut MatchingContext<E::Impl>, + ) -> bool + where + E: TElement, + { + loop { + let condition_ref = &self.container_conditions[id.0 as usize]; + let condition = match condition_ref.condition { + None => return true, + Some(ref c) => c, + }; + let matches = condition + .matches( + stylist, + element, + context.extra_data.originating_element_style, + &mut context.extra_data.cascade_input_flags, + ) + .to_bool(/* unknown = */ false); + if !matches { + return false; + } + id = condition_ref.parent; + } + } + + fn did_finish_rebuild(&mut self) { + self.shrink_maps_if_needed(); + self.compute_layer_order(); + } + + fn shrink_maps_if_needed(&mut self) { + self.normal_rules.shrink_if_needed(); + if let Some(ref mut host_rules) = self.host_rules { + host_rules.shrink_if_needed(); + } + if let Some(ref mut slotted_rules) = self.slotted_rules { + slotted_rules.shrink_if_needed(); + } + self.animations.shrink_if_needed(); + self.custom_property_registrations.shrink_if_needed(); + self.invalidation_map.shrink_if_needed(); + self.relative_selector_invalidation_map.shrink_if_needed(); + self.attribute_dependencies.shrink_if_needed(); + self.nth_of_attribute_dependencies.shrink_if_needed(); + self.nth_of_class_dependencies.shrink_if_needed(); + self.nth_of_mapped_ids.shrink_if_needed(); + self.mapped_ids.shrink_if_needed(); + self.layer_id.shrink_if_needed(); + self.selectors_for_cache_revalidation.shrink_if_needed(); + } + + fn compute_layer_order(&mut self) { + debug_assert_ne!( + self.layers.len(), + 0, + "There should be at least the root layer!" + ); + if self.layers.len() == 1 { + return; // Nothing to do + } + let (first, remaining) = self.layers.split_at_mut(1); + let root = &mut first[0]; + let mut order = LayerOrder::first(); + compute_layer_order_for_subtree(root, remaining, &mut order); + + // NOTE(emilio): This is a bit trickier than it should to avoid having + // to clone() around layer indices. + fn compute_layer_order_for_subtree( + parent: &mut CascadeLayer, + remaining_layers: &mut [CascadeLayer], + order: &mut LayerOrder, + ) { + for child in parent.children.iter() { + debug_assert!( + parent.id < *child, + "Children are always registered after parents" + ); + let child_index = (child.0 - parent.id.0 - 1) as usize; + let (first, remaining) = remaining_layers.split_at_mut(child_index + 1); + let child = &mut first[child_index]; + compute_layer_order_for_subtree(child, remaining, order); + } + + if parent.id != LayerId::root() { + parent.order = *order; + order.inc(); + } + } + self.extra_data.sort_by_layer(&self.layers); + self.animations + .sort_with(&self.layers, compare_keyframes_in_same_layer); + self.custom_property_registrations.sort(&self.layers) + } + + /// Collects all the applicable media query results into `results`. + /// + /// This duplicates part of the logic in `add_stylesheet`, which is + /// a bit unfortunate. + /// + /// FIXME(emilio): With a bit of smartness in + /// `media_feature_affected_matches`, we could convert + /// `EffectiveMediaQueryResults` into a vector without too much effort. + fn collect_applicable_media_query_results_into<S>( + device: &Device, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + results: &mut Vec<MediaListKey>, + contents_list: &mut StyleSheetContentList, + ) where + S: StylesheetInDocument + 'static, + { + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + return; + } + + debug!(" + {:?}", stylesheet); + let contents = stylesheet.contents(); + results.push(contents.to_media_list_key()); + + // Safety: StyleSheetContents are reference-counted with Arc. + contents_list.push(StylesheetContentsPtr(unsafe { + Arc::from_raw_addrefed(contents) + })); + + for rule in stylesheet.effective_rules(device, guard) { + match *rule { + CssRule::Import(ref lock) => { + let import_rule = lock.read_with(guard); + debug!(" + {:?}", import_rule.stylesheet.media(guard)); + results.push(import_rule.to_media_list_key()); + }, + CssRule::Media(ref media_rule) => { + debug!(" + {:?}", media_rule.media_queries.read_with(guard)); + results.push(media_rule.to_media_list_key()); + }, + _ => {}, + } + } + } + + fn add_rule_list<S>( + &mut self, + rules: std::slice::Iter<CssRule>, + device: &Device, + quirks_mode: QuirksMode, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + rebuild_kind: SheetRebuildKind, + containing_rule_state: &mut ContainingRuleState, + mut precomputed_pseudo_element_decls: Option<&mut PrecomputedPseudoElementDeclarations>, + ) -> Result<(), AllocErr> + where + S: StylesheetInDocument + 'static, + { + for rule in rules { + // Handle leaf rules first, as those are by far the most common + // ones, and are always effective, so we can skip some checks. + let mut handled = true; + let mut list_for_nested_rules = None; + match *rule { + CssRule::Style(ref locked) => { + let style_rule = locked.read_with(guard); + self.num_declarations += style_rule.block.read_with(&guard).len(); + + let has_nested_rules = style_rule.rules.is_some(); + let mut ancestor_selectors = + containing_rule_state.ancestor_selector_lists.last_mut(); + let mut replaced_selectors = SmallVec::<[Selector<SelectorImpl>; 4]>::new(); + let collect_replaced_selectors = + has_nested_rules && ancestor_selectors.is_some(); + + for selector in style_rule.selectors.slice() { + self.num_selectors += 1; + + let pseudo_element = selector.pseudo_element(); + if let Some(pseudo) = pseudo_element { + if pseudo.is_precomputed() { + debug_assert!(selector.is_universal()); + debug_assert!(ancestor_selectors.is_none()); + debug_assert!(!has_nested_rules); + debug_assert_eq!(stylesheet.contents().origin, Origin::UserAgent); + debug_assert_eq!(containing_rule_state.layer_id, LayerId::root()); + + precomputed_pseudo_element_decls + .as_mut() + .expect("Expected precomputed declarations for the UA level") + .get_or_insert_with(pseudo, Vec::new) + .push(ApplicableDeclarationBlock::new( + StyleSource::from_rule(locked.clone()), + self.rules_source_order, + CascadeLevel::UANormal, + selector.specificity(), + LayerOrder::root(), + )); + continue; + } + if pseudo.is_unknown_webkit_pseudo_element() { + continue; + } + } + + let selector = match ancestor_selectors { + Some(ref mut s) => selector.replace_parent_selector(&s), + None => selector.clone(), + }; + + let hashes = AncestorHashes::new(&selector, quirks_mode); + + let rule = Rule::new( + selector, + hashes, + locked.clone(), + self.rules_source_order, + containing_rule_state.layer_id, + containing_rule_state.container_condition_id, + ); + + if collect_replaced_selectors { + replaced_selectors.push(rule.selector.clone()) + } + + if rebuild_kind.should_rebuild_invalidation() { + note_selector_for_invalidation( + &rule.selector, + quirks_mode, + &mut self.invalidation_map, + &mut self.relative_selector_invalidation_map, + )?; + let mut needs_revalidation = false; + let mut visitor = StylistSelectorVisitor { + needs_revalidation: &mut needs_revalidation, + passed_rightmost_selector: false, + in_selector_list_of: SelectorListKind::default(), + mapped_ids: &mut self.mapped_ids, + nth_of_mapped_ids: &mut self.nth_of_mapped_ids, + attribute_dependencies: &mut self.attribute_dependencies, + nth_of_class_dependencies: &mut self.nth_of_class_dependencies, + nth_of_attribute_dependencies: &mut self + .nth_of_attribute_dependencies, + state_dependencies: &mut self.state_dependencies, + nth_of_state_dependencies: &mut self.nth_of_state_dependencies, + document_state_dependencies: &mut self.document_state_dependencies, + }; + rule.selector.visit(&mut visitor); + + if needs_revalidation { + self.selectors_for_cache_revalidation.insert( + RevalidationSelectorAndHashes::new( + rule.selector.clone(), + rule.hashes.clone(), + ), + quirks_mode, + )?; + } + } + + // Part is special, since given it doesn't have any + // selectors inside, it's not worth using a whole + // SelectorMap for it. + if let Some(parts) = rule.selector.parts() { + // ::part() has all semantics, so we just need to + // put any of them in the selector map. + // + // We choose the last one quite arbitrarily, + // expecting it's slightly more likely to be more + // specific. + let map = self + .part_rules + .get_or_insert_with(|| Box::new(Default::default())) + .for_insertion(pseudo_element); + map.try_reserve(1)?; + let vec = map.entry(parts.last().unwrap().clone().0).or_default(); + vec.try_reserve(1)?; + vec.push(rule); + } else { + // NOTE(emilio): It's fine to look at :host and then at + // ::slotted(..), since :host::slotted(..) could never + // possibly match, as <slot> is not a valid shadow host. + let rules = if rule + .selector + .is_featureless_host_selector_or_pseudo_element() + { + self.host_rules + .get_or_insert_with(|| Box::new(Default::default())) + } else if rule.selector.is_slotted() { + self.slotted_rules + .get_or_insert_with(|| Box::new(Default::default())) + } else { + &mut self.normal_rules + } + .for_insertion(pseudo_element); + rules.insert(rule, quirks_mode)?; + } + } + self.rules_source_order += 1; + handled = true; + if has_nested_rules { + handled = false; + list_for_nested_rules = Some(if collect_replaced_selectors { + SelectorList::from_iter(replaced_selectors.drain(..)) + } else { + style_rule.selectors.clone() + }); + } + }, + CssRule::Keyframes(ref keyframes_rule) => { + debug!("Found valid keyframes rule: {:?}", *keyframes_rule); + let keyframes_rule = keyframes_rule.read_with(guard); + let name = keyframes_rule.name.as_atom().clone(); + let animation = KeyframesAnimation::from_keyframes( + &keyframes_rule.keyframes, + keyframes_rule.vendor_prefix.clone(), + guard, + ); + self.animations.try_insert_with( + name, + animation, + containing_rule_state.layer_id, + compare_keyframes_in_same_layer, + )?; + }, + CssRule::Property(ref registration) => { + self.custom_property_registrations.try_insert( + registration.name.0.clone(), + Arc::clone(registration), + containing_rule_state.layer_id, + )?; + }, + #[cfg(feature = "gecko")] + CssRule::FontFace(ref rule) => { + // NOTE(emilio): We don't care about container_condition_id + // because: + // + // Global, name-defining at-rules such as @keyframes or + // @font-face or @layer that are defined inside container + // queries are not constrained by the container query + // conditions. + // + // https://drafts.csswg.org/css-contain-3/#container-rule + // (Same elsewhere) + self.extra_data + .add_font_face(rule, containing_rule_state.layer_id); + }, + #[cfg(feature = "gecko")] + CssRule::FontFeatureValues(ref rule) => { + self.extra_data + .add_font_feature_values(rule, containing_rule_state.layer_id); + }, + #[cfg(feature = "gecko")] + CssRule::FontPaletteValues(ref rule) => { + self.extra_data + .add_font_palette_values(rule, containing_rule_state.layer_id); + }, + #[cfg(feature = "gecko")] + CssRule::CounterStyle(ref rule) => { + self.extra_data.add_counter_style( + guard, + rule, + containing_rule_state.layer_id, + )?; + }, + #[cfg(feature = "gecko")] + CssRule::Page(ref rule) => { + self.extra_data + .add_page(guard, rule, containing_rule_state.layer_id)?; + handled = false; + }, + _ => { + handled = false; + }, + } + + if handled { + // Assert that there are no children, and that the rule is + // effective. + if cfg!(debug_assertions) { + let mut effective = false; + let children = EffectiveRulesIterator::children( + rule, + device, + quirks_mode, + guard, + &mut effective, + ); + debug_assert!(children.is_none()); + debug_assert!(effective); + } + continue; + } + + let mut effective = false; + let children = + EffectiveRulesIterator::children(rule, device, quirks_mode, guard, &mut effective); + + if !effective { + continue; + } + + fn maybe_register_layer(data: &mut CascadeData, layer: &LayerName) -> LayerId { + // TODO: Measure what's more common / expensive, if + // layer.clone() or the double hash lookup in the insert + // case. + if let Some(id) = data.layer_id.get(layer) { + return *id; + } + let id = LayerId(data.layers.len() as u16); + + let parent_layer_id = if layer.layer_names().len() > 1 { + let mut parent = layer.clone(); + parent.0.pop(); + + *data + .layer_id + .get_mut(&parent) + .expect("Parent layers should be registered before child layers") + } else { + LayerId::root() + }; + + data.layers[parent_layer_id.0 as usize].children.push(id); + data.layers.push(CascadeLayer { + id, + // NOTE(emilio): Order is evaluated after rebuild in + // compute_layer_order. + order: LayerOrder::first(), + children: vec![], + }); + + data.layer_id.insert(layer.clone(), id); + + id + } + + fn maybe_register_layers( + data: &mut CascadeData, + name: Option<&LayerName>, + containing_rule_state: &mut ContainingRuleState, + ) { + let anon_name; + let name = match name { + Some(name) => name, + None => { + anon_name = LayerName::new_anonymous(); + &anon_name + }, + }; + for name in name.layer_names() { + containing_rule_state.layer_name.0.push(name.clone()); + containing_rule_state.layer_id = + maybe_register_layer(data, &containing_rule_state.layer_name); + } + debug_assert_ne!(containing_rule_state.layer_id, LayerId::root()); + } + + let saved_containing_rule_state = containing_rule_state.save(); + match *rule { + CssRule::Import(ref lock) => { + let import_rule = lock.read_with(guard); + if rebuild_kind.should_rebuild_invalidation() { + self.effective_media_query_results + .saw_effective(import_rule); + } + match import_rule.layer { + ImportLayer::Named(ref name) => { + maybe_register_layers(self, Some(name), containing_rule_state) + }, + ImportLayer::Anonymous => { + maybe_register_layers(self, None, containing_rule_state) + }, + ImportLayer::None => {}, + } + }, + CssRule::Media(ref media_rule) => { + if rebuild_kind.should_rebuild_invalidation() { + self.effective_media_query_results + .saw_effective(&**media_rule); + } + }, + CssRule::LayerBlock(ref rule) => { + maybe_register_layers(self, rule.name.as_ref(), containing_rule_state); + }, + CssRule::LayerStatement(ref rule) => { + for name in &*rule.names { + maybe_register_layers(self, Some(name), containing_rule_state); + // Register each layer individually. + containing_rule_state.restore(&saved_containing_rule_state); + } + }, + CssRule::Style(..) => { + if let Some(s) = list_for_nested_rules { + containing_rule_state.ancestor_selector_lists.push(s); + } + }, + CssRule::Container(ref rule) => { + let id = ContainerConditionId(self.container_conditions.len() as u16); + self.container_conditions.push(ContainerConditionReference { + parent: containing_rule_state.container_condition_id, + condition: Some(rule.condition.clone()), + }); + containing_rule_state.container_condition_id = id; + }, + // We don't care about any other rule. + _ => {}, + } + + if let Some(children) = children { + self.add_rule_list( + children, + device, + quirks_mode, + stylesheet, + guard, + rebuild_kind, + containing_rule_state, + precomputed_pseudo_element_decls.as_deref_mut(), + )?; + } + + containing_rule_state.restore(&saved_containing_rule_state); + } + + Ok(()) + } + + // Returns Err(..) to signify OOM + fn add_stylesheet<S>( + &mut self, + device: &Device, + quirks_mode: QuirksMode, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + rebuild_kind: SheetRebuildKind, + mut precomputed_pseudo_element_decls: Option<&mut PrecomputedPseudoElementDeclarations>, + ) -> Result<(), AllocErr> + where + S: StylesheetInDocument + 'static, + { + if !stylesheet.enabled() || !stylesheet.is_effective_for_device(device, guard) { + return Ok(()); + } + + let contents = stylesheet.contents(); + + if rebuild_kind.should_rebuild_invalidation() { + self.effective_media_query_results.saw_effective(contents); + } + + let mut state = ContainingRuleState::default(); + self.add_rule_list( + contents.rules(guard).iter(), + device, + quirks_mode, + stylesheet, + guard, + rebuild_kind, + &mut state, + precomputed_pseudo_element_decls.as_deref_mut(), + )?; + + Ok(()) + } + + /// Returns whether all the media-feature affected values matched before and + /// match now in the given stylesheet. + pub fn media_feature_affected_matches<S>( + &self, + stylesheet: &S, + guard: &SharedRwLockReadGuard, + device: &Device, + quirks_mode: QuirksMode, + ) -> bool + where + S: StylesheetInDocument + 'static, + { + use crate::invalidation::media_queries::PotentiallyEffectiveMediaRules; + + let effective_now = stylesheet.is_effective_for_device(device, guard); + + let effective_then = self + .effective_media_query_results + .was_effective(stylesheet.contents()); + + if effective_now != effective_then { + debug!( + " > Stylesheet {:?} changed -> {}, {}", + stylesheet.media(guard), + effective_then, + effective_now + ); + return false; + } + + if !effective_now { + return true; + } + + let mut iter = stylesheet.iter_rules::<PotentiallyEffectiveMediaRules>(device, guard); + + while let Some(rule) = iter.next() { + match *rule { + CssRule::Style(..) | + CssRule::Namespace(..) | + CssRule::FontFace(..) | + CssRule::Container(..) | + CssRule::CounterStyle(..) | + CssRule::Supports(..) | + CssRule::Keyframes(..) | + CssRule::Margin(..) | + CssRule::Page(..) | + CssRule::Property(..) | + CssRule::Document(..) | + CssRule::LayerBlock(..) | + CssRule::LayerStatement(..) | + CssRule::FontPaletteValues(..) | + CssRule::FontFeatureValues(..) => { + // Not affected by device changes. + continue; + }, + CssRule::Import(ref lock) => { + let import_rule = lock.read_with(guard); + let effective_now = match import_rule.stylesheet.media(guard) { + Some(m) => m.evaluate(device, quirks_mode), + None => true, + }; + let effective_then = self + .effective_media_query_results + .was_effective(import_rule); + if effective_now != effective_then { + debug!( + " > @import rule {:?} changed {} -> {}", + import_rule.stylesheet.media(guard), + effective_then, + effective_now + ); + return false; + } + + if !effective_now { + iter.skip_children(); + } + }, + CssRule::Media(ref media_rule) => { + let mq = media_rule.media_queries.read_with(guard); + let effective_now = mq.evaluate(device, quirks_mode); + let effective_then = self + .effective_media_query_results + .was_effective(&**media_rule); + + if effective_now != effective_then { + debug!( + " > @media rule {:?} changed {} -> {}", + mq, effective_then, effective_now + ); + return false; + } + + if !effective_now { + iter.skip_children(); + } + }, + } + } + + true + } + + /// Returns the custom properties map. + pub fn custom_property_registrations(&self) -> &LayerOrderedMap<Arc<PropertyRegistration>> { + &self.custom_property_registrations + } + + /// Clears the cascade data, but not the invalidation data. + fn clear_cascade_data(&mut self) { + self.normal_rules.clear(); + if let Some(ref mut slotted_rules) = self.slotted_rules { + slotted_rules.clear(); + } + if let Some(ref mut part_rules) = self.part_rules { + part_rules.clear(); + } + if let Some(ref mut host_rules) = self.host_rules { + host_rules.clear(); + } + self.animations.clear(); + self.custom_property_registrations.clear(); + self.layer_id.clear(); + self.layers.clear(); + self.layers.push(CascadeLayer::root()); + self.container_conditions.clear(); + self.container_conditions + .push(ContainerConditionReference::none()); + self.extra_data.clear(); + self.rules_source_order = 0; + self.num_selectors = 0; + self.num_declarations = 0; + } + + fn clear(&mut self) { + self.clear_cascade_data(); + self.invalidation_map.clear(); + self.relative_selector_invalidation_map.clear(); + self.attribute_dependencies.clear(); + self.nth_of_attribute_dependencies.clear(); + self.nth_of_class_dependencies.clear(); + self.state_dependencies = ElementState::empty(); + self.nth_of_state_dependencies = ElementState::empty(); + self.document_state_dependencies = DocumentState::empty(); + self.mapped_ids.clear(); + self.nth_of_mapped_ids.clear(); + self.selectors_for_cache_revalidation.clear(); + self.effective_media_query_results.clear(); + } +} + +impl CascadeDataCacheEntry for CascadeData { + fn cascade_data(&self) -> &CascadeData { + self + } + + fn rebuild<S>( + device: &Device, + quirks_mode: QuirksMode, + collection: SheetCollectionFlusher<S>, + guard: &SharedRwLockReadGuard, + old: &Self, + ) -> Result<Arc<Self>, AllocErr> + where + S: StylesheetInDocument + PartialEq + 'static, + { + debug_assert!(collection.dirty(), "We surely need to do something?"); + // If we're doing a full rebuild anyways, don't bother cloning the data. + let mut updatable_entry = match collection.data_validity() { + DataValidity::Valid | DataValidity::CascadeInvalid => old.clone(), + DataValidity::FullyInvalid => Self::new(), + }; + updatable_entry.rebuild(device, quirks_mode, collection, guard)?; + Ok(Arc::new(updatable_entry)) + } + + #[cfg(feature = "gecko")] + fn add_size_of(&self, ops: &mut MallocSizeOfOps, sizes: &mut ServoStyleSetSizes) { + self.normal_rules.add_size_of(ops, sizes); + if let Some(ref slotted_rules) = self.slotted_rules { + slotted_rules.add_size_of(ops, sizes); + } + if let Some(ref part_rules) = self.part_rules { + part_rules.add_size_of(ops, sizes); + } + if let Some(ref host_rules) = self.host_rules { + host_rules.add_size_of(ops, sizes); + } + sizes.mInvalidationMap += self.invalidation_map.size_of(ops); + sizes.mRevalidationSelectors += self.selectors_for_cache_revalidation.size_of(ops); + sizes.mOther += self.animations.size_of(ops); + sizes.mOther += self.effective_media_query_results.size_of(ops); + sizes.mOther += self.extra_data.size_of(ops); + } +} + +impl Default for CascadeData { + fn default() -> Self { + CascadeData::new() + } +} + +/// A rule, that wraps a style rule, but represents a single selector of the +/// rule. +#[derive(Clone, Debug, MallocSizeOf)] +pub struct Rule { + /// The selector this struct represents. We store this and the + /// any_{important,normal} booleans inline in the Rule to avoid + /// pointer-chasing when gathering applicable declarations, which + /// can ruin performance when there are a lot of rules. + #[ignore_malloc_size_of = "CssRules have primary refs, we measure there"] + pub selector: Selector<SelectorImpl>, + + /// The ancestor hashes associated with the selector. + pub hashes: AncestorHashes, + + /// The source order this style rule appears in. Note that we only use + /// three bytes to store this value in ApplicableDeclarationsBlock, so + /// we could repurpose that storage here if we needed to. + pub source_order: u32, + + /// The current layer id of this style rule. + pub layer_id: LayerId, + + /// The current @container rule id. + pub container_condition_id: ContainerConditionId, + + /// The actual style rule. + #[cfg_attr( + feature = "gecko", + ignore_malloc_size_of = "Secondary ref. Primary ref is in StyleRule under Stylesheet." + )] + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + pub style_rule: Arc<Locked<StyleRule>>, +} + +impl SelectorMapEntry for Rule { + fn selector(&self) -> SelectorIter<SelectorImpl> { + self.selector.iter() + } +} + +impl Rule { + /// Returns the specificity of the rule. + pub fn specificity(&self) -> u32 { + self.selector.specificity() + } + + /// Turns this rule into an `ApplicableDeclarationBlock` for the given + /// cascade level. + pub fn to_applicable_declaration_block( + &self, + level: CascadeLevel, + cascade_data: &CascadeData, + ) -> ApplicableDeclarationBlock { + let source = StyleSource::from_rule(self.style_rule.clone()); + ApplicableDeclarationBlock::new( + source, + self.source_order, + level, + self.specificity(), + cascade_data.layer_order_for(self.layer_id), + ) + } + + /// Creates a new Rule. + pub fn new( + selector: Selector<SelectorImpl>, + hashes: AncestorHashes, + style_rule: Arc<Locked<StyleRule>>, + source_order: u32, + layer_id: LayerId, + container_condition_id: ContainerConditionId, + ) -> Self { + Rule { + selector, + hashes, + style_rule, + source_order, + layer_id, + container_condition_id, + } + } +} + +// The size of this is critical to performance on the bloom-basic +// microbenchmark. +// When iterating over a large Rule array, we want to be able to fast-reject +// selectors (with the inline hashes) with as few cache misses as possible. +size_of_test!(Rule, 40); + +/// A function to be able to test the revalidation stuff. +pub fn needs_revalidation_for_testing(s: &Selector<SelectorImpl>) -> bool { + let mut needs_revalidation = false; + let mut mapped_ids = Default::default(); + let mut nth_of_mapped_ids = Default::default(); + let mut attribute_dependencies = Default::default(); + let mut nth_of_class_dependencies = Default::default(); + let mut nth_of_attribute_dependencies = Default::default(); + let mut state_dependencies = ElementState::empty(); + let mut nth_of_state_dependencies = ElementState::empty(); + let mut document_state_dependencies = DocumentState::empty(); + let mut visitor = StylistSelectorVisitor { + passed_rightmost_selector: false, + needs_revalidation: &mut needs_revalidation, + in_selector_list_of: SelectorListKind::default(), + mapped_ids: &mut mapped_ids, + nth_of_mapped_ids: &mut nth_of_mapped_ids, + attribute_dependencies: &mut attribute_dependencies, + nth_of_class_dependencies: &mut nth_of_class_dependencies, + nth_of_attribute_dependencies: &mut nth_of_attribute_dependencies, + state_dependencies: &mut state_dependencies, + nth_of_state_dependencies: &mut nth_of_state_dependencies, + document_state_dependencies: &mut document_state_dependencies, + }; + s.visit(&mut visitor); + needs_revalidation +} diff --git a/servo/components/style/thread_state.rs b/servo/components/style/thread_state.rs new file mode 100644 index 0000000000..e07a567fe7 --- /dev/null +++ b/servo/components/style/thread_state.rs @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Supports dynamic assertions in about what sort of thread is running and +//! what state it's in. + +#![deny(missing_docs)] + +use std::cell::RefCell; + +bitflags! { + /// A thread state flag, used for multiple assertions. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct ThreadState: u32 { + /// Whether we're in a script thread. + const SCRIPT = 0x01; + /// Whether we're in a layout thread. + const LAYOUT = 0x02; + + /// Whether we're in a script worker thread (actual web workers), or in + /// a layout worker thread. + const IN_WORKER = 0x0100; + + /// Whether the current thread is going through a GC. + const IN_GC = 0x0200; + } +} + +macro_rules! thread_types ( ( $( $fun:ident = $flag:path ; )* ) => ( + impl ThreadState { + /// Whether the current thread is a worker thread. + pub fn is_worker(self) -> bool { + self.contains(ThreadState::IN_WORKER) + } + + $( + #[allow(missing_docs)] + pub fn $fun(self) -> bool { + self.contains($flag) + } + )* + } +)); + +thread_types! { + is_script = ThreadState::SCRIPT; + is_layout = ThreadState::LAYOUT; +} + +thread_local!(static STATE: RefCell<Option<ThreadState>> = RefCell::new(None)); + +/// Initializes the current thread state. +pub fn initialize(x: ThreadState) { + STATE.with(|ref k| { + if let Some(ref s) = *k.borrow() { + if x != *s { + panic!("Thread state already initialized as {:?}", s); + } + } + *k.borrow_mut() = Some(x); + }); +} + +/// Initializes the current thread as a layout worker thread. +pub fn initialize_layout_worker_thread() { + initialize(ThreadState::LAYOUT | ThreadState::IN_WORKER); +} + +/// Gets the current thread state. +pub fn get() -> ThreadState { + let state = STATE.with(|ref k| { + match *k.borrow() { + None => ThreadState::empty(), // Unknown thread. + Some(s) => s, + } + }); + + state +} + +/// Enters into a given temporary state. Panics if re-entring. +pub fn enter(x: ThreadState) { + let state = get(); + debug_assert!(!state.intersects(x)); + STATE.with(|ref k| { + *k.borrow_mut() = Some(state | x); + }) +} + +/// Exits a given temporary state. +pub fn exit(x: ThreadState) { + let state = get(); + debug_assert!(state.contains(x)); + STATE.with(|ref k| { + *k.borrow_mut() = Some(state & !x); + }) +} diff --git a/servo/components/style/traversal.rs b/servo/components/style/traversal.rs new file mode 100644 index 0000000000..d63c3cb965 --- /dev/null +++ b/servo/components/style/traversal.rs @@ -0,0 +1,842 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Traversing the DOM tree; the bloom filter. + +use crate::context::{ElementCascadeInputs, SharedStyleContext, StyleContext}; +use crate::data::{ElementData, ElementStyles, RestyleKind}; +use crate::dom::{NodeInfo, OpaqueNode, TElement, TNode}; +use crate::invalidation::element::restyle_hints::RestyleHint; +use crate::matching::{ChildRestyleRequirement, MatchMethods}; +use crate::selector_parser::PseudoElement; +use crate::sharing::StyleSharingTarget; +use crate::style_resolver::{PseudoElementResolution, StyleResolverForElement}; +use crate::stylist::RuleInclusion; +use crate::traversal_flags::TraversalFlags; +use selectors::matching::SelectorCaches; +use smallvec::SmallVec; +use std::collections::HashMap; + +/// A cache from element reference to known-valid computed style. +pub type UndisplayedStyleCache = + HashMap<selectors::OpaqueElement, servo_arc::Arc<crate::properties::ComputedValues>>; + +/// A per-traversal-level chunk of data. This is sent down by the traversal, and +/// currently only holds the dom depth for the bloom filter. +/// +/// NB: Keep this as small as possible, please! +#[derive(Clone, Copy, Debug)] +pub struct PerLevelTraversalData { + /// The current dom depth. + /// + /// This is kept with cooperation from the traversal code and the bloom + /// filter. + pub current_dom_depth: usize, +} + +/// We use this structure, rather than just returning a boolean from pre_traverse, +/// to enfore that callers process root invalidations before starting the traversal. +pub struct PreTraverseToken<E: TElement>(Option<E>); +impl<E: TElement> PreTraverseToken<E> { + /// Whether we should traverse children. + pub fn should_traverse(&self) -> bool { + self.0.is_some() + } + + /// Returns the traversal root for the current traversal. + pub(crate) fn traversal_root(self) -> Option<E> { + self.0 + } +} + +/// A global variable holding the state of +/// `is_servo_nonincremental_layout()`. +/// See [#22854](https://github.com/servo/servo/issues/22854). +#[cfg(feature = "servo")] +pub static IS_SERVO_NONINCREMENTAL_LAYOUT: std::sync::atomic::AtomicBool = + std::sync::atomic::AtomicBool::new(false); + +#[cfg(feature = "servo")] +#[inline] +fn is_servo_nonincremental_layout() -> bool { + use std::sync::atomic::Ordering; + + IS_SERVO_NONINCREMENTAL_LAYOUT.load(Ordering::Relaxed) +} + +#[cfg(not(feature = "servo"))] +#[inline] +fn is_servo_nonincremental_layout() -> bool { + false +} + +/// A DOM Traversal trait, that is used to generically implement styling for +/// Gecko and Servo. +pub trait DomTraversal<E: TElement>: Sync { + /// Process `node` on the way down, before its children have been processed. + /// + /// The callback is invoked for each child node that should be processed by + /// the traversal. + fn process_preorder<F>( + &self, + data: &PerLevelTraversalData, + context: &mut StyleContext<E>, + node: E::ConcreteNode, + note_child: F, + ) where + F: FnMut(E::ConcreteNode); + + /// Process `node` on the way up, after its children have been processed. + /// + /// This is only executed if `needs_postorder_traversal` returns true. + fn process_postorder(&self, contect: &mut StyleContext<E>, node: E::ConcreteNode); + + /// Boolean that specifies whether a bottom up traversal should be + /// performed. + /// + /// If it's false, then process_postorder has no effect at all. + fn needs_postorder_traversal() -> bool { + true + } + + /// Handles the postorder step of the traversal, if it exists, by bubbling + /// up the parent chain. + /// + /// If we are the last child that finished processing, recursively process + /// our parent. Else, stop. Also, stop at the root. + /// + /// Thus, if we start with all the leaves of a tree, we end up traversing + /// the whole tree bottom-up because each parent will be processed exactly + /// once (by the last child that finishes processing). + /// + /// The only communication between siblings is that they both + /// fetch-and-subtract the parent's children count. This makes it safe to + /// call durign the parallel traversal. + fn handle_postorder_traversal( + &self, + context: &mut StyleContext<E>, + root: OpaqueNode, + mut node: E::ConcreteNode, + children_to_process: isize, + ) { + // If the postorder step is a no-op, don't bother. + if !Self::needs_postorder_traversal() { + return; + } + + if children_to_process == 0 { + // We are a leaf. Walk up the chain. + loop { + self.process_postorder(context, node); + if node.opaque() == root { + break; + } + let parent = node.traversal_parent().unwrap(); + let remaining = parent.did_process_child(); + if remaining != 0 { + // The parent has other unprocessed descendants. We only + // perform postorder processing after the last descendant + // has been processed. + break; + } + + node = parent.as_node(); + } + } else { + // Otherwise record the number of children to process when the time + // comes. + node.as_element() + .unwrap() + .store_children_to_process(children_to_process); + } + } + + /// Style invalidations happen when traversing from a parent to its children. + /// However, this mechanism can't handle style invalidations on the root. As + /// such, we have a pre-traversal step to handle that part and determine whether + /// a full traversal is needed. + fn pre_traverse(root: E, shared_context: &SharedStyleContext) -> PreTraverseToken<E> { + use crate::invalidation::element::state_and_attributes::propagate_dirty_bit_up_to; + + let traversal_flags = shared_context.traversal_flags; + + let mut data = root.mutate_data(); + let mut data = data.as_mut().map(|d| &mut **d); + + if let Some(ref mut data) = data { + if !traversal_flags.for_animation_only() { + // Invalidate our style, and that of our siblings and + // descendants as needed. + let invalidation_result = data.invalidate_style_if_needed( + root, + shared_context, + None, + &mut SelectorCaches::default(), + ); + + if invalidation_result.has_invalidated_siblings() { + let actual_root = root.as_node().parent_element_or_host().expect( + "How in the world can you invalidate \ + siblings without a parent?", + ); + propagate_dirty_bit_up_to(actual_root, root); + return PreTraverseToken(Some(actual_root)); + } + } + } + + let should_traverse = + Self::element_needs_traversal(root, traversal_flags, data.as_mut().map(|d| &**d)); + + // If we're not going to traverse at all, we may need to clear some state + // off the root (which would normally be done at the end of recalc_style_at). + if !should_traverse && data.is_some() { + clear_state_after_traversing(root, data.unwrap(), traversal_flags); + } + + PreTraverseToken(if should_traverse { Some(root) } else { None }) + } + + /// Returns true if traversal should visit a text node. The style system + /// never processes text nodes, but Servo overrides this to visit them for + /// flow construction when necessary. + fn text_node_needs_traversal(node: E::ConcreteNode, _parent_data: &ElementData) -> bool { + debug_assert!(node.is_text_node()); + false + } + + /// Returns true if traversal is needed for the given element and subtree. + fn element_needs_traversal( + el: E, + traversal_flags: TraversalFlags, + data: Option<&ElementData>, + ) -> bool { + debug!( + "element_needs_traversal({:?}, {:?}, {:?})", + el, traversal_flags, data + ); + + // In case of animation-only traversal we need to traverse the element if the element has + // animation only dirty descendants bit, animation-only restyle hint. + if traversal_flags.for_animation_only() { + return data.map_or(false, |d| d.has_styles()) && + (el.has_animation_only_dirty_descendants() || + data.as_ref() + .unwrap() + .hint + .has_animation_hint_or_recascade()); + } + + // Non-incremental layout visits every node. + if is_servo_nonincremental_layout() { + return true; + } + + // Unwrap the data. + let data = match data { + Some(d) if d.has_styles() => d, + _ => return true, + }; + + // If the dirty descendants bit is set, we need to traverse no matter + // what. Skip examining the ElementData. + if el.has_dirty_descendants() { + return true; + } + + // If we have a restyle hint or need to recascade, we need to visit the + // element. + // + // Note that this is different than checking has_current_styles_for_traversal(), + // since that can return true even if we have a restyle hint indicating + // that the element's descendants (but not necessarily the element) need + // restyling. + if !data.hint.is_empty() { + return true; + } + + // Servo uses the post-order traversal for flow construction, so we need + // to traverse any element with damage so that we can perform fixup / + // reconstruction on our way back up the tree. + if cfg!(feature = "servo") && !data.damage.is_empty() { + return true; + } + + trace!("{:?} doesn't need traversal", el); + false + } + + /// Return the shared style context common to all worker threads. + fn shared_context(&self) -> &SharedStyleContext; +} + +/// Manually resolve style by sequentially walking up the parent chain to the +/// first styled Element, ignoring pending restyles. The resolved style is made +/// available via a callback, and can be dropped by the time this function +/// returns in the display:none subtree case. +pub fn resolve_style<E>( + context: &mut StyleContext<E>, + element: E, + rule_inclusion: RuleInclusion, + pseudo: Option<&PseudoElement>, + mut undisplayed_style_cache: Option<&mut UndisplayedStyleCache>, +) -> ElementStyles +where + E: TElement, +{ + debug_assert!( + rule_inclusion == RuleInclusion::DefaultOnly || + pseudo.map_or(false, |p| p.is_before_or_after()) || + element.borrow_data().map_or(true, |d| !d.has_styles()), + "Why are we here?" + ); + debug_assert!( + rule_inclusion == RuleInclusion::All || undisplayed_style_cache.is_none(), + "can't use the cache for default styles only" + ); + + let mut ancestors_requiring_style_resolution = SmallVec::<[E; 16]>::new(); + + // Clear the bloom filter, just in case the caller is reusing TLS. + context.thread_local.bloom_filter.clear(); + + let mut style = None; + let mut ancestor = element.traversal_parent(); + while let Some(current) = ancestor { + if rule_inclusion == RuleInclusion::All { + if let Some(data) = current.borrow_data() { + if let Some(ancestor_style) = data.styles.get_primary() { + style = Some(ancestor_style.clone()); + break; + } + } + } + if let Some(ref mut cache) = undisplayed_style_cache { + if let Some(s) = cache.get(¤t.opaque()) { + style = Some(s.clone()); + break; + } + } + ancestors_requiring_style_resolution.push(current); + ancestor = current.traversal_parent(); + } + + if let Some(ancestor) = ancestor { + context.thread_local.bloom_filter.rebuild(ancestor); + context.thread_local.bloom_filter.push(ancestor); + } + + let mut layout_parent_style = style.clone(); + while let Some(style) = layout_parent_style.take() { + if !style.is_display_contents() { + layout_parent_style = Some(style); + break; + } + + ancestor = ancestor.unwrap().traversal_parent(); + layout_parent_style = + ancestor.and_then(|a| a.borrow_data().map(|data| data.styles.primary().clone())); + } + + for ancestor in ancestors_requiring_style_resolution.iter().rev() { + context.thread_local.bloom_filter.assert_complete(*ancestor); + + // Actually `PseudoElementResolution` doesn't really matter here. + // (but it does matter below!). + let primary_style = StyleResolverForElement::new( + *ancestor, + context, + rule_inclusion, + PseudoElementResolution::IfApplicable, + ) + .resolve_primary_style(style.as_deref(), layout_parent_style.as_deref()); + + let is_display_contents = primary_style.style().is_display_contents(); + + style = Some(primary_style.style.0); + if !is_display_contents { + layout_parent_style = style.clone(); + } + + if let Some(ref mut cache) = undisplayed_style_cache { + cache.insert(ancestor.opaque(), style.clone().unwrap()); + } + context.thread_local.bloom_filter.push(*ancestor); + } + + context.thread_local.bloom_filter.assert_complete(element); + let styles: ElementStyles = StyleResolverForElement::new( + element, + context, + rule_inclusion, + PseudoElementResolution::Force, + ) + .resolve_style(style.as_deref(), layout_parent_style.as_deref()) + .into(); + + if let Some(ref mut cache) = undisplayed_style_cache { + cache.insert(element.opaque(), styles.primary().clone()); + } + + styles +} + +/// Calculates the style for a single node. +#[inline] +#[allow(unsafe_code)] +pub fn recalc_style_at<E, D, F>( + _traversal: &D, + traversal_data: &PerLevelTraversalData, + context: &mut StyleContext<E>, + element: E, + data: &mut ElementData, + note_child: F, +) where + E: TElement, + D: DomTraversal<E>, + F: FnMut(E::ConcreteNode), +{ + use std::cmp; + + let flags = context.shared.traversal_flags; + let is_initial_style = !data.has_styles(); + + context.thread_local.statistics.elements_traversed += 1; + debug_assert!( + flags.intersects(TraversalFlags::AnimationOnly) || + is_initial_style || + !element.has_snapshot() || + element.handled_snapshot(), + "Should've handled snapshots here already" + ); + + let restyle_kind = data.restyle_kind(&context.shared); + debug!( + "recalc_style_at: {:?} (restyle_kind={:?}, dirty_descendants={:?}, data={:?})", + element, + restyle_kind, + element.has_dirty_descendants(), + data + ); + + let mut child_restyle_requirement = ChildRestyleRequirement::CanSkipCascade; + + // Compute style for this element if necessary. + if let Some(restyle_kind) = restyle_kind { + child_restyle_requirement = + compute_style(traversal_data, context, element, data, restyle_kind); + + if !element.matches_user_and_content_rules() { + // We must always cascade native anonymous subtrees, since they + // may have pseudo-elements underneath that would inherit from the + // closest non-NAC ancestor instead of us. + child_restyle_requirement = cmp::max( + child_restyle_requirement, + ChildRestyleRequirement::MustCascadeChildren, + ); + } + + // If we're restyling this element to display:none, throw away all style + // data in the subtree, notify the caller to early-return. + if data.styles.is_display_none() { + debug!( + "{:?} style is display:none - clearing data from descendants.", + element + ); + unsafe { + clear_descendant_data(element); + } + } + + // Inform any paint worklets of changed style, to speculatively + // evaluate the worklet code. In the case that the size hasn't changed, + // this will result in increased concurrency between script and layout. + notify_paint_worklet(context, data); + } else { + debug_assert!(data.has_styles()); + data.set_traversed_without_styling(); + } + + // Now that matching and cascading is done, clear the bits corresponding to + // those operations and compute the propagated restyle hint (unless we're + // not processing invalidations, in which case don't need to propagate it + // and must avoid clearing it). + debug_assert!( + flags.for_animation_only() || !data.hint.has_animation_hint(), + "animation restyle hint should be handled during \ + animation-only restyles" + ); + let mut propagated_hint = data.hint.propagate(&flags); + trace!( + "propagated_hint={:?}, restyle_requirement={:?}, \ + is_display_none={:?}, implementing_pseudo={:?}", + propagated_hint, + child_restyle_requirement, + data.styles.is_display_none(), + element.implemented_pseudo_element() + ); + + // Integrate the child cascade requirement into the propagated hint. + match child_restyle_requirement { + ChildRestyleRequirement::CanSkipCascade => {}, + ChildRestyleRequirement::MustCascadeDescendants => { + propagated_hint |= RestyleHint::RECASCADE_SELF | RestyleHint::RECASCADE_DESCENDANTS; + }, + ChildRestyleRequirement::MustCascadeChildrenIfInheritResetStyle => { + propagated_hint |= RestyleHint::RECASCADE_SELF_IF_INHERIT_RESET_STYLE; + }, + ChildRestyleRequirement::MustCascadeChildren => { + propagated_hint |= RestyleHint::RECASCADE_SELF; + }, + ChildRestyleRequirement::MustMatchDescendants => { + propagated_hint |= RestyleHint::restyle_subtree(); + }, + } + + let has_dirty_descendants_for_this_restyle = if flags.for_animation_only() { + element.has_animation_only_dirty_descendants() + } else { + element.has_dirty_descendants() + }; + + // Before examining each child individually, try to prove that our children + // don't need style processing. They need processing if any of the following + // conditions hold: + // + // * We have the dirty descendants bit. + // * We're propagating a restyle hint. + // * This is a servo non-incremental traversal. + // + // We only do this if we're not a display: none root, since in that case + // it's useless to style children. + let mut traverse_children = has_dirty_descendants_for_this_restyle || + !propagated_hint.is_empty() || + is_servo_nonincremental_layout(); + + traverse_children = traverse_children && !data.styles.is_display_none(); + + // Examine our children, and enqueue the appropriate ones for traversal. + if traverse_children { + note_children::<E, D, F>( + context, + element, + data, + propagated_hint, + is_initial_style, + note_child, + ); + } + + // FIXME(bholley): Make these assertions pass for servo. + if cfg!(feature = "gecko") && cfg!(debug_assertions) && data.styles.is_display_none() { + debug_assert!(!element.has_dirty_descendants()); + debug_assert!(!element.has_animation_only_dirty_descendants()); + } + + clear_state_after_traversing(element, data, flags); +} + +fn clear_state_after_traversing<E>(element: E, data: &mut ElementData, flags: TraversalFlags) +where + E: TElement, +{ + if flags.intersects(TraversalFlags::FinalAnimationTraversal) { + debug_assert!(flags.for_animation_only()); + data.clear_restyle_flags_and_damage(); + unsafe { + element.unset_animation_only_dirty_descendants(); + } + } +} + +fn compute_style<E>( + traversal_data: &PerLevelTraversalData, + context: &mut StyleContext<E>, + element: E, + data: &mut ElementData, + kind: RestyleKind, +) -> ChildRestyleRequirement +where + E: TElement, +{ + use crate::data::RestyleKind::*; + + context.thread_local.statistics.elements_styled += 1; + debug!("compute_style: {:?} (kind={:?})", element, kind); + + if data.has_styles() { + data.set_restyled(); + } + + let mut important_rules_changed = false; + let new_styles = match kind { + MatchAndCascade => { + debug_assert!( + !context.shared.traversal_flags.for_animation_only(), + "MatchAndCascade shouldn't be processed during \ + animation-only traversal" + ); + // Ensure the bloom filter is up to date. + context + .thread_local + .bloom_filter + .insert_parents_recovering(element, traversal_data.current_dom_depth); + + context.thread_local.bloom_filter.assert_complete(element); + debug_assert_eq!( + context.thread_local.bloom_filter.matching_depth(), + traversal_data.current_dom_depth + ); + + // This is only relevant for animations as of right now. + important_rules_changed = true; + + let mut target = StyleSharingTarget::new(element); + + // Now that our bloom filter is set up, try the style sharing + // cache. + match target.share_style_if_possible(context) { + Some(shared_styles) => { + context.thread_local.statistics.styles_shared += 1; + shared_styles + }, + None => { + context.thread_local.statistics.elements_matched += 1; + // Perform the matching and cascading. + let new_styles = { + let mut resolver = StyleResolverForElement::new( + element, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ); + + resolver.resolve_style_with_default_parents() + }; + + context.thread_local.sharing_cache.insert_if_possible( + &element, + &new_styles.primary, + Some(&mut target), + traversal_data.current_dom_depth, + &context.shared, + ); + + new_styles + }, + } + }, + CascadeWithReplacements(flags) => { + // Skipping full matching, load cascade inputs from previous values. + let mut cascade_inputs = ElementCascadeInputs::new_from_element_data(data); + important_rules_changed = element.replace_rules(flags, context, &mut cascade_inputs); + + let mut resolver = StyleResolverForElement::new( + element, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ); + + resolver.cascade_styles_with_default_parents(cascade_inputs) + }, + CascadeOnly => { + // Skipping full matching, load cascade inputs from previous values. + let cascade_inputs = ElementCascadeInputs::new_from_element_data(data); + + let new_styles = { + let mut resolver = StyleResolverForElement::new( + element, + context, + RuleInclusion::All, + PseudoElementResolution::IfApplicable, + ); + + resolver.cascade_styles_with_default_parents(cascade_inputs) + }; + + // Insert into the cache, but only if this style isn't reused from a + // sibling or cousin. Otherwise, recascading a bunch of identical + // elements would unnecessarily flood the cache with identical entries. + // + // This is analogous to the obvious "don't insert an element that just + // got a hit in the style sharing cache" behavior in the MatchAndCascade + // handling above. + // + // Note that, for the MatchAndCascade path, we still insert elements that + // shared styles via the rule node, because we know that there's something + // different about them that caused them to miss the sharing cache before + // selector matching. If we didn't, we would still end up with the same + // number of eventual styles, but would potentially miss out on various + // opportunities for skipping selector matching, which could hurt + // performance. + if !new_styles.primary.reused_via_rule_node { + context.thread_local.sharing_cache.insert_if_possible( + &element, + &new_styles.primary, + None, + traversal_data.current_dom_depth, + &context.shared, + ); + } + + new_styles + }, + }; + + element.finish_restyle(context, data, new_styles, important_rules_changed) +} + +#[cfg(feature = "servo-layout-2013")] +fn notify_paint_worklet<E>(context: &StyleContext<E>, data: &ElementData) +where + E: TElement, +{ + use crate::values::generics::image::Image; + use style_traits::ToCss; + + // We speculatively evaluate any paint worklets during styling. + // This allows us to run paint worklets in parallel with style and layout. + // Note that this is wasted effort if the size of the node has + // changed, but in may cases it won't have. + if let Some(ref values) = data.styles.primary { + for image in &values.get_background().background_image.0 { + let (name, arguments) = match *image { + Image::PaintWorklet(ref worklet) => (&worklet.name, &worklet.arguments), + _ => continue, + }; + let painter = match context.shared.registered_speculative_painters.get(name) { + Some(painter) => painter, + None => continue, + }; + let properties = painter + .properties() + .iter() + .filter_map(|(name, id)| id.as_shorthand().err().map(|id| (name, id))) + .map(|(name, id)| (name.clone(), values.computed_value_to_string(id))) + .collect(); + let arguments = arguments + .iter() + .map(|argument| argument.to_css_string()) + .collect(); + debug!("Notifying paint worklet {}.", painter.name()); + painter.speculatively_draw_a_paint_image(properties, arguments); + } + } +} + +#[cfg(not(feature = "servo-layout-2013"))] +fn notify_paint_worklet<E>(_context: &StyleContext<E>, _data: &ElementData) +where + E: TElement, +{ + // The CSS paint API is Servo-only at the moment +} + +fn note_children<E, D, F>( + context: &mut StyleContext<E>, + element: E, + data: &ElementData, + propagated_hint: RestyleHint, + is_initial_style: bool, + mut note_child: F, +) where + E: TElement, + D: DomTraversal<E>, + F: FnMut(E::ConcreteNode), +{ + trace!("note_children: {:?}", element); + let flags = context.shared.traversal_flags; + + // Loop over all the traversal children. + for child_node in element.traversal_children() { + let child = match child_node.as_element() { + Some(el) => el, + None => { + if is_servo_nonincremental_layout() || + D::text_node_needs_traversal(child_node, data) + { + note_child(child_node); + } + continue; + }, + }; + + let mut child_data = child.mutate_data(); + let mut child_data = child_data.as_mut().map(|d| &mut **d); + trace!( + " > {:?} -> {:?} + {:?}, pseudo: {:?}", + child, + child_data.as_ref().map(|d| d.hint), + propagated_hint, + child.implemented_pseudo_element() + ); + + if let Some(ref mut child_data) = child_data { + child_data.hint.insert(propagated_hint); + + // Handle element snapshots and invalidation of descendants and siblings + // as needed. + // + // NB: This will be a no-op if there's no snapshot. + child_data.invalidate_style_if_needed( + child, + &context.shared, + Some(&context.thread_local.stack_limit_checker), + &mut context.thread_local.selector_caches, + ); + } + + if D::element_needs_traversal(child, flags, child_data.map(|d| &*d)) { + note_child(child_node); + + // Set the dirty descendants bit on the parent as needed, so that we + // can find elements during the post-traversal. + // + // Note that these bits may be cleared again at the bottom of + // recalc_style_at if requested by the caller. + if !is_initial_style { + if flags.for_animation_only() { + unsafe { + element.set_animation_only_dirty_descendants(); + } + } else { + unsafe { + element.set_dirty_descendants(); + } + } + } + } + } +} + +/// Clear style data for all the subtree under `root` (but not for root itself). +/// +/// We use a list to avoid unbounded recursion, which we need to avoid in the +/// parallel traversal because the rayon stacks are small. +pub unsafe fn clear_descendant_data<E>(root: E) +where + E: TElement, +{ + let mut parents = SmallVec::<[E; 32]>::new(); + parents.push(root); + while let Some(p) = parents.pop() { + for kid in p.traversal_children() { + if let Some(kid) = kid.as_element() { + // We maintain an invariant that, if an element has data, all its + // ancestors have data as well. + // + // By consequence, any element without data has no descendants with + // data. + if kid.has_data() { + kid.clear_data(); + parents.push(kid); + } + } + } + } + + // Make sure not to clear NODE_NEEDS_FRAME on the root. + root.clear_descendant_bits(); +} diff --git a/servo/components/style/traversal_flags.rs b/servo/components/style/traversal_flags.rs new file mode 100644 index 0000000000..54972043fd --- /dev/null +++ b/servo/components/style/traversal_flags.rs @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Flags that control the traversal process. +//! +//! We CamelCase rather than UPPER_CASING so that we can grep for the same +//! strings across gecko and servo. +#![allow(non_upper_case_globals)] + +bitflags! { + /// Flags that control the traversal process. + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct TraversalFlags: u32 { + /// Traverse only elements for animation restyles. + const AnimationOnly = 1 << 0; + /// Traverse and update all elements with CSS animations since + /// @keyframes rules may have changed. Triggered by CSS rule changes. + const ForCSSRuleChanges = 1 << 1; + /// The final animation-only traversal, which shouldn't really care about other + /// style changes anymore. + const FinalAnimationTraversal = 1 << 2; + /// Allows the traversal to run in parallel if there are sufficient cores on + /// the machine. + const ParallelTraversal = 1 << 7; + /// Flush throttled animations. By default, we only update throttled animations + /// when we have other non-throttled work to do. With this flag, we + /// unconditionally tick and process them. + const FlushThrottledAnimations = 1 << 8; + + } +} + +/// Asserts that all TraversalFlags flags have a matching ServoTraversalFlags value in gecko. +#[cfg(feature = "gecko")] +#[inline] +pub fn assert_traversal_flags_match() { + use crate::gecko_bindings::structs; + + macro_rules! check_traversal_flags { + ( $( $a:ident => $b:path ),*, ) => { + if cfg!(debug_assertions) { + let mut modes = TraversalFlags::all(); + $( + assert_eq!(structs::$a as usize, $b.bits() as usize, stringify!($b)); + modes.remove($b); + )* + assert_eq!(modes, TraversalFlags::empty(), "all TraversalFlags bits should have an assertion"); + } + } + } + + check_traversal_flags! { + ServoTraversalFlags_AnimationOnly => TraversalFlags::AnimationOnly, + ServoTraversalFlags_ForCSSRuleChanges => TraversalFlags::ForCSSRuleChanges, + ServoTraversalFlags_FinalAnimationTraversal => TraversalFlags::FinalAnimationTraversal, + ServoTraversalFlags_ParallelTraversal => TraversalFlags::ParallelTraversal, + ServoTraversalFlags_FlushThrottledAnimations => TraversalFlags::FlushThrottledAnimations, + } +} + +impl TraversalFlags { + /// Returns true if the traversal is for animation-only restyles. + #[inline] + pub fn for_animation_only(&self) -> bool { + self.contains(TraversalFlags::AnimationOnly) + } +} diff --git a/servo/components/style/use_counters/mod.rs b/servo/components/style/use_counters/mod.rs new file mode 100644 index 0000000000..5a87372570 --- /dev/null +++ b/servo/components/style/use_counters/mod.rs @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Various stuff for CSS property use counters. + +use crate::properties::{property_counts, CountedUnknownProperty, NonCustomPropertyId}; +use std::cell::Cell; + +#[cfg(target_pointer_width = "64")] +const BITS_PER_ENTRY: usize = 64; + +#[cfg(target_pointer_width = "32")] +const BITS_PER_ENTRY: usize = 32; + +/// One bit per each non-custom CSS property. +#[derive(Default)] +pub struct CountedUnknownPropertyUseCounters { + storage: + [Cell<usize>; (property_counts::COUNTED_UNKNOWN - 1 + BITS_PER_ENTRY) / BITS_PER_ENTRY], +} + +/// One bit per each non-custom CSS property. +#[derive(Default)] +pub struct NonCustomPropertyUseCounters { + storage: [Cell<usize>; (property_counts::NON_CUSTOM - 1 + BITS_PER_ENTRY) / BITS_PER_ENTRY], +} + +macro_rules! property_use_counters_methods { + ($id: ident) => { + /// Returns the bucket a given property belongs in, and the bitmask for that + /// property. + #[inline(always)] + fn bucket_and_pattern(id: $id) -> (usize, usize) { + let bit = id.bit(); + let bucket = bit / BITS_PER_ENTRY; + let bit_in_bucket = bit % BITS_PER_ENTRY; + (bucket, 1 << bit_in_bucket) + } + + /// Record that a given property ID has been parsed. + #[inline] + pub fn record(&self, id: $id) { + let (bucket, pattern) = Self::bucket_and_pattern(id); + let bucket = &self.storage[bucket]; + bucket.set(bucket.get() | pattern) + } + + /// Returns whether a given property ID has been recorded + /// earlier. + #[inline] + pub fn recorded(&self, id: $id) -> bool { + let (bucket, pattern) = Self::bucket_and_pattern(id); + self.storage[bucket].get() & pattern != 0 + } + + /// Merge `other` into `self`. + #[inline] + fn merge(&self, other: &Self) { + for (bucket, other_bucket) in self.storage.iter().zip(other.storage.iter()) { + bucket.set(bucket.get() | other_bucket.get()) + } + } + }; +} + +impl CountedUnknownPropertyUseCounters { + property_use_counters_methods!(CountedUnknownProperty); +} + +impl NonCustomPropertyUseCounters { + property_use_counters_methods!(NonCustomPropertyId); +} + +/// The use-counter data related to a given document we want to store. +#[derive(Default)] +pub struct UseCounters { + /// The counters for non-custom properties that have been parsed in the + /// document's stylesheets. + pub non_custom_properties: NonCustomPropertyUseCounters, + /// The counters for css properties which we haven't implemented yet. + pub counted_unknown_properties: CountedUnknownPropertyUseCounters, +} + +impl UseCounters { + /// Merge the use counters. + /// + /// Used for parallel parsing, where we parse off-main-thread. + #[inline] + pub fn merge(&self, other: &Self) { + self.non_custom_properties + .merge(&other.non_custom_properties); + self.counted_unknown_properties + .merge(&other.counted_unknown_properties); + } +} diff --git a/servo/components/style/values/animated/color.rs b/servo/components/style/values/animated/color.rs new file mode 100644 index 0000000000..f608b72e53 --- /dev/null +++ b/servo/components/style/values/animated/color.rs @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for CSS colors. + +use crate::color::mix::ColorInterpolationMethod; +use crate::color::AbsoluteColor; +use crate::values::animated::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::Percentage; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::color::{ColorMixFlags, GenericColor, GenericColorMix}; + +impl Animate for AbsoluteColor { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (left_weight, right_weight) = procedure.weights(); + Ok(crate::color::mix::mix( + ColorInterpolationMethod::best_interpolation_between(self, other), + self, + left_weight as f32, + other, + right_weight as f32, + ColorMixFlags::empty(), + )) + } +} + +impl ComputeSquaredDistance for AbsoluteColor { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let start = [ + self.alpha, + self.components.0 * self.alpha, + self.components.1 * self.alpha, + self.components.2 * self.alpha, + ]; + let end = [ + other.alpha, + other.components.0 * other.alpha, + other.components.1 * other.alpha, + other.components.2 * other.alpha, + ]; + start + .iter() + .zip(&end) + .map(|(this, other)| this.compute_squared_distance(other)) + .sum() + } +} + +/// An animated value for `<color>`. +pub type Color = GenericColor<Percentage>; + +/// An animated value for `<color-mix>`. +pub type ColorMix = GenericColorMix<Color, Percentage>; + +impl Animate for Color { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (left_weight, right_weight) = procedure.weights(); + Ok(Self::from_color_mix(ColorMix { + interpolation: ColorInterpolationMethod::srgb(), + left: self.clone(), + left_percentage: Percentage(left_weight as f32), + right: other.clone(), + right_percentage: Percentage(right_weight as f32), + // See https://github.com/w3c/csswg-drafts/issues/7324 + flags: ColorMixFlags::empty(), + })) + } +} + +impl ComputeSquaredDistance for Color { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let current_color = AbsoluteColor::TRANSPARENT_BLACK; + self.resolve_to_absolute(¤t_color) + .compute_squared_distance(&other.resolve_to_absolute(¤t_color)) + } +} + +impl ToAnimatedZero for Color { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(Color::Absolute(AbsoluteColor::TRANSPARENT_BLACK)) + } +} diff --git a/servo/components/style/values/animated/effects.rs b/servo/components/style/values/animated/effects.rs new file mode 100644 index 0000000000..67557e54b7 --- /dev/null +++ b/servo/components/style/values/animated/effects.rs @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for CSS values related to effects. + +use crate::values::animated::color::Color; +use crate::values::computed::length::Length; +#[cfg(feature = "gecko")] +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Angle, Number}; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +#[cfg(not(feature = "gecko"))] +use crate::values::Impossible; + +/// An animated value for the `drop-shadow()` filter. +pub type AnimatedSimpleShadow = GenericSimpleShadow<Color, Length, Length>; + +/// An animated value for a single `filter`. +#[cfg(feature = "gecko")] +pub type AnimatedFilter = + GenericFilter<Angle, Number, Number, Length, AnimatedSimpleShadow, ComputedUrl>; + +/// An animated value for a single `filter`. +#[cfg(not(feature = "gecko"))] +pub type AnimatedFilter = GenericFilter<Angle, Number, Number, Length, Impossible, Impossible>; diff --git a/servo/components/style/values/animated/font.rs b/servo/components/style/values/animated/font.rs new file mode 100644 index 0000000000..63d4a14b2f --- /dev/null +++ b/servo/components/style/values/animated/font.rs @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementation for various font-related types. + +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::font::FontVariationSettings; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; + +/// <https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def> +/// +/// Note that the ComputedValue implementation will already have sorted and de-dup'd +/// the lists of settings, so we can just iterate over the two lists together and +/// animate their individual values. +impl Animate for FontVariationSettings { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let result: Vec<_> = + super::lists::by_computed_value::animate(&self.0, &other.0, procedure)?; + Ok(Self(result.into_boxed_slice())) + } +} + +impl ComputeSquaredDistance for FontVariationSettings { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + super::lists::by_computed_value::squared_distance(&self.0, &other.0) + } +} + +impl ToAnimatedZero for FontVariationSettings { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} diff --git a/servo/components/style/values/animated/grid.rs b/servo/components/style/values/animated/grid.rs new file mode 100644 index 0000000000..04f1a2fcaa --- /dev/null +++ b/servo/components/style/values/animated/grid.rs @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementation for various grid-related types. + +// Note: we can implement Animate on their generic types directly, but in this case we need to +// make sure two trait bounds, L: Clone and I: PartialEq, are satisfied on almost all the +// grid-related types and their other trait implementations because Animate needs them. So in +// order to avoid adding these two trait bounds (or maybe more..) everywhere, we implement +// Animate for the computed types, instead of the generic types. + +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::Integer; +use crate::values::computed::LengthPercentage; +use crate::values::computed::{GridTemplateComponent, TrackList, TrackSize}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::grid as generics; + +fn discrete<T: Clone>(from: &T, to: &T, procedure: Procedure) -> Result<T, ()> { + if let Procedure::Interpolate { progress } = procedure { + Ok(if progress < 0.5 { + from.clone() + } else { + to.clone() + }) + } else { + // The discrete animation is not additive, so per spec [1] we should use the |from|, which + // is the underlying value. However this mismatches our animation mechanism (see + // composite_endpoint() in servo/ports/geckolib/glues.rs), which uses the effect value + // (i.e. |to| value here) [2]. So in order to match the behavior of other properties and + // other browsers, we use |to| value for addition and accumulation, i.e. Vresult = Vb. + // + // [1] https://drafts.csswg.org/css-values-4/#not-additive + // [2] https://github.com/w3c/csswg-drafts/issues/9070 + Ok(to.clone()) + } +} + +fn animate_with_discrete_fallback<T: Animate + Clone>( + from: &T, + to: &T, + procedure: Procedure, +) -> Result<T, ()> { + from.animate(to, procedure) + .or_else(|_| discrete(from, to, procedure)) +} + +impl Animate for TrackSize { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&generics::TrackSize::Breadth(ref from), &generics::TrackSize::Breadth(ref to)) => { + animate_with_discrete_fallback(from, to, procedure) + .map(generics::TrackSize::Breadth) + }, + ( + &generics::TrackSize::Minmax(ref from_min, ref from_max), + &generics::TrackSize::Minmax(ref to_min, ref to_max), + ) => Ok(generics::TrackSize::Minmax( + animate_with_discrete_fallback(from_min, to_min, procedure)?, + animate_with_discrete_fallback(from_max, to_max, procedure)?, + )), + ( + &generics::TrackSize::FitContent(ref from), + &generics::TrackSize::FitContent(ref to), + ) => animate_with_discrete_fallback(from, to, procedure) + .map(generics::TrackSize::FitContent), + (_, _) => discrete(self, other, procedure), + } + } +} + +impl Animate for generics::TrackRepeat<LengthPercentage, Integer> { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + // If the keyword, auto-fit/fill, is the same it can result in different + // number of tracks. For both auto-fit/fill, the number of columns isn't + // known until you do layout since it depends on the container size, item + // placement and other factors, so we cannot do the correct interpolation + // by computed values. Therefore, return Err(()) if it's keywords. If it + // is Number, we support animation only if the count is the same and the + // length of track_sizes is the same. + // https://github.com/w3c/csswg-drafts/issues/3503 + match (&self.count, &other.count) { + (&generics::RepeatCount::Number(from), &generics::RepeatCount::Number(to)) + if from == to => + { + () + }, + (_, _) => return Err(()), + } + + let count = self.count; + let track_sizes = super::lists::by_computed_value::animate( + &self.track_sizes, + &other.track_sizes, + procedure, + )?; + + // The length of |line_names| is always 0 or N+1, where N is the length + // of |track_sizes|. Besides, <line-names> is always discrete. + let line_names = discrete(&self.line_names, &other.line_names, procedure)?; + + Ok(generics::TrackRepeat { + count, + line_names, + track_sizes, + }) + } +} + +impl Animate for TrackList { + // Based on https://github.com/w3c/csswg-drafts/issues/3201: + // 1. Check interpolation type per track, so we need to handle discrete animations + // in TrackSize, so any Err(()) returned from TrackSize doesn't make all TrackSize + // fallback to discrete animation. + // 2. line-names is always discrete. + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.values.len() != other.values.len() { + return Err(()); + } + + if self.is_explicit() != other.is_explicit() { + return Err(()); + } + + // For now, repeat(auto-fill/auto-fit, ...) is not animatable. + // TrackRepeat will return Err(()) if we use keywords. Therefore, we can + // early return here to avoid traversing |values| in <auto-track-list>. + // This may be updated in the future. + // https://github.com/w3c/csswg-drafts/issues/3503 + if self.has_auto_repeat() || other.has_auto_repeat() { + return Err(()); + } + + let values = + super::lists::by_computed_value::animate(&self.values, &other.values, procedure)?; + + // The length of |line_names| is always 0 or N+1, where N is the length + // of |track_sizes|. Besides, <line-names> is always discrete. + let line_names = discrete(&self.line_names, &other.line_names, procedure)?; + + Ok(TrackList { + values, + line_names, + auto_repeat_index: self.auto_repeat_index, + }) + } +} + +impl ComputeSquaredDistance for GridTemplateComponent { + #[inline] + fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> { + // TODO: Bug 1518585, we should implement ComputeSquaredDistance. + Err(()) + } +} + +impl ToAnimatedZero for GridTemplateComponent { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + // It's not clear to get a zero grid track list based on the current definition + // of spec, so we return Err(()) directly. + Err(()) + } +} diff --git a/servo/components/style/values/animated/lists.rs b/servo/components/style/values/animated/lists.rs new file mode 100644 index 0000000000..8b3898c497 --- /dev/null +++ b/servo/components/style/values/animated/lists.rs @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Lists have various ways of being animated, this module implements them. +//! +//! See https://drafts.csswg.org/web-animations-1/#animating-properties + +/// https://drafts.csswg.org/web-animations-1/#by-computed-value +pub mod by_computed_value { + use crate::values::{ + animated::{Animate, Procedure}, + distance::{ComputeSquaredDistance, SquaredDistance}, + }; + use std::iter::FromIterator; + + #[allow(missing_docs)] + pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()> + where + T: Animate, + C: FromIterator<T>, + { + if left.len() != right.len() { + return Err(()); + } + left.iter() + .zip(right.iter()) + .map(|(left, right)| left.animate(right, procedure)) + .collect() + } + + #[allow(missing_docs)] + pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()> + where + T: ComputeSquaredDistance, + { + if left.len() != right.len() { + return Err(()); + } + left.iter() + .zip(right.iter()) + .map(|(left, right)| left.compute_squared_distance(right)) + .sum() + } +} + +/// This is the animation used for some of the types like shadows and filters, where the +/// interpolation happens with the zero value if one of the sides is not present. +/// +/// https://drafts.csswg.org/web-animations-1/#animating-shadow-lists +pub mod with_zero { + use crate::values::animated::ToAnimatedZero; + use crate::values::{ + animated::{Animate, Procedure}, + distance::{ComputeSquaredDistance, SquaredDistance}, + }; + use itertools::{EitherOrBoth, Itertools}; + use std::iter::FromIterator; + + #[allow(missing_docs)] + pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()> + where + T: Animate + Clone + ToAnimatedZero, + C: FromIterator<T>, + { + if procedure == Procedure::Add { + return Ok(left.iter().chain(right.iter()).cloned().collect()); + } + left.iter() + .zip_longest(right.iter()) + .map(|it| match it { + EitherOrBoth::Both(left, right) => left.animate(right, procedure), + EitherOrBoth::Left(left) => left.animate(&left.to_animated_zero()?, procedure), + EitherOrBoth::Right(right) => right.to_animated_zero()?.animate(right, procedure), + }) + .collect() + } + + #[allow(missing_docs)] + pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()> + where + T: ToAnimatedZero + ComputeSquaredDistance, + { + left.iter() + .zip_longest(right.iter()) + .map(|it| match it { + EitherOrBoth::Both(left, right) => left.compute_squared_distance(right), + EitherOrBoth::Left(item) | EitherOrBoth::Right(item) => { + item.to_animated_zero()?.compute_squared_distance(item) + }, + }) + .sum() + } +} + +/// https://drafts.csswg.org/web-animations-1/#repeatable-list +pub mod repeatable_list { + use crate::values::{ + animated::{Animate, Procedure}, + distance::{ComputeSquaredDistance, SquaredDistance}, + }; + use std::iter::FromIterator; + + #[allow(missing_docs)] + pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()> + where + T: Animate, + C: FromIterator<T>, + { + use num_integer::lcm; + // If the length of either list is zero, the least common multiple is undefined. + if left.is_empty() || right.is_empty() { + return Err(()); + } + let len = lcm(left.len(), right.len()); + left.iter() + .cycle() + .zip(right.iter().cycle()) + .take(len) + .map(|(left, right)| left.animate(right, procedure)) + .collect() + } + + #[allow(missing_docs)] + pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()> + where + T: ComputeSquaredDistance, + { + use num_integer::lcm; + if left.is_empty() || right.is_empty() { + return Err(()); + } + let len = lcm(left.len(), right.len()); + left.iter() + .cycle() + .zip(right.iter().cycle()) + .take(len) + .map(|(left, right)| left.compute_squared_distance(right)) + .sum() + } +} diff --git a/servo/components/style/values/animated/mod.rs b/servo/components/style/values/animated/mod.rs new file mode 100644 index 0000000000..31ea206fc0 --- /dev/null +++ b/servo/components/style/values/animated/mod.rs @@ -0,0 +1,487 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated values. +//! +//! Some values, notably colors, cannot be interpolated directly with their +//! computed values and need yet another intermediate representation. This +//! module's raison d'être is to ultimately contain all these types. + +use crate::color::AbsoluteColor; +use crate::properties::PropertyId; +use crate::values::computed::length::LengthPercentage; +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::Angle as ComputedAngle; +use crate::values::computed::Image; +use crate::values::specified::SVGPathData; +use crate::values::CSSFloat; +use app_units::Au; +use smallvec::SmallVec; +use std::cmp; + +pub mod color; +pub mod effects; +mod font; +mod grid; +pub mod lists; +mod svg; +pub mod transform; + +/// The category a property falls into for ordering purposes. +/// +/// https://drafts.csswg.org/web-animations/#calculating-computed-keyframes +#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +enum PropertyCategory { + Custom, + PhysicalLonghand, + LogicalLonghand, + Shorthand, +} + +impl PropertyCategory { + fn of(id: &PropertyId) -> Self { + match *id { + PropertyId::NonCustom(id) => match id.longhand_or_shorthand() { + Ok(id) => if id.is_logical() { + PropertyCategory::LogicalLonghand + } else { + PropertyCategory::PhysicalLonghand + }, + Err(..) => PropertyCategory::Shorthand, + }, + PropertyId::Custom(..) => PropertyCategory::Custom, + } + } +} + +/// A comparator to sort PropertyIds such that physical longhands are sorted +/// before logical longhands and shorthands, shorthands with fewer components +/// are sorted before shorthands with more components, and otherwise shorthands +/// are sorted by IDL name as defined by [Web Animations][property-order]. +/// +/// Using this allows us to prioritize values specified by longhands (or smaller +/// shorthand subsets) when longhands and shorthands are both specified on the +/// one keyframe. +/// +/// [property-order] https://drafts.csswg.org/web-animations/#calculating-computed-keyframes +pub fn compare_property_priority(a: &PropertyId, b: &PropertyId) -> cmp::Ordering { + let a_category = PropertyCategory::of(a); + let b_category = PropertyCategory::of(b); + + if a_category != b_category { + return a_category.cmp(&b_category); + } + + if a_category != PropertyCategory::Shorthand { + return cmp::Ordering::Equal; + } + + let a = a.as_shorthand().unwrap(); + let b = b.as_shorthand().unwrap(); + // Within shorthands, sort by the number of subproperties, then by IDL + // name. + let subprop_count_a = a.longhands().count(); + let subprop_count_b = b.longhands().count(); + subprop_count_a + .cmp(&subprop_count_b) + .then_with(|| a.idl_name_sort_order().cmp(&b.idl_name_sort_order())) +} + +/// A helper function to animate two multiplicative factor. +pub fn animate_multiplicative_factor( + this: CSSFloat, + other: CSSFloat, + procedure: Procedure, +) -> Result<CSSFloat, ()> { + Ok((this - 1.).animate(&(other - 1.), procedure)? + 1.) +} + +/// Animate from one value to another. +/// +/// This trait is derivable with `#[derive(Animate)]`. The derived +/// implementation uses a `match` expression with identical patterns for both +/// `self` and `other`, calling `Animate::animate` on each fields of the values. +/// If a field is annotated with `#[animation(constant)]`, the two values should +/// be equal or an error is returned. +/// +/// If a variant is annotated with `#[animation(error)]`, the corresponding +/// `match` arm returns an error. +/// +/// Trait bounds for type parameter `Foo` can be opted out of with +/// `#[animation(no_bound(Foo))]` on the type definition, trait bounds for +/// fields can be opted into with `#[animation(field_bound)]` on the field. +pub trait Animate: Sized { + /// Animate a value towards another one, given an animation procedure. + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()>; +} + +/// An animation procedure. +/// +/// <https://drafts.csswg.org/web-animations/#procedures-for-animating-properties> +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Procedure { + /// <https://drafts.csswg.org/web-animations/#animation-interpolation> + Interpolate { progress: f64 }, + /// <https://drafts.csswg.org/web-animations/#animation-addition> + Add, + /// <https://drafts.csswg.org/web-animations/#animation-accumulation> + Accumulate { count: u64 }, +} + +/// Conversion between computed values and intermediate values for animations. +/// +/// Notably, colors are represented as four floats during animations. +/// +/// This trait is derivable with `#[derive(ToAnimatedValue)]`. +pub trait ToAnimatedValue { + /// The type of the animated value. + type AnimatedValue; + + /// Converts this value to an animated value. + fn to_animated_value(self) -> Self::AnimatedValue; + + /// Converts back an animated value into a computed value. + fn from_animated_value(animated: Self::AnimatedValue) -> Self; +} + +/// Returns a value similar to `self` that represents zero. +/// +/// This trait is derivable with `#[derive(ToAnimatedValue)]`. If a field is +/// annotated with `#[animation(constant)]`, a clone of its value will be used +/// instead of calling `ToAnimatedZero::to_animated_zero` on it. +/// +/// If a variant is annotated with `#[animation(error)]`, the corresponding +/// `match` arm is not generated. +/// +/// Trait bounds for type parameter `Foo` can be opted out of with +/// `#[animation(no_bound(Foo))]` on the type definition. +pub trait ToAnimatedZero: Sized { + /// Returns a value that, when added with an underlying value, will produce the underlying + /// value. This is used for SMIL animation's "by-animation" where SMIL first interpolates from + /// the zero value to the 'by' value, and then adds the result to the underlying value. + /// + /// This is not the necessarily the same as the initial value of a property. For example, the + /// initial value of 'stroke-width' is 1, but the zero value is 0, since adding 1 to the + /// underlying value will not produce the underlying value. + fn to_animated_zero(&self) -> Result<Self, ()>; +} + +impl Procedure { + /// Returns this procedure as a pair of weights. + /// + /// This is useful for animations that don't animate differently + /// depending on the used procedure. + #[inline] + pub fn weights(self) -> (f64, f64) { + match self { + Procedure::Interpolate { progress } => (1. - progress, progress), + Procedure::Add => (1., 1.), + Procedure::Accumulate { count } => (count as f64, 1.), + } + } +} + +/// <https://drafts.csswg.org/css-transitions/#animtype-number> +impl Animate for i32 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(((*self as f64).animate(&(*other as f64), procedure)? + 0.5).floor() as i32) + } +} + +/// <https://drafts.csswg.org/css-transitions/#animtype-number> +impl Animate for f32 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let ret = (*self as f64).animate(&(*other as f64), procedure)?; + Ok(ret.min(f32::MAX as f64).max(f32::MIN as f64) as f32) + } +} + +/// <https://drafts.csswg.org/css-transitions/#animtype-number> +impl Animate for f64 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (self_weight, other_weight) = procedure.weights(); + + let ret = *self * self_weight + *other * other_weight; + Ok(ret.min(f64::MAX).max(f64::MIN)) + } +} + +impl<T> Animate for Option<T> +where + T: Animate, +{ + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self.as_ref(), other.as_ref()) { + (Some(ref this), Some(ref other)) => Ok(Some(this.animate(other, procedure)?)), + (None, None) => Ok(None), + _ => Err(()), + } + } +} + +impl Animate for Au { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Au::new(self.0.animate(&other.0, procedure)?)) + } +} + +impl<T: Animate> Animate for Box<T> { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Box::new((**self).animate(&other, procedure)?)) + } +} + +impl<T> ToAnimatedValue for Option<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Option<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.map(T::to_animated_value) + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.map(T::from_animated_value) + } +} + +impl<T> ToAnimatedValue for Vec<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Vec<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_iter().map(T::to_animated_value).collect() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.into_iter().map(T::from_animated_value).collect() + } +} + +impl<T> ToAnimatedValue for Box<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Box<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + Box::new((*self).to_animated_value()) + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Box::new(T::from_animated_value(*animated)) + } +} + +impl<T> ToAnimatedValue for Box<[T]> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Box<[<T as ToAnimatedValue>::AnimatedValue]>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_vec() + .into_iter() + .map(T::to_animated_value) + .collect::<Vec<_>>() + .into_boxed_slice() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated + .into_vec() + .into_iter() + .map(T::from_animated_value) + .collect::<Vec<_>>() + .into_boxed_slice() + } +} + +impl<T> ToAnimatedValue for crate::OwnedSlice<T> +where + T: ToAnimatedValue, +{ + type AnimatedValue = crate::OwnedSlice<<T as ToAnimatedValue>::AnimatedValue>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_box().to_animated_value().into() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Self::from(Box::from_animated_value(animated.into_box())) + } +} + +impl<T> ToAnimatedValue for SmallVec<[T; 1]> +where + T: ToAnimatedValue, +{ + type AnimatedValue = SmallVec<[T::AnimatedValue; 1]>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_iter().map(T::to_animated_value).collect() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.into_iter().map(T::from_animated_value).collect() + } +} + +macro_rules! trivial_to_animated_value { + ($ty:ty) => { + impl $crate::values::animated::ToAnimatedValue for $ty { + type AnimatedValue = Self; + + #[inline] + fn to_animated_value(self) -> Self { + self + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated + } + } + }; +} + +trivial_to_animated_value!(Au); +trivial_to_animated_value!(LengthPercentage); +trivial_to_animated_value!(ComputedAngle); +trivial_to_animated_value!(ComputedUrl); +trivial_to_animated_value!(bool); +trivial_to_animated_value!(f32); +trivial_to_animated_value!(i32); +trivial_to_animated_value!(AbsoluteColor); +trivial_to_animated_value!(crate::values::generics::color::ColorMixFlags); +// Note: This implementation is for ToAnimatedValue of ShapeSource. +// +// SVGPathData uses Box<[T]>. If we want to derive ToAnimatedValue for all the +// types, we have to do "impl ToAnimatedValue for Box<[T]>" first. +// However, the general version of "impl ToAnimatedValue for Box<[T]>" needs to +// clone |T| and convert it into |T::AnimatedValue|. However, for SVGPathData +// that is unnecessary--moving |T| is sufficient. So here, we implement this +// trait manually. +trivial_to_animated_value!(SVGPathData); +// FIXME: Bug 1514342, Image is not animatable, but we still need to implement +// this to avoid adding this derive to generic::Image and all its arms. We can +// drop this after landing Bug 1514342. +trivial_to_animated_value!(Image); + +impl ToAnimatedZero for Au { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(Au(0)) + } +} + +impl ToAnimatedZero for f32 { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(0.) + } +} + +impl ToAnimatedZero for f64 { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(0.) + } +} + +impl ToAnimatedZero for i32 { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(0) + } +} + +impl<T> ToAnimatedZero for Box<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(Box::new((**self).to_animated_zero()?)) + } +} + +impl<T> ToAnimatedZero for Option<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + match *self { + Some(ref value) => Ok(Some(value.to_animated_zero()?)), + None => Ok(None), + } + } +} + +impl<T> ToAnimatedZero for Vec<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl<T> ToAnimatedZero for Box<[T]> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl<T> ToAnimatedZero for crate::OwnedSlice<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl<T> ToAnimatedZero for crate::ArcSlice<T> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + let v = self + .iter() + .map(|v| v.to_animated_zero()) + .collect::<Result<Vec<_>, _>>()?; + Ok(crate::ArcSlice::from_iter(v.into_iter())) + } +} diff --git a/servo/components/style/values/animated/svg.rs b/servo/components/style/values/animated/svg.rs new file mode 100644 index 0000000000..04e35098ad --- /dev/null +++ b/servo/components/style/values/animated/svg.rs @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementations for various SVG-related types. + +use super::{Animate, Procedure}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::svg::SVGStrokeDashArray; + +/// <https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty> +impl<L> Animate for SVGStrokeDashArray<L> +where + L: Clone + Animate, +{ + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) { + // Non-additive. + return Err(()); + } + match (self, other) { + (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => { + Ok(SVGStrokeDashArray::Values( + super::lists::repeatable_list::animate(this, other, procedure)?, + )) + }, + _ => Err(()), + } + } +} + +impl<L> ComputeSquaredDistance for SVGStrokeDashArray<L> +where + L: ComputeSquaredDistance, +{ + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + match (self, other) { + (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => { + super::lists::repeatable_list::squared_distance(this, other) + }, + _ => Err(()), + } + } +} diff --git a/servo/components/style/values/animated/transform.rs b/servo/components/style/values/animated/transform.rs new file mode 100644 index 0000000000..b91e3ed8bc --- /dev/null +++ b/servo/components/style/values/animated/transform.rs @@ -0,0 +1,1667 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for transform. +// There are still some implementation on Matrix3D in animated_properties.mako.rs +// because they still need mako to generate the code. + +use super::animate_multiplicative_factor; +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::transform::Rotate as ComputedRotate; +use crate::values::computed::transform::Scale as ComputedScale; +use crate::values::computed::transform::Transform as ComputedTransform; +use crate::values::computed::transform::TransformOperation as ComputedTransformOperation; +use crate::values::computed::transform::Translate as ComputedTranslate; +use crate::values::computed::transform::{DirectionVector, Matrix, Matrix3D}; +use crate::values::computed::Angle; +use crate::values::computed::{Length, LengthPercentage}; +use crate::values::computed::{Number, Percentage}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::transform::{self, Transform, TransformOperation}; +use crate::values::generics::transform::{Rotate, Scale, Translate}; +use crate::values::CSSFloat; +use crate::Zero; +use std::cmp; +use std::ops::Add; + +// ------------------------------------ +// Animations for Matrix/Matrix3D. +// ------------------------------------ +/// A 2d matrix for interpolation. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[allow(missing_docs)] +// FIXME: We use custom derive for ComputeSquaredDistance. However, If possible, we should convert +// the InnerMatrix2D into types with physical meaning. This custom derive computes the squared +// distance from each matrix item, and this makes the result different from that in Gecko if we +// have skew factor in the Matrix3D. +pub struct InnerMatrix2D { + pub m11: CSSFloat, + pub m12: CSSFloat, + pub m21: CSSFloat, + pub m22: CSSFloat, +} + +impl Animate for InnerMatrix2D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(InnerMatrix2D { + m11: animate_multiplicative_factor(self.m11, other.m11, procedure)?, + m12: self.m12.animate(&other.m12, procedure)?, + m21: self.m21.animate(&other.m21, procedure)?, + m22: animate_multiplicative_factor(self.m22, other.m22, procedure)?, + }) + } +} + +/// A 2d translation function. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] +pub struct Translate2D(f32, f32); + +/// A 2d scale function. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Scale2D(f32, f32); + +impl Animate for Scale2D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Scale2D( + animate_multiplicative_factor(self.0, other.0, procedure)?, + animate_multiplicative_factor(self.1, other.1, procedure)?, + )) + } +} + +/// A decomposed 2d matrix. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct MatrixDecomposed2D { + /// The translation function. + pub translate: Translate2D, + /// The scale function. + pub scale: Scale2D, + /// The rotation angle. + pub angle: f32, + /// The inner matrix. + pub matrix: InnerMatrix2D, +} + +impl Animate for MatrixDecomposed2D { + /// <https://drafts.csswg.org/css-transforms/#interpolation-of-decomposed-2d-matrix-values> + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + // If x-axis of one is flipped, and y-axis of the other, + // convert to an unflipped rotation. + let mut scale = self.scale; + let mut angle = self.angle; + let mut other_angle = other.angle; + if (scale.0 < 0.0 && other.scale.1 < 0.0) || (scale.1 < 0.0 && other.scale.0 < 0.0) { + scale.0 = -scale.0; + scale.1 = -scale.1; + angle += if angle < 0.0 { 180. } else { -180. }; + } + + // Don't rotate the long way around. + if angle == 0.0 { + angle = 360. + } + if other_angle == 0.0 { + other_angle = 360. + } + + if (angle - other_angle).abs() > 180. { + if angle > other_angle { + angle -= 360. + } else { + other_angle -= 360. + } + } + + // Interpolate all values. + let translate = self.translate.animate(&other.translate, procedure)?; + let scale = scale.animate(&other.scale, procedure)?; + let angle = angle.animate(&other_angle, procedure)?; + let matrix = self.matrix.animate(&other.matrix, procedure)?; + + Ok(MatrixDecomposed2D { + translate, + scale, + angle, + matrix, + }) + } +} + +impl ComputeSquaredDistance for MatrixDecomposed2D { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + // Use Radian to compute the distance. + const RAD_PER_DEG: f64 = std::f64::consts::PI / 180.0; + let angle1 = self.angle as f64 * RAD_PER_DEG; + let angle2 = other.angle as f64 * RAD_PER_DEG; + Ok(self.translate.compute_squared_distance(&other.translate)? + + self.scale.compute_squared_distance(&other.scale)? + + angle1.compute_squared_distance(&angle2)? + + self.matrix.compute_squared_distance(&other.matrix)?) + } +} + +impl From<Matrix3D> for MatrixDecomposed2D { + /// Decompose a 2D matrix. + /// <https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix> + fn from(matrix: Matrix3D) -> MatrixDecomposed2D { + let mut row0x = matrix.m11; + let mut row0y = matrix.m12; + let mut row1x = matrix.m21; + let mut row1y = matrix.m22; + + let translate = Translate2D(matrix.m41, matrix.m42); + let mut scale = Scale2D( + (row0x * row0x + row0y * row0y).sqrt(), + (row1x * row1x + row1y * row1y).sqrt(), + ); + + // If determinant is negative, one axis was flipped. + let determinant = row0x * row1y - row0y * row1x; + if determinant < 0. { + if row0x < row1y { + scale.0 = -scale.0; + } else { + scale.1 = -scale.1; + } + } + + // Renormalize matrix to remove scale. + if scale.0 != 0.0 { + row0x *= 1. / scale.0; + row0y *= 1. / scale.0; + } + if scale.1 != 0.0 { + row1x *= 1. / scale.1; + row1y *= 1. / scale.1; + } + + // Compute rotation and renormalize matrix. + let mut angle = row0y.atan2(row0x); + if angle != 0.0 { + let sn = -row0y; + let cs = row0x; + let m11 = row0x; + let m12 = row0y; + let m21 = row1x; + let m22 = row1y; + row0x = cs * m11 + sn * m21; + row0y = cs * m12 + sn * m22; + row1x = -sn * m11 + cs * m21; + row1y = -sn * m12 + cs * m22; + } + + let m = InnerMatrix2D { + m11: row0x, + m12: row0y, + m21: row1x, + m22: row1y, + }; + + // Convert into degrees because our rotation functions expect it. + angle = angle.to_degrees(); + MatrixDecomposed2D { + translate: translate, + scale: scale, + angle: angle, + matrix: m, + } + } +} + +impl From<MatrixDecomposed2D> for Matrix3D { + /// Recompose a 2D matrix. + /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-2d-matrix> + fn from(decomposed: MatrixDecomposed2D) -> Matrix3D { + let mut computed_matrix = Matrix3D::identity(); + computed_matrix.m11 = decomposed.matrix.m11; + computed_matrix.m12 = decomposed.matrix.m12; + computed_matrix.m21 = decomposed.matrix.m21; + computed_matrix.m22 = decomposed.matrix.m22; + + // Translate matrix. + computed_matrix.m41 = decomposed.translate.0; + computed_matrix.m42 = decomposed.translate.1; + + // Rotate matrix. + let angle = decomposed.angle.to_radians(); + let cos_angle = angle.cos(); + let sin_angle = angle.sin(); + + let mut rotate_matrix = Matrix3D::identity(); + rotate_matrix.m11 = cos_angle; + rotate_matrix.m12 = sin_angle; + rotate_matrix.m21 = -sin_angle; + rotate_matrix.m22 = cos_angle; + + // Multiplication of computed_matrix and rotate_matrix + computed_matrix = rotate_matrix.multiply(&computed_matrix); + + // Scale matrix. + computed_matrix.m11 *= decomposed.scale.0; + computed_matrix.m12 *= decomposed.scale.0; + computed_matrix.m21 *= decomposed.scale.1; + computed_matrix.m22 *= decomposed.scale.1; + computed_matrix + } +} + +impl Animate for Matrix { + #[cfg(feature = "servo")] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let this = Matrix3D::from(*self); + let other = Matrix3D::from(*other); + let this = MatrixDecomposed2D::from(this); + let other = MatrixDecomposed2D::from(other); + Matrix3D::from(this.animate(&other, procedure)?).into_2d() + } + + #[cfg(feature = "gecko")] + // Gecko doesn't exactly follow the spec here; we use a different procedure + // to match it + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let this = Matrix3D::from(*self); + let other = Matrix3D::from(*other); + let from = decompose_2d_matrix(&this)?; + let to = decompose_2d_matrix(&other)?; + Matrix3D::from(from.animate(&to, procedure)?).into_2d() + } +} + +/// A 3d translation. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] +pub struct Translate3D(pub f32, pub f32, pub f32); + +/// A 3d scale function. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Scale3D(pub f32, pub f32, pub f32); + +impl Scale3D { + /// Negate self. + fn negate(&mut self) { + self.0 *= -1.0; + self.1 *= -1.0; + self.2 *= -1.0; + } +} + +impl Animate for Scale3D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Scale3D( + animate_multiplicative_factor(self.0, other.0, procedure)?, + animate_multiplicative_factor(self.1, other.1, procedure)?, + animate_multiplicative_factor(self.2, other.2, procedure)?, + )) + } +} + +/// A 3d skew function. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Animate, Clone, Copy, Debug)] +pub struct Skew(f32, f32, f32); + +impl ComputeSquaredDistance for Skew { + // We have to use atan() to convert the skew factors into skew angles, so implement + // ComputeSquaredDistance manually. + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(self.0.atan().compute_squared_distance(&other.0.atan())? + + self.1.atan().compute_squared_distance(&other.1.atan())? + + self.2.atan().compute_squared_distance(&other.2.atan())?) + } +} + +/// A 3d perspective transformation. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Perspective(pub f32, pub f32, pub f32, pub f32); + +impl Animate for Perspective { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(Perspective( + self.0.animate(&other.0, procedure)?, + self.1.animate(&other.1, procedure)?, + self.2.animate(&other.2, procedure)?, + animate_multiplicative_factor(self.3, other.3, procedure)?, + )) + } +} + +/// A quaternion used to represent a rotation. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Quaternion(f64, f64, f64, f64); + +impl Quaternion { + /// Return a quaternion from a unit direction vector and angle (unit: radian). + #[inline] + fn from_direction_and_angle(vector: &DirectionVector, angle: f64) -> Self { + debug_assert!( + (vector.length() - 1.).abs() < 0.0001, + "Only accept an unit direction vector to create a quaternion" + ); + + // Quaternions between the range [360, 720] will treated as rotations at the other + // direction: [-360, 0]. And quaternions between the range [720*k, 720*(k+1)] will be + // treated as rotations [0, 720]. So it does not make sense to use quaternions to rotate + // the element more than ±360deg. Therefore, we have to make sure its range is (-360, 360). + let half_angle = angle + .abs() + .rem_euclid(std::f64::consts::TAU) + .copysign(angle) / + 2.; + + // Reference: + // https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation + // + // if the direction axis is (x, y, z) = xi + yj + zk, + // and the angle is |theta|, this formula can be done using + // an extension of Euler's formula: + // q = cos(theta/2) + (xi + yj + zk)(sin(theta/2)) + // = cos(theta/2) + + // x*sin(theta/2)i + y*sin(theta/2)j + z*sin(theta/2)k + Quaternion( + vector.x as f64 * half_angle.sin(), + vector.y as f64 * half_angle.sin(), + vector.z as f64 * half_angle.sin(), + half_angle.cos(), + ) + } + + /// Calculate the dot product. + #[inline] + fn dot(&self, other: &Self) -> f64 { + self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3 + } + + /// Return the scaled quaternion by a factor. + #[inline] + fn scale(&self, factor: f64) -> Self { + Quaternion( + self.0 * factor, + self.1 * factor, + self.2 * factor, + self.3 * factor, + ) + } +} + +impl Add for Quaternion { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self( + self.0 + other.0, + self.1 + other.1, + self.2 + other.2, + self.3 + other.3, + ) + } +} + +impl Animate for Quaternion { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (this_weight, other_weight) = procedure.weights(); + debug_assert!( + // Doule EPSILON since both this_weight and other_weght have calculation errors + // which are approximately equal to EPSILON. + (this_weight + other_weight - 1.0f64).abs() <= f64::EPSILON * 2.0 || + other_weight == 1.0f64 || + other_weight == 0.0f64, + "animate should only be used for interpolating or accumulating transforms" + ); + + // We take a specialized code path for accumulation (where other_weight + // is 1). + if let Procedure::Accumulate { .. } = procedure { + debug_assert_eq!(other_weight, 1.0); + if this_weight == 0.0 { + return Ok(*other); + } + + let clamped_w = self.3.min(1.0).max(-1.0); + + // Determine the scale factor. + let mut theta = clamped_w.acos(); + let mut scale = if theta == 0.0 { 0.0 } else { 1.0 / theta.sin() }; + theta *= this_weight; + scale *= theta.sin(); + + // Scale the self matrix by this_weight. + let mut scaled_self = *self; + scaled_self.0 *= scale; + scaled_self.1 *= scale; + scaled_self.2 *= scale; + scaled_self.3 = theta.cos(); + + // Multiply scaled-self by other. + let a = &scaled_self; + let b = other; + return Ok(Quaternion( + a.3 * b.0 + a.0 * b.3 + a.1 * b.2 - a.2 * b.1, + a.3 * b.1 - a.0 * b.2 + a.1 * b.3 + a.2 * b.0, + a.3 * b.2 + a.0 * b.1 - a.1 * b.0 + a.2 * b.3, + a.3 * b.3 - a.0 * b.0 - a.1 * b.1 - a.2 * b.2, + )); + } + + // https://drafts.csswg.org/css-transforms-2/#interpolation-of-decomposed-3d-matrix-values + // + // Dot product, clamped between -1 and 1. + let cos_half_theta = + (self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3) + .min(1.0) + .max(-1.0); + + if cos_half_theta.abs() == 1.0 { + return Ok(*self); + } + + let half_theta = cos_half_theta.acos(); + let sin_half_theta = (1.0 - cos_half_theta * cos_half_theta).sqrt(); + + let right_weight = (other_weight * half_theta).sin() / sin_half_theta; + // The spec would like to use + // "(other_weight * half_theta).cos() - cos_half_theta * right_weight". However, this + // formula may produce some precision issues of floating-point number calculation, e.g. + // when the progress is 100% (i.e. |other_weight| is 1), the |left_weight| may not be + // perfectly equal to 0. It could be something like -2.22e-16, which is approximately equal + // to zero, in the test. And after we recompose the Matrix3D, these approximated zeros + // make us failed to treat this Matrix3D as a Matrix2D, when serializating it. + // + // Therefore, we use another formula to calculate |left_weight| here. Blink and WebKit also + // use this formula, which is defined in: + // https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/index.htm + // https://github.com/w3c/csswg-drafts/issues/9338 + let left_weight = (this_weight * half_theta).sin() / sin_half_theta; + + Ok(self.scale(left_weight) + other.scale(right_weight)) + } +} + +impl ComputeSquaredDistance for Quaternion { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + // Use quaternion vectors to get the angle difference. Both q1 and q2 are unit vectors, + // so we can get their angle difference by: + // cos(theta/2) = (q1 dot q2) / (|q1| * |q2|) = q1 dot q2. + let distance = self.dot(other).max(-1.0).min(1.0).acos() * 2.0; + Ok(SquaredDistance::from_sqrt(distance)) + } +} + +/// A decomposed 3d matrix. +#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct MatrixDecomposed3D { + /// A translation function. + pub translate: Translate3D, + /// A scale function. + pub scale: Scale3D, + /// The skew component of the transformation. + pub skew: Skew, + /// The perspective component of the transformation. + pub perspective: Perspective, + /// The quaternion used to represent the rotation. + pub quaternion: Quaternion, +} + +impl From<MatrixDecomposed3D> for Matrix3D { + /// Recompose a 3D matrix. + /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-3d-matrix> + fn from(decomposed: MatrixDecomposed3D) -> Matrix3D { + let mut matrix = Matrix3D::identity(); + + // Apply perspective + matrix.set_perspective(&decomposed.perspective); + + // Apply translation + matrix.apply_translate(&decomposed.translate); + + // Apply rotation + { + let x = decomposed.quaternion.0; + let y = decomposed.quaternion.1; + let z = decomposed.quaternion.2; + let w = decomposed.quaternion.3; + + // Construct a composite rotation matrix from the quaternion values + // rotationMatrix is a identity 4x4 matrix initially + let mut rotation_matrix = Matrix3D::identity(); + rotation_matrix.m11 = 1.0 - 2.0 * (y * y + z * z) as f32; + rotation_matrix.m12 = 2.0 * (x * y + z * w) as f32; + rotation_matrix.m13 = 2.0 * (x * z - y * w) as f32; + rotation_matrix.m21 = 2.0 * (x * y - z * w) as f32; + rotation_matrix.m22 = 1.0 - 2.0 * (x * x + z * z) as f32; + rotation_matrix.m23 = 2.0 * (y * z + x * w) as f32; + rotation_matrix.m31 = 2.0 * (x * z + y * w) as f32; + rotation_matrix.m32 = 2.0 * (y * z - x * w) as f32; + rotation_matrix.m33 = 1.0 - 2.0 * (x * x + y * y) as f32; + + matrix = rotation_matrix.multiply(&matrix); + } + + // Apply skew + { + let mut temp = Matrix3D::identity(); + if decomposed.skew.2 != 0.0 { + temp.m32 = decomposed.skew.2; + matrix = temp.multiply(&matrix); + temp.m32 = 0.0; + } + + if decomposed.skew.1 != 0.0 { + temp.m31 = decomposed.skew.1; + matrix = temp.multiply(&matrix); + temp.m31 = 0.0; + } + + if decomposed.skew.0 != 0.0 { + temp.m21 = decomposed.skew.0; + matrix = temp.multiply(&matrix); + } + } + + // Apply scale + matrix.apply_scale(&decomposed.scale); + + matrix + } +} + +/// Decompose a 3D matrix. +/// https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix +/// http://www.realtimerendering.com/resources/GraphicsGems/gemsii/unmatrix.c +fn decompose_3d_matrix(mut matrix: Matrix3D) -> Result<MatrixDecomposed3D, ()> { + // Combine 2 point. + let combine = |a: [f32; 3], b: [f32; 3], ascl: f32, bscl: f32| { + [ + (ascl * a[0]) + (bscl * b[0]), + (ascl * a[1]) + (bscl * b[1]), + (ascl * a[2]) + (bscl * b[2]), + ] + }; + // Dot product. + let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + // Cross product. + let cross = |row1: [f32; 3], row2: [f32; 3]| { + [ + row1[1] * row2[2] - row1[2] * row2[1], + row1[2] * row2[0] - row1[0] * row2[2], + row1[0] * row2[1] - row1[1] * row2[0], + ] + }; + + if matrix.m44 == 0.0 { + return Err(()); + } + + let scaling_factor = matrix.m44; + + // Normalize the matrix. + matrix.scale_by_factor(1.0 / scaling_factor); + + // perspective_matrix is used to solve for perspective, but it also provides + // an easy way to test for singularity of the upper 3x3 component. + let mut perspective_matrix = matrix; + + perspective_matrix.m14 = 0.0; + perspective_matrix.m24 = 0.0; + perspective_matrix.m34 = 0.0; + perspective_matrix.m44 = 1.0; + + if perspective_matrix.determinant() == 0.0 { + return Err(()); + } + + // First, isolate perspective. + let perspective = if matrix.m14 != 0.0 || matrix.m24 != 0.0 || matrix.m34 != 0.0 { + let right_hand_side: [f32; 4] = [matrix.m14, matrix.m24, matrix.m34, matrix.m44]; + + perspective_matrix = perspective_matrix.inverse().unwrap().transpose(); + let perspective = perspective_matrix.pre_mul_point4(&right_hand_side); + // NOTE(emilio): Even though the reference algorithm clears the + // fourth column here (matrix.m14..matrix.m44), they're not used below + // so it's not really needed. + Perspective( + perspective[0], + perspective[1], + perspective[2], + perspective[3], + ) + } else { + Perspective(0.0, 0.0, 0.0, 1.0) + }; + + // Next take care of translation (easy). + let translate = Translate3D(matrix.m41, matrix.m42, matrix.m43); + + // Now get scale and shear. 'row' is a 3 element array of 3 component vectors + let mut row = matrix.get_matrix_3x3_part(); + + // Compute X scale factor and normalize first row. + let row0len = (row[0][0] * row[0][0] + row[0][1] * row[0][1] + row[0][2] * row[0][2]).sqrt(); + let mut scale = Scale3D(row0len, 0.0, 0.0); + row[0] = [ + row[0][0] / row0len, + row[0][1] / row0len, + row[0][2] / row0len, + ]; + + // Compute XY shear factor and make 2nd row orthogonal to 1st. + let mut skew = Skew(dot(row[0], row[1]), 0.0, 0.0); + row[1] = combine(row[1], row[0], 1.0, -skew.0); + + // Now, compute Y scale and normalize 2nd row. + let row1len = (row[1][0] * row[1][0] + row[1][1] * row[1][1] + row[1][2] * row[1][2]).sqrt(); + scale.1 = row1len; + row[1] = [ + row[1][0] / row1len, + row[1][1] / row1len, + row[1][2] / row1len, + ]; + skew.0 /= scale.1; + + // Compute XZ and YZ shears, orthogonalize 3rd row + skew.1 = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew.1); + skew.2 = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew.2); + + // Next, get Z scale and normalize 3rd row. + let row2len = (row[2][0] * row[2][0] + row[2][1] * row[2][1] + row[2][2] * row[2][2]).sqrt(); + scale.2 = row2len; + row[2] = [ + row[2][0] / row2len, + row[2][1] / row2len, + row[2][2] / row2len, + ]; + skew.1 /= scale.2; + skew.2 /= scale.2; + + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + if dot(row[0], cross(row[1], row[2])) < 0.0 { + scale.negate(); + for i in 0..3 { + row[i][0] *= -1.0; + row[i][1] *= -1.0; + row[i][2] *= -1.0; + } + } + + // Now, get the rotations out. + let mut quaternion = Quaternion( + 0.5 * ((1.0 + row[0][0] - row[1][1] - row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 - row[0][0] + row[1][1] - row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 - row[0][0] - row[1][1] + row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 + row[0][0] + row[1][1] + row[2][2]).max(0.0) as f64).sqrt(), + ); + + if row[2][1] > row[1][2] { + quaternion.0 = -quaternion.0 + } + if row[0][2] > row[2][0] { + quaternion.1 = -quaternion.1 + } + if row[1][0] > row[0][1] { + quaternion.2 = -quaternion.2 + } + + Ok(MatrixDecomposed3D { + translate, + scale, + skew, + perspective, + quaternion, + }) +} + +/** + * The relevant section of the transitions specification: + * https://drafts.csswg.org/web-animations-1/#animation-types + * http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types- + * defers all of the details to the 2-D and 3-D transforms specifications. + * For the 2-D transforms specification (all that's relevant for us, right + * now), the relevant section is: + * https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms + * This, in turn, refers to the unmatrix program in Graphics Gems, + * available from http://graphicsgems.org/ , and in + * particular as the file GraphicsGems/gemsii/unmatrix.c + * in http://graphicsgems.org/AllGems.tar.gz + * + * The unmatrix reference is for general 3-D transform matrices (any of the + * 16 components can have any value). + * + * For CSS 2-D transforms, we have a 2-D matrix with the bottom row constant: + * + * [ A C E ] + * [ B D F ] + * [ 0 0 1 ] + * + * For that case, I believe the algorithm in unmatrix reduces to: + * + * (1) If A * D - B * C == 0, the matrix is singular. Fail. + * + * (2) Set translation components (Tx and Ty) to the translation parts of + * the matrix (E and F) and then ignore them for the rest of the time. + * (For us, E and F each actually consist of three constants: a + * length, a multiplier for the width, and a multiplier for the + * height. This actually requires its own decomposition, but I'll + * keep that separate.) + * + * (3) Let the X scale (Sx) be sqrt(A^2 + B^2). Then divide both A and B + * by it. + * + * (4) Let the XY shear (K) be A * C + B * D. From C, subtract A times + * the XY shear. From D, subtract B times the XY shear. + * + * (5) Let the Y scale (Sy) be sqrt(C^2 + D^2). Divide C, D, and the XY + * shear (K) by it. + * + * (6) At this point, A * D - B * C is either 1 or -1. If it is -1, + * negate the XY shear (K), the X scale (Sx), and A, B, C, and D. + * (Alternatively, we could negate the XY shear (K) and the Y scale + * (Sy).) + * + * (7) Let the rotation be R = atan2(B, A). + * + * Then the resulting decomposed transformation is: + * + * translate(Tx, Ty) rotate(R) skewX(atan(K)) scale(Sx, Sy) + * + * An interesting result of this is that all of the simple transform + * functions (i.e., all functions other than matrix()), in isolation, + * decompose back to themselves except for: + * 'skewY(φ)', which is 'matrix(1, tan(φ), 0, 1, 0, 0)', which decomposes + * to 'rotate(φ) skewX(φ) scale(sec(φ), cos(φ))' since (ignoring the + * alternate sign possibilities that would get fixed in step 6): + * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) = + * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) = + * sin(φ). In step 4, the XY shear is sin(φ). Thus, after step 4, C = + * -cos(φ)sin(φ) and D = 1 - sin²(φ) = cos²(φ). Thus, in step 5, the Y scale is + * sqrt(cos²(φ)(sin²(φ) + cos²(φ)) = cos(φ). Thus, after step 5, C = -sin(φ), D + * = cos(φ), and the XY shear is tan(φ). Thus, in step 6, A * D - B * C = + * cos²(φ) + sin²(φ) = 1. In step 7, the rotation is thus φ. + * + * skew(θ, φ), which is matrix(1, tan(φ), tan(θ), 1, 0, 0), which decomposes + * to 'rotate(φ) skewX(θ + φ) scale(sec(φ), cos(φ))' since (ignoring + * the alternate sign possibilities that would get fixed in step 6): + * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) = + * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) = + * sin(φ). In step 4, the XY shear is cos(φ)tan(θ) + sin(φ). Thus, after step 4, + * C = tan(θ) - cos(φ)(cos(φ)tan(θ) + sin(φ)) = tan(θ)sin²(φ) - cos(φ)sin(φ) + * D = 1 - sin(φ)(cos(φ)tan(θ) + sin(φ)) = cos²(φ) - sin(φ)cos(φ)tan(θ) + * Thus, in step 5, the Y scale is sqrt(C² + D²) = + * sqrt(tan²(θ)(sin⁴(φ) + sin²(φ)cos²(φ)) - + * 2 tan(θ)(sin³(φ)cos(φ) + sin(φ)cos³(φ)) + + * (sin²(φ)cos²(φ) + cos⁴(φ))) = + * sqrt(tan²(θ)sin²(φ) - 2 tan(θ)sin(φ)cos(φ) + cos²(φ)) = + * cos(φ) - tan(θ)sin(φ) (taking the negative of the obvious solution so + * we avoid flipping in step 6). + * After step 5, C = -sin(φ) and D = cos(φ), and the XY shear is + * (cos(φ)tan(θ) + sin(φ)) / (cos(φ) - tan(θ)sin(φ)) = + * (dividing both numerator and denominator by cos(φ)) + * (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)) = tan(θ + φ). + * (See http://en.wikipedia.org/wiki/List_of_trigonometric_identities .) + * Thus, in step 6, A * D - B * C = cos²(φ) + sin²(φ) = 1. + * In step 7, the rotation is thus φ. + * + * To check this result, we can multiply things back together: + * + * [ cos(φ) -sin(φ) ] [ 1 tan(θ + φ) ] [ sec(φ) 0 ] + * [ sin(φ) cos(φ) ] [ 0 1 ] [ 0 cos(φ) ] + * + * [ cos(φ) cos(φ)tan(θ + φ) - sin(φ) ] [ sec(φ) 0 ] + * [ sin(φ) sin(φ)tan(θ + φ) + cos(φ) ] [ 0 cos(φ) ] + * + * but since tan(θ + φ) = (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)), + * cos(φ)tan(θ + φ) - sin(φ) + * = cos(φ)(tan(θ) + tan(φ)) - sin(φ) + sin(φ)tan(θ)tan(φ) + * = cos(φ)tan(θ) + sin(φ) - sin(φ) + sin(φ)tan(θ)tan(φ) + * = cos(φ)tan(θ) + sin(φ)tan(θ)tan(φ) + * = tan(θ) (cos(φ) + sin(φ)tan(φ)) + * = tan(θ) sec(φ) (cos²(φ) + sin²(φ)) + * = tan(θ) sec(φ) + * and + * sin(φ)tan(θ + φ) + cos(φ) + * = sin(φ)(tan(θ) + tan(φ)) + cos(φ) - cos(φ)tan(θ)tan(φ) + * = tan(θ) (sin(φ) - sin(φ)) + sin(φ)tan(φ) + cos(φ) + * = sec(φ) (sin²(φ) + cos²(φ)) + * = sec(φ) + * so the above is: + * [ cos(φ) tan(θ) sec(φ) ] [ sec(φ) 0 ] + * [ sin(φ) sec(φ) ] [ 0 cos(φ) ] + * + * [ 1 tan(θ) ] + * [ tan(φ) 1 ] + */ + +/// Decompose a 2D matrix for Gecko. This implements the above decomposition algorithm. +#[cfg(feature = "gecko")] +fn decompose_2d_matrix(matrix: &Matrix3D) -> Result<MatrixDecomposed3D, ()> { + // The index is column-major, so the equivalent transform matrix is: + // | m11 m21 0 m41 | => | m11 m21 | and translate(m41, m42) + // | m12 m22 0 m42 | | m12 m22 | + // | 0 0 1 0 | + // | 0 0 0 1 | + let (mut m11, mut m12) = (matrix.m11, matrix.m12); + let (mut m21, mut m22) = (matrix.m21, matrix.m22); + // Check if this is a singular matrix. + if m11 * m22 == m12 * m21 { + return Err(()); + } + + let mut scale_x = (m11 * m11 + m12 * m12).sqrt(); + m11 /= scale_x; + m12 /= scale_x; + + let mut shear_xy = m11 * m21 + m12 * m22; + m21 -= m11 * shear_xy; + m22 -= m12 * shear_xy; + + let scale_y = (m21 * m21 + m22 * m22).sqrt(); + m21 /= scale_y; + m22 /= scale_y; + shear_xy /= scale_y; + + let determinant = m11 * m22 - m12 * m21; + // Determinant should now be 1 or -1. + if 0.99 > determinant.abs() || determinant.abs() > 1.01 { + return Err(()); + } + + if determinant < 0. { + m11 = -m11; + m12 = -m12; + shear_xy = -shear_xy; + scale_x = -scale_x; + } + + Ok(MatrixDecomposed3D { + translate: Translate3D(matrix.m41, matrix.m42, 0.), + scale: Scale3D(scale_x, scale_y, 1.), + skew: Skew(shear_xy, 0., 0.), + perspective: Perspective(0., 0., 0., 1.), + quaternion: Quaternion::from_direction_and_angle( + &DirectionVector::new(0., 0., 1.), + m12.atan2(m11) as f64, + ), + }) +} + +impl Animate for Matrix3D { + #[cfg(feature = "servo")] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.is_3d() || other.is_3d() { + let decomposed_from = decompose_3d_matrix(*self); + let decomposed_to = decompose_3d_matrix(*other); + match (decomposed_from, decomposed_to) { + (Ok(this), Ok(other)) => Ok(Matrix3D::from(this.animate(&other, procedure)?)), + // Matrices can be undecomposable due to couple reasons, e.g., + // non-invertible matrices. In this case, we should report Err + // here, and let the caller do the fallback procedure. + _ => Err(()), + } + } else { + let this = MatrixDecomposed2D::from(*self); + let other = MatrixDecomposed2D::from(*other); + Ok(Matrix3D::from(this.animate(&other, procedure)?)) + } + } + + #[cfg(feature = "gecko")] + // Gecko doesn't exactly follow the spec here; we use a different procedure + // to match it + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let (from, to) = if self.is_3d() || other.is_3d() { + (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?) + } else { + (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?) + }; + // Matrices can be undecomposable due to couple reasons, e.g., + // non-invertible matrices. In this case, we should report Err here, + // and let the caller do the fallback procedure. + Ok(Matrix3D::from(from.animate(&to, procedure)?)) + } +} + +impl ComputeSquaredDistance for Matrix3D { + #[inline] + #[cfg(feature = "servo")] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.is_3d() || other.is_3d() { + let from = decompose_3d_matrix(*self)?; + let to = decompose_3d_matrix(*other)?; + from.compute_squared_distance(&to) + } else { + let from = MatrixDecomposed2D::from(*self); + let to = MatrixDecomposed2D::from(*other); + from.compute_squared_distance(&to) + } + } + + #[inline] + #[cfg(feature = "gecko")] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let (from, to) = if self.is_3d() || other.is_3d() { + (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?) + } else { + (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?) + }; + from.compute_squared_distance(&to) + } +} + +// ------------------------------------ +// Animation for Transform list. +// ------------------------------------ +fn is_matched_operation( + first: &ComputedTransformOperation, + second: &ComputedTransformOperation, +) -> bool { + match (first, second) { + (&TransformOperation::Matrix(..), &TransformOperation::Matrix(..)) | + (&TransformOperation::Matrix3D(..), &TransformOperation::Matrix3D(..)) | + (&TransformOperation::Skew(..), &TransformOperation::Skew(..)) | + (&TransformOperation::SkewX(..), &TransformOperation::SkewX(..)) | + (&TransformOperation::SkewY(..), &TransformOperation::SkewY(..)) | + (&TransformOperation::Rotate(..), &TransformOperation::Rotate(..)) | + (&TransformOperation::Rotate3D(..), &TransformOperation::Rotate3D(..)) | + (&TransformOperation::RotateX(..), &TransformOperation::RotateX(..)) | + (&TransformOperation::RotateY(..), &TransformOperation::RotateY(..)) | + (&TransformOperation::RotateZ(..), &TransformOperation::RotateZ(..)) | + (&TransformOperation::Perspective(..), &TransformOperation::Perspective(..)) => true, + // Match functions that have the same primitive transform function + (a, b) if a.is_translate() && b.is_translate() => true, + (a, b) if a.is_scale() && b.is_scale() => true, + (a, b) if a.is_rotate() && b.is_rotate() => true, + // InterpolateMatrix and AccumulateMatrix are for mismatched transforms + _ => false, + } +} + +/// <https://drafts.csswg.org/css-transforms/#interpolation-of-transforms> +impl Animate for ComputedTransform { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use std::borrow::Cow; + + // Addition for transforms simply means appending to the list of + // transform functions. This is different to how we handle the other + // animation procedures so we treat it separately here rather than + // handling it in TransformOperation. + if procedure == Procedure::Add { + let result = self.0.iter().chain(&*other.0).cloned().collect(); + return Ok(Transform(result)); + } + + let this = Cow::Borrowed(&self.0); + let other = Cow::Borrowed(&other.0); + + // Interpolate the common prefix + let mut result = this + .iter() + .zip(other.iter()) + .take_while(|(this, other)| is_matched_operation(this, other)) + .map(|(this, other)| this.animate(other, procedure)) + .collect::<Result<Vec<_>, _>>()?; + + // Deal with the remainders + let this_remainder = if this.len() > result.len() { + Some(&this[result.len()..]) + } else { + None + }; + let other_remainder = if other.len() > result.len() { + Some(&other[result.len()..]) + } else { + None + }; + + match (this_remainder, other_remainder) { + // If there is a remainder from *both* lists we must have had mismatched functions. + // => Add the remainders to a suitable ___Matrix function. + (Some(this_remainder), Some(other_remainder)) => { + result.push(TransformOperation::animate_mismatched_transforms( + this_remainder, + other_remainder, + procedure, + )?); + }, + // If there is a remainder from just one list, then one list must be shorter but + // completely match the type of the corresponding functions in the longer list. + // => Interpolate the remainder with identity transforms. + (Some(remainder), None) | (None, Some(remainder)) => { + let fill_right = this_remainder.is_some(); + result.append( + &mut remainder + .iter() + .map(|transform| { + let identity = transform.to_animated_zero().unwrap(); + + match transform { + TransformOperation::AccumulateMatrix { .. } | + TransformOperation::InterpolateMatrix { .. } => { + let (from, to) = if fill_right { + (transform, &identity) + } else { + (&identity, transform) + }; + + TransformOperation::animate_mismatched_transforms( + &[from.clone()], + &[to.clone()], + procedure, + ) + }, + _ => { + let (lhs, rhs) = if fill_right { + (transform, &identity) + } else { + (&identity, transform) + }; + lhs.animate(rhs, procedure) + }, + } + }) + .collect::<Result<Vec<_>, _>>()?, + ); + }, + (None, None) => {}, + } + + Ok(Transform(result.into())) + } +} + +impl ComputeSquaredDistance for ComputedTransform { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let squared_dist = super::lists::with_zero::squared_distance(&self.0, &other.0); + + // Roll back to matrix interpolation if there is any Err(()) in the + // transform lists, such as mismatched transform functions. + // + // FIXME: Using a zero size here seems a bit sketchy but matches the + // previous behavior. + if squared_dist.is_err() { + let rect = euclid::Rect::zero(); + let matrix1: Matrix3D = self.to_transform_3d_matrix(Some(&rect))?.0.into(); + let matrix2: Matrix3D = other.to_transform_3d_matrix(Some(&rect))?.0.into(); + return matrix1.compute_squared_distance(&matrix2); + } + + squared_dist + } +} + +/// <http://dev.w3.org/csswg/css-transforms/#interpolation-of-transforms> +impl Animate for ComputedTransformOperation { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => { + Ok(TransformOperation::Matrix3D( + this.animate(other, procedure)?, + )) + }, + (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => { + Ok(TransformOperation::Matrix(this.animate(other, procedure)?)) + }, + ( + &TransformOperation::Skew(ref fx, ref fy), + &TransformOperation::Skew(ref tx, ref ty), + ) => Ok(TransformOperation::Skew( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + )), + (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) => { + Ok(TransformOperation::SkewX(f.animate(t, procedure)?)) + }, + (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => { + Ok(TransformOperation::SkewY(f.animate(t, procedure)?)) + }, + ( + &TransformOperation::Translate3D(ref fx, ref fy, ref fz), + &TransformOperation::Translate3D(ref tx, ref ty, ref tz), + ) => Ok(TransformOperation::Translate3D( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + fz.animate(tz, procedure)?, + )), + ( + &TransformOperation::Translate(ref fx, ref fy), + &TransformOperation::Translate(ref tx, ref ty), + ) => Ok(TransformOperation::Translate( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + )), + (&TransformOperation::TranslateX(ref f), &TransformOperation::TranslateX(ref t)) => { + Ok(TransformOperation::TranslateX(f.animate(t, procedure)?)) + }, + (&TransformOperation::TranslateY(ref f), &TransformOperation::TranslateY(ref t)) => { + Ok(TransformOperation::TranslateY(f.animate(t, procedure)?)) + }, + (&TransformOperation::TranslateZ(ref f), &TransformOperation::TranslateZ(ref t)) => { + Ok(TransformOperation::TranslateZ(f.animate(t, procedure)?)) + }, + ( + &TransformOperation::Scale3D(ref fx, ref fy, ref fz), + &TransformOperation::Scale3D(ref tx, ref ty, ref tz), + ) => Ok(TransformOperation::Scale3D( + animate_multiplicative_factor(*fx, *tx, procedure)?, + animate_multiplicative_factor(*fy, *ty, procedure)?, + animate_multiplicative_factor(*fz, *tz, procedure)?, + )), + (&TransformOperation::ScaleX(ref f), &TransformOperation::ScaleX(ref t)) => Ok( + TransformOperation::ScaleX(animate_multiplicative_factor(*f, *t, procedure)?), + ), + (&TransformOperation::ScaleY(ref f), &TransformOperation::ScaleY(ref t)) => Ok( + TransformOperation::ScaleY(animate_multiplicative_factor(*f, *t, procedure)?), + ), + (&TransformOperation::ScaleZ(ref f), &TransformOperation::ScaleZ(ref t)) => Ok( + TransformOperation::ScaleZ(animate_multiplicative_factor(*f, *t, procedure)?), + ), + ( + &TransformOperation::Scale(ref fx, ref fy), + &TransformOperation::Scale(ref tx, ref ty), + ) => Ok(TransformOperation::Scale( + animate_multiplicative_factor(*fx, *tx, procedure)?, + animate_multiplicative_factor(*fy, *ty, procedure)?, + )), + ( + &TransformOperation::Rotate3D(fx, fy, fz, fa), + &TransformOperation::Rotate3D(tx, ty, tz, ta), + ) => { + let animated = Rotate::Rotate3D(fx, fy, fz, fa) + .animate(&Rotate::Rotate3D(tx, ty, tz, ta), procedure)?; + let (fx, fy, fz, fa) = ComputedRotate::resolve(&animated); + Ok(TransformOperation::Rotate3D(fx, fy, fz, fa)) + }, + (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) => { + Ok(TransformOperation::RotateX(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) => { + Ok(TransformOperation::RotateY(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) => { + Ok(TransformOperation::RotateZ(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::Rotate(fa), &TransformOperation::RotateZ(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateZ(fa), &TransformOperation::Rotate(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + ( + &TransformOperation::Perspective(ref fd), + &TransformOperation::Perspective(ref td), + ) => { + use crate::values::computed::CSSPixelLength; + use crate::values::generics::transform::create_perspective_matrix; + + // From https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions: + // + // The transform functions matrix(), matrix3d() and + // perspective() get converted into 4x4 matrices first and + // interpolated as defined in section Interpolation of + // Matrices afterwards. + // + let from = create_perspective_matrix(fd.infinity_or(|l| l.px())); + let to = create_perspective_matrix(td.infinity_or(|l| l.px())); + + let interpolated = Matrix3D::from(from).animate(&Matrix3D::from(to), procedure)?; + + let decomposed = decompose_3d_matrix(interpolated)?; + let perspective_z = decomposed.perspective.2; + // Clamp results outside of the -1 to 0 range so that we get perspective + // function values between 1 and infinity. + let used_value = if perspective_z >= 0. { + transform::PerspectiveFunction::None + } else { + transform::PerspectiveFunction::Length(CSSPixelLength::new( + if perspective_z <= -1. { + 1. + } else { + -1. / perspective_z + }, + )) + }; + Ok(TransformOperation::Perspective(used_value)) + }, + _ if self.is_translate() && other.is_translate() => self + .to_translate_3d() + .animate(&other.to_translate_3d(), procedure), + _ if self.is_scale() && other.is_scale() => { + self.to_scale_3d().animate(&other.to_scale_3d(), procedure) + }, + _ if self.is_rotate() && other.is_rotate() => self + .to_rotate_3d() + .animate(&other.to_rotate_3d(), procedure), + _ => Err(()), + } + } +} + +impl ComputedTransformOperation { + /// If there are no size dependencies, we try to animate in-place, to avoid + /// creating deeply nested Interpolate* operations. + fn try_animate_mismatched_transforms_in_place( + left: &[Self], + right: &[Self], + procedure: Procedure, + ) -> Result<Self, ()> { + let (left, _left_3d) = Transform::components_to_transform_3d_matrix(left, None)?; + let (right, _right_3d) = Transform::components_to_transform_3d_matrix(right, None)?; + Ok(Self::Matrix3D( + Matrix3D::from(left).animate(&Matrix3D::from(right), procedure)?, + )) + } + + fn animate_mismatched_transforms( + left: &[Self], + right: &[Self], + procedure: Procedure, + ) -> Result<Self, ()> { + if let Ok(op) = Self::try_animate_mismatched_transforms_in_place(left, right, procedure) { + return Ok(op); + } + let from_list = Transform(left.to_vec().into()); + let to_list = Transform(right.to_vec().into()); + Ok(match procedure { + Procedure::Add => { + debug_assert!(false, "Addition should've been handled earlier"); + return Err(()); + }, + Procedure::Interpolate { progress } => Self::InterpolateMatrix { + from_list, + to_list, + progress: Percentage(progress as f32), + }, + Procedure::Accumulate { count } => Self::AccumulateMatrix { + from_list, + to_list, + count: cmp::min(count, i32::max_value() as u64) as i32, + }, + }) + } +} + +// This might not be the most useful definition of distance. It might be better, for example, +// to trace the distance travelled by a point as its transform is interpolated between the two +// lists. That, however, proves to be quite complicated so we take a simple approach for now. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1318591#c0. +impl ComputeSquaredDistance for ComputedTransformOperation { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + match (self, other) { + (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => { + this.compute_squared_distance(other) + }, + (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => { + let this: Matrix3D = (*this).into(); + let other: Matrix3D = (*other).into(); + this.compute_squared_distance(&other) + }, + ( + &TransformOperation::Skew(ref fx, ref fy), + &TransformOperation::Skew(ref tx, ref ty), + ) => Ok(fx.compute_squared_distance(&tx)? + fy.compute_squared_distance(&ty)?), + (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) | + (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => { + f.compute_squared_distance(&t) + }, + ( + &TransformOperation::Translate3D(ref fx, ref fy, ref fz), + &TransformOperation::Translate3D(ref tx, ref ty, ref tz), + ) => { + // For translate, We don't want to require doing layout in order + // to calculate the result, so drop the percentage part. + // + // However, dropping percentage makes us impossible to compute + // the distance for the percentage-percentage case, but Gecko + // uses the same formula, so it's fine for now. + let basis = Length::new(0.); + let fx = fx.resolve(basis).px(); + let fy = fy.resolve(basis).px(); + let tx = tx.resolve(basis).px(); + let ty = ty.resolve(basis).px(); + + Ok(fx.compute_squared_distance(&tx)? + + fy.compute_squared_distance(&ty)? + + fz.compute_squared_distance(&tz)?) + }, + ( + &TransformOperation::Scale3D(ref fx, ref fy, ref fz), + &TransformOperation::Scale3D(ref tx, ref ty, ref tz), + ) => Ok(fx.compute_squared_distance(&tx)? + + fy.compute_squared_distance(&ty)? + + fz.compute_squared_distance(&tz)?), + ( + &TransformOperation::Rotate3D(fx, fy, fz, fa), + &TransformOperation::Rotate3D(tx, ty, tz, ta), + ) => Rotate::Rotate3D(fx, fy, fz, fa) + .compute_squared_distance(&Rotate::Rotate3D(tx, ty, tz, ta)), + (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) | + (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) | + (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) | + (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => { + fa.compute_squared_distance(&ta) + }, + ( + &TransformOperation::Perspective(ref fd), + &TransformOperation::Perspective(ref td), + ) => fd + .infinity_or(|l| l.px()) + .compute_squared_distance(&td.infinity_or(|l| l.px())), + (&TransformOperation::Perspective(ref p), &TransformOperation::Matrix3D(ref m)) | + (&TransformOperation::Matrix3D(ref m), &TransformOperation::Perspective(ref p)) => { + // FIXME(emilio): Is this right? Why interpolating this with + // Perspective but not with anything else? + let mut p_matrix = Matrix3D::identity(); + let p = p.infinity_or(|p| p.px()); + if p >= 0. { + p_matrix.m34 = -1. / p.max(1.); + } + p_matrix.compute_squared_distance(&m) + }, + // Gecko cross-interpolates amongst all translate and all scale + // functions (See ToPrimitive in layout/style/StyleAnimationValue.cpp) + // without falling back to InterpolateMatrix + _ if self.is_translate() && other.is_translate() => self + .to_translate_3d() + .compute_squared_distance(&other.to_translate_3d()), + _ if self.is_scale() && other.is_scale() => self + .to_scale_3d() + .compute_squared_distance(&other.to_scale_3d()), + _ if self.is_rotate() && other.is_rotate() => self + .to_rotate_3d() + .compute_squared_distance(&other.to_rotate_3d()), + _ => Err(()), + } + } +} + +// ------------------------------------ +// Individual transforms. +// ------------------------------------ +/// <https://drafts.csswg.org/css-transforms-2/#propdef-rotate> +impl ComputedRotate { + fn resolve(&self) -> (Number, Number, Number, Angle) { + // According to the spec: + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + // + // If the axis is unspecified, it defaults to "0 0 1" + match *self { + Rotate::None => (0., 0., 1., Angle::zero()), + Rotate::Rotate3D(rx, ry, rz, angle) => (rx, ry, rz, angle), + Rotate::Rotate(angle) => (0., 0., 1., angle), + } + } +} + +impl Animate for ComputedRotate { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use euclid::approxeq::ApproxEq; + match (self, other) { + (&Rotate::None, &Rotate::None) => Ok(Rotate::None), + (&Rotate::Rotate3D(fx, fy, fz, fa), &Rotate::None) => { + // We always normalize direction vector for rotate3d() first, so we should also + // apply the same rule for rotate property. In other words, we promote none into + // a 3d rotate, and normalize both direction vector first, and then do + // interpolation. + let (fx, fy, fz, fa) = transform::get_normalized_vector_and_angle(fx, fy, fz, fa); + Ok(Rotate::Rotate3D( + fx, + fy, + fz, + fa.animate(&Angle::zero(), procedure)?, + )) + }, + (&Rotate::None, &Rotate::Rotate3D(tx, ty, tz, ta)) => { + // Normalize direction vector first. + let (tx, ty, tz, ta) = transform::get_normalized_vector_and_angle(tx, ty, tz, ta); + Ok(Rotate::Rotate3D( + tx, + ty, + tz, + Angle::zero().animate(&ta, procedure)?, + )) + }, + (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => { + // https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions + + let (from, to) = (self.resolve(), other.resolve()); + // For interpolations with the primitive rotate3d(), the direction vectors of the + // transform functions get normalized first. + let (fx, fy, fz, fa) = + transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); + let (tx, ty, tz, ta) = + transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3); + + // The rotation angle gets interpolated numerically and the rotation vector of the + // non-zero angle is used or (0, 0, 1) if both angles are zero. + // + // Note: the normalization may get two different vectors because of the + // floating-point precision, so we have to use approx_eq to compare two + // vectors. + let fv = DirectionVector::new(fx, fy, fz); + let tv = DirectionVector::new(tx, ty, tz); + if fa.is_zero() || ta.is_zero() || fv.approx_eq(&tv) { + let (x, y, z) = if fa.is_zero() && ta.is_zero() { + (0., 0., 1.) + } else if fa.is_zero() { + (tx, ty, tz) + } else { + // ta.is_zero() or both vectors are equal. + (fx, fy, fz) + }; + return Ok(Rotate::Rotate3D(x, y, z, fa.animate(&ta, procedure)?)); + } + + // Slerp algorithm doesn't work well for Procedure::Add, which makes both + // |this_weight| and |other_weight| be 1.0, and this may make the cosine value of + // the angle be out of the range (i.e. the 4th component of the quaternion vector). + // (See Quaternion::animate() for more details about the Slerp formula.) + // Therefore, if the cosine value is out of range, we get an NaN after applying + // acos() on it, and so the result is invalid. + // Note: This is specialized for `rotate` property. The addition of `transform` + // property has been handled in `ComputedTransform::animate()` by merging two list + // directly. + let rq = if procedure == Procedure::Add { + // In Transform::animate(), it converts two rotations into transform matrices, + // and do matrix multiplication. This match the spec definition for the + // addition. + // https://drafts.csswg.org/css-transforms-2/#combining-transform-lists + let f = ComputedTransformOperation::Rotate3D(fx, fy, fz, fa); + let t = ComputedTransformOperation::Rotate3D(tx, ty, tz, ta); + let v = + Transform(vec![f].into()).animate(&Transform(vec![t].into()), procedure)?; + let (m, _) = v.to_transform_3d_matrix(None)?; + // Decompose the matrix and retrive the quaternion vector. + decompose_3d_matrix(Matrix3D::from(m))?.quaternion + } else { + // If the normalized vectors are not equal and both rotation angles are + // non-zero the transform functions get converted into 4x4 matrices first and + // interpolated as defined in section Interpolation of Matrices afterwards. + // However, per the spec issue [1], we prefer to converting the rotate3D into + // quaternion vectors directly, and then apply Slerp algorithm. + // + // Both ways should be identical, and converting rotate3D into quaternion + // vectors directly can avoid redundant math operations, e.g. the generation of + // the equivalent matrix3D and the unnecessary decomposition parts of + // translation, scale, skew, and persepctive in the matrix3D. + // + // [1] https://github.com/w3c/csswg-drafts/issues/9278 + let fq = Quaternion::from_direction_and_angle(&fv, fa.radians64()); + let tq = Quaternion::from_direction_and_angle(&tv, ta.radians64()); + Quaternion::animate(&fq, &tq, procedure)? + }; + + debug_assert!(rq.3 <= 1.0 && rq.3 >= -1.0, "Invalid cosine value"); + let (x, y, z, angle) = transform::get_normalized_vector_and_angle( + rq.0 as f32, + rq.1 as f32, + rq.2 as f32, + rq.3.acos() as f32 * 2.0, + ); + + Ok(Rotate::Rotate3D(x, y, z, Angle::from_radians(angle))) + }, + (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => { + // If this is a 2D rotation, we just animate the <angle> + let (from, to) = (self.resolve().3, other.resolve().3); + Ok(Rotate::Rotate(from.animate(&to, procedure)?)) + }, + } + } +} + +impl ComputeSquaredDistance for ComputedRotate { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + use euclid::approxeq::ApproxEq; + match (self, other) { + (&Rotate::None, &Rotate::None) => Ok(SquaredDistance::from_sqrt(0.)), + (&Rotate::Rotate3D(_, _, _, a), &Rotate::None) | + (&Rotate::None, &Rotate::Rotate3D(_, _, _, a)) => { + a.compute_squared_distance(&Angle::zero()) + }, + (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + let (mut fx, mut fy, mut fz, angle1) = + transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); + let (mut tx, mut ty, mut tz, angle2) = + transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3); + + if angle1.is_zero() && angle2.is_zero() { + (fx, fy, fz) = (0., 0., 1.); + (tx, ty, tz) = (0., 0., 1.); + } else if angle1.is_zero() { + (fx, fy, fz) = (tx, ty, tz); + } else if angle2.is_zero() { + (tx, ty, tz) = (fx, fy, fz); + } + + let v1 = DirectionVector::new(fx, fy, fz); + let v2 = DirectionVector::new(tx, ty, tz); + if v1.approx_eq(&v2) { + angle1.compute_squared_distance(&angle2) + } else { + let q1 = Quaternion::from_direction_and_angle(&v1, angle1.radians64()); + let q2 = Quaternion::from_direction_and_angle(&v2, angle2.radians64()); + q1.compute_squared_distance(&q2) + } + }, + (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => self + .resolve() + .3 + .compute_squared_distance(&other.resolve().3), + } + } +} + +/// <https://drafts.csswg.org/css-transforms-2/#propdef-translate> +impl ComputedTranslate { + fn resolve(&self) -> (LengthPercentage, LengthPercentage, Length) { + // According to the spec: + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + // + // Unspecified translations default to 0px + match *self { + Translate::None => ( + LengthPercentage::zero(), + LengthPercentage::zero(), + Length::zero(), + ), + Translate::Translate(ref tx, ref ty, ref tz) => (tx.clone(), ty.clone(), tz.clone()), + } + } +} + +impl Animate for ComputedTranslate { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&Translate::None, &Translate::None) => Ok(Translate::None), + (&Translate::Translate(_, ..), _) | (_, &Translate::Translate(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + Ok(Translate::Translate( + from.0.animate(&to.0, procedure)?, + from.1.animate(&to.1, procedure)?, + from.2.animate(&to.2, procedure)?, + )) + }, + } + } +} + +impl ComputeSquaredDistance for ComputedTranslate { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let (from, to) = (self.resolve(), other.resolve()); + Ok(from.0.compute_squared_distance(&to.0)? + + from.1.compute_squared_distance(&to.1)? + + from.2.compute_squared_distance(&to.2)?) + } +} + +/// <https://drafts.csswg.org/css-transforms-2/#propdef-scale> +impl ComputedScale { + fn resolve(&self) -> (Number, Number, Number) { + // According to the spec: + // https://drafts.csswg.org/css-transforms-2/#individual-transforms + // + // Unspecified scales default to 1 + match *self { + Scale::None => (1.0, 1.0, 1.0), + Scale::Scale(sx, sy, sz) => (sx, sy, sz), + } + } +} + +impl Animate for ComputedScale { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + match (self, other) { + (&Scale::None, &Scale::None) => Ok(Scale::None), + (&Scale::Scale(_, ..), _) | (_, &Scale::Scale(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + // For transform lists, we add by appending to the list of + // transform functions. However, ComputedScale cannot be + // simply concatenated, so we have to calculate the additive + // result here. + if procedure == Procedure::Add { + // scale(x1,y1,z1)*scale(x2,y2,z2) = scale(x1*x2, y1*y2, z1*z2) + return Ok(Scale::Scale(from.0 * to.0, from.1 * to.1, from.2 * to.2)); + } + Ok(Scale::Scale( + animate_multiplicative_factor(from.0, to.0, procedure)?, + animate_multiplicative_factor(from.1, to.1, procedure)?, + animate_multiplicative_factor(from.2, to.2, procedure)?, + )) + }, + } + } +} + +impl ComputeSquaredDistance for ComputedScale { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let (from, to) = (self.resolve(), other.resolve()); + Ok(from.0.compute_squared_distance(&to.0)? + + from.1.compute_squared_distance(&to.1)? + + from.2.compute_squared_distance(&to.2)?) + } +} diff --git a/servo/components/style/values/computed/align.rs b/servo/components/style/values/computed/align.rs new file mode 100644 index 0000000000..94847fd80f --- /dev/null +++ b/servo/components/style/values/computed/align.rs @@ -0,0 +1,91 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Values for CSS Box Alignment properties +//! +//! https://drafts.csswg.org/css-align/ + +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified; + +pub use super::specified::{ + AlignContent, AlignItems, AlignTracks, ContentDistribution, JustifyContent, JustifyTracks, + SelfAlignment, +}; +pub use super::specified::{AlignSelf, JustifySelf}; + +/// The computed value for the `justify-items` property. +/// +/// Need to carry around both the specified and computed value to handle the +/// special legacy keyword without destroying style sharing. +/// +/// In particular, `justify-items` is a reset property, so we ought to be able +/// to share its computed representation across elements as long as they match +/// the same rules. Except that it's not true if the specified value for +/// `justify-items` is `legacy` and the computed value of the parent has the +/// `legacy` modifier. +/// +/// So instead of computing `legacy` "normally" looking at get_parent_position(), +/// marking it as uncacheable, we carry the specified value around and handle +/// the special case in `StyleAdjuster` instead, only when the result of the +/// computation would vary. +/// +/// Note that we also need to special-case this property in matching.rs, in +/// order to properly handle changes to the legacy keyword... This all kinda +/// sucks :(. +/// +/// See the discussion in https://bugzil.la/1384542. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)] +#[repr(C)] +pub struct ComputedJustifyItems { + /// The specified value for the property. Can contain the bare `legacy` + /// keyword. + #[css(skip)] + pub specified: specified::JustifyItems, + /// The computed value for the property. Cannot contain the bare `legacy` + /// keyword, but note that it could contain it in combination with other + /// keywords like `left`, `right` or `center`. + pub computed: specified::JustifyItems, +} + +pub use self::ComputedJustifyItems as JustifyItems; + +impl JustifyItems { + /// Returns the `legacy` value. + pub fn legacy() -> Self { + Self { + specified: specified::JustifyItems::legacy(), + computed: specified::JustifyItems::normal(), + } + } +} + +impl ToComputedValue for specified::JustifyItems { + type ComputedValue = JustifyItems; + + /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy> + fn to_computed_value(&self, _context: &Context) -> JustifyItems { + use crate::values::specified::align; + let specified = *self; + let computed = if self.0 != align::AlignFlags::LEGACY { + *self + } else { + // If the inherited value of `justify-items` includes the + // `legacy` keyword, `legacy` computes to the inherited value, but + // we assume it computes to `normal`, and handle that special-case + // in StyleAdjuster. + Self::normal() + }; + + JustifyItems { + specified, + computed, + } + } + + #[inline] + fn from_computed_value(computed: &JustifyItems) -> Self { + computed.specified + } +} diff --git a/servo/components/style/values/computed/angle.rs b/servo/components/style/values/computed/angle.rs new file mode 100644 index 0000000000..ea321d2233 --- /dev/null +++ b/servo/components/style/values/computed/angle.rs @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed angles. + +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::CSSFloat; +use crate::Zero; +use std::f64::consts::PI; +use std::fmt::{self, Write}; +use std::{f32, f64}; +use style_traits::{CssWriter, ToCss}; + +/// A computed angle in degrees. +#[derive( + Add, + Animate, + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + PartialOrd, + Serialize, + ToAnimatedZero, + ToResolvedValue, +)] +#[repr(C)] +pub struct Angle(CSSFloat); + +impl ToCss for Angle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.degrees().to_css(dest)?; + dest.write_str("deg") + } +} + +const RAD_PER_DEG: f64 = PI / 180.0; + +impl Angle { + /// Creates a computed `Angle` value from a radian amount. + pub fn from_radians(radians: CSSFloat) -> Self { + Angle(radians / RAD_PER_DEG as f32) + } + + /// Creates a computed `Angle` value from a degrees amount. + #[inline] + pub fn from_degrees(degrees: CSSFloat) -> Self { + Angle(degrees) + } + + /// Returns the amount of radians this angle represents. + #[inline] + pub fn radians(&self) -> CSSFloat { + self.radians64().min(f32::MAX as f64).max(f32::MIN as f64) as f32 + } + + /// Returns the amount of radians this angle represents as a `f64`. + /// + /// Gecko stores angles as singles, but does this computation using doubles. + /// + /// This is significant enough to mess up rounding to the nearest + /// quarter-turn for 225 degrees, for example. + #[inline] + pub fn radians64(&self) -> f64 { + self.0 as f64 * RAD_PER_DEG + } + + /// Return the value in degrees. + #[inline] + pub fn degrees(&self) -> CSSFloat { + self.0 + } +} + +impl Zero for Angle { + #[inline] + fn zero() -> Self { + Angle(0.0) + } + + #[inline] + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl ComputeSquaredDistance for Angle { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + // Use the formula for calculating the distance between angles defined in SVG: + // https://www.w3.org/TR/SVG/animate.html#complexDistances + self.radians64() + .compute_squared_distance(&other.radians64()) + } +} diff --git a/servo/components/style/values/computed/animation.rs b/servo/components/style/values/computed/animation.rs new file mode 100644 index 0000000000..626dbe5347 --- /dev/null +++ b/servo/components/style/values/computed/animation.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for properties related to animations and transitions + +use crate::values::computed::{Context, LengthPercentage, ToComputedValue}; +use crate::values::generics::animation as generics; +use crate::values::specified::animation as specified; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::animation::{ + AnimationName, ScrollAxis, ScrollTimelineName, TransitionProperty, AnimationComposition, + AnimationDirection, AnimationFillMode, AnimationPlayState, +}; + +/// A computed value for the `animation-iteration-count` property. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)] +#[repr(C)] +pub struct AnimationIterationCount(pub f32); + +impl ToComputedValue for specified::AnimationIterationCount { + type ComputedValue = AnimationIterationCount; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + AnimationIterationCount(match *self { + specified::AnimationIterationCount::Number(n) => n.to_computed_value(context).0, + specified::AnimationIterationCount::Infinite => f32::INFINITY, + }) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + use crate::values::specified::NonNegativeNumber; + if computed.0.is_infinite() { + specified::AnimationIterationCount::Infinite + } else { + specified::AnimationIterationCount::Number(NonNegativeNumber::new(computed.0)) + } + } +} + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + Self(1.0) + } +} + +impl ToCss for AnimationIterationCount { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.0.is_infinite() { + dest.write_str("infinite") + } else { + self.0.to_css(dest) + } + } +} + +/// A computed value for the `animation-timeline` property. +pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>; + +/// A computed value for the `view-timeline-inset` property. +pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>; diff --git a/servo/components/style/values/computed/background.rs b/servo/components/style/values/computed/background.rs new file mode 100644 index 0000000000..e2a58f8b74 --- /dev/null +++ b/servo/components/style/values/computed/background.rs @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to backgrounds. + +use crate::values::computed::length::NonNegativeLengthPercentage; +use crate::values::generics::background::BackgroundSize as GenericBackgroundSize; + +pub use crate::values::specified::background::BackgroundRepeat; + +/// A computed value for the `background-size` property. +pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>; diff --git a/servo/components/style/values/computed/basic_shape.rs b/servo/components/style/values/computed/basic_shape.rs new file mode 100644 index 0000000000..d39110ec1c --- /dev/null +++ b/servo/components/style/values/computed/basic_shape.rs @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [`basic-shape`][basic-shape]s +//! +//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape + +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Image, LengthPercentage, NonNegativeLengthPercentage, Position}; +use crate::values::generics::basic_shape as generic; + +/// A computed alias for FillRule. +pub use crate::values::generics::basic_shape::FillRule; + +/// A computed `clip-path` value. +pub type ClipPath = generic::GenericClipPath<BasicShape, ComputedUrl>; + +/// A computed `shape-outside` value. +pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>; + +/// A computed basic shape. +pub type BasicShape = + generic::GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, InsetRect>; + +/// The computed value of `inset()`. +pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>; + +/// A computed circle. +pub type Circle = generic::Circle<Position, NonNegativeLengthPercentage>; + +/// A computed ellipse. +pub type Ellipse = generic::Ellipse<Position, NonNegativeLengthPercentage>; + +/// The computed value of `ShapeRadius`. +pub type ShapeRadius = generic::GenericShapeRadius<NonNegativeLengthPercentage>; diff --git a/servo/components/style/values/computed/border.rs b/servo/components/style/values/computed/border.rs new file mode 100644 index 0000000000..e073f671b3 --- /dev/null +++ b/servo/components/style/values/computed/border.rs @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to borders. + +use crate::values::computed::length::{NonNegativeLength, NonNegativeLengthPercentage}; +use crate::values::computed::{NonNegativeNumber, NonNegativeNumberOrPercentage}; +use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius; +use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice; +use crate::values::generics::border::BorderRadius as GenericBorderRadius; +use crate::values::generics::border::BorderSpacing as GenericBorderSpacing; +use crate::values::generics::border::GenericBorderImageSideWidth; +use crate::values::generics::rect::Rect; +use crate::values::generics::size::Size2D; +use crate::values::generics::NonNegative; +use crate::Zero; +use app_units::Au; + +pub use crate::values::specified::border::BorderImageRepeat; + +/// A computed value for -webkit-text-stroke-width. +pub type LineWidth = Au; + +/// A computed value for border-width (and the like). +pub type BorderSideWidth = Au; + +/// A computed value for the `border-image-width` property. +pub type BorderImageWidth = Rect<BorderImageSideWidth>; + +/// A computed value for a single side of a `border-image-width` property. +pub type BorderImageSideWidth = + GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>; + +/// A computed value for the `border-image-slice` property. +pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>; + +/// A computed value for the `border-radius` property. +pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>; + +/// A computed value for the `border-*-radius` longhand properties. +pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>; + +/// A computed value for the `border-spacing` longhand property. +pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>; + +impl BorderImageSideWidth { + /// Returns `1`. + #[inline] + pub fn one() -> Self { + GenericBorderImageSideWidth::Number(NonNegative(1.)) + } +} + +impl BorderImageSlice { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + GenericBorderImageSlice { + offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()), + fill: false, + } + } +} + +impl BorderSpacing { + /// Returns `0 0`. + pub fn zero() -> Self { + GenericBorderSpacing(Size2D::new( + NonNegativeLength::zero(), + NonNegativeLength::zero(), + )) + } + + /// Returns the horizontal spacing. + pub fn horizontal(&self) -> Au { + Au::from(*self.0.width()) + } + + /// Returns the vertical spacing. + pub fn vertical(&self) -> Au { + Au::from(*self.0.height()) + } +} diff --git a/servo/components/style/values/computed/box.rs b/servo/components/style/values/computed/box.rs new file mode 100644 index 0000000000..62811d9851 --- /dev/null +++ b/servo/components/style/values/computed/box.rs @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for box properties. + +use crate::values::animated::{Animate, Procedure, ToAnimatedValue}; +use crate::values::computed::font::FixedPoint; +use crate::values::computed::length::{LengthPercentage, NonNegativeLength}; +use crate::values::computed::{Context, Integer, Number, ToComputedValue}; +use crate::values::generics::box_::{ + GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign, +}; +use crate::values::specified::box_ as specified; +use std::fmt; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::box_::{ + Appearance, BaselineSource, BreakBetween, BreakWithin, Clear as SpecifiedClear, Contain, + ContainerName, ContainerType, ContentVisibility, Display, Float as SpecifiedFloat, Overflow, + OverflowAnchor, OverflowClipBox, OverscrollBehavior, ScrollSnapAlign, ScrollSnapAxis, + ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, WillChange, +}; + +/// A computed value for the `vertical-align` property. +pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>; + +/// A computed value for the `contain-intrinsic-size` property. +pub type ContainIntrinsicSize = GenericContainIntrinsicSize<NonNegativeLength>; + +impl ContainIntrinsicSize { + /// Converts contain-intrinsic-size to auto style. + pub fn add_auto_if_needed(&self) -> Option<Self> { + Some(match *self { + Self::None => Self::AutoNone, + Self::Length(ref l) => Self::AutoLength(*l), + Self::AutoNone | Self::AutoLength(..) => return None, + }) + } +} + +/// A computed value for the `line-clamp` property. +pub type LineClamp = GenericLineClamp<Integer>; + +impl Animate for LineClamp { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.is_none() != other.is_none() { + return Err(()); + } + if self.is_none() { + return Ok(Self::none()); + } + Ok(Self(self.0.animate(&other.0, procedure)?.max(1))) + } +} + +/// A computed value for the `perspective` property. +pub type Perspective = GenericPerspective<NonNegativeLength>; + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToResolvedValue, +)] +#[repr(u8)] +/// A computed value for the `float` property. +pub enum Float { + Left, + Right, + None, +} + +impl Float { + /// Returns true if `self` is not `None`. + pub fn is_floating(self) -> bool { + self != Self::None + } +} + +impl ToComputedValue for SpecifiedFloat { + type ComputedValue = Float; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let ltr = context.style().writing_mode.is_bidi_ltr(); + // https://drafts.csswg.org/css-logical-props/#float-clear + match *self { + SpecifiedFloat::InlineStart => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Float::Left + } else { + Float::Right + } + }, + SpecifiedFloat::InlineEnd => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Float::Right + } else { + Float::Left + } + }, + SpecifiedFloat::Left => Float::Left, + SpecifiedFloat::Right => Float::Right, + SpecifiedFloat::None => Float::None, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedFloat { + match *computed { + Float::Left => SpecifiedFloat::Left, + Float::Right => SpecifiedFloat::Right, + Float::None => SpecifiedFloat::None, + } + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToResolvedValue, +)] +/// A computed value for the `clear` property. +#[repr(u8)] +pub enum Clear { + None, + Left, + Right, + Both, +} + +impl ToComputedValue for SpecifiedClear { + type ComputedValue = Clear; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let ltr = context.style().writing_mode.is_bidi_ltr(); + // https://drafts.csswg.org/css-logical-props/#float-clear + match *self { + SpecifiedClear::InlineStart => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Clear::Left + } else { + Clear::Right + } + }, + SpecifiedClear::InlineEnd => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Clear::Right + } else { + Clear::Left + } + }, + SpecifiedClear::None => Clear::None, + SpecifiedClear::Left => Clear::Left, + SpecifiedClear::Right => Clear::Right, + SpecifiedClear::Both => Clear::Both, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedClear { + match *computed { + Clear::None => SpecifiedClear::None, + Clear::Left => SpecifiedClear::Left, + Clear::Right => SpecifiedClear::Right, + Clear::Both => SpecifiedClear::Both, + } + } +} + +/// A computed value for the `resize` property. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, ToCss, ToResolvedValue)] +#[repr(u8)] +pub enum Resize { + None, + Both, + Horizontal, + Vertical, +} + +impl ToComputedValue for specified::Resize { + type ComputedValue = Resize; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Resize { + let is_vertical = context.style().writing_mode.is_vertical(); + match self { + specified::Resize::Inline => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if is_vertical { + Resize::Vertical + } else { + Resize::Horizontal + } + }, + specified::Resize::Block => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if is_vertical { + Resize::Horizontal + } else { + Resize::Vertical + } + }, + specified::Resize::None => Resize::None, + specified::Resize::Both => Resize::Both, + specified::Resize::Horizontal => Resize::Horizontal, + specified::Resize::Vertical => Resize::Vertical, + } + } + + #[inline] + fn from_computed_value(computed: &Resize) -> specified::Resize { + match computed { + Resize::None => specified::Resize::None, + Resize::Both => specified::Resize::Both, + Resize::Horizontal => specified::Resize::Horizontal, + Resize::Vertical => specified::Resize::Vertical, + } + } +} + +/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375). +pub const ZOOM_FRACTION_BITS: u16 = 6; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference workaround. +pub type ZoomFixedPoint = FixedPoint<u16, ZOOM_FRACTION_BITS>; + +/// The computed `zoom` property value. We store it as a 16-bit fixed point because we need to +/// store it efficiently in the ComputedStyle representation. The assumption being that zooms over +/// 1000 aren't quite useful. +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(C)] +pub struct Zoom(ZoomFixedPoint); + +impl ToComputedValue for specified::Zoom { + type ComputedValue = Zoom; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + let n = match *self { + Self::Normal => return Zoom::ONE, + Self::Document => return Zoom::DOCUMENT, + Self::Value(ref n) => n.0.to_number().get(), + }; + if n == 0.0 { + // For legacy reasons, zoom: 0 (and 0%) computes to 1. ¯\_(ツ)_/¯ + return Zoom::ONE; + } + Zoom(ZoomFixedPoint::from_float(n)) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::new_number(computed.value()) + } +} + +impl ToCss for Zoom { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + use std::fmt::Write; + if *self == Self::DOCUMENT { + return dest.write_str("document"); + } + self.value().to_css(dest) + } +} + +impl ToAnimatedValue for Zoom { + type AnimatedValue = Number; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.value() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Zoom(ZoomFixedPoint::from_float(animated.max(0.0))) + } +} + +impl Zoom { + /// The value 1. This is by far the most common value. + pub const ONE: Zoom = Zoom(ZoomFixedPoint { + value: 1 << ZOOM_FRACTION_BITS, + }); + + /// The `document` value. This can appear in the computed zoom property value, but not in the + /// `effective_zoom` field. + pub const DOCUMENT: Zoom = Zoom(ZoomFixedPoint { value: 0 }); + + /// Returns whether we're the number 1. + #[inline] + pub fn is_one(self) -> bool { + self == Self::ONE + } + + /// Returns the value as a float. + #[inline] + pub fn value(&self) -> f32 { + self.0.to_float() + } + + /// Computes the effective zoom for a given new zoom value in rhs. + pub fn compute_effective(self, specified: Self) -> Self { + if specified == Self::DOCUMENT { + return Self::ONE; + } + if self == Self::ONE { + return specified; + } + if specified == Self::ONE { + return self; + } + Zoom(self.0 * specified.0) + } + + /// Returns the zoomed value. + #[inline] + pub fn zoom(self, value: f32) -> f32 { + if self == Self::ONE { + return value; + } + self.value() * value + } +} diff --git a/servo/components/style/values/computed/color.rs b/servo/components/style/values/computed/color.rs new file mode 100644 index 0000000000..9b5185d923 --- /dev/null +++ b/servo/components/style/values/computed/color.rs @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed color values. + +use crate::color::parsing::Color as CSSParserColor; +use crate::color::AbsoluteColor; +use crate::values::animated::ToAnimatedZero; +use crate::values::computed::percentage::Percentage; +use crate::values::generics::color::{ + GenericCaretColor, GenericColor, GenericColorMix, GenericColorOrAuto, +}; +use std::fmt; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::color::{ColorScheme, ForcedColorAdjust, PrintColorAdjust}; + +/// The computed value of the `color` property. +pub type ColorPropertyValue = AbsoluteColor; + +/// A computed value for `<color>`. +pub type Color = GenericColor<Percentage>; + +/// A computed color-mix(). +pub type ColorMix = GenericColorMix<Color, Percentage>; + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Self::Absolute(ref c) => c.to_css(dest), + Self::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest), + Self::ColorMix(ref m) => m.to_css(dest), + } + } +} + +impl Color { + /// A fully transparent color. + pub const TRANSPARENT_BLACK: Self = Self::Absolute(AbsoluteColor::TRANSPARENT_BLACK); + + /// An opaque black color. + pub const BLACK: Self = Self::Absolute(AbsoluteColor::BLACK); + + /// An opaque white color. + pub const WHITE: Self = Self::Absolute(AbsoluteColor::WHITE); + + /// Create a new computed [`Color`] from a given color-mix, simplifying it to an absolute color + /// if possible. + pub fn from_color_mix(color_mix: ColorMix) -> Self { + if let Some(absolute) = color_mix.mix_to_absolute() { + Self::Absolute(absolute) + } else { + Self::ColorMix(Box::new(color_mix)) + } + } + + /// Combine this complex color with the given foreground color into an + /// absolute color. + pub fn resolve_to_absolute(&self, current_color: &AbsoluteColor) -> AbsoluteColor { + use crate::values::specified::percentage::ToPercentage; + + match *self { + Self::Absolute(c) => c, + Self::CurrentColor => *current_color, + Self::ColorMix(ref mix) => { + let left = mix.left.resolve_to_absolute(current_color); + let right = mix.right.resolve_to_absolute(current_color); + crate::color::mix::mix( + mix.interpolation, + &left, + mix.left_percentage.to_percentage(), + &right, + mix.right_percentage.to_percentage(), + mix.flags, + ) + }, + } + } +} + +impl ToAnimatedZero for AbsoluteColor { + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(Self::TRANSPARENT_BLACK) + } +} + +/// auto | <color> +pub type ColorOrAuto = GenericColorOrAuto<Color>; + +/// caret-color +pub type CaretColor = GenericCaretColor<Color>; diff --git a/servo/components/style/values/computed/column.rs b/servo/components/style/values/computed/column.rs new file mode 100644 index 0000000000..38437ea110 --- /dev/null +++ b/servo/components/style/values/computed/column.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for the column properties. + +use crate::values::computed::PositiveInteger; +use crate::values::generics::column::ColumnCount as GenericColumnCount; + +/// A computed type for `column-count` values. +pub type ColumnCount = GenericColumnCount<PositiveInteger>; diff --git a/servo/components/style/values/computed/counters.rs b/servo/components/style/values/computed/counters.rs new file mode 100644 index 0000000000..fd5e915c4a --- /dev/null +++ b/servo/components/style/values/computed/counters.rs @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for counter properties + +use crate::values::computed::image::Image; +use crate::values::generics::counters as generics; +use crate::values::generics::counters::CounterIncrement as GenericCounterIncrement; +use crate::values::generics::counters::CounterReset as GenericCounterReset; +use crate::values::generics::counters::CounterSet as GenericCounterSet; + +/// A computed value for the `counter-increment` property. +pub type CounterIncrement = GenericCounterIncrement<i32>; + +/// A computed value for the `counter-reset` property. +pub type CounterReset = GenericCounterReset<i32>; + +/// A computed value for the `counter-set` property. +pub type CounterSet = GenericCounterSet<i32>; + +/// A computed value for the `content` property. +pub type Content = generics::GenericContent<Image>; + +/// A computed content item. +pub type ContentItem = generics::GenericContentItem<Image>; diff --git a/servo/components/style/values/computed/easing.rs b/servo/components/style/values/computed/easing.rs new file mode 100644 index 0000000000..d351b3c71d --- /dev/null +++ b/servo/components/style/values/computed/easing.rs @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS Easing functions. + +use euclid::approxeq::ApproxEq; + +use crate::bezier::Bezier; +use crate::piecewise_linear::PiecewiseLinearFunction; +use crate::values::computed::{Integer, Number}; +use crate::values::generics::easing::{self, BeforeFlag, StepPosition, TimingKeyword}; + +/// A computed timing function. +pub type ComputedTimingFunction = easing::TimingFunction<Integer, Number, PiecewiseLinearFunction>; + +/// An alias of the computed timing function. +pub type TimingFunction = ComputedTimingFunction; + +impl ComputedTimingFunction { + fn calculate_step_output( + steps: i32, + pos: StepPosition, + progress: f64, + before_flag: BeforeFlag, + ) -> f64 { + // User specified values can cause overflow (bug 1706157). Increments/decrements + // should be gravefully handled. + let mut current_step = (progress * (steps as f64)).floor() as i32; + + // Increment current step if it is jump-start or start. + if pos == StepPosition::Start || + pos == StepPosition::JumpStart || + pos == StepPosition::JumpBoth + { + current_step = current_step.checked_add(1).unwrap_or(current_step); + } + + // If the "before flag" is set and we are at a transition point, + // drop back a step + if before_flag == BeforeFlag::Set && + (progress * steps as f64).rem_euclid(1.0).approx_eq(&0.0) + { + current_step = current_step.checked_sub(1).unwrap_or(current_step); + } + + // We should not produce a result outside [0, 1] unless we have an + // input outside that range. This takes care of steps that would otherwise + // occur at boundaries. + if progress >= 0.0 && current_step < 0 { + current_step = 0; + } + + // |jumps| should always be in [1, i32::MAX]. + let jumps = if pos == StepPosition::JumpBoth { + steps.checked_add(1).unwrap_or(steps) + } else if pos == StepPosition::JumpNone { + steps.checked_sub(1).unwrap_or(steps) + } else { + steps + }; + + if progress <= 1.0 && current_step > jumps { + current_step = jumps; + } + + (current_step as f64) / (jumps as f64) + } + + /// The output of the timing function given the progress ratio of this animation. + pub fn calculate_output(&self, progress: f64, before_flag: BeforeFlag, epsilon: f64) -> f64 { + let progress = match self { + TimingFunction::CubicBezier { x1, y1, x2, y2 } => { + Bezier::calculate_bezier_output(progress, epsilon, *x1, *y1, *x2, *y2) + }, + TimingFunction::Steps(steps, pos) => { + Self::calculate_step_output(*steps, *pos, progress, before_flag) + }, + TimingFunction::LinearFunction(function) => function.at(progress as f32).into(), + TimingFunction::Keyword(keyword) => match keyword { + TimingKeyword::Linear => progress, + TimingKeyword::Ease => { + Bezier::calculate_bezier_output(progress, epsilon, 0.25, 0.1, 0.25, 1.) + }, + TimingKeyword::EaseIn => { + Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 1., 1.) + }, + TimingKeyword::EaseOut => { + Bezier::calculate_bezier_output(progress, epsilon, 0., 0., 0.58, 1.) + }, + TimingKeyword::EaseInOut => { + Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 0.58, 1.) + }, + }, + }; + + // The output progress value of an easing function is a real number in the range: + // [-inf, inf]. + // https://drafts.csswg.org/css-easing-1/#output-progress-value + // + // However, we expect to use the finite progress for interpolation and web-animations + // https://drafts.csswg.org/css-values-4/#interpolation + // https://drafts.csswg.org/web-animations-1/#dom-computedeffecttiming-progress + // + // So we clamp the infinite progress, per the spec issue: + // https://github.com/w3c/csswg-drafts/issues/8344 + progress.min(f64::MAX).max(f64::MIN) + } +} diff --git a/servo/components/style/values/computed/effects.rs b/servo/components/style/values/computed/effects.rs new file mode 100644 index 0000000000..b0a92024ca --- /dev/null +++ b/servo/components/style/values/computed/effects.rs @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to effects. + +use crate::values::computed::color::Color; +use crate::values::computed::length::{Length, NonNegativeLength}; +#[cfg(feature = "gecko")] +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Angle, NonNegativeNumber, ZeroToOneNumber}; +use crate::values::generics::effects::BoxShadow as GenericBoxShadow; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +#[cfg(not(feature = "gecko"))] +use crate::values::Impossible; + +/// A computed value for a single shadow of the `box-shadow` property. +pub type BoxShadow = GenericBoxShadow<Color, Length, NonNegativeLength, Length>; + +/// A computed value for a single `filter`. +#[cfg(feature = "gecko")] +pub type Filter = GenericFilter< + Angle, + NonNegativeNumber, + ZeroToOneNumber, + NonNegativeLength, + SimpleShadow, + ComputedUrl, +>; + +/// A computed value for a single `filter`. +#[cfg(feature = "servo")] +pub type Filter = GenericFilter< + Angle, + NonNegativeNumber, + ZeroToOneNumber, + NonNegativeLength, + Impossible, + Impossible, +>; + +/// A computed value for the `drop-shadow()` filter. +pub type SimpleShadow = GenericSimpleShadow<Color, Length, NonNegativeLength>; diff --git a/servo/components/style/values/computed/flex.rs b/servo/components/style/values/computed/flex.rs new file mode 100644 index 0000000000..95c497ecf6 --- /dev/null +++ b/servo/components/style/values/computed/flex.rs @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to flexbox. + +use crate::values::computed::Size; +use crate::values::generics::flex::FlexBasis as GenericFlexBasis; + +/// A computed value for the `flex-basis` property. +pub type FlexBasis = GenericFlexBasis<Size>; + +impl FlexBasis { + /// `auto` + #[inline] + pub fn auto() -> Self { + GenericFlexBasis::Size(Size::auto()) + } +} diff --git a/servo/components/style/values/computed/font.rs b/servo/components/style/values/computed/font.rs new file mode 100644 index 0000000000..de0a5e372b --- /dev/null +++ b/servo/components/style/values/computed/font.rs @@ -0,0 +1,1369 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for font properties + +use crate::parser::{Parse, ParserContext}; +use crate::values::animated::ToAnimatedValue; +use crate::values::computed::{ + Angle, Context, Integer, Length, NonNegativeLength, NonNegativeNumber, Number, Percentage, + ToComputedValue, +}; +use crate::values::generics::font::{ + FeatureTagValue, FontSettings, TaggedFontValue, VariationValue, +}; +use crate::values::generics::{font as generics, NonNegative}; +use crate::values::resolved::{Context as ResolvedContext, ToResolvedValue}; +use crate::values::specified::font::{ + self as specified, KeywordInfo, MAX_FONT_WEIGHT, MIN_FONT_WEIGHT, +}; +use crate::values::specified::length::{FontBaseSize, LineHeightBase, NoCalcLength}; +use crate::Atom; +use cssparser::{serialize_identifier, CssStringWriter, Parser}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use num_traits::abs; +use num_traits::cast::AsPrimitive; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +pub use crate::values::computed::Length as MozScriptMinSize; +pub use crate::values::specified::font::MozScriptSizeMultiplier; +pub use crate::values::specified::font::{FontPalette, FontSynthesis}; +pub use crate::values::specified::font::{ + FontVariantAlternates, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric, XLang, + XTextScale, +}; +pub use crate::values::specified::Integer as SpecifiedInteger; +pub use crate::values::specified::Number as SpecifiedNumber; + +/// Generic template for font property type classes that use a fixed-point +/// internal representation with `FRACTION_BITS` for the fractional part. +/// +/// Values are constructed from and exposed as floating-point, but stored +/// internally as fixed point, so there will be a quantization effect on +/// fractional values, depending on the number of fractional bits used. +/// +/// Using (16-bit) fixed-point types rather than floats for these style +/// attributes reduces the memory footprint of gfxFontEntry and gfxFontStyle; it +/// will also tend to reduce the number of distinct font instances that get +/// created, particularly when styles are animated or set to arbitrary values +/// (e.g. by sliders in the UI), which should reduce pressure on graphics +/// resources and improve cache hit rates. +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[repr(C)] +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +pub struct FixedPoint<T, const FRACTION_BITS: u16> { + /// The actual representation. + pub value: T, +} + +impl<T, const FRACTION_BITS: u16> FixedPoint<T, FRACTION_BITS> +where + T: AsPrimitive<f32>, + f32: AsPrimitive<T>, + u16: AsPrimitive<T>, +{ + const SCALE: u16 = 1 << FRACTION_BITS; + const INVERSE_SCALE: f32 = 1.0 / Self::SCALE as f32; + + /// Returns a fixed-point bit from a floating-point context. + pub fn from_float(v: f32) -> Self { + Self { + value: (v * Self::SCALE as f32).round().as_(), + } + } + + /// Returns the floating-point representation. + pub fn to_float(&self) -> f32 { + self.value.as_() * Self::INVERSE_SCALE + } +} + +// We implement this and mul below only for u16 types, because u32 types might need more care about +// overflow. But it's not hard to implement in either case. +impl<const FRACTION_BITS: u16> std::ops::Div for FixedPoint<u16, FRACTION_BITS> { + type Output = Self; + fn div(self, rhs: Self) -> Self { + Self { + value: (((self.value as u32) << (FRACTION_BITS as u32)) / (rhs.value as u32)) as u16, + } + } +} +impl<const FRACTION_BITS: u16> std::ops::Mul for FixedPoint<u16, FRACTION_BITS> { + type Output = Self; + fn mul(self, rhs: Self) -> Self { + Self { + value: (((self.value as u32) * (rhs.value as u32)) >> (FRACTION_BITS as u32)) as u16, + } + } +} + +/// font-weight: range 1..1000, fractional values permitted; keywords +/// 'normal', 'bold' aliased to 400, 700 respectively. +/// +/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375) +pub const FONT_WEIGHT_FRACTION_BITS: u16 = 6; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference +/// workaround. +pub type FontWeightFixedPoint = FixedPoint<u16, FONT_WEIGHT_FRACTION_BITS>; + +/// A value for the font-weight property per: +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(C)] +pub struct FontWeight(FontWeightFixedPoint); +impl ToAnimatedValue for FontWeight { + type AnimatedValue = Number; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.value() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontWeight::from_float(animated) + } +} + +impl ToCss for FontWeight { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.value().to_css(dest) + } +} + +impl FontWeight { + /// The `normal` keyword. + pub const NORMAL: FontWeight = FontWeight(FontWeightFixedPoint { + value: 400 << FONT_WEIGHT_FRACTION_BITS, + }); + + /// The `bold` value. + pub const BOLD: FontWeight = FontWeight(FontWeightFixedPoint { + value: 700 << FONT_WEIGHT_FRACTION_BITS, + }); + + /// The threshold from which we consider a font bold. + pub const BOLD_THRESHOLD: FontWeight = FontWeight(FontWeightFixedPoint { + value: 600 << FONT_WEIGHT_FRACTION_BITS, + }); + + /// Returns the `normal` keyword value. + pub fn normal() -> Self { + Self::NORMAL + } + + /// Weither this weight is bold + pub fn is_bold(&self) -> bool { + *self >= Self::BOLD_THRESHOLD + } + + /// Returns the value as a float. + pub fn value(&self) -> f32 { + self.0.to_float() + } + + /// Construct a valid weight from a float value. + pub fn from_float(v: f32) -> Self { + Self(FixedPoint::from_float( + v.max(MIN_FONT_WEIGHT).min(MAX_FONT_WEIGHT), + )) + } + + /// Return the bolder weight. + /// + /// See the table in: + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + pub fn bolder(self) -> Self { + let value = self.value(); + if value < 350. { + return Self::NORMAL; + } + if value < 550. { + return Self::BOLD; + } + Self::from_float(value.max(900.)) + } + + /// Return the lighter weight. + /// + /// See the table in: + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + pub fn lighter(self) -> Self { + let value = self.value(); + if value < 550. { + return Self::from_float(value.min(100.)); + } + if value < 750. { + return Self::NORMAL; + } + Self::BOLD + } +} + +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))] +/// The computed value of font-size +pub struct FontSize { + /// The computed size, that we use to compute ems etc. This accounts for + /// e.g., text-zoom. + pub computed_size: NonNegativeLength, + /// The actual used size. This is the computed font size, potentially + /// constrained by other factors like minimum font-size settings and so on. + #[css(skip)] + pub used_size: NonNegativeLength, + /// If derived from a keyword, the keyword and additional transformations applied to it + #[css(skip)] + pub keyword_info: KeywordInfo, +} + +impl FontSize { + /// The actual computed font size. + #[inline] + pub fn computed_size(&self) -> Length { + self.computed_size.0 + } + + /// The actual used font size. + #[inline] + pub fn used_size(&self) -> Length { + self.used_size.0 + } + + #[inline] + /// Get default value of font size. + pub fn medium() -> Self { + Self { + computed_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)), + used_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)), + keyword_info: KeywordInfo::medium(), + } + } +} + +impl ToAnimatedValue for FontSize { + type AnimatedValue = Length; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.computed_size.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontSize { + computed_size: NonNegative(animated.clamp_to_non_negative()), + used_size: NonNegative(animated.clamp_to_non_negative()), + keyword_info: KeywordInfo::none(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, ToComputedValue, ToResolvedValue)] +#[cfg_attr(feature = "servo", derive(Hash, MallocSizeOf, Serialize, Deserialize))] +/// Specifies a prioritized list of font family names or generic family names. +#[repr(C)] +pub struct FontFamily { + /// The actual list of family names. + pub families: FontFamilyList, + /// Whether this font-family came from a specified system-font. + pub is_system_font: bool, + /// Whether this is the initial font-family that might react to language + /// changes. + pub is_initial: bool, +} + +macro_rules! static_font_family { + ($ident:ident, $family:expr) => { + lazy_static! { + static ref $ident: FontFamily = FontFamily { + families: FontFamilyList { + list: crate::ArcSlice::from_iter_leaked(std::iter::once($family)), + }, + is_system_font: false, + is_initial: false, + }; + } + }; +} + +impl FontFamily { + #[inline] + /// Get default font family as `serif` which is a generic font-family + pub fn serif() -> Self { + Self::generic(GenericFontFamily::Serif).clone() + } + + /// Returns the font family for `-moz-bullet-font`. + pub(crate) fn moz_bullet() -> &'static Self { + static_font_family!( + MOZ_BULLET, + SingleFontFamily::FamilyName(FamilyName { + name: atom!("-moz-bullet-font"), + syntax: FontFamilyNameSyntax::Identifiers, + }) + ); + + &*MOZ_BULLET + } + + /// Returns a font family for a single system font. + pub fn for_system_font(name: &str) -> Self { + Self { + families: FontFamilyList { + list: crate::ArcSlice::from_iter(std::iter::once(SingleFontFamily::FamilyName( + FamilyName { + name: Atom::from(name), + syntax: FontFamilyNameSyntax::Identifiers, + }, + ))), + }, + is_system_font: true, + is_initial: false, + } + } + + /// Returns a generic font family. + pub fn generic(generic: GenericFontFamily) -> &'static Self { + macro_rules! generic_font_family { + ($ident:ident, $family:ident) => { + static_font_family!( + $ident, + SingleFontFamily::Generic(GenericFontFamily::$family) + ) + }; + } + + generic_font_family!(SERIF, Serif); + generic_font_family!(SANS_SERIF, SansSerif); + generic_font_family!(MONOSPACE, Monospace); + generic_font_family!(CURSIVE, Cursive); + generic_font_family!(FANTASY, Fantasy); + generic_font_family!(MOZ_EMOJI, MozEmoji); + generic_font_family!(SYSTEM_UI, SystemUi); + + let family = match generic { + GenericFontFamily::None => { + debug_assert!(false, "Bogus caller!"); + &*SERIF + }, + GenericFontFamily::Serif => &*SERIF, + GenericFontFamily::SansSerif => &*SANS_SERIF, + GenericFontFamily::Monospace => &*MONOSPACE, + GenericFontFamily::Cursive => &*CURSIVE, + GenericFontFamily::Fantasy => &*FANTASY, + GenericFontFamily::MozEmoji => &*MOZ_EMOJI, + GenericFontFamily::SystemUi => &*SYSTEM_UI, + }; + debug_assert_eq!( + *family.families.iter().next().unwrap(), + SingleFontFamily::Generic(generic) + ); + family + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for FontFamily { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + use malloc_size_of::MallocUnconditionalSizeOf; + // SharedFontList objects are generally measured from the pointer stored + // in the specified value. So only count this if the SharedFontList is + // unshared. + let shared_font_list = &self.families.list; + if shared_font_list.is_unique() { + shared_font_list.unconditional_size_of(ops) + } else { + 0 + } + } +} + +impl ToCss for FontFamily { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + let mut iter = self.families.iter(); + match iter.next() { + Some(f) => f.to_css(dest)?, + None => return Ok(()), + } + for family in iter { + dest.write_str(", ")?; + family.to_css(dest)?; + } + Ok(()) + } +} + +/// The name of a font family of choice. +#[derive( + Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(C)] +pub struct FamilyName { + /// Name of the font family. + pub name: Atom, + /// Syntax of the font family. + pub syntax: FontFamilyNameSyntax, +} + +impl FamilyName { + fn is_known_icon_font_family(&self) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsKnownIconFontFamily(self.name.as_ptr()) } + } +} + +impl ToCss for FamilyName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + match self.syntax { + FontFamilyNameSyntax::Quoted => { + dest.write_char('"')?; + write!(CssStringWriter::new(dest), "{}", self.name)?; + dest.write_char('"') + }, + FontFamilyNameSyntax::Identifiers => { + let mut first = true; + for ident in self.name.to_string().split(' ') { + if first { + first = false; + } else { + dest.write_char(' ')?; + } + debug_assert!( + !ident.is_empty(), + "Family name with leading, \ + trailing, or consecutive white spaces should \ + have been marked quoted by the parser" + ); + serialize_identifier(ident, dest)?; + } + Ok(()) + }, + } + } +} + +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +/// Font family names must either be given quoted as strings, +/// or unquoted as a sequence of one or more identifiers. +#[repr(u8)] +pub enum FontFamilyNameSyntax { + /// The family name was specified in a quoted form, e.g. "Font Name" + /// or 'Font Name'. + Quoted, + + /// The family name was specified in an unquoted form as a sequence of + /// identifiers. + Identifiers, +} + +/// A set of faces that vary in weight, width or slope. +/// cbindgen:derive-mut-casts=true +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize, Hash))] +#[repr(u8)] +pub enum SingleFontFamily { + /// The name of a font family of choice. + FamilyName(FamilyName), + /// Generic family name. + Generic(GenericFontFamily), +} + +fn system_ui_enabled(_: &ParserContext) -> bool { + static_prefs::pref!("layout.css.system-ui.enabled") +} + +/// A generic font-family name. +/// +/// The order here is important, if you change it make sure that +/// `gfxPlatformFontList.h`s ranged array and `gfxFontFamilyList`'s +/// sSingleGenerics are updated as well. +/// +/// NOTE(emilio): Should be u8, but it's a u32 because of ABI issues between GCC +/// and LLVM see https://bugs.llvm.org/show_bug.cgi?id=44228 / bug 1600735 / +/// bug 1726515. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + Parse, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(u32)] +#[allow(missing_docs)] +pub enum GenericFontFamily { + /// No generic family specified, only for internal usage. + /// + /// NOTE(emilio): Gecko code relies on this variant being zero. + #[css(skip)] + None = 0, + Serif, + SansSerif, + #[parse(aliases = "-moz-fixed")] + Monospace, + Cursive, + Fantasy, + #[parse(condition = "system_ui_enabled")] + SystemUi, + /// An internal value for emoji font selection. + #[css(skip)] + #[cfg(feature = "gecko")] + MozEmoji, +} + +impl GenericFontFamily { + /// When we disallow websites to override fonts, we ignore some generic + /// families that the website might specify, since they're not configured by + /// the user. See bug 789788 and bug 1730098. + pub(crate) fn valid_for_user_font_prioritization(self) -> bool { + match self { + Self::None | Self::Fantasy | Self::Cursive | Self::SystemUi | Self::MozEmoji => false, + + Self::Serif | Self::SansSerif | Self::Monospace => true, + } + } +} + +impl Parse for SingleFontFamily { + /// Parse a font-family value. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) { + return Ok(SingleFontFamily::FamilyName(FamilyName { + name: Atom::from(&*value), + syntax: FontFamilyNameSyntax::Quoted, + })); + } + + if let Ok(generic) = input.try_parse(|i| GenericFontFamily::parse(context, i)) { + return Ok(SingleFontFamily::Generic(generic)); + } + + let first_ident = input.expect_ident_cloned()?; + let reserved = match_ignore_ascii_case! { &first_ident, + // https://drafts.csswg.org/css-fonts/#propdef-font-family + // "Font family names that happen to be the same as a keyword value + // (`inherit`, `serif`, `sans-serif`, `monospace`, `fantasy`, and `cursive`) + // must be quoted to prevent confusion with the keywords with the same names. + // The keywords ‘initial’ and ‘default’ are reserved for future use + // and must also be quoted when used as font names. + // UAs must not consider these keywords as matching the <family-name> type." + "inherit" | "initial" | "unset" | "revert" | "default" => true, + _ => false, + }; + + let mut value = first_ident.as_ref().to_owned(); + let mut serialize_quoted = value.contains(' '); + + // These keywords are not allowed by themselves. + // The only way this value can be valid with with another keyword. + if reserved { + let ident = input.expect_ident()?; + serialize_quoted = serialize_quoted || ident.contains(' '); + value.push(' '); + value.push_str(&ident); + } + while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { + serialize_quoted = serialize_quoted || ident.contains(' '); + value.push(' '); + value.push_str(&ident); + } + let syntax = if serialize_quoted { + // For font family names which contains special white spaces, e.g. + // `font-family: \ a\ \ b\ \ c\ ;`, it is tricky to serialize them + // as identifiers correctly. Just mark them quoted so we don't need + // to worry about them in serialization code. + FontFamilyNameSyntax::Quoted + } else { + FontFamilyNameSyntax::Identifiers + }; + Ok(SingleFontFamily::FamilyName(FamilyName { + name: Atom::from(value), + syntax, + })) + } +} + +#[cfg(feature = "servo")] +impl SingleFontFamily { + /// Get the corresponding font-family with Atom + pub fn from_atom(input: Atom) -> SingleFontFamily { + match input { + atom!("serif") => return SingleFontFamily::Generic(GenericFontFamily::Serif), + atom!("sans-serif") => return SingleFontFamily::Generic(GenericFontFamily::SansSerif), + atom!("cursive") => return SingleFontFamily::Generic(GenericFontFamily::Cursive), + atom!("fantasy") => return SingleFontFamily::Generic(GenericFontFamily::Fantasy), + atom!("monospace") => return SingleFontFamily::Generic(GenericFontFamily::Monospace), + _ => {}, + } + + match_ignore_ascii_case! { &input, + "serif" => return SingleFontFamily::Generic(GenericFontFamily::Serif), + "sans-serif" => return SingleFontFamily::Generic(GenericFontFamily::SansSerif), + "cursive" => return SingleFontFamily::Generic(GenericFontFamily::Cursive), + "fantasy" => return SingleFontFamily::Generic(GenericFontFamily::Fantasy), + "monospace" => return SingleFontFamily::Generic(GenericFontFamily::Monospace), + _ => {} + } + + // We don't know if it's quoted or not. So we set it to + // quoted by default. + SingleFontFamily::FamilyName(FamilyName { + name: input, + syntax: FontFamilyNameSyntax::Quoted, + }) + } +} + +/// A list of font families. +#[derive(Clone, Debug, ToComputedValue, ToResolvedValue, ToShmem, PartialEq, Eq)] +#[repr(C)] +pub struct FontFamilyList { + /// The actual list of font families specified. + pub list: crate::ArcSlice<SingleFontFamily>, +} + +impl FontFamilyList { + /// Return iterator of SingleFontFamily + pub fn iter(&self) -> impl Iterator<Item = &SingleFontFamily> { + self.list.iter() + } + + /// If there's a generic font family on the list which is suitable for user + /// font prioritization, then move it ahead of the other families in the list, + /// except for any families known to be ligature-based icon fonts, where using a + /// generic instead of the site's specified font may cause substantial breakage. + /// If no suitable generic is found in the list, insert the default generic ahead + /// of all the listed families except for known ligature-based icon fonts. + pub(crate) fn prioritize_first_generic_or_prepend(&mut self, generic: GenericFontFamily) { + let mut index_of_first_generic = None; + let mut target_index = None; + + for (i, f) in self.iter().enumerate() { + match &*f { + SingleFontFamily::Generic(f) => { + if index_of_first_generic.is_none() && f.valid_for_user_font_prioritization() { + // If we haven't found a target position, there's nothing to do; + // this entry is already ahead of everything except any whitelisted + // icon fonts. + if target_index.is_none() { + return; + } + index_of_first_generic = Some(i); + break; + } + // A non-prioritized generic (e.g. cursive, fantasy) becomes the target + // position for prioritization, just like arbitrary named families. + if target_index.is_none() { + target_index = Some(i); + } + }, + SingleFontFamily::FamilyName(fam) => { + // Target position for the first generic is in front of the first + // non-whitelisted icon font family we find. + if target_index.is_none() && !fam.is_known_icon_font_family() { + target_index = Some(i); + } + }, + } + } + + let mut new_list = self.list.iter().cloned().collect::<Vec<_>>(); + let first_generic = match index_of_first_generic { + Some(i) => new_list.remove(i), + None => SingleFontFamily::Generic(generic), + }; + + if let Some(i) = target_index { + new_list.insert(i, first_generic); + } else { + new_list.push(first_generic); + } + self.list = crate::ArcSlice::from_iter(new_list.into_iter()); + } + + /// Returns whether we need to prioritize user fonts. + pub(crate) fn needs_user_font_prioritization(&self) -> bool { + self.iter().next().map_or(true, |f| match f { + SingleFontFamily::Generic(f) => !f.valid_for_user_font_prioritization(), + _ => true, + }) + } + + /// Return the generic ID if it is a single generic font + pub fn single_generic(&self) -> Option<GenericFontFamily> { + let mut iter = self.iter(); + if let Some(SingleFontFamily::Generic(f)) = iter.next() { + if iter.next().is_none() { + return Some(*f); + } + } + None + } +} + +/// Preserve the readability of text when font fallback occurs. +pub type FontSizeAdjust = generics::GenericFontSizeAdjust<NonNegativeNumber>; + +impl FontSizeAdjust { + #[inline] + /// Default value of font-size-adjust + pub fn none() -> Self { + FontSizeAdjust::None + } +} + +impl ToComputedValue for specified::FontSizeAdjust { + type ComputedValue = FontSizeAdjust; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::font_metrics::FontMetricsOrientation; + + let font_metrics = |vertical| { + let orient = if vertical { + FontMetricsOrientation::MatchContextPreferVertical + } else { + FontMetricsOrientation::Horizontal + }; + let metrics = context.query_font_metrics(FontBaseSize::CurrentStyle, orient, false); + let font_size = context.style().get_font().clone_font_size().used_size.0; + (metrics, font_size) + }; + + // Macro to resolve a from-font value using the given metric field. If not present, + // returns the fallback value, or if that is negative, resolves using ascent instead + // of the missing field (this is the fallback for cap-height). + macro_rules! resolve { + ($basis:ident, $value:expr, $vertical:expr, $field:ident, $fallback:expr) => {{ + match $value { + specified::FontSizeAdjustFactor::Number(f) => { + FontSizeAdjust::$basis(f.to_computed_value(context)) + }, + specified::FontSizeAdjustFactor::FromFont => { + let (metrics, font_size) = font_metrics($vertical); + let ratio = if let Some(metric) = metrics.$field { + metric / font_size + } else if $fallback >= 0.0 { + $fallback + } else { + metrics.ascent / font_size + }; + if ratio.is_nan() { + FontSizeAdjust::$basis(NonNegative(abs($fallback))) + } else { + FontSizeAdjust::$basis(NonNegative(ratio)) + } + }, + } + }}; + } + + match *self { + Self::None => FontSizeAdjust::None, + Self::ExHeight(val) => resolve!(ExHeight, val, false, x_height, 0.5), + Self::CapHeight(val) => { + resolve!(CapHeight, val, false, cap_height, -1.0 /* fall back to ascent */) + }, + Self::ChWidth(val) => resolve!(ChWidth, val, false, zero_advance_measure, 0.5), + Self::IcWidth(val) => resolve!(IcWidth, val, false, ic_width, 1.0), + Self::IcHeight(val) => resolve!(IcHeight, val, true, ic_width, 1.0), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + macro_rules! case { + ($basis:ident, $val:expr) => { + Self::$basis(specified::FontSizeAdjustFactor::Number( + ToComputedValue::from_computed_value($val), + )) + }; + } + match *computed { + FontSizeAdjust::None => Self::None, + FontSizeAdjust::ExHeight(ref val) => case!(ExHeight, val), + FontSizeAdjust::CapHeight(ref val) => case!(CapHeight, val), + FontSizeAdjust::ChWidth(ref val) => case!(ChWidth, val), + FontSizeAdjust::IcWidth(ref val) => case!(IcWidth, val), + FontSizeAdjust::IcHeight(ref val) => case!(IcHeight, val), + } + } +} + +/// Use FontSettings as computed type of FontFeatureSettings. +pub type FontFeatureSettings = FontSettings<FeatureTagValue<Integer>>; + +/// The computed value for font-variation-settings. +pub type FontVariationSettings = FontSettings<VariationValue<Number>>; + +// The computed value of font-{feature,variation}-settings discards values +// with duplicate tags, keeping only the last occurrence of each tag. +fn dedup_font_settings<T>(settings_list: &mut Vec<T>) +where + T: TaggedFontValue, +{ + if settings_list.len() > 1 { + settings_list.sort_by_key(|k| k.tag().0); + // dedup() keeps the first of any duplicates, but we want the last, + // so we implement it manually here. + let mut prev_tag = settings_list.last().unwrap().tag(); + for i in (0..settings_list.len() - 1).rev() { + let cur_tag = settings_list[i].tag(); + if cur_tag == prev_tag { + settings_list.remove(i); + } + prev_tag = cur_tag; + } + } +} + +impl<T> ToComputedValue for FontSettings<T> +where + T: ToComputedValue, + <T as ToComputedValue>::ComputedValue: TaggedFontValue, +{ + type ComputedValue = FontSettings<T::ComputedValue>; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let mut v = self + .0 + .iter() + .map(|item| item.to_computed_value(context)) + .collect::<Vec<_>>(); + dedup_font_settings(&mut v); + FontSettings(v.into_boxed_slice()) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self( + computed + .0 + .iter() + .map(T::from_computed_value) + .collect::<Vec<_>>() + .into_boxed_slice(), + ) + } +} + +/// font-language-override can only have a single 1-4 ASCII character +/// OpenType "language system" tag, so we should be able to compute +/// it and store it as a 32-bit integer +/// (see http://www.microsoft.com/typography/otspec/languagetags.htm). +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[value_info(other_values = "normal")] +pub struct FontLanguageOverride(pub u32); + +impl FontLanguageOverride { + #[inline] + /// Get computed default value of `font-language-override` with 0 + pub fn normal() -> FontLanguageOverride { + FontLanguageOverride(0) + } + + /// Returns this value as a `&str`, backed by `storage`. + #[inline] + pub(crate) fn to_str(self, storage: &mut [u8; 4]) -> &str { + *storage = u32::to_be_bytes(self.0); + // Safe because we ensure it's ASCII during parsing + let slice = if cfg!(debug_assertions) { + std::str::from_utf8(&storage[..]).unwrap() + } else { + unsafe { std::str::from_utf8_unchecked(&storage[..]) } + }; + slice.trim_end() + } + + /// Unsafe because `Self::to_str` requires the value to represent a UTF-8 + /// string. + #[inline] + pub unsafe fn from_u32(value: u32) -> Self { + Self(value) + } +} + +impl ToCss for FontLanguageOverride { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + if self.0 == 0 { + return dest.write_str("normal"); + } + self.to_str(&mut [0; 4]).to_css(dest) + } +} + +// FIXME(emilio): Make Gecko use the cbindgen'd fontLanguageOverride, then +// remove this. +#[cfg(feature = "gecko")] +impl From<u32> for FontLanguageOverride { + fn from(v: u32) -> Self { + unsafe { Self::from_u32(v) } + } +} + +#[cfg(feature = "gecko")] +impl From<FontLanguageOverride> for u32 { + fn from(v: FontLanguageOverride) -> u32 { + v.0 + } +} + +impl ToComputedValue for specified::MozScriptMinSize { + type ComputedValue = MozScriptMinSize; + + fn to_computed_value(&self, cx: &Context) -> MozScriptMinSize { + // this value is used in the computation of font-size, so + // we use the parent size + let base_size = FontBaseSize::InheritedStyle; + let line_height_base = LineHeightBase::InheritedStyle; + match self.0 { + NoCalcLength::FontRelative(value) => { + value.to_computed_value(cx, base_size, line_height_base) + }, + NoCalcLength::ServoCharacterWidth(value) => { + value.to_computed_value(base_size.resolve(cx).computed_size()) + }, + ref l => l.to_computed_value(cx), + } + } + + fn from_computed_value(other: &MozScriptMinSize) -> Self { + specified::MozScriptMinSize(ToComputedValue::from_computed_value(other)) + } +} + +/// The computed value of the math-depth property. +pub type MathDepth = i8; + +#[cfg(feature = "gecko")] +impl ToComputedValue for specified::MathDepth { + type ComputedValue = MathDepth; + + fn to_computed_value(&self, cx: &Context) -> i8 { + use crate::properties::longhands::math_style::SpecifiedValue as MathStyleValue; + use std::{cmp, i8}; + + let int = match *self { + specified::MathDepth::AutoAdd => { + let parent = cx.builder.get_parent_font().clone_math_depth() as i32; + let style = cx.builder.get_parent_font().clone_math_style(); + if style == MathStyleValue::Compact { + parent.saturating_add(1) + } else { + parent + } + }, + specified::MathDepth::Add(rel) => { + let parent = cx.builder.get_parent_font().clone_math_depth(); + (parent as i32).saturating_add(rel.to_computed_value(cx)) + }, + specified::MathDepth::Absolute(abs) => abs.to_computed_value(cx), + }; + cmp::min(int, i8::MAX as i32) as i8 + } + + fn from_computed_value(other: &i8) -> Self { + let computed_value = *other as i32; + specified::MathDepth::Absolute(SpecifiedInteger::from_computed_value(&computed_value)) + } +} + +/// - Use a signed 8.8 fixed-point value (representable range -128.0..128) +/// +/// Values of <angle> below -90 or above 90 not permitted, so we use out of +/// range values to represent normal | oblique +pub const FONT_STYLE_FRACTION_BITS: u16 = 8; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference +/// workaround. +pub type FontStyleFixedPoint = FixedPoint<i16, FONT_STYLE_FRACTION_BITS>; + +/// The computed value of `font-style`. +/// +/// - Define out of range values min value (-128.0) as meaning 'normal' +/// - Define max value (127.99609375) as 'italic' +/// - Other values represent 'oblique <angle>' +/// - Note that 'oblique 0deg' is distinct from 'normal' (should it be?) +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +#[repr(C)] +pub struct FontStyle(FontStyleFixedPoint); + +impl FontStyle { + /// The normal keyword. + pub const NORMAL: FontStyle = FontStyle(FontStyleFixedPoint { + value: 100 << FONT_STYLE_FRACTION_BITS, + }); + /// The italic keyword. + pub const ITALIC: FontStyle = FontStyle(FontStyleFixedPoint { + value: 101 << FONT_STYLE_FRACTION_BITS, + }); + + /// The default angle for `font-style: oblique`. + /// See also https://github.com/w3c/csswg-drafts/issues/2295 + pub const DEFAULT_OBLIQUE_DEGREES: i16 = 14; + + /// The `oblique` keyword with the default degrees. + pub const OBLIQUE: FontStyle = FontStyle(FontStyleFixedPoint { + value: Self::DEFAULT_OBLIQUE_DEGREES << FONT_STYLE_FRACTION_BITS, + }); + + /// The `normal` value. + #[inline] + pub fn normal() -> Self { + Self::NORMAL + } + + /// Returns the oblique angle for this style. + pub fn oblique(degrees: f32) -> Self { + Self(FixedPoint::from_float( + degrees + .max(specified::FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES) + .min(specified::FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES), + )) + } + + /// Returns the oblique angle for this style. + pub fn oblique_degrees(&self) -> f32 { + debug_assert_ne!(*self, Self::NORMAL); + debug_assert_ne!(*self, Self::ITALIC); + self.0.to_float() + } +} + +impl ToCss for FontStyle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + if *self == Self::NORMAL { + return dest.write_str("normal"); + } + if *self == Self::ITALIC { + return dest.write_str("italic"); + } + if *self == Self::OBLIQUE { + return dest.write_str("oblique"); + } + dest.write_str("oblique ")?; + let angle = Angle::from_degrees(self.oblique_degrees()); + angle.to_css(dest)?; + Ok(()) + } +} + +impl ToAnimatedValue for FontStyle { + type AnimatedValue = generics::FontStyle<Angle>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + if self == Self::NORMAL { + // This allows us to animate between normal and oblique values. Per spec, + // https://drafts.csswg.org/css-fonts-4/#font-style-prop: + // Animation type: by computed value type; 'normal' animates as 'oblique 0deg' + return generics::FontStyle::Oblique(Angle::from_degrees(0.0)); + } + if self == Self::ITALIC { + return generics::FontStyle::Italic; + } + generics::FontStyle::Oblique(Angle::from_degrees(self.oblique_degrees())) + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + match animated { + generics::FontStyle::Normal => Self::NORMAL, + generics::FontStyle::Italic => Self::ITALIC, + generics::FontStyle::Oblique(ref angle) => { + if angle.degrees() == 0.0 { + // Reverse the conversion done in to_animated_value() + Self::NORMAL + } else { + Self::oblique(angle.degrees()) + } + }, + } + } +} + +/// font-stretch is a percentage relative to normal. +/// +/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375) +/// +/// We arbitrarily limit here to 1000%. (If that becomes a problem, we could +/// reduce the number of fractional bits and increase the limit.) +pub const FONT_STRETCH_FRACTION_BITS: u16 = 6; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference +/// workaround. +pub type FontStretchFixedPoint = FixedPoint<u16, FONT_STRETCH_FRACTION_BITS>; + +/// A value for the font-stretch property per: +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[derive( + Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue, +)] +#[repr(C)] +pub struct FontStretch(pub FontStretchFixedPoint); + +impl FontStretch { + /// The fraction bits, as an easy-to-access-constant. + pub const FRACTION_BITS: u16 = FONT_STRETCH_FRACTION_BITS; + /// 0.5 in our floating point representation. + pub const HALF: u16 = 1 << (Self::FRACTION_BITS - 1); + + /// The `ultra-condensed` keyword. + pub const ULTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 50 << Self::FRACTION_BITS, + }); + /// The `extra-condensed` keyword. + pub const EXTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: (62 << Self::FRACTION_BITS) + Self::HALF, + }); + /// The `condensed` keyword. + pub const CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 75 << Self::FRACTION_BITS, + }); + /// The `semi-condensed` keyword. + pub const SEMI_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: (87 << Self::FRACTION_BITS) + Self::HALF, + }); + /// The `normal` keyword. + pub const NORMAL: FontStretch = FontStretch(FontStretchFixedPoint { + value: 100 << Self::FRACTION_BITS, + }); + /// The `semi-expanded` keyword. + pub const SEMI_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: (112 << Self::FRACTION_BITS) + Self::HALF, + }); + /// The `expanded` keyword. + pub const EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 125 << Self::FRACTION_BITS, + }); + /// The `extra-expanded` keyword. + pub const EXTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 150 << Self::FRACTION_BITS, + }); + /// The `ultra-expanded` keyword. + pub const ULTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 200 << Self::FRACTION_BITS, + }); + + /// 100% + pub fn hundred() -> Self { + Self::NORMAL + } + + /// Converts to a computed percentage. + #[inline] + pub fn to_percentage(&self) -> Percentage { + Percentage(self.0.to_float() / 100.0) + } + + /// Converts from a computed percentage value. + pub fn from_percentage(p: f32) -> Self { + Self(FixedPoint::from_float((p * 100.).max(0.0).min(1000.0))) + } + + /// Returns a relevant stretch value from a keyword. + /// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop + pub fn from_keyword(kw: specified::FontStretchKeyword) -> Self { + use specified::FontStretchKeyword::*; + match kw { + UltraCondensed => Self::ULTRA_CONDENSED, + ExtraCondensed => Self::EXTRA_CONDENSED, + Condensed => Self::CONDENSED, + SemiCondensed => Self::SEMI_CONDENSED, + Normal => Self::NORMAL, + SemiExpanded => Self::SEMI_EXPANDED, + Expanded => Self::EXPANDED, + ExtraExpanded => Self::EXTRA_EXPANDED, + UltraExpanded => Self::ULTRA_EXPANDED, + } + } + + /// Returns the stretch keyword if we map to one of the relevant values. + pub fn as_keyword(&self) -> Option<specified::FontStretchKeyword> { + use specified::FontStretchKeyword::*; + // TODO: Can we use match here? + if *self == Self::ULTRA_CONDENSED { + return Some(UltraCondensed); + } + if *self == Self::EXTRA_CONDENSED { + return Some(ExtraCondensed); + } + if *self == Self::CONDENSED { + return Some(Condensed); + } + if *self == Self::SEMI_CONDENSED { + return Some(SemiCondensed); + } + if *self == Self::NORMAL { + return Some(Normal); + } + if *self == Self::SEMI_EXPANDED { + return Some(SemiExpanded); + } + if *self == Self::EXPANDED { + return Some(Expanded); + } + if *self == Self::EXTRA_EXPANDED { + return Some(ExtraExpanded); + } + if *self == Self::ULTRA_EXPANDED { + return Some(UltraExpanded); + } + None + } +} + +impl ToCss for FontStretch { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.to_percentage().to_css(dest) + } +} + +impl ToAnimatedValue for FontStretch { + type AnimatedValue = Percentage; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.to_percentage() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Self::from_percentage(animated.0) + } +} + +/// A computed value for the `line-height` property. +pub type LineHeight = generics::GenericLineHeight<NonNegativeNumber, NonNegativeLength>; + +impl ToResolvedValue for LineHeight { + type ResolvedValue = Self; + + fn to_resolved_value(self, context: &ResolvedContext) -> Self::ResolvedValue { + // Resolve <number> to an absolute <length> based on font size. + if matches!(self, Self::Normal | Self::MozBlockHeight) { + return self; + } + let wm = context.style.writing_mode; + Self::Length(context.device.calc_line_height( + context.style.get_font(), + wm, + Some(context.element_info.element), + )) + } + + #[inline] + fn from_resolved_value(value: Self::ResolvedValue) -> Self { + value + } +} diff --git a/servo/components/style/values/computed/image.rs b/servo/components/style/values/computed/image.rs new file mode 100644 index 0000000000..8a91d95313 --- /dev/null +++ b/servo/components/style/values/computed/image.rs @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [`image`][image]s +//! +//! [image]: https://drafts.csswg.org/css-images/#image-values + +use crate::values::computed::percentage::Percentage; +use crate::values::computed::position::Position; +use crate::values::computed::url::ComputedImageUrl; +use crate::values::computed::{Angle, Color, Context}; +use crate::values::computed::{ + AngleOrPercentage, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage, + Resolution, ToComputedValue, +}; +use crate::values::generics::image::{self as generic, GradientCompatMode}; +use crate::values::specified::image as specified; +use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword}; +use std::f32::consts::PI; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +pub use specified::ImageRendering; + +/// Computed values for an image according to CSS-IMAGES. +/// <https://drafts.csswg.org/css-images/#image-values> +pub type Image = generic::GenericImage<Gradient, ComputedImageUrl, Color, Percentage, Resolution>; + +// Images should remain small, see https://github.com/servo/servo/pull/18430 +size_of_test!(Image, 16); + +/// Computed values for a CSS gradient. +/// <https://drafts.csswg.org/css-images/#gradients> +pub type Gradient = generic::GenericGradient< + LineDirection, + LengthPercentage, + NonNegativeLength, + NonNegativeLengthPercentage, + Position, + Angle, + AngleOrPercentage, + Color, +>; + +/// Computed values for CSS cross-fade +/// <https://drafts.csswg.org/css-images-4/#cross-fade-function> +pub type CrossFade = generic::CrossFade<Image, Color, Percentage>; + +/// A computed radial gradient ending shape. +pub type EndingShape = generic::GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>; + +/// A computed gradient line direction. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue)] +#[repr(C, u8)] +pub enum LineDirection { + /// An angle. + Angle(Angle), + /// A horizontal direction. + Horizontal(HorizontalPositionKeyword), + /// A vertical direction. + Vertical(VerticalPositionKeyword), + /// A corner. + Corner(HorizontalPositionKeyword, VerticalPositionKeyword), +} + +/// The computed value for an `image-set()` image. +pub type ImageSet = generic::GenericImageSet<Image, Resolution>; + +impl ToComputedValue for specified::ImageSet { + type ComputedValue = ImageSet; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let items = self.items.to_computed_value(context); + let dpr = context.device().device_pixel_ratio().get(); + + let mut supported_image = false; + let mut selected_index = std::usize::MAX; + let mut selected_resolution = 0.0; + + for (i, item) in items.iter().enumerate() { + if item.has_mime_type && !context.device().is_supported_mime_type(&item.mime_type) { + // If the MIME type is not supported, we discard the ImageSetItem. + continue; + } + + let candidate_resolution = item.resolution.dppx(); + debug_assert!( + candidate_resolution >= 0.0, + "Resolutions should be non-negative" + ); + if candidate_resolution == 0.0 { + // If the resolution is 0, we also treat it as an invalid image. + continue; + } + + // https://drafts.csswg.org/css-images-4/#image-set-notation: + // + // Make a UA-specific choice of which to load, based on whatever criteria deemed + // relevant (such as the resolution of the display, connection speed, etc). + // + // For now, select the lowest resolution greater than display density, otherwise the + // greatest resolution available. + let better_candidate = || { + if selected_resolution < dpr && candidate_resolution > selected_resolution { + return true; + } + if candidate_resolution < selected_resolution && candidate_resolution >= dpr { + return true; + } + false + }; + + // The first item with a supported MIME type is obviously the current best candidate + if !supported_image || better_candidate() { + supported_image = true; + selected_index = i; + selected_resolution = candidate_resolution; + } + } + + ImageSet { + selected_index, + items, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self { + selected_index: std::usize::MAX, + items: ToComputedValue::from_computed_value(&computed.items), + } + } +} + +impl generic::LineDirection for LineDirection { + fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool { + match *self { + LineDirection::Angle(angle) => angle.radians() == PI, + LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { + compat_mode == GradientCompatMode::Modern + }, + LineDirection::Vertical(VerticalPositionKeyword::Top) => { + compat_mode != GradientCompatMode::Modern + }, + _ => false, + } + } + + fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result + where + W: Write, + { + match *self { + LineDirection::Angle(ref angle) => angle.to_css(dest), + LineDirection::Horizontal(x) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest) + }, + LineDirection::Vertical(y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + y.to_css(dest) + }, + LineDirection::Corner(x, y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest)?; + dest.write_char(' ')?; + y.to_css(dest) + }, + } + } +} + +impl ToComputedValue for specified::LineDirection { + type ComputedValue = LineDirection; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + specified::LineDirection::Angle(ref angle) => { + LineDirection::Angle(angle.to_computed_value(context)) + }, + specified::LineDirection::Horizontal(x) => LineDirection::Horizontal(x), + specified::LineDirection::Vertical(y) => LineDirection::Vertical(y), + specified::LineDirection::Corner(x, y) => LineDirection::Corner(x, y), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + LineDirection::Angle(ref angle) => { + specified::LineDirection::Angle(ToComputedValue::from_computed_value(angle)) + }, + LineDirection::Horizontal(x) => specified::LineDirection::Horizontal(x), + LineDirection::Vertical(y) => specified::LineDirection::Vertical(y), + LineDirection::Corner(x, y) => specified::LineDirection::Corner(x, y), + } + } +} diff --git a/servo/components/style/values/computed/length.rs b/servo/components/style/values/computed/length.rs new file mode 100644 index 0000000000..e75676a76d --- /dev/null +++ b/servo/components/style/values/computed/length.rs @@ -0,0 +1,531 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `<length>` computed values, and related ones. + +use super::{Context, Number, ToComputedValue}; +use crate::values::animated::ToAnimatedValue; +use crate::values::computed::NonNegativeNumber; +use crate::values::generics::length as generics; +use crate::values::generics::length::{ + GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::length::{AbsoluteLength, FontBaseSize, LineHeightBase}; +use crate::values::{specified, CSSFloat}; +use crate::Zero; +use app_units::Au; +use std::fmt::{self, Write}; +use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub}; +use style_traits::{CSSPixel, CssWriter, ToCss}; + +pub use super::image::Image; +pub use super::length_percentage::{LengthPercentage, NonNegativeLengthPercentage}; +pub use crate::values::specified::url::UrlOrNone; +pub use crate::values::specified::{Angle, BorderStyle, Time}; + +impl ToComputedValue for specified::NoCalcLength { + type ComputedValue = Length; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + self.to_computed_value_with_base_size( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::CurrentStyle, + ) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Absolute(AbsoluteLength::Px(computed.px())) + } +} + +impl specified::NoCalcLength { + /// Computes a length with a given font-relative base size. + pub fn to_computed_value_with_base_size( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> Length { + match *self { + Self::Absolute(length) => length.to_computed_value(context), + Self::FontRelative(length) => { + length.to_computed_value(context, base_size, line_height_base) + }, + Self::ViewportPercentage(length) => length.to_computed_value(context), + Self::ContainerRelative(length) => length.to_computed_value(context), + Self::ServoCharacterWidth(length) => length + .to_computed_value(context.style().get_font().clone_font_size().computed_size()), + } + } +} + +impl ToComputedValue for specified::Length { + type ComputedValue = Length; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + Self::NoCalc(l) => l.to_computed_value(context), + Self::Calc(ref calc) => { + let result = calc.to_computed_value(context); + debug_assert!( + result.to_length().is_some(), + "{:?} didn't resolve to a length: {:?}", + calc, + result, + ); + result.to_length().unwrap_or_else(Length::zero) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::NoCalc(specified::NoCalcLength::from_computed_value(computed)) + } +} + +/// Some boilerplate to share between negative and non-negative +/// length-percentage or auto. +macro_rules! computed_length_percentage_or_auto { + ($inner:ty) => { + /// Returns the used value. + #[inline] + pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> { + match *self { + Self::Auto => None, + Self::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)), + } + } + + /// Returns true if the computed value is absolute 0 or 0%. + #[inline] + pub fn is_definitely_zero(&self) -> bool { + use crate::values::generics::length::LengthPercentageOrAuto::*; + match *self { + LengthPercentage(ref l) => l.is_definitely_zero(), + Auto => false, + } + } + }; +} + +/// A computed type for `<length-percentage> | auto`. +pub type LengthPercentageOrAuto = generics::GenericLengthPercentageOrAuto<LengthPercentage>; + +impl LengthPercentageOrAuto { + /// Clamps the value to a non-negative value. + pub fn clamp_to_non_negative(self) -> Self { + use crate::values::generics::length::LengthPercentageOrAuto::*; + match self { + LengthPercentage(l) => LengthPercentage(l.clamp_to_non_negative()), + Auto => Auto, + } + } + + /// Convert to have a borrow inside the enum + pub fn as_ref(&self) -> generics::GenericLengthPercentageOrAuto<&LengthPercentage> { + use crate::values::generics::length::LengthPercentageOrAuto::*; + match *self { + LengthPercentage(ref lp) => LengthPercentage(lp), + Auto => Auto, + } + } + + computed_length_percentage_or_auto!(LengthPercentage); +} + +impl generics::GenericLengthPercentageOrAuto<&LengthPercentage> { + /// Resolves the percentage. + #[inline] + pub fn percentage_relative_to(&self, basis: Length) -> LengthOrAuto { + use crate::values::generics::length::LengthPercentageOrAuto::*; + match self { + LengthPercentage(length_percentage) => { + LengthPercentage(length_percentage.percentage_relative_to(basis)) + }, + Auto => Auto, + } + } + + /// Maybe resolves the percentage. + #[inline] + pub fn maybe_percentage_relative_to(&self, basis: Option<Length>) -> LengthOrAuto { + use crate::values::generics::length::LengthPercentageOrAuto::*; + match self { + LengthPercentage(length_percentage) => length_percentage + .maybe_percentage_relative_to(basis) + .map_or(Auto, LengthPercentage), + Auto => Auto, + } + } +} + +/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0. +pub type NonNegativeLengthPercentageOrAuto = + generics::GenericLengthPercentageOrAuto<NonNegativeLengthPercentage>; + +impl NonNegativeLengthPercentageOrAuto { + computed_length_percentage_or_auto!(NonNegativeLengthPercentage); +} + +#[cfg(feature = "servo")] +impl MaxSize { + /// Convert the computed value into used value. + #[inline] + pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> { + match *self { + GenericMaxSize::None => None, + GenericMaxSize::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)), + } + } +} + +impl Size { + /// Convert the computed value into used value. + #[inline] + #[cfg(feature = "servo")] + pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> { + match *self { + GenericSize::Auto => None, + GenericSize::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)), + } + } + + /// Returns true if the computed value is absolute 0 or 0%. + #[inline] + pub fn is_definitely_zero(&self) -> bool { + match *self { + Self::Auto => false, + Self::LengthPercentage(ref lp) => lp.is_definitely_zero(), + #[cfg(feature = "gecko")] + Self::MinContent | + Self::MaxContent | + Self::FitContent | + Self::MozAvailable | + Self::FitContentFunction(_) => false, + } + } +} + +/// The computed `<length>` value. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Deserialize, + MallocSizeOf, + PartialEq, + PartialOrd, + Serialize, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CSSPixelLength(CSSFloat); + +impl fmt::Debug for CSSPixelLength { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f)?; + f.write_str(" px") + } +} + +impl CSSPixelLength { + /// Return a new CSSPixelLength. + #[inline] + pub fn new(px: CSSFloat) -> Self { + CSSPixelLength(px) + } + + /// Returns a normalized (NaN turned to zero) version of this length. + #[inline] + pub fn normalized(self) -> Self { + Self::new(crate::values::normalize(self.0)) + } + + /// Returns a finite (normalized and clamped to float min and max) version of this length. + #[inline] + pub fn finite(self) -> Self { + Self::new(crate::values::normalize(self.0).min(f32::MAX).max(f32::MIN)) + } + + /// Scale the length by a given amount. + #[inline] + pub fn scale_by(self, scale: CSSFloat) -> Self { + CSSPixelLength(self.0 * scale) + } + + /// Return the containing pixel value. + #[inline] + pub fn px(self) -> CSSFloat { + self.0 + } + + /// Return the length with app_unit i32 type. + #[inline] + pub fn to_i32_au(self) -> i32 { + Au::from(self).0 + } + + /// Return the absolute value of this length. + #[inline] + pub fn abs(self) -> Self { + CSSPixelLength::new(self.0.abs()) + } + + /// Return the clamped value of this length. + #[inline] + pub fn clamp_to_non_negative(self) -> Self { + CSSPixelLength::new(self.0.max(0.)) + } + + /// Returns the minimum between `self` and `other`. + #[inline] + pub fn min(self, other: Self) -> Self { + CSSPixelLength::new(self.0.min(other.0)) + } + + /// Returns the maximum between `self` and `other`. + #[inline] + pub fn max(self, other: Self) -> Self { + CSSPixelLength::new(self.0.max(other.0)) + } + + /// Sets `self` to the maximum between `self` and `other`. + #[inline] + pub fn max_assign(&mut self, other: Self) { + *self = self.max(other); + } + + /// Clamp the value to a lower bound and an optional upper bound. + /// + /// Can be used for example with `min-width` and `max-width`. + #[inline] + pub fn clamp_between_extremums(self, min_size: Self, max_size: Option<Self>) -> Self { + self.clamp_below_max(max_size).max(min_size) + } + + /// Clamp the value to an optional upper bound. + /// + /// Can be used for example with `max-width`. + #[inline] + pub fn clamp_below_max(self, max_size: Option<Self>) -> Self { + match max_size { + None => self, + Some(max_size) => self.min(max_size), + } + } +} + +impl num_traits::Zero for CSSPixelLength { + fn zero() -> Self { + CSSPixelLength::new(0.) + } + + fn is_zero(&self) -> bool { + self.px() == 0. + } +} + +impl ToCss for CSSPixelLength { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + dest.write_str("px") + } +} + +impl std::iter::Sum for CSSPixelLength { + fn sum<I: Iterator<Item = Self>>(iter: I) -> Self { + iter.fold(Length::zero(), Add::add) + } +} + +impl Add for CSSPixelLength { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + Self::new(self.px() + other.px()) + } +} + +impl AddAssign for CSSPixelLength { + #[inline] + fn add_assign(&mut self, other: Self) { + self.0 += other.0; + } +} + +impl Div for CSSPixelLength { + type Output = CSSFloat; + + #[inline] + fn div(self, other: Self) -> CSSFloat { + self.px() / other.px() + } +} + +impl Div<CSSFloat> for CSSPixelLength { + type Output = Self; + + #[inline] + fn div(self, other: CSSFloat) -> Self { + Self::new(self.px() / other) + } +} + +impl MulAssign<CSSFloat> for CSSPixelLength { + #[inline] + fn mul_assign(&mut self, other: CSSFloat) { + self.0 *= other; + } +} + +impl Mul<CSSFloat> for CSSPixelLength { + type Output = Self; + + #[inline] + fn mul(self, other: CSSFloat) -> Self { + Self::new(self.px() * other) + } +} + +impl Neg for CSSPixelLength { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + CSSPixelLength::new(-self.0) + } +} + +impl Sub for CSSPixelLength { + type Output = Self; + + #[inline] + fn sub(self, other: Self) -> Self { + Self::new(self.px() - other.px()) + } +} + +impl From<CSSPixelLength> for Au { + #[inline] + fn from(len: CSSPixelLength) -> Self { + Au::from_f32_px(len.0) + } +} + +impl From<Au> for CSSPixelLength { + #[inline] + fn from(len: Au) -> Self { + CSSPixelLength::new(len.to_f32_px()) + } +} + +impl From<CSSPixelLength> for euclid::Length<CSSFloat, CSSPixel> { + #[inline] + fn from(length: CSSPixelLength) -> Self { + Self::new(length.0) + } +} + +/// An alias of computed `<length>` value. +pub type Length = CSSPixelLength; + +/// Either a computed `<length>` or the `auto` keyword. +pub type LengthOrAuto = generics::GenericLengthPercentageOrAuto<Length>; + +/// Either a non-negative `<length>` or the `auto` keyword. +pub type NonNegativeLengthOrAuto = generics::GenericLengthPercentageOrAuto<NonNegativeLength>; + +/// Either a computed `<length>` or a `<number>` value. +pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>; + +/// A wrapper of Length, whose value must be >= 0. +pub type NonNegativeLength = NonNegative<Length>; + +impl ToAnimatedValue for NonNegativeLength { + type AnimatedValue = Length; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + NonNegativeLength::new(animated.px().max(0.)) + } +} + +impl NonNegativeLength { + /// Create a NonNegativeLength. + #[inline] + pub fn new(px: CSSFloat) -> Self { + NonNegative(Length::new(px.max(0.))) + } + + /// Return the pixel value of |NonNegativeLength|. + #[inline] + pub fn px(&self) -> CSSFloat { + self.0.px() + } + + #[inline] + /// Ensures it is non negative + pub fn clamp(self) -> Self { + if (self.0).0 < 0. { + Self::zero() + } else { + self + } + } +} + +impl From<Length> for NonNegativeLength { + #[inline] + fn from(len: Length) -> Self { + NonNegative(len) + } +} + +impl From<Au> for NonNegativeLength { + #[inline] + fn from(au: Au) -> Self { + NonNegative(au.into()) + } +} + +impl From<NonNegativeLength> for Au { + #[inline] + fn from(non_negative_len: NonNegativeLength) -> Self { + Au::from(non_negative_len.0) + } +} + +/// Either a computed NonNegativeLengthPercentage or the `normal` keyword. +pub type NonNegativeLengthPercentageOrNormal = + GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>; + +/// Either a non-negative `<length>` or a `<number>`. +pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>; + +/// A computed value for `min-width`, `min-height`, `width` or `height` property. +pub type Size = GenericSize<NonNegativeLengthPercentage>; + +/// A computed value for `max-width` or `min-height` property. +pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>; diff --git a/servo/components/style/values/computed/length_percentage.rs b/servo/components/style/values/computed/length_percentage.rs new file mode 100644 index 0000000000..898281a7ef --- /dev/null +++ b/servo/components/style/values/computed/length_percentage.rs @@ -0,0 +1,1055 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `<length-percentage>` computed values, and related ones. +//! +//! The over-all design is a tagged pointer, with the lower bits of the pointer +//! being non-zero if it is a non-calc value. +//! +//! It is expected to take 64 bits both in x86 and x86-64. This is implemented +//! as a `union`, with 4 different variants: +//! +//! * The length and percentage variants have a { tag, f32 } (effectively) +//! layout. The tag has to overlap with the lower 2 bits of the calc variant. +//! +//! * The `calc()` variant is a { tag, pointer } in x86 (so same as the +//! others), or just a { pointer } in x86-64 (so that the two bits of the tag +//! can be obtained from the lower bits of the pointer). +//! +//! * There's a `tag` variant just to make clear when only the tag is intended +//! to be read. Note that the tag needs to be masked always by `TAG_MASK`, to +//! deal with the pointer variant in x86-64. +//! +//! The assertions in the constructor methods ensure that the tag getter matches +//! our expectations. + +use super::{Context, Length, Percentage, ToComputedValue}; +use crate::gecko_bindings::structs::GeckoFontMetrics; +use crate::values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::calc::{CalcUnits, PositivePercentageBasis}; +use crate::values::generics::{calc, NonNegative}; +use crate::values::specified::length::{FontBaseSize, LineHeightBase}; +use crate::values::{specified, CSSFloat}; +use crate::{Zero, ZeroNoPercent}; +use app_units::Au; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ToCss}; + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct LengthVariant { + tag: u8, + length: Length, +} + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct PercentageVariant { + tag: u8, + percentage: Percentage, +} + +// NOTE(emilio): cbindgen only understands the #[cfg] on the top level +// definition. +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +#[cfg(target_pointer_width = "32")] +pub struct CalcVariant { + tag: u8, + ptr: *mut CalcLengthPercentage, +} + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +#[cfg(target_pointer_width = "64")] +pub struct CalcVariant { + ptr: usize, // In little-endian byte order +} + +// `CalcLengthPercentage` is `Send + Sync` as asserted below. +unsafe impl Send for CalcVariant {} +unsafe impl Sync for CalcVariant {} + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct TagVariant { + tag: u8, +} + +/// A `<length-percentage>` value. This can be either a `<length>`, a +/// `<percentage>`, or a combination of both via `calc()`. +/// +/// cbindgen:private-default-tagged-enum-constructor=false +/// cbindgen:derive-mut-casts=true +/// +/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage +/// +/// The tag is stored in the lower two bits. +/// +/// We need to use a struct instead of the union directly because unions with +/// Drop implementations are unstable, looks like. +/// +/// Also we need the union and the variants to be `pub` (even though the member +/// is private) so that cbindgen generates it. They're not part of the public +/// API otherwise. +#[repr(transparent)] +pub struct LengthPercentage(LengthPercentageUnion); + +#[doc(hidden)] +#[repr(C)] +pub union LengthPercentageUnion { + length: LengthVariant, + percentage: PercentageVariant, + calc: CalcVariant, + tag: TagVariant, +} + +impl LengthPercentageUnion { + #[doc(hidden)] // Need to be public so that cbindgen generates it. + pub const TAG_CALC: u8 = 0; + #[doc(hidden)] + pub const TAG_LENGTH: u8 = 1; + #[doc(hidden)] + pub const TAG_PERCENTAGE: u8 = 2; + #[doc(hidden)] + pub const TAG_MASK: u8 = 0b11; +} + +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +enum Tag { + Calc = LengthPercentageUnion::TAG_CALC, + Length = LengthPercentageUnion::TAG_LENGTH, + Percentage = LengthPercentageUnion::TAG_PERCENTAGE, +} + +// All the members should be 64 bits, even in 32-bit builds. +#[allow(unused)] +unsafe fn static_assert() { + fn assert_send_and_sync<T: Send + Sync>() {} + std::mem::transmute::<u64, LengthVariant>(0u64); + std::mem::transmute::<u64, PercentageVariant>(0u64); + std::mem::transmute::<u64, CalcVariant>(0u64); + std::mem::transmute::<u64, LengthPercentage>(0u64); + assert_send_and_sync::<LengthVariant>(); + assert_send_and_sync::<PercentageVariant>(); + assert_send_and_sync::<CalcLengthPercentage>(); +} + +impl Drop for LengthPercentage { + fn drop(&mut self) { + if self.tag() == Tag::Calc { + let _ = unsafe { Box::from_raw(self.calc_ptr()) }; + } + } +} + +impl MallocSizeOf for LengthPercentage { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match self.unpack() { + Unpacked::Length(..) | Unpacked::Percentage(..) => 0, + Unpacked::Calc(c) => unsafe { ops.malloc_size_of(c) }, + } + } +} + +/// An unpacked `<length-percentage>` that borrows the `calc()` variant. +#[derive(Clone, Debug, PartialEq, ToCss)] +enum Unpacked<'a> { + Calc(&'a CalcLengthPercentage), + Length(Length), + Percentage(Percentage), +} + +/// An unpacked `<length-percentage>` that mutably borrows the `calc()` variant. +enum UnpackedMut<'a> { + Calc(&'a mut CalcLengthPercentage), + Length(Length), + Percentage(Percentage), +} + +/// An unpacked `<length-percentage>` that owns the `calc()` variant, for +/// serialization purposes. +#[derive(Deserialize, PartialEq, Serialize)] +enum Serializable { + Calc(CalcLengthPercentage), + Length(Length), + Percentage(Percentage), +} + +impl LengthPercentage { + /// 1px length value for SVG defaults + #[inline] + pub fn one() -> Self { + Self::new_length(Length::new(1.)) + } + + /// 0% + #[inline] + pub fn zero_percent() -> Self { + Self::new_percent(Percentage::zero()) + } + + fn to_calc_node(&self) -> Cow<CalcNode> { + match self.unpack() { + Unpacked::Length(l) => Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Length(l))), + Unpacked::Percentage(p) => { + Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(p))) + }, + Unpacked::Calc(p) => Cow::Borrowed(&p.node), + } + } + + /// Constructs a length value. + #[inline] + pub fn new_length(length: Length) -> Self { + let length = Self(LengthPercentageUnion { + length: LengthVariant { + tag: LengthPercentageUnion::TAG_LENGTH, + length, + }, + }); + debug_assert_eq!(length.tag(), Tag::Length); + length + } + + /// Constructs a percentage value. + #[inline] + pub fn new_percent(percentage: Percentage) -> Self { + let percent = Self(LengthPercentageUnion { + percentage: PercentageVariant { + tag: LengthPercentageUnion::TAG_PERCENTAGE, + percentage, + }, + }); + debug_assert_eq!(percent.tag(), Tag::Percentage); + percent + } + + /// Given a `LengthPercentage` value `v`, construct the value representing + /// `calc(100% - v)`. + pub fn hundred_percent_minus(v: Self, clamping_mode: AllowedNumericType) -> Self { + // TODO: This could in theory take ownership of the calc node in `v` if + // possible instead of cloning. + let mut node = v.to_calc_node().into_owned(); + node.negate(); + + let new_node = CalcNode::Sum( + vec![ + CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(Percentage::hundred())), + node, + ] + .into(), + ); + + Self::new_calc(new_node, clamping_mode) + } + + /// Given a list of `LengthPercentage` values, construct the value representing + /// `calc(100% - the sum of the list)`. + pub fn hundred_percent_minus_list(list: &[&Self], clamping_mode: AllowedNumericType) -> Self { + let mut new_list = vec![CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage( + Percentage::hundred(), + ))]; + + for lp in list.iter() { + let mut node = lp.to_calc_node().into_owned(); + node.negate(); + new_list.push(node) + } + + Self::new_calc(CalcNode::Sum(new_list.into()), clamping_mode) + } + + /// Constructs a `calc()` value. + #[inline] + pub fn new_calc(mut node: CalcNode, clamping_mode: AllowedNumericType) -> Self { + node.simplify_and_sort(); + + match node { + CalcNode::Leaf(l) => { + return match l { + CalcLengthPercentageLeaf::Length(l) => { + Self::new_length(Length::new(clamping_mode.clamp(l.px())).normalized()) + }, + CalcLengthPercentageLeaf::Percentage(p) => Self::new_percent(Percentage( + clamping_mode.clamp(crate::values::normalize(p.0)), + )), + CalcLengthPercentageLeaf::Number(number) => { + debug_assert!( + false, + "The final result of a <length-percentage> should never be a number" + ); + Self::new_length(Length::new(number)) + }, + }; + }, + _ => Self::new_calc_unchecked(Box::new(CalcLengthPercentage { + clamping_mode, + node, + })), + } + } + + /// Private version of new_calc() that constructs a calc() variant without + /// checking. + fn new_calc_unchecked(calc: Box<CalcLengthPercentage>) -> Self { + let ptr = Box::into_raw(calc); + + #[cfg(target_pointer_width = "32")] + let calc = CalcVariant { + tag: LengthPercentageUnion::TAG_CALC, + ptr, + }; + + #[cfg(target_pointer_width = "64")] + let calc = CalcVariant { + #[cfg(target_endian = "little")] + ptr: ptr as usize, + #[cfg(target_endian = "big")] + ptr: (ptr as usize).swap_bytes(), + }; + + let calc = Self(LengthPercentageUnion { calc }); + debug_assert_eq!(calc.tag(), Tag::Calc); + calc + } + + #[inline] + fn tag(&self) -> Tag { + match unsafe { self.0.tag.tag & LengthPercentageUnion::TAG_MASK } { + LengthPercentageUnion::TAG_CALC => Tag::Calc, + LengthPercentageUnion::TAG_LENGTH => Tag::Length, + LengthPercentageUnion::TAG_PERCENTAGE => Tag::Percentage, + _ => unsafe { debug_unreachable!("Bogus tag?") }, + } + } + + #[inline] + fn unpack_mut<'a>(&'a mut self) -> UnpackedMut<'a> { + unsafe { + match self.tag() { + Tag::Calc => UnpackedMut::Calc(&mut *self.calc_ptr()), + Tag::Length => UnpackedMut::Length(self.0.length.length), + Tag::Percentage => UnpackedMut::Percentage(self.0.percentage.percentage), + } + } + } + + #[inline] + fn unpack<'a>(&'a self) -> Unpacked<'a> { + unsafe { + match self.tag() { + Tag::Calc => Unpacked::Calc(&*self.calc_ptr()), + Tag::Length => Unpacked::Length(self.0.length.length), + Tag::Percentage => Unpacked::Percentage(self.0.percentage.percentage), + } + } + } + + #[inline] + unsafe fn calc_ptr(&self) -> *mut CalcLengthPercentage { + #[cfg(not(all(target_endian = "big", target_pointer_width = "64")))] + { + self.0.calc.ptr as *mut _ + } + #[cfg(all(target_endian = "big", target_pointer_width = "64"))] + { + self.0.calc.ptr.swap_bytes() as *mut _ + } + } + + #[inline] + fn to_serializable(&self) -> Serializable { + match self.unpack() { + Unpacked::Calc(c) => Serializable::Calc(c.clone()), + Unpacked::Length(l) => Serializable::Length(l), + Unpacked::Percentage(p) => Serializable::Percentage(p), + } + } + + #[inline] + fn from_serializable(s: Serializable) -> Self { + match s { + Serializable::Calc(c) => Self::new_calc_unchecked(Box::new(c)), + Serializable::Length(l) => Self::new_length(l), + Serializable::Percentage(p) => Self::new_percent(p), + } + } + + /// Returns true if the computed value is absolute 0 or 0%. + #[inline] + pub fn is_definitely_zero(&self) -> bool { + match self.unpack() { + Unpacked::Length(l) => l.px() == 0.0, + Unpacked::Percentage(p) => p.0 == 0.0, + Unpacked::Calc(..) => false, + } + } + + /// Resolves the percentage. + #[inline] + pub fn resolve(&self, basis: Length) -> Length { + match self.unpack() { + Unpacked::Length(l) => l, + Unpacked::Percentage(p) => (basis * p.0).normalized(), + Unpacked::Calc(ref c) => c.resolve(basis), + } + } + + /// Resolves the percentage. Just an alias of resolve(). + #[inline] + pub fn percentage_relative_to(&self, basis: Length) -> Length { + self.resolve(basis) + } + + /// Return whether there's any percentage in this value. + #[inline] + pub fn has_percentage(&self) -> bool { + match self.unpack() { + Unpacked::Length(..) => false, + Unpacked::Percentage(..) | Unpacked::Calc(..) => true, + } + } + + /// Converts to a `<length>` if possible. + pub fn to_length(&self) -> Option<Length> { + match self.unpack() { + Unpacked::Length(l) => Some(l), + Unpacked::Percentage(..) | Unpacked::Calc(..) => { + debug_assert!(self.has_percentage()); + return None; + }, + } + } + + /// Converts to a `<percentage>` if possible. + #[inline] + pub fn to_percentage(&self) -> Option<Percentage> { + match self.unpack() { + Unpacked::Percentage(p) => Some(p), + Unpacked::Length(..) | Unpacked::Calc(..) => None, + } + } + + /// Returns the used value. + #[inline] + pub fn to_used_value(&self, containing_length: Au) -> Au { + Au::from(self.to_pixel_length(containing_length)) + } + + /// Returns the used value as CSSPixelLength. + #[inline] + pub fn to_pixel_length(&self, containing_length: Au) -> Length { + self.resolve(containing_length.into()) + } + + /// Convert the computed value into used value. + #[inline] + pub fn maybe_to_used_value(&self, container_len: Option<Length>) -> Option<Au> { + self.maybe_percentage_relative_to(container_len) + .map(Au::from) + } + + /// If there are special rules for computing percentages in a value (e.g. + /// the height property), they apply whenever a calc() expression contains + /// percentages. + pub fn maybe_percentage_relative_to(&self, container_len: Option<Length>) -> Option<Length> { + if let Unpacked::Length(l) = self.unpack() { + return Some(l); + } + Some(self.resolve(container_len?)) + } + + /// Returns the clamped non-negative values. + #[inline] + pub fn clamp_to_non_negative(mut self) -> Self { + match self.unpack_mut() { + UnpackedMut::Length(l) => Self::new_length(l.clamp_to_non_negative()), + UnpackedMut::Percentage(p) => Self::new_percent(p.clamp_to_non_negative()), + UnpackedMut::Calc(ref mut c) => { + c.clamping_mode = AllowedNumericType::NonNegative; + self + }, + } + } +} + +impl PartialEq for LengthPercentage { + fn eq(&self, other: &Self) -> bool { + self.unpack() == other.unpack() + } +} + +impl fmt::Debug for LengthPercentage { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + self.unpack().fmt(formatter) + } +} + +impl ToAnimatedZero for LengthPercentage { + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(match self.unpack() { + Unpacked::Length(l) => Self::new_length(l.to_animated_zero()?), + Unpacked::Percentage(p) => Self::new_percent(p.to_animated_zero()?), + Unpacked::Calc(c) => Self::new_calc_unchecked(Box::new(c.to_animated_zero()?)), + }) + } +} + +impl Clone for LengthPercentage { + fn clone(&self) -> Self { + match self.unpack() { + Unpacked::Length(l) => Self::new_length(l), + Unpacked::Percentage(p) => Self::new_percent(p), + Unpacked::Calc(c) => Self::new_calc_unchecked(Box::new(c.clone())), + } + } +} + +impl ToComputedValue for specified::LengthPercentage { + type ComputedValue = LengthPercentage; + + fn to_computed_value(&self, context: &Context) -> LengthPercentage { + match *self { + specified::LengthPercentage::Length(ref value) => { + LengthPercentage::new_length(value.to_computed_value(context)) + }, + specified::LengthPercentage::Percentage(value) => LengthPercentage::new_percent(value), + specified::LengthPercentage::Calc(ref calc) => (**calc).to_computed_value(context), + } + } + + fn from_computed_value(computed: &LengthPercentage) -> Self { + match computed.unpack() { + Unpacked::Length(ref l) => { + specified::LengthPercentage::Length(ToComputedValue::from_computed_value(l)) + }, + Unpacked::Percentage(p) => specified::LengthPercentage::Percentage(p), + Unpacked::Calc(c) => { + // We simplify before constructing the LengthPercentage if + // needed, so this is always fine. + specified::LengthPercentage::Calc(Box::new( + specified::CalcLengthPercentage::from_computed_value(c), + )) + }, + } + } +} + +impl ComputeSquaredDistance for LengthPercentage { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + // A somewhat arbitrary base, it doesn't really make sense to mix + // lengths with percentages, but we can't do much better here, and this + // ensures that the distance between length-only and percentage-only + // lengths makes sense. + let basis = Length::new(100.); + self.resolve(basis) + .compute_squared_distance(&other.resolve(basis)) + } +} + +impl ToCss for LengthPercentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.unpack().to_css(dest) + } +} + +impl Zero for LengthPercentage { + fn zero() -> Self { + LengthPercentage::new_length(Length::zero()) + } + + #[inline] + fn is_zero(&self) -> bool { + self.is_definitely_zero() + } +} + +impl ZeroNoPercent for LengthPercentage { + #[inline] + fn is_zero_no_percent(&self) -> bool { + self.is_definitely_zero() && !self.has_percentage() + } +} + +impl Serialize for LengthPercentage { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: serde::Serializer, + { + self.to_serializable().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for LengthPercentage { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::Deserializer<'de>, + { + Ok(Self::from_serializable(Serializable::deserialize( + deserializer, + )?)) + } +} + +/// The leaves of a `<length-percentage>` calc expression. +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum CalcLengthPercentageLeaf { + Length(Length), + Percentage(Percentage), + Number(f32), +} + +impl CalcLengthPercentageLeaf { + fn is_zero_length(&self) -> bool { + match *self { + Self::Length(ref l) => l.is_zero(), + Self::Percentage(..) => false, + Self::Number(..) => false, + } + } +} + +impl calc::CalcNodeLeaf for CalcLengthPercentageLeaf { + fn unit(&self) -> CalcUnits { + match self { + Self::Length(_) => CalcUnits::LENGTH, + Self::Percentage(_) => CalcUnits::PERCENTAGE, + Self::Number(_) => CalcUnits::empty(), + } + } + + fn unitless_value(&self) -> f32 { + match *self { + Self::Length(ref l) => l.px(), + Self::Percentage(ref p) => p.0, + Self::Number(n) => n, + } + } + + fn new_number(value: f32) -> Self { + Self::Number(value) + } + + fn as_number(&self) -> Option<f32> { + match *self { + Self::Length(_) | Self::Percentage(_) => None, + Self::Number(value) => Some(value), + } + } + + fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<std::cmp::Ordering> { + use self::CalcLengthPercentageLeaf::*; + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) { + return None; + } + + let self_negative = self.is_negative(); + if self_negative != other.is_negative() { + return Some(if self_negative { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater }); + } + + match (self, other) { + (&Length(ref one), &Length(ref other)) => one.partial_cmp(other), + (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other), + (&Number(ref one), &Number(ref other)) => one.partial_cmp(other), + _ => unsafe { + match *self { + Length(..) | Percentage(..) | Number(..) => {}, + } + debug_unreachable!("Forgot to handle unit in compare()") + }, + } + } + + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { + use self::CalcLengthPercentageLeaf::*; + + // 0px plus anything else is equal to the right hand side. + if self.is_zero_length() { + *self = other.clone(); + return Ok(()); + } + + if other.is_zero_length() { + return Ok(()); + } + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + match (self, other) { + (&mut Length(ref mut one), &Length(ref other)) => { + *one += *other; + }, + (&mut Percentage(ref mut one), &Percentage(ref other)) => { + one.0 += other.0; + }, + (&mut Number(ref mut one), &Number(ref other)) => { + *one += *other; + }, + _ => unsafe { + match *other { + Length(..) | Percentage(..) | Number(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum_in_place()") + }, + } + + Ok(()) + } + + fn try_product_in_place(&mut self, other: &mut Self) -> bool { + if let Self::Number(ref mut left) = *self { + if let Self::Number(ref right) = *other { + // Both sides are numbers, so we can just modify the left side. + *left *= *right; + true + } else { + // The right side is not a number, so the result should be in the units of the right + // side. + other.map(|v| v * *left); + std::mem::swap(self, other); + true + } + } else if let Self::Number(ref right) = *other { + // The left side is not a number, but the right side is, so the result is the left + // side unit. + self.map(|v| v * *right); + true + } else { + // Neither side is a number, so a product is not possible. + false + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::CalcLengthPercentageLeaf::*; + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + Ok(match (self, other) { + (&Length(ref one), &Length(ref other)) => { + Length(super::Length::new(op(one.px(), other.px()))) + }, + (&Percentage(one), &Percentage(other)) => { + Self::Percentage(super::Percentage(op(one.0, other.0))) + }, + (&Number(one), &Number(other)) => Self::Number(op(one, other)), + _ => unsafe { + match *self { + Length(..) | Percentage(..) | Number(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + fn map(&mut self, mut op: impl FnMut(f32) -> f32) { + match self { + Self::Length(value) => { + *value = Length::new(op(value.px())); + }, + Self::Percentage(value) => { + *value = Percentage(op(value.0)); + }, + Self::Number(value) => { + *value = op(*value); + }, + } + } + + fn simplify(&mut self) {} + + fn sort_key(&self) -> calc::SortKey { + match *self { + Self::Length(..) => calc::SortKey::Px, + Self::Percentage(..) => calc::SortKey::Percentage, + Self::Number(..) => calc::SortKey::Number, + } + } +} + +/// The computed version of a calc() node for `<length-percentage>` values. +pub type CalcNode = calc::GenericCalcNode<CalcLengthPercentageLeaf>; + +/// The representation of a calc() function with mixed lengths and percentages. +#[derive( + Clone, Debug, Deserialize, MallocSizeOf, Serialize, ToAnimatedZero, ToResolvedValue, ToCss, +)] +#[repr(C)] +pub struct CalcLengthPercentage { + #[animation(constant)] + #[css(skip)] + clamping_mode: AllowedNumericType, + node: CalcNode, +} + +impl CalcLengthPercentage { + /// Resolves the percentage. + #[inline] + pub fn resolve(&self, basis: Length) -> Length { + // unwrap() is fine because the conversion below is infallible. + if let CalcLengthPercentageLeaf::Length(px) = self + .node + .resolve_map(|leaf| { + Ok(if let CalcLengthPercentageLeaf::Percentage(p) = leaf { + CalcLengthPercentageLeaf::Length(Length::new(basis.px() * p.0)) + } else { + leaf.clone() + }) + }) + .unwrap() + { + Length::new(self.clamping_mode.clamp(px.px())).normalized() + } else { + unreachable!("resolve_map should turn percentages to lengths, and parsing should ensure that we don't end up with a number"); + } + } +} + +// NOTE(emilio): We don't compare `clamping_mode` since we want to preserve the +// invariant that `from_computed_value(length).to_computed_value(..) == length`. +// +// Right now for e.g. a non-negative length, we set clamping_mode to `All` +// unconditionally for non-calc values, and to `NonNegative` for calc. +// +// If we determine that it's sound, from_computed_value() can generate an +// absolute length, which then would get `All` as the clamping mode. +// +// We may want to just eagerly-detect whether we can clamp in +// `LengthPercentage::new` and switch to `AllowedNumericType::NonNegative` then, +// maybe. +impl PartialEq for CalcLengthPercentage { + fn eq(&self, other: &Self) -> bool { + self.node == other.node + } +} + +impl specified::CalcLengthPercentage { + /// Compute the value, zooming any absolute units by the zoom function. + fn to_computed_value_with_zoom<F>( + &self, + context: &Context, + zoom_fn: F, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> LengthPercentage + where + F: Fn(Length) -> Length, + { + use crate::values::specified::calc::Leaf; + + let node = self.node.map_leaves(|leaf| match *leaf { + Leaf::Percentage(p) => CalcLengthPercentageLeaf::Percentage(Percentage(p)), + Leaf::Length(l) => CalcLengthPercentageLeaf::Length({ + let result = + l.to_computed_value_with_base_size(context, base_size, line_height_base); + if l.should_zoom_text() { + zoom_fn(result) + } else { + result + } + }), + Leaf::Number(n) => CalcLengthPercentageLeaf::Number(n), + Leaf::Angle(..) | Leaf::Time(..) | Leaf::Resolution(..) => { + unreachable!("Shouldn't have parsed") + }, + }); + + LengthPercentage::new_calc(node, self.clamping_mode) + } + + /// Compute font-size or line-height taking into account text-zoom if necessary. + pub fn to_computed_value_zoomed( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> LengthPercentage { + self.to_computed_value_with_zoom( + context, + |abs| context.maybe_zoom_text(abs), + base_size, + line_height_base, + ) + } + + /// Compute the value into pixel length as CSSFloat without context, + /// so it returns Err(()) if there is any non-absolute unit. + pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> { + use crate::values::specified::calc::Leaf; + use crate::values::specified::length::NoCalcLength; + + // Simplification should've turned this into an absolute length, + // otherwise it wouldn't have been able to. + match self.node { + calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()), + _ => Err(()), + } + } + + /// Compute the value into pixel length as CSSFloat, using the get_font_metrics function + /// if provided to resolve font-relative dimensions. + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>, + ) -> Result<CSSFloat, ()> { + use crate::values::specified::calc::Leaf; + use crate::values::specified::length::NoCalcLength; + + match self.node { + calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()), + calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::FontRelative(ref l))) => { + if let Some(getter) = get_font_metrics { + l.to_computed_pixel_length_with_font_metrics(getter) + } else { + Err(()) + } + }, + _ => Err(()), + } + } + + /// Compute the calc using the current font-size and line-height. (and without text-zoom). + pub fn to_computed_value(&self, context: &Context) -> LengthPercentage { + self.to_computed_value_with_zoom( + context, + |abs| abs, + FontBaseSize::CurrentStyle, + LineHeightBase::CurrentStyle, + ) + } + + #[inline] + fn from_computed_value(computed: &CalcLengthPercentage) -> Self { + use crate::values::specified::calc::Leaf; + use crate::values::specified::length::NoCalcLength; + + specified::CalcLengthPercentage { + clamping_mode: computed.clamping_mode, + node: computed.node.map_leaves(|l| match l { + CalcLengthPercentageLeaf::Length(ref l) => { + Leaf::Length(NoCalcLength::from_px(l.px())) + }, + CalcLengthPercentageLeaf::Percentage(ref p) => Leaf::Percentage(p.0), + CalcLengthPercentageLeaf::Number(n) => Leaf::Number(*n), + }), + } + } +} + +/// https://drafts.csswg.org/css-transitions/#animtype-lpcalc +/// https://drafts.csswg.org/css-values-4/#combine-math +/// https://drafts.csswg.org/css-values-4/#combine-mixed +impl Animate for LengthPercentage { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + Ok(match (self.unpack(), other.unpack()) { + (Unpacked::Length(one), Unpacked::Length(other)) => { + Self::new_length(one.animate(&other, procedure)?) + }, + (Unpacked::Percentage(one), Unpacked::Percentage(other)) => { + Self::new_percent(one.animate(&other, procedure)?) + }, + _ => { + use calc::CalcNodeLeaf; + + fn product_with(mut node: CalcNode, product: f32) -> CalcNode { + let mut number = CalcNode::Leaf(CalcLengthPercentageLeaf::new_number(product)); + if !node.try_product_in_place(&mut number) { + CalcNode::Product(vec![node, number].into()) + } else { + node + } + } + + let (l, r) = procedure.weights(); + let one = product_with(self.to_calc_node().into_owned(), l as f32); + let other = product_with(other.to_calc_node().into_owned(), r as f32); + + Self::new_calc( + CalcNode::Sum(vec![one, other].into()), + AllowedNumericType::All, + ) + }, + }) + } +} + +/// A wrapper of LengthPercentage, whose value must be >= 0. +pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>; + +impl ToAnimatedValue for NonNegativeLengthPercentage { + type AnimatedValue = LengthPercentage; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + NonNegative(animated.clamp_to_non_negative()) + } +} + +impl NonNegativeLengthPercentage { + /// Returns true if the computed value is absolute 0 or 0%. + #[inline] + pub fn is_definitely_zero(&self) -> bool { + self.0.is_definitely_zero() + } + + /// Returns the used value. + #[inline] + pub fn to_used_value(&self, containing_length: Au) -> Au { + let resolved = self.0.to_used_value(containing_length); + std::cmp::max(resolved, Au(0)) + } + + /// Convert the computed value into used value. + #[inline] + pub fn maybe_to_used_value(&self, containing_length: Option<Au>) -> Option<Au> { + let resolved = self + .0 + .maybe_to_used_value(containing_length.map(|v| v.into()))?; + Some(std::cmp::max(resolved, Au(0))) + } +} diff --git a/servo/components/style/values/computed/list.rs b/servo/components/style/values/computed/list.rs new file mode 100644 index 0000000000..3e5d1eb220 --- /dev/null +++ b/servo/components/style/values/computed/list.rs @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `list` computed values. + +#[cfg(feature = "gecko")] +pub use crate::values::specified::list::ListStyleType; +pub use crate::values::specified::list::Quotes; + +impl Quotes { + /// Initial value for `quotes`. + #[inline] + pub fn get_initial_value() -> Quotes { + Quotes::Auto + } +} diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs new file mode 100644 index 0000000000..de5db2cdab --- /dev/null +++ b/servo/components/style/values/computed/mod.rs @@ -0,0 +1,1035 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values. + +use self::transform::DirectionVector; +use super::animated::ToAnimatedValue; +use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent; +use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks; +use super::generics::grid::{GenericGridLine, GenericTrackBreadth}; +use super::generics::grid::{GenericTrackSize, TrackList as GenericTrackList}; +use super::generics::transform::IsParallelTo; +use super::generics::{self, GreaterThanOrEqualToOne, NonNegative, ZeroToOne}; +use super::specified; +use super::{CSSFloat, CSSInteger}; +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::QuirksMode; +use crate::custom_properties::ComputedCustomProperties; +use crate::font_metrics::{FontMetrics, FontMetricsOrientation}; +use crate::media_queries::Device; +#[cfg(feature = "gecko")] +use crate::properties; +use crate::properties::{ComputedValues, StyleBuilder}; +use crate::rule_cache::RuleCacheConditions; +use crate::stylesheets::container_rule::{ + ContainerInfo, ContainerSizeQuery, ContainerSizeQueryResult, +}; +use crate::stylist::Stylist; +use crate::values::specified::length::FontBaseSize; +use crate::{ArcSlice, Atom, One}; +use euclid::{default, Point2D, Rect, Size2D}; +use servo_arc::Arc; +use std::cell::RefCell; +use std::cmp; +use std::f32; +use std::ops::{Add, Sub}; + +#[cfg(feature = "gecko")] +pub use self::align::{ + AlignContent, AlignItems, AlignTracks, JustifyContent, JustifyItems, JustifyTracks, + SelfAlignment, +}; +#[cfg(feature = "gecko")] +pub use self::align::{AlignSelf, JustifySelf}; +pub use self::angle::Angle; +pub use self::animation::{ + AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState, + AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis, + ScrollTimelineName, TransitionProperty, ViewTimelineInset +}; +pub use self::background::{BackgroundRepeat, BackgroundSize}; +pub use self::basic_shape::FillRule; +pub use self::border::{ + BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice, + BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, LineWidth, +}; +pub use self::box_::{ + Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize, + ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow, + OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign, + ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, + TouchAction, VerticalAlign, WillChange, Zoom, +}; +pub use self::color::{ + Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust, +}; +pub use self::column::ColumnCount; +pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet}; +pub use self::easing::TimingFunction; +pub use self::effects::{BoxShadow, Filter, SimpleShadow}; +pub use self::flex::FlexBasis; +pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle}; +pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; +pub use self::font::{FontSize, FontSizeAdjust, FontStretch, FontSynthesis, LineHeight}; +pub use self::font::{FontVariantAlternates, FontWeight}; +pub use self::font::{FontVariantEastAsian, FontVariationSettings}; +pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale}; +pub use self::image::{Gradient, Image, ImageRendering, LineDirection}; +pub use self::length::{CSSPixelLength, NonNegativeLength}; +pub use self::length::{Length, LengthOrNumber, LengthPercentage, NonNegativeLengthOrNumber}; +pub use self::length::{LengthOrAuto, LengthPercentageOrAuto, MaxSize, Size}; +pub use self::length::{NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto}; +#[cfg(feature = "gecko")] +pub use self::list::ListStyleType; +pub use self::list::Quotes; +pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate}; +pub use self::outline::OutlineStyle; +pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize}; +pub use self::percentage::{NonNegativePercentage, Percentage}; +pub use self::position::AspectRatio; +pub use self::position::{ + GridAutoFlow, GridTemplateAreas, MasonryAutoFlow, Position, PositionOrAuto, ZIndex, +}; +pub use self::ratio::Ratio; +pub use self::rect::NonNegativeLengthOrNumberRect; +pub use self::resolution::Resolution; +pub use self::svg::{DProperty, MozContextProperties}; +pub use self::svg::{SVGLength, SVGOpacity, SVGPaint, SVGPaintKind}; +pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth}; +pub use self::text::HyphenateCharacter; +pub use self::text::TextUnderlinePosition; +pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextIndent}; +pub use self::text::{OverflowWrap, RubyPosition, TextOverflow, WordBreak, WordSpacing}; +pub use self::text::{TextAlign, TextAlignLast, TextEmphasisPosition, TextEmphasisStyle}; +pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify}; +pub use self::time::Time; +pub use self::transform::{Rotate, Scale, Transform, TransformBox, TransformOperation}; +pub use self::transform::{TransformOrigin, TransformStyle, Translate}; +#[cfg(feature = "gecko")] +pub use self::ui::CursorImage; +pub use self::ui::{BoolInteger, Cursor, UserSelect}; +pub use super::specified::TextTransform; +pub use super::specified::ViewportVariant; +pub use super::specified::{BorderStyle, TextDecorationLine}; +pub use app_units::Au; + +#[cfg(feature = "gecko")] +pub mod align; +pub mod angle; +pub mod animation; +pub mod background; +pub mod basic_shape; +pub mod border; +#[path = "box.rs"] +pub mod box_; +pub mod color; +pub mod column; +pub mod counters; +pub mod easing; +pub mod effects; +pub mod flex; +pub mod font; +pub mod image; +pub mod length; +pub mod length_percentage; +pub mod list; +pub mod motion; +pub mod outline; +pub mod page; +pub mod percentage; +pub mod position; +pub mod ratio; +pub mod rect; +pub mod resolution; +pub mod svg; +pub mod table; +pub mod text; +pub mod time; +pub mod transform; +pub mod ui; +pub mod url; + +/// A `Context` is all the data a specified value could ever need to compute +/// itself and be transformed to a computed value. +pub struct Context<'a> { + /// Values accessed through this need to be in the properties "computed + /// early": color, text-decoration, font-size, display, position, float, + /// border-*-style, outline-style, font-family, writing-mode... + pub builder: StyleBuilder<'a>, + + /// A cached computed system font value, for use by gecko. + /// + /// See properties/longhands/font.mako.rs + #[cfg(feature = "gecko")] + pub cached_system_font: Option<properties::longhands::system_font::ComputedSystemFont>, + + /// A dummy option for servo so initializing a computed::Context isn't + /// painful. + /// + /// TODO(emilio): Make constructors for Context, and drop this. + #[cfg(feature = "servo")] + pub cached_system_font: Option<()>, + + /// Whether or not we are computing the media list in a media query. + pub in_media_query: bool, + + /// Whether or not we are computing the container query condition. + pub in_container_query: bool, + + /// The quirks mode of this context. + pub quirks_mode: QuirksMode, + + /// Whether this computation is being done for a SMIL animation. + /// + /// This is used to allow certain properties to generate out-of-range + /// values, which SMIL allows. + pub for_smil_animation: bool, + + /// Returns the container information to evaluate a given container query. + pub container_info: Option<ContainerInfo>, + + /// Whether we're computing a value for a non-inherited property. + /// False if we are computed a value for an inherited property or not computing for a property + /// at all (e.g. in a media query evaluation). + pub for_non_inherited_property: bool, + + /// The conditions to cache a rule node on the rule cache. + /// + /// FIXME(emilio): Drop the refcell. + pub rule_cache_conditions: RefCell<&'a mut RuleCacheConditions>, + + /// Container size query for this context. + container_size_query: RefCell<ContainerSizeQuery<'a>>, +} + +impl<'a> Context<'a> { + /// Lazily evaluate the container size query, returning the result. + pub fn get_container_size_query(&self) -> ContainerSizeQueryResult { + let mut resolved = self.container_size_query.borrow_mut(); + resolved.get().clone() + } + + /// Creates a suitable context for media query evaluation, in which + /// font-relative units compute against the system_font, and executes `f` + /// with it. + pub fn for_media_query_evaluation<F, R>(device: &Device, quirks_mode: QuirksMode, f: F) -> R + where + F: FnOnce(&Context) -> R, + { + let mut conditions = RuleCacheConditions::default(); + let context = Context { + builder: StyleBuilder::for_inheritance(device, None, None, None), + cached_system_font: None, + in_media_query: true, + in_container_query: false, + quirks_mode, + for_smil_animation: false, + container_info: None, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(&mut conditions), + container_size_query: RefCell::new(ContainerSizeQuery::none()), + }; + f(&context) + } + + /// Creates a suitable context for container query evaluation for the style + /// specified. + pub fn for_container_query_evaluation<F, R>( + device: &Device, + stylist: Option<&Stylist>, + container_info_and_style: Option<(ContainerInfo, Arc<ComputedValues>)>, + container_size_query: ContainerSizeQuery, + f: F, + ) -> R + where + F: FnOnce(&Context) -> R, + { + let mut conditions = RuleCacheConditions::default(); + + let (container_info, style) = match container_info_and_style { + Some((ci, s)) => (Some(ci), Some(s)), + None => (None, None), + }; + + let style = style.as_ref().map(|s| &**s); + let quirks_mode = device.quirks_mode(); + let context = Context { + builder: StyleBuilder::for_inheritance(device, stylist, style, None), + cached_system_font: None, + in_media_query: false, + in_container_query: true, + quirks_mode, + for_smil_animation: false, + container_info, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(&mut conditions), + container_size_query: RefCell::new(container_size_query), + }; + + f(&context) + } + + /// Creates a context suitable for more general cases. + pub fn new( + builder: StyleBuilder<'a>, + quirks_mode: QuirksMode, + rule_cache_conditions: &'a mut RuleCacheConditions, + container_size_query: ContainerSizeQuery<'a>, + ) -> Self { + Self { + builder, + cached_system_font: None, + in_media_query: false, + in_container_query: false, + quirks_mode, + container_info: None, + for_smil_animation: false, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(rule_cache_conditions), + container_size_query: RefCell::new(container_size_query), + } + } + + /// Creates a context suitable for computing animations. + pub fn new_for_animation( + builder: StyleBuilder<'a>, + for_smil_animation: bool, + quirks_mode: QuirksMode, + rule_cache_conditions: &'a mut RuleCacheConditions, + container_size_query: ContainerSizeQuery<'a>, + ) -> Self { + Self { + builder, + cached_system_font: None, + in_media_query: false, + in_container_query: false, + quirks_mode, + container_info: None, + for_smil_animation, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(rule_cache_conditions), + container_size_query: RefCell::new(container_size_query), + } + } + + /// Creates a context suitable for computing the initial value of @property. + pub fn new_for_initial_at_property_value( + stylist: &'a Stylist, + rule_cache_conditions: &'a mut RuleCacheConditions, + ) -> Self { + Self { + builder: StyleBuilder::new(stylist.device(), Some(stylist), None, None, None, false), + cached_system_font: None, + // Because font-relative values are disallowed in @property initial values, we do not + // need to keep track of whether we're in a media query, whether we're in a container + // query, and so on. + in_media_query: false, + in_container_query: false, + quirks_mode: stylist.quirks_mode(), + container_info: None, + for_smil_animation: false, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(rule_cache_conditions), + container_size_query: RefCell::new(ContainerSizeQuery::none()), + } + } + + /// The current device. + pub fn device(&self) -> &Device { + self.builder.device + } + + /// Get the inherited custom properties map. + pub fn inherited_custom_properties(&self) -> &ComputedCustomProperties { + &self.builder.inherited_custom_properties() + } + + /// Whether the style is for the root element. + pub fn is_root_element(&self) -> bool { + self.builder.is_root_element + } + + /// Queries font metrics. + pub fn query_font_metrics( + &self, + base_size: FontBaseSize, + orientation: FontMetricsOrientation, + retrieve_math_scales: bool, + ) -> FontMetrics { + if self.for_non_inherited_property { + self.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + self.builder.add_flags(match base_size { + FontBaseSize::CurrentStyle => ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS, + FontBaseSize::InheritedStyle => ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS, + }); + let size = base_size.resolve(self).used_size(); + let style = self.style(); + + let (wm, font) = match base_size { + FontBaseSize::CurrentStyle => (style.writing_mode, style.get_font()), + // This is only used for font-size computation. + FontBaseSize::InheritedStyle => { + (*style.inherited_writing_mode(), style.get_parent_font()) + }, + }; + + let vertical = match orientation { + FontMetricsOrientation::MatchContextPreferHorizontal => { + wm.is_vertical() && wm.is_upright() + }, + FontMetricsOrientation::MatchContextPreferVertical => wm.is_text_vertical(), + FontMetricsOrientation::Horizontal => false, + }; + self.device().query_font_metrics( + vertical, + font, + size, + self.in_media_or_container_query(), + retrieve_math_scales, + ) + } + + /// The current viewport size, used to resolve viewport units. + pub fn viewport_size_for_viewport_unit_resolution( + &self, + variant: ViewportVariant, + ) -> default::Size2D<Au> { + self.builder + .add_flags(ComputedValueFlags::USES_VIEWPORT_UNITS); + self.builder + .device + .au_viewport_size_for_viewport_unit_resolution(variant) + } + + /// Whether we're in a media or container query. + pub fn in_media_or_container_query(&self) -> bool { + self.in_media_query || self.in_container_query + } + + /// The default computed style we're getting our reset style from. + pub fn default_style(&self) -> &ComputedValues { + self.builder.default_style() + } + + /// The current style. + pub fn style(&self) -> &StyleBuilder { + &self.builder + } + + /// Apply text-zoom if enabled. + #[cfg(feature = "gecko")] + pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength { + if self + .style() + .get_font() + .clone__x_text_scale() + .text_zoom_enabled() + { + self.device().zoom_text(size) + } else { + size + } + } + + /// (Servo doesn't do text-zoom) + #[cfg(feature = "servo")] + pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength { + size + } +} + +/// An iterator over a slice of computed values +#[derive(Clone)] +pub struct ComputedVecIter<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> { + cx: &'cx Context<'cx_a>, + values: &'a [S], +} + +impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ComputedVecIter<'a, 'cx, 'cx_a, S> { + /// Construct an iterator from a slice of specified values and a context + pub fn new(cx: &'cx Context<'cx_a>, values: &'a [S]) -> Self { + ComputedVecIter { cx, values } + } +} + +impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ExactSizeIterator + for ComputedVecIter<'a, 'cx, 'cx_a, S> +{ + fn len(&self) -> usize { + self.values.len() + } +} + +impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> Iterator for ComputedVecIter<'a, 'cx, 'cx_a, S> { + type Item = S::ComputedValue; + fn next(&mut self) -> Option<Self::Item> { + if let Some((next, rest)) = self.values.split_first() { + let ret = next.to_computed_value(self.cx); + self.values = rest; + Some(ret) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option<usize>) { + (self.values.len(), Some(self.values.len())) + } +} + +/// A trait to represent the conversion between computed and specified values. +/// +/// This trait is derivable with `#[derive(ToComputedValue)]`. The derived +/// implementation just calls `ToComputedValue::to_computed_value` on each field +/// of the passed value. The deriving code assumes that if the type isn't +/// generic, then the trait can be implemented as simple `Clone::clone` calls, +/// this means that a manual implementation with `ComputedValue = Self` is bogus +/// if it returns anything else than a clone. +pub trait ToComputedValue { + /// The computed value type we're going to be converted to. + type ComputedValue; + + /// Convert a specified value to a computed value, using itself and the data + /// inside the `Context`. + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue; + + /// Convert a computed value to specified value form. + /// + /// This will be used for recascading during animation. + /// Such from_computed_valued values should recompute to the same value. + fn from_computed_value(computed: &Self::ComputedValue) -> Self; +} + +impl<A, B> ToComputedValue for (A, B) +where + A: ToComputedValue, + B: ToComputedValue, +{ + type ComputedValue = ( + <A as ToComputedValue>::ComputedValue, + <B as ToComputedValue>::ComputedValue, + ); + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ( + self.0.to_computed_value(context), + self.1.to_computed_value(context), + ) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + ( + A::from_computed_value(&computed.0), + B::from_computed_value(&computed.1), + ) + } +} + +impl<T> ToComputedValue for Option<T> +where + T: ToComputedValue, +{ + type ComputedValue = Option<<T as ToComputedValue>::ComputedValue>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + self.as_ref().map(|item| item.to_computed_value(context)) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + computed.as_ref().map(T::from_computed_value) + } +} + +impl<T> ToComputedValue for default::Size2D<T> +where + T: ToComputedValue, +{ + type ComputedValue = default::Size2D<<T as ToComputedValue>::ComputedValue>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + Size2D::new( + self.width.to_computed_value(context), + self.height.to_computed_value(context), + ) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Size2D::new( + T::from_computed_value(&computed.width), + T::from_computed_value(&computed.height), + ) + } +} + +impl<T> ToComputedValue for Vec<T> +where + T: ToComputedValue, +{ + type ComputedValue = Vec<<T as ToComputedValue>::ComputedValue>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + self.iter() + .map(|item| item.to_computed_value(context)) + .collect() + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + computed.iter().map(T::from_computed_value).collect() + } +} + +impl<T> ToComputedValue for Box<T> +where + T: ToComputedValue, +{ + type ComputedValue = Box<<T as ToComputedValue>::ComputedValue>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + Box::new(T::to_computed_value(self, context)) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Box::new(T::from_computed_value(computed)) + } +} + +impl<T> ToComputedValue for Box<[T]> +where + T: ToComputedValue, +{ + type ComputedValue = Box<[<T as ToComputedValue>::ComputedValue]>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + self.iter() + .map(|item| item.to_computed_value(context)) + .collect::<Vec<_>>() + .into_boxed_slice() + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + computed + .iter() + .map(T::from_computed_value) + .collect::<Vec<_>>() + .into_boxed_slice() + } +} + +impl<T> ToComputedValue for crate::OwnedSlice<T> +where + T: ToComputedValue, +{ + type ComputedValue = crate::OwnedSlice<<T as ToComputedValue>::ComputedValue>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + self.iter() + .map(|item| item.to_computed_value(context)) + .collect() + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + computed.iter().map(T::from_computed_value).collect() + } +} + +// NOTE(emilio): This is implementable more generically, but it's unlikely +// what you want there, as it forces you to have an extra allocation. +// +// We could do that if needed, ideally with specialization for the case where +// ComputedValue = T. But we don't need it for now. +impl<T> ToComputedValue for Arc<T> +where + T: ToComputedValue<ComputedValue = T>, +{ + type ComputedValue = Self; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self { + self.clone() + } + + #[inline] + fn from_computed_value(computed: &Self) -> Self { + computed.clone() + } +} + +// Same caveat as above applies. +impl<T> ToComputedValue for ArcSlice<T> +where + T: ToComputedValue<ComputedValue = T>, +{ + type ComputedValue = Self; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self { + self.clone() + } + + #[inline] + fn from_computed_value(computed: &Self) -> Self { + computed.clone() + } +} + +trivial_to_computed_value!(()); +trivial_to_computed_value!(bool); +trivial_to_computed_value!(f32); +trivial_to_computed_value!(i32); +trivial_to_computed_value!(u8); +trivial_to_computed_value!(u16); +trivial_to_computed_value!(u32); +trivial_to_computed_value!(usize); +trivial_to_computed_value!(Atom); +trivial_to_computed_value!(crate::values::AtomIdent); +#[cfg(feature = "servo")] +trivial_to_computed_value!(crate::Namespace); +#[cfg(feature = "servo")] +trivial_to_computed_value!(crate::Prefix); +trivial_to_computed_value!(String); +trivial_to_computed_value!(Box<str>); +trivial_to_computed_value!(crate::OwnedStr); +trivial_to_computed_value!(style_traits::values::specified::AllowedNumericType); +trivial_to_computed_value!(crate::values::generics::color::ColorMixFlags); + +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[repr(C, u8)] +pub enum AngleOrPercentage { + Percentage(Percentage), + Angle(Angle), +} + +impl ToComputedValue for specified::AngleOrPercentage { + type ComputedValue = AngleOrPercentage; + + #[inline] + fn to_computed_value(&self, context: &Context) -> AngleOrPercentage { + match *self { + specified::AngleOrPercentage::Percentage(percentage) => { + AngleOrPercentage::Percentage(percentage.to_computed_value(context)) + }, + specified::AngleOrPercentage::Angle(angle) => { + AngleOrPercentage::Angle(angle.to_computed_value(context)) + }, + } + } + #[inline] + fn from_computed_value(computed: &AngleOrPercentage) -> Self { + match *computed { + AngleOrPercentage::Percentage(percentage) => specified::AngleOrPercentage::Percentage( + ToComputedValue::from_computed_value(&percentage), + ), + AngleOrPercentage::Angle(angle) => { + specified::AngleOrPercentage::Angle(ToComputedValue::from_computed_value(&angle)) + }, + } + } +} + +/// A `<number>` value. +pub type Number = CSSFloat; + +impl IsParallelTo for (Number, Number, Number) { + fn is_parallel_to(&self, vector: &DirectionVector) -> bool { + use euclid::approxeq::ApproxEq; + // If a and b is parallel, the angle between them is 0deg, so + // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0. + let self_vector = DirectionVector::new(self.0, self.1, self.2); + self_vector + .cross(*vector) + .square_length() + .approx_eq(&0.0f32) + } +} + +/// A wrapper of Number, but the value >= 0. +pub type NonNegativeNumber = NonNegative<CSSFloat>; + +impl ToAnimatedValue for NonNegativeNumber { + type AnimatedValue = CSSFloat; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.max(0.).into() + } +} + +impl From<CSSFloat> for NonNegativeNumber { + #[inline] + fn from(number: CSSFloat) -> NonNegativeNumber { + NonNegative::<CSSFloat>(number) + } +} + +impl From<NonNegativeNumber> for CSSFloat { + #[inline] + fn from(number: NonNegativeNumber) -> CSSFloat { + number.0 + } +} + +impl One for NonNegativeNumber { + #[inline] + fn one() -> Self { + NonNegative(1.0) + } + + #[inline] + fn is_one(&self) -> bool { + self.0 == 1.0 + } +} + +/// A wrapper of Number, but the value between 0 and 1 +pub type ZeroToOneNumber = ZeroToOne<CSSFloat>; + +impl ToAnimatedValue for ZeroToOneNumber { + type AnimatedValue = CSSFloat; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Self(animated.max(0.).min(1.)) + } +} + +impl From<CSSFloat> for ZeroToOneNumber { + #[inline] + fn from(number: CSSFloat) -> Self { + Self(number) + } +} + +/// A wrapper of Number, but the value >= 1. +pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<CSSFloat>; + +impl ToAnimatedValue for GreaterThanOrEqualToOneNumber { + type AnimatedValue = CSSFloat; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.max(1.).into() + } +} + +impl From<CSSFloat> for GreaterThanOrEqualToOneNumber { + #[inline] + fn from(number: CSSFloat) -> GreaterThanOrEqualToOneNumber { + GreaterThanOrEqualToOne::<CSSFloat>(number) + } +} + +impl From<GreaterThanOrEqualToOneNumber> for CSSFloat { + #[inline] + fn from(number: GreaterThanOrEqualToOneNumber) -> CSSFloat { + number.0 + } +} + +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[repr(C, u8)] +pub enum NumberOrPercentage { + Percentage(Percentage), + Number(Number), +} + +impl NumberOrPercentage { + fn clamp_to_non_negative(self) -> Self { + match self { + NumberOrPercentage::Percentage(p) => { + NumberOrPercentage::Percentage(p.clamp_to_non_negative()) + }, + NumberOrPercentage::Number(n) => NumberOrPercentage::Number(n.max(0.)), + } + } +} + +impl ToComputedValue for specified::NumberOrPercentage { + type ComputedValue = NumberOrPercentage; + + #[inline] + fn to_computed_value(&self, context: &Context) -> NumberOrPercentage { + match *self { + specified::NumberOrPercentage::Percentage(percentage) => { + NumberOrPercentage::Percentage(percentage.to_computed_value(context)) + }, + specified::NumberOrPercentage::Number(number) => { + NumberOrPercentage::Number(number.to_computed_value(context)) + }, + } + } + #[inline] + fn from_computed_value(computed: &NumberOrPercentage) -> Self { + match *computed { + NumberOrPercentage::Percentage(percentage) => { + specified::NumberOrPercentage::Percentage(ToComputedValue::from_computed_value( + &percentage, + )) + }, + NumberOrPercentage::Number(number) => { + specified::NumberOrPercentage::Number(ToComputedValue::from_computed_value(&number)) + }, + } + } +} + +/// A non-negative <number-percentage>. +pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>; + +impl NonNegativeNumberOrPercentage { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + NonNegative(NumberOrPercentage::Percentage(Percentage::hundred())) + } +} + +impl ToAnimatedValue for NonNegativeNumberOrPercentage { + type AnimatedValue = NumberOrPercentage; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + NonNegative(animated.clamp_to_non_negative()) + } +} + +/// A type used for opacity. +pub type Opacity = CSSFloat; + +/// A `<integer>` value. +pub type Integer = CSSInteger; + +/// A wrapper of Integer, but only accept a value >= 1. +pub type PositiveInteger = GreaterThanOrEqualToOne<CSSInteger>; + +impl ToAnimatedValue for PositiveInteger { + type AnimatedValue = CSSInteger; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + cmp::max(animated, 1).into() + } +} + +impl From<CSSInteger> for PositiveInteger { + #[inline] + fn from(int: CSSInteger) -> PositiveInteger { + GreaterThanOrEqualToOne::<CSSInteger>(int) + } +} + +/// rect(...) | auto +pub type ClipRect = generics::GenericClipRect<LengthOrAuto>; + +/// rect(...) | auto +pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>; + +/// The computed value of a grid `<track-breadth>` +pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>; + +/// The computed value of a grid `<track-size>` +pub type TrackSize = GenericTrackSize<LengthPercentage>; + +/// The computed value of a grid `<track-size>+` +pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>; + +/// The computed value of a grid `<track-list>` +/// (could also be `<auto-track-list>` or `<explicit-track-list>`) +pub type TrackList = GenericTrackList<LengthPercentage, Integer>; + +/// The computed value of a `<grid-line>`. +pub type GridLine = GenericGridLine<Integer>; + +/// `<grid-template-rows> | <grid-template-columns>` +pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>; + +impl ClipRect { + /// Given a border box, resolves the clip rect against the border box + /// in the same space the border box is in + pub fn for_border_rect<T: Copy + From<Length> + Add<Output = T> + Sub<Output = T>, U>( + &self, + border_box: Rect<T, U>, + ) -> Rect<T, U> { + fn extract_clip_component<T: From<Length>>(p: &LengthOrAuto, or: T) -> T { + match *p { + LengthOrAuto::Auto => or, + LengthOrAuto::LengthPercentage(ref length) => T::from(*length), + } + } + + let clip_origin = Point2D::new( + From::from(self.left.auto_is(|| Length::new(0.))), + From::from(self.top.auto_is(|| Length::new(0.))), + ); + let right = extract_clip_component(&self.right, border_box.size.width); + let bottom = extract_clip_component(&self.bottom, border_box.size.height); + let clip_size = Size2D::new(right - clip_origin.x, bottom - clip_origin.y); + + Rect::new(clip_origin, clip_size).translate(border_box.origin.to_vector()) + } +} diff --git a/servo/components/style/values/computed/motion.rs b/servo/components/style/values/computed/motion.rs new file mode 100644 index 0000000000..37b9f4909e --- /dev/null +++ b/servo/components/style/values/computed/motion.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values that are related to motion path. + +use crate::values::computed::basic_shape::BasicShape; +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Angle, LengthPercentage, Position}; +use crate::values::generics::motion::{ + GenericOffsetPath, GenericOffsetPathFunction, GenericOffsetPosition, GenericRayFunction, +}; +use crate::Zero; + +/// The computed value of ray() function. +pub type RayFunction = GenericRayFunction<Angle, Position>; + +/// The computed value of <offset-path>. +pub type OffsetPathFunction = GenericOffsetPathFunction<BasicShape, RayFunction, ComputedUrl>; + +/// The computed value of `offset-path`. +pub type OffsetPath = GenericOffsetPath<OffsetPathFunction>; + +/// The computed value of `offset-position`. +pub type OffsetPosition = GenericOffsetPosition<LengthPercentage, LengthPercentage>; + +#[inline] +fn is_auto_zero_angle(auto: &bool, angle: &Angle) -> bool { + *auto && angle.is_zero() +} + +/// A computed offset-rotate. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[repr(C)] +pub struct OffsetRotate { + /// If auto is false, this is a fixed angle which indicates a + /// constant clockwise rotation transformation applied to it by this + /// specified rotation angle. Otherwise, the angle will be added to + /// the angle of the direction in layout. + #[animation(constant)] + #[css(represents_keyword)] + pub auto: bool, + /// The angle value. + #[css(contextual_skip_if = "is_auto_zero_angle")] + pub angle: Angle, +} + +impl OffsetRotate { + /// Returns "auto 0deg". + #[inline] + pub fn auto() -> Self { + OffsetRotate { + auto: true, + angle: Zero::zero(), + } + } +} diff --git a/servo/components/style/values/computed/outline.rs b/servo/components/style/values/computed/outline.rs new file mode 100644 index 0000000000..f872176529 --- /dev/null +++ b/servo/components/style/values/computed/outline.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for outline properties + +pub use crate::values::specified::OutlineStyle; diff --git a/servo/components/style/values/computed/page.rs b/servo/components/style/values/computed/page.rs new file mode 100644 index 0000000000..6f71c912cf --- /dev/null +++ b/servo/components/style/values/computed/page.rs @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed @page at-rule properties and named-page style properties + +use crate::values::computed::length::NonNegativeLength; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics; +use crate::values::generics::size::Size2D; + +use crate::values::specified::page as specified; +pub use generics::page::GenericPageSize; +pub use generics::page::PageOrientation; +pub use generics::page::PageSizeOrientation; +pub use generics::page::PaperSize; +pub use specified::PageName; + +/// Computed value of the @page size descriptor +/// +/// The spec says that the computed value should be the same as the specified +/// value but with all absolute units, but it's not currently possibly observe +/// the computed value of page-size. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)] +#[repr(C, u8)] +pub enum PageSize { + /// Specified size, paper size, or paper size and orientation. + Size(Size2D<NonNegativeLength>), + /// `landscape` or `portrait` value, no specified size. + Orientation(PageSizeOrientation), + /// `auto` value + Auto, +} + +impl ToComputedValue for specified::PageSize { + type ComputedValue = PageSize; + + fn to_computed_value(&self, ctx: &Context) -> Self::ComputedValue { + match &*self { + Self::Size(s) => PageSize::Size(s.to_computed_value(ctx)), + Self::PaperSize(p, PageSizeOrientation::Landscape) => PageSize::Size(Size2D { + width: p.long_edge().to_computed_value(ctx), + height: p.short_edge().to_computed_value(ctx), + }), + Self::PaperSize(p, PageSizeOrientation::Portrait) => PageSize::Size(Size2D { + width: p.short_edge().to_computed_value(ctx), + height: p.long_edge().to_computed_value(ctx), + }), + Self::Orientation(o) => PageSize::Orientation(*o), + Self::Auto => PageSize::Auto, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + PageSize::Size(s) => Self::Size(ToComputedValue::from_computed_value(&s)), + PageSize::Orientation(o) => Self::Orientation(o), + PageSize::Auto => Self::Auto, + } + } +} + +impl PageSize { + /// `auto` value. + #[inline] + pub fn auto() -> Self { + PageSize::Auto + } + + /// Whether this is the `auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, PageSize::Auto) + } +} diff --git a/servo/components/style/values/computed/percentage.rs b/servo/components/style/values/computed/percentage.rs new file mode 100644 index 0000000000..994c01594a --- /dev/null +++ b/servo/components/style/values/computed/percentage.rs @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed percentages. + +use crate::values::animated::ToAnimatedValue; +use crate::values::generics::NonNegative; +use crate::values::specified::percentage::ToPercentage; +use crate::values::{serialize_normalized_percentage, CSSFloat}; +use crate::Zero; +use std::fmt; +use style_traits::{CssWriter, ToCss}; + +/// A computed percentage. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Default, + Deserialize, + MallocSizeOf, + PartialEq, + PartialOrd, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Percentage(pub CSSFloat); + +impl Percentage { + /// 100% + #[inline] + pub fn hundred() -> Self { + Percentage(1.) + } + + /// Returns the absolute value for this percentage. + #[inline] + pub fn abs(&self) -> Self { + Percentage(self.0.abs()) + } + + /// Clamps this percentage to a non-negative percentage. + #[inline] + pub fn clamp_to_non_negative(self) -> Self { + Percentage(self.0.max(0.)) + } +} + +impl Zero for Percentage { + fn zero() -> Self { + Percentage(0.) + } + + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl ToPercentage for Percentage { + fn to_percentage(&self) -> CSSFloat { + self.0 + } +} + +impl std::ops::AddAssign for Percentage { + fn add_assign(&mut self, other: Self) { + self.0 += other.0 + } +} + +impl std::ops::Add for Percentage { + type Output = Self; + + fn add(self, other: Self) -> Self { + Percentage(self.0 + other.0) + } +} + +impl std::ops::Sub for Percentage { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Percentage(self.0 - other.0) + } +} + +impl std::ops::Rem for Percentage { + type Output = Self; + + fn rem(self, other: Self) -> Self { + Percentage(self.0 % other.0) + } +} + +impl ToCss for Percentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + serialize_normalized_percentage(self.0, dest) + } +} + +/// A wrapper over a `Percentage`, whose value should be clamped to 0. +pub type NonNegativePercentage = NonNegative<Percentage>; + +impl NonNegativePercentage { + /// 100% + #[inline] + pub fn hundred() -> Self { + NonNegative(Percentage::hundred()) + } +} + +impl ToAnimatedValue for NonNegativePercentage { + type AnimatedValue = Percentage; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + NonNegative(animated.clamp_to_non_negative()) + } +} diff --git a/servo/components/style/values/computed/position.rs b/servo/components/style/values/computed/position.rs new file mode 100644 index 0000000000..5a10c0f23d --- /dev/null +++ b/servo/components/style/values/computed/position.rs @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [`position`][position] values. +//! +//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position + +use crate::values::computed::{Integer, LengthPercentage, NonNegativeNumber, Percentage}; +use crate::values::generics::position::AspectRatio as GenericAspectRatio; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::position::PositionComponent as GenericPositionComponent; +use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto; +use crate::values::generics::position::ZIndex as GenericZIndex; +pub use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas, MasonryAutoFlow}; +use crate::Zero; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// The computed value of a CSS `<position>` +pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>; + +/// The computed value of an `auto | <position>` +pub type PositionOrAuto = GenericPositionOrAuto<Position>; + +/// The computed value of a CSS horizontal position. +pub type HorizontalPosition = LengthPercentage; + +/// The computed value of a CSS vertical position. +pub type VerticalPosition = LengthPercentage; + +impl Position { + /// `50% 50%` + #[inline] + pub fn center() -> Self { + Self::new( + LengthPercentage::new_percent(Percentage(0.5)), + LengthPercentage::new_percent(Percentage(0.5)), + ) + } + + /// `0% 0%` + #[inline] + pub fn zero() -> Self { + Self::new(LengthPercentage::zero(), LengthPercentage::zero()) + } +} + +impl ToCss for Position { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.horizontal.to_css(dest)?; + dest.write_char(' ')?; + self.vertical.to_css(dest) + } +} + +impl GenericPositionComponent for LengthPercentage { + fn is_center(&self) -> bool { + match self.to_percentage() { + Some(Percentage(per)) => per == 0.5, + _ => false, + } + } +} + +/// A computed value for the `z-index` property. +pub type ZIndex = GenericZIndex<Integer>; + +/// A computed value for the `aspect-ratio` property. +pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>; diff --git a/servo/components/style/values/computed/ratio.rs b/servo/components/style/values/computed/ratio.rs new file mode 100644 index 0000000000..ae8997cfc0 --- /dev/null +++ b/servo/components/style/values/computed/ratio.rs @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `<ratio>` computed values. + +use crate::values::animated::{Animate, Procedure}; +use crate::values::computed::NonNegativeNumber; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::ratio::Ratio as GenericRatio; +use crate::{One, Zero}; +use std::cmp::{Ordering, PartialOrd}; + +/// A computed <ratio> value. +pub type Ratio = GenericRatio<NonNegativeNumber>; + +impl PartialOrd for Ratio { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + f64::partial_cmp( + &((self.0).0 as f64 * (other.1).0 as f64), + &((self.1).0 as f64 * (other.0).0 as f64), + ) + } +} + +/// https://drafts.csswg.org/css-values/#combine-ratio +impl Animate for Ratio { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + // If either <ratio> is degenerate, the values cannot be interpolated. + if self.is_degenerate() || other.is_degenerate() { + return Err(()); + } + + // Addition of <ratio>s is not possible, and based on + // https://drafts.csswg.org/css-values-4/#not-additive, + // we simply use the first value as the result value. + // Besides, the procedure for accumulation should be identical to addition here. + if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) { + return Ok(self.clone()); + } + + // The interpolation of a <ratio> is defined by converting each <ratio> to a number by + // dividing the first value by the second (so a ratio of 3 / 2 would become 1.5), taking + // the logarithm of that result (so the 1.5 would become approximately 0.176), then + // interpolating those values. + // + // The result during the interpolation is converted back to a <ratio> by inverting the + // logarithm, then interpreting the result as a <ratio> with the result as the first value + // and 1 as the second value. + let start = self.to_f32().ln(); + let end = other.to_f32().ln(); + let e = std::f32::consts::E; + let result = e.powf(start.animate(&end, procedure)?); + // The range of the result is [0, inf), based on the easing function. + if result.is_zero() || result.is_infinite() { + return Err(()); + } + Ok(Ratio::new(result, 1.0f32)) + } +} + +impl ComputeSquaredDistance for Ratio { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.is_degenerate() || other.is_degenerate() { + return Err(()); + } + // Use the distance of their logarithm values. (This is used by testing, so don't need to + // care about the base. Here we use the same base as that in animate().) + self.to_f32() + .ln() + .compute_squared_distance(&other.to_f32().ln()) + } +} + +impl Zero for Ratio { + fn zero() -> Self { + Self::new(Zero::zero(), One::one()) + } + + fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +impl Ratio { + /// Returns a new Ratio. + #[inline] + pub fn new(a: f32, b: f32) -> Self { + GenericRatio(a.into(), b.into()) + } + + /// Returns the used value. A ratio of 0/0 behaves as the ratio 1/0. + /// https://drafts.csswg.org/css-values-4/#ratios + pub fn used_value(self) -> Self { + if self.0.is_zero() && self.1.is_zero() { + Ratio::new(One::one(), Zero::zero()) + } else { + self + } + } + + /// Returns true if this is a degenerate ratio. + /// https://drafts.csswg.org/css-values/#degenerate-ratio + #[inline] + pub fn is_degenerate(&self) -> bool { + self.0.is_zero() || self.1.is_zero() + } + + /// Returns the f32 value by dividing the first value by the second one. + #[inline] + fn to_f32(&self) -> f32 { + debug_assert!(!self.is_degenerate()); + (self.0).0 / (self.1).0 + } +} diff --git a/servo/components/style/values/computed/rect.rs b/servo/components/style/values/computed/rect.rs new file mode 100644 index 0000000000..ec44360fc8 --- /dev/null +++ b/servo/components/style/values/computed/rect.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS borders. + +use crate::values::computed::length::NonNegativeLengthOrNumber; +use crate::values::generics::rect::Rect; + +/// A specified rectangle made of four `<length-or-number>` values. +pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>; diff --git a/servo/components/style/values/computed/resolution.rs b/servo/components/style/values/computed/resolution.rs new file mode 100644 index 0000000000..430c80d211 --- /dev/null +++ b/servo/components/style/values/computed/resolution.rs @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Resolution values: +//! +//! https://drafts.csswg.org/css-values/#resolution + +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified; +use crate::values::CSSFloat; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A computed `<resolution>`. +#[repr(C)] +#[derive(Animate, Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)] +pub struct Resolution(CSSFloat); + +impl Resolution { + /// Returns this resolution value as dppx. + #[inline] + pub fn dppx(&self) -> CSSFloat { + self.0 + } + + /// Return a computed `resolution` value from a dppx float value. + #[inline] + pub fn from_dppx(dppx: CSSFloat) -> Self { + Resolution(dppx) + } +} + +impl ToComputedValue for specified::Resolution { + type ComputedValue = Resolution; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + Resolution(crate::values::normalize(self.dppx().max(0.0))) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + specified::Resolution::from_dppx(computed.dppx()) + } +} + +impl ToCss for Resolution { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.dppx().to_css(dest)?; + dest.write_str("dppx") + } +} diff --git a/servo/components/style/values/computed/svg.rs b/servo/components/style/values/computed/svg.rs new file mode 100644 index 0000000000..6efdfca36b --- /dev/null +++ b/servo/components/style/values/computed/svg.rs @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for SVG properties. + +use crate::values::computed::color::Color; +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{LengthPercentage, NonNegativeLengthPercentage, Opacity}; +use crate::values::generics::svg as generic; +use crate::Zero; + +pub use crate::values::specified::{DProperty, MozContextProperties, SVGPaintOrder}; + +/// Computed SVG Paint value +pub type SVGPaint = generic::GenericSVGPaint<Color, ComputedUrl>; + +/// Computed SVG Paint Kind value +pub type SVGPaintKind = generic::GenericSVGPaintKind<Color, ComputedUrl>; + +impl SVGPaint { + /// Opaque black color + pub const BLACK: Self = Self { + kind: generic::SVGPaintKind::Color(Color::BLACK), + fallback: generic::SVGPaintFallback::Unset, + }; +} + +/// <length> | <percentage> | <number> | context-value +pub type SVGLength = generic::GenericSVGLength<LengthPercentage>; + +impl SVGLength { + /// `0px` + pub fn zero() -> Self { + generic::SVGLength::LengthPercentage(LengthPercentage::zero()) + } +} + +/// An non-negative wrapper of SVGLength. +pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>; + +impl SVGWidth { + /// `1px`. + pub fn one() -> Self { + use crate::values::generics::NonNegative; + generic::SVGLength::LengthPercentage(NonNegative(LengthPercentage::one())) + } +} + +/// [ <length> | <percentage> | <number> ]# | context-value +pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>; + +impl Default for SVGStrokeDashArray { + fn default() -> Self { + generic::SVGStrokeDashArray::Values(Default::default()) + } +} + +/// <opacity-value> | context-fill-opacity | context-stroke-opacity +pub type SVGOpacity = generic::GenericSVGOpacity<Opacity>; + +impl Default for SVGOpacity { + fn default() -> Self { + generic::SVGOpacity::Opacity(1.) + } +} diff --git a/servo/components/style/values/computed/table.rs b/servo/components/style/values/computed/table.rs new file mode 100644 index 0000000000..47109e20ec --- /dev/null +++ b/servo/components/style/values/computed/table.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to tables. + +pub use super::specified::table::CaptionSide; diff --git a/servo/components/style/values/computed/text.rs b/servo/components/style/values/computed/text.rs new file mode 100644 index 0000000000..a4fec654a5 --- /dev/null +++ b/servo/components/style/values/computed/text.rs @@ -0,0 +1,228 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for text properties. + +#[cfg(feature = "servo")] +use crate::properties::StyleBuilder; +use crate::values::computed::length::{Length, LengthPercentage}; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::text::InitialLetter as GenericInitialLetter; +use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing}; +use crate::values::specified::text::{self as specified, TextOverflowSide}; +use crate::values::specified::text::{TextEmphasisFillMode, TextEmphasisShapeKeyword}; +use crate::values::{CSSFloat, CSSInteger}; +use crate::Zero; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::text::{ + MozControlCharacterVisibility, TextAlignLast, TextUnderlinePosition, +}; +pub use crate::values::specified::HyphenateCharacter; +pub use crate::values::specified::{LineBreak, OverflowWrap, RubyPosition, WordBreak}; +pub use crate::values::specified::{TextDecorationLine, TextEmphasisPosition}; +pub use crate::values::specified::{TextDecorationSkipInk, TextJustify, TextTransform}; + +/// A computed value for the `initial-letter` property. +pub type InitialLetter = GenericInitialLetter<CSSFloat, CSSInteger>; + +/// Implements type for `text-decoration-thickness` property. +pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>; + +/// The computed value of `text-align`. +pub type TextAlign = specified::TextAlignKeyword; + +/// The computed value of `text-indent`. +pub type TextIndent = GenericTextIndent<LengthPercentage>; + +/// A computed value for the `letter-spacing` property. +#[repr(transparent)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedValue, + ToAnimatedZero, + ToResolvedValue, +)] +pub struct LetterSpacing(pub Length); + +impl LetterSpacing { + /// Return the `normal` computed value, which is just zero. + #[inline] + pub fn normal() -> Self { + LetterSpacing(Length::zero()) + } +} + +impl ToCss for LetterSpacing { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + // https://drafts.csswg.org/css-text/#propdef-letter-spacing + // + // For legacy reasons, a computed letter-spacing of zero yields a + // resolved value (getComputedStyle() return value) of normal. + if self.0.is_zero() { + return dest.write_str("normal"); + } + self.0.to_css(dest) + } +} + +impl ToComputedValue for specified::LetterSpacing { + type ComputedValue = LetterSpacing; + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + Spacing::Normal => LetterSpacing(Length::zero()), + Spacing::Value(ref v) => LetterSpacing(v.to_computed_value(context)), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if computed.0.is_zero() { + return Spacing::Normal; + } + Spacing::Value(ToComputedValue::from_computed_value(&computed.0)) + } +} + +/// A computed value for the `word-spacing` property. +pub type WordSpacing = LengthPercentage; + +impl ToComputedValue for specified::WordSpacing { + type ComputedValue = WordSpacing; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + Spacing::Normal => LengthPercentage::zero(), + Spacing::Value(ref v) => v.to_computed_value(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Spacing::Value(ToComputedValue::from_computed_value(computed)) + } +} + +impl WordSpacing { + /// Return the `normal` computed value, which is just zero. + #[inline] + pub fn normal() -> Self { + LengthPercentage::zero() + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue)] +#[repr(C)] +/// text-overflow. +/// When the specified value only has one side, that's the "second" +/// side, and the sides are logical, so "second" means "end". The +/// start side is Clip in that case. +/// +/// When the specified value has two sides, those are our "first" +/// and "second" sides, and they are physical sides ("left" and +/// "right"). +pub struct TextOverflow { + /// First side + pub first: TextOverflowSide, + /// Second side + pub second: TextOverflowSide, + /// True if the specified value only has one side. + pub sides_are_logical: bool, +} + +impl TextOverflow { + /// Returns the initial `text-overflow` value + pub fn get_initial_value() -> TextOverflow { + TextOverflow { + first: TextOverflowSide::Clip, + second: TextOverflowSide::Clip, + sides_are_logical: true, + } + } +} + +impl ToCss for TextOverflow { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.sides_are_logical { + debug_assert_eq!(self.first, TextOverflowSide::Clip); + self.second.to_css(dest)?; + } else { + self.first.to_css(dest)?; + dest.write_char(' ')?; + self.second.to_css(dest)?; + } + Ok(()) + } +} + +/// A struct that represents the _used_ value of the text-decoration property. +/// +/// FIXME(emilio): This is done at style resolution time, though probably should +/// be done at layout time, otherwise we need to account for display: contents +/// and similar stuff when we implement it. +/// +/// FIXME(emilio): Also, should be just a bitfield instead of three bytes. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToResolvedValue)] +pub struct TextDecorationsInEffect { + /// Whether an underline is in effect. + pub underline: bool, + /// Whether an overline decoration is in effect. + pub overline: bool, + /// Whether a line-through style is in effect. + pub line_through: bool, +} + +impl TextDecorationsInEffect { + /// Computes the text-decorations in effect for a given style. + #[cfg(feature = "servo")] + pub fn from_style(style: &StyleBuilder) -> Self { + // Start with no declarations if this is an atomic inline-level box; + // otherwise, start with the declarations in effect and add in the text + // decorations that this block specifies. + let mut result = if style.get_box().clone_display().is_atomic_inline_level() { + Self::default() + } else { + style + .get_parent_inherited_text() + .text_decorations_in_effect + .clone() + }; + + let line = style.get_text().clone_text_decoration_line(); + + result.underline |= line.contains(TextDecorationLine::UNDERLINE); + result.overline |= line.contains(TextDecorationLine::OVERLINE); + result.line_through |= line.contains(TextDecorationLine::LINE_THROUGH); + + result + } +} + +/// Computed value for the text-emphasis-style property +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)] +#[allow(missing_docs)] +#[repr(C, u8)] +pub enum TextEmphasisStyle { + /// [ <fill> || <shape> ] + Keyword { + #[css(skip_if = "TextEmphasisFillMode::is_filled")] + fill: TextEmphasisFillMode, + shape: TextEmphasisShapeKeyword, + }, + /// `none` + None, + /// `<string>` (of which only the first grapheme cluster will be used). + String(crate::OwnedStr), +} diff --git a/servo/components/style/values/computed/time.rs b/servo/components/style/values/computed/time.rs new file mode 100644 index 0000000000..b81c6e879a --- /dev/null +++ b/servo/components/style/values/computed/time.rs @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed time values. + +use crate::values::CSSFloat; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A computed `<time>` value. +#[derive(Animate, Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(C)] +pub struct Time { + seconds: CSSFloat, +} + +impl Time { + /// Creates a time value from a seconds amount. + pub fn from_seconds(seconds: CSSFloat) -> Self { + Time { seconds } + } + + /// Returns `0s`. + pub fn zero() -> Self { + Self::from_seconds(0.0) + } + + /// Returns the amount of seconds this time represents. + #[inline] + pub fn seconds(&self) -> CSSFloat { + self.seconds + } +} + +impl ToCss for Time { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.seconds().to_css(dest)?; + dest.write_char('s') + } +} diff --git a/servo/components/style/values/computed/transform.rs b/servo/components/style/values/computed/transform.rs new file mode 100644 index 0000000000..b1a617fd4b --- /dev/null +++ b/servo/components/style/values/computed/transform.rs @@ -0,0 +1,559 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values that are related to transformations. + +use super::CSSFloat; +use crate::values::animated::transform::{Perspective, Scale3D, Translate3D}; +use crate::values::animated::ToAnimatedZero; +use crate::values::computed::{Angle, Integer, Length, LengthPercentage, Number, Percentage}; +use crate::values::generics::transform as generic; +use crate::Zero; +use euclid::default::{Transform3D, Vector3D}; + +pub use crate::values::generics::transform::TransformStyle; +pub use crate::values::specified::transform::TransformBox; + +/// A single operation in a computed CSS `transform` +pub type TransformOperation = + generic::GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>; +/// A computed CSS `transform` +pub type Transform = generic::GenericTransform<TransformOperation>; + +/// The computed value of a CSS `<transform-origin>` +pub type TransformOrigin = + generic::GenericTransformOrigin<LengthPercentage, LengthPercentage, Length>; + +/// The computed value of the `perspective()` transform function. +pub type PerspectiveFunction = generic::PerspectiveFunction<Length>; + +/// A vector to represent the direction vector (rotate axis) for Rotate3D. +pub type DirectionVector = Vector3D<CSSFloat>; + +impl TransformOrigin { + /// Returns the initial computed value for `transform-origin`. + #[inline] + pub fn initial_value() -> Self { + Self::new( + LengthPercentage::new_percent(Percentage(0.5)), + LengthPercentage::new_percent(Percentage(0.5)), + Length::new(0.), + ) + } +} + +/// computed value of matrix3d() +pub type Matrix3D = generic::Matrix3D<Number>; + +/// computed value of matrix() +pub type Matrix = generic::Matrix<Number>; + +// we rustfmt_skip here because we want the matrices to look like +// matrices instead of being split across lines +#[cfg_attr(rustfmt, rustfmt_skip)] +impl Matrix3D { + /// Get an identity matrix + #[inline] + pub fn identity() -> Self { + Self { + m11: 1.0, m12: 0.0, m13: 0.0, m14: 0.0, + m21: 0.0, m22: 1.0, m23: 0.0, m24: 0.0, + m31: 0.0, m32: 0.0, m33: 1.0, m34: 0.0, + m41: 0., m42: 0., m43: 0., m44: 1.0 + } + } + + /// Convert to a 2D Matrix + #[inline] + pub fn into_2d(self) -> Result<Matrix, ()> { + if self.m13 == 0. && self.m23 == 0. && + self.m31 == 0. && self.m32 == 0. && + self.m33 == 1. && self.m34 == 0. && + self.m14 == 0. && self.m24 == 0. && + self.m43 == 0. && self.m44 == 1. { + Ok(Matrix { + a: self.m11, c: self.m21, e: self.m41, + b: self.m12, d: self.m22, f: self.m42, + }) + } else { + Err(()) + } + } + + /// Return true if this has 3D components. + #[inline] + pub fn is_3d(&self) -> bool { + self.m13 != 0.0 || self.m14 != 0.0 || + self.m23 != 0.0 || self.m24 != 0.0 || + self.m31 != 0.0 || self.m32 != 0.0 || + self.m33 != 1.0 || self.m34 != 0.0 || + self.m43 != 0.0 || self.m44 != 1.0 + } + + /// Return determinant value. + #[inline] + pub fn determinant(&self) -> CSSFloat { + self.m14 * self.m23 * self.m32 * self.m41 - + self.m13 * self.m24 * self.m32 * self.m41 - + self.m14 * self.m22 * self.m33 * self.m41 + + self.m12 * self.m24 * self.m33 * self.m41 + + self.m13 * self.m22 * self.m34 * self.m41 - + self.m12 * self.m23 * self.m34 * self.m41 - + self.m14 * self.m23 * self.m31 * self.m42 + + self.m13 * self.m24 * self.m31 * self.m42 + + self.m14 * self.m21 * self.m33 * self.m42 - + self.m11 * self.m24 * self.m33 * self.m42 - + self.m13 * self.m21 * self.m34 * self.m42 + + self.m11 * self.m23 * self.m34 * self.m42 + + self.m14 * self.m22 * self.m31 * self.m43 - + self.m12 * self.m24 * self.m31 * self.m43 - + self.m14 * self.m21 * self.m32 * self.m43 + + self.m11 * self.m24 * self.m32 * self.m43 + + self.m12 * self.m21 * self.m34 * self.m43 - + self.m11 * self.m22 * self.m34 * self.m43 - + self.m13 * self.m22 * self.m31 * self.m44 + + self.m12 * self.m23 * self.m31 * self.m44 + + self.m13 * self.m21 * self.m32 * self.m44 - + self.m11 * self.m23 * self.m32 * self.m44 - + self.m12 * self.m21 * self.m33 * self.m44 + + self.m11 * self.m22 * self.m33 * self.m44 + } + + /// Transpose a matrix. + #[inline] + pub fn transpose(&self) -> Self { + Self { + m11: self.m11, m12: self.m21, m13: self.m31, m14: self.m41, + m21: self.m12, m22: self.m22, m23: self.m32, m24: self.m42, + m31: self.m13, m32: self.m23, m33: self.m33, m34: self.m43, + m41: self.m14, m42: self.m24, m43: self.m34, m44: self.m44, + } + } + + /// Return inverse matrix. + pub fn inverse(&self) -> Result<Matrix3D, ()> { + let mut det = self.determinant(); + + if det == 0.0 { + return Err(()); + } + + det = 1.0 / det; + let x = Matrix3D { + m11: det * + (self.m23 * self.m34 * self.m42 - self.m24 * self.m33 * self.m42 + + self.m24 * self.m32 * self.m43 - self.m22 * self.m34 * self.m43 - + self.m23 * self.m32 * self.m44 + self.m22 * self.m33 * self.m44), + m12: det * + (self.m14 * self.m33 * self.m42 - self.m13 * self.m34 * self.m42 - + self.m14 * self.m32 * self.m43 + self.m12 * self.m34 * self.m43 + + self.m13 * self.m32 * self.m44 - self.m12 * self.m33 * self.m44), + m13: det * + (self.m13 * self.m24 * self.m42 - self.m14 * self.m23 * self.m42 + + self.m14 * self.m22 * self.m43 - self.m12 * self.m24 * self.m43 - + self.m13 * self.m22 * self.m44 + self.m12 * self.m23 * self.m44), + m14: det * + (self.m14 * self.m23 * self.m32 - self.m13 * self.m24 * self.m32 - + self.m14 * self.m22 * self.m33 + self.m12 * self.m24 * self.m33 + + self.m13 * self.m22 * self.m34 - self.m12 * self.m23 * self.m34), + m21: det * + (self.m24 * self.m33 * self.m41 - self.m23 * self.m34 * self.m41 - + self.m24 * self.m31 * self.m43 + self.m21 * self.m34 * self.m43 + + self.m23 * self.m31 * self.m44 - self.m21 * self.m33 * self.m44), + m22: det * + (self.m13 * self.m34 * self.m41 - self.m14 * self.m33 * self.m41 + + self.m14 * self.m31 * self.m43 - self.m11 * self.m34 * self.m43 - + self.m13 * self.m31 * self.m44 + self.m11 * self.m33 * self.m44), + m23: det * + (self.m14 * self.m23 * self.m41 - self.m13 * self.m24 * self.m41 - + self.m14 * self.m21 * self.m43 + self.m11 * self.m24 * self.m43 + + self.m13 * self.m21 * self.m44 - self.m11 * self.m23 * self.m44), + m24: det * + (self.m13 * self.m24 * self.m31 - self.m14 * self.m23 * self.m31 + + self.m14 * self.m21 * self.m33 - self.m11 * self.m24 * self.m33 - + self.m13 * self.m21 * self.m34 + self.m11 * self.m23 * self.m34), + m31: det * + (self.m22 * self.m34 * self.m41 - self.m24 * self.m32 * self.m41 + + self.m24 * self.m31 * self.m42 - self.m21 * self.m34 * self.m42 - + self.m22 * self.m31 * self.m44 + self.m21 * self.m32 * self.m44), + m32: det * + (self.m14 * self.m32 * self.m41 - self.m12 * self.m34 * self.m41 - + self.m14 * self.m31 * self.m42 + self.m11 * self.m34 * self.m42 + + self.m12 * self.m31 * self.m44 - self.m11 * self.m32 * self.m44), + m33: det * + (self.m12 * self.m24 * self.m41 - self.m14 * self.m22 * self.m41 + + self.m14 * self.m21 * self.m42 - self.m11 * self.m24 * self.m42 - + self.m12 * self.m21 * self.m44 + self.m11 * self.m22 * self.m44), + m34: det * + (self.m14 * self.m22 * self.m31 - self.m12 * self.m24 * self.m31 - + self.m14 * self.m21 * self.m32 + self.m11 * self.m24 * self.m32 + + self.m12 * self.m21 * self.m34 - self.m11 * self.m22 * self.m34), + m41: det * + (self.m23 * self.m32 * self.m41 - self.m22 * self.m33 * self.m41 - + self.m23 * self.m31 * self.m42 + self.m21 * self.m33 * self.m42 + + self.m22 * self.m31 * self.m43 - self.m21 * self.m32 * self.m43), + m42: det * + (self.m12 * self.m33 * self.m41 - self.m13 * self.m32 * self.m41 + + self.m13 * self.m31 * self.m42 - self.m11 * self.m33 * self.m42 - + self.m12 * self.m31 * self.m43 + self.m11 * self.m32 * self.m43), + m43: det * + (self.m13 * self.m22 * self.m41 - self.m12 * self.m23 * self.m41 - + self.m13 * self.m21 * self.m42 + self.m11 * self.m23 * self.m42 + + self.m12 * self.m21 * self.m43 - self.m11 * self.m22 * self.m43), + m44: det * + (self.m12 * self.m23 * self.m31 - self.m13 * self.m22 * self.m31 + + self.m13 * self.m21 * self.m32 - self.m11 * self.m23 * self.m32 - + self.m12 * self.m21 * self.m33 + self.m11 * self.m22 * self.m33), + }; + + Ok(x) + } + + /// Multiply `pin * self`. + #[inline] + pub fn pre_mul_point4(&self, pin: &[f32; 4]) -> [f32; 4] { + [ + pin[0] * self.m11 + pin[1] * self.m21 + pin[2] * self.m31 + pin[3] * self.m41, + pin[0] * self.m12 + pin[1] * self.m22 + pin[2] * self.m32 + pin[3] * self.m42, + pin[0] * self.m13 + pin[1] * self.m23 + pin[2] * self.m33 + pin[3] * self.m43, + pin[0] * self.m14 + pin[1] * self.m24 + pin[2] * self.m34 + pin[3] * self.m44, + ] + } + + /// Return the multiplication of two 4x4 matrices. + #[inline] + pub fn multiply(&self, other: &Self) -> Self { + Matrix3D { + m11: self.m11 * other.m11 + self.m12 * other.m21 + + self.m13 * other.m31 + self.m14 * other.m41, + m12: self.m11 * other.m12 + self.m12 * other.m22 + + self.m13 * other.m32 + self.m14 * other.m42, + m13: self.m11 * other.m13 + self.m12 * other.m23 + + self.m13 * other.m33 + self.m14 * other.m43, + m14: self.m11 * other.m14 + self.m12 * other.m24 + + self.m13 * other.m34 + self.m14 * other.m44, + m21: self.m21 * other.m11 + self.m22 * other.m21 + + self.m23 * other.m31 + self.m24 * other.m41, + m22: self.m21 * other.m12 + self.m22 * other.m22 + + self.m23 * other.m32 + self.m24 * other.m42, + m23: self.m21 * other.m13 + self.m22 * other.m23 + + self.m23 * other.m33 + self.m24 * other.m43, + m24: self.m21 * other.m14 + self.m22 * other.m24 + + self.m23 * other.m34 + self.m24 * other.m44, + m31: self.m31 * other.m11 + self.m32 * other.m21 + + self.m33 * other.m31 + self.m34 * other.m41, + m32: self.m31 * other.m12 + self.m32 * other.m22 + + self.m33 * other.m32 + self.m34 * other.m42, + m33: self.m31 * other.m13 + self.m32 * other.m23 + + self.m33 * other.m33 + self.m34 * other.m43, + m34: self.m31 * other.m14 + self.m32 * other.m24 + + self.m33 * other.m34 + self.m34 * other.m44, + m41: self.m41 * other.m11 + self.m42 * other.m21 + + self.m43 * other.m31 + self.m44 * other.m41, + m42: self.m41 * other.m12 + self.m42 * other.m22 + + self.m43 * other.m32 + self.m44 * other.m42, + m43: self.m41 * other.m13 + self.m42 * other.m23 + + self.m43 * other.m33 + self.m44 * other.m43, + m44: self.m41 * other.m14 + self.m42 * other.m24 + + self.m43 * other.m34 + self.m44 * other.m44, + } + } + + /// Scale the matrix by a factor. + #[inline] + pub fn scale_by_factor(&mut self, scaling_factor: CSSFloat) { + self.m11 *= scaling_factor; + self.m12 *= scaling_factor; + self.m13 *= scaling_factor; + self.m14 *= scaling_factor; + self.m21 *= scaling_factor; + self.m22 *= scaling_factor; + self.m23 *= scaling_factor; + self.m24 *= scaling_factor; + self.m31 *= scaling_factor; + self.m32 *= scaling_factor; + self.m33 *= scaling_factor; + self.m34 *= scaling_factor; + self.m41 *= scaling_factor; + self.m42 *= scaling_factor; + self.m43 *= scaling_factor; + self.m44 *= scaling_factor; + } + + /// Return the matrix 3x3 part (top-left corner). + /// This is used by retrieving the scale and shear factors + /// during decomposing a 3d matrix. + #[inline] + pub fn get_matrix_3x3_part(&self) -> [[f32; 3]; 3] { + [ + [ self.m11, self.m12, self.m13 ], + [ self.m21, self.m22, self.m23 ], + [ self.m31, self.m32, self.m33 ], + ] + } + + /// Set perspective on the matrix. + #[inline] + pub fn set_perspective(&mut self, perspective: &Perspective) { + self.m14 = perspective.0; + self.m24 = perspective.1; + self.m34 = perspective.2; + self.m44 = perspective.3; + } + + /// Apply translate on the matrix. + #[inline] + pub fn apply_translate(&mut self, translate: &Translate3D) { + self.m41 += translate.0 * self.m11 + translate.1 * self.m21 + translate.2 * self.m31; + self.m42 += translate.0 * self.m12 + translate.1 * self.m22 + translate.2 * self.m32; + self.m43 += translate.0 * self.m13 + translate.1 * self.m23 + translate.2 * self.m33; + self.m44 += translate.0 * self.m14 + translate.1 * self.m24 + translate.2 * self.m34; + } + + /// Apply scale on the matrix. + #[inline] + pub fn apply_scale(&mut self, scale: &Scale3D) { + self.m11 *= scale.0; + self.m12 *= scale.0; + self.m13 *= scale.0; + self.m14 *= scale.0; + self.m21 *= scale.1; + self.m22 *= scale.1; + self.m23 *= scale.1; + self.m24 *= scale.1; + self.m31 *= scale.2; + self.m32 *= scale.2; + self.m33 *= scale.2; + self.m34 *= scale.2; + } +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +impl Matrix { + #[inline] + /// Get an identity matrix + pub fn identity() -> Self { + Self { + a: 1., c: 0., /* 0 0*/ + b: 0., d: 1., /* 0 0*/ + /* 0 0 1 0 */ + e: 0., f: 0., /* 0 1 */ + } + } +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +impl From<Matrix> for Matrix3D { + fn from(m: Matrix) -> Self { + Self { + m11: m.a, m12: m.b, m13: 0.0, m14: 0.0, + m21: m.c, m22: m.d, m23: 0.0, m24: 0.0, + m31: 0.0, m32: 0.0, m33: 1.0, m34: 0.0, + m41: m.e, m42: m.f, m43: 0.0, m44: 1.0 + } + } +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +impl From<Transform3D<CSSFloat>> for Matrix3D { + #[inline] + fn from(m: Transform3D<CSSFloat>) -> Self { + Matrix3D { + m11: m.m11, m12: m.m12, m13: m.m13, m14: m.m14, + m21: m.m21, m22: m.m22, m23: m.m23, m24: m.m24, + m31: m.m31, m32: m.m32, m33: m.m33, m34: m.m34, + m41: m.m41, m42: m.m42, m43: m.m43, m44: m.m44 + } + } +} + +impl TransformOperation { + /// Convert to a Translate3D. + /// + /// Must be called on a Translate function + pub fn to_translate_3d(&self) -> Self { + match *self { + generic::TransformOperation::Translate3D(..) => self.clone(), + generic::TransformOperation::TranslateX(ref x) => { + generic::TransformOperation::Translate3D( + x.clone(), + LengthPercentage::zero(), + Length::zero(), + ) + }, + generic::TransformOperation::Translate(ref x, ref y) => { + generic::TransformOperation::Translate3D(x.clone(), y.clone(), Length::zero()) + }, + generic::TransformOperation::TranslateY(ref y) => { + generic::TransformOperation::Translate3D( + LengthPercentage::zero(), + y.clone(), + Length::zero(), + ) + }, + generic::TransformOperation::TranslateZ(ref z) => { + generic::TransformOperation::Translate3D( + LengthPercentage::zero(), + LengthPercentage::zero(), + z.clone(), + ) + }, + _ => unreachable!(), + } + } + + /// Convert to a Rotate3D. + /// + /// Must be called on a Rotate function. + pub fn to_rotate_3d(&self) -> Self { + match *self { + generic::TransformOperation::Rotate3D(..) => self.clone(), + generic::TransformOperation::RotateZ(ref angle) | + generic::TransformOperation::Rotate(ref angle) => { + generic::TransformOperation::Rotate3D(0., 0., 1., angle.clone()) + }, + generic::TransformOperation::RotateX(ref angle) => { + generic::TransformOperation::Rotate3D(1., 0., 0., angle.clone()) + }, + generic::TransformOperation::RotateY(ref angle) => { + generic::TransformOperation::Rotate3D(0., 1., 0., angle.clone()) + }, + _ => unreachable!(), + } + } + + /// Convert to a Scale3D. + /// + /// Must be called on a Scale function + pub fn to_scale_3d(&self) -> Self { + match *self { + generic::TransformOperation::Scale3D(..) => self.clone(), + generic::TransformOperation::Scale(x, y) => { + generic::TransformOperation::Scale3D(x, y, 1.) + }, + generic::TransformOperation::ScaleX(x) => { + generic::TransformOperation::Scale3D(x, 1., 1.) + }, + generic::TransformOperation::ScaleY(y) => { + generic::TransformOperation::Scale3D(1., y, 1.) + }, + generic::TransformOperation::ScaleZ(z) => { + generic::TransformOperation::Scale3D(1., 1., z) + }, + _ => unreachable!(), + } + } +} + +/// Build an equivalent 'identity transform function list' based +/// on an existing transform list. +/// http://dev.w3.org/csswg/css-transforms/#none-transform-animation +impl ToAnimatedZero for TransformOperation { + fn to_animated_zero(&self) -> Result<Self, ()> { + match *self { + generic::TransformOperation::Matrix3D(..) => { + Ok(generic::TransformOperation::Matrix3D(Matrix3D::identity())) + }, + generic::TransformOperation::Matrix(..) => { + Ok(generic::TransformOperation::Matrix(Matrix::identity())) + }, + generic::TransformOperation::Skew(sx, sy) => Ok(generic::TransformOperation::Skew( + sx.to_animated_zero()?, + sy.to_animated_zero()?, + )), + generic::TransformOperation::SkewX(s) => { + Ok(generic::TransformOperation::SkewX(s.to_animated_zero()?)) + }, + generic::TransformOperation::SkewY(s) => { + Ok(generic::TransformOperation::SkewY(s.to_animated_zero()?)) + }, + generic::TransformOperation::Translate3D(ref tx, ref ty, ref tz) => { + Ok(generic::TransformOperation::Translate3D( + tx.to_animated_zero()?, + ty.to_animated_zero()?, + tz.to_animated_zero()?, + )) + }, + generic::TransformOperation::Translate(ref tx, ref ty) => { + Ok(generic::TransformOperation::Translate( + tx.to_animated_zero()?, + ty.to_animated_zero()?, + )) + }, + generic::TransformOperation::TranslateX(ref t) => Ok( + generic::TransformOperation::TranslateX(t.to_animated_zero()?), + ), + generic::TransformOperation::TranslateY(ref t) => Ok( + generic::TransformOperation::TranslateY(t.to_animated_zero()?), + ), + generic::TransformOperation::TranslateZ(ref t) => Ok( + generic::TransformOperation::TranslateZ(t.to_animated_zero()?), + ), + generic::TransformOperation::Scale3D(..) => { + Ok(generic::TransformOperation::Scale3D(1.0, 1.0, 1.0)) + }, + generic::TransformOperation::Scale(_, _) => { + Ok(generic::TransformOperation::Scale(1.0, 1.0)) + }, + generic::TransformOperation::ScaleX(..) => Ok(generic::TransformOperation::ScaleX(1.0)), + generic::TransformOperation::ScaleY(..) => Ok(generic::TransformOperation::ScaleY(1.0)), + generic::TransformOperation::ScaleZ(..) => Ok(generic::TransformOperation::ScaleZ(1.0)), + generic::TransformOperation::Rotate3D(x, y, z, a) => { + let (x, y, z, _) = generic::get_normalized_vector_and_angle(x, y, z, a); + Ok(generic::TransformOperation::Rotate3D( + x, + y, + z, + Angle::zero(), + )) + }, + generic::TransformOperation::RotateX(_) => { + Ok(generic::TransformOperation::RotateX(Angle::zero())) + }, + generic::TransformOperation::RotateY(_) => { + Ok(generic::TransformOperation::RotateY(Angle::zero())) + }, + generic::TransformOperation::RotateZ(_) => { + Ok(generic::TransformOperation::RotateZ(Angle::zero())) + }, + generic::TransformOperation::Rotate(_) => { + Ok(generic::TransformOperation::Rotate(Angle::zero())) + }, + generic::TransformOperation::Perspective(_) => Ok( + generic::TransformOperation::Perspective(generic::PerspectiveFunction::None), + ), + generic::TransformOperation::AccumulateMatrix { .. } | + generic::TransformOperation::InterpolateMatrix { .. } => { + // AccumulateMatrix/InterpolateMatrix: We do interpolation on + // AccumulateMatrix/InterpolateMatrix by reading it as a ComputedMatrix + // (with layout information), and then do matrix interpolation. + // + // Therefore, we use an identity matrix to represent the identity transform list. + // http://dev.w3.org/csswg/css-transforms/#identity-transform-function + Ok(generic::TransformOperation::Matrix3D(Matrix3D::identity())) + }, + } + } +} + +impl ToAnimatedZero for Transform { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(generic::Transform( + self.0 + .iter() + .map(|op| op.to_animated_zero()) + .collect::<Result<crate::OwnedSlice<_>, _>>()?, + )) + } +} + +/// A computed CSS `rotate` +pub type Rotate = generic::GenericRotate<Number, Angle>; + +/// A computed CSS `translate` +pub type Translate = generic::GenericTranslate<LengthPercentage, Length>; + +/// A computed CSS `scale` +pub type Scale = generic::GenericScale<Number>; diff --git a/servo/components/style/values/computed/ui.rs b/servo/components/style/values/computed/ui.rs new file mode 100644 index 0000000000..f285c0626b --- /dev/null +++ b/servo/components/style/values/computed/ui.rs @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for UI properties + +use crate::values::computed::color::Color; +use crate::values::computed::image::Image; +use crate::values::computed::Number; +use crate::values::generics::ui as generics; + +pub use crate::values::specified::ui::{BoolInteger, CursorKind, MozTheme, UserSelect}; + +/// A computed value for the `cursor` property. +pub type Cursor = generics::GenericCursor<CursorImage>; + +/// A computed value for item of `image cursors`. +pub type CursorImage = generics::GenericCursorImage<Image, Number>; + +/// A computed value for `scrollbar-color` property. +pub type ScrollbarColor = generics::GenericScrollbarColor<Color>; diff --git a/servo/components/style/values/computed/url.rs b/servo/components/style/values/computed/url.rs new file mode 100644 index 0000000000..9f0d8f5bb3 --- /dev/null +++ b/servo/components/style/values/computed/url.rs @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common handling for the computed value CSS url() values. + +use crate::values::generics::url::UrlOrNone as GenericUrlOrNone; + +#[cfg(feature = "gecko")] +pub use crate::gecko::url::{ComputedImageUrl, ComputedUrl}; +#[cfg(feature = "servo")] +pub use crate::servo::url::{ComputedImageUrl, ComputedUrl}; + +/// Computed <url> | <none> +pub type UrlOrNone = GenericUrlOrNone<ComputedUrl>; diff --git a/servo/components/style/values/distance.rs b/servo/components/style/values/distance.rs new file mode 100644 index 0000000000..fef376cf5f --- /dev/null +++ b/servo/components/style/values/distance.rs @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Machinery to compute distances between animatable values. + +use app_units::Au; +use euclid::default::Size2D; +use std::iter::Sum; +use std::ops::Add; + +/// A trait to compute squared distances between two animatable values. +/// +/// This trait is derivable with `#[derive(ComputeSquaredDistance)]`. The derived +/// implementation uses a `match` expression with identical patterns for both +/// `self` and `other`, calling `ComputeSquaredDistance::compute_squared_distance` +/// on each fields of the values. +/// +/// If a variant is annotated with `#[animation(error)]`, the corresponding +/// `match` arm returns an error. +/// +/// Trait bounds for type parameter `Foo` can be opted out of with +/// `#[animation(no_bound(Foo))]` on the type definition, trait bounds for +/// fields can be opted into with `#[distance(field_bound)]` on the field. +pub trait ComputeSquaredDistance { + /// Computes the squared distance between two animatable values. + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()>; +} + +/// A distance between two animatable values. +#[derive(Add, Clone, Copy, Debug, From)] +pub struct SquaredDistance { + value: f64, +} + +impl SquaredDistance { + /// Returns a squared distance from its square root. + #[inline] + pub fn from_sqrt(sqrt: f64) -> Self { + Self { value: sqrt * sqrt } + } +} + +impl ComputeSquaredDistance for u16 { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(SquaredDistance::from_sqrt( + ((*self as f64) - (*other as f64)).abs(), + )) + } +} + +impl ComputeSquaredDistance for i16 { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64)) + } +} + +impl ComputeSquaredDistance for i32 { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64)) + } +} + +impl ComputeSquaredDistance for f32 { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64)) + } +} + +impl ComputeSquaredDistance for f64 { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(SquaredDistance::from_sqrt((*self - *other).abs())) + } +} + +impl ComputeSquaredDistance for Au { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + self.0.compute_squared_distance(&other.0) + } +} + +impl<T> ComputeSquaredDistance for Box<T> +where + T: ComputeSquaredDistance, +{ + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + (**self).compute_squared_distance(&**other) + } +} + +impl<T> ComputeSquaredDistance for Option<T> +where + T: ComputeSquaredDistance, +{ + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + match (self.as_ref(), other.as_ref()) { + (Some(this), Some(other)) => this.compute_squared_distance(other), + (None, None) => Ok(SquaredDistance::from_sqrt(0.)), + _ => Err(()), + } + } +} + +impl<T> ComputeSquaredDistance for Size2D<T> +where + T: ComputeSquaredDistance, +{ + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + Ok(self.width.compute_squared_distance(&other.width)? + + self.height.compute_squared_distance(&other.height)?) + } +} + +impl SquaredDistance { + /// Returns the square root of this squared distance. + #[inline] + pub fn sqrt(self) -> f64 { + self.value.sqrt() + } +} + +impl Sum for SquaredDistance { + fn sum<I>(iter: I) -> Self + where + I: Iterator<Item = Self>, + { + iter.fold(SquaredDistance::from_sqrt(0.), Add::add) + } +} diff --git a/servo/components/style/values/generics/animation.rs b/servo/components/style/values/generics/animation.rs new file mode 100644 index 0000000000..edee9e9f25 --- /dev/null +++ b/servo/components/style/values/generics/animation.rs @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic values for properties related to animations and transitions. + +use crate::values::generics::length::GenericLengthPercentageOrAuto; +use crate::values::specified::animation::{ScrollAxis, ScrollFunction}; +use crate::values::TimelineName; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// The view() notation. +/// https://drafts.csswg.org/scroll-animations-1/#view-notation +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(function = "view")] +#[repr(C)] +pub struct GenericViewFunction<LengthPercent> { + /// The axis of scrolling that drives the progress of the timeline. + #[css(skip_if = "ScrollAxis::is_default")] + pub axis: ScrollAxis, + /// An adjustment of the view progress visibility range. + #[css(skip_if = "GenericViewTimelineInset::is_auto")] + #[css(field_bound)] + pub inset: GenericViewTimelineInset<LengthPercent>, +} + +pub use self::GenericViewFunction as ViewFunction; + +/// A value for the <single-animation-timeline>. +/// +/// https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericAnimationTimeline<LengthPercent> { + /// Use default timeline. The animation’s timeline is a DocumentTimeline. + Auto, + /// The scroll-timeline name or view-timeline-name. + /// https://drafts.csswg.org/scroll-animations-1/#scroll-timelines-named + /// https://drafts.csswg.org/scroll-animations-1/#view-timeline-name + Timeline(TimelineName), + /// The scroll() notation. + /// https://drafts.csswg.org/scroll-animations-1/#scroll-notation + Scroll(ScrollFunction), + /// The view() notation. + /// https://drafts.csswg.org/scroll-animations-1/#view-notation + View(#[css(field_bound)] GenericViewFunction<LengthPercent>), +} + +pub use self::GenericAnimationTimeline as AnimationTimeline; + +impl<LengthPercent> AnimationTimeline<LengthPercent> { + /// Returns the `auto` value. + pub fn auto() -> Self { + Self::Auto + } + + /// Returns true if it is auto (i.e. the default value). + pub fn is_auto(&self) -> bool { + matches!(self, Self::Auto) + } +} + +/// A generic value for the `[ [ auto | <length-percentage> ]{1,2} ]`. +/// +/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericViewTimelineInset<LengthPercent> { + /// The start inset in the relevant axis. + pub start: GenericLengthPercentageOrAuto<LengthPercent>, + /// The end inset. + pub end: GenericLengthPercentageOrAuto<LengthPercent>, +} + +pub use self::GenericViewTimelineInset as ViewTimelineInset; + +impl<LengthPercent> ViewTimelineInset<LengthPercent> { + /// Returns true if it is auto. + #[inline] + fn is_auto(&self) -> bool { + self.start.is_auto() && self.end.is_auto() + } +} + +impl<LengthPercent> ToCss for ViewTimelineInset<LengthPercent> +where + LengthPercent: PartialEq + ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.start.to_css(dest)?; + if self.end != self.start { + dest.write_char(' ')?; + self.end.to_css(dest)?; + } + Ok(()) + } +} + +impl<LengthPercent> Default for ViewTimelineInset<LengthPercent> { + fn default() -> Self { + Self { + start: GenericLengthPercentageOrAuto::auto(), + end: GenericLengthPercentageOrAuto::auto(), + } + } +} diff --git a/servo/components/style/values/generics/background.rs b/servo/components/style/values/generics/background.rs new file mode 100644 index 0000000000..d9b6624595 --- /dev/null +++ b/servo/components/style/values/generics/background.rs @@ -0,0 +1,54 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values related to backgrounds. + +use crate::values::generics::length::{GenericLengthPercentageOrAuto, LengthPercentageOrAuto}; + +/// A generic value for the `background-size` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericBackgroundSize<LengthPercent> { + /// `<width> <height>` + ExplicitSize { + /// Explicit width. + width: GenericLengthPercentageOrAuto<LengthPercent>, + /// Explicit height. + #[css(skip_if = "GenericLengthPercentageOrAuto::is_auto")] + height: GenericLengthPercentageOrAuto<LengthPercent>, + }, + /// `cover` + #[animation(error)] + Cover, + /// `contain` + #[animation(error)] + Contain, +} + +pub use self::GenericBackgroundSize as BackgroundSize; + +impl<LengthPercentage> BackgroundSize<LengthPercentage> { + /// Returns `auto auto`. + pub fn auto() -> Self { + GenericBackgroundSize::ExplicitSize { + width: LengthPercentageOrAuto::Auto, + height: LengthPercentageOrAuto::Auto, + } + } +} diff --git a/servo/components/style/values/generics/basic_shape.rs b/servo/components/style/values/generics/basic_shape.rs new file mode 100644 index 0000000000..13d27995c1 --- /dev/null +++ b/servo/components/style/values/generics/basic_shape.rs @@ -0,0 +1,567 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the [`basic-shape`](https://drafts.csswg.org/css-shapes/#typedef-basic-shape) +//! types that are generic over their `ToCss` implementations. + +use crate::values::animated::{lists, Animate, Procedure, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::border::GenericBorderRadius; +use crate::values::generics::position::GenericPositionOrAuto; +use crate::values::generics::rect::Rect; +use crate::values::specified::SVGPathData; +use crate::Zero; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// <https://drafts.fxtf.org/css-masking-1/#typedef-geometry-box> +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ShapeGeometryBox { + /// Depending on which kind of element this style value applied on, the + /// default value of the reference-box can be different. For an HTML + /// element, the default value of reference-box is border-box; for an SVG + /// element, the default value is fill-box. Since we can not determine the + /// default value at parsing time, we keep this value to make a decision on + /// it. + #[css(skip)] + ElementDependent, + FillBox, + StrokeBox, + ViewBox, + ShapeBox(ShapeBox), +} + +impl Default for ShapeGeometryBox { + fn default() -> Self { + Self::ElementDependent + } +} + +/// Skip the serialization if the author omits the box or specifies border-box. +#[inline] +fn is_default_box_for_clip_path(b: &ShapeGeometryBox) -> bool { + // Note: for clip-path, ElementDependent is always border-box, so we have to check both of them + // for serialization. + matches!(b, ShapeGeometryBox::ElementDependent) || + matches!(b, ShapeGeometryBox::ShapeBox(ShapeBox::BorderBox)) +} + +/// https://drafts.csswg.org/css-shapes-1/#typedef-shape-box +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Animate, + Clone, + Copy, + ComputeSquaredDistance, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ShapeBox { + MarginBox, + BorderBox, + PaddingBox, + ContentBox, +} + +impl Default for ShapeBox { + fn default() -> Self { + ShapeBox::MarginBox + } +} + +/// A value for the `clip-path` property. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[animation(no_bound(U))] +#[repr(u8)] +pub enum GenericClipPath<BasicShape, U> { + #[animation(error)] + None, + #[animation(error)] + Url(U), + Shape( + Box<BasicShape>, + #[css(skip_if = "is_default_box_for_clip_path")] ShapeGeometryBox, + ), + #[animation(error)] + Box(ShapeGeometryBox), +} + +pub use self::GenericClipPath as ClipPath; + +/// A value for the `shape-outside` property. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[animation(no_bound(I))] +#[repr(u8)] +pub enum GenericShapeOutside<BasicShape, I> { + #[animation(error)] + None, + #[animation(error)] + Image(I), + Shape(Box<BasicShape>, #[css(skip_if = "is_default")] ShapeBox), + #[animation(error)] + Box(ShapeBox), +} + +pub use self::GenericShapeOutside as ShapeOutside; + +/// The <basic-shape>. +/// +/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, BasicShapeRect> +{ + /// The <basic-shape-rect>. + Rect(BasicShapeRect), + /// Defines a circle with a center and a radius. + Circle( + #[css(field_bound)] + #[shmem(field_bound)] + Circle<Position, NonNegativeLengthPercentage>, + ), + /// Defines an ellipse with a center and x-axis/y-axis radii. + Ellipse( + #[css(field_bound)] + #[shmem(field_bound)] + Ellipse<Position, NonNegativeLengthPercentage>, + ), + /// Defines a polygon with pair arguments. + Polygon(GenericPolygon<LengthPercentage>), + /// Defines a path with SVG path syntax. + Path(Path), + // TODO: Bug 1823463. Add shape(). + // https://drafts.csswg.org/css-shapes-2/#shape-function +} + +pub use self::GenericBasicShape as BasicShape; + +/// <https://drafts.csswg.org/css-shapes/#funcdef-inset> +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(function = "inset")] +#[repr(C)] +pub struct GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage> { + pub rect: Rect<LengthPercentage>, + #[shmem(field_bound)] + pub round: GenericBorderRadius<NonNegativeLengthPercentage>, +} + +pub use self::GenericInsetRect as InsetRect; + +/// <https://drafts.csswg.org/css-shapes/#funcdef-circle> +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(function)] +#[repr(C)] +pub struct Circle<Position, NonNegativeLengthPercentage> { + pub position: GenericPositionOrAuto<Position>, + pub radius: GenericShapeRadius<NonNegativeLengthPercentage>, +} + +/// <https://drafts.csswg.org/css-shapes/#funcdef-ellipse> +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(function)] +#[repr(C)] +pub struct Ellipse<Position, NonNegativeLengthPercentage> { + pub position: GenericPositionOrAuto<Position>, + pub semiaxis_x: GenericShapeRadius<NonNegativeLengthPercentage>, + pub semiaxis_y: GenericShapeRadius<NonNegativeLengthPercentage>, +} + +/// <https://drafts.csswg.org/css-shapes/#typedef-shape-radius> +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericShapeRadius<NonNegativeLengthPercentage> { + Length(NonNegativeLengthPercentage), + #[animation(error)] + ClosestSide, + #[animation(error)] + FarthestSide, +} + +pub use self::GenericShapeRadius as ShapeRadius; + +/// A generic type for representing the `polygon()` function +/// +/// <https://drafts.csswg.org/css-shapes/#funcdef-polygon> +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma, function = "polygon")] +#[repr(C)] +pub struct GenericPolygon<LengthPercentage> { + /// The filling rule for a polygon. + #[css(skip_if = "is_default")] + pub fill: FillRule, + /// A collection of (x, y) coordinates to draw the polygon. + #[css(iterable)] + pub coordinates: crate::OwnedSlice<PolygonCoord<LengthPercentage>>, +} + +pub use self::GenericPolygon as Polygon; + +/// Coordinates for Polygon. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct PolygonCoord<LengthPercentage>(pub LengthPercentage, pub LengthPercentage); + +// https://drafts.csswg.org/css-shapes/#typedef-fill-rule +// NOTE: Basic shapes spec says that these are the only two values, however +// https://www.w3.org/TR/SVG/painting.html#FillRuleProperty +// says that it can also be `inherit` +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + Eq, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum FillRule { + Nonzero, + Evenodd, +} + +/// The path function defined in css-shape-2. +/// +/// https://drafts.csswg.org/css-shapes-2/#funcdef-path +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma, function = "path")] +#[repr(C)] +pub struct Path { + /// The filling rule for the svg path. + #[css(skip_if = "is_default")] + pub fill: FillRule, + /// The svg path data. + pub path: SVGPathData, +} + +impl<B, U> ToAnimatedZero for ClipPath<B, U> { + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +impl<B, U> ToAnimatedZero for ShapeOutside<B, U> { + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +impl<Length, NonNegativeLength> ToCss for InsetRect<Length, NonNegativeLength> +where + Length: ToCss + PartialEq, + NonNegativeLength: ToCss + PartialEq + Zero, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("inset(")?; + self.rect.to_css(dest)?; + if !self.round.is_zero() { + dest.write_str(" round ")?; + self.round.to_css(dest)?; + } + dest.write_char(')') + } +} + +impl<Position, NonNegativeLengthPercentage> ToCss for Circle<Position, NonNegativeLengthPercentage> +where + Position: ToCss, + NonNegativeLengthPercentage: ToCss + PartialEq, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let has_radius = self.radius != Default::default(); + + dest.write_str("circle(")?; + if has_radius { + self.radius.to_css(dest)?; + } + + // Preserve the `at <position>` even if it specified the default value. + // https://github.com/w3c/csswg-drafts/issues/8695 + if !matches!(self.position, GenericPositionOrAuto::Auto) { + if has_radius { + dest.write_char(' ')?; + } + dest.write_str("at ")?; + self.position.to_css(dest)?; + } + dest.write_char(')') + } +} + +impl<Position, NonNegativeLengthPercentage> ToCss for Ellipse<Position, NonNegativeLengthPercentage> +where + Position: ToCss, + NonNegativeLengthPercentage: ToCss + PartialEq, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let has_radii = + self.semiaxis_x != Default::default() || self.semiaxis_y != Default::default(); + + dest.write_str("ellipse(")?; + if has_radii { + self.semiaxis_x.to_css(dest)?; + dest.write_char(' ')?; + self.semiaxis_y.to_css(dest)?; + } + + // Preserve the `at <position>` even if it specified the default value. + // https://github.com/w3c/csswg-drafts/issues/8695 + if !matches!(self.position, GenericPositionOrAuto::Auto) { + if has_radii { + dest.write_char(' ')?; + } + dest.write_str("at ")?; + self.position.to_css(dest)?; + } + dest.write_char(')') + } +} + +impl<L> Default for ShapeRadius<L> { + #[inline] + fn default() -> Self { + ShapeRadius::ClosestSide + } +} + +impl<L> Animate for Polygon<L> +where + L: Animate, +{ + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.fill != other.fill { + return Err(()); + } + let coordinates = + lists::by_computed_value::animate(&self.coordinates, &other.coordinates, procedure)?; + Ok(Polygon { + fill: self.fill, + coordinates, + }) + } +} + +impl<L> ComputeSquaredDistance for Polygon<L> +where + L: ComputeSquaredDistance, +{ + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.fill != other.fill { + return Err(()); + } + lists::by_computed_value::squared_distance(&self.coordinates, &other.coordinates) + } +} + +impl Default for FillRule { + #[inline] + fn default() -> Self { + FillRule::Nonzero + } +} + +#[inline] +fn is_default<T: Default + PartialEq>(fill: &T) -> bool { + *fill == Default::default() +} diff --git a/servo/components/style/values/generics/border.rs b/servo/components/style/values/generics/border.rs new file mode 100644 index 0000000000..feb80998d1 --- /dev/null +++ b/servo/components/style/values/generics/border.rs @@ -0,0 +1,261 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values related to borders. + +use crate::values::generics::rect::Rect; +use crate::values::generics::size::Size2D; +use crate::Zero; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A generic value for a single side of a `border-image-width` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericBorderImageSideWidth<LP, N> { + /// `<number>` + /// + /// NOTE: Numbers need to be before length-percentagess, in order to parse + /// them first, since `0` should be a number, not the `0px` length. + Number(N), + /// `<length-or-percentage>` + LengthPercentage(LP), + /// `auto` + Auto, +} + +pub use self::GenericBorderImageSideWidth as BorderImageSideWidth; + +/// A generic value for the `border-image-slice` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericBorderImageSlice<NumberOrPercentage> { + /// The offsets. + #[css(field_bound)] + pub offsets: Rect<NumberOrPercentage>, + /// Whether to fill the middle part. + #[animation(constant)] + #[css(represents_keyword)] + pub fill: bool, +} + +pub use self::GenericBorderImageSlice as BorderImageSlice; + +/// A generic value for the `border-*-radius` longhand properties. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + Serialize, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericBorderCornerRadius<L>( + #[css(field_bound)] + #[shmem(field_bound)] + pub Size2D<L>, +); + +pub use self::GenericBorderCornerRadius as BorderCornerRadius; + +impl<L> BorderCornerRadius<L> { + /// Trivially create a `BorderCornerRadius`. + pub fn new(w: L, h: L) -> Self { + BorderCornerRadius(Size2D::new(w, h)) + } +} + +impl<L: Zero> Zero for BorderCornerRadius<L> { + fn zero() -> Self { + BorderCornerRadius(Size2D::zero()) + } + + fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +/// A generic value for the `border-spacing` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct BorderSpacing<L>( + #[css(field_bound)] + #[shmem(field_bound)] + pub Size2D<L>, +); + +impl<L> BorderSpacing<L> { + /// Trivially create a `BorderCornerRadius`. + pub fn new(w: L, h: L) -> Self { + BorderSpacing(Size2D::new(w, h)) + } +} + +/// A generic value for `border-radius` and `inset()`. +/// +/// <https://drafts.csswg.org/css-backgrounds-3/#border-radius> +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + Serialize, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericBorderRadius<LengthPercentage> { + /// The top left radius. + #[shmem(field_bound)] + pub top_left: GenericBorderCornerRadius<LengthPercentage>, + /// The top right radius. + pub top_right: GenericBorderCornerRadius<LengthPercentage>, + /// The bottom right radius. + pub bottom_right: GenericBorderCornerRadius<LengthPercentage>, + /// The bottom left radius. + pub bottom_left: GenericBorderCornerRadius<LengthPercentage>, +} + +pub use self::GenericBorderRadius as BorderRadius; + +impl<L> BorderRadius<L> { + /// Returns a new `BorderRadius<L>`. + #[inline] + pub fn new( + tl: BorderCornerRadius<L>, + tr: BorderCornerRadius<L>, + br: BorderCornerRadius<L>, + bl: BorderCornerRadius<L>, + ) -> Self { + BorderRadius { + top_left: tl, + top_right: tr, + bottom_right: br, + bottom_left: bl, + } + } + + /// Serialises two given rects following the syntax of the `border-radius`` + /// property. + pub fn serialize_rects<W>( + widths: Rect<&L>, + heights: Rect<&L>, + dest: &mut CssWriter<W>, + ) -> fmt::Result + where + L: PartialEq + ToCss, + W: Write, + { + widths.to_css(dest)?; + if widths != heights { + dest.write_str(" / ")?; + heights.to_css(dest)?; + } + Ok(()) + } +} + +impl<L: Zero> Zero for BorderRadius<L> { + fn zero() -> Self { + Self::new( + BorderCornerRadius::<L>::zero(), + BorderCornerRadius::<L>::zero(), + BorderCornerRadius::<L>::zero(), + BorderCornerRadius::<L>::zero(), + ) + } + + fn is_zero(&self) -> bool { + self.top_left.is_zero() && + self.top_right.is_zero() && + self.bottom_right.is_zero() && + self.bottom_left.is_zero() + } +} + +impl<L> ToCss for BorderRadius<L> +where + L: PartialEq + ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let BorderRadius { + top_left: BorderCornerRadius(ref tl), + top_right: BorderCornerRadius(ref tr), + bottom_right: BorderCornerRadius(ref br), + bottom_left: BorderCornerRadius(ref bl), + } = *self; + + let widths = Rect::new(&tl.width, &tr.width, &br.width, &bl.width); + let heights = Rect::new(&tl.height, &tr.height, &br.height, &bl.height); + + Self::serialize_rects(widths, heights, dest) + } +} diff --git a/servo/components/style/values/generics/box.rs b/servo/components/style/values/generics/box.rs new file mode 100644 index 0000000000..12c5f28bfb --- /dev/null +++ b/servo/components/style/values/generics/box.rs @@ -0,0 +1,211 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for box properties. + +use crate::values::animated::ToAnimatedZero; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + FromPrimitive, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum VerticalAlignKeyword { + Baseline, + Sub, + Super, + Top, + TextTop, + Middle, + Bottom, + TextBottom, + #[cfg(feature = "gecko")] + MozMiddleWithBaseline, +} + +/// A generic value for the `vertical-align` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericVerticalAlign<LengthPercentage> { + /// One of the vertical-align keywords. + Keyword(VerticalAlignKeyword), + /// `<length-percentage>` + Length(LengthPercentage), +} + +pub use self::GenericVerticalAlign as VerticalAlign; + +impl<L> VerticalAlign<L> { + /// Returns `baseline`. + #[inline] + pub fn baseline() -> Self { + VerticalAlign::Keyword(VerticalAlignKeyword::Baseline) + } +} + +impl<L> ToAnimatedZero for VerticalAlign<L> { + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +/// https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToAnimatedValue, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "auto")] +#[repr(C, u8)] +pub enum GenericContainIntrinsicSize<L> { + /// The keyword `none`. + None, + /// The keywords 'auto none', + AutoNone, + /// A non-negative length. + Length(L), + /// "auto <Length>" + AutoLength(L), +} + +pub use self::GenericContainIntrinsicSize as ContainIntrinsicSize; + +impl<L: ToCss> ToCss for ContainIntrinsicSize<L> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Self::None => dest.write_str("none"), + Self::AutoNone => dest.write_str("auto none"), + Self::Length(ref l) => l.to_css(dest), + Self::AutoLength(ref l) => { + dest.write_str("auto ")?; + l.to_css(dest) + }, + } + } +} + +/// Note that we only implement -webkit-line-clamp as a single, longhand +/// property for now, but the spec defines line-clamp as a shorthand for +/// separate max-lines, block-ellipsis, and continue properties. +/// +/// https://drafts.csswg.org/css-overflow-3/#line-clamp +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToAnimatedValue, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +#[value_info(other_values = "none")] +pub struct GenericLineClamp<I>(pub I); + +pub use self::GenericLineClamp as LineClamp; + +impl<I: crate::Zero> LineClamp<I> { + /// Returns the `none` value. + pub fn none() -> Self { + Self(crate::Zero::zero()) + } + + /// Returns whether we're the `none` value. + pub fn is_none(&self) -> bool { + self.0.is_zero() + } +} + +impl<I: crate::Zero + ToCss> ToCss for LineClamp<I> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_none() { + return dest.write_str("none"); + } + self.0.to_css(dest) + } +} + +/// A generic value for the `perspective` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericPerspective<NonNegativeLength> { + /// A non-negative length. + Length(NonNegativeLength), + /// The keyword `none`. + None, +} + +pub use self::GenericPerspective as Perspective; + +impl<L> Perspective<L> { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + Perspective::None + } +} diff --git a/servo/components/style/values/generics/calc.rs b/servo/components/style/values/generics/calc.rs new file mode 100644 index 0000000000..abcb5fe6eb --- /dev/null +++ b/servo/components/style/values/generics/calc.rs @@ -0,0 +1,1820 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Calc expressions][calc]. +//! +//! [calc]: https://drafts.csswg.org/css-values/#calc-notation + +use num_traits::Zero; +use smallvec::SmallVec; +use std::fmt::{self, Write}; +use std::ops::{Add, Mul, Neg, Rem, Sub}; +use std::{cmp, mem}; +use style_traits::{CssWriter, ToCss}; + +/// Whether we're a `min` or `max` function. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum MinMaxOp { + /// `min()` + Min, + /// `max()` + Max, +} + +/// Whether we're a `mod` or `rem` function. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ModRemOp { + /// `mod()` + Mod, + /// `rem()` + Rem, +} + +impl ModRemOp { + fn apply(self, dividend: f32, divisor: f32) -> f32 { + // In mod(A, B) only, if B is infinite and A has opposite sign to B + // (including an oppositely-signed zero), the result is NaN. + // https://drafts.csswg.org/css-values/#round-infinities + if matches!(self, Self::Mod) && + divisor.is_infinite() && + dividend.is_sign_negative() != divisor.is_sign_negative() + { + return f32::NAN; + } + + let (r, same_sign_as) = match self { + Self::Mod => (dividend - divisor * (dividend / divisor).floor(), divisor), + Self::Rem => (dividend - divisor * (dividend / divisor).trunc(), dividend), + }; + if r == 0.0 && same_sign_as.is_sign_negative() { + -0.0 + } else { + r + } + } +} + +/// The strategy used in `round()` +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum RoundingStrategy { + /// `round(nearest, a, b)` + /// round a to the nearest multiple of b + Nearest, + /// `round(up, a, b)` + /// round a up to the nearest multiple of b + Up, + /// `round(down, a, b)` + /// round a down to the nearest multiple of b + Down, + /// `round(to-zero, a, b)` + /// round a to the nearest multiple of b that is towards zero + ToZero, +} + +/// This determines the order in which we serialize members of a calc() sum. +/// +/// See https://drafts.csswg.org/css-values-4/#sort-a-calculations-children +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +#[allow(missing_docs)] +pub enum SortKey { + Number, + Percentage, + Cap, + Ch, + Cqb, + Cqh, + Cqi, + Cqmax, + Cqmin, + Cqw, + Deg, + Dppx, + Dvb, + Dvh, + Dvi, + Dvmax, + Dvmin, + Dvw, + Em, + Ex, + Ic, + Lh, + Lvb, + Lvh, + Lvi, + Lvmax, + Lvmin, + Lvw, + Px, + Rem, + Rlh, + Sec, + Svb, + Svh, + Svi, + Svmax, + Svmin, + Svw, + Vb, + Vh, + Vi, + Vmax, + Vmin, + Vw, + Other, +} + +/// A generic node in a calc expression. +/// +/// FIXME: This would be much more elegant if we used `Self` in the types below, +/// but we can't because of https://github.com/serde-rs/serde/issues/1565. +/// +/// FIXME: The following annotations are to workaround an LLVM inlining bug, see +/// bug 1631929. +/// +/// cbindgen:destructor-attributes=MOZ_NEVER_INLINE +/// cbindgen:copy-constructor-attributes=MOZ_NEVER_INLINE +/// cbindgen:eq-attributes=MOZ_NEVER_INLINE +#[repr(u8)] +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +pub enum GenericCalcNode<L> { + /// A leaf node. + Leaf(L), + /// A node that negates its child, e.g. Negate(1) == -1. + Negate(Box<GenericCalcNode<L>>), + /// A node that inverts its child, e.g. Invert(10) == 1 / 10 == 0.1. The child must always + /// resolve to a number unit. + Invert(Box<GenericCalcNode<L>>), + /// A sum node, representing `a + b + c` where a, b, and c are the + /// arguments. + Sum(crate::OwnedSlice<GenericCalcNode<L>>), + /// A product node, representing `a * b * c` where a, b, and c are the + /// arguments. + Product(crate::OwnedSlice<GenericCalcNode<L>>), + /// A `min` or `max` function. + MinMax(crate::OwnedSlice<GenericCalcNode<L>>, MinMaxOp), + /// A `clamp()` function. + Clamp { + /// The minimum value. + min: Box<GenericCalcNode<L>>, + /// The central value. + center: Box<GenericCalcNode<L>>, + /// The maximum value. + max: Box<GenericCalcNode<L>>, + }, + /// A `round()` function. + Round { + /// The rounding strategy. + strategy: RoundingStrategy, + /// The value to round. + value: Box<GenericCalcNode<L>>, + /// The step value. + step: Box<GenericCalcNode<L>>, + }, + /// A `mod()` or `rem()` function. + ModRem { + /// The dividend calculation. + dividend: Box<GenericCalcNode<L>>, + /// The divisor calculation. + divisor: Box<GenericCalcNode<L>>, + /// Is the function mod or rem? + op: ModRemOp, + }, + /// A `hypot()` function + Hypot(crate::OwnedSlice<GenericCalcNode<L>>), + /// An `abs()` function. + Abs(Box<GenericCalcNode<L>>), + /// A `sign()` function. + Sign(Box<GenericCalcNode<L>>), +} + +pub use self::GenericCalcNode as CalcNode; + +bitflags! { + /// Expected units we allow parsing within a `calc()` expression. + /// + /// This is used as a hint for the parser to fast-reject invalid + /// expressions. Numbers are always allowed because they multiply other + /// units. + #[derive(Clone, Copy, PartialEq, Eq)] + pub struct CalcUnits: u8 { + /// <length> + const LENGTH = 1 << 0; + /// <percentage> + const PERCENTAGE = 1 << 1; + /// <angle> + const ANGLE = 1 << 2; + /// <time> + const TIME = 1 << 3; + /// <resolution> + const RESOLUTION = 1 << 4; + + /// <length-percentage> + const LENGTH_PERCENTAGE = Self::LENGTH.bits() | Self::PERCENTAGE.bits(); + // NOTE: When you add to this, make sure to make Atan2 deal with these. + /// Allow all units. + const ALL = Self::LENGTH.bits() | Self::PERCENTAGE.bits() | Self::ANGLE.bits() | Self::TIME.bits() | Self::RESOLUTION.bits(); + } +} + +impl CalcUnits { + /// Returns whether the flags only represent a single unit. This will return true for 0, which + /// is a "number" this is also fine. + #[inline] + fn is_single_unit(&self) -> bool { + self.bits() == 0 || self.bits() & (self.bits() - 1) == 0 + } + + /// Returns true if this unit is allowed to be summed with the given unit, otherwise false. + #[inline] + fn can_sum_with(&self, other: Self) -> bool { + match *self { + Self::LENGTH => other.intersects(Self::LENGTH | Self::PERCENTAGE), + Self::PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE), + Self::LENGTH_PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE), + u => u.is_single_unit() && other == u, + } + } +} + +/// For percentage resolution, sometimes we can't assume that the percentage basis is positive (so +/// we don't know whether a percentage is larger than another). +pub enum PositivePercentageBasis { + /// The percent basis is not known-positive, we can't compare percentages. + Unknown, + /// The percent basis is known-positive, we assume larger percentages are larger. + Yes, +} + +macro_rules! compare_helpers { + () => { + /// Return whether a leaf is greater than another. + #[allow(unused)] + fn gt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool { + self.compare(other, basis_positive) == Some(cmp::Ordering::Greater) + } + + /// Return whether a leaf is less than another. + fn lt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool { + self.compare(other, basis_positive) == Some(cmp::Ordering::Less) + } + + /// Return whether a leaf is smaller or equal than another. + fn lte(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool { + match self.compare(other, basis_positive) { + Some(cmp::Ordering::Less) => true, + Some(cmp::Ordering::Equal) => true, + Some(cmp::Ordering::Greater) => false, + None => false, + } + } + }; +} + +/// A trait that represents all the stuff a valid leaf of a calc expression. +pub trait CalcNodeLeaf: Clone + Sized + PartialEq + ToCss { + /// Returns the unit of the leaf. + fn unit(&self) -> CalcUnits; + + /// Returns the unitless value of this leaf. + fn unitless_value(&self) -> f32; + + /// Return true if the units of both leaves are equal. (NOTE: Does not take + /// the values into account) + fn is_same_unit_as(&self, other: &Self) -> bool { + std::mem::discriminant(self) == std::mem::discriminant(other) + } + + /// Do a partial comparison of these values. + fn compare( + &self, + other: &Self, + base_is_positive: PositivePercentageBasis, + ) -> Option<cmp::Ordering>; + compare_helpers!(); + + /// Create a new leaf with a number value. + fn new_number(value: f32) -> Self; + + /// Returns a float value if the leaf is a number. + fn as_number(&self) -> Option<f32>; + + /// Whether this value is known-negative. + fn is_negative(&self) -> bool { + self.unitless_value().is_sign_negative() + } + + /// Whether this value is infinite. + fn is_infinite(&self) -> bool { + self.unitless_value().is_infinite() + } + + /// Whether this value is zero. + fn is_zero(&self) -> bool { + self.unitless_value().is_zero() + } + + /// Whether this value is NaN. + fn is_nan(&self) -> bool { + self.unitless_value().is_nan() + } + + /// Tries to merge one leaf into another using the sum, that is, perform `x` + `y`. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()>; + + /// Try to merge the right leaf into the left by using a multiplication. Return true if the + /// merge was successful, otherwise false. + fn try_product_in_place(&mut self, other: &mut Self) -> bool; + + /// Tries a generic arithmetic operation. + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32; + + /// Map the value of this node with the given operation. + fn map(&mut self, op: impl FnMut(f32) -> f32); + + /// Negates the leaf. + fn negate(&mut self) { + self.map(std::ops::Neg::neg); + } + + /// Canonicalizes the expression if necessary. + fn simplify(&mut self); + + /// Returns the sort key for simplification. + fn sort_key(&self) -> SortKey; + + /// Create a new leaf containing the sign() result of the given leaf. + fn sign_from(leaf: &impl CalcNodeLeaf) -> Self { + Self::new_number(if leaf.is_nan() { + f32::NAN + } else if leaf.is_zero() { + leaf.unitless_value() + } else if leaf.is_negative() { + -1.0 + } else { + 1.0 + }) + } +} + +/// The level of any argument being serialized in `to_css_impl`. +enum ArgumentLevel { + /// The root of a calculation tree. + CalculationRoot, + /// The root of an operand node's argument, e.g. `min(10, 20)`, `10` and `20` will have this + /// level, but min in this case will have `TopMost`. + ArgumentRoot, + /// Any other values serialized in the tree. + Nested, +} + +impl<L: CalcNodeLeaf> CalcNode<L> { + /// Create a dummy CalcNode that can be used to do replacements of other nodes. + fn dummy() -> Self { + Self::MinMax(Default::default(), MinMaxOp::Max) + } + + /// Change all the leaf nodes to have the given value. This is useful when + /// you have `calc(1px * nan)` and you want to replace the product node with + /// `calc(nan)`, in which case the unit will be retained. + fn coerce_to_value(&mut self, value: f32) { + self.map(|_| value); + } + + /// Return true if a product is distributive over this node. + /// Is distributive: (2 + 3) * 4 = 8 + 12 + /// Not distributive: sign(2 + 3) * 4 != sign(8 + 12) + #[inline] + pub fn is_product_distributive(&self) -> bool { + match self { + Self::Leaf(_) => true, + Self::Sum(children) => children.iter().all(|c| c.is_product_distributive()), + _ => false, + } + } + + /// If the node has a valid unit outcome, then return it, otherwise fail. + pub fn unit(&self) -> Result<CalcUnits, ()> { + Ok(match self { + CalcNode::Leaf(l) => l.unit(), + CalcNode::Negate(child) | CalcNode::Invert(child) | CalcNode::Abs(child) => { + child.unit()? + }, + CalcNode::Sum(children) => { + let mut unit = children.first().unwrap().unit()?; + for child in children.iter().skip(1) { + let child_unit = child.unit()?; + if !child_unit.can_sum_with(unit) { + return Err(()); + } + unit |= child_unit; + } + unit + }, + CalcNode::Product(children) => { + // Only one node is allowed to have a unit, the rest must be numbers. + let mut unit = None; + for child in children.iter() { + let child_unit = child.unit()?; + if child_unit.is_empty() { + // Numbers are always allowed in a product, so continue with the next. + continue; + } + + if unit.is_some() { + // We already have a unit for the node, so another unit node is invalid. + return Err(()); + } + + // We have the unit for the node. + unit = Some(child_unit); + } + // We only keep track of specified units, so if we end up with a None and no failure + // so far, then we have a number. + unit.unwrap_or(CalcUnits::empty()) + }, + CalcNode::MinMax(children, _) | CalcNode::Hypot(children) => { + let mut unit = children.first().unwrap().unit()?; + for child in children.iter().skip(1) { + let child_unit = child.unit()?; + if !child_unit.can_sum_with(unit) { + return Err(()); + } + unit |= child_unit; + } + unit + }, + CalcNode::Clamp { min, center, max } => { + let min_unit = min.unit()?; + let center_unit = center.unit()?; + + if !min_unit.can_sum_with(center_unit) { + return Err(()); + } + + let max_unit = max.unit()?; + + if !center_unit.can_sum_with(max_unit) { + return Err(()); + } + + min_unit | center_unit | max_unit + }, + CalcNode::Round { value, step, .. } => { + let value_unit = value.unit()?; + let step_unit = step.unit()?; + if !step_unit.can_sum_with(value_unit) { + return Err(()); + } + value_unit | step_unit + }, + CalcNode::ModRem { + dividend, divisor, .. + } => { + let dividend_unit = dividend.unit()?; + let divisor_unit = divisor.unit()?; + if !divisor_unit.can_sum_with(dividend_unit) { + return Err(()); + } + dividend_unit | divisor_unit + }, + CalcNode::Sign(ref child) => { + // sign() always resolves to a number, but we still need to make sure that the + // child units make sense. + let _ = child.unit()?; + CalcUnits::empty() + }, + }) + } + + /// Negate the node inline. If the node is distributive, it is replaced by the result, + /// otherwise the node is wrapped in a [`Negate`] node. + pub fn negate(&mut self) { + /// Node(params) -> Negate(Node(params)) + fn wrap_self_in_negate<L: CalcNodeLeaf>(s: &mut CalcNode<L>) { + let result = mem::replace(s, CalcNode::dummy()); + *s = CalcNode::Negate(Box::new(result)); + } + + match *self { + CalcNode::Leaf(ref mut leaf) => leaf.negate(), + CalcNode::Negate(ref mut value) => { + // Don't negate the value here. Replace `self` with it's child. + let result = mem::replace(value.as_mut(), Self::dummy()); + *self = result; + }, + CalcNode::Invert(_) => { + // -(1 / -10) == -(-0.1) == 0.1 + wrap_self_in_negate(self) + }, + CalcNode::Sum(ref mut children) => { + for child in children.iter_mut() { + child.negate(); + } + }, + CalcNode::Product(_) => { + // -(2 * 3 / 4) == -(1.5) + wrap_self_in_negate(self); + }, + CalcNode::MinMax(ref mut children, ref mut op) => { + for child in children.iter_mut() { + child.negate(); + } + + // Negating min-max means the operation is swapped. + *op = match *op { + MinMaxOp::Min => MinMaxOp::Max, + MinMaxOp::Max => MinMaxOp::Min, + }; + }, + CalcNode::Clamp { + ref mut min, + ref mut center, + ref mut max, + } => { + if min.lte(max, PositivePercentageBasis::Unknown) { + min.negate(); + center.negate(); + max.negate(); + + mem::swap(min, max); + } else { + wrap_self_in_negate(self); + } + }, + CalcNode::Round { + ref mut strategy, + ref mut value, + ref mut step, + } => { + match *strategy { + RoundingStrategy::Nearest => { + // Nearest is tricky because we'd have to swap the + // behavior at the half-way point from using the upper + // to lower bound. + // Simpler to just wrap self in a negate node. + wrap_self_in_negate(self); + return; + }, + RoundingStrategy::Up => *strategy = RoundingStrategy::Down, + RoundingStrategy::Down => *strategy = RoundingStrategy::Up, + RoundingStrategy::ToZero => (), + } + value.negate(); + step.negate(); + }, + CalcNode::ModRem { + ref mut dividend, + ref mut divisor, + .. + } => { + dividend.negate(); + divisor.negate(); + }, + CalcNode::Hypot(ref mut children) => { + for child in children.iter_mut() { + child.negate(); + } + }, + CalcNode::Abs(_) => { + wrap_self_in_negate(self); + }, + CalcNode::Sign(ref mut child) => { + child.negate(); + }, + } + } + + fn sort_key(&self) -> SortKey { + match *self { + Self::Leaf(ref l) => l.sort_key(), + _ => SortKey::Other, + } + } + + /// Returns the leaf if we can (if simplification has allowed it). + pub fn as_leaf(&self) -> Option<&L> { + match *self { + Self::Leaf(ref l) => Some(l), + _ => None, + } + } + + /// Tries to merge one node into another using the sum, that is, perform `x` + `y`. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { + match (self, other) { + (&mut CalcNode::Leaf(ref mut one), &CalcNode::Leaf(ref other)) => { + one.try_sum_in_place(other) + }, + _ => Err(()), + } + } + + /// Tries to merge one node into another using the product, that is, perform `x` * `y`. + pub fn try_product_in_place(&mut self, other: &mut Self) -> bool { + if let Ok(resolved) = other.resolve() { + if let Some(number) = resolved.as_number() { + if number == 1.0 { + return true; + } + + if self.is_product_distributive() { + self.map(|v| v * number); + return true; + } + } + } + + if let Ok(resolved) = self.resolve() { + if let Some(number) = resolved.as_number() { + if number == 1.0 { + std::mem::swap(self, other); + return true; + } + + if other.is_product_distributive() { + other.map(|v| v * number); + std::mem::swap(self, other); + return true; + } + } + } + + false + } + + /// Tries to apply a generic arithmetic operator + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + match (self, other) { + (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => { + Ok(CalcNode::Leaf(one.try_op(other, op)?)) + }, + _ => Err(()), + } + } + + /// Map the value of this node with the given operation. + pub fn map(&mut self, mut op: impl FnMut(f32) -> f32) { + fn map_internal<L: CalcNodeLeaf>(node: &mut CalcNode<L>, op: &mut impl FnMut(f32) -> f32) { + match node { + CalcNode::Leaf(l) => l.map(op), + CalcNode::Negate(v) | CalcNode::Invert(v) => map_internal(v, op), + CalcNode::Sum(children) | CalcNode::Product(children) => { + for node in &mut **children { + map_internal(node, op); + } + }, + CalcNode::MinMax(children, _) => { + for node in &mut **children { + map_internal(node, op); + } + }, + CalcNode::Clamp { min, center, max } => { + map_internal(min, op); + map_internal(center, op); + map_internal(max, op); + }, + CalcNode::Round { value, step, .. } => { + map_internal(value, op); + map_internal(step, op); + }, + CalcNode::ModRem { + dividend, divisor, .. + } => { + map_internal(dividend, op); + map_internal(divisor, op); + }, + CalcNode::Hypot(children) => { + for node in &mut **children { + map_internal(node, op); + } + }, + CalcNode::Abs(child) | CalcNode::Sign(child) => { + map_internal(child, op); + }, + } + } + + map_internal(self, &mut op); + } + + /// Convert this `CalcNode` into a `CalcNode` with a different leaf kind. + pub fn map_leaves<O, F>(&self, mut map: F) -> CalcNode<O> + where + O: CalcNodeLeaf, + F: FnMut(&L) -> O, + { + self.map_leaves_internal(&mut map) + } + + fn map_leaves_internal<O, F>(&self, map: &mut F) -> CalcNode<O> + where + O: CalcNodeLeaf, + F: FnMut(&L) -> O, + { + fn map_children<L, O, F>( + children: &[CalcNode<L>], + map: &mut F, + ) -> crate::OwnedSlice<CalcNode<O>> + where + L: CalcNodeLeaf, + O: CalcNodeLeaf, + F: FnMut(&L) -> O, + { + children + .iter() + .map(|c| c.map_leaves_internal(map)) + .collect() + } + + match *self { + Self::Leaf(ref l) => CalcNode::Leaf(map(l)), + Self::Negate(ref c) => CalcNode::Negate(Box::new(c.map_leaves_internal(map))), + Self::Invert(ref c) => CalcNode::Invert(Box::new(c.map_leaves_internal(map))), + Self::Sum(ref c) => CalcNode::Sum(map_children(c, map)), + Self::Product(ref c) => CalcNode::Product(map_children(c, map)), + Self::MinMax(ref c, op) => CalcNode::MinMax(map_children(c, map), op), + Self::Clamp { + ref min, + ref center, + ref max, + } => { + let min = Box::new(min.map_leaves_internal(map)); + let center = Box::new(center.map_leaves_internal(map)); + let max = Box::new(max.map_leaves_internal(map)); + CalcNode::Clamp { min, center, max } + }, + Self::Round { + strategy, + ref value, + ref step, + } => { + let value = Box::new(value.map_leaves_internal(map)); + let step = Box::new(step.map_leaves_internal(map)); + CalcNode::Round { + strategy, + value, + step, + } + }, + Self::ModRem { + ref dividend, + ref divisor, + op, + } => { + let dividend = Box::new(dividend.map_leaves_internal(map)); + let divisor = Box::new(divisor.map_leaves_internal(map)); + CalcNode::ModRem { + dividend, + divisor, + op, + } + }, + Self::Hypot(ref c) => CalcNode::Hypot(map_children(c, map)), + Self::Abs(ref c) => CalcNode::Abs(Box::new(c.map_leaves_internal(map))), + Self::Sign(ref c) => CalcNode::Sign(Box::new(c.map_leaves_internal(map))), + } + } + + /// Resolve this node into a value. + pub fn resolve(&self) -> Result<L, ()> { + self.resolve_map(|l| Ok(l.clone())) + } + + /// Resolve this node into a value, given a function that maps the leaf values. + pub fn resolve_map<F>(&self, mut leaf_to_output_fn: F) -> Result<L, ()> + where + F: FnMut(&L) -> Result<L, ()>, + { + self.resolve_internal(&mut leaf_to_output_fn) + } + + fn resolve_internal<F>(&self, leaf_to_output_fn: &mut F) -> Result<L, ()> + where + F: FnMut(&L) -> Result<L, ()>, + { + match self { + Self::Leaf(l) => leaf_to_output_fn(l), + Self::Negate(child) => { + let mut result = child.resolve_internal(leaf_to_output_fn)?; + result.map(|v| v.neg()); + Ok(result) + }, + Self::Invert(child) => { + let mut result = child.resolve_internal(leaf_to_output_fn)?; + result.map(|v| 1.0 / v); + Ok(result) + }, + Self::Sum(children) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + + for child in children.iter().skip(1) { + let right = child.resolve_internal(leaf_to_output_fn)?; + // try_op will make sure we only sum leaves with the same type. + result = result.try_op(&right, |left, right| left + right)?; + } + + Ok(result) + }, + Self::Product(children) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + + for child in children.iter().skip(1) { + let right = child.resolve_internal(leaf_to_output_fn)?; + // Mutliply only allowed when either side is a number. + match result.as_number() { + Some(left) => { + // Left side is a number, so we use the right node as the result. + result = right; + result.map(|v| v * left); + }, + None => { + // Left side is not a number, so check if the right side is. + match right.as_number() { + Some(right) => { + result.map(|v| v * right); + }, + None => { + // Multiplying with both sides having units. + return Err(()); + }, + } + }, + } + } + + Ok(result) + }, + Self::MinMax(children, op) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + + if result.is_nan() { + return Ok(result); + } + + for child in children.iter().skip(1) { + let candidate = child.resolve_internal(leaf_to_output_fn)?; + + // Leave types must match for each child. + if !result.is_same_unit_as(&candidate) { + return Err(()); + } + + if candidate.is_nan() { + result = candidate; + break; + } + + let candidate_wins = match op { + MinMaxOp::Min => candidate.lt(&result, PositivePercentageBasis::Yes), + MinMaxOp::Max => candidate.gt(&result, PositivePercentageBasis::Yes), + }; + + if candidate_wins { + result = candidate; + } + } + + Ok(result) + }, + Self::Clamp { min, center, max } => { + let min = min.resolve_internal(leaf_to_output_fn)?; + let center = center.resolve_internal(leaf_to_output_fn)?; + let max = max.resolve_internal(leaf_to_output_fn)?; + + if !min.is_same_unit_as(¢er) || !max.is_same_unit_as(¢er) { + return Err(()); + } + + if min.is_nan() { + return Ok(min); + } + + if center.is_nan() { + return Ok(center); + } + + if max.is_nan() { + return Ok(max); + } + + let mut result = center; + if result.gt(&max, PositivePercentageBasis::Yes) { + result = max; + } + if result.lt(&min, PositivePercentageBasis::Yes) { + result = min + } + + Ok(result) + }, + Self::Round { + strategy, + value, + step, + } => { + let mut value = value.resolve_internal(leaf_to_output_fn)?; + let step = step.resolve_internal(leaf_to_output_fn)?; + + if !value.is_same_unit_as(&step) { + return Err(()); + } + + let step = step.unitless_value().abs(); + + value.map(|value| { + // TODO(emilio): Seems like at least a few of these + // special-cases could be removed if we do the math in a + // particular order. + if step.is_zero() { + return f32::NAN; + } + + if value.is_infinite() { + if step.is_infinite() { + return f32::NAN; + } + return value; + } + + if step.is_infinite() { + match strategy { + RoundingStrategy::Nearest | RoundingStrategy::ToZero => { + return if value.is_sign_negative() { -0.0 } else { 0.0 } + }, + RoundingStrategy::Up => { + return if !value.is_sign_negative() && !value.is_zero() { + f32::INFINITY + } else if !value.is_sign_negative() && value.is_zero() { + value + } else { + -0.0 + } + }, + RoundingStrategy::Down => { + return if value.is_sign_negative() && !value.is_zero() { + -f32::INFINITY + } else if value.is_sign_negative() && value.is_zero() { + value + } else { + 0.0 + } + }, + } + } + + let div = value / step; + let lower_bound = div.floor() * step; + let upper_bound = div.ceil() * step; + + match strategy { + RoundingStrategy::Nearest => { + // In case of a tie, use the upper bound + if value - lower_bound < upper_bound - value { + lower_bound + } else { + upper_bound + } + }, + RoundingStrategy::Up => upper_bound, + RoundingStrategy::Down => lower_bound, + RoundingStrategy::ToZero => { + // In case of a tie, use the upper bound + if lower_bound.abs() < upper_bound.abs() { + lower_bound + } else { + upper_bound + } + }, + } + }); + + Ok(value) + }, + Self::ModRem { + dividend, + divisor, + op, + } => { + let mut dividend = dividend.resolve_internal(leaf_to_output_fn)?; + let divisor = divisor.resolve_internal(leaf_to_output_fn)?; + + if !dividend.is_same_unit_as(&divisor) { + return Err(()); + } + + let divisor = divisor.unitless_value(); + dividend.map(|dividend| op.apply(dividend, divisor)); + Ok(dividend) + }, + Self::Hypot(children) => { + let mut result = children[0].resolve_internal(leaf_to_output_fn)?; + result.map(|v| v.powi(2)); + + for child in children.iter().skip(1) { + let child_value = child.resolve_internal(leaf_to_output_fn)?; + + if !result.is_same_unit_as(&child_value) { + return Err(()); + } + + result.map(|v| v + child_value.unitless_value().powi(2)); + } + + result.map(|v| v.sqrt()); + Ok(result) + }, + Self::Abs(ref c) => { + let mut result = c.resolve_internal(leaf_to_output_fn)?; + + result.map(|v| v.abs()); + + Ok(result) + }, + Self::Sign(ref c) => { + let result = c.resolve_internal(leaf_to_output_fn)?; + Ok(L::sign_from(&result)) + }, + } + } + + fn is_negative_leaf(&self) -> bool { + match *self { + Self::Leaf(ref l) => l.is_negative(), + _ => false, + } + } + + fn is_zero_leaf(&self) -> bool { + match *self { + Self::Leaf(ref l) => l.is_zero(), + _ => false, + } + } + + fn is_infinite_leaf(&self) -> bool { + match *self { + Self::Leaf(ref l) => l.is_infinite(), + _ => false, + } + } + + /// Visits all the nodes in this calculation tree recursively, starting by + /// the leaves and bubbling all the way up. + /// + /// This is useful for simplification, but can also be used for validation + /// and such. + pub fn visit_depth_first(&mut self, mut f: impl FnMut(&mut Self)) { + self.visit_depth_first_internal(&mut f) + } + + fn visit_depth_first_internal(&mut self, f: &mut impl FnMut(&mut Self)) { + match *self { + Self::Clamp { + ref mut min, + ref mut center, + ref mut max, + } => { + min.visit_depth_first_internal(f); + center.visit_depth_first_internal(f); + max.visit_depth_first_internal(f); + }, + Self::Round { + ref mut value, + ref mut step, + .. + } => { + value.visit_depth_first_internal(f); + step.visit_depth_first_internal(f); + }, + Self::ModRem { + ref mut dividend, + ref mut divisor, + .. + } => { + dividend.visit_depth_first_internal(f); + divisor.visit_depth_first_internal(f); + }, + Self::Sum(ref mut children) | + Self::Product(ref mut children) | + Self::MinMax(ref mut children, _) | + Self::Hypot(ref mut children) => { + for child in &mut **children { + child.visit_depth_first_internal(f); + } + }, + Self::Negate(ref mut value) | Self::Invert(ref mut value) => { + value.visit_depth_first_internal(f); + }, + Self::Abs(ref mut value) | Self::Sign(ref mut value) => { + value.visit_depth_first_internal(f); + }, + Self::Leaf(..) => {}, + } + f(self); + } + + /// This function simplifies and sorts the calculation of the specified node. It simplifies + /// directly nested nodes while assuming that all nodes below it have already been simplified. + /// It is recommended to use this function in combination with `visit_depth_first()`. + /// + /// This function is necessary only if the node needs to be preserved after parsing, + /// specifically for `<length-percentage>` cases where the calculation contains percentages or + /// relative units. Otherwise, the node can be evaluated using `resolve()`, which will + /// automatically provide a simplified value. + /// + /// <https://drafts.csswg.org/css-values-4/#calc-simplification> + pub fn simplify_and_sort_direct_children(&mut self) { + macro_rules! replace_self_with { + ($slot:expr) => {{ + let result = mem::replace($slot, Self::dummy()); + *self = result; + }}; + } + + macro_rules! value_or_stop { + ($op:expr) => {{ + match $op { + Ok(value) => value, + Err(_) => return, + } + }}; + } + + match *self { + Self::Clamp { + ref mut min, + ref mut center, + ref mut max, + } => { + // NOTE: clamp() is max(min, min(center, max)) + let min_cmp_center = match min.compare(¢er, PositivePercentageBasis::Unknown) { + Some(o) => o, + None => return, + }; + + // So if we can prove that min is more than center, then we won, + // as that's what we should always return. + if matches!(min_cmp_center, cmp::Ordering::Greater) { + replace_self_with!(&mut **min); + return; + } + + // Otherwise try with max. + let max_cmp_center = match max.compare(¢er, PositivePercentageBasis::Unknown) { + Some(o) => o, + None => return, + }; + + if matches!(max_cmp_center, cmp::Ordering::Less) { + // max is less than center, so we need to return effectively + // `max(min, max)`. + let max_cmp_min = match max.compare(&min, PositivePercentageBasis::Unknown) { + Some(o) => o, + None => { + debug_assert!( + false, + "We compared center with min and max, how are \ + min / max not comparable with each other?" + ); + return; + }, + }; + + if matches!(max_cmp_min, cmp::Ordering::Less) { + replace_self_with!(&mut **min); + return; + } + + replace_self_with!(&mut **max); + return; + } + + // Otherwise we're the center node. + replace_self_with!(&mut **center); + }, + Self::Round { + strategy, + ref mut value, + ref mut step, + } => { + if step.is_zero_leaf() { + value.coerce_to_value(f32::NAN); + replace_self_with!(&mut **value); + return; + } + + if value.is_infinite_leaf() && step.is_infinite_leaf() { + value.coerce_to_value(f32::NAN); + replace_self_with!(&mut **value); + return; + } + + if value.is_infinite_leaf() { + replace_self_with!(&mut **value); + return; + } + + if step.is_infinite_leaf() { + match strategy { + RoundingStrategy::Nearest | RoundingStrategy::ToZero => { + value.coerce_to_value(0.0); + replace_self_with!(&mut **value); + return; + }, + RoundingStrategy::Up => { + if !value.is_negative_leaf() && !value.is_zero_leaf() { + value.coerce_to_value(f32::INFINITY); + replace_self_with!(&mut **value); + return; + } else if !value.is_negative_leaf() && value.is_zero_leaf() { + replace_self_with!(&mut **value); + return; + } else { + value.coerce_to_value(0.0); + replace_self_with!(&mut **value); + return; + } + }, + RoundingStrategy::Down => { + if value.is_negative_leaf() && !value.is_zero_leaf() { + value.coerce_to_value(f32::INFINITY); + replace_self_with!(&mut **value); + return; + } else if value.is_negative_leaf() && value.is_zero_leaf() { + replace_self_with!(&mut **value); + return; + } else { + value.coerce_to_value(0.0); + replace_self_with!(&mut **value); + return; + } + }, + } + } + + if step.is_negative_leaf() { + step.negate(); + } + + let remainder = value_or_stop!(value.try_op(step, Rem::rem)); + if remainder.is_zero_leaf() { + replace_self_with!(&mut **value); + return; + } + + let (mut lower_bound, mut upper_bound) = if value.is_negative_leaf() { + let upper_bound = value_or_stop!(value.try_op(&remainder, Sub::sub)); + let lower_bound = value_or_stop!(upper_bound.try_op(&step, Sub::sub)); + + (lower_bound, upper_bound) + } else { + let lower_bound = value_or_stop!(value.try_op(&remainder, Sub::sub)); + let upper_bound = value_or_stop!(lower_bound.try_op(&step, Add::add)); + + (lower_bound, upper_bound) + }; + + match strategy { + RoundingStrategy::Nearest => { + let lower_diff = value_or_stop!(value.try_op(&lower_bound, Sub::sub)); + let upper_diff = value_or_stop!(upper_bound.try_op(value, Sub::sub)); + // In case of a tie, use the upper bound + if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) { + replace_self_with!(&mut lower_bound); + } else { + replace_self_with!(&mut upper_bound); + } + }, + RoundingStrategy::Up => { + replace_self_with!(&mut upper_bound); + }, + RoundingStrategy::Down => { + replace_self_with!(&mut lower_bound); + }, + RoundingStrategy::ToZero => { + let mut lower_diff = lower_bound.clone(); + let mut upper_diff = upper_bound.clone(); + + if lower_diff.is_negative_leaf() { + lower_diff.negate(); + } + + if upper_diff.is_negative_leaf() { + upper_diff.negate(); + } + + // In case of a tie, use the upper bound + if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) { + replace_self_with!(&mut lower_bound); + } else { + replace_self_with!(&mut upper_bound); + } + }, + }; + }, + Self::ModRem { + ref dividend, + ref divisor, + op, + } => { + let mut result = value_or_stop!(dividend.try_op(divisor, |a, b| op.apply(a, b))); + replace_self_with!(&mut result); + }, + Self::MinMax(ref mut children, op) => { + let winning_order = match op { + MinMaxOp::Min => cmp::Ordering::Less, + MinMaxOp::Max => cmp::Ordering::Greater, + }; + + let mut result = 0; + for i in 1..children.len() { + let o = match children[i] + .compare(&children[result], PositivePercentageBasis::Unknown) + { + // We can't compare all the children, so we can't + // know which one will actually win. Bail out and + // keep ourselves as a min / max function. + // + // TODO: Maybe we could simplify compatible children, + // see https://github.com/w3c/csswg-drafts/issues/4756 + None => return, + Some(o) => o, + }; + + if o == winning_order { + result = i; + } + } + + replace_self_with!(&mut children[result]); + }, + Self::Sum(ref mut children_slot) => { + let mut sums_to_merge = SmallVec::<[_; 3]>::new(); + let mut extra_kids = 0; + for (i, child) in children_slot.iter().enumerate() { + if let Self::Sum(ref children) = *child { + extra_kids += children.len(); + sums_to_merge.push(i); + } + } + + // If we only have one kid, we've already simplified it, and it + // doesn't really matter whether it's a sum already or not, so + // lift it up and continue. + if children_slot.len() == 1 { + replace_self_with!(&mut children_slot[0]); + return; + } + + let mut children = mem::take(children_slot).into_vec(); + + if !sums_to_merge.is_empty() { + children.reserve(extra_kids - sums_to_merge.len()); + // Merge all our nested sums, in reverse order so that the + // list indices are not invalidated. + for i in sums_to_merge.drain(..).rev() { + let kid_children = match children.swap_remove(i) { + Self::Sum(c) => c, + _ => unreachable!(), + }; + + // This would be nicer with + // https://github.com/rust-lang/rust/issues/59878 fixed. + children.extend(kid_children.into_vec()); + } + } + + debug_assert!(children.len() >= 2, "Should still have multiple kids!"); + + // Sort by spec order. + children.sort_unstable_by_key(|c| c.sort_key()); + + // NOTE: if the function returns true, by the docs of dedup_by, + // a is removed. + children.dedup_by(|a, b| b.try_sum_in_place(a).is_ok()); + + if children.len() == 1 { + // If only one children remains, lift it up, and carry on. + replace_self_with!(&mut children[0]); + } else { + // Else put our simplified children back. + *children_slot = children.into_boxed_slice().into(); + } + }, + Self::Product(ref mut children_slot) => { + let mut products_to_merge = SmallVec::<[_; 3]>::new(); + let mut extra_kids = 0; + for (i, child) in children_slot.iter().enumerate() { + if let Self::Product(ref children) = *child { + extra_kids += children.len(); + products_to_merge.push(i); + } + } + + // If we only have one kid, we've already simplified it, and it + // doesn't really matter whether it's a product already or not, + // so lift it up and continue. + if children_slot.len() == 1 { + replace_self_with!(&mut children_slot[0]); + return; + } + + let mut children = mem::take(children_slot).into_vec(); + + if !products_to_merge.is_empty() { + children.reserve(extra_kids - products_to_merge.len()); + // Merge all our nested sums, in reverse order so that the + // list indices are not invalidated. + for i in products_to_merge.drain(..).rev() { + let kid_children = match children.swap_remove(i) { + Self::Product(c) => c, + _ => unreachable!(), + }; + + // This would be nicer with + // https://github.com/rust-lang/rust/issues/59878 fixed. + children.extend(kid_children.into_vec()); + } + } + + debug_assert!(children.len() >= 2, "Should still have multiple kids!"); + + // NOTE: if the function returns true, by the docs of dedup_by, + // a is removed. + children.dedup_by(|right, left| left.try_product_in_place(right)); + + if children.len() == 1 { + // If only one children remains, lift it up, and carry on. + replace_self_with!(&mut children[0]); + } else { + // Else put our simplified children back. + *children_slot = children.into_boxed_slice().into(); + } + }, + Self::Hypot(ref children) => { + let mut result = value_or_stop!(children[0].try_op(&children[0], Mul::mul)); + + for child in children.iter().skip(1) { + let square = value_or_stop!(child.try_op(&child, Mul::mul)); + result = value_or_stop!(result.try_op(&square, Add::add)); + } + + result = value_or_stop!(result.try_op(&result, |a, _| a.sqrt())); + + replace_self_with!(&mut result); + }, + Self::Abs(ref mut child) => { + if let CalcNode::Leaf(leaf) = child.as_mut() { + leaf.map(|v| v.abs()); + replace_self_with!(&mut **child); + } + }, + Self::Sign(ref mut child) => { + if let CalcNode::Leaf(leaf) = child.as_mut() { + let mut result = Self::Leaf(L::sign_from(leaf)); + replace_self_with!(&mut result); + } + }, + Self::Negate(ref mut child) => { + // Step 6. + match &mut **child { + CalcNode::Leaf(_) => { + // 1. If root’s child is a numeric value, return an equivalent numeric value, but + // with the value negated (0 - value). + child.negate(); + replace_self_with!(&mut **child); + }, + CalcNode::Negate(value) => { + // 2. If root’s child is a Negate node, return the child’s child. + replace_self_with!(&mut **value); + }, + _ => { + // 3. Return root. + }, + } + }, + Self::Invert(ref mut child) => { + // Step 7. + match &mut **child { + CalcNode::Leaf(leaf) => { + // 1. If root’s child is a number (not a percentage or dimension) return the + // reciprocal of the child’s value. + if leaf.unit().is_empty() { + child.map(|v| 1.0 / v); + replace_self_with!(&mut **child); + } + }, + CalcNode::Invert(value) => { + // 2. If root’s child is an Invert node, return the child’s child. + replace_self_with!(&mut **value); + }, + _ => { + // 3. Return root. + }, + } + }, + Self::Leaf(ref mut l) => { + l.simplify(); + }, + } + } + + /// Simplifies and sorts the kids in the whole calculation subtree. + pub fn simplify_and_sort(&mut self) { + self.visit_depth_first(|node| node.simplify_and_sort_direct_children()) + } + + fn to_css_impl<W>(&self, dest: &mut CssWriter<W>, level: ArgumentLevel) -> fmt::Result + where + W: Write, + { + let write_closing_paren = match *self { + Self::MinMax(_, op) => { + dest.write_str(match op { + MinMaxOp::Max => "max(", + MinMaxOp::Min => "min(", + })?; + true + }, + Self::Clamp { .. } => { + dest.write_str("clamp(")?; + true + }, + Self::Round { strategy, .. } => { + match strategy { + RoundingStrategy::Nearest => dest.write_str("round("), + RoundingStrategy::Up => dest.write_str("round(up, "), + RoundingStrategy::Down => dest.write_str("round(down, "), + RoundingStrategy::ToZero => dest.write_str("round(to-zero, "), + }?; + + true + }, + Self::ModRem { op, .. } => { + dest.write_str(match op { + ModRemOp::Mod => "mod(", + ModRemOp::Rem => "rem(", + })?; + + true + }, + Self::Hypot(_) => { + dest.write_str("hypot(")?; + true + }, + Self::Abs(_) => { + dest.write_str("abs(")?; + true + }, + Self::Sign(_) => { + dest.write_str("sign(")?; + true + }, + Self::Negate(_) => { + // We never generate a [`Negate`] node as the root of a calculation, only inside + // [`Sum`] nodes as a child. Because negate nodes are handled by the [`Sum`] node + // directly (see below), this node will never be serialized. + debug_assert!( + false, + "We never serialize Negate nodes as they are handled inside Sum nodes." + ); + dest.write_str("(-1 * ")?; + true + }, + Self::Invert(_) => { + dest.write_str("(1 / ")?; + true + }, + Self::Sum(_) | Self::Product(_) => match level { + ArgumentLevel::CalculationRoot => { + dest.write_str("calc(")?; + true + }, + ArgumentLevel::ArgumentRoot => false, + ArgumentLevel::Nested => { + dest.write_str("(")?; + true + }, + }, + Self::Leaf(_) => match level { + ArgumentLevel::CalculationRoot => { + dest.write_str("calc(")?; + true + }, + ArgumentLevel::ArgumentRoot | ArgumentLevel::Nested => false, + }, + }; + + match *self { + Self::MinMax(ref children, _) | Self::Hypot(ref children) => { + let mut first = true; + for child in &**children { + if !first { + dest.write_str(", ")?; + } + first = false; + child.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + } + }, + Self::Negate(ref value) | Self::Invert(ref value) => { + value.to_css_impl(dest, ArgumentLevel::Nested)? + }, + Self::Sum(ref children) => { + let mut first = true; + for child in &**children { + if !first { + match child { + Self::Leaf(l) => { + if l.is_negative() { + dest.write_str(" - ")?; + let mut negated = l.clone(); + negated.negate(); + negated.to_css(dest)?; + } else { + dest.write_str(" + ")?; + l.to_css(dest)?; + } + }, + Self::Negate(n) => { + dest.write_str(" - ")?; + n.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + _ => { + dest.write_str(" + ")?; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + } + } else { + first = false; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + } + } + }, + Self::Product(ref children) => { + let mut first = true; + for child in &**children { + if !first { + match child { + Self::Invert(n) => { + dest.write_str(" / ")?; + n.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + _ => { + dest.write_str(" * ")?; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + }, + } + } else { + first = false; + child.to_css_impl(dest, ArgumentLevel::Nested)?; + } + } + }, + Self::Clamp { + ref min, + ref center, + ref max, + } => { + min.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + center.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + max.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + }, + Self::Round { + ref value, + ref step, + .. + } => { + value.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + step.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + }, + Self::ModRem { + ref dividend, + ref divisor, + .. + } => { + dividend.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + dest.write_str(", ")?; + divisor.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?; + }, + Self::Abs(ref v) | Self::Sign(ref v) => { + v.to_css_impl(dest, ArgumentLevel::ArgumentRoot)? + }, + Self::Leaf(ref l) => l.to_css(dest)?, + } + + if write_closing_paren { + dest.write_char(')')?; + } + Ok(()) + } + + fn compare( + &self, + other: &Self, + basis_positive: PositivePercentageBasis, + ) -> Option<cmp::Ordering> { + match (self, other) { + (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => { + one.compare(other, basis_positive) + }, + _ => None, + } + } + + compare_helpers!(); +} + +impl<L: CalcNodeLeaf> ToCss for CalcNode<L> { + /// <https://drafts.csswg.org/css-values/#calc-serialize> + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.to_css_impl(dest, ArgumentLevel::CalculationRoot) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_sum_with_checks() { + assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH)); + assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::PERCENTAGE)); + assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH_PERCENTAGE)); + + assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH)); + assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE)); + assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE)); + + assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH)); + assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE)); + assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE)); + + assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::TIME)); + assert!(CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE)); + + assert!(!(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE)); + assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME)); + assert!( + !(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME) + ); + } +} diff --git a/servo/components/style/values/generics/color.rs b/servo/components/style/values/generics/color.rs new file mode 100644 index 0000000000..e37cabdc59 --- /dev/null +++ b/servo/components/style/values/generics/color.rs @@ -0,0 +1,209 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for color properties. + +use crate::color::mix::ColorInterpolationMethod; +use crate::color::AbsoluteColor; +use crate::values::specified::percentage::ToPercentage; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// This struct represents a combined color from a numeric color and +/// the current foreground color (currentcolor keyword). +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] +#[repr(C)] +pub enum GenericColor<Percentage> { + /// The actual numeric color. + Absolute(AbsoluteColor), + /// The `CurrentColor` keyword. + CurrentColor, + /// The color-mix() function. + ColorMix(Box<GenericColorMix<Self, Percentage>>), +} + +/// Flags used to modify the calculation of a color mix result. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +#[repr(C)] +pub struct ColorMixFlags(u8); +bitflags! { + impl ColorMixFlags : u8 { + /// Normalize the weights of the mix. + const NORMALIZE_WEIGHTS = 1 << 0; + /// The result should always be converted to the modern color syntax. + const RESULT_IN_MODERN_SYNTAX = 1 << 1; + } +} + +/// A restricted version of the css `color-mix()` function, which only supports +/// percentages. +/// +/// https://drafts.csswg.org/css-color-5/#color-mix +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(C)] +pub struct GenericColorMix<Color, Percentage> { + pub interpolation: ColorInterpolationMethod, + pub left: Color, + pub left_percentage: Percentage, + pub right: Color, + pub right_percentage: Percentage, + pub flags: ColorMixFlags, +} + +pub use self::GenericColorMix as ColorMix; + +impl<Color: ToCss, Percentage: ToCss + ToPercentage> ToCss for ColorMix<Color, Percentage> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + fn can_omit<Percentage: ToPercentage>( + percent: &Percentage, + other: &Percentage, + is_left: bool, + ) -> bool { + if percent.is_calc() { + return false; + } + if percent.to_percentage() == 0.5 { + return other.to_percentage() == 0.5; + } + if is_left { + return false; + } + (1.0 - percent.to_percentage() - other.to_percentage()).abs() <= f32::EPSILON + } + + dest.write_str("color-mix(")?; + self.interpolation.to_css(dest)?; + dest.write_str(", ")?; + self.left.to_css(dest)?; + if !can_omit(&self.left_percentage, &self.right_percentage, true) { + dest.write_char(' ')?; + self.left_percentage.to_css(dest)?; + } + dest.write_str(", ")?; + self.right.to_css(dest)?; + if !can_omit(&self.right_percentage, &self.left_percentage, false) { + dest.write_char(' ')?; + self.right_percentage.to_css(dest)?; + } + dest.write_char(')') + } +} + +impl<Percentage> ColorMix<GenericColor<Percentage>, Percentage> { + /// Mix the colors so that we get a single color. If any of the 2 colors are + /// not mixable (perhaps not absolute?), then return None. + pub fn mix_to_absolute(&self) -> Option<AbsoluteColor> + where + Percentage: ToPercentage, + { + let left = self.left.as_absolute()?; + let right = self.right.as_absolute()?; + + Some(crate::color::mix::mix( + self.interpolation, + &left, + self.left_percentage.to_percentage(), + &right, + self.right_percentage.to_percentage(), + self.flags, + )) + } +} + +pub use self::GenericColor as Color; + +impl<Percentage> Color<Percentage> { + /// If this color is absolute return it's value, otherwise return None. + pub fn as_absolute(&self) -> Option<&AbsoluteColor> { + match *self { + Self::Absolute(ref absolute) => Some(absolute), + _ => None, + } + } + + /// Returns a color value representing currentcolor. + pub fn currentcolor() -> Self { + Self::CurrentColor + } + + /// Whether it is a currentcolor value (no numeric color component). + pub fn is_currentcolor(&self) -> bool { + matches!(*self, Self::CurrentColor) + } + + /// Whether this color is an absolute color. + pub fn is_absolute(&self) -> bool { + matches!(*self, Self::Absolute(..)) + } +} + +/// Either `<color>` or `auto`. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToCss, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericColorOrAuto<C> { + /// A `<color>`. + Color(C), + /// `auto` + Auto, +} + +pub use self::GenericColorOrAuto as ColorOrAuto; + +/// Caret color is effectively a ColorOrAuto, but resolves `auto` to +/// currentColor. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToShmem, +)] +#[repr(transparent)] +pub struct GenericCaretColor<C>(pub GenericColorOrAuto<C>); + +impl<C> GenericCaretColor<C> { + /// Returns the `auto` value. + pub fn auto() -> Self { + GenericCaretColor(GenericColorOrAuto::Auto) + } +} + +pub use self::GenericCaretColor as CaretColor; diff --git a/servo/components/style/values/generics/column.rs b/servo/components/style/values/generics/column.rs new file mode 100644 index 0000000000..4b5f0e0399 --- /dev/null +++ b/servo/components/style/values/generics/column.rs @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for the column properties. + +/// A generic type for `column-count` values. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum ColumnCount<PositiveInteger> { + /// A positive integer. + Integer(PositiveInteger), + /// The keyword `auto`. + #[animation(error)] + Auto, +} + +impl<I> ColumnCount<I> { + /// Returns `auto`. + #[inline] + pub fn auto() -> Self { + ColumnCount::Auto + } + + /// Returns whether this value is `auto`. + #[inline] + pub fn is_auto(self) -> bool { + matches!(self, ColumnCount::Auto) + } +} diff --git a/servo/components/style/values/generics/counters.rs b/servo/components/style/values/generics/counters.rs new file mode 100644 index 0000000000..1d4518c57b --- /dev/null +++ b/servo/components/style/values/generics/counters.rs @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for counters-related CSS values. + +#[cfg(feature = "servo-layout-2013")] +use crate::computed_values::list_style_type::T as ListStyleType; +#[cfg(feature = "gecko")] +use crate::values::generics::CounterStyle; +#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] +use crate::values::specified::Attr; +use crate::values::CustomIdent; +use std::fmt::{self, Write}; +use std::ops::Deref; +use style_traits::{CssWriter, ToCss}; + +/// A name / value pair for counters. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericCounterPair<Integer> { + /// The name of the counter. + pub name: CustomIdent, + /// The value of the counter / increment / etc. + pub value: Integer, + /// If true, then this represents `reversed(name)`. + /// NOTE: It can only be true on `counter-reset` values. + pub is_reversed: bool, +} +pub use self::GenericCounterPair as CounterPair; + +impl<Integer> ToCss for CounterPair<Integer> +where + Integer: ToCss + PartialEq<i32>, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_reversed { + dest.write_str("reversed(")?; + } + self.name.to_css(dest)?; + if self.is_reversed { + dest.write_char(')')?; + if self.value == i32::min_value() { + return Ok(()); + } + } + dest.write_char(' ')?; + self.value.to_css(dest) + } +} + +/// A generic value for the `counter-increment` property. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct GenericCounterIncrement<I>(#[css(field_bound)] pub GenericCounters<I>); +pub use self::GenericCounterIncrement as CounterIncrement; + +impl<I> CounterIncrement<I> { + /// Returns a new value for `counter-increment`. + #[inline] + pub fn new(counters: Vec<CounterPair<I>>) -> Self { + CounterIncrement(Counters(counters.into())) + } +} + +impl<I> Deref for CounterIncrement<I> { + type Target = [CounterPair<I>]; + + #[inline] + fn deref(&self) -> &Self::Target { + &(self.0).0 + } +} + +/// A generic value for the `counter-set` property. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct GenericCounterSet<I>(#[css(field_bound)] pub GenericCounters<I>); +pub use self::GenericCounterSet as CounterSet; + +impl<I> CounterSet<I> { + /// Returns a new value for `counter-set`. + #[inline] + pub fn new(counters: Vec<CounterPair<I>>) -> Self { + CounterSet(Counters(counters.into())) + } +} + +impl<I> Deref for CounterSet<I> { + type Target = [CounterPair<I>]; + + #[inline] + fn deref(&self) -> &Self::Target { + &(self.0).0 + } +} + +/// A generic value for the `counter-reset` property. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct GenericCounterReset<I>(#[css(field_bound)] pub GenericCounters<I>); +pub use self::GenericCounterReset as CounterReset; + +impl<I> CounterReset<I> { + /// Returns a new value for `counter-reset`. + #[inline] + pub fn new(counters: Vec<CounterPair<I>>) -> Self { + CounterReset(Counters(counters.into())) + } +} + +impl<I> Deref for CounterReset<I> { + type Target = [CounterPair<I>]; + + #[inline] + fn deref(&self) -> &Self::Target { + &(self.0).0 + } +} + +/// A generic value for lists of counters. +/// +/// Keyword `none` is represented by an empty vector. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct GenericCounters<I>( + #[css(field_bound)] + #[css(iterable, if_empty = "none")] + crate::OwnedSlice<GenericCounterPair<I>>, +); +pub use self::GenericCounters as Counters; + +#[cfg(feature = "servo-layout-2013")] +type CounterStyleType = ListStyleType; + +#[cfg(feature = "gecko")] +type CounterStyleType = CounterStyle; + +#[cfg(feature = "servo-layout-2013")] +#[inline] +fn is_decimal(counter_type: &CounterStyleType) -> bool { + *counter_type == ListStyleType::Decimal +} + +#[cfg(feature = "gecko")] +#[inline] +fn is_decimal(counter_type: &CounterStyleType) -> bool { + *counter_type == CounterStyle::decimal() +} + +/// The specified value for the `content` property. +/// +/// https://drafts.csswg.org/css-content/#propdef-content +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum GenericContent<Image> { + /// `normal` reserved keyword. + Normal, + /// `none` reserved keyword. + None, + /// Content items. + Items(#[css(iterable)] crate::OwnedSlice<GenericContentItem<Image>>), +} + +pub use self::GenericContent as Content; + +impl<Image> Content<Image> { + /// Whether `self` represents list of items. + #[inline] + pub fn is_items(&self) -> bool { + matches!(*self, Self::Items(..)) + } + + /// Set `content` property to `normal`. + #[inline] + pub fn normal() -> Self { + Content::Normal + } +} + +/// Items for the `content` property. +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + SpecifiedValueInfo, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum GenericContentItem<I> { + /// Literal string content. + String(crate::OwnedStr), + /// `counter(name, style)`. + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + #[css(comma, function)] + Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyleType), + /// `counters(name, separator, style)`. + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + #[css(comma, function)] + Counters( + CustomIdent, + crate::OwnedStr, + #[css(skip_if = "is_decimal")] CounterStyleType, + ), + /// `open-quote`. + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + OpenQuote, + /// `close-quote`. + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + CloseQuote, + /// `no-open-quote`. + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + NoOpenQuote, + /// `no-close-quote`. + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + NoCloseQuote, + /// `-moz-alt-content`. + #[cfg(feature = "gecko")] + MozAltContent, + /// `-moz-label-content`. + /// This is needed to make `accesskey` work for XUL labels. It's basically + /// attr(value) otherwise. + #[cfg(feature = "gecko")] + MozLabelContent, + /// `attr([namespace? `|`]? ident)` + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + Attr(Attr), + /// image-set(url) | url(url) + Image(I), +} + +pub use self::GenericContentItem as ContentItem; diff --git a/servo/components/style/values/generics/easing.rs b/servo/components/style/values/generics/easing.rs new file mode 100644 index 0000000000..e04b49a4be --- /dev/null +++ b/servo/components/style/values/generics/easing.rs @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS Easing Functions. +//! https://drafts.csswg.org/css-easing/#timing-functions + +use crate::parser::ParserContext; + +/// A generic easing function. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToShmem, + Serialize, + Deserialize, +)] +#[value_info(ty = "TIMING_FUNCTION")] +#[repr(u8, C)] +pub enum TimingFunction<Integer, Number, LinearStops> { + /// `linear | ease | ease-in | ease-out | ease-in-out` + Keyword(TimingKeyword), + /// `cubic-bezier(<number>, <number>, <number>, <number>)` + #[allow(missing_docs)] + #[css(comma, function)] + CubicBezier { + x1: Number, + y1: Number, + x2: Number, + y2: Number, + }, + /// `step-start | step-end | steps(<integer>, [ <step-position> ]?)` + /// `<step-position> = jump-start | jump-end | jump-none | jump-both | start | end` + #[css(comma, function)] + #[value_info(other_values = "step-start,step-end")] + Steps(Integer, #[css(skip_if = "is_end")] StepPosition), + /// linear([<linear-stop>]#) + /// <linear-stop> = <output> && <linear-stop-length>? + /// <linear-stop-length> = <percentage>{1, 2} + #[css(function = "linear")] + LinearFunction(LinearStops), +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[repr(u8)] +pub enum TimingKeyword { + Linear, + Ease, + EaseIn, + EaseOut, + EaseInOut, +} + +/// Before flag, defined as per https://drafts.csswg.org/css-easing/#before-flag +/// This flag is never user-specified. +#[allow(missing_docs)] +#[derive(PartialEq)] +#[repr(u8)] +pub enum BeforeFlag { + Unset, + Set, +} + +#[cfg(feature = "gecko")] +fn step_position_jump_enabled(_context: &ParserContext) -> bool { + true +} + +#[cfg(feature = "servo")] +fn step_position_jump_enabled(_context: &ParserContext) -> bool { + false +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[repr(u8)] +pub enum StepPosition { + #[parse(condition = "step_position_jump_enabled")] + JumpStart, + #[parse(condition = "step_position_jump_enabled")] + JumpEnd, + #[parse(condition = "step_position_jump_enabled")] + JumpNone, + #[parse(condition = "step_position_jump_enabled")] + JumpBoth, + Start, + End, +} + +#[inline] +fn is_end(position: &StepPosition) -> bool { + *position == StepPosition::JumpEnd || *position == StepPosition::End +} + +impl<Integer, Number, LinearStops> TimingFunction<Integer, Number, LinearStops> { + /// `ease` + #[inline] + pub fn ease() -> Self { + TimingFunction::Keyword(TimingKeyword::Ease) + } + + /// Returns true if it is `ease`. + #[inline] + pub fn is_ease(&self) -> bool { + matches!(*self, TimingFunction::Keyword(TimingKeyword::Ease)) + } +} diff --git a/servo/components/style/values/generics/effects.rs b/servo/components/style/values/generics/effects.rs new file mode 100644 index 0000000000..f5666f3055 --- /dev/null +++ b/servo/components/style/values/generics/effects.rs @@ -0,0 +1,121 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values related to effects. + +/// A generic value for a single `box-shadow`. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericBoxShadow<Color, SizeLength, BlurShapeLength, ShapeLength> { + /// The base shadow. + pub base: GenericSimpleShadow<Color, SizeLength, BlurShapeLength>, + /// The spread radius. + pub spread: ShapeLength, + /// Whether this is an inset box shadow. + #[animation(constant)] + #[css(represents_keyword)] + pub inset: bool, +} + +pub use self::GenericBoxShadow as BoxShadow; + +/// A generic value for a single `filter`. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[animation(no_bound(U))] +#[repr(C, u8)] +pub enum GenericFilter<Angle, NonNegativeFactor, ZeroToOneFactor, Length, Shadow, U> { + /// `blur(<length>)` + #[css(function)] + Blur(Length), + /// `brightness(<factor>)` + #[css(function)] + Brightness(NonNegativeFactor), + /// `contrast(<factor>)` + #[css(function)] + Contrast(NonNegativeFactor), + /// `grayscale(<factor>)` + #[css(function)] + Grayscale(ZeroToOneFactor), + /// `hue-rotate(<angle>)` + #[css(function)] + HueRotate(Angle), + /// `invert(<factor>)` + #[css(function)] + Invert(ZeroToOneFactor), + /// `opacity(<factor>)` + #[css(function)] + Opacity(ZeroToOneFactor), + /// `saturate(<factor>)` + #[css(function)] + Saturate(NonNegativeFactor), + /// `sepia(<factor>)` + #[css(function)] + Sepia(ZeroToOneFactor), + /// `drop-shadow(...)` + #[css(function)] + DropShadow(Shadow), + /// `<url>` + #[animation(error)] + Url(U), +} + +pub use self::GenericFilter as Filter; + +/// A generic value for the `drop-shadow()` filter and the `text-shadow` property. +/// +/// Contrary to the canonical order from the spec, the color is serialised +/// first, like in Gecko and Webkit. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericSimpleShadow<Color, SizeLength, ShapeLength> { + /// Color. + pub color: Color, + /// Horizontal radius. + pub horizontal: SizeLength, + /// Vertical radius. + pub vertical: SizeLength, + /// Blur radius. + pub blur: ShapeLength, +} + +pub use self::GenericSimpleShadow as SimpleShadow; diff --git a/servo/components/style/values/generics/flex.rs b/servo/components/style/values/generics/flex.rs new file mode 100644 index 0000000000..85b64000f2 --- /dev/null +++ b/servo/components/style/values/generics/flex.rs @@ -0,0 +1,33 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values related to flexbox. + +/// A generic value for the `flex-basis` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub enum GenericFlexBasis<S> { + /// `content` + Content, + /// `<width>` + Size(S), +} + +pub use self::GenericFlexBasis as FlexBasis; diff --git a/servo/components/style/values/generics/font.rs b/servo/components/style/values/generics/font.rs new file mode 100644 index 0000000000..91dd2d8515 --- /dev/null +++ b/servo/components/style/values/generics/font.rs @@ -0,0 +1,316 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for font stuff. + +use crate::parser::{Parse, ParserContext}; +use crate::values::animated::ToAnimatedZero; +use crate::One; +use byteorder::{BigEndian, ReadBytesExt}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use std::io::Cursor; +use style_traits::{CssWriter, ParseError}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// A trait for values that are labelled with a FontTag (for feature and +/// variation settings). +pub trait TaggedFontValue { + /// The value's tag. + fn tag(&self) -> FontTag; +} + +/// https://drafts.csswg.org/css-fonts-4/#feature-tag-value +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct FeatureTagValue<Integer> { + /// A four-character tag, packed into a u32 (one byte per character). + pub tag: FontTag, + /// The actual value. + pub value: Integer, +} + +impl<T> TaggedFontValue for FeatureTagValue<T> { + fn tag(&self) -> FontTag { + self.tag + } +} + +impl<Integer> ToCss for FeatureTagValue<Integer> +where + Integer: One + ToCss + PartialEq, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.tag.to_css(dest)?; + // Don't serialize the default value. + if !self.value.is_one() { + dest.write_char(' ')?; + self.value.to_css(dest)?; + } + + Ok(()) + } +} + +/// Variation setting for a single feature, see: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct VariationValue<Number> { + /// A four-character tag, packed into a u32 (one byte per character). + #[animation(constant)] + pub tag: FontTag, + /// The actual value. + pub value: Number, +} + +impl<T> TaggedFontValue for VariationValue<T> { + fn tag(&self) -> FontTag { + self.tag + } +} + +/// A value both for font-variation-settings and font-feature-settings. +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToResolvedValue, ToShmem, +)] +#[css(comma)] +pub struct FontSettings<T>(#[css(if_empty = "normal", iterable)] pub Box<[T]>); + +impl<T> FontSettings<T> { + /// Default value of font settings as `normal`. + #[inline] + pub fn normal() -> Self { + FontSettings(vec![].into_boxed_slice()) + } +} + +impl<T: Parse> Parse for FontSettings<T> { + /// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-feature-settings + /// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(Self::normal()); + } + + Ok(FontSettings( + input + .parse_comma_separated(|i| T::parse(context, i))? + .into_boxed_slice(), + )) + } +} + +/// A font four-character tag, represented as a u32 for convenience. +/// +/// See: +/// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def +/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-feature-settings +/// +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct FontTag(pub u32); + +impl ToCss for FontTag { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + use byteorder::ByteOrder; + use std::str; + + let mut raw = [0u8; 4]; + BigEndian::write_u32(&mut raw, self.0); + str::from_utf8(&raw).unwrap_or_default().to_css(dest) + } +} + +impl Parse for FontTag { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let tag = input.expect_string()?; + + // allowed strings of length 4 containing chars: <U+20, U+7E> + if tag.len() != 4 || tag.as_bytes().iter().any(|c| *c < b' ' || *c > b'~') { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut raw = Cursor::new(tag.as_bytes()); + Ok(FontTag(raw.read_u32::<BigEndian>().unwrap())) + } +} + +/// A generic value for the `font-style` property. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-style-prop +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToResolvedValue, + ToShmem, +)] +pub enum FontStyle<Angle> { + #[animation(error)] + Normal, + #[animation(error)] + Italic, + #[value_info(starts_with_keyword)] + Oblique(Angle), +} + +/// A generic value for the `font-size-adjust` property. +/// +/// https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop +#[allow(missing_docs)] +#[repr(u8)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub enum GenericFontSizeAdjust<Factor> { + #[animation(error)] + None, + #[value_info(starts_with_keyword)] + ExHeight(Factor), + #[value_info(starts_with_keyword)] + CapHeight(Factor), + #[value_info(starts_with_keyword)] + ChWidth(Factor), + #[value_info(starts_with_keyword)] + IcWidth(Factor), + #[value_info(starts_with_keyword)] + IcHeight(Factor), +} + +impl<Factor: ToCss> ToCss for GenericFontSizeAdjust<Factor> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let (prefix, value) = match self { + Self::None => return dest.write_str("none"), + Self::ExHeight(v) => ("", v), + Self::CapHeight(v) => ("cap-height ", v), + Self::ChWidth(v) => ("ch-width ", v), + Self::IcWidth(v) => ("ic-width ", v), + Self::IcHeight(v) => ("ic-height ", v), + }; + + dest.write_str(prefix)?; + value.to_css(dest) + } +} + +/// A generic value for the `line-height` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToCss, + ToShmem, + Parse, +)] +#[repr(C, u8)] +pub enum GenericLineHeight<N, L> { + /// `normal` + Normal, + /// `-moz-block-height` + #[cfg(feature = "gecko")] + #[parse(condition = "ParserContext::in_ua_sheet")] + MozBlockHeight, + /// `<number>` + Number(N), + /// `<length-percentage>` + Length(L), +} + +pub use self::GenericLineHeight as LineHeight; + +impl<N, L> ToAnimatedZero for LineHeight<N, L> { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +impl<N, L> LineHeight<N, L> { + /// Returns `normal`. + #[inline] + pub fn normal() -> Self { + LineHeight::Normal + } +} diff --git a/servo/components/style/values/generics/grid.rs b/servo/components/style/values/generics/grid.rs new file mode 100644 index 0000000000..22fe249c83 --- /dev/null +++ b/servo/components/style/values/generics/grid.rs @@ -0,0 +1,867 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for the handling of +//! [grids](https://drafts.csswg.org/css-grid/). + +use crate::parser::{Parse, ParserContext}; +use crate::values::specified; +use crate::values::{CSSFloat, CustomIdent}; +use crate::{One, Zero}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use std::{cmp, usize}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// These are the limits that we choose to clamp grid line numbers to. +/// http://drafts.csswg.org/css-grid/#overlarge-grids +/// line_num is clamped to this range at parse time. +pub const MIN_GRID_LINE: i32 = -10000; +/// See above. +pub const MAX_GRID_LINE: i32 = 10000; + +/// A `<grid-line>` type. +/// +/// <https://drafts.csswg.org/css-grid/#typedef-grid-row-start-grid-line> +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericGridLine<Integer> { + /// A custom identifier for named lines, or the empty atom otherwise. + /// + /// <https://drafts.csswg.org/css-grid/#grid-placement-slot> + pub ident: CustomIdent, + /// Denotes the nth grid line from grid item's placement. + /// + /// This is clamped by MIN_GRID_LINE and MAX_GRID_LINE. + /// + /// NOTE(emilio): If we ever allow animating these we need to either do + /// something more complicated for the clamping, or do this clamping at + /// used-value time. + pub line_num: Integer, + /// Flag to check whether it's a `span` keyword. + pub is_span: bool, +} + +pub use self::GenericGridLine as GridLine; + +impl<Integer> GridLine<Integer> +where + Integer: PartialEq + Zero, +{ + /// The `auto` value. + pub fn auto() -> Self { + Self { + is_span: false, + line_num: Zero::zero(), + ident: CustomIdent(atom!("")), + } + } + + /// Check whether this `<grid-line>` represents an `auto` value. + pub fn is_auto(&self) -> bool { + self.ident.0 == atom!("") && self.line_num.is_zero() && !self.is_span + } + + /// Check whether this `<grid-line>` represents a `<custom-ident>` value. + pub fn is_ident_only(&self) -> bool { + self.ident.0 != atom!("") && self.line_num.is_zero() && !self.is_span + } + + /// Check if `self` makes `other` omittable according to the rules at: + /// https://drafts.csswg.org/css-grid/#propdef-grid-column + /// https://drafts.csswg.org/css-grid/#propdef-grid-area + pub fn can_omit(&self, other: &Self) -> bool { + if self.is_ident_only() { + self == other + } else { + other.is_auto() + } + } +} + +impl<Integer> ToCss for GridLine<Integer> +where + Integer: ToCss + PartialEq + Zero + One, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + // 1. `auto` + if self.is_auto() { + return dest.write_str("auto"); + } + + // 2. `<custom-ident>` + if self.is_ident_only() { + return self.ident.to_css(dest); + } + + // 3. `[ span && [ <integer [1,∞]> || <custom-ident> ] ]` + let has_ident = self.ident.0 != atom!(""); + if self.is_span { + dest.write_str("span")?; + debug_assert!(!self.line_num.is_zero() || has_ident); + + // We omit `line_num` if + // 1. we don't specify it, or + // 2. it is the default value, i.e. 1.0, and the ident is specified. + // https://drafts.csswg.org/css-grid/#grid-placement-span-int + if !self.line_num.is_zero() && !(self.line_num.is_one() && has_ident) { + dest.write_char(' ')?; + self.line_num.to_css(dest)?; + } + + if has_ident { + dest.write_char(' ')?; + self.ident.to_css(dest)?; + } + return Ok(()); + } + + // 4. `[ <integer [-∞,-1]> | <integer [1,∞]> ] && <custom-ident>? ]` + debug_assert!(!self.line_num.is_zero()); + self.line_num.to_css(dest)?; + if has_ident { + dest.write_char(' ')?; + self.ident.to_css(dest)?; + } + Ok(()) + } +} + +impl Parse for GridLine<specified::Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut grid_line = Self::auto(); + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(grid_line); + } + + // <custom-ident> | [ <integer> && <custom-ident>? ] | [ span && [ <integer> || <custom-ident> ] ] + // This <grid-line> horror is simply, + // [ span? && [ <custom-ident> || <integer> ] ] + // And, for some magical reason, "span" should be the first or last value and not in-between. + let mut val_before_span = false; + + for _ in 0..3 { + // Maximum possible entities for <grid-line> + let location = input.current_source_location(); + if input.try_parse(|i| i.expect_ident_matching("span")).is_ok() { + if grid_line.is_span { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if !grid_line.line_num.is_zero() || grid_line.ident.0 != atom!("") { + val_before_span = true; + } + + grid_line.is_span = true; + } else if let Ok(i) = input.try_parse(|i| specified::Integer::parse(context, i)) { + // FIXME(emilio): Probably shouldn't reject if it's calc()... + let value = i.value(); + if value == 0 || val_before_span || !grid_line.line_num.is_zero() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + grid_line.line_num = specified::Integer::new(cmp::max( + MIN_GRID_LINE, + cmp::min(value, MAX_GRID_LINE), + )); + } else if let Ok(name) = input.try_parse(|i| CustomIdent::parse(i, &["auto"])) { + if val_before_span || grid_line.ident.0 != atom!("") { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + // NOTE(emilio): `span` is consumed above, so we only need to + // reject `auto`. + grid_line.ident = name; + } else { + break; + } + } + + if grid_line.is_auto() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if grid_line.is_span { + if !grid_line.line_num.is_zero() { + if grid_line.line_num.value() <= 0 { + // disallow negative integers for grid spans + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } else if grid_line.ident.0 == atom!("") { + // integer could be omitted + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + Ok(grid_line) + } +} + +/// A track breadth for explicit grid track sizing. It's generic solely to +/// avoid re-implementing it for the computed type. +/// +/// <https://drafts.csswg.org/css-grid/#typedef-track-breadth> +#[derive( + Animate, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericTrackBreadth<L> { + /// The generic type is almost always a non-negative `<length-percentage>` + Breadth(L), + /// A flex fraction specified in `fr` units. + #[css(dimension)] + Fr(CSSFloat), + /// `auto` + Auto, + /// `min-content` + MinContent, + /// `max-content` + MaxContent, +} + +pub use self::GenericTrackBreadth as TrackBreadth; + +impl<L> TrackBreadth<L> { + /// Check whether this is a `<fixed-breadth>` (i.e., it only has `<length-percentage>`) + /// + /// <https://drafts.csswg.org/css-grid/#typedef-fixed-breadth> + #[inline] + pub fn is_fixed(&self) -> bool { + matches!(*self, TrackBreadth::Breadth(..)) + } +} + +/// A `<track-size>` type for explicit grid track sizing. Like `<track-breadth>`, this is +/// generic only to avoid code bloat. It only takes `<length-percentage>` +/// +/// <https://drafts.csswg.org/css-grid/#typedef-track-size> +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericTrackSize<L> { + /// A flexible `<track-breadth>` + Breadth(GenericTrackBreadth<L>), + /// A `minmax` function for a range over an inflexible `<track-breadth>` + /// and a flexible `<track-breadth>` + /// + /// <https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-minmax> + #[css(function)] + Minmax(GenericTrackBreadth<L>, GenericTrackBreadth<L>), + /// A `fit-content` function. + /// + /// This stores a TrackBreadth<L> for convenience, but it can only be a + /// LengthPercentage. + /// + /// <https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-fit-content> + #[css(function)] + FitContent(GenericTrackBreadth<L>), +} + +pub use self::GenericTrackSize as TrackSize; + +impl<L> TrackSize<L> { + /// The initial value. + const INITIAL_VALUE: Self = TrackSize::Breadth(TrackBreadth::Auto); + + /// Returns the initial value. + pub const fn initial_value() -> Self { + Self::INITIAL_VALUE + } + + /// Returns true if `self` is the initial value. + pub fn is_initial(&self) -> bool { + matches!(*self, TrackSize::Breadth(TrackBreadth::Auto)) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585 + } + + /// Check whether this is a `<fixed-size>` + /// + /// <https://drafts.csswg.org/css-grid/#typedef-fixed-size> + pub fn is_fixed(&self) -> bool { + match *self { + TrackSize::Breadth(ref breadth) => breadth.is_fixed(), + // For minmax function, it could be either + // minmax(<fixed-breadth>, <track-breadth>) or minmax(<inflexible-breadth>, <fixed-breadth>), + // and since both variants are a subset of minmax(<inflexible-breadth>, <track-breadth>), we only + // need to make sure that they're fixed. So, we don't have to modify the parsing function. + TrackSize::Minmax(ref breadth_1, ref breadth_2) => { + if breadth_1.is_fixed() { + return true; // the second value is always a <track-breadth> + } + + match *breadth_1 { + TrackBreadth::Fr(_) => false, // should be <inflexible-breadth> at this point + _ => breadth_2.is_fixed(), + } + }, + TrackSize::FitContent(_) => false, + } + } +} + +impl<L> Default for TrackSize<L> { + fn default() -> Self { + Self::initial_value() + } +} + +impl<L: ToCss> ToCss for TrackSize<L> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + TrackSize::Breadth(ref breadth) => breadth.to_css(dest), + TrackSize::Minmax(ref min, ref max) => { + // According to gecko minmax(auto, <flex>) is equivalent to <flex>, + // and both are serialized as <flex>. + if let TrackBreadth::Auto = *min { + if let TrackBreadth::Fr(_) = *max { + return max.to_css(dest); + } + } + + dest.write_str("minmax(")?; + min.to_css(dest)?; + dest.write_str(", ")?; + max.to_css(dest)?; + dest.write_char(')') + }, + TrackSize::FitContent(ref lp) => { + dest.write_str("fit-content(")?; + lp.to_css(dest)?; + dest.write_char(')') + }, + } + } +} + +/// A `<track-size>+`. +/// We use the empty slice as `auto`, and always parse `auto` as an empty slice. +/// This means it's impossible to have a slice containing only one auto item. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct GenericImplicitGridTracks<T>( + #[css(if_empty = "auto", iterable)] pub crate::OwnedSlice<T>, +); + +pub use self::GenericImplicitGridTracks as ImplicitGridTracks; + +impl<T: fmt::Debug + Default + PartialEq> ImplicitGridTracks<T> { + /// Returns true if current value is same as its initial value (i.e. auto). + pub fn is_initial(&self) -> bool { + debug_assert_ne!( + *self, + ImplicitGridTracks(crate::OwnedSlice::from(vec![Default::default()])) + ); + self.0.is_empty() + } +} + +/// Helper function for serializing identifiers with a prefix and suffix, used +/// for serializing <line-names> (in grid). +pub fn concat_serialize_idents<W>( + prefix: &str, + suffix: &str, + slice: &[CustomIdent], + sep: &str, + dest: &mut CssWriter<W>, +) -> fmt::Result +where + W: Write, +{ + if let Some((ref first, rest)) = slice.split_first() { + dest.write_str(prefix)?; + first.to_css(dest)?; + for thing in rest { + dest.write_str(sep)?; + thing.to_css(dest)?; + } + + dest.write_str(suffix)?; + } + + Ok(()) +} + +/// The initial argument of the `repeat` function. +/// +/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat> +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum RepeatCount<Integer> { + /// A positive integer. This is allowed only for `<track-repeat>` and `<fixed-repeat>` + Number(Integer), + /// An `<auto-fill>` keyword allowed only for `<auto-repeat>` + AutoFill, + /// An `<auto-fit>` keyword allowed only for `<auto-repeat>` + AutoFit, +} + +impl Parse for RepeatCount<specified::Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(mut i) = input.try_parse(|i| specified::Integer::parse_positive(context, i)) { + if i.value() > MAX_GRID_LINE { + i = specified::Integer::new(MAX_GRID_LINE); + } + return Ok(RepeatCount::Number(i)); + } + try_match_ident_ignore_ascii_case! { input, + "auto-fill" => Ok(RepeatCount::AutoFill), + "auto-fit" => Ok(RepeatCount::AutoFit), + } + } +} + +/// The structure containing `<line-names>` and `<track-size>` values. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(function = "repeat")] +#[repr(C)] +pub struct GenericTrackRepeat<L, I> { + /// The number of times for the value to be repeated (could also be `auto-fit` or `auto-fill`) + pub count: RepeatCount<I>, + /// `<line-names>` accompanying `<track_size>` values. + /// + /// If there's no `<line-names>`, then it's represented by an empty vector. + /// For N `<track-size>` values, there will be N+1 `<line-names>`, and so this vector's + /// length is always one value more than that of the `<track-size>`. + pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>, + /// `<track-size>` values. + pub track_sizes: crate::OwnedSlice<GenericTrackSize<L>>, +} + +pub use self::GenericTrackRepeat as TrackRepeat; + +impl<L: ToCss, I: ToCss> ToCss for TrackRepeat<L, I> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("repeat(")?; + self.count.to_css(dest)?; + dest.write_str(", ")?; + + let mut line_names_iter = self.line_names.iter(); + for (i, (ref size, ref names)) in self + .track_sizes + .iter() + .zip(&mut line_names_iter) + .enumerate() + { + if i > 0 { + dest.write_char(' ')?; + } + + concat_serialize_idents("[", "] ", names, " ", dest)?; + size.to_css(dest)?; + } + + if let Some(line_names_last) = line_names_iter.next() { + concat_serialize_idents(" [", "]", line_names_last, " ", dest)?; + } + + dest.write_char(')')?; + + Ok(()) + } +} + +/// Track list values. Can be <track-size> or <track-repeat> +#[derive( + Animate, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericTrackListValue<LengthPercentage, Integer> { + /// A <track-size> value. + TrackSize(#[animation(field_bound)] GenericTrackSize<LengthPercentage>), + /// A <track-repeat> value. + TrackRepeat(#[animation(field_bound)] GenericTrackRepeat<LengthPercentage, Integer>), +} + +pub use self::GenericTrackListValue as TrackListValue; + +impl<L, I> TrackListValue<L, I> { + // FIXME: can't use TrackSize::initial_value() here b/c rustc error "is not yet stable as a const fn" + const INITIAL_VALUE: Self = TrackListValue::TrackSize(TrackSize::Breadth(TrackBreadth::Auto)); + + fn is_repeat(&self) -> bool { + matches!(*self, TrackListValue::TrackRepeat(..)) + } + + /// Returns true if `self` is the initial value. + pub fn is_initial(&self) -> bool { + matches!( + *self, + TrackListValue::TrackSize(TrackSize::Breadth(TrackBreadth::Auto)) + ) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585 + } +} + +impl<L, I> Default for TrackListValue<L, I> { + #[inline] + fn default() -> Self { + Self::INITIAL_VALUE + } +} + +/// A grid `<track-list>` type. +/// +/// <https://drafts.csswg.org/css-grid/#typedef-track-list> +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericTrackList<LengthPercentage, Integer> { + /// The index in `values` where our `<auto-repeat>` value is, if in bounds. + #[css(skip)] + pub auto_repeat_index: usize, + /// A vector of `<track-size> | <track-repeat>` values. + pub values: crate::OwnedSlice<GenericTrackListValue<LengthPercentage, Integer>>, + /// `<line-names>` accompanying `<track-size> | <track-repeat>` values. + /// + /// If there's no `<line-names>`, then it's represented by an empty vector. + /// For N values, there will be N+1 `<line-names>`, and so this vector's + /// length is always one value more than that of the `<track-size>`. + pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>, +} + +pub use self::GenericTrackList as TrackList; + +impl<L, I> TrackList<L, I> { + /// Whether this track list is an explicit track list (that is, doesn't have + /// any repeat values). + pub fn is_explicit(&self) -> bool { + !self.values.iter().any(|v| v.is_repeat()) + } + + /// Whether this track list has an `<auto-repeat>` value. + pub fn has_auto_repeat(&self) -> bool { + self.auto_repeat_index < self.values.len() + } +} + +impl<L: ToCss, I: ToCss> ToCss for TrackList<L, I> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let mut values_iter = self.values.iter().peekable(); + let mut line_names_iter = self.line_names.iter().peekable(); + + for idx in 0.. { + let names = line_names_iter.next().unwrap(); // This should exist! + concat_serialize_idents("[", "]", names, " ", dest)?; + + match values_iter.next() { + Some(value) => { + if !names.is_empty() { + dest.write_char(' ')?; + } + + value.to_css(dest)?; + }, + None => break, + } + + if values_iter.peek().is_some() || + line_names_iter.peek().map_or(false, |v| !v.is_empty()) || + (idx + 1 == self.auto_repeat_index) + { + dest.write_char(' ')?; + } + } + + Ok(()) + } +} + +/// The `<name-repeat>` for subgrids. +/// +/// <name-repeat> = repeat( [ <integer [1,∞]> | auto-fill ], <line-names>+) +/// +/// https://drafts.csswg.org/css-grid/#typedef-name-repeat +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericNameRepeat<I> { + /// The number of times for the value to be repeated (could also be `auto-fill`). + /// Note: `RepeatCount` accepts `auto-fit`, so we should reject it after parsing it. + pub count: RepeatCount<I>, + /// This represents `<line-names>+`. The length of the outer vector is at least one. + pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>, +} + +pub use self::GenericNameRepeat as NameRepeat; + +impl<I: ToCss> ToCss for NameRepeat<I> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("repeat(")?; + self.count.to_css(dest)?; + dest.write_char(',')?; + + for ref names in self.line_names.iter() { + if names.is_empty() { + // Note: concat_serialize_idents() skip the empty list so we have to handle it + // manually for NameRepeat. + dest.write_str(" []")?; + } else { + concat_serialize_idents(" [", "]", names, " ", dest)?; + } + } + + dest.write_char(')') + } +} + +impl<I> NameRepeat<I> { + /// Returns true if it is auto-fill. + #[inline] + pub fn is_auto_fill(&self) -> bool { + matches!(self.count, RepeatCount::AutoFill) + } +} + +/// A single value for `<line-names>` or `<name-repeat>`. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericLineNameListValue<I> { + /// `<line-names>`. + LineNames(crate::OwnedSlice<CustomIdent>), + /// `<name-repeat>`. + Repeat(GenericNameRepeat<I>), +} + +pub use self::GenericLineNameListValue as LineNameListValue; + +impl<I: ToCss> ToCss for LineNameListValue<I> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Self::Repeat(ref r) => r.to_css(dest), + Self::LineNames(ref names) => { + dest.write_char('[')?; + + if let Some((ref first, rest)) = names.split_first() { + first.to_css(dest)?; + for name in rest { + dest.write_char(' ')?; + name.to_css(dest)?; + } + } + + dest.write_char(']') + }, + } + } +} + +/// The `<line-name-list>` for subgrids. +/// +/// <line-name-list> = [ <line-names> | <name-repeat> ]+ +/// <name-repeat> = repeat( [ <integer [1,∞]> | auto-fill ], <line-names>+) +/// +/// https://drafts.csswg.org/css-grid/#typedef-line-name-list +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericLineNameList<I> { + /// The pre-computed length of line_names, without the length of repeat(auto-fill, ...). + // We precomputed this at parsing time, so we can avoid an extra loop when expanding + // repeat(auto-fill). + pub expanded_line_names_length: usize, + /// The line name list. + pub line_names: crate::OwnedSlice<GenericLineNameListValue<I>>, +} + +pub use self::GenericLineNameList as LineNameList; + +impl<I: ToCss> ToCss for LineNameList<I> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("subgrid")?; + + for value in self.line_names.iter() { + dest.write_char(' ')?; + value.to_css(dest)?; + } + + Ok(()) + } +} + +/// Variants for `<grid-template-rows> | <grid-template-columns>` +#[derive( + Animate, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "subgrid")] +#[repr(C, u8)] +pub enum GenericGridTemplateComponent<L, I> { + /// `none` value. + None, + /// The grid `<track-list>` + TrackList( + #[animation(field_bound)] + #[compute(field_bound)] + #[resolve(field_bound)] + #[shmem(field_bound)] + Box<GenericTrackList<L, I>>, + ), + /// A `subgrid <line-name-list>?` + /// TODO: Support animations for this after subgrid is addressed in [grid-2] spec. + #[animation(error)] + Subgrid(Box<GenericLineNameList<I>>), + /// `masonry` value. + /// https://github.com/w3c/csswg-drafts/issues/4650 + Masonry, +} + +pub use self::GenericGridTemplateComponent as GridTemplateComponent; + +impl<L, I> GridTemplateComponent<L, I> { + /// The initial value. + const INITIAL_VALUE: Self = Self::None; + + /// Returns length of the <track-list>s <track-size> + pub fn track_list_len(&self) -> usize { + match *self { + GridTemplateComponent::TrackList(ref tracklist) => tracklist.values.len(), + _ => 0, + } + } + + /// Returns true if `self` is the initial value. + pub fn is_initial(&self) -> bool { + matches!(*self, Self::None) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585 + } +} + +impl<L, I> Default for GridTemplateComponent<L, I> { + #[inline] + fn default() -> Self { + Self::INITIAL_VALUE + } +} diff --git a/servo/components/style/values/generics/image.rs b/servo/components/style/values/generics/image.rs new file mode 100644 index 0000000000..6fc0870e15 --- /dev/null +++ b/servo/components/style/values/generics/image.rs @@ -0,0 +1,631 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for the handling of [images]. +//! +//! [images]: https://drafts.csswg.org/css-images/#image-values + +use crate::color::mix::ColorInterpolationMethod; +use crate::custom_properties; +use crate::values::generics::position::PositionComponent; +use crate::values::generics::Optional; +use crate::values::serialize_atom_identifier; +use crate::Atom; +use crate::Zero; +use servo_arc::Arc; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// An `<image> | none` value. +/// +/// https://drafts.csswg.org/css-images/#image-values +#[derive( + Clone, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(C, u8)] +pub enum GenericImage<G, ImageUrl, Color, Percentage, Resolution> { + /// `none` variant. + None, + /// A `<url()>` image. + Url(ImageUrl), + + /// A `<gradient>` image. Gradients are rather large, and not nearly as + /// common as urls, so we box them here to keep the size of this enum sane. + Gradient(Box<G>), + + /// A `-moz-element(# <element-id>)` + #[cfg(feature = "gecko")] + #[css(function = "-moz-element")] + Element(Atom), + + /// A paint worklet image. + /// <https://drafts.css-houdini.org/css-paint-api/> + #[cfg(feature = "servo-layout-2013")] + PaintWorklet(PaintWorklet), + + /// A `<cross-fade()>` image. Storing this directly inside of + /// GenericImage increases the size by 8 bytes so we box it here + /// and store images directly inside of cross-fade instead of + /// boxing them there. + CrossFade(Box<GenericCrossFade<Self, Color, Percentage>>), + + /// An `image-set()` function. + ImageSet(#[compute(field_bound)] Box<GenericImageSet<Self, Resolution>>), +} + +pub use self::GenericImage as Image; + +/// <https://drafts.csswg.org/css-images-4/#cross-fade-function> +#[derive( + Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem, ToCss, ToComputedValue, +)] +#[css(comma, function = "cross-fade")] +#[repr(C)] +pub struct GenericCrossFade<Image, Color, Percentage> { + /// All of the image percent pairings passed as arguments to + /// cross-fade. + #[css(iterable)] + pub elements: crate::OwnedSlice<GenericCrossFadeElement<Image, Color, Percentage>>, +} + +/// An optional percent and a cross fade image. +#[derive( + Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, ToCss, +)] +#[repr(C)] +pub struct GenericCrossFadeElement<Image, Color, Percentage> { + /// The percent of the final image that `image` will be. + pub percent: Optional<Percentage>, + /// A color or image that will be blended when cross-fade is + /// evaluated. + pub image: GenericCrossFadeImage<Image, Color>, +} + +/// An image or a color. `cross-fade` takes either when blending +/// images together. +#[derive( + Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, ToCss, +)] +#[repr(C, u8)] +pub enum GenericCrossFadeImage<I, C> { + /// A boxed image value. Boxing provides indirection so images can + /// be cross-fades and cross-fades can be images. + Image(I), + /// A color value. + Color(C), +} + +pub use self::GenericCrossFade as CrossFade; +pub use self::GenericCrossFadeElement as CrossFadeElement; +pub use self::GenericCrossFadeImage as CrossFadeImage; + +/// https://drafts.csswg.org/css-images-4/#image-set-notation +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)] +#[css(comma, function = "image-set")] +#[repr(C)] +pub struct GenericImageSet<Image, Resolution> { + /// The index of the selected candidate. usize::MAX for specified values or invalid images. + #[css(skip)] + pub selected_index: usize, + + /// All of the image and resolution pairs. + #[css(iterable)] + pub items: crate::OwnedSlice<GenericImageSetItem<Image, Resolution>>, +} + +/// An optional percent and a cross fade image. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] +#[repr(C)] +pub struct GenericImageSetItem<Image, Resolution> { + /// `<image>`. `<string>` is converted to `Image::Url` at parse time. + pub image: Image, + /// The `<resolution>`. + /// + /// TODO: Skip serialization if it is 1x. + pub resolution: Resolution, + + /// The `type(<string>)` + /// (Optional) Specify the image's MIME type + pub mime_type: crate::OwnedStr, + + /// True if mime_type has been specified + pub has_mime_type: bool, +} + +impl<I: style_traits::ToCss, R: style_traits::ToCss> ToCss for GenericImageSetItem<I, R> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + self.image.to_css(dest)?; + dest.write_char(' ')?; + self.resolution.to_css(dest)?; + + if self.has_mime_type { + dest.write_char(' ')?; + dest.write_str("type(")?; + self.mime_type.to_css(dest)?; + dest.write_char(')')?; + } + Ok(()) + } +} + +pub use self::GenericImageSet as ImageSet; +pub use self::GenericImageSetItem as ImageSetItem; + +/// State flags stored on each variant of a Gradient. +#[derive( + Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(C)] +pub struct GradientFlags(u8); +bitflags! { + impl GradientFlags: u8 { + /// Set if this is a repeating gradient. + const REPEATING = 1 << 0; + /// Set if the color interpolation method matches the default for the items. + const HAS_DEFAULT_COLOR_INTERPOLATION_METHOD = 1 << 1; + } +} + +/// A CSS gradient. +/// <https://drafts.csswg.org/css-images/#gradients> +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] +#[repr(C)] +pub enum GenericGradient< + LineDirection, + LengthPercentage, + NonNegativeLength, + NonNegativeLengthPercentage, + Position, + Angle, + AngleOrPercentage, + Color, +> { + /// A linear gradient. + Linear { + /// Line direction + direction: LineDirection, + /// Method to use for color interpolation. + color_interpolation_method: ColorInterpolationMethod, + /// The color stops and interpolation hints. + items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>, + /// State flags for the gradient. + flags: GradientFlags, + /// Compatibility mode. + compat_mode: GradientCompatMode, + }, + /// A radial gradient. + Radial { + /// Shape of gradient + shape: GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>, + /// Center of gradient + position: Position, + /// Method to use for color interpolation. + color_interpolation_method: ColorInterpolationMethod, + /// The color stops and interpolation hints. + items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>, + /// State flags for the gradient. + flags: GradientFlags, + /// Compatibility mode. + compat_mode: GradientCompatMode, + }, + /// A conic gradient. + Conic { + /// Start angle of gradient + angle: Angle, + /// Center of gradient + position: Position, + /// Method to use for color interpolation. + color_interpolation_method: ColorInterpolationMethod, + /// The color stops and interpolation hints. + items: crate::OwnedSlice<GenericGradientItem<Color, AngleOrPercentage>>, + /// State flags for the gradient. + flags: GradientFlags, + }, +} + +pub use self::GenericGradient as Gradient; + +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(u8)] +/// Whether we used the modern notation or the compatibility `-webkit`, `-moz` prefixes. +pub enum GradientCompatMode { + /// Modern syntax. + Modern, + /// `-webkit` prefix. + WebKit, + /// `-moz` prefix + Moz, +} + +/// A radial gradient's ending shape. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, +)] +#[repr(C, u8)] +pub enum GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage> { + /// A circular gradient. + Circle(GenericCircle<NonNegativeLength>), + /// An elliptic gradient. + Ellipse(GenericEllipse<NonNegativeLengthPercentage>), +} + +pub use self::GenericEndingShape as EndingShape; + +/// A circle shape. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(C, u8)] +pub enum GenericCircle<NonNegativeLength> { + /// A circle radius. + Radius(NonNegativeLength), + /// A circle extent. + Extent(ShapeExtent), +} + +pub use self::GenericCircle as Circle; + +/// An ellipse shape. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, +)] +#[repr(C, u8)] +pub enum GenericEllipse<NonNegativeLengthPercentage> { + /// An ellipse pair of radii. + Radii(NonNegativeLengthPercentage, NonNegativeLengthPercentage), + /// An ellipse extent. + Extent(ShapeExtent), +} + +pub use self::GenericEllipse as Ellipse; + +/// <https://drafts.csswg.org/css-images/#typedef-extent-keyword> +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ShapeExtent { + ClosestSide, + FarthestSide, + ClosestCorner, + FarthestCorner, + Contain, + Cover, +} + +/// A gradient item. +/// <https://drafts.csswg.org/css-images-4/#color-stop-syntax> +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, +)] +#[repr(C, u8)] +pub enum GenericGradientItem<Color, T> { + /// A simple color stop, without position. + SimpleColorStop(Color), + /// A complex color stop, with a position. + ComplexColorStop { + /// The color for the stop. + color: Color, + /// The position for the stop. + position: T, + }, + /// An interpolation hint. + InterpolationHint(T), +} + +pub use self::GenericGradientItem as GradientItem; + +/// A color stop. +/// <https://drafts.csswg.org/css-images/#typedef-color-stop-list> +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem, +)] +pub struct ColorStop<Color, T> { + /// The color of this stop. + pub color: Color, + /// The position of this stop. + pub position: Option<T>, +} + +impl<Color, T> ColorStop<Color, T> { + /// Convert the color stop into an appropriate `GradientItem`. + #[inline] + pub fn into_item(self) -> GradientItem<Color, T> { + match self.position { + Some(position) => GradientItem::ComplexColorStop { + color: self.color, + position, + }, + None => GradientItem::SimpleColorStop(self.color), + } + } +} + +/// Specified values for a paint worklet. +/// <https://drafts.css-houdini.org/css-paint-api/> +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Clone, Debug, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] +pub struct PaintWorklet { + /// The name the worklet was registered with. + pub name: Atom, + /// The arguments for the worklet. + /// TODO: store a parsed representation of the arguments. + #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")] + #[compute(no_field_bound)] + #[resolve(no_field_bound)] + pub arguments: Vec<Arc<custom_properties::SpecifiedValue>>, +} + +impl ::style_traits::SpecifiedValueInfo for PaintWorklet {} + +impl ToCss for PaintWorklet { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("paint(")?; + serialize_atom_identifier(&self.name, dest)?; + for argument in &self.arguments { + dest.write_str(", ")?; + argument.to_css(dest)?; + } + dest.write_char(')') + } +} + +impl<G, U, C, P, Resolution> fmt::Debug for Image<G, U, C, P, Resolution> +where + Image<G, U, C, P, Resolution>: ToCss, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(&mut CssWriter::new(f)) + } +} + +impl<G, U, C, P, Resolution> ToCss for Image<G, U, C, P, Resolution> +where + G: ToCss, + U: ToCss, + C: ToCss, + P: ToCss, + Resolution: ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Image::None => dest.write_str("none"), + Image::Url(ref url) => url.to_css(dest), + Image::Gradient(ref gradient) => gradient.to_css(dest), + #[cfg(feature = "servo-layout-2013")] + Image::PaintWorklet(ref paint_worklet) => paint_worklet.to_css(dest), + #[cfg(feature = "gecko")] + Image::Element(ref selector) => { + dest.write_str("-moz-element(#")?; + serialize_atom_identifier(selector, dest)?; + dest.write_char(')') + }, + Image::ImageSet(ref is) => is.to_css(dest), + Image::CrossFade(ref cf) => cf.to_css(dest), + } + } +} + +impl<D, LP, NL, NLP, P, A: Zero, AoP, C> ToCss for Gradient<D, LP, NL, NLP, P, A, AoP, C> +where + D: LineDirection, + LP: ToCss, + NL: ToCss, + NLP: ToCss, + P: PositionComponent + ToCss, + A: ToCss, + AoP: ToCss, + C: ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let (compat_mode, repeating, has_default_color_interpolation_method) = match *self { + Gradient::Linear { + compat_mode, flags, .. + } | + Gradient::Radial { + compat_mode, flags, .. + } => ( + compat_mode, + flags.contains(GradientFlags::REPEATING), + flags.contains(GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD), + ), + Gradient::Conic { flags, .. } => ( + GradientCompatMode::Modern, + flags.contains(GradientFlags::REPEATING), + flags.contains(GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD), + ), + }; + + match compat_mode { + GradientCompatMode::WebKit => dest.write_str("-webkit-")?, + GradientCompatMode::Moz => dest.write_str("-moz-")?, + _ => {}, + } + + if repeating { + dest.write_str("repeating-")?; + } + + match *self { + Gradient::Linear { + ref direction, + ref color_interpolation_method, + ref items, + compat_mode, + .. + } => { + dest.write_str("linear-gradient(")?; + let mut skip_comma = true; + if !direction.points_downwards(compat_mode) { + direction.to_css(dest, compat_mode)?; + skip_comma = false; + } + if !has_default_color_interpolation_method { + if !skip_comma { + dest.write_char(' ')?; + } + color_interpolation_method.to_css(dest)?; + skip_comma = false; + } + for item in &**items { + if !skip_comma { + dest.write_str(", ")?; + } + skip_comma = false; + item.to_css(dest)?; + } + }, + Gradient::Radial { + ref shape, + ref position, + ref color_interpolation_method, + ref items, + compat_mode, + .. + } => { + dest.write_str("radial-gradient(")?; + let omit_shape = match *shape { + EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::Cover)) | + EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) => true, + _ => false, + }; + let omit_position = position.is_center(); + if compat_mode == GradientCompatMode::Modern { + if !omit_shape { + shape.to_css(dest)?; + if !omit_position { + dest.write_char(' ')?; + } + } + if !omit_position { + dest.write_str("at ")?; + position.to_css(dest)?; + } + } else { + if !omit_position { + position.to_css(dest)?; + if !omit_shape { + dest.write_str(", ")?; + } + } + if !omit_shape { + shape.to_css(dest)?; + } + } + if !has_default_color_interpolation_method { + if !omit_shape || !omit_position { + dest.write_char(' ')?; + } + color_interpolation_method.to_css(dest)?; + } + + let mut skip_comma = + omit_shape && omit_position && has_default_color_interpolation_method; + for item in &**items { + if !skip_comma { + dest.write_str(", ")?; + } + skip_comma = false; + item.to_css(dest)?; + } + }, + Gradient::Conic { + ref angle, + ref position, + ref color_interpolation_method, + ref items, + .. + } => { + dest.write_str("conic-gradient(")?; + let omit_angle = angle.is_zero(); + let omit_position = position.is_center(); + if !omit_angle { + dest.write_str("from ")?; + angle.to_css(dest)?; + if !omit_position { + dest.write_char(' ')?; + } + } + if !omit_position { + dest.write_str("at ")?; + position.to_css(dest)?; + } + if !has_default_color_interpolation_method { + if !omit_angle || !omit_position { + dest.write_char(' ')?; + } + color_interpolation_method.to_css(dest)?; + } + let mut skip_comma = + omit_angle && omit_position && has_default_color_interpolation_method; + for item in &**items { + if !skip_comma { + dest.write_str(", ")?; + } + skip_comma = false; + item.to_css(dest)?; + } + }, + } + dest.write_char(')') + } +} + +/// The direction of a linear gradient. +pub trait LineDirection { + /// Whether this direction points towards, and thus can be omitted. + fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool; + + /// Serialises this direction according to the compatibility mode. + fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result + where + W: Write; +} + +impl<L> ToCss for Circle<L> +where + L: ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Circle::Extent(ShapeExtent::FarthestCorner) | Circle::Extent(ShapeExtent::Cover) => { + dest.write_str("circle") + }, + Circle::Extent(keyword) => { + dest.write_str("circle ")?; + keyword.to_css(dest) + }, + Circle::Radius(ref length) => length.to_css(dest), + } + } +} diff --git a/servo/components/style/values/generics/length.rs b/servo/components/style/values/generics/length.rs new file mode 100644 index 0000000000..de0dd7fbc1 --- /dev/null +++ b/servo/components/style/values/generics/length.rs @@ -0,0 +1,304 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values related to length. + +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::Zero; +use cssparser::Parser; +use style_traits::ParseError; + +/// A `<length-percentage> | auto` value. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericLengthPercentageOrAuto<LengthPercent> { + LengthPercentage(LengthPercent), + Auto, +} + +pub use self::GenericLengthPercentageOrAuto as LengthPercentageOrAuto; + +impl<LengthPercentage> LengthPercentageOrAuto<LengthPercentage> { + /// `auto` value. + #[inline] + pub fn auto() -> Self { + LengthPercentageOrAuto::Auto + } + + /// Whether this is the `auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, LengthPercentageOrAuto::Auto) + } + + /// A helper function to parse this with quirks or not and so forth. + pub fn parse_with<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parser: impl FnOnce( + &ParserContext, + &mut Parser<'i, 't>, + ) -> Result<LengthPercentage, ParseError<'i>>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(LengthPercentageOrAuto::Auto); + } + + Ok(LengthPercentageOrAuto::LengthPercentage(parser( + context, input, + )?)) + } +} + +impl<LengthPercentage> LengthPercentageOrAuto<LengthPercentage> +where + LengthPercentage: Clone, +{ + /// Resolves `auto` values by calling `f`. + #[inline] + pub fn auto_is(&self, f: impl FnOnce() -> LengthPercentage) -> LengthPercentage { + match self { + LengthPercentageOrAuto::LengthPercentage(length) => length.clone(), + LengthPercentageOrAuto::Auto => f(), + } + } + + /// Returns the non-`auto` value, if any. + #[inline] + pub fn non_auto(&self) -> Option<LengthPercentage> { + match self { + LengthPercentageOrAuto::LengthPercentage(length) => Some(length.clone()), + LengthPercentageOrAuto::Auto => None, + } + } + + /// Maps the length of this value. + pub fn map<T>(&self, f: impl FnOnce(LengthPercentage) -> T) -> LengthPercentageOrAuto<T> { + match self { + LengthPercentageOrAuto::LengthPercentage(l) => { + LengthPercentageOrAuto::LengthPercentage(f(l.clone())) + }, + LengthPercentageOrAuto::Auto => LengthPercentageOrAuto::Auto, + } + } +} + +impl<LengthPercentage: Zero> Zero for LengthPercentageOrAuto<LengthPercentage> { + fn zero() -> Self { + LengthPercentageOrAuto::LengthPercentage(Zero::zero()) + } + + fn is_zero(&self) -> bool { + match *self { + LengthPercentageOrAuto::LengthPercentage(ref l) => l.is_zero(), + LengthPercentageOrAuto::Auto => false, + } + } +} + +impl<LengthPercentage: Parse> Parse for LengthPercentageOrAuto<LengthPercentage> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, LengthPercentage::parse) + } +} + +/// A generic value for the `width`, `height`, `min-width`, or `min-height` property. +/// +/// Unlike `max-width` or `max-height` properties, a Size can be `auto`, +/// and cannot be `none`. +/// +/// Note that it only accepts non-negative values. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericSize<LengthPercent> { + LengthPercentage(LengthPercent), + Auto, + #[animation(error)] + MaxContent, + #[animation(error)] + MinContent, + #[animation(error)] + FitContent, + #[animation(error)] + MozAvailable, + #[animation(error)] + #[css(function = "fit-content")] + FitContentFunction(LengthPercent), +} + +pub use self::GenericSize as Size; + +impl<LengthPercentage> Size<LengthPercentage> { + /// `auto` value. + #[inline] + pub fn auto() -> Self { + Size::Auto + } + + /// Returns whether we're the auto value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, Size::Auto) + } +} + +/// A generic value for the `max-width` or `max-height` property. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericMaxSize<LengthPercent> { + LengthPercentage(LengthPercent), + None, + #[animation(error)] + MaxContent, + #[animation(error)] + MinContent, + #[animation(error)] + FitContent, + #[animation(error)] + MozAvailable, + #[animation(error)] + #[css(function = "fit-content")] + FitContentFunction(LengthPercent), +} + +pub use self::GenericMaxSize as MaxSize; + +impl<LengthPercentage> MaxSize<LengthPercentage> { + /// `none` value. + #[inline] + pub fn none() -> Self { + MaxSize::None + } +} + +/// A generic `<length>` | `<number>` value for the `tab-size` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericLengthOrNumber<L, N> { + /// A number. + /// + /// NOTE: Numbers need to be before lengths, in order to parse them + /// first, since `0` should be a number, not the `0px` length. + Number(N), + /// A length. + Length(L), +} + +pub use self::GenericLengthOrNumber as LengthOrNumber; + +impl<L, N: Zero> Zero for LengthOrNumber<L, N> { + fn zero() -> Self { + LengthOrNumber::Number(Zero::zero()) + } + + fn is_zero(&self) -> bool { + match *self { + LengthOrNumber::Number(ref n) => n.is_zero(), + LengthOrNumber::Length(..) => false, + } + } +} + +/// A generic `<length-percentage>` | normal` value. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +#[allow(missing_docs)] +pub enum GenericLengthPercentageOrNormal<LengthPercent> { + LengthPercentage(LengthPercent), + Normal, +} + +pub use self::GenericLengthPercentageOrNormal as LengthPercentageOrNormal; + +impl<LengthPercent> LengthPercentageOrNormal<LengthPercent> { + /// Returns the normal value. + #[inline] + pub fn normal() -> Self { + LengthPercentageOrNormal::Normal + } +} diff --git a/servo/components/style/values/generics/mod.rs b/servo/components/style/values/generics/mod.rs new file mode 100644 index 0000000000..800d058170 --- /dev/null +++ b/servo/components/style/values/generics/mod.rs @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types that share their serialization implementations +//! for both specified and computed values. + +use super::CustomIdent; +use crate::counter_style::{parse_counter_style_name, Symbols}; +use crate::parser::{Parse, ParserContext}; +use crate::Zero; +use cssparser::Parser; +use std::ops::Add; +use style_traits::{KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind}; + +pub mod animation; +pub mod background; +pub mod basic_shape; +pub mod border; +#[path = "box.rs"] +pub mod box_; +pub mod calc; +pub mod color; +pub mod column; +pub mod counters; +pub mod easing; +pub mod effects; +pub mod flex; +pub mod font; +pub mod grid; +pub mod image; +pub mod length; +pub mod motion; +pub mod page; +pub mod position; +pub mod ratio; +pub mod rect; +pub mod size; +pub mod svg; +pub mod text; +pub mod transform; +pub mod ui; +pub mod url; + +/// https://drafts.csswg.org/css-counter-styles/#typedef-symbols-type +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum SymbolsType { + Cyclic, + Numeric, + Alphabetic, + Symbolic, + Fixed, +} + +/// <https://drafts.csswg.org/css-counter-styles/#typedef-counter-style> +/// +/// Note that 'none' is not a valid name. +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Debug, Eq, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +pub enum CounterStyle { + /// `<counter-style-name>` + Name(CustomIdent), + /// `symbols()` + #[css(function)] + Symbols(#[css(skip_if = "is_symbolic")] SymbolsType, Symbols), +} + +#[inline] +fn is_symbolic(symbols_type: &SymbolsType) -> bool { + *symbols_type == SymbolsType::Symbolic +} + +impl CounterStyle { + /// disc value + pub fn disc() -> Self { + CounterStyle::Name(CustomIdent(atom!("disc"))) + } + + /// decimal value + pub fn decimal() -> Self { + CounterStyle::Name(CustomIdent(atom!("decimal"))) + } + + /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`) + #[inline] + pub fn is_bullet(&self) -> bool { + match self { + CounterStyle::Name(CustomIdent(ref name)) => { + name == &atom!("disc") || + name == &atom!("circle") || + name == &atom!("square") || + name == &atom!("disclosure-closed") || + name == &atom!("disclosure-open") + }, + _ => false, + } + } +} + +impl Parse for CounterStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(name) = input.try_parse(|i| parse_counter_style_name(i)) { + return Ok(CounterStyle::Name(name)); + } + input.expect_function_matching("symbols")?; + input.parse_nested_block(|input| { + let symbols_type = input + .try_parse(SymbolsType::parse) + .unwrap_or(SymbolsType::Symbolic); + let symbols = Symbols::parse(context, input)?; + // There must be at least two symbols for alphabetic or + // numeric system. + if (symbols_type == SymbolsType::Alphabetic || symbols_type == SymbolsType::Numeric) && + symbols.0.len() < 2 + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + // Identifier is not allowed in symbols() function. + if symbols.0.iter().any(|sym| !sym.is_allowed_in_symbols()) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(CounterStyle::Symbols(symbols_type, symbols)) + }) + } +} + +impl SpecifiedValueInfo for CounterStyle { + fn collect_completion_keywords(f: KeywordsCollectFn) { + // XXX The best approach for implementing this is probably + // having a CounterStyleName type wrapping CustomIdent, and + // put the predefined list for that type in counter_style mod. + // But that's a non-trivial change itself, so we use a simpler + // approach here. + macro_rules! predefined { + ($($name:expr,)+) => { + f(&["symbols", $($name,)+]) + } + } + include!("../../counter_style/predefined.rs"); + } +} + +/// A wrapper of Non-negative values. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + Serialize, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct NonNegative<T>(pub T); + +impl<T: Add<Output = T>> Add<NonNegative<T>> for NonNegative<T> { + type Output = Self; + + fn add(self, other: Self) -> Self { + NonNegative(self.0 + other.0) + } +} + +impl<T: Zero> Zero for NonNegative<T> { + fn is_zero(&self) -> bool { + self.0.is_zero() + } + + fn zero() -> Self { + NonNegative(T::zero()) + } +} + +/// A wrapper of greater-than-or-equal-to-one values. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct GreaterThanOrEqualToOne<T>(pub T); + +/// A wrapper of values between zero and one. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct ZeroToOne<T>(pub T); + +/// A clip rect for clip and image-region +#[allow(missing_docs)] +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(function = "rect", comma)] +#[repr(C)] +pub struct GenericClipRect<LengthOrAuto> { + pub top: LengthOrAuto, + pub right: LengthOrAuto, + pub bottom: LengthOrAuto, + pub left: LengthOrAuto, +} + +pub use self::GenericClipRect as ClipRect; + +/// Either a clip-rect or `auto`. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericClipRectOrAuto<R> { + Auto, + Rect(R), +} + +pub use self::GenericClipRectOrAuto as ClipRectOrAuto; + +impl<L> ClipRectOrAuto<L> { + /// Returns the `auto` value. + #[inline] + pub fn auto() -> Self { + ClipRectOrAuto::Auto + } + + /// Returns whether this value is the `auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, ClipRectOrAuto::Auto) + } +} + +pub use page::PageSize; + +/// An optional value, much like `Option<T>`, but with a defined struct layout +/// to be able to use it from C++ as well. +/// +/// Note that this is relatively inefficient, struct-layout-wise, as you have +/// one byte for the tag, but padding to the alignment of T. If you have +/// multiple optional values and care about struct compactness, you might be +/// better off "coalescing" the combinations into a parent enum. But that +/// shouldn't matter for most use cases. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[repr(C, u8)] +pub enum Optional<T> { + #[css(skip)] + None, + Some(T), +} + +impl<T> Optional<T> { + /// Returns whether this value is present. + pub fn is_some(&self) -> bool { + matches!(*self, Self::Some(..)) + } + + /// Returns whether this value is not present. + pub fn is_none(&self) -> bool { + matches!(*self, Self::None) + } + + /// Turns this Optional<> into a regular rust Option<>. + pub fn into_rust(self) -> Option<T> { + match self { + Self::Some(v) => Some(v), + Self::None => None, + } + } + + /// Return a reference to the containing value, if any, as a plain rust + /// Option<>. + pub fn as_ref(&self) -> Option<&T> { + match *self { + Self::Some(ref v) => Some(v), + Self::None => None, + } + } +} + +impl<T> From<Option<T>> for Optional<T> { + fn from(rust: Option<T>) -> Self { + match rust { + Some(t) => Self::Some(t), + None => Self::None, + } + } +} diff --git a/servo/components/style/values/generics/motion.rs b/servo/components/style/values/generics/motion.rs new file mode 100644 index 0000000000..ee6f5702da --- /dev/null +++ b/servo/components/style/values/generics/motion.rs @@ -0,0 +1,270 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS Motion Path. + +use crate::values::animated::ToAnimatedZero; +use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto}; +use crate::values::specified::motion::CoordBox; +use serde::Deserializer; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// The <size> in ray() function. +/// +/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-size +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum RaySize { + ClosestSide, + ClosestCorner, + FarthestSide, + FarthestCorner, + Sides, +} + +/// The `ray()` function, `ray( [ <angle> && <size> && contain? && [at <position>]? ] )` +/// +/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-ray +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericRayFunction<Angle, Position> { + /// The bearing angle with `0deg` pointing up and positive angles + /// representing clockwise rotation. + pub angle: Angle, + /// Decide the path length used when `offset-distance` is expressed + /// as a percentage. + pub size: RaySize, + /// Clamp `offset-distance` so that the box is entirely contained + /// within the path. + #[animation(constant)] + pub contain: bool, + /// The "at <position>" part. If omitted, we use auto to represent it. + pub position: GenericPositionOrAuto<Position>, +} + +pub use self::GenericRayFunction as RayFunction; + +impl<Angle, Position> ToCss for RayFunction<Angle, Position> +where + Angle: ToCss, + Position: ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.angle.to_css(dest)?; + + if !matches!(self.size, RaySize::ClosestSide) { + dest.write_char(' ')?; + self.size.to_css(dest)?; + } + + if self.contain { + dest.write_str(" contain")?; + } + + if !matches!(self.position, GenericPositionOrAuto::Auto) { + dest.write_str(" at ")?; + self.position.to_css(dest)?; + } + + Ok(()) + } +} + +/// Return error if we try to deserialize the url, for Gecko IPC purposes. +// Note: we cannot use #[serde(skip_deserializing)] variant attribute, which may cause the fatal +// error when trying to read the parameters because it cannot deserialize the input byte buffer, +// even if the type of OffsetPathFunction is not an url(), in our tests. This may be an issue of +// #[serde(skip_deserializing)] on enum, at least in the version (1.0) we are using. So we have to +// manually implement this deseriailzing function, but return error. +// FIXME: Bug 1847620, fiure out this is a serde issue or a gecko bug. +fn deserialize_url<'de, D, T>(_deserializer: D) -> Result<T, D::Error> +where + D: Deserializer<'de>, +{ + use crate::serde::de::Error; + // Return Err() so the IPC will catch it and assert this as a fetal error. + Err(<D as Deserializer>::Error::custom( + "we don't support the deserializing for url", + )) +} + +/// The <offset-path> value. +/// <offset-path> = <ray()> | <url> | <basic-shape> +/// +/// https://drafts.fxtf.org/motion-1/#typedef-offset-path +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[animation(no_bound(U))] +#[repr(C, u8)] +pub enum GenericOffsetPathFunction<Shapes, RayFunction, U> { + /// ray() function, which defines a path in the polar coordinate system. + /// Use Box<> to make sure the size of offset-path is not too large. + #[css(function)] + Ray(RayFunction), + /// A URL reference to an SVG shape element. If the URL does not reference a shape element, + /// this behaves as path("m 0 0") instead. + #[animation(error)] + #[serde(deserialize_with = "deserialize_url")] + #[serde(skip_serializing)] + Url(U), + /// The <basic-shape> value. + Shape(Shapes), +} + +pub use self::GenericOffsetPathFunction as OffsetPathFunction; + +/// The offset-path property. +/// offset-path: none | <offset-path> || <coord-box> +/// +/// https://drafts.fxtf.org/motion-1/#offset-path-property +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericOffsetPath<Function> { + /// <offset-path> || <coord-box>. + OffsetPath { + /// <offset-path> part. + // Note: Use Box<> to make sure the size of this property doesn't go over the threshold. + path: Box<Function>, + /// <coord-box> part. + #[css(skip_if = "CoordBox::is_default")] + coord_box: CoordBox, + }, + /// Only <coord-box>. This represents that <offset-path> is omitted, so we use the default + /// value, inset(0 round X), where X is the value of border-radius on the element that + /// establishes the containing block for this element. + CoordBox(CoordBox), + /// None value. + #[animation(error)] + None, +} + +pub use self::GenericOffsetPath as OffsetPath; + +impl<Function> OffsetPath<Function> { + /// Return None. + #[inline] + pub fn none() -> Self { + OffsetPath::None + } +} + +impl<Function> ToAnimatedZero for OffsetPath<Function> { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +/// The offset-position property, which specifies the offset starting position that is used by the +/// <offset-path> functions if they don’t specify their own starting position. +/// +/// https://drafts.fxtf.org/motion-1/#offset-position-property +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericOffsetPosition<H, V> { + /// The element does not have an offset starting position. + Normal, + /// The offset starting position is the top-left corner of the box. + Auto, + /// The offset starting position is the result of using the <position> to position a 0x0 object + /// area within the box’s containing block. + Position( + #[css(field_bound)] + #[parse(field_bound)] + GenericPosition<H, V>, + ), +} + +pub use self::GenericOffsetPosition as OffsetPosition; + +impl<H, V> OffsetPosition<H, V> { + /// Returns the initial value, normal. + #[inline] + pub fn normal() -> Self { + Self::Normal + } +} diff --git a/servo/components/style/values/generics/page.rs b/servo/components/style/values/generics/page.rs new file mode 100644 index 0000000000..91f02bc4b3 --- /dev/null +++ b/servo/components/style/values/generics/page.rs @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! @page at-rule properties + +use crate::values::generics::NonNegative; +use crate::values::specified::length::AbsoluteLength; + +/// Page size names. +/// +/// https://drafts.csswg.org/css-page-3/#typedef-page-size-page-size +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum PaperSize { + /// ISO A5 media + A5, + /// ISO A4 media + A4, + /// ISO A3 media + A3, + /// ISO B5 media + B5, + /// ISO B4 media + B4, + /// JIS B5 media + JisB5, + /// JIS B4 media + JisB4, + /// North American Letter size + Letter, + /// North American Legal size + Legal, + /// North American Ledger size + Ledger, +} + +impl PaperSize { + /// Gets the long edge length of the paper size + pub fn long_edge(&self) -> NonNegative<AbsoluteLength> { + NonNegative(match *self { + PaperSize::A5 => AbsoluteLength::Mm(210.0), + PaperSize::A4 => AbsoluteLength::Mm(297.0), + PaperSize::A3 => AbsoluteLength::Mm(420.0), + PaperSize::B5 => AbsoluteLength::Mm(250.0), + PaperSize::B4 => AbsoluteLength::Mm(353.0), + PaperSize::JisB5 => AbsoluteLength::Mm(257.0), + PaperSize::JisB4 => AbsoluteLength::Mm(364.0), + PaperSize::Letter => AbsoluteLength::In(11.0), + PaperSize::Legal => AbsoluteLength::In(14.0), + PaperSize::Ledger => AbsoluteLength::In(17.0), + }) + } + /// Gets the short edge length of the paper size + pub fn short_edge(&self) -> NonNegative<AbsoluteLength> { + NonNegative(match *self { + PaperSize::A5 => AbsoluteLength::Mm(148.0), + PaperSize::A4 => AbsoluteLength::Mm(210.0), + PaperSize::A3 => AbsoluteLength::Mm(297.0), + PaperSize::B5 => AbsoluteLength::Mm(176.0), + PaperSize::B4 => AbsoluteLength::Mm(250.0), + PaperSize::JisB5 => AbsoluteLength::Mm(182.0), + PaperSize::JisB4 => AbsoluteLength::Mm(257.0), + PaperSize::Letter => AbsoluteLength::In(8.5), + PaperSize::Legal => AbsoluteLength::In(8.5), + PaperSize::Ledger => AbsoluteLength::In(11.0), + }) + } +} + +/// Page orientation names. +/// +/// https://drafts.csswg.org/css-page-3/#page-orientation-prop +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum PageOrientation { + /// upright + Upright, + /// rotate-left (counter-clockwise) + RotateLeft, + /// rotate-right (clockwise) + RotateRight, +} + +/// Paper orientation +/// +/// https://drafts.csswg.org/css-page-3/#page-size-prop +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum PageSizeOrientation { + /// Portrait orientation + Portrait, + /// Landscape orientation + Landscape, +} + +#[inline] +fn is_portrait(orientation: &PageSizeOrientation) -> bool { + *orientation == PageSizeOrientation::Portrait +} + +/// Page size property +/// +/// https://drafts.csswg.org/css-page-3/#page-size-prop +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[repr(C, u8)] +pub enum GenericPageSize<S> { + /// `auto` value. + Auto, + /// Page dimensions. + Size(S), + /// An orientation with no size. + Orientation(PageSizeOrientation), + /// Paper size by name + PaperSize( + PaperSize, + #[css(skip_if = "is_portrait")] PageSizeOrientation, + ), +} + +pub use self::GenericPageSize as PageSize; + +impl<S> PageSize<S> { + /// `auto` value. + #[inline] + pub fn auto() -> Self { + PageSize::Auto + } + + /// Whether this is the `auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, PageSize::Auto) + } +} diff --git a/servo/components/style/values/generics/position.rs b/servo/components/style/values/generics/position.rs new file mode 100644 index 0000000000..8a4a8c9e24 --- /dev/null +++ b/servo/components/style/values/generics/position.rs @@ -0,0 +1,238 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS handling of specified and computed values of +//! [`position`](https://drafts.csswg.org/css-backgrounds-3/#position) + +use crate::values::animated::ToAnimatedZero; +use crate::values::generics::ratio::Ratio; + +/// A generic type for representing a CSS [position](https://drafts.csswg.org/css-values/#position). +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericPosition<H, V> { + /// The horizontal component of position. + pub horizontal: H, + /// The vertical component of position. + pub vertical: V, +} + +impl<H, V> PositionComponent for Position<H, V> +where + H: PositionComponent, + V: PositionComponent, +{ + #[inline] + fn is_center(&self) -> bool { + self.horizontal.is_center() && self.vertical.is_center() + } +} + +pub use self::GenericPosition as Position; + +impl<H, V> Position<H, V> { + /// Returns a new position. + pub fn new(horizontal: H, vertical: V) -> Self { + Self { + horizontal, + vertical, + } + } +} + +/// Implements a method that checks if the position is centered. +pub trait PositionComponent { + /// Returns if the position component is 50% or center. + /// For pixel lengths, it always returns false. + fn is_center(&self) -> bool; +} + +/// A generic type for representing an `Auto | <position>`. +/// This is used by <offset-anchor> for now. +/// https://drafts.fxtf.org/motion-1/#offset-anchor-property +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericPositionOrAuto<Pos> { + /// The <position> value. + Position(Pos), + /// The keyword `auto`. + Auto, +} + +pub use self::GenericPositionOrAuto as PositionOrAuto; + +impl<Pos> PositionOrAuto<Pos> { + /// Return `auto`. + #[inline] + pub fn auto() -> Self { + PositionOrAuto::Auto + } + + /// Return true if it is 'auto'. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(self, PositionOrAuto::Auto) + } +} + +/// A generic value for the `z-index` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericZIndex<I> { + /// An integer value. + Integer(I), + /// The keyword `auto`. + Auto, +} + +pub use self::GenericZIndex as ZIndex; + +impl<Integer> ZIndex<Integer> { + /// Returns `auto` + #[inline] + pub fn auto() -> Self { + ZIndex::Auto + } + + /// Returns whether `self` is `auto`. + #[inline] + pub fn is_auto(self) -> bool { + matches!(self, ZIndex::Auto) + } + + /// Returns the integer value if it is an integer, or `auto`. + #[inline] + pub fn integer_or(self, auto: Integer) -> Integer { + match self { + ZIndex::Integer(n) => n, + ZIndex::Auto => auto, + } + } +} + +/// Ratio or None. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum PreferredRatio<N> { + /// Without specified ratio + #[css(skip)] + None, + /// With specified ratio + Ratio( + #[animation(field_bound)] + #[css(field_bound)] + #[distance(field_bound)] + Ratio<N>, + ), +} + +/// A generic value for the `aspect-ratio` property, the value is `auto || <ratio>`. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericAspectRatio<N> { + /// Specifiy auto or not. + #[animation(constant)] + #[css(represents_keyword)] + pub auto: bool, + /// The preferred aspect-ratio value. + #[animation(field_bound)] + #[css(field_bound)] + #[distance(field_bound)] + pub ratio: PreferredRatio<N>, +} + +pub use self::GenericAspectRatio as AspectRatio; + +impl<N> AspectRatio<N> { + /// Returns `auto` + #[inline] + pub fn auto() -> Self { + AspectRatio { + auto: true, + ratio: PreferredRatio::None, + } + } +} + +impl<N> ToAnimatedZero for AspectRatio<N> { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} diff --git a/servo/components/style/values/generics/ratio.rs b/servo/components/style/values/generics/ratio.rs new file mode 100644 index 0000000000..8c66fed602 --- /dev/null +++ b/servo/components/style/values/generics/ratio.rs @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values related to <ratio>. +//! https://drafts.csswg.org/css-values/#ratios + +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A generic value for the `<ratio>` value. +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Ratio<N>(pub N, pub N); + +impl<N> ToCss for Ratio<N> +where + N: ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + // Even though 1 could be omitted, we don't per + // https://drafts.csswg.org/css-values-4/#ratio-value: + // + // The second <number> is optional, defaulting to 1. However, + // <ratio> is always serialized with both components. + // + // And for compat reasons, see bug 1669742. + // + // We serialize with spaces for consistency with all other + // slash-delimited things, see + // https://github.com/w3c/csswg-drafts/issues/4282 + dest.write_str(" / ")?; + self.1.to_css(dest)?; + Ok(()) + } +} diff --git a/servo/components/style/values/generics/rect.rs b/servo/components/style/values/generics/rect.rs new file mode 100644 index 0000000000..ea9de67732 --- /dev/null +++ b/servo/components/style/values/generics/rect.rs @@ -0,0 +1,146 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values that are composed of four sides. + +use crate::parser::{Parse, ParserContext}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A CSS value made of four components, where its `ToCss` impl will try to +/// serialize as few components as possible, like for example in `border-width`. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + Serialize, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Rect<T>(pub T, pub T, pub T, pub T); + +impl<T> Rect<T> { + /// Returns a new `Rect<T>` value. + pub fn new(first: T, second: T, third: T, fourth: T) -> Self { + Rect(first, second, third, fourth) + } +} + +impl<T> Rect<T> +where + T: Clone, +{ + /// Returns a rect with all the values equal to `v`. + pub fn all(v: T) -> Self { + Rect::new(v.clone(), v.clone(), v.clone(), v) + } + + /// Parses a new `Rect<T>` value with the given parse function. + pub fn parse_with<'i, 't, Parse>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse: Parse, + ) -> Result<Self, ParseError<'i>> + where + Parse: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>, + { + let first = parse(context, input)?; + let second = if let Ok(second) = input.try_parse(|i| parse(context, i)) { + second + } else { + // <first> + return Ok(Self::new( + first.clone(), + first.clone(), + first.clone(), + first, + )); + }; + let third = if let Ok(third) = input.try_parse(|i| parse(context, i)) { + third + } else { + // <first> <second> + return Ok(Self::new(first.clone(), second.clone(), first, second)); + }; + let fourth = if let Ok(fourth) = input.try_parse(|i| parse(context, i)) { + fourth + } else { + // <first> <second> <third> + return Ok(Self::new(first, second.clone(), third, second)); + }; + // <first> <second> <third> <fourth> + Ok(Self::new(first, second, third, fourth)) + } + + /// Parses a new `Rect<T>` value which all components must be specified, with the given parse + /// function. + pub fn parse_all_components_with<'i, 't, Parse>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse: Parse, + ) -> Result<Self, ParseError<'i>> + where + Parse: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>, + { + let first = parse(context, input)?; + let second = parse(context, input)?; + let third = parse(context, input)?; + let fourth = parse(context, input)?; + // <first> <second> <third> <fourth> + Ok(Self::new(first, second, third, fourth)) + } +} + +impl<T> Parse for Rect<T> +where + T: Clone + Parse, +{ + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, T::parse) + } +} + +impl<T> ToCss for Rect<T> +where + T: PartialEq + ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + let same_vertical = self.0 == self.2; + let same_horizontal = self.1 == self.3; + if same_vertical && same_horizontal && self.0 == self.1 { + return Ok(()); + } + dest.write_char(' ')?; + self.1.to_css(dest)?; + if same_vertical && same_horizontal { + return Ok(()); + } + dest.write_char(' ')?; + self.2.to_css(dest)?; + if same_horizontal { + return Ok(()); + } + dest.write_char(' ')?; + self.3.to_css(dest) + } +} diff --git a/servo/components/style/values/generics/size.rs b/servo/components/style/values/generics/size.rs new file mode 100644 index 0000000000..979b8f9322 --- /dev/null +++ b/servo/components/style/values/generics/size.rs @@ -0,0 +1,101 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic type for CSS properties that are composed by two dimensions. + +use crate::parser::ParserContext; +use crate::Zero; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A generic size, for `border-*-radius` longhand properties, or +/// `border-spacing`. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + Serialize, + ToAnimatedZero, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(C)] +pub struct Size2D<L> { + pub width: L, + pub height: L, +} + +impl<L> Size2D<L> { + #[inline] + /// Create a new `Size2D` for an area of given width and height. + pub fn new(width: L, height: L) -> Self { + Self { width, height } + } + + /// Returns the width component. + pub fn width(&self) -> &L { + &self.width + } + + /// Returns the height component. + pub fn height(&self) -> &L { + &self.height + } + + /// Parse a `Size2D` with a given parsing function. + pub fn parse_with<'i, 't, F>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse_one: F, + ) -> Result<Self, ParseError<'i>> + where + L: Clone, + F: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<L, ParseError<'i>>, + { + let first = parse_one(context, input)?; + let second = input + .try_parse(|i| parse_one(context, i)) + .unwrap_or_else(|_| first.clone()); + Ok(Self::new(first, second)) + } +} + +impl<L> ToCss for Size2D<L> +where + L: ToCss + PartialEq, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.width.to_css(dest)?; + + if self.height != self.width { + dest.write_char(' ')?; + self.height.to_css(dest)?; + } + + Ok(()) + } +} + +impl<L: Zero> Zero for Size2D<L> { + fn zero() -> Self { + Self::new(L::zero(), L::zero()) + } + + fn is_zero(&self) -> bool { + self.width.is_zero() && self.height.is_zero() + } +} diff --git a/servo/components/style/values/generics/svg.rs b/servo/components/style/values/generics/svg.rs new file mode 100644 index 0000000000..43ba77f1ff --- /dev/null +++ b/servo/components/style/values/generics/svg.rs @@ -0,0 +1,221 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values in SVG + +use crate::parser::{Parse, ParserContext}; +use cssparser::Parser; +use style_traits::ParseError; + +/// The fallback of an SVG paint server value. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericSVGPaintFallback<C> { + /// The `none` keyword. + None, + /// A magic value that represents no fallback specified and serializes to + /// the empty string. + #[css(skip)] + Unset, + /// A color. + Color(C), +} + +pub use self::GenericSVGPaintFallback as SVGPaintFallback; + +/// An SVG paint value +/// +/// <https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint> +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[animation(no_bound(Url))] +#[repr(C)] +pub struct GenericSVGPaint<Color, Url> { + /// The paint source. + pub kind: GenericSVGPaintKind<Color, Url>, + /// The fallback color. + pub fallback: GenericSVGPaintFallback<Color>, +} + +pub use self::GenericSVGPaint as SVGPaint; + +impl<C, U> Default for SVGPaint<C, U> { + fn default() -> Self { + Self { + kind: SVGPaintKind::None, + fallback: SVGPaintFallback::Unset, + } + } +} + +/// An SVG paint value without the fallback. +/// +/// Whereas the spec only allows PaintServer to have a fallback, Gecko lets the +/// context properties have a fallback as well. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[animation(no_bound(U))] +#[repr(C, u8)] +pub enum GenericSVGPaintKind<C, U> { + /// `none` + #[animation(error)] + None, + /// `<color>` + Color(C), + /// `url(...)` + #[animation(error)] + PaintServer(U), + /// `context-fill` + ContextFill, + /// `context-stroke` + ContextStroke, +} + +pub use self::GenericSVGPaintKind as SVGPaintKind; + +impl<C: Parse, U: Parse> Parse for SVGPaint<C, U> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let kind = SVGPaintKind::parse(context, input)?; + if matches!(kind, SVGPaintKind::None | SVGPaintKind::Color(..)) { + return Ok(SVGPaint { + kind, + fallback: SVGPaintFallback::Unset, + }); + } + let fallback = input + .try_parse(|i| SVGPaintFallback::parse(context, i)) + .unwrap_or(SVGPaintFallback::Unset); + Ok(SVGPaint { kind, fallback }) + } +} + +/// An SVG length value supports `context-value` in addition to length. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericSVGLength<L> { + /// `<length> | <percentage> | <number>` + LengthPercentage(L), + /// `context-value` + #[animation(error)] + ContextValue, +} + +pub use self::GenericSVGLength as SVGLength; + +/// Generic value for stroke-dasharray. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericSVGStrokeDashArray<L> { + /// `[ <length> | <percentage> | <number> ]#` + #[css(comma)] + Values(#[css(if_empty = "none", iterable)] crate::OwnedSlice<L>), + /// `context-value` + ContextValue, +} + +pub use self::GenericSVGStrokeDashArray as SVGStrokeDashArray; + +/// An SVG opacity value accepts `context-{fill,stroke}-opacity` in +/// addition to opacity value. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericSVGOpacity<OpacityType> { + /// `<opacity-value>` + Opacity(OpacityType), + /// `context-fill-opacity` + #[animation(error)] + ContextFillOpacity, + /// `context-stroke-opacity` + #[animation(error)] + ContextStrokeOpacity, +} + +pub use self::GenericSVGOpacity as SVGOpacity; diff --git a/servo/components/style/values/generics/text.rs b/servo/components/style/values/generics/text.rs new file mode 100644 index 0000000000..ef2647b014 --- /dev/null +++ b/servo/components/style/values/generics/text.rs @@ -0,0 +1,148 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for text properties. + +use crate::parser::ParserContext; +use crate::Zero; +use cssparser::Parser; +use style_traits::ParseError; + +/// A generic value for the `initial-letter` property. +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum InitialLetter<Number, Integer> { + /// `normal` + Normal, + /// `<number> <integer>?` + Specified(Number, Option<Integer>), +} + +impl<N, I> InitialLetter<N, I> { + /// Returns `normal`. + #[inline] + pub fn normal() -> Self { + InitialLetter::Normal + } +} + +/// A generic spacing value for the `letter-spacing` and `word-spacing` properties. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum Spacing<Value> { + /// `normal` + Normal, + /// `<value>` + Value(Value), +} + +impl<Value> Spacing<Value> { + /// Returns `normal`. + #[inline] + pub fn normal() -> Self { + Spacing::Normal + } + + /// Parses. + #[inline] + pub fn parse_with<'i, 't, F>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse: F, + ) -> Result<Self, ParseError<'i>> + where + F: FnOnce(&ParserContext, &mut Parser<'i, 't>) -> Result<Value, ParseError<'i>>, + { + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(Spacing::Normal); + } + parse(context, input).map(Spacing::Value) + } +} + +/// Implements type for text-decoration-thickness +/// which takes the grammar of auto | from-font | <length> | <percentage> +/// +/// https://drafts.csswg.org/css-text-decor-4/ +#[repr(C, u8)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Animate, + Clone, + Copy, + ComputeSquaredDistance, + ToAnimatedZero, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum GenericTextDecorationLength<L> { + LengthPercentage(L), + Auto, + FromFont, +} + +/// Implements type for text-indent +/// which takes the grammar of [<length-percentage>] && hanging? && each-line? +/// +/// https://drafts.csswg.org/css-text/#propdef-text-indent +#[repr(C)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct GenericTextIndent<LengthPercentage> { + /// The amount of indent to be applied to the inline-start of the first line. + pub length: LengthPercentage, + /// Apply indent to non-first lines instead of first. + #[animation(constant)] + #[css(represents_keyword)] + pub hanging: bool, + /// Apply to each line after a hard break, not only first in block. + #[animation(constant)] + #[css(represents_keyword)] + pub each_line: bool, +} + +impl<LengthPercentage: Zero> GenericTextIndent<LengthPercentage> { + /// Return the initial zero value. + pub fn zero() -> Self { + Self { + length: LengthPercentage::zero(), + hanging: false, + each_line: false, + } + } +} diff --git a/servo/components/style/values/generics/transform.rs b/servo/components/style/values/generics/transform.rs new file mode 100644 index 0000000000..3a65c460a7 --- /dev/null +++ b/servo/components/style/values/generics/transform.rs @@ -0,0 +1,879 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for CSS values that are related to transformations. + +use crate::values::computed::length::Length as ComputedLength; +use crate::values::computed::length::LengthPercentage as ComputedLengthPercentage; +use crate::values::specified::angle::Angle as SpecifiedAngle; +use crate::values::specified::length::Length as SpecifiedLength; +use crate::values::specified::length::LengthPercentage as SpecifiedLengthPercentage; +use crate::values::{computed, CSSFloat}; +use crate::{Zero, ZeroNoPercent}; +use euclid; +use euclid::default::{Rect, Transform3D}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A generic 2D transformation matrix. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma, function = "matrix")] +#[repr(C)] +pub struct GenericMatrix<T> { + pub a: T, + pub b: T, + pub c: T, + pub d: T, + pub e: T, + pub f: T, +} + +pub use self::GenericMatrix as Matrix; + +#[allow(missing_docs)] +#[cfg_attr(rustfmt, rustfmt_skip)] +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma, function = "matrix3d")] +#[repr(C)] +pub struct GenericMatrix3D<T> { + pub m11: T, pub m12: T, pub m13: T, pub m14: T, + pub m21: T, pub m22: T, pub m23: T, pub m24: T, + pub m31: T, pub m32: T, pub m33: T, pub m34: T, + pub m41: T, pub m42: T, pub m43: T, pub m44: T, +} + +pub use self::GenericMatrix3D as Matrix3D; + +#[cfg_attr(rustfmt, rustfmt_skip)] +impl<T: Into<f64>> From<Matrix<T>> for Transform3D<f64> { + #[inline] + fn from(m: Matrix<T>) -> Self { + Transform3D::new( + m.a.into(), m.b.into(), 0.0, 0.0, + m.c.into(), m.d.into(), 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + m.e.into(), m.f.into(), 0.0, 1.0, + ) + } +} + +#[cfg_attr(rustfmt, rustfmt_skip)] +impl<T: Into<f64>> From<Matrix3D<T>> for Transform3D<f64> { + #[inline] + fn from(m: Matrix3D<T>) -> Self { + Transform3D::new( + m.m11.into(), m.m12.into(), m.m13.into(), m.m14.into(), + m.m21.into(), m.m22.into(), m.m23.into(), m.m24.into(), + m.m31.into(), m.m32.into(), m.m33.into(), m.m34.into(), + m.m41.into(), m.m42.into(), m.m43.into(), m.m44.into(), + ) + } +} + +/// A generic transform origin. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericTransformOrigin<H, V, Depth> { + /// The horizontal origin. + pub horizontal: H, + /// The vertical origin. + pub vertical: V, + /// The depth. + pub depth: Depth, +} + +pub use self::GenericTransformOrigin as TransformOrigin; + +impl<H, V, D> TransformOrigin<H, V, D> { + /// Returns a new transform origin. + pub fn new(horizontal: H, vertical: V, depth: D) -> Self { + Self { + horizontal, + vertical, + depth, + } + } +} + +fn is_same<N: PartialEq>(x: &N, y: &N) -> bool { + x == y +} + +/// A value for the `perspective()` transform function, which is either a +/// non-negative `<length>` or `none`. +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericPerspectiveFunction<L> { + /// `none` + None, + /// A `<length>`. + Length(L), +} + +impl<L> GenericPerspectiveFunction<L> { + /// Returns `f32::INFINITY` or the result of a function on the length value. + pub fn infinity_or(&self, f: impl FnOnce(&L) -> f32) -> f32 { + match *self { + Self::None => f32::INFINITY, + Self::Length(ref l) => f(l), + } + } +} + +pub use self::GenericPerspectiveFunction as PerspectiveFunction; + +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// A single operation in the list of a `transform` value +pub enum GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage> +where + Angle: Zero, + LengthPercentage: Zero + ZeroNoPercent, + Number: PartialEq, +{ + /// Represents a 2D 2x3 matrix. + Matrix(GenericMatrix<Number>), + /// Represents a 3D 4x4 matrix. + Matrix3D(GenericMatrix3D<Number>), + /// A 2D skew. + /// + /// If the second angle is not provided it is assumed zero. + /// + /// Syntax can be skew(angle) or skew(angle, angle) + #[css(comma, function)] + Skew(Angle, #[css(skip_if = "Zero::is_zero")] Angle), + /// skewX(angle) + #[css(function = "skewX")] + SkewX(Angle), + /// skewY(angle) + #[css(function = "skewY")] + SkewY(Angle), + /// translate(x, y) or translate(x) + #[css(comma, function)] + Translate( + LengthPercentage, + #[css(skip_if = "ZeroNoPercent::is_zero_no_percent")] LengthPercentage, + ), + /// translateX(x) + #[css(function = "translateX")] + TranslateX(LengthPercentage), + /// translateY(y) + #[css(function = "translateY")] + TranslateY(LengthPercentage), + /// translateZ(z) + #[css(function = "translateZ")] + TranslateZ(Length), + /// translate3d(x, y, z) + #[css(comma, function = "translate3d")] + Translate3D(LengthPercentage, LengthPercentage, Length), + /// A 2D scaling factor. + /// + /// Syntax can be scale(factor) or scale(factor, factor) + #[css(comma, function)] + Scale(Number, #[css(contextual_skip_if = "is_same")] Number), + /// scaleX(factor) + #[css(function = "scaleX")] + ScaleX(Number), + /// scaleY(factor) + #[css(function = "scaleY")] + ScaleY(Number), + /// scaleZ(factor) + #[css(function = "scaleZ")] + ScaleZ(Number), + /// scale3D(factorX, factorY, factorZ) + #[css(comma, function = "scale3d")] + Scale3D(Number, Number, Number), + /// Describes a 2D Rotation. + /// + /// In a 3D scene `rotate(angle)` is equivalent to `rotateZ(angle)`. + #[css(function)] + Rotate(Angle), + /// Rotation in 3D space around the x-axis. + #[css(function = "rotateX")] + RotateX(Angle), + /// Rotation in 3D space around the y-axis. + #[css(function = "rotateY")] + RotateY(Angle), + /// Rotation in 3D space around the z-axis. + #[css(function = "rotateZ")] + RotateZ(Angle), + /// Rotation in 3D space. + /// + /// Generalization of rotateX, rotateY and rotateZ. + #[css(comma, function = "rotate3d")] + Rotate3D(Number, Number, Number, Angle), + /// Specifies a perspective projection matrix. + /// + /// Part of CSS Transform Module Level 2 and defined at + /// [§ 13.1. 3D Transform Function](https://drafts.csswg.org/css-transforms-2/#funcdef-perspective). + /// + /// The value must be greater than or equal to zero. + #[css(function)] + Perspective(GenericPerspectiveFunction<Length>), + /// A intermediate type for interpolation of mismatched transform lists. + #[allow(missing_docs)] + #[css(comma, function = "interpolatematrix")] + InterpolateMatrix { + from_list: GenericTransform< + GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>, + >, + to_list: GenericTransform< + GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>, + >, + progress: computed::Percentage, + }, + /// A intermediate type for accumulation of mismatched transform lists. + #[allow(missing_docs)] + #[css(comma, function = "accumulatematrix")] + AccumulateMatrix { + from_list: GenericTransform< + GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>, + >, + to_list: GenericTransform< + GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>, + >, + count: Integer, + }, +} + +pub use self::GenericTransformOperation as TransformOperation; + +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// A value of the `transform` property +pub struct GenericTransform<T>(#[css(if_empty = "none", iterable)] pub crate::OwnedSlice<T>); + +pub use self::GenericTransform as Transform; + +impl<Angle, Number, Length, Integer, LengthPercentage> + TransformOperation<Angle, Number, Length, Integer, LengthPercentage> +where + Angle: Zero, + LengthPercentage: Zero + ZeroNoPercent, + Number: PartialEq, +{ + /// Check if it is any rotate function. + pub fn is_rotate(&self) -> bool { + use self::TransformOperation::*; + matches!( + *self, + Rotate(..) | Rotate3D(..) | RotateX(..) | RotateY(..) | RotateZ(..) + ) + } + + /// Check if it is any translate function + pub fn is_translate(&self) -> bool { + use self::TransformOperation::*; + match *self { + Translate(..) | Translate3D(..) | TranslateX(..) | TranslateY(..) | TranslateZ(..) => { + true + }, + _ => false, + } + } + + /// Check if it is any scale function + pub fn is_scale(&self) -> bool { + use self::TransformOperation::*; + match *self { + Scale(..) | Scale3D(..) | ScaleX(..) | ScaleY(..) | ScaleZ(..) => true, + _ => false, + } + } +} + +/// Convert a length type into the absolute lengths. +pub trait ToAbsoluteLength { + /// Returns the absolute length as pixel value. + fn to_pixel_length(&self, containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()>; +} + +impl ToAbsoluteLength for SpecifiedLength { + // This returns Err(()) if there is any relative length or percentage. We use this when + // parsing a transform list of DOMMatrix because we want to return a DOM Exception + // if there is relative length. + #[inline] + fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> { + match *self { + SpecifiedLength::NoCalc(len) => len.to_computed_pixel_length_without_context(), + SpecifiedLength::Calc(ref calc) => calc.to_computed_pixel_length_without_context(), + } + } +} + +impl ToAbsoluteLength for SpecifiedLengthPercentage { + // This returns Err(()) if there is any relative length or percentage. We use this when + // parsing a transform list of DOMMatrix because we want to return a DOM Exception + // if there is relative length. + #[inline] + fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> { + use self::SpecifiedLengthPercentage::*; + match *self { + Length(len) => len.to_computed_pixel_length_without_context(), + Calc(ref calc) => calc.to_computed_pixel_length_without_context(), + Percentage(..) => Err(()), + } + } +} + +impl ToAbsoluteLength for ComputedLength { + #[inline] + fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> { + Ok(self.px()) + } +} + +impl ToAbsoluteLength for ComputedLengthPercentage { + #[inline] + fn to_pixel_length(&self, containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> { + Ok(self + .maybe_percentage_relative_to(containing_len) + .ok_or(())? + .px()) + } +} + +/// Support the conversion to a 3d matrix. +pub trait ToMatrix { + /// Check if it is a 3d transform function. + fn is_3d(&self) -> bool; + + /// Return the equivalent 3d matrix. + fn to_3d_matrix( + &self, + reference_box: Option<&Rect<ComputedLength>>, + ) -> Result<Transform3D<f64>, ()>; +} + +/// A little helper to deal with both specified and computed angles. +pub trait ToRadians { + /// Return the radians value as a 64-bit floating point value. + fn radians64(&self) -> f64; +} + +impl ToRadians for computed::angle::Angle { + #[inline] + fn radians64(&self) -> f64 { + computed::angle::Angle::radians64(self) + } +} + +impl ToRadians for SpecifiedAngle { + #[inline] + fn radians64(&self) -> f64 { + computed::angle::Angle::from_degrees(self.degrees()).radians64() + } +} + +impl<Angle, Number, Length, Integer, LoP> ToMatrix + for TransformOperation<Angle, Number, Length, Integer, LoP> +where + Angle: Zero + ToRadians + Copy, + Number: PartialEq + Copy + Into<f32> + Into<f64>, + Length: ToAbsoluteLength, + LoP: Zero + ToAbsoluteLength + ZeroNoPercent, +{ + #[inline] + fn is_3d(&self) -> bool { + use self::TransformOperation::*; + match *self { + Translate3D(..) | TranslateZ(..) | Rotate3D(..) | RotateX(..) | RotateY(..) | + RotateZ(..) | Scale3D(..) | ScaleZ(..) | Perspective(..) | Matrix3D(..) => true, + _ => false, + } + } + + /// If |reference_box| is None, we will drop the percent part from translate because + /// we cannot resolve it without the layout info, for computed TransformOperation. + /// However, for specified TransformOperation, we will return Err(()) if there is any relative + /// lengths because the only caller, DOMMatrix, doesn't accept relative lengths. + #[inline] + fn to_3d_matrix( + &self, + reference_box: Option<&Rect<ComputedLength>>, + ) -> Result<Transform3D<f64>, ()> { + use self::TransformOperation::*; + + let reference_width = reference_box.map(|v| v.size.width); + let reference_height = reference_box.map(|v| v.size.height); + let matrix = match *self { + Rotate3D(ax, ay, az, theta) => { + let theta = theta.radians64(); + let (ax, ay, az, theta) = + get_normalized_vector_and_angle(ax.into(), ay.into(), az.into(), theta); + Transform3D::rotation( + ax as f64, + ay as f64, + az as f64, + euclid::Angle::radians(theta), + ) + }, + RotateX(theta) => { + let theta = euclid::Angle::radians(theta.radians64()); + Transform3D::rotation(1., 0., 0., theta) + }, + RotateY(theta) => { + let theta = euclid::Angle::radians(theta.radians64()); + Transform3D::rotation(0., 1., 0., theta) + }, + RotateZ(theta) | Rotate(theta) => { + let theta = euclid::Angle::radians(theta.radians64()); + Transform3D::rotation(0., 0., 1., theta) + }, + Perspective(ref p) => { + let px = match p { + PerspectiveFunction::None => f32::INFINITY, + PerspectiveFunction::Length(ref p) => p.to_pixel_length(None)?, + }; + create_perspective_matrix(px).cast() + }, + Scale3D(sx, sy, sz) => Transform3D::scale(sx.into(), sy.into(), sz.into()), + Scale(sx, sy) => Transform3D::scale(sx.into(), sy.into(), 1.), + ScaleX(s) => Transform3D::scale(s.into(), 1., 1.), + ScaleY(s) => Transform3D::scale(1., s.into(), 1.), + ScaleZ(s) => Transform3D::scale(1., 1., s.into()), + Translate3D(ref tx, ref ty, ref tz) => { + let tx = tx.to_pixel_length(reference_width)? as f64; + let ty = ty.to_pixel_length(reference_height)? as f64; + Transform3D::translation(tx, ty, tz.to_pixel_length(None)? as f64) + }, + Translate(ref tx, ref ty) => { + let tx = tx.to_pixel_length(reference_width)? as f64; + let ty = ty.to_pixel_length(reference_height)? as f64; + Transform3D::translation(tx, ty, 0.) + }, + TranslateX(ref t) => { + let t = t.to_pixel_length(reference_width)? as f64; + Transform3D::translation(t, 0., 0.) + }, + TranslateY(ref t) => { + let t = t.to_pixel_length(reference_height)? as f64; + Transform3D::translation(0., t, 0.) + }, + TranslateZ(ref z) => Transform3D::translation(0., 0., z.to_pixel_length(None)? as f64), + Skew(theta_x, theta_y) => Transform3D::skew( + euclid::Angle::radians(theta_x.radians64()), + euclid::Angle::radians(theta_y.radians64()), + ), + SkewX(theta) => Transform3D::skew( + euclid::Angle::radians(theta.radians64()), + euclid::Angle::radians(0.), + ), + SkewY(theta) => Transform3D::skew( + euclid::Angle::radians(0.), + euclid::Angle::radians(theta.radians64()), + ), + Matrix3D(m) => m.into(), + Matrix(m) => m.into(), + InterpolateMatrix { .. } | AccumulateMatrix { .. } => { + // TODO: Convert InterpolateMatrix/AccumulateMatrix into a valid Transform3D by + // the reference box and do interpolation on these two Transform3D matrices. + // Both Gecko and Servo don't support this for computing distance, and Servo + // doesn't support animations on InterpolateMatrix/AccumulateMatrix, so + // return an identity matrix. + // Note: DOMMatrix doesn't go into this arm. + Transform3D::identity() + }, + }; + Ok(matrix) + } +} + +impl<T> Transform<T> { + /// `none` + pub fn none() -> Self { + Transform(Default::default()) + } +} + +impl<T: ToMatrix> Transform<T> { + /// Return the equivalent 3d matrix of this transform list. + /// + /// We return a pair: the first one is the transform matrix, and the second one + /// indicates if there is any 3d transform function in this transform list. + #[cfg_attr(rustfmt, rustfmt_skip)] + pub fn to_transform_3d_matrix( + &self, + reference_box: Option<&Rect<ComputedLength>> + ) -> Result<(Transform3D<CSSFloat>, bool), ()> { + Self::components_to_transform_3d_matrix(&self.0, reference_box) + } + + /// Converts a series of components to a 3d matrix. + #[cfg_attr(rustfmt, rustfmt_skip)] + pub fn components_to_transform_3d_matrix( + ops: &[T], + reference_box: Option<&Rect<ComputedLength>>, + ) -> Result<(Transform3D<CSSFloat>, bool), ()> { + let cast_3d_transform = |m: Transform3D<f64>| -> Transform3D<CSSFloat> { + use std::{f32, f64}; + let cast = |v: f64| v.min(f32::MAX as f64).max(f32::MIN as f64) as f32; + Transform3D::new( + cast(m.m11), cast(m.m12), cast(m.m13), cast(m.m14), + cast(m.m21), cast(m.m22), cast(m.m23), cast(m.m24), + cast(m.m31), cast(m.m32), cast(m.m33), cast(m.m34), + cast(m.m41), cast(m.m42), cast(m.m43), cast(m.m44), + ) + }; + + let (m, is_3d) = Self::components_to_transform_3d_matrix_f64(ops, reference_box)?; + Ok((cast_3d_transform(m), is_3d)) + } + + /// Same as Transform::to_transform_3d_matrix but a f64 version. + fn components_to_transform_3d_matrix_f64( + ops: &[T], + reference_box: Option<&Rect<ComputedLength>>, + ) -> Result<(Transform3D<f64>, bool), ()> { + // We intentionally use Transform3D<f64> during computation to avoid + // error propagation because using f32 to compute triangle functions + // (e.g. in rotation()) is not accurate enough. In Gecko, we also use + // "double" to compute the triangle functions. Therefore, let's use + // Transform3D<f64> during matrix computation and cast it into f32 in + // the end. + let mut transform = Transform3D::<f64>::identity(); + let mut contain_3d = false; + + for operation in ops { + let matrix = operation.to_3d_matrix(reference_box)?; + contain_3d = contain_3d || operation.is_3d(); + transform = matrix.then(&transform); + } + + Ok((transform, contain_3d)) + } +} + +/// Return the transform matrix from a perspective length. +#[inline] +pub fn create_perspective_matrix(d: CSSFloat) -> Transform3D<CSSFloat> { + if d.is_finite() { + Transform3D::perspective(d.max(1.)) + } else { + Transform3D::identity() + } +} + +/// Return the normalized direction vector and its angle for Rotate3D. +pub fn get_normalized_vector_and_angle<T: Zero>( + x: CSSFloat, + y: CSSFloat, + z: CSSFloat, + angle: T, +) -> (CSSFloat, CSSFloat, CSSFloat, T) { + use crate::values::computed::transform::DirectionVector; + use euclid::approxeq::ApproxEq; + let vector = DirectionVector::new(x, y, z); + if vector.square_length().approx_eq(&f32::zero()) { + // https://www.w3.org/TR/css-transforms-1/#funcdef-rotate3d + // A direction vector that cannot be normalized, such as [0, 0, 0], will cause the + // rotation to not be applied, so we use identity matrix (i.e. rotate3d(0, 0, 1, 0)). + (0., 0., 1., T::zero()) + } else { + let vector = vector.robust_normalize(); + (vector.x, vector.y, vector.z, angle) + } +} + +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// A value of the `Rotate` property +/// +/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms> +pub enum GenericRotate<Number, Angle> { + /// 'none' + None, + /// '<angle>' + Rotate(Angle), + /// '<number>{3} <angle>' + Rotate3D(Number, Number, Number, Angle), +} + +pub use self::GenericRotate as Rotate; + +/// A trait to check if the current 3D vector is parallel to the DirectionVector. +/// This is especially for serialization on Rotate. +pub trait IsParallelTo { + /// Returns true if this is parallel to the vector. + fn is_parallel_to(&self, vector: &computed::transform::DirectionVector) -> bool; +} + +impl<Number, Angle> ToCss for Rotate<Number, Angle> +where + Number: Copy + ToCss + Zero, + Angle: ToCss, + (Number, Number, Number): IsParallelTo, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + use crate::values::computed::transform::DirectionVector; + match *self { + Rotate::None => dest.write_str("none"), + Rotate::Rotate(ref angle) => angle.to_css(dest), + Rotate::Rotate3D(x, y, z, ref angle) => { + // If the axis is parallel with the x or y axes, it must serialize as the + // appropriate keyword. If a rotation about the z axis (that is, in 2D) is + // specified, the property must serialize as just an <angle> + // + // https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization + let v = (x, y, z); + let axis = if x.is_zero() && y.is_zero() && z.is_zero() { + // The zero length vector is parallel to every other vector, so + // is_parallel_to() returns true for it. However, it is definitely different + // from x axis, y axis, or z axis, and it's meaningless to perform a rotation + // using that direction vector. So we *have* to serialize it using that same + // vector - we can't simplify to some theoretically parallel axis-aligned + // vector. + None + } else if v.is_parallel_to(&DirectionVector::new(1., 0., 0.)) { + Some("x ") + } else if v.is_parallel_to(&DirectionVector::new(0., 1., 0.)) { + Some("y ") + } else if v.is_parallel_to(&DirectionVector::new(0., 0., 1.)) { + // When we're parallel to the z-axis, we can just serialize the angle. + return angle.to_css(dest); + } else { + None + }; + match axis { + Some(a) => dest.write_str(a)?, + None => { + x.to_css(dest)?; + dest.write_char(' ')?; + y.to_css(dest)?; + dest.write_char(' ')?; + z.to_css(dest)?; + dest.write_char(' ')?; + }, + } + angle.to_css(dest) + }, + } + } +} + +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// A value of the `Scale` property +/// +/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms> +pub enum GenericScale<Number> { + /// 'none' + None, + /// '<number>{1,3}' + Scale(Number, Number, Number), +} + +pub use self::GenericScale as Scale; + +impl<Number> ToCss for Scale<Number> +where + Number: ToCss + PartialEq + Copy, + f32: From<Number>, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + f32: From<Number>, + { + match *self { + Scale::None => dest.write_str("none"), + Scale::Scale(ref x, ref y, ref z) => { + x.to_css(dest)?; + + let is_3d = f32::from(*z) != 1.0; + if is_3d || x != y { + dest.write_char(' ')?; + y.to_css(dest)?; + } + + if is_3d { + dest.write_char(' ')?; + z.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +#[inline] +fn y_axis_and_z_axis_are_zero<LengthPercentage: Zero + ZeroNoPercent, Length: Zero>( + _: &LengthPercentage, + y: &LengthPercentage, + z: &Length, +) -> bool { + y.is_zero_no_percent() && z.is_zero() +} + +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// A value of the `translate` property +/// +/// https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization: +/// +/// If a 2d translation is specified, the property must serialize with only one +/// or two values (per usual, if the second value is 0px, the default, it must +/// be omitted when serializing; however if 0% is the second value, it is included). +/// +/// If a 3d translation is specified and the value can be expressed as 2d, we treat as 2d and +/// serialize accoringly. Otherwise, we serialize all three values. +/// https://github.com/w3c/csswg-drafts/issues/3305 +/// +/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms> +pub enum GenericTranslate<LengthPercentage, Length> +where + LengthPercentage: Zero + ZeroNoPercent, + Length: Zero, +{ + /// 'none' + None, + /// <length-percentage> [ <length-percentage> <length>? ]? + Translate( + LengthPercentage, + #[css(contextual_skip_if = "y_axis_and_z_axis_are_zero")] LengthPercentage, + #[css(skip_if = "Zero::is_zero")] Length, + ), +} + +pub use self::GenericTranslate as Translate; + +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TransformStyle { + Flat, + #[css(keyword = "preserve-3d")] + Preserve3d, +} diff --git a/servo/components/style/values/generics/ui.rs b/servo/components/style/values/generics/ui.rs new file mode 100644 index 0000000000..87c8674182 --- /dev/null +++ b/servo/components/style/values/generics/ui.rs @@ -0,0 +1,129 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic values for UI properties. + +use crate::values::specified::ui::CursorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A generic value for the `cursor` property. +/// +/// https://drafts.csswg.org/css-ui/#cursor +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericCursor<Image> { + /// The parsed images for the cursor. + pub images: crate::OwnedSlice<Image>, + /// The kind of the cursor [default | help | ...]. + pub keyword: CursorKind, +} + +pub use self::GenericCursor as Cursor; + +impl<Image> Cursor<Image> { + /// Set `cursor` to `auto` + #[inline] + pub fn auto() -> Self { + Self { + images: Default::default(), + keyword: CursorKind::Auto, + } + } +} + +impl<Image: ToCss> ToCss for Cursor<Image> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + for image in &*self.images { + image.to_css(dest)?; + dest.write_str(", ")?; + } + self.keyword.to_css(dest) + } +} + +/// A generic value for item of `image cursors`. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] +#[repr(C)] +pub struct GenericCursorImage<Image, Number> { + /// The url to parse images from. + pub image: Image, + /// Whether the image has a hotspot or not. + pub has_hotspot: bool, + /// The x coordinate. + pub hotspot_x: Number, + /// The y coordinate. + pub hotspot_y: Number, +} + +pub use self::GenericCursorImage as CursorImage; + +impl<Image: ToCss, Number: ToCss> ToCss for CursorImage<Image, Number> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.image.to_css(dest)?; + if self.has_hotspot { + dest.write_char(' ')?; + self.hotspot_x.to_css(dest)?; + dest.write_char(' ')?; + self.hotspot_y.to_css(dest)?; + } + Ok(()) + } +} + +/// A generic value for `scrollbar-color` property. +/// +/// https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericScrollbarColor<Color> { + /// `auto` + Auto, + /// `<color>{2}` + Colors { + /// First `<color>`, for color of the scrollbar thumb. + thumb: Color, + /// Second `<color>`, for color of the scrollbar track. + track: Color, + }, +} + +pub use self::GenericScrollbarColor as ScrollbarColor; + +impl<Color> Default for ScrollbarColor<Color> { + #[inline] + fn default() -> Self { + ScrollbarColor::Auto + } +} diff --git a/servo/components/style/values/generics/url.rs b/servo/components/style/values/generics/url.rs new file mode 100644 index 0000000000..46ed453e82 --- /dev/null +++ b/servo/components/style/values/generics/url.rs @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Generic types for url properties. + +/// An image url or none, used for example in list-style-image +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericUrlOrNone<U> { + /// `none` + None, + /// A URL. + Url(U), +} + +pub use self::GenericUrlOrNone as UrlOrNone; + +impl<Url> UrlOrNone<Url> { + /// Initial "none" value for properties such as `list-style-image` + pub fn none() -> Self { + UrlOrNone::None + } + + /// Returns whether the value is `none`. + pub fn is_none(&self) -> bool { + match *self { + UrlOrNone::None => true, + UrlOrNone::Url(..) => false, + } + } +} diff --git a/servo/components/style/values/mod.rs b/servo/components/style/values/mod.rs new file mode 100644 index 0000000000..6138d5a2ab --- /dev/null +++ b/servo/components/style/values/mod.rs @@ -0,0 +1,796 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common [values][values] used in CSS. +//! +//! [values]: https://drafts.csswg.org/css-values/ + +#![deny(missing_docs)] + +use crate::parser::{Parse, ParserContext}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::Atom; +pub use cssparser::{serialize_identifier, serialize_name, CowRcStr, Parser}; +pub use cssparser::{SourceLocation, Token}; +use precomputed_hash::PrecomputedHash; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Debug, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use to_shmem::impl_trivial_to_shmem; + +#[cfg(feature = "gecko")] +pub use crate::gecko::url::CssUrl; +#[cfg(feature = "servo")] +pub use crate::servo::url::CssUrl; + +pub mod animated; +pub mod computed; +pub mod distance; +pub mod generics; +pub mod resolved; +pub mod specified; + +/// A CSS float value. +pub type CSSFloat = f32; + +/// Normalizes a float value to zero after a set of operations that might turn +/// it into NaN. +#[inline] +pub fn normalize(v: CSSFloat) -> CSSFloat { + if v.is_nan() { + 0.0 + } else { + v + } +} + +/// A CSS integer value. +pub type CSSInteger = i32; + +/// Serialize an identifier which is represented as an atom. +#[cfg(feature = "gecko")] +pub fn serialize_atom_identifier<W>(ident: &Atom, dest: &mut W) -> fmt::Result +where + W: Write, +{ + ident.with_str(|s| serialize_identifier(s, dest)) +} + +/// Serialize an identifier which is represented as an atom. +#[cfg(feature = "servo")] +pub fn serialize_atom_identifier<Static, W>( + ident: &::string_cache::Atom<Static>, + dest: &mut W, +) -> fmt::Result +where + Static: string_cache::StaticAtomSet, + W: Write, +{ + serialize_identifier(&ident, dest) +} + +/// Serialize a name which is represented as an Atom. +#[cfg(feature = "gecko")] +pub fn serialize_atom_name<W>(ident: &Atom, dest: &mut W) -> fmt::Result +where + W: Write, +{ + ident.with_str(|s| serialize_name(s, dest)) +} + +/// Serialize a name which is represented as an Atom. +#[cfg(feature = "servo")] +pub fn serialize_atom_name<Static, W>( + ident: &::string_cache::Atom<Static>, + dest: &mut W, +) -> fmt::Result +where + Static: string_cache::StaticAtomSet, + W: Write, +{ + serialize_name(&ident, dest) +} + +/// Serialize a number with calc, and NaN/infinity handling (if enabled) +pub fn serialize_number<W>(v: f32, was_calc: bool, dest: &mut CssWriter<W>) -> fmt::Result +where + W: Write, +{ + serialize_specified_dimension(v, "", was_calc, dest) +} + +/// Serialize a specified dimension with unit, calc, and NaN/infinity handling (if enabled) +pub fn serialize_specified_dimension<W>( + v: f32, + unit: &str, + was_calc: bool, + dest: &mut CssWriter<W>, +) -> fmt::Result +where + W: Write, +{ + if was_calc { + dest.write_str("calc(")?; + } + + if !v.is_finite() { + // https://drafts.csswg.org/css-values/#calc-error-constants: + // "While not technically numbers, these keywords act as numeric values, + // similar to e and pi. Thus to get an infinite length, for example, + // requires an expression like calc(infinity * 1px)." + + if v.is_nan() { + dest.write_str("NaN")?; + } else if v == f32::INFINITY { + dest.write_str("infinity")?; + } else if v == f32::NEG_INFINITY { + dest.write_str("-infinity")?; + } + + if !unit.is_empty() { + dest.write_str(" * 1")?; + } + } else { + v.to_css(dest)?; + } + + dest.write_str(unit)?; + + if was_calc { + dest.write_char(')')?; + } + Ok(()) +} + +/// A CSS string stored as an `Atom`. +#[repr(transparent)] +#[derive( + Clone, + Debug, + Default, + Deref, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct AtomString(pub Atom); + +#[cfg(feature = "servo")] +impl AsRef<str> for AtomString { + fn as_ref(&self) -> &str { + &*self.0 + } +} + +impl cssparser::ToCss for AtomString { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: Write, + { + // Wrap in quotes to form a string literal + dest.write_char('"')?; + #[cfg(feature = "servo")] + { + cssparser::CssStringWriter::new(dest).write_str(self.as_ref())?; + } + #[cfg(feature = "gecko")] + { + self.0 + .with_str(|s| cssparser::CssStringWriter::new(dest).write_str(s))?; + } + dest.write_char('"') + } +} + +impl style_traits::ToCss for AtomString { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + cssparser::ToCss::to_css(self, dest) + } +} + +impl PrecomputedHash for AtomString { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.0.precomputed_hash() + } +} + +impl<'a> From<&'a str> for AtomString { + #[inline] + fn from(string: &str) -> Self { + Self(Atom::from(string)) + } +} + +/// A generic CSS `<ident>` stored as an `Atom`. +#[cfg(feature = "servo")] +#[repr(transparent)] +#[derive(Deref)] +pub struct GenericAtomIdent<Set>(pub string_cache::Atom<Set>) +where + Set: string_cache::StaticAtomSet; + +/// A generic CSS `<ident>` stored as an `Atom`, for the default atom set. +#[cfg(feature = "servo")] +pub type AtomIdent = GenericAtomIdent<servo_atoms::AtomStaticSet>; + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> style_traits::SpecifiedValueInfo for GenericAtomIdent<Set> {} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> Default for GenericAtomIdent<Set> { + fn default() -> Self { + Self(string_cache::Atom::default()) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> std::fmt::Debug for GenericAtomIdent<Set> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + self.0.fmt(f) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> std::hash::Hash for GenericAtomIdent<Set> { + fn hash<H: std::hash::Hasher>(&self, state: &mut H) { + self.0.hash(state) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> Eq for GenericAtomIdent<Set> {} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> PartialEq for GenericAtomIdent<Set> { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> Clone for GenericAtomIdent<Set> { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> to_shmem::ToShmem for GenericAtomIdent<Set> { + fn to_shmem(&self, builder: &mut to_shmem::SharedMemoryBuilder) -> to_shmem::Result<Self> { + use std::mem::ManuallyDrop; + + let atom = self.0.to_shmem(builder)?; + Ok(ManuallyDrop::new(Self(ManuallyDrop::into_inner(atom)))) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> malloc_size_of::MallocSizeOf for GenericAtomIdent<Set> { + fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize { + self.0.size_of(ops) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> cssparser::ToCss for GenericAtomIdent<Set> { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> PrecomputedHash for GenericAtomIdent<Set> { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.0.precomputed_hash() + } +} + +#[cfg(feature = "servo")] +impl<'a, Set: string_cache::StaticAtomSet> From<&'a str> for GenericAtomIdent<Set> { + #[inline] + fn from(string: &str) -> Self { + Self(string_cache::Atom::from(string)) + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> std::borrow::Borrow<string_cache::Atom<Set>> + for GenericAtomIdent<Set> +{ + #[inline] + fn borrow(&self) -> &string_cache::Atom<Set> { + &self.0 + } +} + +#[cfg(feature = "servo")] +impl<Set: string_cache::StaticAtomSet> GenericAtomIdent<Set> { + /// Constructs a new GenericAtomIdent. + #[inline] + pub fn new(atom: string_cache::Atom<Set>) -> Self { + Self(atom) + } + + /// Cast an atom ref to an AtomIdent ref. + #[inline] + pub fn cast<'a>(atom: &'a string_cache::Atom<Set>) -> &'a Self { + let ptr = atom as *const _ as *const Self; + // safety: repr(transparent) + unsafe { &*ptr } + } +} + +/// A CSS `<ident>` stored as an `Atom`. +#[cfg(feature = "gecko")] +#[repr(transparent)] +#[derive( + Clone, Debug, Default, Deref, Eq, Hash, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem, +)] +pub struct AtomIdent(pub Atom); + +#[cfg(feature = "gecko")] +impl cssparser::ToCss for AtomIdent { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +#[cfg(feature = "gecko")] +impl style_traits::ToCss for AtomIdent { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + cssparser::ToCss::to_css(self, dest) + } +} + +#[cfg(feature = "gecko")] +impl PrecomputedHash for AtomIdent { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.0.precomputed_hash() + } +} + +#[cfg(feature = "gecko")] +impl<'a> From<&'a str> for AtomIdent { + #[inline] + fn from(string: &str) -> Self { + Self(Atom::from(string)) + } +} + +#[cfg(feature = "gecko")] +impl AtomIdent { + /// Constructs a new AtomIdent. + #[inline] + pub fn new(atom: Atom) -> Self { + Self(atom) + } + + /// Like `Atom::with` but for `AtomIdent`. + pub unsafe fn with<F, R>(ptr: *const crate::gecko_bindings::structs::nsAtom, callback: F) -> R + where + F: FnOnce(&Self) -> R, + { + Atom::with(ptr, |atom: &Atom| { + // safety: repr(transparent) + let atom = atom as *const Atom as *const AtomIdent; + callback(&*atom) + }) + } + + /// Cast an atom ref to an AtomIdent ref. + #[inline] + pub fn cast<'a>(atom: &'a Atom) -> &'a Self { + let ptr = atom as *const _ as *const Self; + // safety: repr(transparent) + unsafe { &*ptr } + } +} + +#[cfg(feature = "gecko")] +impl std::borrow::Borrow<crate::gecko_string_cache::WeakAtom> for AtomIdent { + #[inline] + fn borrow(&self) -> &crate::gecko_string_cache::WeakAtom { + self.0.borrow() + } +} + +/// Serialize a value into percentage. +pub fn serialize_percentage<W>(value: CSSFloat, dest: &mut CssWriter<W>) -> fmt::Result +where + W: Write, +{ + serialize_specified_dimension(value * 100., "%", /* was_calc = */ false, dest) +} + +/// Serialize a value into normalized (no NaN/inf serialization) percentage. +pub fn serialize_normalized_percentage<W>(value: CSSFloat, dest: &mut CssWriter<W>) -> fmt::Result +where + W: Write, +{ + (value * 100.).to_css(dest)?; + dest.write_char('%') +} + +/// Convenience void type to disable some properties and values through types. +#[cfg_attr(feature = "servo", derive(Deserialize, MallocSizeOf, Serialize))] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, +)] +pub enum Impossible {} + +// FIXME(nox): This should be derived but the derive code cannot cope +// with uninhabited enums. +impl ComputeSquaredDistance for Impossible { + #[inline] + fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> { + match *self {} + } +} + +impl_trivial_to_shmem!(Impossible); + +impl Parse for Impossible { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// A struct representing one of two kinds of values. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum Either<A, B> { + /// The first value. + First(A), + /// The second kind of value. + Second(B), +} + +impl<A: Debug, B: Debug> Debug for Either<A, B> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Either::First(ref v) => v.fmt(f), + Either::Second(ref v) => v.fmt(f), + } + } +} + +/// <https://drafts.csswg.org/css-values-4/#custom-idents> +#[derive( + Clone, + Debug, + Default, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CustomIdent(pub Atom); + +impl CustomIdent { + /// Parse a <custom-ident> + /// + /// TODO(zrhoffman, bug 1844501): Use CustomIdent::parse in more places instead of + /// CustomIdent::from_ident. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + invalid: &[&str], + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + CustomIdent::from_ident(location, ident, invalid) + } + + /// Parse an already-tokenizer identifier + pub fn from_ident<'i>( + location: SourceLocation, + ident: &CowRcStr<'i>, + excluding: &[&str], + ) -> Result<Self, ParseError<'i>> { + if !Self::is_valid(ident, excluding) { + return Err( + location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())) + ); + } + if excluding.iter().any(|s| ident.eq_ignore_ascii_case(s)) { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(CustomIdent(Atom::from(ident.as_ref()))) + } + } + + fn is_valid(ident: &str, excluding: &[&str]) -> bool { + use crate::properties::CSSWideKeyword; + // https://drafts.csswg.org/css-values-4/#custom-idents: + // + // The CSS-wide keywords are not valid <custom-ident>s. The default + // keyword is reserved and is also not a valid <custom-ident>. + if CSSWideKeyword::from_ident(ident).is_ok() || ident.eq_ignore_ascii_case("default") { + return false; + } + + // https://drafts.csswg.org/css-values-4/#custom-idents: + // + // Excluded keywords are excluded in all ASCII case permutations. + !excluding.iter().any(|s| ident.eq_ignore_ascii_case(s)) + } +} + +impl ToCss for CustomIdent { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +/// <https://www.w3.org/TR/css-values-4/#dashed-idents> +/// This is simply an Atom, but will only parse if the identifier starts with "--". +#[repr(transparent)] +#[derive( + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct DashedIdent(pub Atom); + +impl Parse for DashedIdent { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + if ident.starts_with("--") { + Ok(Self(Atom::from(ident.as_ref()))) + } else { + Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + } + } +} + +impl ToCss for DashedIdent { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +/// The <timeline-name> or <keyframes-name>. +/// The definition of these two names are the same, so we use the same type for them. +/// +/// <https://drafts.csswg.org/css-animations-2/#typedef-timeline-name> +/// <https://drafts.csswg.org/css-animations/#typedef-keyframes-name> +/// +/// We use a single atom for these. Empty atom represents `none` animation. +#[repr(transparent)] +#[derive( + Clone, + Debug, + Hash, + PartialEq, + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct TimelineOrKeyframesName(Atom); + +impl TimelineOrKeyframesName { + /// <https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name> + pub fn from_ident(value: &str) -> Self { + Self(Atom::from(value)) + } + + /// Returns the `none` value. + pub fn none() -> Self { + Self(atom!("")) + } + + /// Returns whether this is the special `none` value. + pub fn is_none(&self) -> bool { + self.0 == atom!("") + } + + /// Create a new TimelineOrKeyframesName from Atom. + #[cfg(feature = "gecko")] + pub fn from_atom(atom: Atom) -> Self { + Self(atom) + } + + /// The name as an Atom + pub fn as_atom(&self) -> &Atom { + &self.0 + } + + fn parse<'i, 't>(input: &mut Parser<'i, 't>, invalid: &[&str]) -> Result<Self, ParseError<'i>> { + debug_assert!(invalid.contains(&"none")); + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::Ident(ref s) => Self(CustomIdent::from_ident(location, s, invalid)?.0), + Token::QuotedString(ref s) => Self(Atom::from(s.as_ref())), + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } + + fn to_css<W>(&self, dest: &mut CssWriter<W>, invalid: &[&str]) -> fmt::Result + where + W: Write, + { + debug_assert!(invalid.contains(&"none")); + + if self.0 == atom!("") { + return dest.write_str("none"); + } + + self.0.with_str(|s| { + if CustomIdent::is_valid(s, invalid) { + serialize_identifier(s, dest) + } else { + s.to_css(dest) + } + }) + } +} + +impl Eq for TimelineOrKeyframesName {} + +/// The typedef of <timeline-name>. +#[repr(transparent)] +#[derive( + Clone, + Debug, + Deref, + Hash, + Eq, + PartialEq, + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct TimelineName(TimelineOrKeyframesName); + +impl TimelineName { + /// Create a new TimelineName from Atom. + #[cfg(feature = "gecko")] + pub fn from_atom(atom: Atom) -> Self { + Self(TimelineOrKeyframesName::from_atom(atom)) + } + + /// Returns the `none` value. + pub fn none() -> Self { + Self(TimelineOrKeyframesName::none()) + } +} + +impl Parse for TimelineName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self(TimelineOrKeyframesName::parse( + input, + &["none", "auto"], + )?)) + } +} + +impl ToCss for TimelineName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest, &["none", "auto"]) + } +} + +/// The typedef of <keyframes-name>. +#[repr(transparent)] +#[derive( + Clone, + Debug, + Deref, + Hash, + Eq, + PartialEq, + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct KeyframesName(TimelineOrKeyframesName); + +impl KeyframesName { + /// Create a new KeyframesName from Atom. + #[cfg(feature = "gecko")] + pub fn from_atom(atom: Atom) -> Self { + Self(TimelineOrKeyframesName::from_atom(atom)) + } + + /// Returns the `none` value. + pub fn none() -> Self { + Self(TimelineOrKeyframesName::none()) + } +} + +impl Parse for KeyframesName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self(TimelineOrKeyframesName::parse(input, &["none"])?)) + } +} + +impl ToCss for KeyframesName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest, &["none"]) + } +} diff --git a/servo/components/style/values/resolved/color.rs b/servo/components/style/values/resolved/color.rs new file mode 100644 index 0000000000..79dfd8685f --- /dev/null +++ b/servo/components/style/values/resolved/color.rs @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Resolved color values. + +use super::{Context, ToResolvedValue}; + +use crate::color::AbsoluteColor; +use crate::values::computed::color as computed; +use crate::values::generics::color as generics; + +impl ToResolvedValue for computed::Color { + // A resolved color value is an rgba color, with currentcolor resolved. + type ResolvedValue = AbsoluteColor; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + context.style.resolve_color(self) + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + generics::Color::Absolute(resolved) + } +} + +impl ToResolvedValue for computed::CaretColor { + // A resolved caret-color value is an rgba color, with auto resolving to + // currentcolor. + type ResolvedValue = AbsoluteColor; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + let color = match self.0 { + generics::ColorOrAuto::Color(color) => color, + generics::ColorOrAuto::Auto => generics::Color::currentcolor(), + }; + color.to_resolved_value(context) + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + generics::CaretColor(generics::ColorOrAuto::Color( + computed::Color::from_resolved_value(resolved), + )) + } +} diff --git a/servo/components/style/values/resolved/counters.rs b/servo/components/style/values/resolved/counters.rs new file mode 100644 index 0000000000..c1332449ad --- /dev/null +++ b/servo/components/style/values/resolved/counters.rs @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Resolved values for counter properties + +use super::{Context, ToResolvedValue}; +use crate::values::computed; + +/// https://drafts.csswg.org/css-content/#content-property +/// +/// We implement this at resolved value time because otherwise it causes us to +/// allocate a bunch of useless initial structs for ::before / ::after, which is +/// a bit unfortunate. +/// +/// Though these should be temporary, mostly, so if this causes complexity in +/// other places, it should be fine to move to `StyleAdjuster`. +/// +/// See https://github.com/w3c/csswg-drafts/issues/4632 for where some related +/// issues are being discussed. +impl ToResolvedValue for computed::Content { + type ResolvedValue = Self; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self { + let (is_pseudo, is_before_or_after, is_marker) = match context.style.pseudo() { + Some(ref pseudo) => (true, pseudo.is_before_or_after(), pseudo.is_marker()), + None => (false, false, false), + }; + match self { + Self::Normal if is_before_or_after => Self::None, + // For now, make `content: none` compute to `normal` for pseudos + // other than ::before, ::after and ::marker, as we don't respect it. + // https://github.com/w3c/csswg-drafts/issues/6124 + // Ditto for non-pseudo elements if the pref is disabled. + Self::None + if (is_pseudo && !is_before_or_after && !is_marker) || + (!is_pseudo && + !static_prefs::pref!("layout.css.element-content-none.enabled")) => + { + Self::Normal + }, + other => other, + } + } + + #[inline] + fn from_resolved_value(resolved: Self) -> Self { + resolved + } +} diff --git a/servo/components/style/values/resolved/mod.rs b/servo/components/style/values/resolved/mod.rs new file mode 100644 index 0000000000..675f3cca68 --- /dev/null +++ b/servo/components/style/values/resolved/mod.rs @@ -0,0 +1,275 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Resolved values. These are almost always computed values, but in some cases +//! there are used values. + +use crate::media_queries::Device; +use crate::properties::ComputedValues; +use crate::ArcSlice; +use servo_arc::Arc; +use smallvec::SmallVec; + +mod color; +mod counters; + +use crate::values::computed; + +/// Element-specific information needed to resolve property values. +pub struct ResolvedElementInfo<'a> { + /// Element we're resolving line-height against. + #[cfg(feature = "gecko")] + pub element: crate::gecko::wrapper::GeckoElement<'a>, +} + +/// Information needed to resolve a given value. +pub struct Context<'a> { + /// The style we're resolving for. This is useful to resolve currentColor. + pub style: &'a ComputedValues, + /// The device / document we're resolving style for. Useful to do font metrics stuff needed for + /// line-height. + pub device: &'a Device, + /// The element-specific information to resolve the value. + pub element_info: ResolvedElementInfo<'a>, +} + +/// A trait to represent the conversion between resolved and resolved values. +/// +/// This trait is derivable with `#[derive(ToResolvedValue)]`. +/// +/// The deriving code assumes that if the type isn't generic, then the trait can +/// be implemented as simple move. This means that a manual implementation with +/// `ResolvedValue = Self` is bogus if it returns anything else than a clone. +pub trait ToResolvedValue { + /// The resolved value type we're going to be converted to. + type ResolvedValue; + + /// Convert a resolved value to a resolved value. + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue; + + /// Convert a resolved value to resolved value form. + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self; +} + +macro_rules! trivial_to_resolved_value { + ($ty:ty) => { + impl $crate::values::resolved::ToResolvedValue for $ty { + type ResolvedValue = Self; + + #[inline] + fn to_resolved_value(self, _: &Context) -> Self { + self + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + resolved + } + } + }; +} + +trivial_to_resolved_value!(()); +trivial_to_resolved_value!(bool); +trivial_to_resolved_value!(f32); +trivial_to_resolved_value!(u8); +trivial_to_resolved_value!(i8); +trivial_to_resolved_value!(u16); +trivial_to_resolved_value!(i16); +trivial_to_resolved_value!(u32); +trivial_to_resolved_value!(i32); +trivial_to_resolved_value!(usize); +trivial_to_resolved_value!(String); +trivial_to_resolved_value!(Box<str>); +trivial_to_resolved_value!(crate::OwnedStr); +trivial_to_resolved_value!(crate::color::AbsoluteColor); +trivial_to_resolved_value!(crate::values::generics::color::ColorMixFlags); +trivial_to_resolved_value!(crate::Atom); +trivial_to_resolved_value!(crate::values::AtomIdent); +trivial_to_resolved_value!(app_units::Au); +trivial_to_resolved_value!(computed::url::ComputedUrl); +#[cfg(feature = "gecko")] +trivial_to_resolved_value!(computed::url::ComputedImageUrl); +#[cfg(feature = "servo")] +trivial_to_resolved_value!(crate::Namespace); +#[cfg(feature = "servo")] +trivial_to_resolved_value!(crate::Prefix); +trivial_to_resolved_value!(computed::LengthPercentage); +trivial_to_resolved_value!(style_traits::values::specified::AllowedNumericType); +trivial_to_resolved_value!(computed::TimingFunction); + +impl<A, B> ToResolvedValue for (A, B) +where + A: ToResolvedValue, + B: ToResolvedValue, +{ + type ResolvedValue = ( + <A as ToResolvedValue>::ResolvedValue, + <B as ToResolvedValue>::ResolvedValue, + ); + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + ( + self.0.to_resolved_value(context), + self.1.to_resolved_value(context), + ) + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + ( + A::from_resolved_value(resolved.0), + B::from_resolved_value(resolved.1), + ) + } +} + +impl<T> ToResolvedValue for Option<T> +where + T: ToResolvedValue, +{ + type ResolvedValue = Option<<T as ToResolvedValue>::ResolvedValue>; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + self.map(|item| item.to_resolved_value(context)) + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + resolved.map(T::from_resolved_value) + } +} + +impl<T> ToResolvedValue for SmallVec<[T; 1]> +where + T: ToResolvedValue, +{ + type ResolvedValue = SmallVec<[<T as ToResolvedValue>::ResolvedValue; 1]>; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + self.into_iter() + .map(|item| item.to_resolved_value(context)) + .collect() + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + resolved.into_iter().map(T::from_resolved_value).collect() + } +} + +impl<T> ToResolvedValue for Vec<T> +where + T: ToResolvedValue, +{ + type ResolvedValue = Vec<<T as ToResolvedValue>::ResolvedValue>; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + self.into_iter() + .map(|item| item.to_resolved_value(context)) + .collect() + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + resolved.into_iter().map(T::from_resolved_value).collect() + } +} + +impl<T> ToResolvedValue for Box<T> +where + T: ToResolvedValue, +{ + type ResolvedValue = Box<<T as ToResolvedValue>::ResolvedValue>; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + Box::new(T::to_resolved_value(*self, context)) + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + Box::new(T::from_resolved_value(*resolved)) + } +} + +impl<T> ToResolvedValue for Box<[T]> +where + T: ToResolvedValue, +{ + type ResolvedValue = Box<[<T as ToResolvedValue>::ResolvedValue]>; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + Vec::from(self) + .to_resolved_value(context) + .into_boxed_slice() + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + Vec::from_resolved_value(Vec::from(resolved)).into_boxed_slice() + } +} + +impl<T> ToResolvedValue for crate::OwnedSlice<T> +where + T: ToResolvedValue, +{ + type ResolvedValue = crate::OwnedSlice<<T as ToResolvedValue>::ResolvedValue>; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + self.into_box().to_resolved_value(context).into() + } + + #[inline] + fn from_resolved_value(resolved: Self::ResolvedValue) -> Self { + Self::from(Box::from_resolved_value(resolved.into_box())) + } +} + +// NOTE(emilio): This is implementable more generically, but it's unlikely what +// you want there, as it forces you to have an extra allocation. +// +// We could do that if needed, ideally with specialization for the case where +// ResolvedValue = T. But we don't need it for now. +impl<T> ToResolvedValue for Arc<T> +where + T: ToResolvedValue<ResolvedValue = T>, +{ + type ResolvedValue = Self; + + #[inline] + fn to_resolved_value(self, _: &Context) -> Self { + self + } + + #[inline] + fn from_resolved_value(resolved: Self) -> Self { + resolved + } +} + +// Same caveat as above applies. +impl<T> ToResolvedValue for ArcSlice<T> +where + T: ToResolvedValue<ResolvedValue = T>, +{ + type ResolvedValue = Self; + + #[inline] + fn to_resolved_value(self, _: &Context) -> Self { + self + } + + #[inline] + fn from_resolved_value(resolved: Self) -> Self { + resolved + } +} diff --git a/servo/components/style/values/specified/align.rs b/servo/components/style/values/specified/align.rs new file mode 100644 index 0000000000..60eca4556b --- /dev/null +++ b/servo/components/style/values/specified/align.rs @@ -0,0 +1,820 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Values for CSS Box Alignment properties +//! +//! https://drafts.csswg.org/css-align/ + +use crate::parser::{Parse, ParserContext}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss}; + +/// Constants shared by multiple CSS Box Alignment properties +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(C)] +pub struct AlignFlags(u8); +bitflags! { + impl AlignFlags: u8 { + // Enumeration stored in the lower 5 bits: + /// {align,justify}-{content,items,self}: 'auto' + const AUTO = 0; + /// 'normal' + const NORMAL = 1; + /// 'start' + const START = 2; + /// 'end' + const END = 3; + /// 'flex-start' + const FLEX_START = 4; + /// 'flex-end' + const FLEX_END = 5; + /// 'center' + const CENTER = 6; + /// 'left' + const LEFT = 7; + /// 'right' + const RIGHT = 8; + /// 'baseline' + const BASELINE = 9; + /// 'last-baseline' + const LAST_BASELINE = 10; + /// 'stretch' + const STRETCH = 11; + /// 'self-start' + const SELF_START = 12; + /// 'self-end' + const SELF_END = 13; + /// 'space-between' + const SPACE_BETWEEN = 14; + /// 'space-around' + const SPACE_AROUND = 15; + /// 'space-evenly' + const SPACE_EVENLY = 16; + + // Additional flags stored in the upper bits: + /// 'legacy' (mutually exclusive w. SAFE & UNSAFE) + const LEGACY = 1 << 5; + /// 'safe' + const SAFE = 1 << 6; + /// 'unsafe' (mutually exclusive w. SAFE) + const UNSAFE = 1 << 7; + + /// Mask for the additional flags above. + const FLAG_BITS = 0b11100000; + } +} + +impl AlignFlags { + /// Returns the enumeration value stored in the lower 5 bits. + #[inline] + fn value(&self) -> Self { + *self & !AlignFlags::FLAG_BITS + } +} + +impl ToCss for AlignFlags { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let extra_flags = *self & AlignFlags::FLAG_BITS; + let value = self.value(); + + match extra_flags { + AlignFlags::LEGACY => { + dest.write_str("legacy")?; + if value.is_empty() { + return Ok(()); + } + dest.write_char(' ')?; + }, + AlignFlags::SAFE => dest.write_str("safe ")?, + AlignFlags::UNSAFE => dest.write_str("unsafe ")?, + _ => { + debug_assert_eq!(extra_flags, AlignFlags::empty()); + }, + } + + dest.write_str(match value { + AlignFlags::AUTO => "auto", + AlignFlags::NORMAL => "normal", + AlignFlags::START => "start", + AlignFlags::END => "end", + AlignFlags::FLEX_START => "flex-start", + AlignFlags::FLEX_END => "flex-end", + AlignFlags::CENTER => "center", + AlignFlags::LEFT => "left", + AlignFlags::RIGHT => "right", + AlignFlags::BASELINE => "baseline", + AlignFlags::LAST_BASELINE => "last baseline", + AlignFlags::STRETCH => "stretch", + AlignFlags::SELF_START => "self-start", + AlignFlags::SELF_END => "self-end", + AlignFlags::SPACE_BETWEEN => "space-between", + AlignFlags::SPACE_AROUND => "space-around", + AlignFlags::SPACE_EVENLY => "space-evenly", + _ => unreachable!(), + }) + } +} + +/// An axis direction, either inline (for the `justify` properties) or block, +/// (for the `align` properties). +#[derive(Clone, Copy, PartialEq)] +pub enum AxisDirection { + /// Block direction. + Block, + /// Inline direction. + Inline, +} + +/// Shared value for the `align-content` and `justify-content` properties. +/// +/// <https://drafts.csswg.org/css-align/#content-distribution> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct ContentDistribution { + primary: AlignFlags, + // FIXME(https://github.com/w3c/csswg-drafts/issues/1002): This will need to + // accept fallback alignment, eventually. +} + +impl ContentDistribution { + /// The initial value 'normal' + #[inline] + pub fn normal() -> Self { + Self::new(AlignFlags::NORMAL) + } + + /// `start` + #[inline] + pub fn start() -> Self { + Self::new(AlignFlags::START) + } + + /// The initial value 'normal' + #[inline] + pub fn new(primary: AlignFlags) -> Self { + Self { primary } + } + + /// Returns whether this value is a <baseline-position>. + pub fn is_baseline_position(&self) -> bool { + matches!( + self.primary.value(), + AlignFlags::BASELINE | AlignFlags::LAST_BASELINE + ) + } + + /// The primary alignment + #[inline] + pub fn primary(self) -> AlignFlags { + self.primary + } + + /// Parse a value for align-content / justify-content. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update the `list_keywords` function below + // when this function is updated. + + // Try to parse normal first + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(ContentDistribution::normal()); + } + + // Parse <baseline-position>, but only on the block axis. + if axis == AxisDirection::Block { + if let Ok(value) = input.try_parse(parse_baseline) { + return Ok(ContentDistribution::new(value)); + } + } + + // <content-distribution> + if let Ok(value) = input.try_parse(parse_content_distribution) { + return Ok(ContentDistribution::new(value)); + } + + // <overflow-position>? <content-position> + let overflow_position = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + + let content_position = try_match_ident_ignore_ascii_case! { input, + "start" => AlignFlags::START, + "end" => AlignFlags::END, + "flex-start" => AlignFlags::FLEX_START, + "flex-end" => AlignFlags::FLEX_END, + "center" => AlignFlags::CENTER, + "left" if axis == AxisDirection::Inline => AlignFlags::LEFT, + "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT, + }; + + Ok(ContentDistribution::new( + content_position | overflow_position, + )) + } + + fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + f(&["normal"]); + if axis == AxisDirection::Block { + list_baseline_keywords(f); + } + list_content_distribution_keywords(f); + list_overflow_position_keywords(f); + f(&["start", "end", "flex-start", "flex-end", "center"]); + if axis == AxisDirection::Inline { + f(&["left", "right"]); + } + } +} + +/// Value for the `align-content` property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-content> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct AlignContent(pub ContentDistribution); + +impl Parse for AlignContent { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(AlignContent(ContentDistribution::parse( + input, + AxisDirection::Block, + )?)) + } +} + +impl SpecifiedValueInfo for AlignContent { + fn collect_completion_keywords(f: KeywordsCollectFn) { + ContentDistribution::list_keywords(f, AxisDirection::Block); + } +} + +/// Value for the `align-tracks` property. +/// +/// <https://github.com/w3c/csswg-drafts/issues/4650> +#[derive( + Clone, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +#[css(comma)] +pub struct AlignTracks(#[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<AlignContent>); + +impl Parse for AlignTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(|input| AlignContent::parse(context, input))?; + Ok(AlignTracks(values.into())) + } +} + +/// Value for the `justify-content` property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-justify-content> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct JustifyContent(pub ContentDistribution); + +impl Parse for JustifyContent { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(JustifyContent(ContentDistribution::parse( + input, + AxisDirection::Inline, + )?)) + } +} + +impl SpecifiedValueInfo for JustifyContent { + fn collect_completion_keywords(f: KeywordsCollectFn) { + ContentDistribution::list_keywords(f, AxisDirection::Inline); + } +} +/// Value for the `justify-tracks` property. +/// +/// <https://github.com/w3c/csswg-drafts/issues/4650> +#[derive( + Clone, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +#[css(comma)] +pub struct JustifyTracks( + #[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<JustifyContent>, +); + +impl Parse for JustifyTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(|input| JustifyContent::parse(context, input))?; + Ok(JustifyTracks(values.into())) + } +} + +/// <https://drafts.csswg.org/css-align/#self-alignment> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct SelfAlignment(pub AlignFlags); + +impl SelfAlignment { + /// The initial value 'auto' + #[inline] + pub fn auto() -> Self { + SelfAlignment(AlignFlags::AUTO) + } + + /// Returns whether this value is valid for both axis directions. + pub fn is_valid_on_both_axes(&self) -> bool { + match self.0.value() { + // left | right are only allowed on the inline axis. + AlignFlags::LEFT | AlignFlags::RIGHT => false, + + _ => true, + } + } + + /// Parse a self-alignment value on one of the axis. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update the `list_keywords` function below + // when this function is updated. + + // <baseline-position> + // + // It's weird that this accepts <baseline-position>, but not + // justify-content... + if let Ok(value) = input.try_parse(parse_baseline) { + return Ok(SelfAlignment(value)); + } + + // auto | normal | stretch + if let Ok(value) = input.try_parse(parse_auto_normal_stretch) { + return Ok(SelfAlignment(value)); + } + + // <overflow-position>? <self-position> + let overflow_position = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, axis)?; + Ok(SelfAlignment(overflow_position | self_position)) + } + + fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + list_baseline_keywords(f); + list_auto_normal_stretch(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, axis); + } +} + +/// The specified value of the align-self property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-self> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct AlignSelf(pub SelfAlignment); + +impl Parse for AlignSelf { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(AlignSelf(SelfAlignment::parse( + input, + AxisDirection::Block, + )?)) + } +} + +impl SpecifiedValueInfo for AlignSelf { + fn collect_completion_keywords(f: KeywordsCollectFn) { + SelfAlignment::list_keywords(f, AxisDirection::Block); + } +} + +/// The specified value of the justify-self property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-justify-self> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct JustifySelf(pub SelfAlignment); + +impl Parse for JustifySelf { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(JustifySelf(SelfAlignment::parse( + input, + AxisDirection::Inline, + )?)) + } +} + +impl SpecifiedValueInfo for JustifySelf { + fn collect_completion_keywords(f: KeywordsCollectFn) { + SelfAlignment::list_keywords(f, AxisDirection::Inline); + } +} + +/// Value of the `align-items` property +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-items> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct AlignItems(pub AlignFlags); + +impl AlignItems { + /// The initial value 'normal' + #[inline] + pub fn normal() -> Self { + AlignItems(AlignFlags::NORMAL) + } +} + +impl Parse for AlignItems { + // normal | stretch | <baseline-position> | + // <overflow-position>? <self-position> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // <baseline-position> + if let Ok(baseline) = input.try_parse(parse_baseline) { + return Ok(AlignItems(baseline)); + } + + // normal | stretch + if let Ok(value) = input.try_parse(parse_normal_stretch) { + return Ok(AlignItems(value)); + } + // <overflow-position>? <self-position> + let overflow = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, AxisDirection::Block)?; + Ok(AlignItems(self_position | overflow)) + } +} + +impl SpecifiedValueInfo for AlignItems { + fn collect_completion_keywords(f: KeywordsCollectFn) { + list_baseline_keywords(f); + list_normal_stretch(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, AxisDirection::Block); + } +} + +/// Value of the `justify-items` property +/// +/// <https://drafts.csswg.org/css-align/#justify-items-property> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)] +#[repr(C)] +pub struct JustifyItems(pub AlignFlags); + +impl JustifyItems { + /// The initial value 'legacy' + #[inline] + pub fn legacy() -> Self { + JustifyItems(AlignFlags::LEGACY) + } + + /// The value 'normal' + #[inline] + pub fn normal() -> Self { + JustifyItems(AlignFlags::NORMAL) + } +} + +impl Parse for JustifyItems { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // <baseline-position> + // + // It's weird that this accepts <baseline-position>, but not + // justify-content... + if let Ok(baseline) = input.try_parse(parse_baseline) { + return Ok(JustifyItems(baseline)); + } + + // normal | stretch + if let Ok(value) = input.try_parse(parse_normal_stretch) { + return Ok(JustifyItems(value)); + } + + // legacy | [ legacy && [ left | right | center ] ] + if let Ok(value) = input.try_parse(parse_legacy) { + return Ok(JustifyItems(value)); + } + + // <overflow-position>? <self-position> + let overflow = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, AxisDirection::Inline)?; + Ok(JustifyItems(overflow | self_position)) + } +} + +impl SpecifiedValueInfo for JustifyItems { + fn collect_completion_keywords(f: KeywordsCollectFn) { + list_baseline_keywords(f); + list_normal_stretch(f); + list_legacy_keywords(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, AxisDirection::Inline); + } +} + +// auto | normal | stretch +fn parse_auto_normal_stretch<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_auto_normal_stretch` function + // below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "auto" => Ok(AlignFlags::AUTO), + "normal" => Ok(AlignFlags::NORMAL), + "stretch" => Ok(AlignFlags::STRETCH), + } +} + +fn list_auto_normal_stretch(f: KeywordsCollectFn) { + f(&["auto", "normal", "stretch"]); +} + +// normal | stretch +fn parse_normal_stretch<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_normal_stretch` function below + // when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "normal" => Ok(AlignFlags::NORMAL), + "stretch" => Ok(AlignFlags::STRETCH), + } +} + +fn list_normal_stretch(f: KeywordsCollectFn) { + f(&["normal", "stretch"]); +} + +// <baseline-position> +fn parse_baseline<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_baseline_keywords` function + // below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "baseline" => Ok(AlignFlags::BASELINE), + "first" => { + input.expect_ident_matching("baseline")?; + Ok(AlignFlags::BASELINE) + }, + "last" => { + input.expect_ident_matching("baseline")?; + Ok(AlignFlags::LAST_BASELINE) + }, + } +} + +fn list_baseline_keywords(f: KeywordsCollectFn) { + f(&["baseline", "first baseline", "last baseline"]); +} + +// <content-distribution> +fn parse_content_distribution<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_content_distribution_keywords` + // function below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "stretch" => Ok(AlignFlags::STRETCH), + "space-between" => Ok(AlignFlags::SPACE_BETWEEN), + "space-around" => Ok(AlignFlags::SPACE_AROUND), + "space-evenly" => Ok(AlignFlags::SPACE_EVENLY), + } +} + +fn list_content_distribution_keywords(f: KeywordsCollectFn) { + f(&["stretch", "space-between", "space-around", "space-evenly"]); +} + +// <overflow-position> +fn parse_overflow_position<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_overflow_position_keywords` + // function below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "safe" => Ok(AlignFlags::SAFE), + "unsafe" => Ok(AlignFlags::UNSAFE), + } +} + +fn list_overflow_position_keywords(f: KeywordsCollectFn) { + f(&["safe", "unsafe"]); +} + +// <self-position> | left | right in the inline axis. +fn parse_self_position<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_self_position_keywords` + // function below when this function is updated. + Ok(try_match_ident_ignore_ascii_case! { input, + "start" => AlignFlags::START, + "end" => AlignFlags::END, + "flex-start" => AlignFlags::FLEX_START, + "flex-end" => AlignFlags::FLEX_END, + "center" => AlignFlags::CENTER, + "self-start" => AlignFlags::SELF_START, + "self-end" => AlignFlags::SELF_END, + "left" if axis == AxisDirection::Inline => AlignFlags::LEFT, + "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT, + }) +} + +fn list_self_position_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + f(&[ + "start", + "end", + "flex-start", + "flex-end", + "center", + "self-start", + "self-end", + ]); + if axis == AxisDirection::Inline { + f(&["left", "right"]); + } +} + +fn parse_left_right_center<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_legacy_keywords` function below + // when this function is updated. + Ok(try_match_ident_ignore_ascii_case! { input, + "left" => AlignFlags::LEFT, + "right" => AlignFlags::RIGHT, + "center" => AlignFlags::CENTER, + }) +} + +// legacy | [ legacy && [ left | right | center ] ] +fn parse_legacy<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_legacy_keywords` function below + // when this function is updated. + let flags = try_match_ident_ignore_ascii_case! { input, + "legacy" => { + let flags = input.try_parse(parse_left_right_center) + .unwrap_or(AlignFlags::empty()); + + return Ok(AlignFlags::LEGACY | flags) + }, + "left" => AlignFlags::LEFT, + "right" => AlignFlags::RIGHT, + "center" => AlignFlags::CENTER, + }; + + input.expect_ident_matching("legacy")?; + Ok(AlignFlags::LEGACY | flags) +} + +fn list_legacy_keywords(f: KeywordsCollectFn) { + f(&["legacy", "left", "right", "center"]); +} diff --git a/servo/components/style/values/specified/angle.rs b/servo/components/style/values/specified/angle.rs new file mode 100644 index 0000000000..fb4554eb85 --- /dev/null +++ b/servo/components/style/values/specified/angle.rs @@ -0,0 +1,276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified angles. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::angle::Angle as ComputedAngle; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::CSSFloat; +use crate::Zero; +use cssparser::{Parser, Token}; +use std::f32::consts::PI; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss}; + +/// A specified angle dimension. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToCss, ToShmem)] +pub enum AngleDimension { + /// An angle with degree unit. + #[css(dimension)] + Deg(CSSFloat), + /// An angle with gradian unit. + #[css(dimension)] + Grad(CSSFloat), + /// An angle with radian unit. + #[css(dimension)] + Rad(CSSFloat), + /// An angle with turn unit. + #[css(dimension)] + Turn(CSSFloat), +} + +impl Zero for AngleDimension { + fn zero() -> Self { + AngleDimension::Deg(0.) + } + + fn is_zero(&self) -> bool { + self.unitless_value() == 0.0 + } +} + +impl AngleDimension { + /// Returns the amount of degrees this angle represents. + #[inline] + fn degrees(&self) -> CSSFloat { + const DEG_PER_RAD: f32 = 180.0 / PI; + const DEG_PER_TURN: f32 = 360.0; + const DEG_PER_GRAD: f32 = 180.0 / 200.0; + + match *self { + AngleDimension::Deg(d) => d, + AngleDimension::Rad(rad) => rad * DEG_PER_RAD, + AngleDimension::Turn(turns) => turns * DEG_PER_TURN, + AngleDimension::Grad(gradians) => gradians * DEG_PER_GRAD, + } + } + + fn unitless_value(&self) -> CSSFloat { + match *self { + AngleDimension::Deg(v) | + AngleDimension::Rad(v) | + AngleDimension::Turn(v) | + AngleDimension::Grad(v) => v, + } + } + + fn unit(&self) -> &'static str { + match *self { + AngleDimension::Deg(_) => "deg", + AngleDimension::Rad(_) => "rad", + AngleDimension::Turn(_) => "turn", + AngleDimension::Grad(_) => "grad", + } + } +} + +/// A specified Angle value, which is just the angle dimension, plus whether it +/// was specified as `calc()` or not. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Angle { + value: AngleDimension, + was_calc: bool, +} + +impl Zero for Angle { + fn zero() -> Self { + Self { + value: Zero::zero(), + was_calc: false, + } + } + + fn is_zero(&self) -> bool { + self.value.is_zero() + } +} + +impl ToCss for Angle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.value.unitless_value(), + self.value.unit(), + self.was_calc, + dest, + ) + } +} + +impl ToComputedValue for Angle { + type ComputedValue = ComputedAngle; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + let degrees = self.degrees(); + + // NaN and +-infinity should degenerate to 0: https://github.com/w3c/csswg-drafts/issues/6105 + ComputedAngle::from_degrees(if degrees.is_finite() { degrees } else { 0.0 }) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Angle { + value: AngleDimension::Deg(computed.degrees()), + was_calc: false, + } + } +} + +impl Angle { + /// Creates an angle with the given value in degrees. + #[inline] + pub fn from_degrees(value: CSSFloat, was_calc: bool) -> Self { + Angle { + value: AngleDimension::Deg(value), + was_calc, + } + } + + /// Creates an angle with the given value in radians. + #[inline] + pub fn from_radians(value: CSSFloat) -> Self { + Angle { + value: AngleDimension::Rad(value), + was_calc: false, + } + } + + /// Return `0deg`. + pub fn zero() -> Self { + Self::from_degrees(0.0, false) + } + + /// Returns the value of the angle in degrees, mostly for `calc()`. + #[inline] + pub fn degrees(&self) -> CSSFloat { + self.value.degrees() + } + + /// Returns the value of the angle in radians. + #[inline] + pub fn radians(&self) -> CSSFloat { + const RAD_PER_DEG: f32 = PI / 180.0; + self.value.degrees() * RAD_PER_DEG + } + + /// Whether this specified angle came from a `calc()` expression. + #[inline] + pub fn was_calc(&self) -> bool { + self.was_calc + } + + /// Returns an `Angle` parsed from a `calc()` expression. + pub fn from_calc(degrees: CSSFloat) -> Self { + Angle { + value: AngleDimension::Deg(degrees), + was_calc: true, + } + } + + /// Returns the unit of the angle. + #[inline] + pub fn unit(&self) -> &'static str { + self.value.unit() + } +} + +/// Whether to allow parsing an unitless zero as a valid angle. +/// +/// This should always be `No`, except for exceptions like: +/// +/// https://github.com/w3c/fxtf-drafts/issues/228 +/// +/// See also: https://github.com/w3c/csswg-drafts/issues/1162. +#[allow(missing_docs)] +pub enum AllowUnitlessZeroAngle { + Yes, + No, +} + +impl Parse for Angle { + /// Parses an angle according to CSS-VALUES § 6.1. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowUnitlessZeroAngle::No) + } +} + +impl Angle { + /// Parse an `<angle>` value given a value and an unit. + pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result<Angle, ()> { + let value = match_ignore_ascii_case! { unit, + "deg" => AngleDimension::Deg(value), + "grad" => AngleDimension::Grad(value), + "turn" => AngleDimension::Turn(value), + "rad" => AngleDimension::Rad(value), + _ => return Err(()) + }; + + Ok(Self { value, was_calc }) + } + + /// Parse an `<angle>` allowing unitless zero to represent a zero angle. + /// + /// See the comment in `AllowUnitlessZeroAngle` for why. + #[inline] + pub fn parse_with_unitless<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowUnitlessZeroAngle::Yes) + } + + pub(super) fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_unitless_zero: AllowUnitlessZeroAngle, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let t = input.next()?; + let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes); + match *t { + Token::Dimension { + value, ref unit, .. + } => { + match Angle::parse_dimension(value, unit, /* from_calc = */ false) { + Ok(angle) => Ok(angle), + Err(()) => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse_angle(context, input, function) + }, + Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()), + ref t => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + } +} + +impl SpecifiedValueInfo for Angle {} diff --git a/servo/components/style/values/specified/animation.rs b/servo/components/style/values/specified/animation.rs new file mode 100644 index 0000000000..e7bbf26fb3 --- /dev/null +++ b/servo/components/style/values/specified/animation.rs @@ -0,0 +1,463 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for properties related to animations and transitions. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::{NonCustomPropertyId, PropertyId, ShorthandId}; +use crate::values::generics::animation as generics; +use crate::values::specified::{LengthPercentage, NonNegativeNumber}; +use crate::values::{CustomIdent, KeyframesName, TimelineName}; +use crate::Atom; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{ + CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss, +}; + +/// A given transition property, that is either `All`, a longhand or shorthand +/// property, or an unsupported or custom property. +#[derive( + Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(u8)] +pub enum TransitionProperty { + /// A non-custom property. + NonCustom(NonCustomPropertyId), + /// A custom property. + Custom(Atom), + /// Unrecognized property which could be any non-transitionable, custom property, or + /// unknown property. + Unsupported(CustomIdent), +} + +impl ToCss for TransitionProperty { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + TransitionProperty::NonCustom(ref id) => id.to_css(dest), + TransitionProperty::Custom(ref name) => { + dest.write_str("--")?; + crate::values::serialize_atom_name(name, dest) + }, + TransitionProperty::Unsupported(ref i) => i.to_css(dest), + } + } +} + +impl Parse for TransitionProperty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + let id = match PropertyId::parse_ignoring_rule_type(&ident, context) { + Ok(id) => id, + Err(..) => { + // None is not acceptable as a single transition-property. + return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident( + location, + ident, + &["none"], + )?)); + }, + }; + + Ok(match id { + PropertyId::NonCustom(id) => TransitionProperty::NonCustom(id.unaliased()), + PropertyId::Custom(name) => TransitionProperty::Custom(name), + }) + } +} + +impl SpecifiedValueInfo for TransitionProperty { + fn collect_completion_keywords(f: KeywordsCollectFn) { + // `transition-property` can actually accept all properties and + // arbitrary identifiers, but `all` is a special one we'd like + // to list. + f(&["all"]); + } +} + +impl TransitionProperty { + /// Returns the `none` value. + #[inline] + pub fn none() -> Self { + TransitionProperty::Unsupported(CustomIdent(atom!("none"))) + } + + /// Returns whether we're the `none` value. + #[inline] + pub fn is_none(&self) -> bool { + matches!(*self, TransitionProperty::Unsupported(ref ident) if ident.0 == atom!("none")) + } + + /// Returns `all`. + #[inline] + pub fn all() -> Self { + TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(ShorthandId::All)) + } + + /// Returns true if it is `all`. + #[inline] + pub fn is_all(&self) -> bool { + self == &TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand( + ShorthandId::All, + )) + } +} + +/// https://drafts.csswg.org/css-animations/#animation-iteration-count +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AnimationIterationCount { + /// A `<number>` value. + Number(NonNegativeNumber), + /// The `infinite` keyword. + Infinite, +} + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + Self::Number(NonNegativeNumber::new(1.0)) + } +} + +/// A value for the `animation-name` property. +#[derive( + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "none")] +#[repr(C)] +pub struct AnimationName(pub KeyframesName); + +impl AnimationName { + /// Get the name of the animation as an `Atom`. + pub fn as_atom(&self) -> Option<&Atom> { + if self.is_none() { + return None; + } + Some(self.0.as_atom()) + } + + /// Returns the `none` value. + pub fn none() -> Self { + AnimationName(KeyframesName::none()) + } + + /// Returns whether this is the none value. + pub fn is_none(&self) -> bool { + self.0.is_none() + } +} + +impl Parse for AnimationName { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) { + return Ok(AnimationName(name)); + } + + input.expect_ident_matching("none")?; + Ok(AnimationName(KeyframesName::none())) + } +} + +/// https://drafts.csswg.org/css-animations/#propdef-animation-direction +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationDirection { + Normal, + Reverse, + Alternate, + AlternateReverse, +} + +/// https://drafts.csswg.org/css-animations/#animation-play-state +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationPlayState { + Running, + Paused, +} + +/// https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationFillMode { + None, + Forwards, + Backwards, + Both, +} + +/// https://drafts.csswg.org/css-animations-2/#animation-composition +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationComposition { + Replace, + Add, + Accumulate, +} + +/// A value for the <Scroller> used in scroll(). +/// +/// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller +#[derive( + Copy, + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Scroller { + /// The nearest ancestor scroll container. (Default.) + Nearest, + /// The document viewport as the scroll container. + Root, + /// Specifies to use the element’s own principal box as the scroll container. + #[css(keyword = "self")] + SelfElement, +} + +impl Scroller { + /// Returns true if it is default. + #[inline] + fn is_default(&self) -> bool { + matches!(*self, Self::Nearest) + } +} + +impl Default for Scroller { + fn default() -> Self { + Self::Nearest + } +} + +/// A value for the <Axis> used in scroll(), or a value for {scroll|view}-timeline-axis. +/// +/// https://drafts.csswg.org/scroll-animations-1/#typedef-axis +/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis +/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis +#[derive( + Copy, + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollAxis { + /// The block axis of the scroll container. (Default.) + Block = 0, + /// The inline axis of the scroll container. + Inline = 1, + /// The vertical block axis of the scroll container. + Vertical = 2, + /// The horizontal axis of the scroll container. + Horizontal = 3, +} + +impl ScrollAxis { + /// Returns true if it is default. + #[inline] + pub fn is_default(&self) -> bool { + matches!(*self, Self::Block) + } +} + +impl Default for ScrollAxis { + fn default() -> Self { + Self::Block + } +} + +/// The scroll() notation. +/// https://drafts.csswg.org/scroll-animations-1/#scroll-notation +#[derive( + Copy, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(function = "scroll")] +#[repr(C)] +pub struct ScrollFunction { + /// The scroll container element whose scroll position drives the progress of the timeline. + #[css(skip_if = "Scroller::is_default")] + pub scroller: Scroller, + /// The axis of scrolling that drives the progress of the timeline. + #[css(skip_if = "ScrollAxis::is_default")] + pub axis: ScrollAxis, +} + +impl ScrollFunction { + /// Parse the inner function arguments of `scroll()`. + fn parse_arguments<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + // <scroll()> = scroll( [ <scroller> || <axis> ]? ) + // https://drafts.csswg.org/scroll-animations-1/#funcdef-scroll + let mut scroller = None; + let mut axis = None; + loop { + if scroller.is_none() { + scroller = input.try_parse(Scroller::parse).ok(); + } + + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + if axis.is_some() { + continue; + } + } + break; + } + + Ok(Self { + scroller: scroller.unwrap_or_default(), + axis: axis.unwrap_or_default(), + }) + } +} + +impl generics::ViewFunction<LengthPercentage> { + /// Parse the inner function arguments of `view()`. + fn parse_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // <view()> = view( [ <axis> || <'view-timeline-inset'> ]? ) + // https://drafts.csswg.org/scroll-animations-1/#funcdef-view + let mut axis = None; + let mut inset = None; + loop { + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + } + + if inset.is_none() { + inset = input + .try_parse(|i| ViewTimelineInset::parse(context, i)) + .ok(); + if inset.is_some() { + continue; + } + } + break; + } + + Ok(Self { + inset: inset.unwrap_or_default(), + axis: axis.unwrap_or_default(), + }) + } +} + +/// A specified value for the `animation-timeline` property. +pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>; + +impl Parse for AnimationTimeline { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::generics::animation::ViewFunction; + + // <single-animation-timeline> = auto | none | <custom-ident> | <scroll()> | <view()> + // https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(Self::Auto); + } + + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(AnimationTimeline::Timeline(TimelineName::none())); + } + + if let Ok(name) = input.try_parse(|i| TimelineName::parse(context, i)) { + return Ok(AnimationTimeline::Timeline(name)); + } + + // Parse possible functions + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "scroll" => ScrollFunction::parse_arguments(i).map(Self::Scroll), + "view" => ViewFunction::parse_arguments(context, i).map(Self::View), + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + }) + } +} + +/// A value for the scroll-timeline-name or view-timeline-name. +pub type ScrollTimelineName = AnimationName; + +/// A specified value for the `view-timeline-inset` property. +pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>; + +impl Parse for ViewTimelineInset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::LengthPercentageOrAuto; + + let start = LengthPercentageOrAuto::parse(context, input)?; + let end = match input.try_parse(|input| LengthPercentageOrAuto::parse(context, input)) { + Ok(end) => end, + Err(_) => start.clone(), + }; + + Ok(Self { start, end }) + } +} diff --git a/servo/components/style/values/specified/background.rs b/servo/components/style/values/specified/background.rs new file mode 100644 index 0000000000..39a5a85193 --- /dev/null +++ b/servo/components/style/values/specified/background.rs @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to backgrounds. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::background::BackgroundSize as GenericBackgroundSize; +use crate::values::specified::length::{ + NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A specified value for the `background-size` property. +pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>; + +impl Parse for BackgroundSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(width) = input.try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i)) + { + let height = input + .try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i)) + .unwrap_or(NonNegativeLengthPercentageOrAuto::auto()); + return Ok(GenericBackgroundSize::ExplicitSize { width, height }); + } + Ok(try_match_ident_ignore_ascii_case! { input, + "cover" => GenericBackgroundSize::Cover, + "contain" => GenericBackgroundSize::Contain, + }) + } +} + +/// One of the keywords for `background-repeat`. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[value_info(other_values = "repeat-x,repeat-y")] +pub enum BackgroundRepeatKeyword { + Repeat, + Space, + Round, + NoRepeat, +} + +/// The value of the `background-repeat` property, with `repeat-x` / `repeat-y` +/// represented as the combination of `no-repeat` and `repeat` in the opposite +/// axes. +/// +/// https://drafts.csswg.org/css-backgrounds/#the-background-repeat +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct BackgroundRepeat(pub BackgroundRepeatKeyword, pub BackgroundRepeatKeyword); + +impl BackgroundRepeat { + /// Returns the `repeat repeat` value. + pub fn repeat() -> Self { + BackgroundRepeat( + BackgroundRepeatKeyword::Repeat, + BackgroundRepeatKeyword::Repeat, + ) + } +} + +impl ToCss for BackgroundRepeat { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match (self.0, self.1) { + (BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat) => { + dest.write_str("repeat-x") + }, + (BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat) => { + dest.write_str("repeat-y") + }, + (horizontal, vertical) => { + horizontal.to_css(dest)?; + if horizontal != vertical { + dest.write_char(' ')?; + vertical.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for BackgroundRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let ident = input.expect_ident_cloned()?; + + match_ignore_ascii_case! { &ident, + "repeat-x" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat)); + }, + "repeat-y" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat)); + }, + _ => {}, + } + + let horizontal = match BackgroundRepeatKeyword::from_ident(&ident) { + Ok(h) => h, + Err(()) => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())) + ); + }, + }; + + let vertical = input.try_parse(BackgroundRepeatKeyword::parse).ok(); + Ok(BackgroundRepeat(horizontal, vertical.unwrap_or(horizontal))) + } +} diff --git a/servo/components/style/values/specified/basic_shape.rs b/servo/components/style/values/specified/basic_shape.rs new file mode 100644 index 0000000000..526296b735 --- /dev/null +++ b/servo/components/style/values/specified/basic_shape.rs @@ -0,0 +1,719 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the specified value of +//! [`basic-shape`][basic-shape]s +//! +//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::basic_shape as generic; +use crate::values::generics::basic_shape::{Path, PolygonCoord}; +use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto}; +use crate::values::generics::rect::Rect; +use crate::values::specified::border::BorderRadius; +use crate::values::specified::image::Image; +use crate::values::specified::length::LengthPercentageOrAuto; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData}; +use crate::Zero; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A specified alias for FillRule. +pub use crate::values::generics::basic_shape::FillRule; + +/// A specified `clip-path` value. +pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>; + +/// A specified `shape-outside` value. +pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>; + +/// A specified value for `at <position>` in circle() and ellipse(). +// Note: its computed value is the same as computed::position::Position. We just want to always use +// LengthPercentage as the type of its components, for basic shapes. +pub type ShapePosition = GenericPosition<LengthPercentage, LengthPercentage>; + +/// A specified basic shape. +pub type BasicShape = generic::GenericBasicShape< + ShapePosition, + LengthPercentage, + NonNegativeLengthPercentage, + BasicShapeRect, +>; + +/// The specified value of `inset()`. +pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>; + +/// A specified circle. +pub type Circle = generic::Circle<ShapePosition, NonNegativeLengthPercentage>; + +/// A specified ellipse. +pub type Ellipse = generic::Ellipse<ShapePosition, NonNegativeLengthPercentage>; + +/// The specified value of `ShapeRadius`. +pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>; + +/// The specified value of `Polygon`. +pub type Polygon = generic::GenericPolygon<LengthPercentage>; + +/// The specified value of `xywh()`. +/// Defines a rectangle via offsets from the top and left edge of the reference box, and a +/// specified width and height. +/// +/// The four <length-percentage>s define, respectively, the inset from the left edge of the +/// reference box, the inset from the top edge of the reference box, the width of the rectangle, +/// and the height of the rectangle. +/// +/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +pub struct Xywh { + /// The left edge of the reference box. + pub x: LengthPercentage, + /// The top edge of the reference box. + pub y: LengthPercentage, + /// The specified width. + pub width: NonNegativeLengthPercentage, + /// The specified height. + pub height: NonNegativeLengthPercentage, + /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle + /// using the border-radius shorthand syntax. + pub round: BorderRadius, +} + +/// Defines a rectangle via insets from the top and left edges of the reference box. +/// +/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +#[repr(C)] +pub struct ShapeRectFunction { + /// The four <length-percentage>s define the position of the top, right, bottom, and left edges + /// of a rectangle, respectively, as insets from the top edge of the reference box (for the + /// first and third values) or the left edge of the reference box (for the second and fourth + /// values). + /// + /// An auto value makes the edge of the box coincide with the corresponding edge of the + /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and + /// equivalent to 100% as the second (right) or third (bottom) value. + pub rect: Rect<LengthPercentageOrAuto>, + /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle + /// using the border-radius shorthand syntax. + pub round: BorderRadius, +} + +/// The specified value of <basic-shape-rect>. +/// <basic-shape-rect> = <inset()> | <rect()> | <xywh()> +/// +/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum BasicShapeRect { + /// Defines an inset rectangle via insets from each edge of the reference box. + Inset(InsetRect), + /// Defines a xywh function. + #[css(function)] + Xywh(Xywh), + /// Defines a rect function. + #[css(function)] + Rect(ShapeRectFunction), +} + +/// For filled shapes, we use fill-rule, and store it for path() and polygon(). +/// For outline shapes, we should ignore fill-rule. +/// +/// https://github.com/w3c/fxtf-drafts/issues/512 +/// https://github.com/w3c/csswg-drafts/issues/7390 +/// https://github.com/w3c/csswg-drafts/issues/3468 +pub enum ShapeType { + /// The CSS property uses filled shapes. The default behavior. + Filled, + /// The CSS property uses outline shapes. This is especially useful for offset-path. + Outline, +} + +bitflags! { + /// The flags to represent which basic shapes we would like to support. + /// + /// Different properties may use different subsets of <basic-shape>: + /// e.g. + /// clip-path: all basic shapes. + /// motion-path: all basic shapes (but ignore fill-rule). + /// shape-outside: inset(), circle(), ellipse(), polygon(). + /// + /// Also there are some properties we don't support for now: + /// shape-inside: inset(), circle(), ellipse(), polygon(). + /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon(). + /// + /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now + /// we use the bitflags to choose the supported basic shapes for each property at the parse + /// time. + /// https://github.com/w3c/csswg-drafts/issues/7390 + #[derive(Clone, Copy)] + #[repr(C)] + pub struct AllowedBasicShapes: u8 { + /// inset(). + const INSET = 1 << 0; + /// xywh(). + const XYWH = 1 << 1; + /// rect(). + const RECT = 1 << 2; + /// circle(). + const CIRCLE = 1 << 3; + /// ellipse(). + const ELLIPSE = 1 << 4; + /// polygon(). + const POLYGON = 1 << 5; + /// path(). + const PATH = 1 << 6; + // TODO: Bug 1823463. Add shape(). + // const SHAPE = 1 << 7; + + /// All flags. + const ALL = + Self::INSET.bits() | + Self::XYWH.bits() | + Self::RECT.bits() | + Self::CIRCLE.bits() | + Self::ELLIPSE.bits() | + Self::POLYGON.bits() | + Self::PATH.bits(); + + /// For shape-outside. + const SHAPE_OUTSIDE = + Self::INSET.bits() | + Self::CIRCLE.bits() | + Self::ELLIPSE.bits() | + Self::POLYGON.bits(); + } +} + +/// A helper for both clip-path and shape-outside parsing of shapes. +fn parse_shape_or_box<'i, 't, R, ReferenceBox>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R, + to_reference_box: impl FnOnce(ReferenceBox) -> R, + flags: AllowedBasicShapes, +) -> Result<R, ParseError<'i>> +where + ReferenceBox: Default + Parse, +{ + let mut shape = None; + let mut ref_box = None; + loop { + if shape.is_none() { + shape = input + .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled)) + .ok(); + } + + if ref_box.is_none() { + ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok(); + if ref_box.is_some() { + continue; + } + } + break; + } + + if let Some(shp) = shape { + return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default())); + } + + match ref_box { + Some(r) => Ok(to_reference_box(r)), + None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } +} + +impl Parse for ClipPath { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ClipPath::None); + } + + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(ClipPath::Url(url)); + } + + parse_shape_or_box( + context, + input, + ClipPath::Shape, + ClipPath::Box, + AllowedBasicShapes::ALL, + ) + } +} + +impl Parse for ShapeOutside { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Need to parse this here so that `Image::parse_with_cors_anonymous` + // doesn't parse it. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ShapeOutside::None); + } + + if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) { + debug_assert_ne!(image, Image::None); + return Ok(ShapeOutside::Image(image)); + } + + parse_shape_or_box( + context, + input, + ShapeOutside::Shape, + ShapeOutside::Box, + AllowedBasicShapes::SHAPE_OUTSIDE, + ) + } +} + +impl BasicShape { + /// Parse with some parameters. + /// 1. The supported <basic-shape>. + /// 2. The type of shapes. Should we ignore fill-rule? + /// 3. The default value of `at <position>`. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + flags: AllowedBasicShapes, + shape_type: ShapeType, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "inset" if flags.contains(AllowedBasicShapes::INSET) => { + InsetRect::parse_function_arguments(context, i) + .map(BasicShapeRect::Inset) + .map(BasicShape::Rect) + }, + "xywh" + if flags.contains(AllowedBasicShapes::XYWH) + && static_prefs::pref!("layout.css.basic-shape-xywh.enabled") => + { + Xywh::parse_function_arguments(context, i) + .map(BasicShapeRect::Xywh) + .map(BasicShape::Rect) + }, + "rect" + if flags.contains(AllowedBasicShapes::RECT) + && static_prefs::pref!("layout.css.basic-shape-rect.enabled") => + { + ShapeRectFunction::parse_function_arguments(context, i) + .map(BasicShapeRect::Rect) + .map(BasicShape::Rect) + }, + "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => { + Circle::parse_function_arguments(context, i) + .map(BasicShape::Circle) + }, + "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => { + Ellipse::parse_function_arguments(context, i) + .map(BasicShape::Ellipse) + }, + "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => { + Polygon::parse_function_arguments(context, i, shape_type) + .map(BasicShape::Polygon) + }, + "path" if flags.contains(AllowedBasicShapes::PATH) => { + Path::parse_function_arguments(i, shape_type).map(BasicShape::Path) + }, + _ => Err(location + .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))), + } + }) + } +} + +impl Parse for InsetRect { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("inset")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +fn parse_round<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<BorderRadius, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("round")) + .is_ok() + { + return BorderRadius::parse(context, input); + } + + Ok(BorderRadius::zero()) +} + +impl InsetRect { + /// Parse the inner function arguments of `inset()` + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let rect = Rect::parse_with(context, input, LengthPercentage::parse)?; + let round = parse_round(context, input)?; + Ok(generic::InsetRect { rect, round }) + } +} + +impl ToCss for ShapePosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.horizontal.to_css(dest)?; + dest.write_char(' ')?; + self.vertical.to_css(dest) + } +} + +fn parse_at_position<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<GenericPositionOrAuto<ShapePosition>, ParseError<'i>> { + use crate::values::specified::position::{Position, Side}; + use crate::values::specified::{AllowedNumericType, Percentage, PositionComponent}; + + fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage { + // Convert the value when parsing, to make sure we serialize it properly for both + // specified and computed values. + // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization + match c { + // Since <position> keywords stand in for percentages, keywords without an offset + // turn into percentages. + PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)), + PositionComponent::Side(keyword, None) => { + Percentage::new(if keyword.is_start() { 0. } else { 1. }).into() + }, + // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of + // "avoiding calc() expressions where possible" and "avoiding calc() + // transformations" will be removed from the spec, and we should follow the + // css-values-4 for position, i.e. we make it as length-percentage always. + // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization. + // https://drafts.csswg.org/css-values-4/#typedef-position + PositionComponent::Side(keyword, Some(length)) => { + if keyword.is_start() { + length + } else { + length.hundred_percent_minus(AllowedNumericType::All) + } + }, + PositionComponent::Length(length) => length, + } + } + + if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + Position::parse(context, input).map(|pos| { + GenericPositionOrAuto::Position(ShapePosition::new( + convert_to_length_percentage(pos.horizontal), + convert_to_length_percentage(pos.vertical), + )) + }) + } else { + // `at <position>` is omitted. + Ok(GenericPositionOrAuto::Auto) + } +} + +impl Parse for Circle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("circle")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Circle { + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let radius = input + .try_parse(|i| ShapeRadius::parse(context, i)) + .unwrap_or_default(); + let position = parse_at_position(context, input)?; + + Ok(generic::Circle { radius, position }) + } +} + +impl Parse for Ellipse { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("ellipse")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Ellipse { + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let (semiaxis_x, semiaxis_y) = input + .try_parse(|i| -> Result<_, ParseError> { + Ok(( + ShapeRadius::parse(context, i)?, + ShapeRadius::parse(context, i)?, + )) + }) + .unwrap_or_default(); + let position = parse_at_position(context, input)?; + + Ok(generic::Ellipse { + semiaxis_x, + semiaxis_y, + position, + }) + } +} + +fn parse_fill_rule<'i, 't>(input: &mut Parser<'i, 't>, shape_type: ShapeType) -> FillRule { + match shape_type { + // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default + // value. + // [1] https://github.com/w3c/csswg-drafts/issues/3468 + // [2] https://github.com/w3c/csswg-drafts/issues/7390 + // + // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g. + // offset-path, which means we don't parse it when setting `ShapeType::Outline`. + // This should be web compatible because the shipped "offset-path:path()" doesn't have + // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the + // preference. + // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321 + // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929 + ShapeType::Outline => Default::default(), + ShapeType::Filled => input + .try_parse(|i| -> Result<_, ParseError> { + let fill = FillRule::parse(i)?; + i.expect_comma()?; + Ok(fill) + }) + .unwrap_or_default(), + } +} + +impl Parse for Polygon { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("polygon")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled)) + } +} + +impl Polygon { + /// Parse the inner arguments of a `polygon` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + shape_type: ShapeType, + ) -> Result<Self, ParseError<'i>> { + let fill = parse_fill_rule(input, shape_type); + let coordinates = input + .parse_comma_separated(|i| { + Ok(PolygonCoord( + LengthPercentage::parse(context, i)?, + LengthPercentage::parse(context, i)?, + )) + })? + .into(); + + Ok(Polygon { fill, coordinates }) + } +} + +impl Path { + /// Parse the inner arguments of a `path` function. + fn parse_function_arguments<'i, 't>( + input: &mut Parser<'i, 't>, + shape_type: ShapeType, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::svg_path::AllowEmpty; + + let fill = parse_fill_rule(input, shape_type); + let path = SVGPathData::parse(input, AllowEmpty::No)?; + Ok(Path { fill, path }) + } +} + +fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result +where + W: Write, +{ + if !round.is_zero() { + dest.write_str(" round ")?; + round.to_css(dest)?; + } + Ok(()) +} + +impl ToCss for Xywh { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.x.to_css(dest)?; + dest.write_char(' ')?; + self.y.to_css(dest)?; + dest.write_char(' ')?; + self.width.to_css(dest)?; + dest.write_char(' ')?; + self.height.to_css(dest)?; + round_to_css(&self.round, dest) + } +} + +impl Xywh { + /// Parse the inner function arguments of `xywh()`. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let x = LengthPercentage::parse(context, input)?; + let y = LengthPercentage::parse(context, input)?; + let width = NonNegativeLengthPercentage::parse(context, input)?; + let height = NonNegativeLengthPercentage::parse(context, input)?; + let round = parse_round(context, input)?; + Ok(Xywh { + x, + y, + width, + height, + round, + }) + } +} + +impl ToCss for ShapeRectFunction { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.rect.0.to_css(dest)?; + dest.write_char(' ')?; + self.rect.1.to_css(dest)?; + dest.write_char(' ')?; + self.rect.2.to_css(dest)?; + dest.write_char(' ')?; + self.rect.3.to_css(dest)?; + round_to_css(&self.round, dest) + } +} + +impl ShapeRectFunction { + /// Parse the inner function arguments of `rect()`. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?; + let round = parse_round(context, input)?; + Ok(ShapeRectFunction { rect, round }) + } +} + +impl ToComputedValue for BasicShapeRect { + type ComputedValue = ComputedInsetRect; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::LengthPercentage; + use crate::values::computed::LengthPercentageOrAuto; + use style_traits::values::specified::AllowedNumericType; + + match self { + Self::Inset(ref inset) => inset.to_computed_value(context), + Self::Xywh(ref xywh) => { + // Given `xywh(x y w h)`, construct the equivalent inset() function, + // `inset(y calc(100% - x - w) calc(100% - y - h) x)`. + // + // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values + // https://github.com/w3c/csswg-drafts/issues/9053 + let x = xywh.x.to_computed_value(context); + let y = xywh.y.to_computed_value(context); + let w = xywh.width.to_computed_value(context); + let h = xywh.height.to_computed_value(context); + // calc(100% - x - w). + let right = LengthPercentage::hundred_percent_minus_list( + &[&x, &w.0], + AllowedNumericType::All, + ); + // calc(100% - y - h). + let bottom = LengthPercentage::hundred_percent_minus_list( + &[&y, &h.0], + AllowedNumericType::All, + ); + + ComputedInsetRect { + rect: Rect::new(y, right, bottom, x), + round: xywh.round.to_computed_value(context), + } + }, + Self::Rect(ref rect) => { + // Given `rect(t r b l)`, the equivalent function is + // `inset(t calc(100% - r) calc(100% - b) l)`. + // + // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values + fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage { + match v { + // it’s equivalent to 0% as the first (top) or fourth (left) value. + // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect + LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), + LengthPercentageOrAuto::LengthPercentage(lp) => lp, + } + } + fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage { + match v { + // It's equivalent to 100% as the second (right) or third (bottom) value. + // So calc(100% - 100%) = 0%. + // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect + LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), + LengthPercentageOrAuto::LengthPercentage(lp) => { + LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All) + }, + } + } + + let round = rect.round.to_computed_value(context); + let rect = rect.rect.to_computed_value(context); + let rect = Rect::new( + compute_top_or_left(rect.0), + compute_bottom_or_right(rect.1), + compute_bottom_or_right(rect.2), + compute_top_or_left(rect.3), + ); + + ComputedInsetRect { rect, round } + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Inset(ToComputedValue::from_computed_value(computed)) + } +} diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs new file mode 100644 index 0000000000..a4660c7f60 --- /dev/null +++ b/servo/components/style/values/specified/border.rs @@ -0,0 +1,398 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to borders. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius; +use crate::values::generics::border::BorderImageSideWidth as GenericBorderImageSideWidth; +use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice; +use crate::values::generics::border::BorderRadius as GenericBorderRadius; +use crate::values::generics::border::BorderSpacing as GenericBorderSpacing; +use crate::values::generics::rect::Rect; +use crate::values::generics::size::Size2D; +use crate::values::specified::length::{Length, NonNegativeLength, NonNegativeLengthPercentage}; +use crate::values::specified::Color; +use crate::values::specified::{AllowQuirks, NonNegativeNumber, NonNegativeNumberOrPercentage}; +use crate::Zero; +use app_units::Au; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{values::SequenceWriter, CssWriter, ParseError, ToCss}; + +/// A specified value for a single side of a `border-style` property. +/// +/// The order here corresponds to the integer values from the border conflict +/// resolution rules in CSS 2.1 § 17.6.2.1. Higher values override lower values. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Ord, + Parse, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BorderStyle { + Hidden, + None, + Inset, + Groove, + Outset, + Ridge, + Dotted, + Dashed, + Solid, + Double, +} + +impl BorderStyle { + /// Whether this border style is either none or hidden. + #[inline] + pub fn none_or_hidden(&self) -> bool { + matches!(*self, BorderStyle::None | BorderStyle::Hidden) + } +} + +/// A specified value for the `border-image-width` property. +pub type BorderImageWidth = Rect<BorderImageSideWidth>; + +/// A specified value for a single side of a `border-image-width` property. +pub type BorderImageSideWidth = + GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>; + +/// A specified value for the `border-image-slice` property. +pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>; + +/// A specified value for the `border-radius` property. +pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>; + +/// A specified value for the `border-*-radius` longhand properties. +pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>; + +/// A specified value for the `border-spacing` longhand properties. +pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>; + +impl BorderImageSlice { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + GenericBorderImageSlice { + offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()), + fill: false, + } + } +} + +/// https://drafts.csswg.org/css-backgrounds-3/#typedef-line-width +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum LineWidth { + /// `thin` + Thin, + /// `medium` + Medium, + /// `thick` + Thick, + /// `<length>` + Length(NonNegativeLength), +} + +impl LineWidth { + /// Returns the `0px` value. + #[inline] + pub fn zero() -> Self { + Self::Length(NonNegativeLength::zero()) + } + + fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if let Ok(length) = + input.try_parse(|i| NonNegativeLength::parse_quirky(context, i, allow_quirks)) + { + return Ok(Self::Length(length)); + } + Ok(try_match_ident_ignore_ascii_case! { input, + "thin" => Self::Thin, + "medium" => Self::Medium, + "thick" => Self::Thick, + }) + } +} + +impl Parse for LineWidth { + fn parse<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for LineWidth { + type ComputedValue = app_units::Au; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + // https://drafts.csswg.org/css-backgrounds-3/#line-width + Self::Thin => Au::from_px(1), + Self::Medium => Au::from_px(3), + Self::Thick => Au::from_px(5), + Self::Length(ref length) => Au::from_f32_px(length.to_computed_value(context).px()), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Length(NonNegativeLength::from_px(computed.to_f32_px())) + } +} + +/// A specified value for a single side of the `border-width` property. The difference between this +/// and LineWidth is whether we snap to device pixels or not. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct BorderSideWidth(LineWidth); + +impl BorderSideWidth { + /// Returns the `medium` value. + pub fn medium() -> Self { + Self(LineWidth::Medium) + } + + /// Returns a bare px value from the argument. + pub fn from_px(px: f32) -> Self { + Self(LineWidth::Length(Length::from_px(px).into())) + } + + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Ok(Self(LineWidth::parse_quirky(context, input, allow_quirks)?)) + } +} + +impl Parse for BorderSideWidth { + fn parse<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for BorderSideWidth { + type ComputedValue = app_units::Au; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let width = self.0.to_computed_value(context); + // Round `width` down to the nearest device pixel, but any non-zero value that would round + // down to zero is clamped to 1 device pixel. + if width == Au(0) { + return width; + } + + let au_per_dev_px = context.device().app_units_per_device_pixel(); + std::cmp::max( + Au(au_per_dev_px), + Au(width.0 / au_per_dev_px * au_per_dev_px), + ) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self(LineWidth::from_computed_value(computed)) + } +} + +impl BorderImageSideWidth { + /// Returns `1`. + #[inline] + pub fn one() -> Self { + GenericBorderImageSideWidth::Number(NonNegativeNumber::new(1.)) + } +} + +impl Parse for BorderImageSlice { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok(); + let offsets = Rect::parse_with(context, input, NonNegativeNumberOrPercentage::parse)?; + if !fill { + fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok(); + } + Ok(GenericBorderImageSlice { offsets, fill }) + } +} + +impl Parse for BorderRadius { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let widths = Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)?; + let heights = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)? + } else { + widths.clone() + }; + + Ok(GenericBorderRadius { + top_left: BorderCornerRadius::new(widths.0, heights.0), + top_right: BorderCornerRadius::new(widths.1, heights.1), + bottom_right: BorderCornerRadius::new(widths.2, heights.2), + bottom_left: BorderCornerRadius::new(widths.3, heights.3), + }) + } +} + +impl Parse for BorderCornerRadius { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size2D::parse_with(context, input, NonNegativeLengthPercentage::parse) + .map(GenericBorderCornerRadius) + } +} + +impl Parse for BorderSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size2D::parse_with(context, input, |context, input| { + NonNegativeLength::parse_quirky(context, input, AllowQuirks::Yes) + }) + .map(GenericBorderSpacing) + } +} + +/// A single border-image-repeat keyword. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum BorderImageRepeatKeyword { + Stretch, + Repeat, + Round, + Space, +} + +/// The specified value for the `border-image-repeat` property. +/// +/// https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct BorderImageRepeat(pub BorderImageRepeatKeyword, pub BorderImageRepeatKeyword); + +impl ToCss for BorderImageRepeat { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + if self.0 != self.1 { + dest.write_char(' ')?; + self.1.to_css(dest)?; + } + Ok(()) + } +} + +impl BorderImageRepeat { + /// Returns the `stretch` value. + #[inline] + pub fn stretch() -> Self { + BorderImageRepeat( + BorderImageRepeatKeyword::Stretch, + BorderImageRepeatKeyword::Stretch, + ) + } +} + +impl Parse for BorderImageRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let horizontal = BorderImageRepeatKeyword::parse(input)?; + let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok(); + Ok(BorderImageRepeat( + horizontal, + vertical.unwrap_or(horizontal), + )) + } +} + +/// Serializes a border shorthand value composed of width/style/color. +pub fn serialize_directional_border<W>( + dest: &mut CssWriter<W>, + width: &BorderSideWidth, + style: &BorderStyle, + color: &Color, +) -> fmt::Result +where + W: Write, +{ + let has_style = *style != BorderStyle::None; + let has_color = *color != Color::CurrentColor; + let has_width = *width != BorderSideWidth::medium(); + if !has_style && !has_color && !has_width { + return width.to_css(dest); + } + let mut writer = SequenceWriter::new(dest, " "); + if has_width { + writer.item(width)?; + } + if has_style { + writer.item(style)?; + } + if has_color { + writer.item(color)?; + } + Ok(()) +} diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs new file mode 100644 index 0000000000..8414591c2b --- /dev/null +++ b/servo/components/style/values/specified/box.rs @@ -0,0 +1,1945 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for box properties. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::{LonghandId, PropertyDeclarationId, PropertyId}; +use crate::values::generics::box_::{ + GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign, + VerticalAlignKeyword, +}; +use crate::values::specified::length::{LengthPercentage, NonNegativeLength}; +use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumberOrPercentage}; +use crate::values::CustomIdent; +use cssparser::Parser; +use num_traits::FromPrimitive; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(not(feature = "servo-layout-2020"))] +fn flexbox_enabled() -> bool { + true +} + +#[cfg(feature = "servo-layout-2020")] +fn flexbox_enabled() -> bool { + servo_config::prefs::pref_map() + .get("layout.flexbox.enabled") + .as_bool() + .unwrap_or(false) +} + +/// Defines an element’s display type, which consists of +/// the two basic qualities of how an element generates boxes +/// <https://drafts.csswg.org/css-display/#propdef-display> +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayOutside { + None = 0, + Inline, + Block, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCaption, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + InternalTable, + #[cfg(feature = "gecko")] + InternalRuby, +} + +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayInside { + None = 0, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Contents, + Flow, + FlowRoot, + Flex, + #[cfg(feature = "gecko")] + Grid, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Table, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRowGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumn, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumnGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableHeaderGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableFooterGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRow, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCell, + #[cfg(feature = "gecko")] + Ruby, + #[cfg(feature = "gecko")] + RubyBase, + #[cfg(feature = "gecko")] + RubyBaseContainer, + #[cfg(feature = "gecko")] + RubyText, + #[cfg(feature = "gecko")] + RubyTextContainer, + #[cfg(feature = "gecko")] + WebkitBox, +} + +impl DisplayInside { + fn is_valid_for_list_item(self) -> bool { + match self { + DisplayInside::Flow => true, + #[cfg(feature = "gecko")] + DisplayInside::FlowRoot => true, + _ => false, + } + } + + /// https://drafts.csswg.org/css-display/#inside-model: + /// If <display-outside> is omitted, the element’s outside display type defaults to block + /// — except for ruby, which defaults to inline. + fn default_display_outside(self) -> DisplayOutside { + match self { + #[cfg(feature = "gecko")] + DisplayInside::Ruby => DisplayOutside::Inline, + _ => DisplayOutside::Block, + } + } +} + +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Display(u16); + +/// Gecko-only impl block for Display (shared stuff later in this file): +#[allow(missing_docs)] +#[allow(non_upper_case_globals)] +impl Display { + // Our u16 bits are used as follows: LOOOOOOOIIIIIIII + pub const LIST_ITEM_MASK: u16 = 0b1000000000000000; + pub const OUTSIDE_MASK: u16 = 0b0111111100000000; + pub const INSIDE_MASK: u16 = 0b0000000011111111; + pub const OUTSIDE_SHIFT: u16 = 8; + + /// https://drafts.csswg.org/css-display/#the-display-properties + /// ::new() inlined so cbindgen can use it + pub const None: Self = + Self(((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::None as u16); + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + pub const Contents: Self = Self( + ((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Contents as u16, + ); + pub const Inline: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16); + pub const InlineBlock: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16, + ); + pub const Block: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16); + #[cfg(feature = "gecko")] + pub const FlowRoot: Self = Self( + ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16, + ); + pub const Flex: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16); + pub const InlineFlex: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16); + #[cfg(feature = "gecko")] + pub const Grid: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16); + #[cfg(feature = "gecko")] + pub const InlineGrid: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const Table: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const InlineTable: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCaption: Self = Self( + ((DisplayOutside::TableCaption as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16, + ); + #[cfg(feature = "gecko")] + pub const Ruby: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Ruby as u16); + #[cfg(feature = "gecko")] + pub const WebkitBox: Self = Self( + ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16, + ); + #[cfg(feature = "gecko")] + pub const WebkitInlineBox: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16, + ); + + // Internal table boxes. + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRowGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableRowGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableHeaderGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableHeaderGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableFooterGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableFooterGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumn: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableColumn as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumnGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableColumnGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRow: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableRow as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCell: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableCell as u16, + ); + + /// Internal ruby boxes. + #[cfg(feature = "gecko")] + pub const RubyBase: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyBase as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyBaseContainer: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyBaseContainer as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyText: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyText as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyTextContainer: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyTextContainer as u16, + ); + + /// Make a raw display value from <display-outside> and <display-inside> values. + #[inline] + const fn new(outside: DisplayOutside, inside: DisplayInside) -> Self { + Self((outside as u16) << Self::OUTSIDE_SHIFT | inside as u16) + } + + /// Make a display enum value from <display-outside> and <display-inside> values. + #[inline] + fn from3(outside: DisplayOutside, inside: DisplayInside, list_item: bool) -> Self { + let v = Self::new(outside, inside); + if !list_item { + return v; + } + Self(v.0 | Self::LIST_ITEM_MASK) + } + + /// Accessor for the <display-inside> value. + #[inline] + pub fn inside(&self) -> DisplayInside { + DisplayInside::from_u16(self.0 & Self::INSIDE_MASK).unwrap() + } + + /// Accessor for the <display-outside> value. + #[inline] + pub fn outside(&self) -> DisplayOutside { + DisplayOutside::from_u16((self.0 & Self::OUTSIDE_MASK) >> Self::OUTSIDE_SHIFT).unwrap() + } + + /// Returns the raw underlying u16 value. + #[inline] + pub const fn to_u16(&self) -> u16 { + self.0 + } + + /// Whether this is `display: inline` (or `inline list-item`). + #[inline] + pub fn is_inline_flow(&self) -> bool { + self.outside() == DisplayOutside::Inline && self.inside() == DisplayInside::Flow + } + + /// Returns whether this `display` value is some kind of list-item. + #[inline] + pub const fn is_list_item(&self) -> bool { + (self.0 & Self::LIST_ITEM_MASK) != 0 + } + + /// Returns whether this `display` value is a ruby level container. + pub fn is_ruby_level_container(&self) -> bool { + match *self { + #[cfg(feature = "gecko")] + Display::RubyBaseContainer | Display::RubyTextContainer => true, + _ => false, + } + } + + /// Returns whether this `display` value is one of the types for ruby. + pub fn is_ruby_type(&self) -> bool { + match self.inside() { + #[cfg(feature = "gecko")] + DisplayInside::Ruby | + DisplayInside::RubyBase | + DisplayInside::RubyText | + DisplayInside::RubyBaseContainer | + DisplayInside::RubyTextContainer => true, + _ => false, + } + } +} + +/// Shared Display impl for both Gecko and Servo. +impl Display { + /// The initial display value. + #[inline] + pub fn inline() -> Self { + Display::Inline + } + + /// <https://drafts.csswg.org/css2/visuren.html#x13> + #[cfg(feature = "servo")] + #[inline] + pub fn is_atomic_inline_level(&self) -> bool { + match *self { + Display::InlineBlock | Display::InlineFlex => true, + #[cfg(any(feature = "servo-layout-2013"))] + Display::InlineTable => true, + _ => false, + } + } + + /// Returns whether this `display` value is the display of a flex or + /// grid container. + /// + /// This is used to implement various style fixups. + pub fn is_item_container(&self) -> bool { + match self.inside() { + DisplayInside::Flex => true, + #[cfg(feature = "gecko")] + DisplayInside::Grid => true, + _ => false, + } + } + + /// Returns whether an element with this display type is a line + /// participant, which means it may lay its children on the same + /// line as itself. + pub fn is_line_participant(&self) -> bool { + if self.is_inline_flow() { + return true; + } + match *self { + #[cfg(feature = "gecko")] + Display::Contents | Display::Ruby | Display::RubyBaseContainer => true, + _ => false, + } + } + + /// Convert this display into an equivalent block display. + /// + /// Also used for :root style adjustments. + pub fn equivalent_block_display(&self, _is_root_element: bool) -> Self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + { + // Special handling for `contents` and `list-item`s on the root element. + if _is_root_element && (self.is_contents() || self.is_list_item()) { + return Display::Block; + } + } + + match self.outside() { + DisplayOutside::Inline => { + let inside = match self.inside() { + // `inline-block` blockifies to `block` rather than + // `flow-root`, for legacy reasons. + DisplayInside::FlowRoot => DisplayInside::Flow, + inside => inside, + }; + Display::from3(DisplayOutside::Block, inside, self.is_list_item()) + }, + DisplayOutside::Block | DisplayOutside::None => *self, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + _ => Display::Block, + } + } + + /// Convert this display into an equivalent inline-outside display. + /// https://drafts.csswg.org/css-display/#inlinify + #[cfg(feature = "gecko")] + pub fn inlinify(&self) -> Self { + match self.outside() { + DisplayOutside::Block => { + let inside = match self.inside() { + // `display: block` inlinifies to `display: inline-block`, + // rather than `inline`, for legacy reasons. + DisplayInside::Flow => DisplayInside::FlowRoot, + inside => inside, + }; + Display::from3(DisplayOutside::Inline, inside, self.is_list_item()) + }, + _ => *self, + } + } + + /// Returns true if the value is `Contents` + #[inline] + pub fn is_contents(&self) -> bool { + match *self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Display::Contents => true, + _ => false, + } + } + + /// Returns true if the value is `None` + #[inline] + pub fn is_none(&self) -> bool { + *self == Display::None + } +} + +enum DisplayKeyword { + Full(Display), + Inside(DisplayInside), + Outside(DisplayOutside), + ListItem, +} + +impl DisplayKeyword { + fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + use self::DisplayKeyword::*; + Ok(try_match_ident_ignore_ascii_case! { input, + "none" => Full(Display::None), + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "contents" => Full(Display::Contents), + "inline-block" => Full(Display::InlineBlock), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "inline-table" => Full(Display::InlineTable), + "-webkit-flex" if flexbox_enabled() => Full(Display::Flex), + "inline-flex" | "-webkit-inline-flex" if flexbox_enabled() => Full(Display::InlineFlex), + #[cfg(feature = "gecko")] + "inline-grid" => Full(Display::InlineGrid), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-caption" => Full(Display::TableCaption), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row-group" => Full(Display::TableRowGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-header-group" => Full(Display::TableHeaderGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-footer-group" => Full(Display::TableFooterGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column" => Full(Display::TableColumn), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column-group" => Full(Display::TableColumnGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row" => Full(Display::TableRow), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-cell" => Full(Display::TableCell), + #[cfg(feature = "gecko")] + "ruby-base" => Full(Display::RubyBase), + #[cfg(feature = "gecko")] + "ruby-base-container" => Full(Display::RubyBaseContainer), + #[cfg(feature = "gecko")] + "ruby-text" => Full(Display::RubyText), + #[cfg(feature = "gecko")] + "ruby-text-container" => Full(Display::RubyTextContainer), + #[cfg(feature = "gecko")] + "-webkit-box" => Full(Display::WebkitBox), + #[cfg(feature = "gecko")] + "-webkit-inline-box" => Full(Display::WebkitInlineBox), + + /// <display-outside> = block | inline | run-in + /// https://drafts.csswg.org/css-display/#typedef-display-outside + "block" => Outside(DisplayOutside::Block), + "inline" => Outside(DisplayOutside::Inline), + + "list-item" => ListItem, + + /// <display-inside> = flow | flow-root | table | flex | grid | ruby + /// https://drafts.csswg.org/css-display/#typedef-display-inside + "flow" => Inside(DisplayInside::Flow), + "flex" if flexbox_enabled() => Inside(DisplayInside::Flex), + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "flow-root" => Inside(DisplayInside::FlowRoot), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table" => Inside(DisplayInside::Table), + #[cfg(feature = "gecko")] + "grid" => Inside(DisplayInside::Grid), + #[cfg(feature = "gecko")] + "ruby" => Inside(DisplayInside::Ruby), + }) + } +} + +impl ToCss for Display { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + let outside = self.outside(); + let inside = self.inside(); + match *self { + Display::Block | Display::Inline => outside.to_css(dest), + Display::InlineBlock => dest.write_str("inline-block"), + #[cfg(feature = "gecko")] + Display::WebkitInlineBox => dest.write_str("-webkit-inline-box"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Display::TableCaption => dest.write_str("table-caption"), + _ => match (outside, inside) { + #[cfg(feature = "gecko")] + (DisplayOutside::Inline, DisplayInside::Grid) => dest.write_str("inline-grid"), + (DisplayOutside::Inline, DisplayInside::Flex) => dest.write_str("inline-flex"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + (DisplayOutside::Inline, DisplayInside::Table) => dest.write_str("inline-table"), + #[cfg(feature = "gecko")] + (DisplayOutside::Block, DisplayInside::Ruby) => dest.write_str("block ruby"), + (_, inside) => { + if self.is_list_item() { + if outside != DisplayOutside::Block { + outside.to_css(dest)?; + dest.write_char(' ')?; + } + if inside != DisplayInside::Flow { + inside.to_css(dest)?; + dest.write_char(' ')?; + } + dest.write_str("list-item") + } else { + inside.to_css(dest) + } + }, + }, + } + } +} + +impl Parse for Display { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Display, ParseError<'i>> { + let mut got_list_item = false; + let mut inside = None; + let mut outside = None; + match DisplayKeyword::parse(input)? { + DisplayKeyword::Full(d) => return Ok(d), + DisplayKeyword::Outside(o) => { + outside = Some(o); + }, + DisplayKeyword::Inside(i) => { + inside = Some(i); + }, + DisplayKeyword::ListItem => { + got_list_item = true; + }, + }; + + while let Ok(kw) = input.try_parse(DisplayKeyword::parse) { + match kw { + DisplayKeyword::ListItem if !got_list_item => { + got_list_item = true; + }, + DisplayKeyword::Outside(o) if outside.is_none() => { + outside = Some(o); + }, + DisplayKeyword::Inside(i) if inside.is_none() => { + inside = Some(i); + }, + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + let inside = inside.unwrap_or(DisplayInside::Flow); + let outside = outside.unwrap_or_else(|| inside.default_display_outside()); + if got_list_item && !inside.is_valid_for_list_item() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + return Ok(Display::from3(outside, inside, got_list_item)); + } +} + +impl SpecifiedValueInfo for Display { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&[ + "block", + "contents", + "flex", + "flow-root", + "flow-root list-item", + "grid", + "inline", + "inline-block", + "inline-flex", + "inline-grid", + "inline-table", + "inline list-item", + "inline flow-root list-item", + "list-item", + "none", + "block ruby", + "ruby", + "ruby-base", + "ruby-base-container", + "ruby-text", + "ruby-text-container", + "table", + "table-caption", + "table-cell", + "table-column", + "table-column-group", + "table-footer-group", + "table-header-group", + "table-row", + "table-row-group", + "-webkit-box", + "-webkit-inline-box", + ]); + } +} + +/// A specified value for the `contain-intrinsic-size` property. +pub type ContainIntrinsicSize = GenericContainIntrinsicSize<NonNegativeLength>; + +/// A specified value for the `line-clamp` property. +pub type LineClamp = GenericLineClamp<Integer>; + +/// A specified value for the `vertical-align` property. +pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>; + +impl Parse for VerticalAlign { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) + { + return Ok(GenericVerticalAlign::Length(lp)); + } + + Ok(GenericVerticalAlign::Keyword(VerticalAlignKeyword::parse( + input, + )?)) + } +} + +/// A specified value for the `baseline-source` property. +/// https://drafts.csswg.org/css-inline-3/#baseline-source +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToShmem, + ToComputedValue, + ToResolvedValue, +)] +#[repr(u8)] +pub enum BaselineSource { + /// `Last` for `inline-block`, `First` otherwise. + Auto, + /// Use first baseline for alignment. + First, + /// Use last baseline for alignment. + Last, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-axis +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAxis { + X, + Y, + Block, + Inline, + Both, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapStrictness { + #[css(skip)] + None, // Used to represent scroll-snap-type: none. It's not parsed. + Mandatory, + Proximity, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapType { + axis: ScrollSnapAxis, + strictness: ScrollSnapStrictness, +} + +impl ScrollSnapType { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + Self { + axis: ScrollSnapAxis::Both, + strictness: ScrollSnapStrictness::None, + } + } +} + +impl Parse for ScrollSnapType { + /// none | [ x | y | block | inline | both ] [ mandatory | proximity ]? + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(ScrollSnapType::none()); + } + + let axis = ScrollSnapAxis::parse(input)?; + let strictness = input + .try_parse(ScrollSnapStrictness::parse) + .unwrap_or(ScrollSnapStrictness::Proximity); + Ok(Self { axis, strictness }) + } +} + +impl ToCss for ScrollSnapType { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.strictness == ScrollSnapStrictness::None { + return dest.write_str("none"); + } + self.axis.to_css(dest)?; + if self.strictness != ScrollSnapStrictness::Proximity { + dest.write_char(' ')?; + self.strictness.to_css(dest)?; + } + Ok(()) + } +} + +/// Specified value of scroll-snap-align keyword value. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAlignKeyword { + None, + Start, + End, + Center, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapAlign { + block: ScrollSnapAlignKeyword, + inline: ScrollSnapAlignKeyword, +} + +impl ScrollSnapAlign { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + ScrollSnapAlign { + block: ScrollSnapAlignKeyword::None, + inline: ScrollSnapAlignKeyword::None, + } + } +} + +impl Parse for ScrollSnapAlign { + /// [ none | start | end | center ]{1,2} + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<ScrollSnapAlign, ParseError<'i>> { + let block = ScrollSnapAlignKeyword::parse(input)?; + let inline = input + .try_parse(ScrollSnapAlignKeyword::parse) + .unwrap_or(block); + Ok(ScrollSnapAlign { block, inline }) + } +} + +impl ToCss for ScrollSnapAlign { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.block.to_css(dest)?; + if self.block != self.inline { + dest.write_char(' ')?; + self.inline.to_css(dest)?; + } + Ok(()) + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapStop { + Normal, + Always, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverscrollBehavior { + Auto, + Contain, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowAnchor { + Auto, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowClipBox { + PaddingBox, + ContentBox, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma)] +#[repr(C)] +/// Provides a rendering hint to the user agent, stating what kinds of changes +/// the author expects to perform on the element. +/// +/// `auto` is represented by an empty `features` list. +/// +/// <https://drafts.csswg.org/css-will-change/#will-change> +pub struct WillChange { + /// The features that are supposed to change. + /// + /// TODO(emilio): Consider using ArcSlice since we just clone them from the + /// specified value? That'd save an allocation, which could be worth it. + #[css(iterable, if_empty = "auto")] + features: crate::OwnedSlice<CustomIdent>, + /// A bitfield with the kind of change that the value will create, based + /// on the above field. + #[css(skip)] + bits: WillChangeBits, +} + +impl WillChange { + #[inline] + /// Get default value of `will-change` as `auto` + pub fn auto() -> Self { + Self::default() + } +} + +/// The change bits that we care about. +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct WillChangeBits(u16); +bitflags! { + impl WillChangeBits: u16 { + /// Whether a property which can create a stacking context **on any + /// box** will change. + const STACKING_CONTEXT_UNCONDITIONAL = 1 << 0; + /// Whether `transform` or related properties will change. + const TRANSFORM = 1 << 1; + /// Whether `scroll-position` will change. + const SCROLL = 1 << 2; + /// Whether `contain` will change. + const CONTAIN = 1 << 3; + /// Whether `opacity` will change. + const OPACITY = 1 << 4; + /// Whether `perspective` will change. + const PERSPECTIVE = 1 << 5; + /// Whether `z-index` will change. + const Z_INDEX = 1 << 6; + /// Whether any property which creates a containing block for non-svg + /// text frames will change. + const FIXPOS_CB_NON_SVG = 1 << 7; + /// Whether the position property will change. + const POSITION = 1 << 8; + } +} + +fn change_bits_for_longhand(longhand: LonghandId) -> WillChangeBits { + match longhand { + LonghandId::Opacity => WillChangeBits::OPACITY, + LonghandId::Contain => WillChangeBits::CONTAIN, + LonghandId::Perspective => WillChangeBits::PERSPECTIVE, + LonghandId::Position => { + WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::POSITION + }, + LonghandId::ZIndex => WillChangeBits::Z_INDEX, + LonghandId::Transform | + LonghandId::TransformStyle | + LonghandId::Translate | + LonghandId::Rotate | + LonghandId::Scale | + LonghandId::OffsetPath => WillChangeBits::TRANSFORM, + LonghandId::BackdropFilter | LonghandId::Filter => { + WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::FIXPOS_CB_NON_SVG + }, + LonghandId::MixBlendMode | + LonghandId::Isolation | + LonghandId::MaskImage | + LonghandId::ClipPath => WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL, + _ => WillChangeBits::empty(), + } +} + +fn change_bits_for_maybe_property(ident: &str, context: &ParserContext) -> WillChangeBits { + let id = match PropertyId::parse_ignoring_rule_type(ident, context) { + Ok(id) => id, + Err(..) => return WillChangeBits::empty(), + }; + + match id.as_shorthand() { + Ok(shorthand) => shorthand + .longhands() + .fold(WillChangeBits::empty(), |flags, p| { + flags | change_bits_for_longhand(p) + }), + Err(PropertyDeclarationId::Longhand(longhand)) => change_bits_for_longhand(longhand), + Err(PropertyDeclarationId::Custom(..)) => WillChangeBits::empty(), + } +} + +impl Parse for WillChange { + /// auto | <animateable-feature># + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(Self::default()); + } + + let mut bits = WillChangeBits::empty(); + let custom_idents = input.parse_comma_separated(|i| { + let location = i.current_source_location(); + let parser_ident = i.expect_ident()?; + let ident = CustomIdent::from_ident( + location, + parser_ident, + &["will-change", "none", "all", "auto"], + )?; + + if context.in_ua_sheet() && ident.0 == atom!("-moz-fixed-pos-containing-block") { + bits |= WillChangeBits::FIXPOS_CB_NON_SVG; + } else if ident.0 == atom!("scroll-position") { + bits |= WillChangeBits::SCROLL; + } else { + bits |= change_bits_for_maybe_property(&parser_ident, context); + } + Ok(ident) + })?; + + Ok(Self { + features: custom_idents.into(), + bits, + }) + } +} + +/// Values for the `touch-action` property. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags(single = "none,auto,manipulation", mixed = "pan-x,pan-y,pinch-zoom"))] +#[repr(C)] +pub struct TouchAction(u8); +bitflags! { + impl TouchAction: u8 { + /// `none` variant + const NONE = 1 << 0; + /// `auto` variant + const AUTO = 1 << 1; + /// `pan-x` variant + const PAN_X = 1 << 2; + /// `pan-y` variant + const PAN_Y = 1 << 3; + /// `manipulation` variant + const MANIPULATION = 1 << 4; + /// `pinch-zoom` variant + const PINCH_ZOOM = 1 << 5; + } +} + +impl TouchAction { + #[inline] + /// Get default `touch-action` as `auto` + pub fn auto() -> TouchAction { + TouchAction::AUTO + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags( + single = "none,strict,content", + mixed = "size,layout,style,paint,inline-size", + overlapping_bits +))] +#[repr(C)] +/// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property +pub struct Contain(u8); +bitflags! { + impl Contain: u8 { + /// `none` variant, just for convenience. + const NONE = 0; + /// `inline-size` variant, turns on single-axis inline size containment + const INLINE_SIZE = 1 << 0; + /// `block-size` variant, turns on single-axis block size containment, internal only + const BLOCK_SIZE = 1 << 1; + /// `layout` variant, turns on layout containment + const LAYOUT = 1 << 2; + /// `style` variant, turns on style containment + const STYLE = 1 << 3; + /// `paint` variant, turns on paint containment + const PAINT = 1 << 4; + /// 'size' variant, turns on size containment + const SIZE = 1 << 5 | Contain::INLINE_SIZE.bits() | Contain::BLOCK_SIZE.bits(); + /// `content` variant, turns on layout and paint containment + const CONTENT = 1 << 6 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits(); + /// `strict` variant, turns on all types of containment + const STRICT = 1 << 7 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits() | Contain::SIZE.bits(); + } +} + +impl Parse for ContainIntrinsicSize { + /// none | <length> | auto <length> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(l) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + return Ok(Self::Length(l)); + } + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(Self::AutoNone); + } + + let l = NonNegativeLength::parse(context, input)?; + return Ok(Self::AutoLength(l)); + } + + input.expect_ident_matching("none")?; + Ok(Self::None) + } +} + +impl Parse for LineClamp { + /// none | <positive-integer> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(i) = + input.try_parse(|i| crate::values::specified::PositiveInteger::parse(context, i)) + { + return Ok(Self(i.0)); + } + input.expect_ident_matching("none")?; + Ok(Self::none()) + } +} + +/// https://drafts.csswg.org/css-contain-2/#content-visibility +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ContentVisibility { + /// `auto` variant, the element turns on layout containment, style containment, and paint + /// containment. In addition, if the element is not relevant to the user (such as by being + /// offscreen) it also skips its content + Auto, + /// `hidden` variant, the element skips its content + Hidden, + /// 'visible' variant, no effect + Visible, +} + +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + Parse, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +#[allow(missing_docs)] +/// https://drafts.csswg.org/css-contain-3/#container-type +pub enum ContainerType { + /// The `normal` variant. + Normal, + /// The `inline-size` variant. + InlineSize, + /// The `size` variant. + Size, +} + +impl ContainerType { + /// Is this container-type: normal? + pub fn is_normal(self) -> bool { + self == Self::Normal + } + + /// Is this type containing size in any way? + pub fn is_size_container_type(self) -> bool { + !self.is_normal() + } +} + +/// https://drafts.csswg.org/css-contain-3/#container-name +#[repr(transparent)] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct ContainerName(#[css(iterable, if_empty = "none")] pub crate::OwnedSlice<CustomIdent>); + +impl ContainerName { + /// Return the `none` value. + pub fn none() -> Self { + Self(Default::default()) + } + + /// Returns whether this is the `none` value. + pub fn is_none(&self) -> bool { + self.0.is_empty() + } + + fn parse_internal<'i>( + input: &mut Parser<'i, '_>, + for_query: bool, + ) -> Result<Self, ParseError<'i>> { + let mut idents = vec![]; + let location = input.current_source_location(); + let first = input.expect_ident()?; + if !for_query && first.eq_ignore_ascii_case("none") { + return Ok(Self::none()); + } + const DISALLOWED_CONTAINER_NAMES: &'static [&'static str] = &["none", "not", "or", "and"]; + idents.push(CustomIdent::from_ident( + location, + first, + DISALLOWED_CONTAINER_NAMES, + )?); + if !for_query { + while let Ok(name) = + input.try_parse(|input| CustomIdent::parse(input, DISALLOWED_CONTAINER_NAMES)) + { + idents.push(name); + } + } + Ok(ContainerName(idents.into())) + } + + /// https://github.com/w3c/csswg-drafts/issues/7203 + /// Only a single name allowed in @container rule. + /// Disallow none for container-name in @container rule. + pub fn parse_for_query<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(input, /* for_query = */ true) + } +} + +impl Parse for ContainerName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(input, /* for_query = */ false) + } +} + +/// A specified value for the `perspective` property. +pub type Perspective = GenericPerspective<NonNegativeLength>; + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css-box/#propdef-float +pub enum Float { + Left, + Right, + None, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css2/#propdef-clear +pub enum Clear { + None, + Left, + Right, + Both, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +/// https://drafts.csswg.org/css-ui/#propdef-resize +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum Resize { + None, + Both, + Horizontal, + Vertical, + // https://drafts.csswg.org/css-logical-1/#resize + Inline, + Block, +} + +/// The value for the `appearance` property. +/// +/// https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Appearance { + /// No appearance at all. + None, + /// Default appearance for the element. + /// + /// This value doesn't make sense for -moz-default-appearance, but we don't bother to guard + /// against parsing it. + Auto, + /// A searchfield. + Searchfield, + /// A multi-line text field, e.g. HTML <textarea>. + Textarea, + /// A checkbox element. + Checkbox, + /// A radio element within a radio group. + Radio, + /// A dropdown list. + Menulist, + /// List boxes. + Listbox, + /// A horizontal meter bar. + Meter, + /// A horizontal progress bar. + ProgressBar, + /// A typical dialog button. + Button, + /// A single-line text field, e.g. HTML <input type=text>. + Textfield, + /// The dropdown button(s) that open up a dropdown list. + MenulistButton, + /// Various arrows that go in buttons + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowDown, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowNext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowPrevious, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowUp, + /// A dual toolbar button (e.g., a Back button with a dropdown) + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Dualbutton, + /// Menu Popup background. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Menupopup, + /// The meter bar's meter indicator. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Meterchunk, + /// The "arrowed" part of the dropdown button that open up a dropdown list. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMenulistArrowButton, + /// For HTML's <input type=number> + #[parse(condition = "ParserContext::chrome_rules_enabled")] + NumberInput, + /// The progress bar's progress indicator + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Progresschunk, + /// nsRangeFrame and its subparts + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Range, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + RangeThumb, + /// The scrollbar slider + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarHorizontal, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarVertical, + /// A scrollbar button (up/down/left/right). + /// Keep these in order (some code casts these values to `int` in order to + /// compare them against each other). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonUp, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonDown, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonLeft, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonRight, + /// The scrollbar thumb. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarthumbHorizontal, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarthumbVertical, + /// The scrollbar track. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbartrackHorizontal, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbartrackVertical, + /// The scroll corner + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Scrollcorner, + /// A separator. Can be horizontal or vertical. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Separator, + /// A spin control (up/down control for time/date pickers). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Spinner, + /// The up button of a spin control. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + SpinnerUpbutton, + /// The down button of a spin control. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + SpinnerDownbutton, + /// The textfield of a spin control + #[parse(condition = "ParserContext::chrome_rules_enabled")] + SpinnerTextfield, + /// A splitter. Can be horizontal or vertical. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Splitter, + /// A status bar in a main application window. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Statusbar, + /// A single tab in a tab widget. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tab, + /// A single pane (inside the tabpanels container). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tabpanel, + /// The tab panels container. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tabpanels, + /// The tabs scroll arrows (left/right). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + TabScrollArrowBack, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + TabScrollArrowForward, + /// A toolbar in an application window. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Toolbar, + /// A single toolbar button (with no associated dropdown). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Toolbarbutton, + /// The dropdown portion of a toolbar button + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ToolbarbuttonDropdown, + /// The toolbox that contains the toolbars. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Toolbox, + /// A tooltip. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tooltip, + /// A listbox or tree widget header + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeheader, + /// An individual header cell + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeheadercell, + /// A tree item. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeitem, + /// A tree widget branch line + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeline, + /// A tree widget twisty. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treetwisty, + /// Open tree widget twisty. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treetwistyopen, + /// A tree widget. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeview, + + /// Mac help button. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacHelpButton, + + /// An appearance value for the root, so that we can get unified toolbar looks (which require a + /// transparent gecko background) without really using the whole transparency set-up which + /// otherwise loses window borders, see bug 1870481. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacUnifiedToolbarWindow, + + /// Windows themed window frame elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonBox, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonClose, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonMaximize, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonMinimize, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonRestore, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowTitlebar, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowTitlebarMaximized, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowDecorations, + + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDisclosureButtonClosed, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDisclosureButtonOpen, + + /// A themed focus outline (for outline:auto). + /// + /// This isn't exposed to CSS at all, just here for convenience. + #[css(skip)] + FocusOutline, + + /// A dummy variant that should be last to let the GTK widget do hackery. + #[css(skip)] + Count, +} + +/// A kind of break between two boxes. +/// +/// https://drafts.csswg.org/css-break/#break-between +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BreakBetween { + Always, + Auto, + Page, + Avoid, + Left, + Right, +} + +impl BreakBetween { + /// Parse a legacy break-between value for `page-break-{before,after}`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + #[inline] + pub(crate) fn parse_legacy<'i>( + _: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + let break_value = BreakBetween::parse(input)?; + match break_value { + BreakBetween::Always => Ok(BreakBetween::Page), + BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => { + Ok(break_value) + }, + BreakBetween::Page => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + } + + /// Serialize a legacy break-between value for `page-break-*`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => { + self.to_css(dest) + }, + BreakBetween::Page => dest.write_str("always"), + BreakBetween::Always => Ok(()), + } + } +} + +/// A kind of break within a box. +/// +/// https://drafts.csswg.org/css-break/#break-within +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BreakWithin { + Auto, + Avoid, + AvoidPage, + AvoidColumn, +} + +impl BreakWithin { + /// Parse a legacy break-between value for `page-break-inside`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + #[inline] + pub(crate) fn parse_legacy<'i>( + _: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + let break_value = BreakWithin::parse(input)?; + match break_value { + BreakWithin::Auto | BreakWithin::Avoid => Ok(break_value), + BreakWithin::AvoidPage | BreakWithin::AvoidColumn => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + } + + /// Serialize a legacy break-between value for `page-break-inside`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + BreakWithin::Auto | BreakWithin::Avoid => self.to_css(dest), + BreakWithin::AvoidPage | BreakWithin::AvoidColumn => Ok(()), + } + } +} + +/// The value for the `overflow-x` / `overflow-y` properties. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Overflow { + Visible, + Hidden, + Scroll, + Auto, + #[cfg(feature = "gecko")] + Clip, +} + +// This can be derived once we remove or keep `-moz-hidden-unscrollable` +// indefinitely. +impl Parse for Overflow { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "visible" => Self::Visible, + "hidden" => Self::Hidden, + "scroll" => Self::Scroll, + "auto" | "overlay" => Self::Auto, + #[cfg(feature = "gecko")] + "clip" => Self::Clip, + #[cfg(feature = "gecko")] + "-moz-hidden-unscrollable" if static_prefs::pref!("layout.css.overflow-moz-hidden-unscrollable.enabled") => { + Overflow::Clip + }, + }) + } +} + +impl Overflow { + /// Return true if the value will create a scrollable box. + #[inline] + pub fn is_scrollable(&self) -> bool { + matches!(*self, Self::Hidden | Self::Scroll | Self::Auto) + } + /// Convert the value to a scrollable value if it's not already scrollable. + /// This maps `visible` to `auto` and `clip` to `hidden`. + #[inline] + pub fn to_scrollable(&self) -> Self { + match *self { + Self::Hidden | Self::Scroll | Self::Auto => *self, + Self::Visible => Self::Auto, + #[cfg(feature = "gecko")] + Self::Clip => Self::Hidden, + } + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[css(bitflags( + single = "auto", + mixed = "stable,both-edges", + validate_mixed = "Self::has_stable" +))] +/// Values for scrollbar-gutter: +/// <https://drafts.csswg.org/css-overflow-3/#scrollbar-gutter-property> +pub struct ScrollbarGutter(u8); +bitflags! { + impl ScrollbarGutter: u8 { + /// `auto` variant. Just for convenience if there is no flag set. + const AUTO = 0; + /// `stable` variant. + const STABLE = 1 << 0; + /// `both-edges` variant. + const BOTH_EDGES = 1 << 1; + } +} + +impl ScrollbarGutter { + #[inline] + fn has_stable(&self) -> bool { + self.intersects(Self::STABLE) + } +} + +/// A specified value for the zoom property. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum Zoom { + Normal, + /// An internal value that resets the effective zoom to 1. Used for scrollbar parts, which + /// disregard zoom. We use this name because WebKit has this value exposed to the web. + #[parse(condition = "ParserContext::in_ua_sheet")] + Document, + Value(NonNegativeNumberOrPercentage), +} + +impl Zoom { + /// Return a particular number value of the zoom property. + #[inline] + pub fn new_number(n: f32) -> Self { + Self::Value(NonNegativeNumberOrPercentage::new_number(n)) + } +} diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs new file mode 100644 index 0000000000..2660864319 --- /dev/null +++ b/servo/components/style/values/specified/calc.rs @@ -0,0 +1,1086 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Calc expressions][calc]. +//! +//! [calc]: https://drafts.csswg.org/css-values/#calc-notation + +use crate::color::parsing::{AngleOrNumber, NumberOrPercentage}; +use crate::parser::ParserContext; +use crate::values::generics::calc::{ + self as generic, CalcNodeLeaf, CalcUnits, MinMaxOp, ModRemOp, PositivePercentageBasis, + RoundingStrategy, SortKey, +}; +use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength}; +use crate::values::specified::length::{ContainerRelativeLength, ViewportPercentageLength}; +use crate::values::specified::{self, Angle, Resolution, Time}; +use crate::values::{serialize_number, serialize_percentage, CSSFloat, CSSInteger}; +use cssparser::{CowRcStr, Parser, Token}; +use smallvec::SmallVec; +use std::cmp; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +/// The name of the mathematical function that we're parsing. +#[derive(Clone, Copy, Debug, Parse)] +pub enum MathFunction { + /// `calc()`: https://drafts.csswg.org/css-values-4/#funcdef-calc + Calc, + /// `min()`: https://drafts.csswg.org/css-values-4/#funcdef-min + Min, + /// `max()`: https://drafts.csswg.org/css-values-4/#funcdef-max + Max, + /// `clamp()`: https://drafts.csswg.org/css-values-4/#funcdef-clamp + Clamp, + /// `round()`: https://drafts.csswg.org/css-values-4/#funcdef-round + Round, + /// `mod()`: https://drafts.csswg.org/css-values-4/#funcdef-mod + Mod, + /// `rem()`: https://drafts.csswg.org/css-values-4/#funcdef-rem + Rem, + /// `sin()`: https://drafts.csswg.org/css-values-4/#funcdef-sin + Sin, + /// `cos()`: https://drafts.csswg.org/css-values-4/#funcdef-cos + Cos, + /// `tan()`: https://drafts.csswg.org/css-values-4/#funcdef-tan + Tan, + /// `asin()`: https://drafts.csswg.org/css-values-4/#funcdef-asin + Asin, + /// `acos()`: https://drafts.csswg.org/css-values-4/#funcdef-acos + Acos, + /// `atan()`: https://drafts.csswg.org/css-values-4/#funcdef-atan + Atan, + /// `atan2()`: https://drafts.csswg.org/css-values-4/#funcdef-atan2 + Atan2, + /// `pow()`: https://drafts.csswg.org/css-values-4/#funcdef-pow + Pow, + /// `sqrt()`: https://drafts.csswg.org/css-values-4/#funcdef-sqrt + Sqrt, + /// `hypot()`: https://drafts.csswg.org/css-values-4/#funcdef-hypot + Hypot, + /// `log()`: https://drafts.csswg.org/css-values-4/#funcdef-log + Log, + /// `exp()`: https://drafts.csswg.org/css-values-4/#funcdef-exp + Exp, + /// `abs()`: https://drafts.csswg.org/css-values-4/#funcdef-abs + Abs, + /// `sign()`: https://drafts.csswg.org/css-values-4/#funcdef-sign + Sign, +} + +/// A leaf node inside a `Calc` expression's AST. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum Leaf { + /// `<length>` + Length(NoCalcLength), + /// `<angle>` + Angle(Angle), + /// `<time>` + Time(Time), + /// `<resolution>` + Resolution(Resolution), + /// `<percentage>` + Percentage(CSSFloat), + /// `<number>` + Number(CSSFloat), +} + +impl Leaf { + fn as_length(&self) -> Option<&NoCalcLength> { + match *self { + Self::Length(ref l) => Some(l), + _ => None, + } + } +} + +impl ToCss for Leaf { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Self::Length(ref l) => l.to_css(dest), + Self::Number(n) => serialize_number(n, /* was_calc = */ false, dest), + Self::Resolution(ref r) => r.to_css(dest), + Self::Percentage(p) => serialize_percentage(p, dest), + Self::Angle(ref a) => a.to_css(dest), + Self::Time(ref t) => t.to_css(dest), + } + } +} + +/// A struct to hold a simplified `<length>` or `<percentage>` expression. +/// +/// In some cases, e.g. DOMMatrix, we support calc(), but reject all the +/// relative lengths, and to_computed_pixel_length_without_context() handles +/// this case. Therefore, if you want to add a new field, please make sure this +/// function work properly. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub struct CalcLengthPercentage { + #[css(skip)] + pub clamping_mode: AllowedNumericType, + pub node: CalcNode, +} + +impl CalcLengthPercentage { + fn same_unit_length_as(a: &Self, b: &Self) -> Option<(CSSFloat, CSSFloat)> { + debug_assert_eq!(a.clamping_mode, b.clamping_mode); + debug_assert_eq!(a.clamping_mode, AllowedNumericType::All); + + let a = a.node.as_leaf()?; + let b = b.node.as_leaf()?; + + if a.sort_key() != b.sort_key() { + return None; + } + + let a = a.as_length()?.unitless_value(); + let b = b.as_length()?.unitless_value(); + return Some((a, b)); + } +} + +impl SpecifiedValueInfo for CalcLengthPercentage {} + +impl generic::CalcNodeLeaf for Leaf { + fn unit(&self) -> CalcUnits { + match self { + Leaf::Length(_) => CalcUnits::LENGTH, + Leaf::Angle(_) => CalcUnits::ANGLE, + Leaf::Time(_) => CalcUnits::TIME, + Leaf::Resolution(_) => CalcUnits::RESOLUTION, + Leaf::Percentage(_) => CalcUnits::PERCENTAGE, + Leaf::Number(_) => CalcUnits::empty(), + } + } + + fn unitless_value(&self) -> f32 { + match *self { + Self::Length(ref l) => l.unitless_value(), + Self::Percentage(n) | Self::Number(n) => n, + Self::Resolution(ref r) => r.dppx(), + Self::Angle(ref a) => a.degrees(), + Self::Time(ref t) => t.seconds(), + } + } + + fn new_number(value: f32) -> Self { + Self::Number(value) + } + + fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<cmp::Ordering> { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) { + return None; + } + + let self_negative = self.is_negative(); + if self_negative != other.is_negative() { + return Some(if self_negative { cmp::Ordering::Less } else { cmp::Ordering::Greater }); + } + + match (self, other) { + (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other), + (&Length(ref one), &Length(ref other)) => one.partial_cmp(other), + (&Angle(ref one), &Angle(ref other)) => one.degrees().partial_cmp(&other.degrees()), + (&Time(ref one), &Time(ref other)) => one.seconds().partial_cmp(&other.seconds()), + (&Resolution(ref one), &Resolution(ref other)) => one.dppx().partial_cmp(&other.dppx()), + (&Number(ref one), &Number(ref other)) => one.partial_cmp(other), + _ => { + match *self { + Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) | + Resolution(..) => {}, + } + unsafe { + debug_unreachable!("Forgot a branch?"); + } + }, + } + } + + fn as_number(&self) -> Option<f32> { + match *self { + Leaf::Length(_) | + Leaf::Angle(_) | + Leaf::Time(_) | + Leaf::Resolution(_) | + Leaf::Percentage(_) => None, + Leaf::Number(value) => Some(value), + } + } + + fn sort_key(&self) -> SortKey { + match *self { + Self::Number(..) => SortKey::Number, + Self::Percentage(..) => SortKey::Percentage, + Self::Time(..) => SortKey::Sec, + Self::Resolution(..) => SortKey::Dppx, + Self::Angle(..) => SortKey::Deg, + Self::Length(ref l) => match *l { + NoCalcLength::Absolute(..) => SortKey::Px, + NoCalcLength::FontRelative(ref relative) => match *relative { + FontRelativeLength::Ch(..) => SortKey::Ch, + FontRelativeLength::Em(..) => SortKey::Em, + FontRelativeLength::Ex(..) => SortKey::Ex, + FontRelativeLength::Cap(..) => SortKey::Cap, + FontRelativeLength::Ic(..) => SortKey::Ic, + FontRelativeLength::Rem(..) => SortKey::Rem, + FontRelativeLength::Lh(..) => SortKey::Lh, + FontRelativeLength::Rlh(..) => SortKey::Rlh, + }, + NoCalcLength::ViewportPercentage(ref vp) => match *vp { + ViewportPercentageLength::Vh(..) => SortKey::Vh, + ViewportPercentageLength::Svh(..) => SortKey::Svh, + ViewportPercentageLength::Lvh(..) => SortKey::Lvh, + ViewportPercentageLength::Dvh(..) => SortKey::Dvh, + ViewportPercentageLength::Vw(..) => SortKey::Vw, + ViewportPercentageLength::Svw(..) => SortKey::Svw, + ViewportPercentageLength::Lvw(..) => SortKey::Lvw, + ViewportPercentageLength::Dvw(..) => SortKey::Dvw, + ViewportPercentageLength::Vmax(..) => SortKey::Vmax, + ViewportPercentageLength::Svmax(..) => SortKey::Svmax, + ViewportPercentageLength::Lvmax(..) => SortKey::Lvmax, + ViewportPercentageLength::Dvmax(..) => SortKey::Dvmax, + ViewportPercentageLength::Vmin(..) => SortKey::Vmin, + ViewportPercentageLength::Svmin(..) => SortKey::Svmin, + ViewportPercentageLength::Lvmin(..) => SortKey::Lvmin, + ViewportPercentageLength::Dvmin(..) => SortKey::Dvmin, + ViewportPercentageLength::Vb(..) => SortKey::Vb, + ViewportPercentageLength::Svb(..) => SortKey::Svb, + ViewportPercentageLength::Lvb(..) => SortKey::Lvb, + ViewportPercentageLength::Dvb(..) => SortKey::Dvb, + ViewportPercentageLength::Vi(..) => SortKey::Vi, + ViewportPercentageLength::Svi(..) => SortKey::Svi, + ViewportPercentageLength::Lvi(..) => SortKey::Lvi, + ViewportPercentageLength::Dvi(..) => SortKey::Dvi, + }, + NoCalcLength::ContainerRelative(ref cq) => match *cq { + ContainerRelativeLength::Cqw(..) => SortKey::Cqw, + ContainerRelativeLength::Cqh(..) => SortKey::Cqh, + ContainerRelativeLength::Cqi(..) => SortKey::Cqi, + ContainerRelativeLength::Cqb(..) => SortKey::Cqb, + ContainerRelativeLength::Cqmin(..) => SortKey::Cqmin, + ContainerRelativeLength::Cqmax(..) => SortKey::Cqmax, + }, + NoCalcLength::ServoCharacterWidth(..) => unreachable!(), + }, + } + } + + fn simplify(&mut self) { + if let Self::Length(NoCalcLength::Absolute(ref mut abs)) = *self { + *abs = AbsoluteLength::Px(abs.to_px()); + } + } + + /// Tries to merge one sum to another, that is, perform `x` + `y`. + /// + /// Only handles leaf nodes, it's the caller's responsibility to simplify + /// them before calling this if needed. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + match (self, other) { + (&mut Number(ref mut one), &Number(ref other)) | + (&mut Percentage(ref mut one), &Percentage(ref other)) => { + *one += *other; + }, + (&mut Angle(ref mut one), &Angle(ref other)) => { + *one = specified::Angle::from_calc(one.degrees() + other.degrees()); + }, + (&mut Time(ref mut one), &Time(ref other)) => { + *one = specified::Time::from_seconds(one.seconds() + other.seconds()); + }, + (&mut Resolution(ref mut one), &Resolution(ref other)) => { + *one = specified::Resolution::from_dppx(one.dppx() + other.dppx()); + }, + (&mut Length(ref mut one), &Length(ref other)) => { + *one = one.try_op(other, std::ops::Add::add)?; + }, + _ => { + match *other { + Number(..) | Percentage(..) | Angle(..) | Time(..) | Resolution(..) | + Length(..) => {}, + } + unsafe { + debug_unreachable!(); + } + }, + } + + Ok(()) + } + + fn try_product_in_place(&mut self, other: &mut Self) -> bool { + if let Self::Number(ref mut left) = *self { + if let Self::Number(ref right) = *other { + // Both sides are numbers, so we can just modify the left side. + *left *= *right; + true + } else { + // The right side is not a number, so the result should be in the units of the right + // side. + other.map(|v| v * *left); + std::mem::swap(self, other); + true + } + } else if let Self::Number(ref right) = *other { + // The left side is not a number, but the right side is, so the result is the left + // side unit. + self.map(|v| v * *right); + true + } else { + // Neither side is a number, so a product is not possible. + false + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + match (self, other) { + (&Number(one), &Number(other)) => { + return Ok(Leaf::Number(op(one, other))); + }, + (&Percentage(one), &Percentage(other)) => { + return Ok(Leaf::Percentage(op(one, other))); + }, + (&Angle(ref one), &Angle(ref other)) => { + return Ok(Leaf::Angle(specified::Angle::from_calc(op( + one.degrees(), + other.degrees(), + )))); + }, + (&Resolution(ref one), &Resolution(ref other)) => { + return Ok(Leaf::Resolution(specified::Resolution::from_dppx(op( + one.dppx(), + other.dppx(), + )))); + }, + (&Time(ref one), &Time(ref other)) => { + return Ok(Leaf::Time(specified::Time::from_seconds(op( + one.seconds(), + other.seconds(), + )))); + }, + (&Length(ref one), &Length(ref other)) => { + return Ok(Leaf::Length(one.try_op(other, op)?)); + }, + _ => { + match *other { + Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) | + Resolution(..) => {}, + } + unsafe { + debug_unreachable!(); + } + }, + } + } + + fn map(&mut self, mut op: impl FnMut(f32) -> f32) { + match self { + Leaf::Length(one) => *one = one.map(op), + Leaf::Angle(one) => *one = specified::Angle::from_calc(op(one.degrees())), + Leaf::Time(one) => *one = specified::Time::from_seconds(op(one.seconds())), + Leaf::Resolution(one) => *one = specified::Resolution::from_dppx(op(one.dppx())), + Leaf::Percentage(one) => *one = op(*one), + Leaf::Number(one) => *one = op(*one), + } + } +} + +/// A calc node representation for specified values. +pub type CalcNode = generic::GenericCalcNode<Leaf>; + +impl CalcNode { + /// Tries to parse a single element in the expression, that is, a + /// `<length>`, `<angle>`, `<time>`, `<percentage>`, `<resolution>`, etc. + /// + /// May return a "complex" `CalcNode`, in the presence of a parenthesized + /// expression, for example. + fn parse_one<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match input.next()? { + &Token::Number { value, .. } => Ok(CalcNode::Leaf(Leaf::Number(value))), + &Token::Dimension { + value, ref unit, .. + } => { + if allowed_units.intersects(CalcUnits::LENGTH) { + if let Ok(l) = NoCalcLength::parse_dimension(context, value, unit) { + return Ok(CalcNode::Leaf(Leaf::Length(l))); + } + } + if allowed_units.intersects(CalcUnits::ANGLE) { + if let Ok(a) = Angle::parse_dimension(value, unit, /* from_calc = */ true) { + return Ok(CalcNode::Leaf(Leaf::Angle(a))); + } + } + if allowed_units.intersects(CalcUnits::TIME) { + if let Ok(t) = Time::parse_dimension(value, unit) { + return Ok(CalcNode::Leaf(Leaf::Time(t))); + } + } + if allowed_units.intersects(CalcUnits::RESOLUTION) { + if let Ok(t) = Resolution::parse_dimension(value, unit) { + return Ok(CalcNode::Leaf(Leaf::Resolution(t))); + } + } + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }, + &Token::Percentage { unit_value, .. } + if allowed_units.intersects(CalcUnits::PERCENTAGE) => + { + Ok(CalcNode::Leaf(Leaf::Percentage(unit_value))) + }, + &Token::ParenthesisBlock => input.parse_nested_block(|input| { + CalcNode::parse_argument(context, input, allowed_units) + }), + &Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse(context, input, function, allowed_units) + }, + &Token::Ident(ref ident) => { + let number = match_ignore_ascii_case! { &**ident, + "e" => std::f32::consts::E, + "pi" => std::f32::consts::PI, + "infinity" => f32::INFINITY, + "-infinity" => f32::NEG_INFINITY, + "nan" => f32::NAN, + _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))), + }; + Ok(CalcNode::Leaf(Leaf::Number(number))) + }, + t => Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parse a top-level `calc` expression, with all nested sub-expressions. + /// + /// This is in charge of parsing, for example, `2 + 3 * 100%`. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + input.parse_nested_block(|input| { + match function { + MathFunction::Calc => Self::parse_argument(context, input, allowed_units), + MathFunction::Clamp => { + let min = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let center = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let max = Self::parse_argument(context, input, allowed_units)?; + Ok(Self::Clamp { + min: Box::new(min), + center: Box::new(center), + max: Box::new(max), + }) + }, + MathFunction::Round => { + let strategy = input.try_parse(parse_rounding_strategy); + + // <rounding-strategy> = nearest | up | down | to-zero + // https://drafts.csswg.org/css-values-4/#calc-syntax + fn parse_rounding_strategy<'i, 't>( + input: &mut Parser<'i, 't>, + ) -> Result<RoundingStrategy, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "nearest" => RoundingStrategy::Nearest, + "up" => RoundingStrategy::Up, + "down" => RoundingStrategy::Down, + "to-zero" => RoundingStrategy::ToZero, + }) + } + + if strategy.is_ok() { + input.expect_comma()?; + } + + let value = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let step = Self::parse_argument(context, input, allowed_units)?; + + Ok(Self::Round { + strategy: strategy.unwrap_or(RoundingStrategy::Nearest), + value: Box::new(value), + step: Box::new(step), + }) + }, + MathFunction::Mod | MathFunction::Rem => { + let dividend = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let divisor = Self::parse_argument(context, input, allowed_units)?; + + let op = match function { + MathFunction::Mod => ModRemOp::Mod, + MathFunction::Rem => ModRemOp::Rem, + _ => unreachable!(), + }; + Ok(Self::ModRem { + dividend: Box::new(dividend), + divisor: Box::new(divisor), + op, + }) + }, + MathFunction::Min | MathFunction::Max => { + // TODO(emilio): The common case for parse_comma_separated + // is just one element, but for min / max is two, really... + // + // Consider adding an API to cssparser to specify the + // initial vector capacity? + let arguments = input.parse_comma_separated(|input| { + Self::parse_argument(context, input, allowed_units) + })?; + + let op = match function { + MathFunction::Min => MinMaxOp::Min, + MathFunction::Max => MinMaxOp::Max, + _ => unreachable!(), + }; + + Ok(Self::MinMax(arguments.into(), op)) + }, + MathFunction::Sin | MathFunction::Cos | MathFunction::Tan => { + let a = Self::parse_angle_argument(context, input)?; + + let number = match function { + MathFunction::Sin => a.sin(), + MathFunction::Cos => a.cos(), + MathFunction::Tan => a.tan(), + _ => unsafe { + debug_unreachable!("We just checked!"); + }, + }; + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Asin | MathFunction::Acos | MathFunction::Atan => { + let a = Self::parse_number_argument(context, input)?; + + let radians = match function { + MathFunction::Asin => a.asin(), + MathFunction::Acos => a.acos(), + MathFunction::Atan => a.atan(), + _ => unsafe { + debug_unreachable!("We just checked!"); + }, + }; + + Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians)))) + }, + MathFunction::Atan2 => { + let a = Self::parse_argument(context, input, CalcUnits::ALL)?; + input.expect_comma()?; + let b = Self::parse_argument(context, input, CalcUnits::ALL)?; + + let radians = Self::try_resolve(input, || { + if let Ok(a) = a.to_number() { + let b = b.to_number()?; + return Ok(a.atan2(b)); + } + + if let Ok(a) = a.to_percentage() { + let b = b.to_percentage()?; + return Ok(a.atan2(b)); + } + + if let Ok(a) = a.to_time(None) { + let b = b.to_time(None)?; + return Ok(a.seconds().atan2(b.seconds())); + } + + if let Ok(a) = a.to_angle() { + let b = b.to_angle()?; + return Ok(a.radians().atan2(b.radians())); + } + + if let Ok(a) = a.to_resolution() { + let b = b.to_resolution()?; + return Ok(a.dppx().atan2(b.dppx())); + } + + let a = a.into_length_or_percentage(AllowedNumericType::All)?; + let b = b.into_length_or_percentage(AllowedNumericType::All)?; + let (a, b) = CalcLengthPercentage::same_unit_length_as(&a, &b).ok_or(())?; + + Ok(a.atan2(b)) + })?; + + Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians)))) + }, + MathFunction::Pow => { + let a = Self::parse_number_argument(context, input)?; + input.expect_comma()?; + let b = Self::parse_number_argument(context, input)?; + + let number = a.powf(b); + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Sqrt => { + let a = Self::parse_number_argument(context, input)?; + + let number = a.sqrt(); + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Hypot => { + let arguments = input.parse_comma_separated(|input| { + Self::parse_argument(context, input, allowed_units) + })?; + + Ok(Self::Hypot(arguments.into())) + }, + MathFunction::Log => { + let a = Self::parse_number_argument(context, input)?; + let b = input + .try_parse(|input| { + input.expect_comma()?; + Self::parse_number_argument(context, input) + }) + .ok(); + + let number = match b { + Some(b) => a.log(b), + None => a.ln(), + }; + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Exp => { + let a = Self::parse_number_argument(context, input)?; + let number = a.exp(); + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Abs => { + let node = Self::parse_argument(context, input, allowed_units)?; + Ok(Self::Abs(Box::new(node))) + }, + MathFunction::Sign => { + // The sign of a percentage is dependent on the percentage basis, so if + // percentages aren't allowed (so there's no basis) we shouldn't allow them in + // sign(). The rest of the units are safe tho. + let sign_units = allowed_units | (CalcUnits::ALL - CalcUnits::PERCENTAGE); + let node = Self::parse_argument(context, input, sign_units)?; + Ok(Self::Sign(Box::new(node))) + }, + } + }) + } + + fn parse_angle_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<CSSFloat, ParseError<'i>> { + let argument = Self::parse_argument(context, input, CalcUnits::ANGLE)?; + argument + .to_number() + .or_else(|()| Ok(argument.to_angle()?.radians())) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + fn parse_number_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse_argument(context, input, CalcUnits::empty())? + .to_number() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + fn parse_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + let mut sum = SmallVec::<[CalcNode; 1]>::new(); + sum.push(Self::parse_product(context, input, allowed_units)?); + + loop { + let start = input.state(); + match input.next_including_whitespace() { + Ok(&Token::WhiteSpace(_)) => { + if input.is_exhausted() { + break; // allow trailing whitespace + } + match *input.next()? { + Token::Delim('+') => { + sum.push(Self::parse_product(context, input, allowed_units)?); + }, + Token::Delim('-') => { + let mut rhs = Self::parse_product(context, input, allowed_units)?; + rhs.negate(); + sum.push(rhs); + }, + _ => { + input.reset(&start); + break; + }, + } + }, + _ => { + input.reset(&start); + break; + }, + } + } + + Ok(if sum.len() == 1 { + sum.drain(..).next().unwrap() + } else { + Self::Sum(sum.into_boxed_slice().into()) + }) + } + + /// Parse a top-level `calc` expression, and all the products that may + /// follow, and stop as soon as a non-product expression is found. + /// + /// This should parse correctly: + /// + /// * `2` + /// * `2 * 2` + /// * `2 * 2 + 2` (but will leave the `+ 2` unparsed). + /// + fn parse_product<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + let mut product = SmallVec::<[CalcNode; 1]>::new(); + product.push(Self::parse_one(context, input, allowed_units)?); + + loop { + let start = input.state(); + match input.next() { + Ok(&Token::Delim('*')) => { + let mut rhs = Self::parse_one(context, input, allowed_units)?; + + // We can unwrap here, becuase we start the function by adding a node to + // the list. + if !product.last_mut().unwrap().try_product_in_place(&mut rhs) { + product.push(rhs); + } + }, + Ok(&Token::Delim('/')) => { + let rhs = Self::parse_one(context, input, allowed_units)?; + + enum InPlaceDivisionResult { + /// The right was merged into the left. + Merged, + /// The right is not a number or could not be resolved, so the left is + /// unchanged. + Unchanged, + /// The right was resolved, but was not a number, so the calculation is + /// invalid. + Invalid, + } + + fn try_division_in_place( + left: &mut CalcNode, + right: &CalcNode, + ) -> InPlaceDivisionResult { + if let Ok(resolved) = right.resolve() { + if let Some(number) = resolved.as_number() { + if number != 1.0 && left.is_product_distributive() { + left.map(|l| l / number); + return InPlaceDivisionResult::Merged; + } + } else { + return InPlaceDivisionResult::Invalid; + } + } + InPlaceDivisionResult::Unchanged + } + + // The right hand side of a division *must* be a number, so if we can + // already resolve it, then merge it with the last node on the product list. + // We can unwrap here, becuase we start the function by adding a node to + // the list. + match try_division_in_place(product.last_mut().unwrap(), &rhs) { + InPlaceDivisionResult::Merged => {}, + InPlaceDivisionResult::Unchanged => { + product.push(Self::Invert(Box::new(rhs))) + }, + InPlaceDivisionResult::Invalid => { + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ) + }, + } + }, + _ => { + input.reset(&start); + break; + }, + } + } + + Ok(if product.len() == 1 { + product.drain(..).next().unwrap() + } else { + Self::Product(product.into_boxed_slice().into()) + }) + } + + fn try_resolve<'i, 't, F>( + input: &Parser<'i, 't>, + closure: F, + ) -> Result<CSSFloat, ParseError<'i>> + where + F: FnOnce() -> Result<CSSFloat, ()>, + { + closure().map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Tries to simplify this expression into a `<length>` or `<percentage>` + /// value. + pub fn into_length_or_percentage( + mut self, + clamping_mode: AllowedNumericType, + ) -> Result<CalcLengthPercentage, ()> { + self.simplify_and_sort(); + + // Although we allow numbers inside CalcLengthPercentage, calculations that resolve to a + // number result is still not allowed. + let unit = self.unit()?; + if !CalcUnits::LENGTH_PERCENTAGE.intersects(unit) { + Err(()) + } else { + Ok(CalcLengthPercentage { + clamping_mode, + node: self, + }) + } + } + + /// Tries to simplify this expression into a `<time>` value. + fn to_time(&self, clamping_mode: Option<AllowedNumericType>) -> Result<Time, ()> { + let seconds = if let Leaf::Time(time) = self.resolve()? { + time.seconds() + } else { + return Err(()); + }; + + Ok(Time::from_seconds_with_calc_clamping_mode( + seconds, + clamping_mode, + )) + } + + /// Tries to simplify the expression into a `<resolution>` value. + fn to_resolution(&self) -> Result<Resolution, ()> { + let dppx = if let Leaf::Resolution(resolution) = self.resolve()? { + resolution.dppx() + } else { + return Err(()); + }; + + Ok(Resolution::from_dppx_calc(dppx)) + } + + /// Tries to simplify this expression into an `Angle` value. + fn to_angle(&self) -> Result<Angle, ()> { + let degrees = if let Leaf::Angle(angle) = self.resolve()? { + angle.degrees() + } else { + return Err(()); + }; + + let result = Angle::from_calc(degrees); + Ok(result) + } + + /// Tries to simplify this expression into a `<number>` value. + fn to_number(&self) -> Result<CSSFloat, ()> { + let number = if let Leaf::Number(number) = self.resolve()? { + number + } else { + return Err(()); + }; + + let result = number; + + Ok(result) + } + + /// Tries to simplify this expression into a `<percentage>` value. + fn to_percentage(&self) -> Result<CSSFloat, ()> { + if let Leaf::Percentage(percentage) = self.resolve()? { + Ok(percentage) + } else { + Err(()) + } + } + + /// Given a function name, and the location from where the token came from, + /// return a mathematical function corresponding to that name or an error. + #[inline] + pub fn math_function<'i>( + _: &ParserContext, + name: &CowRcStr<'i>, + location: cssparser::SourceLocation, + ) -> Result<MathFunction, ParseError<'i>> { + let function = match MathFunction::from_ident(&*name) { + Ok(f) => f, + Err(()) => { + return Err(location.new_unexpected_token_error(Token::Function(name.clone()))) + }, + }; + + Ok(function) + } + + /// Convenience parsing function for integers. + pub fn parse_integer<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSInteger, ParseError<'i>> { + Self::parse_number(context, input, function).map(|n| (n + 0.5).floor() as CSSInteger) + } + + /// Convenience parsing function for `<length> | <percentage>`. + pub fn parse_length_or_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<CalcLengthPercentage, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::LENGTH_PERCENTAGE)? + .into_length_or_percentage(clamping_mode) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for percentages. + pub fn parse_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::PERCENTAGE)? + .to_percentage() + .map(crate::values::normalize) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<length>`. + pub fn parse_length<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<CalcLengthPercentage, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::LENGTH)? + .into_length_or_percentage(clamping_mode) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<number>`. + pub fn parse_number<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::empty())? + .to_number() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<angle>`. + pub fn parse_angle<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<Angle, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::ANGLE)? + .to_angle() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<time>`. + pub fn parse_time<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<Time, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::TIME)? + .to_time(Some(clamping_mode)) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<resolution>`. + pub fn parse_resolution<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<Resolution, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::RESOLUTION)? + .to_resolution() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<number>` or `<percentage>`. + pub fn parse_number_or_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<NumberOrPercentage, ParseError<'i>> { + let node = Self::parse(context, input, function, CalcUnits::PERCENTAGE)?; + + if let Ok(value) = node.to_number() { + return Ok(NumberOrPercentage::Number { value }); + } + + match node.to_percentage() { + Ok(unit_value) => Ok(NumberOrPercentage::Percentage { unit_value }), + Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + /// Convenience parsing function for `<number>` or `<angle>`. + pub fn parse_angle_or_number<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<AngleOrNumber, ParseError<'i>> { + let node = Self::parse(context, input, function, CalcUnits::ANGLE)?; + + if let Ok(angle) = node.to_angle() { + let degrees = angle.degrees(); + return Ok(AngleOrNumber::Angle { degrees }); + } + + match node.to_number() { + Ok(value) => Ok(AngleOrNumber::Number { value }), + Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs new file mode 100644 index 0000000000..3a19a2f4a3 --- /dev/null +++ b/servo/components/style/values/specified/color.rs @@ -0,0 +1,1175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified color values. + +use super::AllowQuirks; +use crate::color::parsing::{ + self, AngleOrNumber, Color as CSSParserColor, FromParsedColor, NumberOrPercentage, +}; +use crate::color::{mix::ColorInterpolationMethod, AbsoluteColor, ColorSpace}; +use crate::media_queries::Device; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; +use crate::values::generics::color::{ + ColorMixFlags, GenericCaretColor, GenericColorMix, GenericColorOrAuto, +}; +use crate::values::specified::calc::CalcNode; +use crate::values::specified::Percentage; +use crate::values::{normalize, CustomIdent}; +use cssparser::{color::PredefinedColorSpace, BasicParseErrorKind, ParseErrorKind, Parser, Token}; +use itoa; +use std::fmt::{self, Write}; +use std::io::Write as IoWrite; +use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind}; +use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; + +/// A specified color-mix(). +pub type ColorMix = GenericColorMix<Color, Percentage>; + +impl ColorMix { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + preserve_authored: PreserveAuthored, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("color-mix")?; + + input.parse_nested_block(|input| { + let interpolation = ColorInterpolationMethod::parse(context, input)?; + input.expect_comma()?; + + let try_parse_percentage = |input: &mut Parser| -> Option<Percentage> { + input + .try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input)) + .ok() + }; + + let mut left_percentage = try_parse_percentage(input); + + let left = Color::parse_internal(context, input, preserve_authored)?; + if left_percentage.is_none() { + left_percentage = try_parse_percentage(input); + } + + input.expect_comma()?; + + let mut right_percentage = try_parse_percentage(input); + + let right = Color::parse_internal(context, input, preserve_authored)?; + + if right_percentage.is_none() { + right_percentage = try_parse_percentage(input); + } + + let right_percentage = right_percentage + .unwrap_or_else(|| Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get()))); + + let left_percentage = + left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get())); + + if left_percentage.get() + right_percentage.get() <= 0.0 { + // If the percentages sum to zero, the function is invalid. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // Pass RESULT_IN_MODERN_SYNTAX here, because the result of the color-mix() function + // should always be in the modern color syntax to allow for out of gamut results and + // to preserve floating point precision. + Ok(ColorMix { + interpolation, + left, + left_percentage, + right, + right_percentage, + flags: ColorMixFlags::NORMALIZE_WEIGHTS | ColorMixFlags::RESULT_IN_MODERN_SYNTAX, + }) + }) + } +} + +/// Container holding an absolute color and the text specified by an author. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Absolute { + /// The specified color. + pub color: AbsoluteColor, + /// Authored representation. + pub authored: Option<Box<str>>, +} + +impl ToCss for Absolute { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if let Some(ref authored) = self.authored { + dest.write_str(authored) + } else { + self.color.to_css(dest) + } + } +} + +/// Specified color value +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum Color { + /// The 'currentColor' keyword + CurrentColor, + /// An absolute color. + /// https://w3c.github.io/csswg-drafts/css-color-4/#typedef-absolute-color-function + Absolute(Box<Absolute>), + /// A system color. + #[cfg(feature = "gecko")] + System(SystemColor), + /// A color mix. + ColorMix(Box<ColorMix>), + /// A light-dark() color. + LightDark(Box<LightDark>), + /// Quirksmode-only rule for inheriting color from the body + #[cfg(feature = "gecko")] + InheritFromBodyQuirk, +} + +/// A light-dark(<light-color>, <dark-color>) function. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, ToCss)] +#[css(function, comma)] +pub struct LightDark { + /// The <color> that is returned when using a light theme. + pub light: Color, + /// The <color> that is returned when using a dark theme. + pub dark: Color, +} + +impl LightDark { + fn compute(&self, cx: &Context) -> ComputedColor { + let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme(); + let dark = cx.device().is_dark_color_scheme(&style_color_scheme); + let used = if dark { &self.dark } else { &self.light }; + used.to_computed_value(cx) + } + + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + preserve_authored: PreserveAuthored, + ) -> Result<Self, ParseError<'i>> { + let enabled = + context.chrome_rules_enabled() || static_prefs::pref!("layout.css.light-dark.enabled"); + if !enabled { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + input.expect_function_matching("light-dark")?; + input.parse_nested_block(|input| { + let light = Color::parse_internal(context, input, preserve_authored)?; + input.expect_comma()?; + let dark = Color::parse_internal(context, input, preserve_authored)?; + Ok(LightDark { light, dark }) + }) + } +} + +impl From<AbsoluteColor> for Color { + #[inline] + fn from(value: AbsoluteColor) -> Self { + Self::from_absolute_color(value) + } +} + +/// System colors. A bunch of these are ad-hoc, others come from Windows: +/// +/// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor +/// +/// Others are HTML/CSS specific. Spec is: +/// +/// https://drafts.csswg.org/css-color/#css-system-colors +/// https://drafts.csswg.org/css-color/#deprecated-system-colors +#[allow(missing_docs)] +#[cfg(feature = "gecko")] +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum SystemColor { + Activeborder, + /// Background in the (active) titlebar. + Activecaption, + Appworkspace, + Background, + Buttonface, + Buttonhighlight, + Buttonshadow, + Buttontext, + Buttonborder, + /// Text color in the (active) titlebar. + Captiontext, + #[parse(aliases = "-moz-field")] + Field, + /// Used for disabled field backgrounds. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozDisabledfield, + #[parse(aliases = "-moz-fieldtext")] + Fieldtext, + + Mark, + Marktext, + + /// Combobox widgets + MozComboboxtext, + MozCombobox, + + Graytext, + Highlight, + Highlighttext, + Inactiveborder, + /// Background in the (inactive) titlebar. + Inactivecaption, + /// Text color in the (inactive) titlebar. + Inactivecaptiontext, + Infobackground, + Infotext, + Menu, + Menutext, + Scrollbar, + Threeddarkshadow, + Threedface, + Threedhighlight, + Threedlightshadow, + Threedshadow, + Window, + Windowframe, + Windowtext, + #[parse(aliases = "-moz-default-color")] + Canvastext, + #[parse(aliases = "-moz-default-background-color")] + Canvas, + MozDialog, + MozDialogtext, + /// Used for selected but not focused cell backgrounds. + #[parse(aliases = "-moz-html-cellhighlight")] + MozCellhighlight, + /// Used for selected but not focused cell text. + #[parse(aliases = "-moz-html-cellhighlighttext")] + MozCellhighlighttext, + /// Used for selected and focused html cell backgrounds. + Selecteditem, + /// Used for selected and focused html cell text. + Selecteditemtext, + /// Used to button text background when hovered. + MozButtonhoverface, + /// Used to button text color when hovered. + MozButtonhovertext, + /// Used for menu item backgrounds when hovered. + MozMenuhover, + /// Used for menu item backgrounds when hovered and disabled. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMenuhoverdisabled, + /// Used for menu item text when hovered. + MozMenuhovertext, + /// Used for menubar item text when hovered. + MozMenubarhovertext, + + /// On platforms where these colors are the same as -moz-field, use + /// -moz-fieldtext as foreground color + MozEventreerow, + MozOddtreerow, + + /// Used for button text when pressed. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButtonactivetext, + + /// Used for button background when pressed. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButtonactiveface, + + /// Used for button background when disabled. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButtondisabledface, + + /// Colors used for the header bar (sorta like the tab bar / menubar). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbar, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbartext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbarinactive, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbarinactivetext, + + /// Foreground color of default buttons. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDefaultbuttontext, + /// Ring color around text fields and lists. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacFocusring, + /// Text color of disabled text on toolbars. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDisabledtoolbartext, + /// The background of a sidebar. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozSidebar, + /// The foreground color of a sidebar. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozSidebartext, + /// The border color of a sidebar. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozSidebarborder, + + /// Theme accent color. + /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolor + Accentcolor, + + /// Foreground for the accent color. + /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolortext + Accentcolortext, + + /// The background-color for :autofill-ed inputs. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozAutofillBackground, + + /// Hyperlink color extracted from the system, not affected by the browser.anchor_color user + /// pref. + /// + /// There is no OS-specified safe background color for this text, but it is used regularly + /// within Windows and the Gnome DE on Dialog and Window colors. + #[css(skip)] + MozNativehyperlinktext, + + /// As above, but visited link color. + #[css(skip)] + MozNativevisitedhyperlinktext, + + #[parse(aliases = "-moz-hyperlinktext")] + Linktext, + #[parse(aliases = "-moz-activehyperlinktext")] + Activetext, + #[parse(aliases = "-moz-visitedhyperlinktext")] + Visitedtext, + + /// Color of tree column headers + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheader, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheadertext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderhover, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderhovertext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderactive, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderactivetext, + + #[parse(condition = "ParserContext::chrome_rules_enabled")] + TextSelectDisabledBackground, + #[css(skip)] + TextSelectAttentionBackground, + #[css(skip)] + TextSelectAttentionForeground, + #[css(skip)] + TextHighlightBackground, + #[css(skip)] + TextHighlightForeground, + #[css(skip)] + IMERawInputBackground, + #[css(skip)] + IMERawInputForeground, + #[css(skip)] + IMERawInputUnderline, + #[css(skip)] + IMESelectedRawTextBackground, + #[css(skip)] + IMESelectedRawTextForeground, + #[css(skip)] + IMESelectedRawTextUnderline, + #[css(skip)] + IMEConvertedTextBackground, + #[css(skip)] + IMEConvertedTextForeground, + #[css(skip)] + IMEConvertedTextUnderline, + #[css(skip)] + IMESelectedConvertedTextBackground, + #[css(skip)] + IMESelectedConvertedTextForeground, + #[css(skip)] + IMESelectedConvertedTextUnderline, + #[css(skip)] + SpellCheckerUnderline, + #[css(skip)] + ThemedScrollbar, + #[css(skip)] + ThemedScrollbarInactive, + #[css(skip)] + ThemedScrollbarThumb, + #[css(skip)] + ThemedScrollbarThumbHover, + #[css(skip)] + ThemedScrollbarThumbActive, + #[css(skip)] + ThemedScrollbarThumbInactive, + + #[css(skip)] + End, // Just for array-indexing purposes. +} + +#[cfg(feature = "gecko")] +impl SystemColor { + #[inline] + fn compute(&self, cx: &Context) -> ComputedColor { + use crate::gecko::values::convert_nscolor_to_absolute_color; + use crate::gecko_bindings::bindings; + + // TODO: We should avoid cloning here most likely, though it's cheap-ish. + let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme(); + let color = cx.device().system_nscolor(*self, &style_color_scheme); + if color == bindings::NS_SAME_AS_FOREGROUND_COLOR { + return ComputedColor::currentcolor(); + } + ComputedColor::Absolute(convert_nscolor_to_absolute_color(color)) + } +} + +impl FromParsedColor for Color { + fn from_current_color() -> Self { + Color::CurrentColor + } + + fn from_rgba(r: u8, g: u8, b: u8, a: f32) -> Self { + AbsoluteColor::srgb_legacy(r, g, b, a).into() + } + + fn from_hsl( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Hsl, hue, saturation, lightness, alpha).into() + } + + fn from_hwb( + hue: Option<f32>, + whiteness: Option<f32>, + blackness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Hwb, hue, whiteness, blackness, alpha).into() + } + + fn from_lab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Lab, lightness, a, b, alpha).into() + } + + fn from_lch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Lch, lightness, chroma, hue, alpha).into() + } + + fn from_oklab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Oklab, lightness, a, b, alpha).into() + } + + fn from_oklch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Oklch, lightness, chroma, hue, alpha).into() + } + + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(color_space.into(), c1, c2, c3, alpha).into() + } +} + +struct ColorParser<'a, 'b: 'a>(&'a ParserContext<'b>); +impl<'a, 'b: 'a, 'i: 'a> parsing::ColorParser<'i> for ColorParser<'a, 'b> { + type Output = Color; + type Error = StyleParseErrorKind<'i>; + + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<AngleOrNumber, ParseError<'i>> { + use crate::values::specified::Angle; + + let location = input.current_source_location(); + let token = input.next()?.clone(); + match token { + Token::Dimension { + value, ref unit, .. + } => { + let angle = Angle::parse_dimension(value, unit, /* from_calc = */ false); + + let degrees = match angle { + Ok(angle) => angle.degrees(), + Err(()) => return Err(location.new_unexpected_token_error(token.clone())), + }; + + Ok(AngleOrNumber::Angle { degrees }) + }, + Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }), + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + CalcNode::parse_angle_or_number(self.0, input, function) + }, + t => return Err(location.new_unexpected_token_error(t)), + } + } + + fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { + Ok(Percentage::parse(self.0, input)?.get()) + } + + fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { + use crate::values::specified::Number; + + Ok(Number::parse(self.0, input)?.get()) + } + + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<NumberOrPercentage, ParseError<'i>> { + let location = input.current_source_location(); + + match *input.next()? { + Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }), + Token::Percentage { unit_value, .. } => { + Ok(NumberOrPercentage::Percentage { unit_value }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + CalcNode::parse_number_or_percentage(self.0, input, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +/// Whether to preserve authored colors during parsing. That's useful only if we +/// plan to serialize the color back. +#[derive(Copy, Clone)] +enum PreserveAuthored { + No, + Yes, +} + +impl Parse for Color { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, PreserveAuthored::Yes) + } +} + +impl Color { + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + preserve_authored: PreserveAuthored, + ) -> Result<Self, ParseError<'i>> { + let authored = match preserve_authored { + PreserveAuthored::No => None, + PreserveAuthored::Yes => { + // Currently we only store authored value for color keywords, + // because all browsers serialize those values as keywords for + // specified value. + let start = input.state(); + let authored = input.expect_ident_cloned().ok(); + input.reset(&start); + authored + }, + }; + + let color_parser = ColorParser(&*context); + match input.try_parse(|i| parsing::parse_color_with(&color_parser, i)) { + Ok(mut color) => { + if let Color::Absolute(ref mut absolute) = color { + // Because we can't set the `authored` value at construction time, we have to set it + // here. + absolute.authored = authored.map(|s| s.to_ascii_lowercase().into_boxed_str()); + } + Ok(color) + }, + Err(e) => { + #[cfg(feature = "gecko")] + { + if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) { + return Ok(Color::System(system)); + } + } + + if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i, preserve_authored)) + { + return Ok(Color::ColorMix(Box::new(mix))); + } + + if let Ok(ld) = input.try_parse(|i| LightDark::parse(context, i, preserve_authored)) + { + return Ok(Color::LightDark(Box::new(ld))); + } + + match e.kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => { + Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( + ValueParseErrorKind::InvalidColor(t), + ))) + }, + _ => Err(e), + } + }, + } + } + + /// Returns whether a given color is valid for authors. + pub fn is_valid(context: &ParserContext, input: &mut Parser) -> bool { + input + .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No)) + .is_ok() + } + + /// Tries to parse a color and compute it with a given device. + pub fn parse_and_compute( + context: &ParserContext, + input: &mut Parser, + device: Option<&Device>, + ) -> Option<ComputedColor> { + use crate::error_reporting::ContextualParseError; + let start = input.position(); + let result = input + .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No)); + + let specified = match result { + Ok(s) => s, + Err(e) => { + if !context.error_reporting_enabled() { + return None; + } + // Ignore other kinds of errors that might be reported, such as + // ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken), + // since Gecko didn't use to report those to the error console. + // + // TODO(emilio): Revise whether we want to keep this at all, we + // use this only for canvas, this warnings are disabled by + // default and not available on OffscreenCanvas anyways... + if let ParseErrorKind::Custom(StyleParseErrorKind::ValueError(..)) = e.kind { + let location = e.location.clone(); + let error = ContextualParseError::UnsupportedValue(input.slice_from(start), e); + context.log_css_error(location, error); + } + return None; + }, + }; + + match device { + Some(device) => { + Context::for_media_query_evaluation(device, device.quirks_mode(), |context| { + specified.to_computed_color(Some(&context)) + }) + }, + None => specified.to_computed_color(None), + } + } +} + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Color::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest), + Color::Absolute(ref absolute) => absolute.to_css(dest), + Color::ColorMix(ref mix) => mix.to_css(dest), + Color::LightDark(ref ld) => ld.to_css(dest), + #[cfg(feature = "gecko")] + Color::System(system) => system.to_css(dest), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => Ok(()), + } + } +} + +impl Color { + /// Returns whether this color is allowed in forced-colors mode. + pub fn honored_in_forced_colors_mode(&self, allow_transparent: bool) -> bool { + match *self { + Self::InheritFromBodyQuirk => false, + Self::CurrentColor | Color::System(..) => true, + Self::Absolute(ref absolute) => allow_transparent && absolute.color.is_transparent(), + Self::LightDark(ref ld) => { + ld.light.honored_in_forced_colors_mode(allow_transparent) && + ld.dark.honored_in_forced_colors_mode(allow_transparent) + }, + Self::ColorMix(ref mix) => { + mix.left.honored_in_forced_colors_mode(allow_transparent) && + mix.right.honored_in_forced_colors_mode(allow_transparent) + }, + } + } + + /// Returns currentcolor value. + #[inline] + pub fn currentcolor() -> Self { + Self::CurrentColor + } + + /// Returns transparent value. + #[inline] + pub fn transparent() -> Self { + // We should probably set authored to "transparent", but maybe it doesn't matter. + Self::from_absolute_color(AbsoluteColor::TRANSPARENT_BLACK) + } + + /// Create a color from an [`AbsoluteColor`]. + pub fn from_absolute_color(color: AbsoluteColor) -> Self { + Color::Absolute(Box::new(Absolute { + color, + authored: None, + })) + } + + /// Parse a color, with quirks. + /// + /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk> + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| Self::parse(context, i)).or_else(|e| { + if !allow_quirks.allowed(context.quirks_mode) { + return Err(e); + } + Color::parse_quirky_color(input).map_err(|_| e) + }) + } + + fn parse_hash<'i>( + bytes: &[u8], + loc: &cssparser::SourceLocation, + ) -> Result<Self, ParseError<'i>> { + match cssparser::color::parse_hash_color(bytes) { + Ok((r, g, b, a)) => Ok(Self::from_rgba(r, g, b, a)), + Err(()) => Err(loc.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + /// Parse a <quirky-color> value. + /// + /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk> + fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let (value, unit) = match *input.next()? { + Token::Number { + int_value: Some(integer), + .. + } => (integer, None), + Token::Dimension { + int_value: Some(integer), + ref unit, + .. + } => (integer, Some(unit)), + Token::Ident(ref ident) => { + if ident.len() != 3 && ident.len() != 6 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Self::parse_hash(ident.as_bytes(), &location); + }, + ref t => { + return Err(location.new_unexpected_token_error(t.clone())); + }, + }; + if value < 0 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + let length = if value <= 9 { + 1 + } else if value <= 99 { + 2 + } else if value <= 999 { + 3 + } else if value <= 9999 { + 4 + } else if value <= 99999 { + 5 + } else if value <= 999999 { + 6 + } else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }; + let total = length + unit.as_ref().map_or(0, |d| d.len()); + if total > 6 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + let mut serialization = [b'0'; 6]; + let space_padding = 6 - total; + let mut written = space_padding; + let mut buf = itoa::Buffer::new(); + let s = buf.format(value); + (&mut serialization[written..]) + .write_all(s.as_bytes()) + .unwrap(); + written += s.len(); + if let Some(unit) = unit { + written += (&mut serialization[written..]) + .write(unit.as_bytes()) + .unwrap(); + } + debug_assert_eq!(written, 6); + Self::parse_hash(&serialization, &location) + } +} + +impl Color { + /// Converts this Color into a ComputedColor. + /// + /// If `context` is `None`, and the specified color requires data from + /// the context to resolve, then `None` is returned. + pub fn to_computed_color(&self, context: Option<&Context>) -> Option<ComputedColor> { + Some(match *self { + Color::CurrentColor => ComputedColor::CurrentColor, + Color::Absolute(ref absolute) => { + let mut color = absolute.color; + + // Computed lightness values can not be NaN. + if matches!( + color.color_space, + ColorSpace::Lab | ColorSpace::Oklab | ColorSpace::Lch | ColorSpace::Oklch + ) { + color.components.0 = normalize(color.components.0); + } + + // Computed RGB and XYZ components can not be NaN. + if !color.is_legacy_syntax() && color.color_space.is_rgb_or_xyz_like() { + color.components = color.components.map(normalize); + } + + color.alpha = normalize(color.alpha); + + ComputedColor::Absolute(color) + }, + Color::LightDark(ref ld) => ld.compute(context?), + Color::ColorMix(ref mix) => { + use crate::values::computed::percentage::Percentage; + + let left = mix.left.to_computed_color(context)?; + let right = mix.right.to_computed_color(context)?; + + ComputedColor::from_color_mix(GenericColorMix { + interpolation: mix.interpolation, + left, + left_percentage: Percentage(mix.left_percentage.get()), + right, + right_percentage: Percentage(mix.right_percentage.get()), + flags: mix.flags, + }) + }, + #[cfg(feature = "gecko")] + Color::System(system) => system.compute(context?), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => { + ComputedColor::Absolute(context?.device().body_text_color()) + }, + }) + } +} + +impl ToComputedValue for Color { + type ComputedValue = ComputedColor; + + fn to_computed_value(&self, context: &Context) -> ComputedColor { + self.to_computed_color(Some(context)).unwrap() + } + + fn from_computed_value(computed: &ComputedColor) -> Self { + match *computed { + ComputedColor::Absolute(ref color) => Self::from_absolute_color(color.clone()), + ComputedColor::CurrentColor => Color::CurrentColor, + ComputedColor::ColorMix(ref mix) => { + Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix))) + }, + } + } +} + +impl SpecifiedValueInfo for Color { + const SUPPORTED_TYPES: u8 = CssType::COLOR; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + // We are not going to insert all the color names here. Caller and + // devtools should take care of them. XXX Actually, transparent + // should probably be handled that way as well. + // XXX `currentColor` should really be `currentcolor`. But let's + // keep it consistent with the old system for now. + f(&[ + "rgb", + "rgba", + "hsl", + "hsla", + "hwb", + "currentColor", + "transparent", + "color-mix", + "color", + "lab", + "lch", + "oklab", + "oklch", + ]); + } +} + +/// Specified value for the "color" property, which resolves the `currentcolor` +/// keyword to the parent color instead of self's color. +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct ColorPropertyValue(pub Color); + +impl ToComputedValue for ColorPropertyValue { + type ComputedValue = AbsoluteColor; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let current_color = context.builder.get_parent_inherited_text().clone_color(); + self.0 + .to_computed_value(context) + .resolve_to_absolute(¤t_color) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + ColorPropertyValue(Color::from_absolute_color(*computed).into()) + } +} + +impl Parse for ColorPropertyValue { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Color::parse_quirky(context, input, AllowQuirks::Yes).map(ColorPropertyValue) + } +} + +/// auto | <color> +pub type ColorOrAuto = GenericColorOrAuto<Color>; + +/// caret-color +pub type CaretColor = GenericCaretColor<Color>; + +impl Parse for CaretColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + ColorOrAuto::parse(context, input).map(GenericCaretColor) + } +} + +/// Various flags to represent the color-scheme property in an efficient +/// way. +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[value_info(other_values = "light,dark,only")] +pub struct ColorSchemeFlags(u8); +bitflags! { + impl ColorSchemeFlags: u8 { + /// Whether the author specified `light`. + const LIGHT = 1 << 0; + /// Whether the author specified `dark`. + const DARK = 1 << 1; + /// Whether the author specified `only`. + const ONLY = 1 << 2; + } +} + +/// <https://drafts.csswg.org/css-color-adjust/#color-scheme-prop> +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[value_info(other_values = "normal")] +pub struct ColorScheme { + #[ignore_malloc_size_of = "Arc"] + idents: crate::ArcSlice<CustomIdent>, + bits: ColorSchemeFlags, +} + +impl ColorScheme { + /// Returns the `normal` value. + pub fn normal() -> Self { + Self { + idents: Default::default(), + bits: ColorSchemeFlags::empty(), + } + } + + /// Returns the raw bitfield. + pub fn raw_bits(&self) -> u8 { + self.bits.bits() + } +} + +impl Parse for ColorScheme { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut idents = vec![]; + let mut bits = ColorSchemeFlags::empty(); + + let mut location = input.current_source_location(); + while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { + let mut is_only = false; + match_ignore_ascii_case! { &ident, + "normal" => { + if idents.is_empty() && bits.is_empty() { + return Ok(Self::normal()); + } + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }, + "light" => bits.insert(ColorSchemeFlags::LIGHT), + "dark" => bits.insert(ColorSchemeFlags::DARK), + "only" => { + if bits.intersects(ColorSchemeFlags::ONLY) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + bits.insert(ColorSchemeFlags::ONLY); + is_only = true; + }, + _ => {}, + }; + + if is_only { + if !idents.is_empty() { + // Only is allowed either at the beginning or at the end, + // but not in the middle. + break; + } + } else { + idents.push(CustomIdent::from_ident(location, &ident, &[])?); + } + location = input.current_source_location(); + } + + if idents.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(Self { + idents: crate::ArcSlice::from_iter(idents.into_iter()), + bits, + }) + } +} + +impl ToCss for ColorScheme { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.idents.is_empty() { + debug_assert!(self.bits.is_empty()); + return dest.write_str("normal"); + } + let mut first = true; + for ident in self.idents.iter() { + if !first { + dest.write_char(' ')?; + } + first = false; + ident.to_css(dest)?; + } + if self.bits.intersects(ColorSchemeFlags::ONLY) { + dest.write_str(" only")?; + } + Ok(()) + } +} + +/// https://drafts.csswg.org/css-color-adjust/#print-color-adjust +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum PrintColorAdjust { + /// Ignore backgrounds and darken text. + Economy, + /// Respect specified colors. + Exact, +} + +/// https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ForcedColorAdjust { + /// Adjust colors if needed. + Auto, + /// Respect specified colors. + None, +} diff --git a/servo/components/style/values/specified/column.rs b/servo/components/style/values/specified/column.rs new file mode 100644 index 0000000000..2dd7bb0144 --- /dev/null +++ b/servo/components/style/values/specified/column.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for the column properties. + +use crate::values::generics::column::ColumnCount as GenericColumnCount; +use crate::values::specified::PositiveInteger; + +/// A specified type for `column-count` values. +pub type ColumnCount = GenericColumnCount<PositiveInteger>; diff --git a/servo/components/style/values/specified/counters.rs b/servo/components/style/values/specified/counters.rs new file mode 100644 index 0000000000..9d8261ce6c --- /dev/null +++ b/servo/components/style/values/specified/counters.rs @@ -0,0 +1,279 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for counter properties. + +#[cfg(feature = "servo-layout-2013")] +use crate::computed_values::list_style_type::T as ListStyleType; +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::counters as generics; +use crate::values::generics::counters::CounterPair; +#[cfg(feature = "gecko")] +use crate::values::generics::CounterStyle; +use crate::values::specified::image::Image; +#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] +use crate::values::specified::Attr; +use crate::values::specified::Integer; +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +#[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +#[derive(PartialEq)] +enum CounterType { + Increment, + Set, + Reset, +} + +impl CounterType { + fn default_value(&self) -> i32 { + match *self { + Self::Increment => 1, + Self::Reset | Self::Set => 0, + } + } +} + +/// A specified value for the `counter-increment` property. +pub type CounterIncrement = generics::GenericCounterIncrement<Integer>; + +impl Parse for CounterIncrement { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters( + context, + input, + CounterType::Increment, + )?)) + } +} + +/// A specified value for the `counter-set` property. +pub type CounterSet = generics::GenericCounterSet<Integer>; + +impl Parse for CounterSet { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters(context, input, CounterType::Set)?)) + } +} + +/// A specified value for the `counter-reset` property. +pub type CounterReset = generics::GenericCounterReset<Integer>; + +impl Parse for CounterReset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters( + context, + input, + CounterType::Reset, + )?)) + } +} + +fn parse_counters<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + counter_type: CounterType, +) -> Result<Vec<CounterPair<Integer>>, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(vec![]); + } + + let mut counters = Vec::new(); + loop { + let location = input.current_source_location(); + let (name, is_reversed) = match input.next() { + Ok(&Token::Ident(ref ident)) => { + (CustomIdent::from_ident(location, ident, &["none"])?, false) + }, + Ok(&Token::Function(ref name)) + if counter_type == CounterType::Reset && name.eq_ignore_ascii_case("reversed") => + { + input + .parse_nested_block(|input| Ok((CustomIdent::parse(input, &["none"])?, true)))? + }, + Ok(t) => { + let t = t.clone(); + return Err(location.new_unexpected_token_error(t)); + }, + Err(_) => break, + }; + + let value = match input.try_parse(|input| Integer::parse(context, input)) { + Ok(start) => { + if start.value == i32::min_value() { + // The spec says that values must be clamped to the valid range, + // and we reserve i32::min_value() as an internal magic value. + // https://drafts.csswg.org/css-lists/#auto-numbering + Integer::new(i32::min_value() + 1) + } else { + start + } + }, + _ => Integer::new(if is_reversed { + i32::min_value() + } else { + counter_type.default_value() + }), + }; + counters.push(CounterPair { + name, + value, + is_reversed, + }); + } + + if !counters.is_empty() { + Ok(counters) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// The specified value for the `content` property. +pub type Content = generics::GenericContent<Image>; + +/// The specified value for a content item in the `content` property. +pub type ContentItem = generics::GenericContentItem<Image>; + +impl Content { + #[cfg(feature = "servo-layout-2013")] + fn parse_counter_style(_: &ParserContext, input: &mut Parser) -> ListStyleType { + input + .try_parse(|input| { + input.expect_comma()?; + ListStyleType::parse(input) + }) + .unwrap_or(ListStyleType::Decimal) + } + + #[cfg(feature = "gecko")] + fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyle { + input + .try_parse(|input| { + input.expect_comma()?; + CounterStyle::parse(context, input) + }) + .unwrap_or(CounterStyle::decimal()) + } +} + +impl Parse for Content { + // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote | + // no-close-quote ]+ + // TODO: <uri>, attr(<identifier>) + #[cfg_attr(feature = "servo", allow(unused_mut))] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(generics::Content::Normal); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(generics::Content::None); + } + + let mut content = vec![]; + let mut has_alt_content = false; + loop { + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + { + if let Ok(image) = input.try_parse(|i| Image::parse_forbid_none(context, i)) { + content.push(generics::ContentItem::Image(image)); + continue; + } + } + match input.next() { + Ok(&Token::QuotedString(ref value)) => { + content.push(generics::ContentItem::String( + value.as_ref().to_owned().into(), + )); + }, + Ok(&Token::Function(ref name)) => { + let result = match_ignore_ascii_case! { &name, + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + "counter" => input.parse_nested_block(|input| { + let name = CustomIdent::parse(input, &[])?; + let style = Content::parse_counter_style(context, input); + Ok(generics::ContentItem::Counter(name, style)) + }), + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + "counters" => input.parse_nested_block(|input| { + let name = CustomIdent::parse(input, &[])?; + input.expect_comma()?; + let separator = input.expect_string()?.as_ref().to_owned().into(); + let style = Content::parse_counter_style(context, input); + Ok(generics::ContentItem::Counters(name, separator, style)) + }), + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + "attr" => input.parse_nested_block(|input| { + Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?)) + }), + _ => { + use style_traits::StyleParseErrorKind; + let name = name.clone(); + return Err(input.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(name), + )) + } + }?; + content.push(result); + }, + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + Ok(&Token::Ident(ref ident)) => { + content.push(match_ignore_ascii_case! { &ident, + "open-quote" => generics::ContentItem::OpenQuote, + "close-quote" => generics::ContentItem::CloseQuote, + "no-open-quote" => generics::ContentItem::NoOpenQuote, + "no-close-quote" => generics::ContentItem::NoCloseQuote, + #[cfg(feature = "gecko")] + "-moz-alt-content" => { + has_alt_content = true; + generics::ContentItem::MozAltContent + }, + "-moz-label-content" if context.chrome_rules_enabled() => { + generics::ContentItem::MozLabelContent + }, + _ =>{ + let ident = ident.clone(); + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident) + )); + } + }); + }, + Err(_) => break, + Ok(t) => { + let t = t.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + } + } + // We don't allow to parse `-moz-alt-content` in multiple positions. + if content.is_empty() || (has_alt_content && content.len() != 1) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(generics::Content::Items(content.into())) + } +} diff --git a/servo/components/style/values/specified/easing.rs b/servo/components/style/values/specified/easing.rs new file mode 100644 index 0000000000..5e4d8ae1ea --- /dev/null +++ b/servo/components/style/values/specified/easing.rs @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS Easing functions. +use crate::parser::{Parse, ParserContext}; +use crate::piecewise_linear::{PiecewiseLinearFunction, PiecewiseLinearFunctionBuilder}; +use crate::values::computed::easing::TimingFunction as ComputedTimingFunction; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::easing::TimingFunction as GenericTimingFunction; +use crate::values::generics::easing::{StepPosition, TimingKeyword}; +use crate::values::specified::{Integer, Number, Percentage}; +use cssparser::{Delimiter, Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified timing function. +pub type TimingFunction = GenericTimingFunction<Integer, Number, PiecewiseLinearFunction>; + +impl Parse for TimingFunction { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(keyword) = input.try_parse(TimingKeyword::parse) { + return Ok(GenericTimingFunction::Keyword(keyword)); + } + if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { + let position = match_ignore_ascii_case! { &ident, + "step-start" => StepPosition::Start, + "step-end" => StepPosition::End, + _ => { + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )); + }, + }; + return Ok(GenericTimingFunction::Steps(Integer::new(1), position)); + } + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "cubic-bezier" => Self::parse_cubic_bezier(context, i), + "steps" => Self::parse_steps(context, i), + "linear" => Self::parse_linear_function(context, i), + _ => Err(location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))), + } + }) + } +} + +impl TimingFunction { + fn parse_cubic_bezier<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let x1 = Number::parse(context, input)?; + input.expect_comma()?; + let y1 = Number::parse(context, input)?; + input.expect_comma()?; + let x2 = Number::parse(context, input)?; + input.expect_comma()?; + let y2 = Number::parse(context, input)?; + + if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 }) + } + + fn parse_steps<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let steps = Integer::parse_positive(context, input)?; + let position = input + .try_parse(|i| { + i.expect_comma()?; + StepPosition::parse(context, i) + }) + .unwrap_or(StepPosition::End); + + // jump-none accepts a positive integer greater than 1. + // FIXME(emilio): The spec asks us to avoid rejecting it at parse + // time except until computed value time. + // + // It's not totally clear it's worth it though, and no other browser + // does this. + if position == StepPosition::JumpNone && steps.value() <= 1 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(GenericTimingFunction::Steps(steps, position)) + } + + fn parse_linear_function<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut builder = PiecewiseLinearFunctionBuilder::default(); + let mut num_specified_stops = 0; + // Closely follows `parse_comma_separated`, but can generate multiple entries for one comma-separated entry. + loop { + input.parse_until_before(Delimiter::Comma, |i| { + let builder = &mut builder; + let mut input_start = i.try_parse(|i| Percentage::parse(context, i)).ok(); + let mut input_end = i.try_parse(|i| Percentage::parse(context, i)).ok(); + + let output = Number::parse(context, i)?; + if input_start.is_none() { + debug_assert!(input_end.is_none(), "Input end parsed without input start?"); + input_start = i.try_parse(|i| Percentage::parse(context, i)).ok(); + input_end = i.try_parse(|i| Percentage::parse(context, i)).ok(); + } + builder.push(output.into(), input_start.map(|v| v.get()).into()); + num_specified_stops += 1; + if input_end.is_some() { + debug_assert!( + input_start.is_some(), + "Input end valid but not input start?" + ); + builder.push(output.into(), input_end.map(|v| v.get()).into()); + } + + Ok(()) + })?; + + match input.next() { + Err(_) => break, + Ok(&Token::Comma) => continue, + Ok(_) => unreachable!(), + } + } + // By spec, specifying only a single stop makes the function invalid, even if that single entry may generate + // two entries. + if num_specified_stops < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(GenericTimingFunction::LinearFunction(builder.build())) + } +} + +// We need this for converting the specified TimingFunction into computed TimingFunction without +// Context (for some FFIs in glue.rs). In fact, we don't really need Context to get the computed +// value of TimingFunction. +impl TimingFunction { + /// Generate the ComputedTimingFunction without Context. + pub fn to_computed_value_without_context(&self) -> ComputedTimingFunction { + match &self { + GenericTimingFunction::Steps(steps, pos) => { + GenericTimingFunction::Steps(steps.value(), *pos) + }, + GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => { + GenericTimingFunction::CubicBezier { + x1: x1.get(), + y1: y1.get(), + x2: x2.get(), + y2: y2.get(), + } + }, + GenericTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword), + GenericTimingFunction::LinearFunction(function) => { + GenericTimingFunction::LinearFunction(function.clone()) + }, + } + } +} + +impl ToComputedValue for TimingFunction { + type ComputedValue = ComputedTimingFunction; + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + self.to_computed_value_without_context() + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match &computed { + ComputedTimingFunction::Steps(steps, pos) => Self::Steps(Integer::new(*steps), *pos), + ComputedTimingFunction::CubicBezier { x1, y1, x2, y2 } => Self::CubicBezier { + x1: Number::new(*x1), + y1: Number::new(*y1), + x2: Number::new(*x2), + y2: Number::new(*y2), + }, + ComputedTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword), + ComputedTimingFunction::LinearFunction(function) => { + GenericTimingFunction::LinearFunction(function.clone()) + }, + } + } +} diff --git a/servo/components/style/values/specified/effects.rs b/servo/components/style/values/specified/effects.rs new file mode 100644 index 0000000000..0453582768 --- /dev/null +++ b/servo/components/style/values/specified/effects.rs @@ -0,0 +1,453 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to effects. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::effects::BoxShadow as ComputedBoxShadow; +use crate::values::computed::effects::SimpleShadow as ComputedSimpleShadow; +#[cfg(feature = "gecko")] +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::Angle as ComputedAngle; +use crate::values::computed::CSSPixelLength as ComputedCSSPixelLength; +use crate::values::computed::Filter as ComputedFilter; +use crate::values::computed::NonNegativeLength as ComputedNonNegativeLength; +use crate::values::computed::NonNegativeNumber as ComputedNonNegativeNumber; +use crate::values::computed::ZeroToOneNumber as ComputedZeroToOneNumber; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::effects::BoxShadow as GenericBoxShadow; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +use crate::values::generics::NonNegative; +use crate::values::specified::color::Color; +use crate::values::specified::length::{Length, NonNegativeLength}; +#[cfg(feature = "gecko")] +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, Number, NumberOrPercentage}; +#[cfg(feature = "servo")] +use crate::values::Impossible; +use crate::Zero; +use cssparser::{self, BasicParseErrorKind, Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind, ValueParseErrorKind}; + +/// A specified value for a single shadow of the `box-shadow` property. +pub type BoxShadow = + GenericBoxShadow<Option<Color>, Length, Option<NonNegativeLength>, Option<Length>>; + +/// A specified value for a single `filter`. +#[cfg(feature = "gecko")] +pub type SpecifiedFilter = GenericFilter< + Angle, + NonNegativeFactor, + ZeroToOneFactor, + NonNegativeLength, + SimpleShadow, + SpecifiedUrl, +>; + +/// A specified value for a single `filter`. +#[cfg(feature = "servo")] +pub type SpecifiedFilter = GenericFilter< + Angle, + NonNegativeFactor, + ZeroToOneFactor, + NonNegativeLength, + Impossible, + Impossible, +>; + +pub use self::SpecifiedFilter as Filter; + +/// A value for the `<factor>` parts in `Filter`. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct NonNegativeFactor(NumberOrPercentage); + +/// A value for the `<factor>` parts in `Filter` which clamps to one. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct ZeroToOneFactor(NumberOrPercentage); + +/// Clamp the value to 1 if the value is over 100%. +#[inline] +fn clamp_to_one(number: NumberOrPercentage) -> NumberOrPercentage { + match number { + NumberOrPercentage::Percentage(percent) => { + NumberOrPercentage::Percentage(percent.clamp_to_hundred()) + }, + NumberOrPercentage::Number(number) => NumberOrPercentage::Number(number.clamp_to_one()), + } +} + +macro_rules! factor_impl_common { + ($ty:ty, $computed_ty:ty) => { + impl $ty { + #[inline] + fn one() -> Self { + Self(NumberOrPercentage::Number(Number::new(1.))) + } + } + + impl ToComputedValue for $ty { + type ComputedValue = $computed_ty; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::NumberOrPercentage; + match self.0.to_computed_value(context) { + NumberOrPercentage::Number(n) => n.into(), + NumberOrPercentage::Percentage(p) => p.0.into(), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self(NumberOrPercentage::Number( + ToComputedValue::from_computed_value(&computed.0), + )) + } + } + }; +} +factor_impl_common!(NonNegativeFactor, ComputedNonNegativeNumber); +factor_impl_common!(ZeroToOneFactor, ComputedZeroToOneNumber); + +impl Parse for NonNegativeFactor { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + NumberOrPercentage::parse_non_negative(context, input).map(Self) + } +} + +impl Parse for ZeroToOneFactor { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + NumberOrPercentage::parse_non_negative(context, input) + .map(clamp_to_one) + .map(Self) + } +} + +/// A specified value for the `drop-shadow()` filter. +pub type SimpleShadow = GenericSimpleShadow<Option<Color>, Length, Option<NonNegativeLength>>; + +impl Parse for BoxShadow { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut lengths = None; + let mut color = None; + let mut inset = false; + + loop { + if !inset { + if input + .try_parse(|input| input.expect_ident_matching("inset")) + .is_ok() + { + inset = true; + continue; + } + } + if lengths.is_none() { + let value = input.try_parse::<_, _, ParseError>(|i| { + let horizontal = Length::parse(context, i)?; + let vertical = Length::parse(context, i)?; + let (blur, spread) = + match i.try_parse(|i| Length::parse_non_negative(context, i)) { + Ok(blur) => { + let spread = i.try_parse(|i| Length::parse(context, i)).ok(); + (Some(blur.into()), spread) + }, + Err(_) => (None, None), + }; + Ok((horizontal, vertical, blur, spread)) + }); + if let Ok(value) = value { + lengths = Some(value); + continue; + } + } + if color.is_none() { + if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) { + color = Some(value); + continue; + } + } + break; + } + + let lengths = + lengths.ok_or(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?; + Ok(BoxShadow { + base: SimpleShadow { + color: color, + horizontal: lengths.0, + vertical: lengths.1, + blur: lengths.2, + }, + spread: lengths.3, + inset: inset, + }) + } +} + +impl ToComputedValue for BoxShadow { + type ComputedValue = ComputedBoxShadow; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedBoxShadow { + base: self.base.to_computed_value(context), + spread: self + .spread + .as_ref() + .unwrap_or(&Length::zero()) + .to_computed_value(context), + inset: self.inset, + } + } + + #[inline] + fn from_computed_value(computed: &ComputedBoxShadow) -> Self { + BoxShadow { + base: ToComputedValue::from_computed_value(&computed.base), + spread: Some(ToComputedValue::from_computed_value(&computed.spread)), + inset: computed.inset, + } + } +} + +// We need this for converting the specified Filter into computed Filter without Context (for +// some FFIs in glue.rs). This can fail because in some circumstances, we still need Context to +// determine the computed value. +impl Filter { + /// Generate the ComputedFilter without Context. + pub fn to_computed_value_without_context(&self) -> Result<ComputedFilter, ()> { + match *self { + Filter::Blur(ref length) => Ok(ComputedFilter::Blur(ComputedNonNegativeLength::new( + length.0.to_computed_pixel_length_without_context()?, + ))), + Filter::Brightness(ref factor) => Ok(ComputedFilter::Brightness( + ComputedNonNegativeNumber::from(factor.0.to_number().get()), + )), + Filter::Contrast(ref factor) => Ok(ComputedFilter::Contrast( + ComputedNonNegativeNumber::from(factor.0.to_number().get()), + )), + Filter::Grayscale(ref factor) => Ok(ComputedFilter::Grayscale( + ComputedZeroToOneNumber::from(factor.0.to_number().get()), + )), + Filter::HueRotate(ref angle) => Ok(ComputedFilter::HueRotate( + ComputedAngle::from_degrees(angle.degrees()), + )), + Filter::Invert(ref factor) => Ok(ComputedFilter::Invert( + ComputedZeroToOneNumber::from(factor.0.to_number().get()), + )), + Filter::Opacity(ref factor) => Ok(ComputedFilter::Opacity( + ComputedZeroToOneNumber::from(factor.0.to_number().get()), + )), + Filter::Saturate(ref factor) => Ok(ComputedFilter::Saturate( + ComputedNonNegativeNumber::from(factor.0.to_number().get()), + )), + Filter::Sepia(ref factor) => Ok(ComputedFilter::Sepia(ComputedZeroToOneNumber::from( + factor.0.to_number().get(), + ))), + Filter::DropShadow(ref shadow) => { + if cfg!(feature = "gecko") { + let color = match shadow + .color + .as_ref() + .unwrap_or(&Color::currentcolor()) + .to_computed_color(None) + { + Some(c) => c, + None => return Err(()), + }; + + let horizontal = ComputedCSSPixelLength::new( + shadow + .horizontal + .to_computed_pixel_length_without_context()?, + ); + let vertical = ComputedCSSPixelLength::new( + shadow.vertical.to_computed_pixel_length_without_context()?, + ); + let blur = ComputedNonNegativeLength::new( + shadow + .blur + .as_ref() + .unwrap_or(&NonNegativeLength::zero()) + .0 + .to_computed_pixel_length_without_context()?, + ); + + Ok(ComputedFilter::DropShadow(ComputedSimpleShadow { + color, + horizontal, + vertical, + blur, + })) + } else { + Err(()) + } + }, + Filter::Url(ref url) => { + if cfg!(feature = "gecko") { + Ok(ComputedFilter::Url(ComputedUrl(url.clone()))) + } else { + Err(()) + } + }, + } + } +} + +impl Parse for Filter { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + #[cfg(feature = "gecko")] + { + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(GenericFilter::Url(url)); + } + } + let location = input.current_source_location(); + let function = match input.expect_function() { + Ok(f) => f.clone(), + Err(cssparser::BasicParseError { + kind: BasicParseErrorKind::UnexpectedToken(t), + location, + }) => return Err(location.new_custom_error(ValueParseErrorKind::InvalidFilter(t))), + Err(e) => return Err(e.into()), + }; + input.parse_nested_block(|i| { + match_ignore_ascii_case! { &*function, + "blur" => Ok(GenericFilter::Blur( + i.try_parse(|i| NonNegativeLength::parse(context, i)) + .unwrap_or(Zero::zero()), + )), + "brightness" => Ok(GenericFilter::Brightness( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "contrast" => Ok(GenericFilter::Contrast( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "grayscale" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-grayscale + Ok(GenericFilter::Grayscale( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "hue-rotate" => { + // We allow unitless zero here, see: + // https://github.com/w3c/fxtf-drafts/issues/228 + Ok(GenericFilter::HueRotate( + i.try_parse(|i| Angle::parse_with_unitless(context, i)) + .unwrap_or(Zero::zero()), + )) + }, + "invert" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-invert + Ok(GenericFilter::Invert( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "opacity" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-opacity + Ok(GenericFilter::Opacity( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "saturate" => Ok(GenericFilter::Saturate( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "sepia" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-sepia + Ok(GenericFilter::Sepia( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "drop-shadow" => Ok(GenericFilter::DropShadow(Parse::parse(context, i)?)), + _ => Err(location.new_custom_error( + ValueParseErrorKind::InvalidFilter(Token::Function(function.clone())) + )), + } + }) + } +} + +impl Parse for SimpleShadow { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let color = input.try_parse(|i| Color::parse(context, i)).ok(); + let horizontal = Length::parse(context, input)?; + let vertical = Length::parse(context, input)?; + let blur = input + .try_parse(|i| Length::parse_non_negative(context, i)) + .ok(); + let blur = blur.map(NonNegative::<Length>); + let color = color.or_else(|| input.try_parse(|i| Color::parse(context, i)).ok()); + + Ok(SimpleShadow { + color, + horizontal, + vertical, + blur, + }) + } +} + +impl ToComputedValue for SimpleShadow { + type ComputedValue = ComputedSimpleShadow; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedSimpleShadow { + color: self + .color + .as_ref() + .unwrap_or(&Color::currentcolor()) + .to_computed_value(context), + horizontal: self.horizontal.to_computed_value(context), + vertical: self.vertical.to_computed_value(context), + blur: self + .blur + .as_ref() + .unwrap_or(&NonNegativeLength::zero()) + .to_computed_value(context), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + SimpleShadow { + color: Some(ToComputedValue::from_computed_value(&computed.color)), + horizontal: ToComputedValue::from_computed_value(&computed.horizontal), + vertical: ToComputedValue::from_computed_value(&computed.vertical), + blur: Some(ToComputedValue::from_computed_value(&computed.blur)), + } + } +} diff --git a/servo/components/style/values/specified/flex.rs b/servo/components/style/values/specified/flex.rs new file mode 100644 index 0000000000..7c767cdf34 --- /dev/null +++ b/servo/components/style/values/specified/flex.rs @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to flexbox. + +use crate::values::generics::flex::FlexBasis as GenericFlexBasis; +use crate::values::specified::Size; + +/// A specified value for the `flex-basis` property. +pub type FlexBasis = GenericFlexBasis<Size>; + +impl FlexBasis { + /// `auto` + #[inline] + pub fn auto() -> Self { + GenericFlexBasis::Size(Size::auto()) + } + + /// `0%` + #[inline] + pub fn zero_percent() -> Self { + GenericFlexBasis::Size(Size::zero_percent()) + } +} diff --git a/servo/components/style/values/specified/font.rs b/servo/components/style/values/specified/font.rs new file mode 100644 index 0000000000..2435682ce3 --- /dev/null +++ b/servo/components/style/values/specified/font.rs @@ -0,0 +1,2222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified values for font properties + +use crate::context::QuirksMode; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::font::{FamilyName, FontFamilyList, SingleFontFamily}; +use crate::values::computed::Percentage as ComputedPercentage; +use crate::values::computed::{font as computed, Length, NonNegativeLength}; +use crate::values::computed::{CSSPixelLength, Context, ToComputedValue}; +use crate::values::generics::font::{ + self as generics, FeatureTagValue, FontSettings, FontTag, GenericLineHeight, VariationValue, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::length::{FontBaseSize, LineHeightBase, PX_PER_PT}; +use crate::values::specified::{AllowQuirks, Angle, Integer, LengthPercentage}; +use crate::values::specified::{ + FontRelativeLength, NoCalcLength, NonNegativeLengthPercentage, NonNegativeNumber, + NonNegativePercentage, Number, +}; +use crate::values::{serialize_atom_identifier, CustomIdent, SelectorParseErrorKind}; +use crate::Atom; +use cssparser::{Parser, Token}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalSizeOf}; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +// FIXME(emilio): The system font code is copy-pasta, and should be cleaned up. +macro_rules! system_font_methods { + ($ty:ident, $field:ident) => { + system_font_methods!($ty); + + fn compute_system(&self, _context: &Context) -> <$ty as ToComputedValue>::ComputedValue { + debug_assert!(matches!(*self, $ty::System(..))); + #[cfg(feature = "gecko")] + { + _context.cached_system_font.as_ref().unwrap().$field.clone() + } + #[cfg(feature = "servo")] + { + unreachable!() + } + } + }; + + ($ty:ident) => { + /// Get a specified value that represents a system font. + pub fn system_font(f: SystemFont) -> Self { + $ty::System(f) + } + + /// Retreive a SystemFont from the specified value. + pub fn get_system(&self) -> Option<SystemFont> { + if let $ty::System(s) = *self { + Some(s) + } else { + None + } + } + }; +} + +/// System fonts. +#[repr(u8)] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum SystemFont { + /// https://drafts.csswg.org/css-fonts/#valdef-font-caption + Caption, + /// https://drafts.csswg.org/css-fonts/#valdef-font-icon + Icon, + /// https://drafts.csswg.org/css-fonts/#valdef-font-menu + Menu, + /// https://drafts.csswg.org/css-fonts/#valdef-font-message-box + MessageBox, + /// https://drafts.csswg.org/css-fonts/#valdef-font-small-caption + SmallCaption, + /// https://drafts.csswg.org/css-fonts/#valdef-font-status-bar + StatusBar, + /// Internal system font, used by the `<menupopup>`s on macOS. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozPullDownMenu, + /// Internal system font, used for `<button>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButton, + /// Internal font, used by `<select>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozList, + /// Internal font, used by `<input>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozField, + #[css(skip)] + End, // Just for indexing purposes. +} + +const DEFAULT_SCRIPT_MIN_SIZE_PT: u32 = 8; +const DEFAULT_SCRIPT_SIZE_MULTIPLIER: f64 = 0.71; + +/// The minimum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MIN_FONT_WEIGHT: f32 = 1.; + +/// The maximum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MAX_FONT_WEIGHT: f32 = 1000.; + +/// A specified font-weight value. +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontWeight { + /// `<font-weight-absolute>` + Absolute(AbsoluteFontWeight), + /// Bolder variant + Bolder, + /// Lighter variant + Lighter, + /// System font variant. + #[css(skip)] + System(SystemFont), +} + +impl FontWeight { + system_font_methods!(FontWeight, font_weight); + + /// `normal` + #[inline] + pub fn normal() -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Normal) + } + + /// Get a specified FontWeight from a gecko keyword + pub fn from_gecko_keyword(kw: u32) -> Self { + debug_assert!(kw % 100 == 0); + debug_assert!(kw as f32 <= MAX_FONT_WEIGHT); + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::new(kw as f32))) + } +} + +impl ToComputedValue for FontWeight { + type ComputedValue = computed::FontWeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontWeight::Absolute(ref abs) => abs.compute(), + FontWeight::Bolder => context + .builder + .get_parent_font() + .clone_font_weight() + .bolder(), + FontWeight::Lighter => context + .builder + .get_parent_font() + .clone_font_weight() + .lighter(), + FontWeight::System(_) => self.compute_system(context), + } + } + + #[inline] + fn from_computed_value(computed: &computed::FontWeight) -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::from_computed_value( + &computed.value(), + ))) + } +} + +/// An absolute font-weight value for a @font-face rule. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-absolute-values +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AbsoluteFontWeight { + /// A `<number>`, with the additional constraints specified in: + /// + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + Weight(Number), + /// Normal font weight. Same as 400. + Normal, + /// Bold font weight. Same as 700. + Bold, +} + +impl AbsoluteFontWeight { + /// Returns the computed value for this absolute font weight. + pub fn compute(&self) -> computed::FontWeight { + match *self { + AbsoluteFontWeight::Weight(weight) => computed::FontWeight::from_float(weight.get()), + AbsoluteFontWeight::Normal => computed::FontWeight::NORMAL, + AbsoluteFontWeight::Bold => computed::FontWeight::BOLD, + } + } +} + +impl Parse for AbsoluteFontWeight { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(number) = input.try_parse(|input| Number::parse(context, input)) { + // We could add another AllowedNumericType value, but it doesn't + // seem worth it just for a single property with such a weird range, + // so we do the clamping here manually. + if !number.was_calc() && + (number.get() < MIN_FONT_WEIGHT || number.get() > MAX_FONT_WEIGHT) + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(AbsoluteFontWeight::Weight(number)); + } + + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => AbsoluteFontWeight::Normal, + "bold" => AbsoluteFontWeight::Bold, + }) + } +} + +/// The specified value of the `font-style` property, without the system font +/// crap. +pub type SpecifiedFontStyle = generics::FontStyle<Angle>; + +impl ToCss for SpecifiedFontStyle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + generics::FontStyle::Normal => dest.write_str("normal"), + generics::FontStyle::Italic => dest.write_str("italic"), + generics::FontStyle::Oblique(ref angle) => { + dest.write_str("oblique")?; + if *angle != Self::default_angle() { + dest.write_char(' ')?; + angle.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for SpecifiedFontStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => generics::FontStyle::Normal, + "italic" => generics::FontStyle::Italic, + "oblique" => { + let angle = input.try_parse(|input| Self::parse_angle(context, input)) + .unwrap_or_else(|_| Self::default_angle()); + + generics::FontStyle::Oblique(angle) + }, + }) + } +} + +impl ToComputedValue for SpecifiedFontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + match *self { + Self::Normal => computed::FontStyle::NORMAL, + Self::Italic => computed::FontStyle::ITALIC, + Self::Oblique(ref angle) => computed::FontStyle::oblique(angle.degrees()), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if *computed == computed::FontStyle::NORMAL { + return Self::Normal; + } + if *computed == computed::FontStyle::ITALIC { + return Self::Italic; + } + let degrees = computed.oblique_degrees(); + generics::FontStyle::Oblique(Angle::from_degrees(degrees, /* was_calc = */ false)) + } +} + +/// From https://drafts.csswg.org/css-fonts-4/#valdef-font-style-oblique-angle: +/// +/// Values less than -90deg or values greater than 90deg are +/// invalid and are treated as parse errors. +/// +/// The maximum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES: f32 = 90.; + +/// The minimum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES: f32 = -90.; + +impl SpecifiedFontStyle { + /// Gets a clamped angle in degrees from a specified Angle. + pub fn compute_angle_degrees(angle: &Angle) -> f32 { + angle + .degrees() + .max(FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES) + .min(FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES) + } + + /// Parse a suitable angle for font-style: oblique. + pub fn parse_angle<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Angle, ParseError<'i>> { + let angle = Angle::parse(context, input)?; + if angle.was_calc() { + return Ok(angle); + } + + let degrees = angle.degrees(); + if degrees < FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES || + degrees > FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(angle); + } + + /// The default angle for `font-style: oblique`. + pub fn default_angle() -> Angle { + Angle::from_degrees( + computed::FontStyle::DEFAULT_OBLIQUE_DEGREES as f32, + /* was_calc = */ false, + ) + } +} + +/// The specified value of the `font-style` property. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum FontStyle { + Specified(SpecifiedFontStyle), + #[css(skip)] + System(SystemFont), +} + +impl FontStyle { + /// Return the `normal` value. + #[inline] + pub fn normal() -> Self { + FontStyle::Specified(generics::FontStyle::Normal) + } + + system_font_methods!(FontStyle, font_style); +} + +impl ToComputedValue for FontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStyle::Specified(ref specified) => specified.to_computed_value(context), + FontStyle::System(..) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStyle::Specified(SpecifiedFontStyle::from_computed_value(computed)) + } +} + +/// A value for the `font-stretch` property. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop +#[allow(missing_docs)] +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontStretch { + Stretch(NonNegativePercentage), + Keyword(FontStretchKeyword), + #[css(skip)] + System(SystemFont), +} + +/// A keyword value for `font-stretch`. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum FontStretchKeyword { + Normal, + Condensed, + UltraCondensed, + ExtraCondensed, + SemiCondensed, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} + +impl FontStretchKeyword { + /// Turns the keyword into a computed value. + pub fn compute(&self) -> computed::FontStretch { + computed::FontStretch::from_keyword(*self) + } + + /// Does the opposite operation to `compute`, in order to serialize keywords + /// if possible. + pub fn from_percentage(p: f32) -> Option<Self> { + computed::FontStretch::from_percentage(p).as_keyword() + } +} + +impl FontStretch { + /// `normal`. + pub fn normal() -> Self { + FontStretch::Keyword(FontStretchKeyword::Normal) + } + + system_font_methods!(FontStretch, font_stretch); +} + +impl ToComputedValue for FontStretch { + type ComputedValue = computed::FontStretch; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStretch::Stretch(ref percentage) => { + let percentage = percentage.to_computed_value(context).0; + computed::FontStretch::from_percentage(percentage.0) + }, + FontStretch::Keyword(ref kw) => kw.compute(), + FontStretch::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStretch::Stretch(NonNegativePercentage::from_computed_value(&NonNegative( + computed.to_percentage(), + ))) + } +} + +#[cfg(feature = "gecko")] +fn math_depth_enabled(_context: &ParserContext) -> bool { + static_prefs::pref!("layout.css.math-depth.enabled") +} + +#[cfg(feature = "servo")] +fn math_depth_enabled(_context: &ParserContext) -> bool { + false +} + +/// CSS font keywords +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum FontSizeKeyword { + #[css(keyword = "xx-small")] + XXSmall, + XSmall, + Small, + Medium, + Large, + XLarge, + #[css(keyword = "xx-large")] + XXLarge, + #[css(keyword = "xxx-large")] + XXXLarge, + /// Indicate whether to apply font-size: math is specified so that extra + /// scaling due to math-depth changes is applied during the cascade. + #[parse(condition = "math_depth_enabled")] + Math, + #[css(skip)] + None, +} + +impl FontSizeKeyword { + /// Convert to an HTML <font size> value + #[inline] + pub fn html_size(self) -> u8 { + self as u8 + } +} + +impl Default for FontSizeKeyword { + fn default() -> Self { + FontSizeKeyword::Medium + } +} + +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))] +/// Additional information for keyword-derived font sizes. +pub struct KeywordInfo { + /// The keyword used + pub kw: FontSizeKeyword, + /// A factor to be multiplied by the computed size of the keyword + #[css(skip)] + pub factor: f32, + /// An additional fixed offset to add to the kw * factor in the case of + /// `calc()`. + #[css(skip)] + pub offset: CSSPixelLength, +} + +impl KeywordInfo { + /// KeywordInfo value for font-size: medium + pub fn medium() -> Self { + Self::new(FontSizeKeyword::Medium) + } + + /// KeywordInfo value for font-size: none + pub fn none() -> Self { + Self::new(FontSizeKeyword::None) + } + + fn new(kw: FontSizeKeyword) -> Self { + KeywordInfo { + kw, + factor: 1., + offset: CSSPixelLength::new(0.), + } + } + + /// Computes the final size for this font-size keyword, accounting for + /// text-zoom. + fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + debug_assert_ne!(self.kw, FontSizeKeyword::None); + debug_assert_ne!(self.kw, FontSizeKeyword::Math); + let base = context.maybe_zoom_text(self.kw.to_length(context).0); + base * self.factor + context.maybe_zoom_text(self.offset) + } + + /// Given a parent keyword info (self), apply an additional factor/offset to + /// it. + fn compose(self, factor: f32) -> Self { + if self.kw == FontSizeKeyword::None { + return self; + } + KeywordInfo { + kw: self.kw, + factor: self.factor * factor, + offset: self.offset * factor, + } + } +} + +impl SpecifiedValueInfo for KeywordInfo { + fn collect_completion_keywords(f: KeywordsCollectFn) { + <FontSizeKeyword as SpecifiedValueInfo>::collect_completion_keywords(f); + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// A specified font-size value +pub enum FontSize { + /// A length; e.g. 10px. + Length(LengthPercentage), + /// A keyword value, along with a ratio and absolute offset. + /// The ratio in any specified keyword value + /// will be 1 (with offset 0), but we cascade keywordness even + /// after font-relative (percent and em) values + /// have been applied, which is where the ratio + /// comes in. The offset comes in if we cascaded a calc value, + /// where the font-relative portion (em and percentage) will + /// go into the ratio, and the remaining units all computed together + /// will go into the offset. + /// See bug 1355707. + Keyword(KeywordInfo), + /// font-size: smaller + Smaller, + /// font-size: larger + Larger, + /// Derived from a specified system font. + #[css(skip)] + System(SystemFont), +} + +/// Specifies a prioritized list of font family names or generic family names. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[cfg_attr(feature = "servo", derive(Hash))] +pub enum FontFamily { + /// List of `font-family` + #[css(comma)] + Values(#[css(iterable)] FontFamilyList), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontFamily { + system_font_methods!(FontFamily, font_family); +} + +impl ToComputedValue for FontFamily { + type ComputedValue = computed::FontFamily; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontFamily::Values(ref list) => computed::FontFamily { + families: list.clone(), + is_system_font: false, + is_initial: false, + }, + FontFamily::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontFamily) -> Self { + FontFamily::Values(other.families.clone()) + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for FontFamily { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + FontFamily::Values(ref v) => { + // Although the family list is refcounted, we always attribute + // its size to the specified value. + v.list.unconditional_size_of(ops) + }, + FontFamily::System(_) => 0, + } + } +} + +impl Parse for FontFamily { + /// <family-name># + /// <family-name> = <string> | [ <ident>+ ] + /// TODO: <generic-family> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontFamily, ParseError<'i>> { + let values = + input.parse_comma_separated(|input| SingleFontFamily::parse(context, input))?; + Ok(FontFamily::Values(FontFamilyList { + list: crate::ArcSlice::from_iter(values.into_iter()), + })) + } +} + +impl SpecifiedValueInfo for FontFamily {} + +/// `FamilyName::parse` is based on `SingleFontFamily::parse` and not the other +/// way around because we want the former to exclude generic family keywords. +impl Parse for FamilyName { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + match SingleFontFamily::parse(context, input) { + Ok(SingleFontFamily::FamilyName(name)) => Ok(name), + Ok(SingleFontFamily::Generic(_)) => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Err(e) => Err(e), + } + } +} + +/// A factor for one of the font-size-adjust metrics, which may be either a number +/// or the `from-font` keyword. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontSizeAdjustFactor { + /// An explicitly-specified number. + Number(NonNegativeNumber), + /// The from-font keyword: resolve the number from font metrics. + FromFont, +} + +/// Specified value for font-size-adjust, intended to help +/// preserve the readability of text when font fallback occurs. +/// +/// https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop +pub type FontSizeAdjust = generics::GenericFontSizeAdjust<FontSizeAdjustFactor>; + +impl Parse for FontSizeAdjust { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + // First check if we have an adjustment factor without a metrics-basis keyword. + if let Ok(factor) = input.try_parse(|i| FontSizeAdjustFactor::parse(context, i)) { + return Ok(Self::ExHeight(factor)); + } + + let ident = input.expect_ident()?; + let basis = match_ignore_ascii_case! { &ident, + "none" => return Ok(Self::None), + // Check for size adjustment basis keywords. + "ex-height" => Self::ExHeight, + "cap-height" => Self::CapHeight, + "ch-width" => Self::ChWidth, + "ic-width" => Self::IcWidth, + "ic-height" => Self::IcHeight, + // Unknown keyword. + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + }; + + Ok(basis(FontSizeAdjustFactor::parse(context, input)?)) + } +} + +/// This is the ratio applied for font-size: larger +/// and smaller by both Firefox and Chrome +const LARGER_FONT_SIZE_RATIO: f32 = 1.2; + +/// The default font size. +pub const FONT_MEDIUM_PX: f32 = 16.0; +/// The default line height. +pub const FONT_MEDIUM_LINE_HEIGHT_PX: f32 = FONT_MEDIUM_PX * 1.2; + +impl FontSizeKeyword { + #[inline] + #[cfg(feature = "servo")] + fn to_length(&self, _: &Context) -> NonNegativeLength { + let medium = Length::new(FONT_MEDIUM_PX); + // https://drafts.csswg.org/css-fonts-3/#font-size-prop + NonNegative(match *self { + FontSizeKeyword::XXSmall => medium * 3.0 / 5.0, + FontSizeKeyword::XSmall => medium * 3.0 / 4.0, + FontSizeKeyword::Small => medium * 8.0 / 9.0, + FontSizeKeyword::Medium => medium, + FontSizeKeyword::Large => medium * 6.0 / 5.0, + FontSizeKeyword::XLarge => medium * 3.0 / 2.0, + FontSizeKeyword::XXLarge => medium * 2.0, + FontSizeKeyword::XXXLarge => medium * 3.0, + FontSizeKeyword::Math | FontSizeKeyword::None => unreachable!(), + }) + } + + #[cfg(feature = "gecko")] + #[inline] + fn to_length(&self, cx: &Context) -> NonNegativeLength { + let font = cx.style().get_font(); + let family = &font.mFont.family.families; + let generic = family + .single_generic() + .unwrap_or(computed::GenericFontFamily::None); + let base_size = unsafe { + Atom::with(font.mLanguage.mRawPtr, |language| { + cx.device().base_size_for_generic(language, generic) + }) + }; + self.to_length_without_context(cx.quirks_mode, base_size) + } + + /// Resolve a keyword length without any context, with explicit arguments. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_length_without_context( + &self, + quirks_mode: QuirksMode, + base_size: Length, + ) -> NonNegativeLength { + debug_assert_ne!(*self, FontSizeKeyword::Math); + // The tables in this function are originally from + // nsRuleNode::CalcFontPointSize in Gecko: + // + // https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3150 + // + // Mapping from base size and HTML size to pixels + // The first index is (base_size - 9), the second is the + // HTML size. "0" is CSS keyword xx-small, not HTML size 0, + // since HTML size 0 is the same as 1. + // + // xxs xs s m l xl xxl - + // - 0/1 2 3 4 5 6 7 + static FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 27], + [9, 9, 9, 10, 12, 15, 20, 30], + [9, 9, 10, 11, 13, 17, 22, 33], + [9, 9, 10, 12, 14, 18, 24, 36], + [9, 10, 12, 13, 16, 20, 26, 39], + [9, 10, 12, 14, 17, 21, 28, 42], + [9, 10, 13, 15, 18, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + // This table gives us compatibility with WinNav4 for the default fonts only. + // In WinNav4, the default fonts were: + // + // Times/12pt == Times/16px at 96ppi + // Courier/10pt == Courier/13px at 96ppi + // + // xxs xs s m l xl xxl - + // - 1 2 3 4 5 6 7 + static QUIRKS_FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 28], + [9, 9, 9, 10, 12, 15, 20, 31], + [9, 9, 9, 11, 13, 17, 22, 34], + [9, 9, 10, 12, 14, 18, 24, 37], + [9, 9, 10, 13, 16, 20, 26, 40], + [9, 9, 11, 14, 17, 21, 28, 42], + [9, 10, 12, 15, 17, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + static FONT_SIZE_FACTORS: [i32; 8] = [60, 75, 89, 100, 120, 150, 200, 300]; + let base_size_px = base_size.px().round() as i32; + let html_size = self.html_size() as usize; + NonNegative(if base_size_px >= 9 && base_size_px <= 16 { + let mapping = if quirks_mode == QuirksMode::Quirks { + QUIRKS_FONT_SIZE_MAPPING + } else { + FONT_SIZE_MAPPING + }; + Length::new(mapping[(base_size_px - 9) as usize][html_size] as f32) + } else { + base_size * FONT_SIZE_FACTORS[html_size] as f32 / 100.0 + }) + } +} + +impl FontSize { + /// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-font-size> + pub fn from_html_size(size: u8) -> Self { + FontSize::Keyword(KeywordInfo::new(match size { + // If value is less than 1, let it be 1. + 0 | 1 => FontSizeKeyword::XSmall, + 2 => FontSizeKeyword::Small, + 3 => FontSizeKeyword::Medium, + 4 => FontSizeKeyword::Large, + 5 => FontSizeKeyword::XLarge, + 6 => FontSizeKeyword::XXLarge, + // If value is greater than 7, let it be 7. + _ => FontSizeKeyword::XXXLarge, + })) + } + + /// Compute it against a given base font size + pub fn to_computed_value_against( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> computed::FontSize { + let compose_keyword = |factor| { + context + .style() + .get_parent_font() + .clone_font_size() + .keyword_info + .compose(factor) + }; + let mut info = KeywordInfo::none(); + let size = match *self { + FontSize::Length(LengthPercentage::Length(ref l)) => { + if let NoCalcLength::FontRelative(ref value) = *l { + if let FontRelativeLength::Em(em) = *value { + // If the parent font was keyword-derived, this is + // too. Tack the em unit onto the factor + info = compose_keyword(em); + } + } + let result = + l.to_computed_value_with_base_size(context, base_size, line_height_base); + if l.should_zoom_text() { + context.maybe_zoom_text(result) + } else { + result + } + }, + FontSize::Length(LengthPercentage::Percentage(pc)) => { + // If the parent font was keyword-derived, this is too. + // Tack the % onto the factor + info = compose_keyword(pc.0); + (base_size.resolve(context).computed_size() * pc.0).normalized() + }, + FontSize::Length(LengthPercentage::Calc(ref calc)) => { + let calc = calc.to_computed_value_zoomed(context, base_size, line_height_base); + calc.resolve(base_size.resolve(context).computed_size()) + }, + FontSize::Keyword(i) => { + if i.kw == FontSizeKeyword::Math { + // Scaling is done in recompute_math_font_size_if_needed(). + info = compose_keyword(1.); + info.kw = FontSizeKeyword::Math; + FontRelativeLength::Em(1.).to_computed_value( + context, + base_size, + line_height_base, + ) + } else { + // As a specified keyword, this is keyword derived + info = i; + i.to_computed_value(context).clamp_to_non_negative() + } + }, + FontSize::Smaller => { + info = compose_keyword(1. / LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(1. / LARGER_FONT_SIZE_RATIO).to_computed_value( + context, + base_size, + line_height_base, + ) + }, + FontSize::Larger => { + info = compose_keyword(LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(LARGER_FONT_SIZE_RATIO).to_computed_value( + context, + base_size, + line_height_base, + ) + }, + + FontSize::System(_) => { + #[cfg(feature = "servo")] + { + unreachable!() + } + #[cfg(feature = "gecko")] + { + context + .cached_system_font + .as_ref() + .unwrap() + .font_size + .computed_size() + } + }, + }; + computed::FontSize { + computed_size: NonNegative(size), + used_size: NonNegative(size), + keyword_info: info, + } + } +} + +impl ToComputedValue for FontSize { + type ComputedValue = computed::FontSize; + + #[inline] + fn to_computed_value(&self, context: &Context) -> computed::FontSize { + self.to_computed_value_against( + context, + FontBaseSize::InheritedStyle, + LineHeightBase::InheritedStyle, + ) + } + + #[inline] + fn from_computed_value(computed: &computed::FontSize) -> Self { + FontSize::Length(LengthPercentage::Length( + ToComputedValue::from_computed_value(&computed.computed_size()), + )) + } +} + +impl FontSize { + system_font_methods!(FontSize); + + /// Get initial value for specified font size. + #[inline] + pub fn medium() -> Self { + FontSize::Keyword(KeywordInfo::medium()) + } + + /// Parses a font-size, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<FontSize, ParseError<'i>> { + if let Ok(lp) = input + .try_parse(|i| LengthPercentage::parse_non_negative_quirky(context, i, allow_quirks)) + { + return Ok(FontSize::Length(lp)); + } + + if let Ok(kw) = input.try_parse(|i| FontSizeKeyword::parse(context, i)) { + return Ok(FontSize::Keyword(KeywordInfo::new(kw))); + } + + try_match_ident_ignore_ascii_case! { input, + "smaller" => Ok(FontSize::Smaller), + "larger" => Ok(FontSize::Larger), + } + } +} + +impl Parse for FontSize { + /// <length> | <percentage> | <absolute-size> | <relative-size> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontSize, ParseError<'i>> { + FontSize::parse_quirky(context, input, AllowQuirks::No) + } +} + +bitflags! { + #[derive(Clone, Copy)] + /// Flags of variant alternates in bit + struct VariantAlternatesParsingFlags: u8 { + /// None of variant alternates enabled + const NORMAL = 0; + /// Historical forms + const HISTORICAL_FORMS = 0x01; + /// Stylistic Alternates + const STYLISTIC = 0x02; + /// Stylistic Sets + const STYLESET = 0x04; + /// Character Variant + const CHARACTER_VARIANT = 0x08; + /// Swash glyphs + const SWASH = 0x10; + /// Ornaments glyphs + const ORNAMENTS = 0x20; + /// Annotation forms + const ANNOTATION = 0x40; + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// Set of variant alternates +pub enum VariantAlternates { + /// Enables display of stylistic alternates + #[css(function)] + Stylistic(CustomIdent), + /// Enables display with stylistic sets + #[css(comma, function)] + Styleset(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of specific character variants + #[css(comma, function)] + CharacterVariant(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of swash glyphs + #[css(function)] + Swash(CustomIdent), + /// Enables replacement of default glyphs with ornaments + #[css(function)] + Ornaments(CustomIdent), + /// Enables display of alternate annotation forms + #[css(function)] + Annotation(CustomIdent), + /// Enables display of historical forms + HistoricalForms, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +/// List of Variant Alternates +pub struct FontVariantAlternates( + #[css(if_empty = "normal", iterable)] crate::OwnedSlice<VariantAlternates>, +); + +impl FontVariantAlternates { + /// Returns the length of all variant alternates. + pub fn len(&self) -> usize { + self.0.iter().fold(0, |acc, alternate| match *alternate { + VariantAlternates::Swash(_) | + VariantAlternates::Stylistic(_) | + VariantAlternates::Ornaments(_) | + VariantAlternates::Annotation(_) => acc + 1, + VariantAlternates::Styleset(ref slice) | + VariantAlternates::CharacterVariant(ref slice) => acc + slice.len(), + _ => acc, + }) + } +} + +impl FontVariantAlternates { + #[inline] + /// Get initial specified value with VariantAlternatesList + pub fn get_initial_specified_value() -> Self { + Default::default() + } +} + +impl Parse for FontVariantAlternates { + /// normal | + /// [ stylistic(<feature-value-name>) || + /// historical-forms || + /// styleset(<feature-value-name> #) || + /// character-variant(<feature-value-name> #) || + /// swash(<feature-value-name>) || + /// ornaments(<feature-value-name>) || + /// annotation(<feature-value-name>) ] + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariantAlternates, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(Default::default()); + } + + let mut stylistic = None; + let mut historical = None; + let mut styleset = None; + let mut character_variant = None; + let mut swash = None; + let mut ornaments = None; + let mut annotation = None; + + // Parse values for the various alternate types in any order. + let mut parsed_alternates = VariantAlternatesParsingFlags::empty(); + macro_rules! check_if_parsed( + ($input:expr, $flag:path) => ( + if parsed_alternates.contains($flag) { + return Err($input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + parsed_alternates |= $flag; + ) + ); + while let Ok(_) = input.try_parse(|input| match *input.next()? { + Token::Ident(ref value) if value.eq_ignore_ascii_case("historical-forms") => { + check_if_parsed!(input, VariantAlternatesParsingFlags::HISTORICAL_FORMS); + historical = Some(VariantAlternates::HistoricalForms); + Ok(()) + }, + Token::Function(ref name) => { + let name = name.clone(); + input.parse_nested_block(|i| { + match_ignore_ascii_case! { &name, + "swash" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::SWASH); + let ident = CustomIdent::parse(i, &[])?; + swash = Some(VariantAlternates::Swash(ident)); + Ok(()) + }, + "stylistic" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLISTIC); + let ident = CustomIdent::parse(i, &[])?; + stylistic = Some(VariantAlternates::Stylistic(ident)); + Ok(()) + }, + "ornaments" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ORNAMENTS); + let ident = CustomIdent::parse(i, &[])?; + ornaments = Some(VariantAlternates::Ornaments(ident)); + Ok(()) + }, + "annotation" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ANNOTATION); + let ident = CustomIdent::parse(i, &[])?; + annotation = Some(VariantAlternates::Annotation(ident)); + Ok(()) + }, + "styleset" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLESET); + let idents = i.parse_comma_separated(|i| { + CustomIdent::parse(i, &[]) + })?; + styleset = Some(VariantAlternates::Styleset(idents.into())); + Ok(()) + }, + "character-variant" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::CHARACTER_VARIANT); + let idents = i.parse_comma_separated(|i| { + CustomIdent::parse(i, &[]) + })?; + character_variant = Some(VariantAlternates::CharacterVariant(idents.into())); + Ok(()) + }, + _ => return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + }) + }, + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }) {} + + if parsed_alternates.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // Collect the parsed values in canonical order, so that we'll serialize correctly. + let mut alternates = Vec::new(); + macro_rules! push_if_some( + ($value:expr) => ( + if let Some(v) = $value { + alternates.push(v); + } + ) + ); + push_if_some!(stylistic); + push_if_some!(historical); + push_if_some!(styleset); + push_if_some!(character_variant); + push_if_some!(swash); + push_if_some!(ornaments); + push_if_some!(annotation); + + Ok(FontVariantAlternates(alternates.into())) + } +} + +macro_rules! impl_variant_east_asian { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants for east asian variant + pub struct FontVariantEastAsian(u16); + bitflags! { + impl FontVariantEastAsian: u16 { + /// None of the features + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantEastAsian { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(Self::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_east_asian_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, FontVariantEastAsian::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantEastAsian { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_east_asian! { + /// Enables rendering of JIS78 forms (OpenType feature: jp78) + JIS78 / "jis78" => NS_FONT_VARIANT_EAST_ASIAN_JIS78 = 0x01, + /// Enables rendering of JIS83 forms (OpenType feature: jp83). + JIS83 / "jis83" => NS_FONT_VARIANT_EAST_ASIAN_JIS83 = 0x02, + /// Enables rendering of JIS90 forms (OpenType feature: jp90). + JIS90 / "jis90" => NS_FONT_VARIANT_EAST_ASIAN_JIS90 = 0x04, + /// Enables rendering of JIS2004 forms (OpenType feature: jp04). + JIS04 / "jis04" => NS_FONT_VARIANT_EAST_ASIAN_JIS04 = 0x08, + /// Enables rendering of simplified forms (OpenType feature: smpl). + SIMPLIFIED / "simplified" => NS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED = 0x10, + /// Enables rendering of traditional forms (OpenType feature: trad). + TRADITIONAL / "traditional" => NS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL = 0x20, + /// Enables rendering of full-width variants (OpenType feature: fwid). + FULL_WIDTH / "full-width" => NS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH = 0x40, + /// Enables rendering of proportionally-spaced variants (OpenType feature: pwid). + PROPORTIONAL_WIDTH / "proportional-width" => NS_FONT_VARIANT_EAST_ASIAN_PROP_WIDTH = 0x80, + /// Enables display of ruby variant glyphs (OpenType feature: ruby). + RUBY / "ruby" => NS_FONT_VARIANT_EAST_ASIAN_RUBY = 0x100, +} + +#[cfg(feature = "gecko")] +impl FontVariantEastAsian { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantEastAsian, u16); + +impl Parse for FontVariantEastAsian { + /// normal | [ <east-asian-variant-values> || <east-asian-width-values> || ruby ] + /// <east-asian-variant-values> = [ jis78 | jis83 | jis90 | jis04 | simplified | traditional ] + /// <east-asian-width-values> = [ full-width | proportional-width ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "jis78" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS78), + "jis83" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS83), + "jis90" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS90), + "jis04" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS04), + "simplified" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::SIMPLIFIED), + "traditional" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::TRADITIONAL), + "full-width" => + exclusive_value!((result, Self::FULL_WIDTH | + Self::PROPORTIONAL_WIDTH + ) => Self::FULL_WIDTH), + "proportional-width" => + exclusive_value!((result, Self::FULL_WIDTH | + Self::PROPORTIONAL_WIDTH + ) => Self::PROPORTIONAL_WIDTH), + "ruby" => + exclusive_value!((result, Self::RUBY) => Self::RUBY), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_ligatures { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants of ligatures + pub struct FontVariantLigatures(u16); + bitflags! { + impl FontVariantLigatures: u16 { + /// Specifies that common default features are enabled + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantLigatures { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + if self.contains(FontVariantLigatures::NONE) { + return dest.write_str("none"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(FontVariantLigatures::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_ligatures_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, FontVariantLigatures::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantLigatures { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_ligatures! { + /// Specifies that all types of ligatures and contextual forms + /// covered by this property are explicitly disabled + NONE / "none" => NS_FONT_VARIANT_LIGATURES_NONE = 0x01, + /// Enables display of common ligatures + COMMON_LIGATURES / "common-ligatures" => NS_FONT_VARIANT_LIGATURES_COMMON = 0x02, + /// Disables display of common ligatures + NO_COMMON_LIGATURES / "no-common-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_COMMON = 0x04, + /// Enables display of discretionary ligatures + DISCRETIONARY_LIGATURES / "discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_DISCRETIONARY = 0x08, + /// Disables display of discretionary ligatures + NO_DISCRETIONARY_LIGATURES / "no-discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_DISCRETIONARY = 0x10, + /// Enables display of historical ligatures + HISTORICAL_LIGATURES / "historical-ligatures" => NS_FONT_VARIANT_LIGATURES_HISTORICAL = 0x20, + /// Disables display of historical ligatures + NO_HISTORICAL_LIGATURES / "no-historical-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_HISTORICAL = 0x40, + /// Enables display of contextual alternates + CONTEXTUAL / "contextual" => NS_FONT_VARIANT_LIGATURES_CONTEXTUAL = 0x80, + /// Disables display of contextual alternates + NO_CONTEXTUAL / "no-contextual" => NS_FONT_VARIANT_LIGATURES_NO_CONTEXTUAL = 0x100, +} + +#[cfg(feature = "gecko")] +impl FontVariantLigatures { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantLigatures, u16); + +impl Parse for FontVariantLigatures { + /// normal | none | + /// [ <common-lig-values> || + /// <discretionary-lig-values> || + /// <historical-lig-values> || + /// <contextual-alt-values> ] + /// <common-lig-values> = [ common-ligatures | no-common-ligatures ] + /// <discretionary-lig-values> = [ discretionary-ligatures | no-discretionary-ligatures ] + /// <historical-lig-values> = [ historical-ligatures | no-historical-ligatures ] + /// <contextual-alt-values> = [ contextual | no-contextual ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(Self::NONE); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "common-ligatures" => + exclusive_value!((result, Self::COMMON_LIGATURES | + Self::NO_COMMON_LIGATURES + ) => Self::COMMON_LIGATURES), + "no-common-ligatures" => + exclusive_value!((result, Self::COMMON_LIGATURES | + Self::NO_COMMON_LIGATURES + ) => Self::NO_COMMON_LIGATURES), + "discretionary-ligatures" => + exclusive_value!((result, Self::DISCRETIONARY_LIGATURES | + Self::NO_DISCRETIONARY_LIGATURES + ) => Self::DISCRETIONARY_LIGATURES), + "no-discretionary-ligatures" => + exclusive_value!((result, Self::DISCRETIONARY_LIGATURES | + Self::NO_DISCRETIONARY_LIGATURES + ) => Self::NO_DISCRETIONARY_LIGATURES), + "historical-ligatures" => + exclusive_value!((result, Self::HISTORICAL_LIGATURES | + Self::NO_HISTORICAL_LIGATURES + ) => Self::HISTORICAL_LIGATURES), + "no-historical-ligatures" => + exclusive_value!((result, Self::HISTORICAL_LIGATURES | + Self::NO_HISTORICAL_LIGATURES + ) => Self::NO_HISTORICAL_LIGATURES), + "contextual" => + exclusive_value!((result, Self::CONTEXTUAL | + Self::NO_CONTEXTUAL + ) => Self::CONTEXTUAL), + "no-contextual" => + exclusive_value!((result, Self::CONTEXTUAL | + Self::NO_CONTEXTUAL + ) => Self::NO_CONTEXTUAL), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_numeric { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants of numeric values + pub struct FontVariantNumeric(u8); + bitflags! { + impl FontVariantNumeric: u8 { + /// None of other variants are enabled. + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantNumeric { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(FontVariantNumeric::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_numeric_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u8, FontVariantNumeric::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantNumeric { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_numeric! { + /// Enables display of lining numerals. + LINING_NUMS / "lining-nums" => NS_FONT_VARIANT_NUMERIC_LINING = 0x01, + /// Enables display of old-style numerals. + OLDSTYLE_NUMS / "oldstyle-nums" => NS_FONT_VARIANT_NUMERIC_OLDSTYLE = 0x02, + /// Enables display of proportional numerals. + PROPORTIONAL_NUMS / "proportional-nums" => NS_FONT_VARIANT_NUMERIC_PROPORTIONAL = 0x04, + /// Enables display of tabular numerals. + TABULAR_NUMS / "tabular-nums" => NS_FONT_VARIANT_NUMERIC_TABULAR = 0x08, + /// Enables display of lining diagonal fractions. + DIAGONAL_FRACTIONS / "diagonal-fractions" => NS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS = 0x10, + /// Enables display of lining stacked fractions. + STACKED_FRACTIONS / "stacked-fractions" => NS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS = 0x20, + /// Enables display of letter forms used with ordinal numbers. + ORDINAL / "ordinal" => NS_FONT_VARIANT_NUMERIC_ORDINAL = 0x80, + /// Enables display of slashed zeros. + SLASHED_ZERO / "slashed-zero" => NS_FONT_VARIANT_NUMERIC_SLASHZERO = 0x40, +} + +#[cfg(feature = "gecko")] +impl FontVariantNumeric { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u8) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u8 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantNumeric, u8); + +impl Parse for FontVariantNumeric { + /// normal | + /// [ <numeric-figure-values> || + /// <numeric-spacing-values> || + /// <numeric-fraction-values> || + /// ordinal || + /// slashed-zero ] + /// <numeric-figure-values> = [ lining-nums | oldstyle-nums ] + /// <numeric-spacing-values> = [ proportional-nums | tabular-nums ] + /// <numeric-fraction-values> = [ diagonal-fractions | stacked-fractions ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "ordinal" => + exclusive_value!((result, Self::ORDINAL) => Self::ORDINAL), + "slashed-zero" => + exclusive_value!((result, Self::SLASHED_ZERO) => Self::SLASHED_ZERO), + "lining-nums" => + exclusive_value!((result, Self::LINING_NUMS | + Self::OLDSTYLE_NUMS + ) => Self::LINING_NUMS), + "oldstyle-nums" => + exclusive_value!((result, Self::LINING_NUMS | + Self::OLDSTYLE_NUMS + ) => Self::OLDSTYLE_NUMS), + "proportional-nums" => + exclusive_value!((result, Self::PROPORTIONAL_NUMS | + Self::TABULAR_NUMS + ) => Self::PROPORTIONAL_NUMS), + "tabular-nums" => + exclusive_value!((result, Self::PROPORTIONAL_NUMS | + Self::TABULAR_NUMS + ) => Self::TABULAR_NUMS), + "diagonal-fractions" => + exclusive_value!((result, Self::DIAGONAL_FRACTIONS | + Self::STACKED_FRACTIONS + ) => Self::DIAGONAL_FRACTIONS), + "stacked-fractions" => + exclusive_value!((result, Self::DIAGONAL_FRACTIONS | + Self::STACKED_FRACTIONS + ) => Self::STACKED_FRACTIONS), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// This property provides low-level control over OpenType or TrueType font features. +pub type FontFeatureSettings = FontSettings<FeatureTagValue<Integer>>; + +/// For font-language-override, use the same representation as the computed value. +pub use crate::values::computed::font::FontLanguageOverride; + +impl Parse for FontLanguageOverride { + /// normal | <string> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontLanguageOverride, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontLanguageOverride::normal()); + } + + let string = input.expect_string()?; + + // The OpenType spec requires tags to be 1 to 4 ASCII characters: + // https://learn.microsoft.com/en-gb/typography/opentype/spec/otff#data-types + if string.is_empty() || string.len() > 4 || !string.is_ascii() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut bytes = [b' '; 4]; + for (byte, str_byte) in bytes.iter_mut().zip(string.as_bytes()) { + *byte = *str_byte; + } + + Ok(FontLanguageOverride(u32::from_be_bytes(bytes))) + } +} + +/// A value for any of the font-synthesis-{weight,style,small-caps} properties. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum FontSynthesis { + /// This attribute may be synthesized if not supported by a face. + Auto, + /// Do not attempt to synthesis this style attribute. + None, +} + +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Allows authors to choose a palette from those supported by a color font +/// (and potentially @font-palette-values overrides). +pub struct FontPalette(Atom); + +#[allow(missing_docs)] +impl FontPalette { + pub fn normal() -> Self { + Self(atom!("normal")) + } + pub fn light() -> Self { + Self(atom!("light")) + } + pub fn dark() -> Self { + Self(atom!("dark")) + } +} + +impl Parse for FontPalette { + /// normal | light | dark | dashed-ident + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontPalette, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + match_ignore_ascii_case! { &ident, + "normal" => Ok(Self::normal()), + "light" => Ok(Self::light()), + "dark" => Ok(Self::dark()), + _ => if ident.starts_with("--") { + Ok(Self(Atom::from(ident.as_ref()))) + } else { + Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + }, + } + } +} + +impl ToCss for FontPalette { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +/// This property provides low-level control over OpenType or TrueType font +/// variations. +pub type FontVariationSettings = FontSettings<VariationValue<Number>>; + +fn parse_one_feature_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Integer, ParseError<'i>> { + if let Ok(integer) = input.try_parse(|i| Integer::parse_non_negative(context, i)) { + return Ok(integer); + } + + try_match_ident_ignore_ascii_case! { input, + "on" => Ok(Integer::new(1)), + "off" => Ok(Integer::new(0)), + } +} + +impl Parse for FeatureTagValue<Integer> { + /// https://drafts.csswg.org/css-fonts-4/#feature-tag-value + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = input + .try_parse(|i| parse_one_feature_value(context, i)) + .unwrap_or_else(|_| Integer::new(1)); + + Ok(Self { tag, value }) + } +} + +impl Parse for VariationValue<Number> { + /// This is the `<string> <number>` part of the font-variation-settings + /// syntax. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = Number::parse(context, input)?; + Ok(Self { tag, value }) + } +} + +/// A metrics override value for a @font-face descriptor +/// +/// https://drafts.csswg.org/css-fonts/#font-metrics-override-desc +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum MetricsOverride { + /// A non-negative `<percentage>` of the computed font size + Override(NonNegativePercentage), + /// Normal metrics from the font. + Normal, +} + +impl MetricsOverride { + #[inline] + /// Get default value with `normal` + pub fn normal() -> MetricsOverride { + MetricsOverride::Normal + } + + /// The ToComputedValue implementation, used for @font-face descriptors. + /// + /// Valid override percentages must be non-negative; we return -1.0 to indicate + /// the absence of an override (i.e. 'normal'). + #[inline] + pub fn compute(&self) -> ComputedPercentage { + match *self { + MetricsOverride::Normal => ComputedPercentage(-1.0), + MetricsOverride::Override(percent) => ComputedPercentage(percent.0.get()), + } + } +} + +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +/// How to do font-size scaling. +pub enum XTextScale { + /// Both min-font-size and text zoom are enabled. + All, + /// Text-only zoom is enabled, but min-font-size is not honored. + ZoomOnly, + /// Neither of them is enabled. + None, +} + +impl XTextScale { + /// Returns whether text zoom is enabled. + #[inline] + pub fn text_zoom_enabled(self) -> bool { + self != Self::None + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Internal property that reflects the lang attribute +pub struct XLang(#[css(skip)] pub Atom); + +impl XLang { + #[inline] + /// Get default value for `-x-lang` + pub fn get_initial_value() -> XLang { + XLang(atom!("")) + } +} + +impl Parse for XLang { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<XLang, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Specifies the minimum font size allowed due to changes in scriptlevel. +/// Ref: https://wiki.mozilla.org/MathML:mstyle +pub struct MozScriptMinSize(pub NoCalcLength); + +impl MozScriptMinSize { + #[inline] + /// Calculate initial value of -moz-script-min-size. + pub fn get_initial_value() -> Length { + Length::new(DEFAULT_SCRIPT_MIN_SIZE_PT as f32 * PX_PER_PT) + } +} + +impl Parse for MozScriptMinSize { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptMinSize, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// A value for the `math-depth` property. +/// https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum MathDepth { + /// Increment math-depth if math-style is compact. + AutoAdd, + + /// Add the function's argument to math-depth. + #[css(function)] + Add(Integer), + + /// Set math-depth to the specified value. + Absolute(Integer), +} + +impl Parse for MathDepth { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MathDepth, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("auto-add")) + .is_ok() + { + return Ok(MathDepth::AutoAdd); + } + if let Ok(math_depth_value) = input.try_parse(|input| Integer::parse(context, input)) { + return Ok(MathDepth::Absolute(math_depth_value)); + } + input.expect_function_matching("add")?; + let math_depth_delta_value = + input.parse_nested_block(|input| Integer::parse(context, input))?; + Ok(MathDepth::Add(math_depth_delta_value)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Specifies the multiplier to be used to adjust font size +/// due to changes in scriptlevel. +/// +/// Ref: https://www.w3.org/TR/MathML3/chapter3.html#presm.mstyle.attrs +pub struct MozScriptSizeMultiplier(pub f32); + +impl MozScriptSizeMultiplier { + #[inline] + /// Get default value of `-moz-script-size-multiplier` + pub fn get_initial_value() -> MozScriptSizeMultiplier { + MozScriptSizeMultiplier(DEFAULT_SCRIPT_SIZE_MULTIPLIER as f32) + } +} + +impl Parse for MozScriptSizeMultiplier { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptSizeMultiplier, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +impl From<f32> for MozScriptSizeMultiplier { + fn from(v: f32) -> Self { + MozScriptSizeMultiplier(v) + } +} + +impl From<MozScriptSizeMultiplier> for f32 { + fn from(v: MozScriptSizeMultiplier) -> f32 { + v.0 + } +} + +/// A specified value for the `line-height` property. +pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>; + +impl ToComputedValue for LineHeight { + type ComputedValue = computed::LineHeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(number) => { + GenericLineHeight::Number(number.to_computed_value(context)) + }, + GenericLineHeight::Length(ref non_negative_lp) => { + let result = match non_negative_lp.0 { + LengthPercentage::Length(NoCalcLength::Absolute(ref abs)) => { + context.maybe_zoom_text(abs.to_computed_value(context)) + }, + LengthPercentage::Length(ref length) => { + // line-height units specifically resolve against parent's + // font and line-height properties, while the rest of font + // relative units still resolve against the element's own + // properties. + length.to_computed_value_with_base_size( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ) + }, + LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0) + .to_computed_value( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ), + LengthPercentage::Calc(ref calc) => { + let computed_calc = calc.to_computed_value_zoomed( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ); + let base = context.style().get_font().clone_font_size().computed_size(); + computed_calc.resolve(base) + }, + }; + GenericLineHeight::Length(result.into()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(ref number) => { + GenericLineHeight::Number(NonNegativeNumber::from_computed_value(number)) + }, + GenericLineHeight::Length(ref length) => { + GenericLineHeight::Length(NoCalcLength::from_computed_value(&length.0).into()) + }, + } + } +} diff --git a/servo/components/style/values/specified/gecko.rs b/servo/components/style/values/specified/gecko.rs new file mode 100644 index 0000000000..e721add59c --- /dev/null +++ b/servo/components/style/values/specified/gecko.rs @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for legacy Gecko-only properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, Length, LengthPercentage}; +use crate::values::generics::rect::Rect; +use cssparser::{Parser, Token}; +use std::fmt; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +fn parse_pixel_or_percent<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<LengthPercentage, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + let value = match *token { + Token::Dimension { + value, ref unit, .. + } => { + match_ignore_ascii_case! { unit, + "px" => Ok(LengthPercentage::new_length(Length::new(value))), + _ => Err(()), + } + }, + Token::Percentage { unit_value, .. } => Ok(LengthPercentage::new_percent( + computed::Percentage(unit_value), + )), + _ => Err(()), + }; + value.map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) +} + +/// The value of an IntersectionObserver's rootMargin property. +/// +/// Only bare px or percentage values are allowed. Other length units and +/// calc() values are not allowed. +/// +/// <https://w3c.github.io/IntersectionObserver/#parse-a-root-margin> +#[repr(transparent)] +pub struct IntersectionObserverRootMargin(pub Rect<LengthPercentage>); + +impl Parse for IntersectionObserverRootMargin { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::Zero; + if input.is_exhausted() { + // If there are zero elements in tokens, set tokens to ["0px"]. + return Ok(IntersectionObserverRootMargin(Rect::all( + LengthPercentage::zero(), + ))); + } + let rect = Rect::parse_with(context, input, parse_pixel_or_percent)?; + Ok(IntersectionObserverRootMargin(rect)) + } +} + +// Strictly speaking this is not ToCss. It's serializing for DOM. But +// we can just reuse the infrastructure of this. +// +// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin> +impl ToCss for IntersectionObserverRootMargin { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + // We cannot use the ToCss impl of Rect, because that would + // merge items when they are equal. We want to list them all. + let mut writer = SequenceWriter::new(dest, " "); + let rect = &self.0; + writer.item(&rect.0)?; + writer.item(&rect.1)?; + writer.item(&rect.2)?; + writer.item(&rect.3) + } +} diff --git a/servo/components/style/values/specified/grid.rs b/servo/components/style/values/specified/grid.rs new file mode 100644 index 0000000000..5c78ff399b --- /dev/null +++ b/servo/components/style/values/specified/grid.rs @@ -0,0 +1,441 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [grids](https://drafts.csswg.org/css-grid/) + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::grid::{GridTemplateComponent, ImplicitGridTracks, RepeatCount}; +use crate::values::generics::grid::{LineNameList, LineNameListValue, NameRepeat, TrackBreadth}; +use crate::values::generics::grid::{TrackList, TrackListValue, TrackRepeat, TrackSize}; +use crate::values::specified::{Integer, LengthPercentage}; +use crate::values::{CSSFloat, CustomIdent}; +use cssparser::{Parser, Token}; +use std::mem; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// Parse a single flexible length. +pub fn parse_flex<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CSSFloat, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Dimension { + value, ref unit, .. + } if unit.eq_ignore_ascii_case("fr") && value.is_sign_positive() => Ok(value), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } +} + +impl<L> TrackBreadth<L> { + fn parse_keyword<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + #[derive(Parse)] + enum TrackKeyword { + Auto, + MaxContent, + MinContent, + } + + Ok(match TrackKeyword::parse(input)? { + TrackKeyword::Auto => TrackBreadth::Auto, + TrackKeyword::MaxContent => TrackBreadth::MaxContent, + TrackKeyword::MinContent => TrackBreadth::MinContent, + }) + } +} + +impl Parse for TrackBreadth<LengthPercentage> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // FIXME: This and other callers in this file should use + // NonNegativeLengthPercentage instead. + // + // Though it seems these cannot be animated so it's ~ok. + if let Ok(lp) = input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) { + return Ok(TrackBreadth::Breadth(lp)); + } + + if let Ok(f) = input.try_parse(parse_flex) { + return Ok(TrackBreadth::Fr(f)); + } + + Self::parse_keyword(input) + } +} + +impl Parse for TrackSize<LengthPercentage> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(b) = input.try_parse(|i| TrackBreadth::parse(context, i)) { + return Ok(TrackSize::Breadth(b)); + } + + if input + .try_parse(|i| i.expect_function_matching("minmax")) + .is_ok() + { + return input.parse_nested_block(|input| { + let inflexible_breadth = + match input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) { + Ok(lp) => TrackBreadth::Breadth(lp), + Err(..) => TrackBreadth::parse_keyword(input)?, + }; + + input.expect_comma()?; + Ok(TrackSize::Minmax( + inflexible_breadth, + TrackBreadth::parse(context, input)?, + )) + }); + } + + input.expect_function_matching("fit-content")?; + let lp = input.parse_nested_block(|i| LengthPercentage::parse_non_negative(context, i))?; + Ok(TrackSize::FitContent(TrackBreadth::Breadth(lp))) + } +} + +impl Parse for ImplicitGridTracks<TrackSize<LengthPercentage>> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use style_traits::{Separator, Space}; + let track_sizes = Space::parse(input, |i| TrackSize::parse(context, i))?; + if track_sizes.len() == 1 && track_sizes[0].is_initial() { + // A single track with the initial value is always represented by an empty slice. + return Ok(Default::default()); + } + return Ok(ImplicitGridTracks(track_sizes.into())); + } +} + +/// Parse the grid line names into a vector of owned strings. +/// +/// <https://drafts.csswg.org/css-grid/#typedef-line-names> +pub fn parse_line_names<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<crate::OwnedSlice<CustomIdent>, ParseError<'i>> { + input.expect_square_bracket_block()?; + input.parse_nested_block(|input| { + let mut values = vec![]; + while let Ok(ident) = input.try_parse(|i| CustomIdent::parse(i, &["span", "auto"])) { + values.push(ident); + } + + Ok(values.into()) + }) +} + +/// The type of `repeat` function (only used in parsing). +/// +/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat> +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +enum RepeatType { + /// [`<auto-repeat>`](https://drafts.csswg.org/css-grid/#typedef-auto-repeat) + Auto, + /// [`<track-repeat>`](https://drafts.csswg.org/css-grid/#typedef-track-repeat) + Normal, + /// [`<fixed-repeat>`](https://drafts.csswg.org/css-grid/#typedef-fixed-repeat) + Fixed, +} + +impl TrackRepeat<LengthPercentage, Integer> { + fn parse_with_repeat_type<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(Self, RepeatType), ParseError<'i>> { + input + .try_parse(|i| i.expect_function_matching("repeat").map_err(|e| e.into())) + .and_then(|_| { + input.parse_nested_block(|input| { + let count = RepeatCount::parse(context, input)?; + input.expect_comma()?; + + let is_auto = count == RepeatCount::AutoFit || count == RepeatCount::AutoFill; + let mut repeat_type = if is_auto { + RepeatType::Auto + } else { + // <fixed-size> is a subset of <track-size>, so it should work for both + RepeatType::Fixed + }; + + let mut names = vec![]; + let mut values = vec![]; + let mut current_names; + + loop { + current_names = input.try_parse(parse_line_names).unwrap_or_default(); + if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + if is_auto { + // should be <fixed-size> for <auto-repeat> + return Err(input + .new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if repeat_type == RepeatType::Fixed { + repeat_type = RepeatType::Normal // <track-size> for sure + } + } + + values.push(track_size); + names.push(current_names); + } else { + if values.is_empty() { + // expecting at least one <track-size> + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + + names.push(current_names); // final `<line-names>` + break; // no more <track-size>, breaking + } + } + + let repeat = TrackRepeat { + count, + track_sizes: values.into(), + line_names: names.into(), + }; + + Ok((repeat, repeat_type)) + }) + }) + } +} + +impl Parse for TrackList<LengthPercentage, Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut current_names = vec![]; + let mut names = vec![]; + let mut values = vec![]; + + // Whether we've parsed an `<auto-repeat>` value. + let mut auto_repeat_index = None; + // assume that everything is <fixed-size>. This flag is useful when we encounter <auto-repeat> + let mut at_least_one_not_fixed = false; + loop { + current_names + .extend_from_slice(&mut input.try_parse(parse_line_names).unwrap_or_default()); + if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + at_least_one_not_fixed = true; + if auto_repeat_index.is_some() { + // <auto-track-list> only accepts <fixed-size> and <fixed-repeat> + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + let vec = mem::replace(&mut current_names, vec![]); + names.push(vec.into()); + values.push(TrackListValue::TrackSize(track_size)); + } else if let Ok((repeat, type_)) = + input.try_parse(|i| TrackRepeat::parse_with_repeat_type(context, i)) + { + match type_ { + RepeatType::Normal => { + at_least_one_not_fixed = true; + if auto_repeat_index.is_some() { + // only <fixed-repeat> + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + }, + RepeatType::Auto => { + if auto_repeat_index.is_some() || at_least_one_not_fixed { + // We've either seen <auto-repeat> earlier, or there's at least one non-fixed value + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + auto_repeat_index = Some(values.len()); + }, + RepeatType::Fixed => {}, + } + + let vec = mem::replace(&mut current_names, vec![]); + names.push(vec.into()); + values.push(TrackListValue::TrackRepeat(repeat)); + } else { + if values.is_empty() && auto_repeat_index.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + names.push(current_names.into()); + break; + } + } + + Ok(TrackList { + auto_repeat_index: auto_repeat_index.unwrap_or(std::usize::MAX), + values: values.into(), + line_names: names.into(), + }) + } +} + +#[cfg(feature = "gecko")] +#[inline] +fn allow_grid_template_subgrids() -> bool { + true +} + +#[cfg(feature = "servo")] +#[inline] +fn allow_grid_template_subgrids() -> bool { + false +} + +#[cfg(feature = "gecko")] +#[inline] +fn allow_grid_template_masonry() -> bool { + static_prefs::pref!("layout.css.grid-template-masonry-value.enabled") +} + +#[cfg(feature = "servo")] +#[inline] +fn allow_grid_template_masonry() -> bool { + false +} + +impl Parse for GridTemplateComponent<LengthPercentage, Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(GridTemplateComponent::None); + } + + Self::parse_without_none(context, input) + } +} + +impl GridTemplateComponent<LengthPercentage, Integer> { + /// Parses a `GridTemplateComponent<LengthPercentage>` except `none` keyword. + pub fn parse_without_none<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if allow_grid_template_subgrids() { + if let Ok(t) = input.try_parse(|i| LineNameList::parse(context, i)) { + return Ok(GridTemplateComponent::Subgrid(Box::new(t))); + } + } + if allow_grid_template_masonry() { + if input + .try_parse(|i| i.expect_ident_matching("masonry")) + .is_ok() + { + return Ok(GridTemplateComponent::Masonry); + } + } + let track_list = TrackList::parse(context, input)?; + Ok(GridTemplateComponent::TrackList(Box::new(track_list))) + } +} + +impl Parse for NameRepeat<Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("repeat")?; + input.parse_nested_block(|i| { + let count = RepeatCount::parse(context, i)?; + // NameRepeat doesn't accept `auto-fit` + // https://drafts.csswg.org/css-grid/#typedef-name-repeat + if matches!(count, RepeatCount::AutoFit) { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + i.expect_comma()?; + let mut names_list = vec![]; + names_list.push(parse_line_names(i)?); // there should be at least one + while let Ok(names) = i.try_parse(parse_line_names) { + names_list.push(names); + } + + Ok(NameRepeat { + count, + line_names: names_list.into(), + }) + }) + } +} + +impl Parse for LineNameListValue<Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(repeat) = input.try_parse(|i| NameRepeat::parse(context, i)) { + return Ok(LineNameListValue::Repeat(repeat)); + } + + parse_line_names(input).map(LineNameListValue::LineNames) + } +} + +impl LineNameListValue<Integer> { + /// Returns the length of `<line-names>` after expanding repeat(N, ...). This returns zero for + /// repeat(auto-fill, ...). + #[inline] + pub fn line_names_length(&self) -> usize { + match *self { + Self::LineNames(..) => 1, + Self::Repeat(ref r) => { + match r.count { + // Note: RepeatCount is always >= 1. + RepeatCount::Number(v) => r.line_names.len() * v.value() as usize, + _ => 0, + } + }, + } + } +} + +impl Parse for LineNameList<Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_ident_matching("subgrid")?; + + let mut auto_repeat = false; + let mut expanded_line_names_length = 0; + let mut line_names = vec![]; + while let Ok(value) = input.try_parse(|i| LineNameListValue::parse(context, i)) { + match value { + LineNameListValue::Repeat(ref r) if r.is_auto_fill() => { + if auto_repeat { + // On a subgridded axis, the auto-fill keyword is only valid once per + // <line-name-list>. + // https://drafts.csswg.org/css-grid/#auto-repeat + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + auto_repeat = true; + }, + _ => (), + }; + + expanded_line_names_length += value.line_names_length(); + line_names.push(value); + } + + Ok(LineNameList { + expanded_line_names_length, + line_names: line_names.into(), + }) + } +} diff --git a/servo/components/style/values/specified/image.rs b/servo/components/style/values/specified/image.rs new file mode 100644 index 0000000000..76bbbf85df --- /dev/null +++ b/servo/components/style/values/specified/image.rs @@ -0,0 +1,1340 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the specified value of +//! [`image`][image]s +//! +//! [image]: https://drafts.csswg.org/css-images/#image-values + +use crate::color::mix::ColorInterpolationMethod; +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::CorsMode; +use crate::values::generics::color::ColorMixFlags; +use crate::values::generics::image::{ + self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent, +}; +use crate::values::generics::image::{GradientFlags, PaintWorklet}; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::NonNegative; +use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword}; +use crate::values::specified::position::{Position, PositionComponent, Side}; +use crate::values::specified::url::SpecifiedImageUrl; +use crate::values::specified::{ + Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength, + NonNegativeLengthPercentage, Resolution, +}; +use crate::values::specified::{Number, NumberOrPercentage, Percentage}; +use crate::Atom; +use cssparser::{Delimiter, Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +#[cfg(feature = "servo")] +use servo_url::ServoUrl; +use std::cmp::Ordering; +use std::fmt::{self, Write}; +use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[inline] +fn gradient_color_interpolation_method_enabled() -> bool { + static_prefs::pref!("layout.css.gradient-color-interpolation-method.enabled") +} + +/// Specified values for an image according to CSS-IMAGES. +/// <https://drafts.csswg.org/css-images/#image-values> +pub type Image = generic::Image<Gradient, SpecifiedImageUrl, Color, Percentage, Resolution>; + +// Images should remain small, see https://github.com/servo/servo/pull/18430 +size_of_test!(Image, 16); + +/// Specified values for a CSS gradient. +/// <https://drafts.csswg.org/css-images/#gradients> +pub type Gradient = generic::Gradient< + LineDirection, + LengthPercentage, + NonNegativeLength, + NonNegativeLengthPercentage, + Position, + Angle, + AngleOrPercentage, + Color, +>; + +/// Specified values for CSS cross-fade +/// cross-fade( CrossFadeElement, ...) +/// <https://drafts.csswg.org/css-images-4/#cross-fade-function> +pub type CrossFade = generic::CrossFade<Image, Color, Percentage>; +/// CrossFadeElement = percent? CrossFadeImage +pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>; +/// CrossFadeImage = image | color +pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>; + +/// `image-set()` +pub type ImageSet = generic::ImageSet<Image, Resolution>; + +/// Each of the arguments to `image-set()` +pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>; + +type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>; + +impl Color { + fn has_modern_syntax(&self) -> bool { + match self { + Self::Absolute(absolute) => !absolute.color.is_legacy_syntax(), + Self::ColorMix(mix) => { + if mix.flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) { + true + } else { + mix.left.has_modern_syntax() || mix.right.has_modern_syntax() + } + }, + Self::LightDark(ld) => ld.light.has_modern_syntax() || ld.dark.has_modern_syntax(), + + // The default is that this color doesn't have any modern syntax. + _ => false, + } + } +} + +fn default_color_interpolation_method<T>( + items: &[generic::GradientItem<Color, T>], +) -> ColorInterpolationMethod { + let has_modern_syntax_item = items.iter().any(|item| match item { + generic::GenericGradientItem::SimpleColorStop(color) => color.has_modern_syntax(), + generic::GenericGradientItem::ComplexColorStop { color, .. } => color.has_modern_syntax(), + generic::GenericGradientItem::InterpolationHint(_) => false, + }); + + if has_modern_syntax_item { + ColorInterpolationMethod::oklab() + } else { + ColorInterpolationMethod::srgb() + } +} + +#[cfg(feature = "gecko")] +fn cross_fade_enabled() -> bool { + static_prefs::pref!("layout.css.cross-fade.enabled") +} + +#[cfg(feature = "servo")] +fn cross_fade_enabled() -> bool { + false +} + +impl SpecifiedValueInfo for Gradient { + const SUPPORTED_TYPES: u8 = CssType::GRADIENT; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + // This list here should keep sync with that in Gradient::parse. + f(&[ + "linear-gradient", + "-webkit-linear-gradient", + "-moz-linear-gradient", + "repeating-linear-gradient", + "-webkit-repeating-linear-gradient", + "-moz-repeating-linear-gradient", + "radial-gradient", + "-webkit-radial-gradient", + "-moz-radial-gradient", + "repeating-radial-gradient", + "-webkit-repeating-radial-gradient", + "-moz-repeating-radial-gradient", + "-webkit-gradient", + "conic-gradient", + "repeating-conic-gradient", + ]); + } +} + +// Need to manually implement as whether or not cross-fade shows up in +// completions & etc is dependent on it being enabled. +impl<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> { + const SUPPORTED_TYPES: u8 = 0; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + if cross_fade_enabled() { + f(&["cross-fade"]); + } + } +} + +impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> { + const SUPPORTED_TYPES: u8 = 0; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["image-set"]); + } +} + +/// A specified gradient line direction. +/// +/// FIXME(emilio): This should be generic over Angle. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum LineDirection { + /// An angular direction. + Angle(Angle), + /// A horizontal direction. + Horizontal(HorizontalPositionKeyword), + /// A vertical direction. + Vertical(VerticalPositionKeyword), + /// A direction towards a corner of a box. + Corner(HorizontalPositionKeyword, VerticalPositionKeyword), +} + +/// A specified ending shape. +pub type EndingShape = generic::EndingShape<NonNegativeLength, NonNegativeLengthPercentage>; + +bitflags! { + #[derive(Clone, Copy)] + struct ParseImageFlags: u8 { + const FORBID_NONE = 1 << 0; + const FORBID_IMAGE_SET = 1 << 1; + const FORBID_NON_URL = 1 << 2; + } +} + +impl Parse for Image { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Image::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::empty()) + } +} + +impl Image { + fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Image, ParseError<'i>> { + if !flags.contains(ParseImageFlags::FORBID_NONE) && + input.try_parse(|i| i.expect_ident_matching("none")).is_ok() + { + return Ok(generic::Image::None); + } + + if let Ok(url) = input + .try_parse(|input| SpecifiedImageUrl::parse_with_cors_mode(context, input, cors_mode)) + { + return Ok(generic::Image::Url(url)); + } + + if !flags.contains(ParseImageFlags::FORBID_IMAGE_SET) { + if let Ok(is) = + input.try_parse(|input| ImageSet::parse(context, input, cors_mode, flags)) + { + return Ok(generic::Image::ImageSet(Box::new(is))); + } + } + + if flags.contains(ParseImageFlags::FORBID_NON_URL) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) { + return Ok(generic::Image::Gradient(Box::new(gradient))); + } + + let function = input.expect_function()?.clone(); + input.parse_nested_block(|input| { + Ok(match_ignore_ascii_case! { &function, + #[cfg(feature = "servo-layout-2013")] + "paint" => Self::PaintWorklet(PaintWorklet::parse_args(context, input)?), + "cross-fade" if cross_fade_enabled() => Self::CrossFade(Box::new(CrossFade::parse_args(context, input, cors_mode, flags)?)), + #[cfg(feature = "gecko")] + "-moz-element" => Self::Element(Self::parse_element(input)?), + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function))), + }) + }) + } +} + +impl Image { + /// Creates an already specified image value from an already resolved URL + /// for insertion in the cascade. + #[cfg(feature = "servo")] + pub fn for_cascade(url: ServoUrl) -> Self { + use crate::values::CssUrl; + generic::Image::Url(CssUrl::for_cascade(url)) + } + + /// Parses a `-moz-element(# <element-id>)`. + #[cfg(feature = "gecko")] + fn parse_element<'i>(input: &mut Parser<'i, '_>) -> Result<Atom, ParseError<'i>> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::IDHash(ref id) => Atom::from(id.as_ref()), + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } + + /// Provides an alternate method for parsing that associates the URL with + /// anonymous CORS headers. + pub fn parse_with_cors_anonymous<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Self::parse_with_cors_mode( + context, + input, + CorsMode::Anonymous, + ParseImageFlags::empty(), + ) + } + + /// Provides an alternate method for parsing, but forbidding `none` + pub fn parse_forbid_none<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Self::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::FORBID_NONE) + } + + /// Provides an alternate method for parsing, but only for urls. + pub fn parse_only_url<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Self::parse_with_cors_mode( + context, + input, + CorsMode::None, + ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL, + ) + } +} + +impl CrossFade { + /// cross-fade() = cross-fade( <cf-image># ) + fn parse_args<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + let elements = crate::OwnedSlice::from(input.parse_comma_separated(|input| { + CrossFadeElement::parse(context, input, cors_mode, flags) + })?); + Ok(Self { elements }) + } +} + +impl CrossFadeElement { + fn parse_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Option<Percentage> { + // We clamp our values here as this is the way that Safari and Chrome's + // implementation handle out-of-bounds percentages but whether or not + // this behavior follows the specification is still being discussed. + // See: <https://github.com/w3c/csswg-drafts/issues/5333> + input + .try_parse(|input| Percentage::parse_non_negative(context, input)) + .ok() + .map(|p| p.clamp_to_hundred()) + } + + /// <cf-image> = <percentage>? && [ <image> | <color> ] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + // Try and parse a leading percent sign. + let mut percent = Self::parse_percentage(context, input); + // Parse the image + let image = CrossFadeImage::parse(context, input, cors_mode, flags)?; + // Try and parse a trailing percent sign. + if percent.is_none() { + percent = Self::parse_percentage(context, input); + } + Ok(Self { + percent: percent.into(), + image, + }) + } +} + +impl CrossFadeImage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + if let Ok(image) = input.try_parse(|input| { + Image::parse_with_cors_mode( + context, + input, + cors_mode, + flags | ParseImageFlags::FORBID_NONE, + ) + }) { + return Ok(Self::Image(image)); + } + Ok(Self::Color(Color::parse(context, input)?)) + } +} + +impl ImageSet { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + let function = input.expect_function()?; + match_ignore_ascii_case! { &function, + "-webkit-image-set" | "image-set" => {}, + _ => { + let func = function.clone(); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func))); + } + } + let items = input.parse_nested_block(|input| { + input.parse_comma_separated(|input| { + ImageSetItem::parse(context, input, cors_mode, flags) + }) + })?; + Ok(Self { + selected_index: std::usize::MAX, + items: items.into(), + }) + } +} + +impl ImageSetItem { + fn parse_type<'i>(p: &mut Parser<'i, '_>) -> Result<crate::OwnedStr, ParseError<'i>> { + p.expect_function_matching("type")?; + p.parse_nested_block(|input| Ok(input.expect_string()?.as_ref().to_owned().into())) + } + + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + let image = match input.try_parse(|i| i.expect_url_or_string()) { + Ok(url) => Image::Url(SpecifiedImageUrl::parse_from_string( + url.as_ref().into(), + context, + cors_mode, + )), + Err(..) => Image::parse_with_cors_mode( + context, + input, + cors_mode, + flags | ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_IMAGE_SET, + )?, + }; + + let mut resolution = input + .try_parse(|input| Resolution::parse(context, input)) + .ok(); + let mime_type = input.try_parse(Self::parse_type).ok(); + + // Try to parse resolution after type(). + if mime_type.is_some() && resolution.is_none() { + resolution = input + .try_parse(|input| Resolution::parse(context, input)) + .ok(); + } + + let resolution = resolution.unwrap_or_else(|| Resolution::from_x(1.0)); + let has_mime_type = mime_type.is_some(); + let mime_type = mime_type.unwrap_or_default(); + + Ok(Self { + image, + resolution, + has_mime_type, + mime_type, + }) + } +} + +impl Parse for Gradient { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + enum Shape { + Linear, + Radial, + Conic, + } + + let func = input.expect_function()?; + let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func, + "linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::Modern) + }, + "-webkit-linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::Moz) + }, + "repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::Modern) + }, + "-webkit-repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::Moz) + }, + "radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::Modern) + }, + "-webkit-radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::Moz) + }, + "repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::Modern) + }, + "-webkit-repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::Moz) + }, + "conic-gradient" => { + (Shape::Conic, false, GradientCompatMode::Modern) + }, + "repeating-conic-gradient" => { + (Shape::Conic, true, GradientCompatMode::Modern) + }, + "-webkit-gradient" => { + return input.parse_nested_block(|i| { + Self::parse_webkit_gradient_argument(context, i) + }); + }, + _ => { + let func = func.clone(); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func))); + } + }; + + Ok(input.parse_nested_block(|i| { + Ok(match shape { + Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?, + Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?, + Shape::Conic => Self::parse_conic(context, i, repeating)?, + }) + })?) + } +} + +impl Gradient { + fn parse_webkit_gradient_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::position::{ + HorizontalPositionKeyword as X, VerticalPositionKeyword as Y, + }; + type Point = GenericPosition<Component<X>, Component<Y>>; + + #[derive(Clone, Copy, Parse)] + enum Component<S> { + Center, + Number(NumberOrPercentage), + Side(S), + } + + impl LineDirection { + fn from_points(first: Point, second: Point) -> Self { + let h_ord = first.horizontal.partial_cmp(&second.horizontal); + let v_ord = first.vertical.partial_cmp(&second.vertical); + let (h, v) = match (h_ord, v_ord) { + (Some(h), Some(v)) => (h, v), + _ => return LineDirection::Vertical(Y::Bottom), + }; + match (h, v) { + (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom), + (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right), + (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top), + (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top), + (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => { + LineDirection::Vertical(Y::Bottom) + }, + (Ordering::Greater, Ordering::Less) => { + LineDirection::Corner(X::Left, Y::Bottom) + }, + (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left), + (Ordering::Greater, Ordering::Greater) => { + LineDirection::Corner(X::Left, Y::Top) + }, + } + } + } + + impl From<Point> for Position { + fn from(point: Point) -> Self { + Self::new(point.horizontal.into(), point.vertical.into()) + } + } + + impl Parse for Point { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| { + let x = Component::parse(context, i)?; + let y = Component::parse(context, i)?; + + Ok(Self::new(x, y)) + }) + } + } + + impl<S: Side> From<Component<S>> for NumberOrPercentage { + fn from(component: Component<S>) -> Self { + match component { + Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)), + Component::Number(number) => number, + Component::Side(side) => { + let p = if side.is_start() { + Percentage::zero() + } else { + Percentage::hundred() + }; + NumberOrPercentage::Percentage(p) + }, + } + } + } + + impl<S: Side> From<Component<S>> for PositionComponent<S> { + fn from(component: Component<S>) -> Self { + match component { + Component::Center => PositionComponent::Center, + Component::Number(NumberOrPercentage::Number(number)) => { + PositionComponent::Length(Length::from_px(number.value).into()) + }, + Component::Number(NumberOrPercentage::Percentage(p)) => { + PositionComponent::Length(p.into()) + }, + Component::Side(side) => PositionComponent::Side(side, None), + } + } + } + + impl<S: Copy + Side> Component<S> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match ( + NumberOrPercentage::from(*self), + NumberOrPercentage::from(*other), + ) { + (NumberOrPercentage::Percentage(a), NumberOrPercentage::Percentage(b)) => { + a.get().partial_cmp(&b.get()) + }, + (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => { + a.value.partial_cmp(&b.value) + }, + (_, _) => None, + } + } + } + + let ident = input.expect_ident_cloned()?; + input.expect_comma()?; + + Ok(match_ignore_ascii_case! { &ident, + "linear" => { + let first = Point::parse(context, input)?; + input.expect_comma()?; + let second = Point::parse(context, input)?; + + let direction = LineDirection::from_points(first, second); + let items = Gradient::parse_webkit_gradient_stops(context, input, false)?; + + generic::Gradient::Linear { + direction, + color_interpolation_method: ColorInterpolationMethod::srgb(), + items, + // Legacy gradients always use srgb as a default. + flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + compat_mode: GradientCompatMode::Modern, + } + }, + "radial" => { + let first_point = Point::parse(context, input)?; + input.expect_comma()?; + let first_radius = Number::parse_non_negative(context, input)?; + input.expect_comma()?; + let second_point = Point::parse(context, input)?; + input.expect_comma()?; + let second_radius = Number::parse_non_negative(context, input)?; + + let (reverse_stops, point, radius) = if second_radius.value >= first_radius.value { + (false, second_point, second_radius) + } else { + (true, first_point, first_radius) + }; + + let rad = Circle::Radius(NonNegative(Length::from_px(radius.value))); + let shape = generic::EndingShape::Circle(rad); + let position: Position = point.into(); + let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?; + + generic::Gradient::Radial { + shape, + position, + color_interpolation_method: ColorInterpolationMethod::srgb(), + items, + // Legacy gradients always use srgb as a default. + flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + compat_mode: GradientCompatMode::Modern, + } + }, + _ => { + let e = SelectorParseErrorKind::UnexpectedIdent(ident.clone()); + return Err(input.new_custom_error(e)); + }, + }) + } + + fn parse_webkit_gradient_stops<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + reverse_stops: bool, + ) -> Result<LengthPercentageItemList, ParseError<'i>> { + let mut items = input + .try_parse(|i| { + i.expect_comma()?; + i.parse_comma_separated(|i| { + let function = i.expect_function()?.clone(); + let (color, mut p) = i.parse_nested_block(|i| { + let p = match_ignore_ascii_case! { &function, + "color-stop" => { + let p = NumberOrPercentage::parse(context, i)?.to_percentage(); + i.expect_comma()?; + p + }, + "from" => Percentage::zero(), + "to" => Percentage::hundred(), + _ => { + return Err(i.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + }; + let color = Color::parse(context, i)?; + if color == Color::CurrentColor { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok((color.into(), p)) + })?; + if reverse_stops { + p.reverse(); + } + Ok(generic::GradientItem::ComplexColorStop { + color, + position: p.into(), + }) + }) + }) + .unwrap_or(vec![]); + + if items.is_empty() { + items = vec![ + generic::GradientItem::ComplexColorStop { + color: Color::transparent(), + position: LengthPercentage::zero_percent(), + }, + generic::GradientItem::ComplexColorStop { + color: Color::transparent(), + position: LengthPercentage::hundred_percent(), + }, + ]; + } else if items.len() == 1 { + let first = items[0].clone(); + items.push(first); + } else { + items.sort_by(|a, b| { + match (a, b) { + ( + &generic::GradientItem::ComplexColorStop { + position: ref a_position, + .. + }, + &generic::GradientItem::ComplexColorStop { + position: ref b_position, + .. + }, + ) => match (a_position, b_position) { + (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) => { + return a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal); + }, + _ => {}, + }, + _ => {}, + } + if reverse_stops { + Ordering::Greater + } else { + Ordering::Less + } + }) + } + Ok(items.into()) + } + + /// Not used for -webkit-gradient syntax and conic-gradient + fn parse_stops<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<LengthPercentageItemList, ParseError<'i>> { + let items = + generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?; + if items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(items) + } + + /// Try to parse a color interpolation method. + fn try_parse_color_interpolation_method<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Option<ColorInterpolationMethod> { + if gradient_color_interpolation_method_enabled() { + input + .try_parse(|i| ColorInterpolationMethod::parse(context, i)) + .ok() + } else { + None + } + } + + /// Parses a linear gradient. + /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword. + fn parse_linear<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + mut compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + let mut flags = GradientFlags::empty(); + flags.set(GradientFlags::REPEATING, repeating); + + let mut color_interpolation_method = + Self::try_parse_color_interpolation_method(context, input); + + let direction = input + .try_parse(|p| LineDirection::parse(context, p, &mut compat_mode)) + .ok(); + + if direction.is_some() && color_interpolation_method.is_none() { + color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); + } + + // If either of the 2 options were specified, we require a comma. + if color_interpolation_method.is_some() || direction.is_some() { + input.expect_comma()?; + } + + let items = Gradient::parse_stops(context, input)?; + + let default = default_color_interpolation_method(&items); + let color_interpolation_method = color_interpolation_method.unwrap_or(default); + flags.set( + GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + default == color_interpolation_method, + ); + + let direction = direction.unwrap_or(match compat_mode { + GradientCompatMode::Modern => LineDirection::Vertical(VerticalPositionKeyword::Bottom), + _ => LineDirection::Vertical(VerticalPositionKeyword::Top), + }); + + Ok(Gradient::Linear { + direction, + color_interpolation_method, + items, + flags, + compat_mode, + }) + } + + /// Parses a radial gradient. + fn parse_radial<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + let mut flags = GradientFlags::empty(); + flags.set(GradientFlags::REPEATING, repeating); + + let mut color_interpolation_method = + Self::try_parse_color_interpolation_method(context, input); + + let (shape, position) = match compat_mode { + GradientCompatMode::Modern => { + let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode)); + let position = input.try_parse(|i| { + i.expect_ident_matching("at")?; + Position::parse(context, i) + }); + (shape, position.ok()) + }, + _ => { + let position = input.try_parse(|i| Position::parse(context, i)); + let shape = input.try_parse(|i| { + if position.is_ok() { + i.expect_comma()?; + } + EndingShape::parse(context, i, compat_mode) + }); + (shape, position.ok()) + }, + }; + + let has_shape_or_position = shape.is_ok() || position.is_some(); + if has_shape_or_position && color_interpolation_method.is_none() { + color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); + } + + if has_shape_or_position || color_interpolation_method.is_some() { + input.expect_comma()?; + } + + let shape = shape.unwrap_or({ + generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) + }); + + let position = position.unwrap_or(Position::center()); + + let items = Gradient::parse_stops(context, input)?; + + let default = default_color_interpolation_method(&items); + let color_interpolation_method = color_interpolation_method.unwrap_or(default); + flags.set( + GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + default == color_interpolation_method, + ); + + Ok(Gradient::Radial { + shape, + position, + color_interpolation_method, + items, + flags, + compat_mode, + }) + } + + /// Parse a conic gradient. + fn parse_conic<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + ) -> Result<Self, ParseError<'i>> { + let mut flags = GradientFlags::empty(); + flags.set(GradientFlags::REPEATING, repeating); + + let mut color_interpolation_method = + Self::try_parse_color_interpolation_method(context, input); + + let angle = input.try_parse(|i| { + i.expect_ident_matching("from")?; + // Spec allows unitless zero start angles + // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle + Angle::parse_with_unitless(context, i) + }); + let position = input.try_parse(|i| { + i.expect_ident_matching("at")?; + Position::parse(context, i) + }); + + let has_angle_or_position = angle.is_ok() || position.is_ok(); + if has_angle_or_position && color_interpolation_method.is_none() { + color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); + } + + if has_angle_or_position || color_interpolation_method.is_some() { + input.expect_comma()?; + } + + let angle = angle.unwrap_or(Angle::zero()); + + let position = position.unwrap_or(Position::center()); + + let items = generic::GradientItem::parse_comma_separated( + context, + input, + AngleOrPercentage::parse_with_unitless, + )?; + + if items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let default = default_color_interpolation_method(&items); + let color_interpolation_method = color_interpolation_method.unwrap_or(default); + flags.set( + GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + default == color_interpolation_method, + ); + + Ok(Gradient::Conic { + angle, + position, + color_interpolation_method, + items, + flags, + }) + } +} + +impl generic::LineDirection for LineDirection { + fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool { + match *self { + LineDirection::Angle(ref angle) => angle.degrees() == 180.0, + LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { + compat_mode == GradientCompatMode::Modern + }, + LineDirection::Vertical(VerticalPositionKeyword::Top) => { + compat_mode != GradientCompatMode::Modern + }, + _ => false, + } + } + + fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result + where + W: Write, + { + match *self { + LineDirection::Angle(angle) => angle.to_css(dest), + LineDirection::Horizontal(x) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest) + }, + LineDirection::Vertical(y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + y.to_css(dest) + }, + LineDirection::Corner(x, y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest)?; + dest.write_char(' ')?; + y.to_css(dest) + }, + } + } +} + +impl LineDirection { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + compat_mode: &mut GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + // Gradients allow unitless zero angles as an exception, see: + // https://github.com/w3c/csswg-drafts/issues/1162 + if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) { + return Ok(LineDirection::Angle(angle)); + } + + input.try_parse(|i| { + let to_ident = i.try_parse(|i| i.expect_ident_matching("to")); + match *compat_mode { + // `to` keyword is mandatory in modern syntax. + GradientCompatMode::Modern => to_ident?, + // Fall back to Modern compatibility mode in case there is a `to` keyword. + // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like + // `linear-gradient(to ...)`. + GradientCompatMode::Moz if to_ident.is_ok() => { + *compat_mode = GradientCompatMode::Modern + }, + // There is no `to` keyword in webkit prefixed syntax. If it's consumed, + // parsing should throw an error. + GradientCompatMode::WebKit if to_ident.is_ok() => { + return Err( + i.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("to".into())) + ); + }, + _ => {}, + } + + if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { + if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) { + return Ok(LineDirection::Corner(x, y)); + } + return Ok(LineDirection::Horizontal(x)); + } + let y = VerticalPositionKeyword::parse(i)?; + if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { + return Ok(LineDirection::Corner(x, y)); + } + Ok(LineDirection::Vertical(y)) + }) + } +} + +impl EndingShape { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + if input + .try_parse(|i| i.expect_ident_matching("circle")) + .is_ok() + { + return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); + } + let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); + } + if input + .try_parse(|i| i.expect_ident_matching("circle")) + .is_ok() + { + if let Ok(extent) = + input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); + } + if compat_mode == GradientCompatMode::Modern { + if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + return Ok(generic::EndingShape::Circle(Circle::Radius(length))); + } + } + return Ok(generic::EndingShape::Circle(Circle::Extent( + ShapeExtent::FarthestCorner, + ))); + } + if input + .try_parse(|i| i.expect_ident_matching("ellipse")) + .is_ok() + { + if let Ok(extent) = + input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); + } + if compat_mode == GradientCompatMode::Modern { + let pair: Result<_, ParseError> = input.try_parse(|i| { + let x = NonNegativeLengthPercentage::parse(context, i)?; + let y = NonNegativeLengthPercentage::parse(context, i)?; + Ok((x, y)) + }); + if let Ok((x, y)) = pair { + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y))); + } + } + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent( + ShapeExtent::FarthestCorner, + ))); + } + if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { + if compat_mode == GradientCompatMode::Modern { + let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); + } + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(length.0)), + y, + ))); + } + if compat_mode == GradientCompatMode::Modern { + let y = input.try_parse(|i| { + i.expect_ident_matching("ellipse")?; + NonNegativeLengthPercentage::parse(context, i) + }); + if let Ok(y) = y { + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(length.0)), + y, + ))); + } + let _ = input.try_parse(|i| i.expect_ident_matching("circle")); + } + + return Ok(generic::EndingShape::Circle(Circle::Radius(length))); + } + input.try_parse(|i| { + let x = Percentage::parse_non_negative(context, i)?; + let y = if let Ok(y) = i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { + if compat_mode == GradientCompatMode::Modern { + let _ = i.try_parse(|i| i.expect_ident_matching("ellipse")); + } + y + } else { + if compat_mode == GradientCompatMode::Modern { + i.expect_ident_matching("ellipse")?; + } + NonNegativeLengthPercentage::parse(context, i)? + }; + Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(x)), + y, + ))) + }) + } +} + +impl ShapeExtent { + fn parse_with_compat_mode<'i, 't>( + input: &mut Parser<'i, 't>, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + match Self::parse(input)? { + ShapeExtent::Contain | ShapeExtent::Cover + if compat_mode == GradientCompatMode::Modern => + { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide), + ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner), + keyword => Ok(keyword), + } + } +} + +impl<T> generic::GradientItem<Color, T> { + fn parse_comma_separated<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse_position: impl for<'i1, 't1> Fn(&ParserContext, &mut Parser<'i1, 't1>) -> Result<T, ParseError<'i1>> + + Copy, + ) -> Result<crate::OwnedSlice<Self>, ParseError<'i>> { + let mut items = Vec::new(); + let mut seen_stop = false; + + loop { + input.parse_until_before(Delimiter::Comma, |input| { + if seen_stop { + if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) { + seen_stop = false; + items.push(generic::GradientItem::InterpolationHint(hint)); + return Ok(()); + } + } + + let stop = generic::ColorStop::parse(context, input, parse_position)?; + + if let Ok(multi_position) = input.try_parse(|i| parse_position(context, i)) { + let stop_color = stop.color.clone(); + items.push(stop.into_item()); + items.push( + generic::ColorStop { + color: stop_color, + position: Some(multi_position), + } + .into_item(), + ); + } else { + items.push(stop.into_item()); + } + + seen_stop = true; + Ok(()) + })?; + + match input.next() { + Err(_) => break, + Ok(&Token::Comma) => continue, + Ok(_) => unreachable!(), + } + } + + if !seen_stop || items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(items.into()) + } +} + +impl<T> generic::ColorStop<Color, T> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse_position: impl for<'i1, 't1> Fn( + &ParserContext, + &mut Parser<'i1, 't1>, + ) -> Result<T, ParseError<'i1>>, + ) -> Result<Self, ParseError<'i>> { + Ok(generic::ColorStop { + color: Color::parse(context, input)?, + position: input.try_parse(|i| parse_position(context, i)).ok(), + }) + } +} + +impl PaintWorklet { + #[cfg(feature = "servo")] + fn parse_args<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + use crate::custom_properties::SpecifiedValue; + let name = Atom::from(&**input.expect_ident()?); + let arguments = input + .try_parse(|input| { + input.expect_comma()?; + input.parse_comma_separated(SpecifiedValue::parse) + }) + .unwrap_or_default(); + Ok(Self { name, arguments }) + } +} + +/// https://drafts.csswg.org/css-images/#propdef-image-rendering +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ImageRendering { + Auto, + Smooth, + #[parse(aliases = "-moz-crisp-edges")] + CrispEdges, + Pixelated, + // From the spec: + // + // This property previously accepted the values optimizeSpeed and + // optimizeQuality. These are now deprecated; a user agent must accept + // them as valid values but must treat them as having the same behavior + // as crisp-edges and smooth respectively, and authors must not use + // them. + // + Optimizespeed, + Optimizequality, +} diff --git a/servo/components/style/values/specified/length.rs b/servo/components/style/values/specified/length.rs new file mode 100644 index 0000000000..d2e1d7d346 --- /dev/null +++ b/servo/components/style/values/specified/length.rs @@ -0,0 +1,2031 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Length values][length]. +//! +//! [length]: https://drafts.csswg.org/css-values/#lengths + +use super::{AllowQuirks, Number, Percentage, ToComputedValue}; +use crate::computed_value_flags::ComputedValueFlags; +use crate::font_metrics::{FontMetrics, FontMetricsOrientation}; +use crate::gecko_bindings::structs::GeckoFontMetrics; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, CSSPixelLength, Context}; +use crate::values::generics::length as generics; +use crate::values::generics::length::{ + GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::calc::{self, CalcNode}; +use crate::values::specified::NonNegativeNumber; +use crate::values::CSSFloat; +use crate::{Zero, ZeroNoPercent}; +use app_units::AU_PER_PX; +use cssparser::{Parser, Token}; +use std::cmp; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +pub use super::image::Image; +pub use super::image::{EndingShape as GradientEndingShape, Gradient}; +pub use crate::values::specified::calc::CalcLengthPercentage; + +/// Number of pixels per inch +pub const PX_PER_IN: CSSFloat = 96.; +/// Number of pixels per centimeter +pub const PX_PER_CM: CSSFloat = PX_PER_IN / 2.54; +/// Number of pixels per millimeter +pub const PX_PER_MM: CSSFloat = PX_PER_IN / 25.4; +/// Number of pixels per quarter +pub const PX_PER_Q: CSSFloat = PX_PER_MM / 4.; +/// Number of pixels per point +pub const PX_PER_PT: CSSFloat = PX_PER_IN / 72.; +/// Number of pixels per pica +pub const PX_PER_PC: CSSFloat = PX_PER_PT * 12.; + +/// A font relative length. Note that if any new value is +/// added here, `custom_properties::NonCustomReferences::from_unit` +/// must also be updated. Consult the comment in that function as to why. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum FontRelativeLength { + /// A "em" value: https://drafts.csswg.org/css-values/#em + #[css(dimension)] + Em(CSSFloat), + /// A "ex" value: https://drafts.csswg.org/css-values/#ex + #[css(dimension)] + Ex(CSSFloat), + /// A "ch" value: https://drafts.csswg.org/css-values/#ch + #[css(dimension)] + Ch(CSSFloat), + /// A "cap" value: https://drafts.csswg.org/css-values/#cap + #[css(dimension)] + Cap(CSSFloat), + /// An "ic" value: https://drafts.csswg.org/css-values/#ic + #[css(dimension)] + Ic(CSSFloat), + /// A "rem" value: https://drafts.csswg.org/css-values/#rem + #[css(dimension)] + Rem(CSSFloat), + /// A "lh" value: https://drafts.csswg.org/css-values/#lh + #[css(dimension)] + Lh(CSSFloat), + /// A "rlh" value: https://drafts.csswg.org/css-values/#lh + #[css(dimension)] + Rlh(CSSFloat), +} + +/// A source to resolve font-relative units against +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FontBaseSize { + /// Use the font-size of the current element. + CurrentStyle, + /// Use the inherited font-size. + InheritedStyle, +} + +/// A source to resolve font-relative line-height units against. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineHeightBase { + /// Use the line-height of the current element. + CurrentStyle, + /// Use the inherited line-height. + InheritedStyle, +} + +impl FontBaseSize { + /// Calculate the actual size for a given context + pub fn resolve(&self, context: &Context) -> computed::FontSize { + match *self { + Self::CurrentStyle => context.style().get_font().clone_font_size(), + Self::InheritedStyle => context.style().get_parent_font().clone_font_size(), + } + } +} + +impl FontRelativeLength { + /// Unit identifier for `em`. + pub const EM: &'static str = "em"; + /// Unit identifier for `ex`. + pub const EX: &'static str = "ex"; + /// Unit identifier for `ch`. + pub const CH: &'static str = "ch"; + /// Unit identifier for `cap`. + pub const CAP: &'static str = "cap"; + /// Unit identifier for `ic`. + pub const IC: &'static str = "ic"; + /// Unit identifier for `rem`. + pub const REM: &'static str = "rem"; + /// Unit identifier for `lh`. + pub const LH: &'static str = "lh"; + /// Unit identifier for `rlh`. + pub const RLH: &'static str = "rlh"; + + /// Return the unitless, raw value. + fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Em(v) | + Self::Ex(v) | + Self::Ch(v) | + Self::Cap(v) | + Self::Ic(v) | + Self::Rem(v) | + Self::Lh(v) | + Self::Rlh(v) => v, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Em(_) => Self::EM, + Self::Ex(_) => Self::EX, + Self::Ch(_) => Self::CH, + Self::Cap(_) => Self::CAP, + Self::Ic(_) => Self::IC, + Self::Rem(_) => Self::REM, + Self::Lh(_) => Self::LH, + Self::Rlh(_) => Self::RLH, + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::FontRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Em(one), &Em(other)) => Em(op(one, other)), + (&Ex(one), &Ex(other)) => Ex(op(one, other)), + (&Ch(one), &Ch(other)) => Ch(op(one, other)), + (&Cap(one), &Cap(other)) => Cap(op(one, other)), + (&Ic(one), &Ic(other)) => Ic(op(one, other)), + (&Rem(one), &Rem(other)) => Rem(op(one, other)), + (&Lh(one), &Lh(other)) => Lh(op(one, other)), + (&Rlh(one), &Rlh(other)) => Rlh(op(one, other)), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + match self { + Self::Em(x) => Self::Em(op(*x)), + Self::Ex(x) => Self::Ex(op(*x)), + Self::Ch(x) => Self::Ch(op(*x)), + Self::Cap(x) => Self::Cap(op(*x)), + Self::Ic(x) => Self::Ic(op(*x)), + Self::Rem(x) => Self::Rem(op(*x)), + Self::Lh(x) => Self::Lh(op(*x)), + Self::Rlh(x) => Self::Lh(op(*x)), + } + } + + /// Computes the font-relative length. + pub fn to_computed_value( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> computed::Length { + let (reference_size, length) = + self.reference_font_size_and_length(context, base_size, line_height_base); + (reference_size * length).finite() + } + + /// Computes the length, given a GeckoFontMetrics getter to resolve font-relative units. + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: impl Fn() -> GeckoFontMetrics, + ) -> Result<CSSFloat, ()> { + let metrics = get_font_metrics(); + Ok(match *self { + Self::Em(v) => v * metrics.mComputedEmSize.px(), + Self::Ex(v) => v * metrics.mXSize.px(), + Self::Ch(v) => v * metrics.mChSize.px(), + Self::Cap(v) => v * metrics.mCapHeight.px(), + Self::Ic(v) => v * metrics.mIcWidth.px(), + // `lh`, `rlh` & `rem` are unsupported as we have no context for it. + Self::Rem(_) | Self::Lh(_) | Self::Rlh(_) => return Err(()), + }) + } + + /// Return reference font size. + /// + /// We use the base_size flag to pass a different size for computing + /// font-size and unconstrained font-size. + /// + /// This returns a pair, the first one is the reference font size, and the + /// second one is the unpacked relative length. + fn reference_font_size_and_length( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> (computed::Length, CSSFloat) { + fn query_font_metrics( + context: &Context, + base_size: FontBaseSize, + orientation: FontMetricsOrientation, + ) -> FontMetrics { + let retrieve_math_scales = false; + context.query_font_metrics(base_size, orientation, retrieve_math_scales) + } + + let reference_font_size = base_size.resolve(context); + match *self { + Self::Em(length) => { + if context.for_non_inherited_property && base_size == FontBaseSize::CurrentStyle { + context + .rule_cache_conditions + .borrow_mut() + .set_font_size_dependency(reference_font_size.computed_size); + } + + (reference_font_size.computed_size(), length) + }, + Self::Ex(length) => { + // The x-height is an intrinsically horizontal metric. + let metrics = + query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal); + let reference_size = metrics.x_height.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ex + // + // In the cases where it is impossible or impractical to + // determine the x-height, a value of 0.5em must be + // assumed. + // + // (But note we use 0.5em of the used, not computed + // font-size) + reference_font_size.used_size() * 0.5 + }); + (reference_size, length) + }, + Self::Ch(length) => { + // https://drafts.csswg.org/css-values/#ch: + // + // Equal to the used advance measure of the “0” (ZERO, + // U+0030) glyph in the font used to render it. (The advance + // measure of a glyph is its advance width or height, + // whichever is in the inline axis of the element.) + // + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::MatchContextPreferHorizontal, + ); + let reference_size = metrics.zero_advance_measure.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ch + // + // In the cases where it is impossible or impractical to + // determine the measure of the “0” glyph, it must be + // assumed to be 0.5em wide by 1em tall. Thus, the ch + // unit falls back to 0.5em in the general case, and to + // 1em when it would be typeset upright (i.e. + // writing-mode is vertical-rl or vertical-lr and + // text-orientation is upright). + // + // Same caveat about computed vs. used font-size applies + // above. + let wm = context.style().writing_mode; + if wm.is_vertical() && wm.is_upright() { + reference_font_size.used_size() + } else { + reference_font_size.used_size() * 0.5 + } + }); + (reference_size, length) + }, + Self::Cap(length) => { + let metrics = + query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal); + let reference_size = metrics.cap_height.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#cap + // + // In the cases where it is impossible or impractical to + // determine the cap-height, the font’s ascent must be + // used. + // + metrics.ascent + }); + (reference_size, length) + }, + Self::Ic(length) => { + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::MatchContextPreferVertical, + ); + let reference_size = metrics.ic_width.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ic + // + // In the cases where it is impossible or impractical to + // determine the ideographic advance measure, it must be + // assumed to be 1em. + // + // Same caveat about computed vs. used as for other + // metric-dependent units. + reference_font_size.used_size() + }); + (reference_size, length) + }, + Self::Rem(length) => { + // https://drafts.csswg.org/css-values/#rem: + // + // When specified on the font-size property of the root + // element, the rem units refer to the property's initial + // value. + // + let reference_size = if context.builder.is_root_element || context.in_media_query { + reference_font_size.computed_size() + } else { + context.device().root_font_size() + }; + (reference_size, length) + }, + Self::Lh(length) => { + // https://drafts.csswg.org/css-values-4/#lh + // + // When specified in media-query, the lh units refer to the + // initial values of font and line-height properties. + // + let reference_size = if context.in_media_query { + context + .device() + .calc_line_height( + &context.default_style().get_font(), + context.style().writing_mode, + None, + ) + .0 + } else { + let line_height = context.builder.calc_line_height( + context.device(), + line_height_base, + context.style().writing_mode, + ); + if context.for_non_inherited_property && + line_height_base == LineHeightBase::CurrentStyle + { + context + .rule_cache_conditions + .borrow_mut() + .set_line_height_dependency(line_height) + } + line_height.0 + }; + (reference_size, length) + }, + Self::Rlh(length) => { + // https://drafts.csswg.org/css-values-4/#rlh + // + // When specified on the root element, the rlh units refer + // to the initial values of font and line-height properties. + // + let reference_size: CSSPixelLength = + if context.builder.is_root_element || context.in_media_query { + context + .device() + .calc_line_height( + &context.default_style().get_font(), + context.style().writing_mode, + None, + ) + .0 + } else { + context.device().root_line_height() + }; + (reference_size, length) + }, + } + } +} + +/// https://drafts.csswg.org/css-values/#viewport-variants +pub enum ViewportVariant { + /// https://drafts.csswg.org/css-values/#ua-default-viewport-size + UADefault, + /// https://drafts.csswg.org/css-values/#small-viewport-percentage-units + Small, + /// https://drafts.csswg.org/css-values/#large-viewport-percentage-units + Large, + /// https://drafts.csswg.org/css-values/#dynamic-viewport-percentage-units + Dynamic, +} + +/// https://drafts.csswg.org/css-values/#viewport-relative-units +#[derive(PartialEq)] +enum ViewportUnit { + /// *vw units. + Vw, + /// *vh units. + Vh, + /// *vmin units. + Vmin, + /// *vmax units. + Vmax, + /// *vb units. + Vb, + /// *vi units. + Vi, +} + +/// A viewport-relative length. +/// +/// <https://drafts.csswg.org/css-values/#viewport-relative-lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum ViewportPercentageLength { + /// <https://drafts.csswg.org/css-values/#valdef-length-vw> + #[css(dimension)] + Vw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svw> + #[css(dimension)] + Svw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvw> + #[css(dimension)] + Lvw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvw> + #[css(dimension)] + Dvw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vh> + #[css(dimension)] + Vh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svh> + #[css(dimension)] + Svh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvh> + #[css(dimension)] + Lvh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvh> + #[css(dimension)] + Dvh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vmin> + #[css(dimension)] + Vmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svmin> + #[css(dimension)] + Svmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvmin> + #[css(dimension)] + Lvmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvmin> + #[css(dimension)] + Dvmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vmax> + #[css(dimension)] + Vmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svmax> + #[css(dimension)] + Svmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvmax> + #[css(dimension)] + Lvmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvmax> + #[css(dimension)] + Dvmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vb> + #[css(dimension)] + Vb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svb> + #[css(dimension)] + Svb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvb> + #[css(dimension)] + Lvb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvb> + #[css(dimension)] + Dvb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vi> + #[css(dimension)] + Vi(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svi> + #[css(dimension)] + Svi(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvi> + #[css(dimension)] + Lvi(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvi> + #[css(dimension)] + Dvi(CSSFloat), +} + +impl ViewportPercentageLength { + /// Return the unitless, raw value. + fn unitless_value(&self) -> CSSFloat { + self.unpack().2 + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Vw(_) => "vw", + Self::Lvw(_) => "lvw", + Self::Svw(_) => "svw", + Self::Dvw(_) => "dvw", + Self::Vh(_) => "vh", + Self::Svh(_) => "svh", + Self::Lvh(_) => "lvh", + Self::Dvh(_) => "dvh", + Self::Vmin(_) => "vmin", + Self::Svmin(_) => "svmin", + Self::Lvmin(_) => "lvmin", + Self::Dvmin(_) => "dvmin", + Self::Vmax(_) => "vmax", + Self::Svmax(_) => "svmax", + Self::Lvmax(_) => "lvmax", + Self::Dvmax(_) => "dvmax", + Self::Vb(_) => "vb", + Self::Svb(_) => "svb", + Self::Lvb(_) => "lvb", + Self::Dvb(_) => "dvb", + Self::Vi(_) => "vi", + Self::Svi(_) => "svi", + Self::Lvi(_) => "lvi", + Self::Dvi(_) => "dvi", + } + } + + fn unpack(&self) -> (ViewportVariant, ViewportUnit, CSSFloat) { + match *self { + Self::Vw(v) => (ViewportVariant::UADefault, ViewportUnit::Vw, v), + Self::Svw(v) => (ViewportVariant::Small, ViewportUnit::Vw, v), + Self::Lvw(v) => (ViewportVariant::Large, ViewportUnit::Vw, v), + Self::Dvw(v) => (ViewportVariant::Dynamic, ViewportUnit::Vw, v), + Self::Vh(v) => (ViewportVariant::UADefault, ViewportUnit::Vh, v), + Self::Svh(v) => (ViewportVariant::Small, ViewportUnit::Vh, v), + Self::Lvh(v) => (ViewportVariant::Large, ViewportUnit::Vh, v), + Self::Dvh(v) => (ViewportVariant::Dynamic, ViewportUnit::Vh, v), + Self::Vmin(v) => (ViewportVariant::UADefault, ViewportUnit::Vmin, v), + Self::Svmin(v) => (ViewportVariant::Small, ViewportUnit::Vmin, v), + Self::Lvmin(v) => (ViewportVariant::Large, ViewportUnit::Vmin, v), + Self::Dvmin(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmin, v), + Self::Vmax(v) => (ViewportVariant::UADefault, ViewportUnit::Vmax, v), + Self::Svmax(v) => (ViewportVariant::Small, ViewportUnit::Vmax, v), + Self::Lvmax(v) => (ViewportVariant::Large, ViewportUnit::Vmax, v), + Self::Dvmax(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmax, v), + Self::Vb(v) => (ViewportVariant::UADefault, ViewportUnit::Vb, v), + Self::Svb(v) => (ViewportVariant::Small, ViewportUnit::Vb, v), + Self::Lvb(v) => (ViewportVariant::Large, ViewportUnit::Vb, v), + Self::Dvb(v) => (ViewportVariant::Dynamic, ViewportUnit::Vb, v), + Self::Vi(v) => (ViewportVariant::UADefault, ViewportUnit::Vi, v), + Self::Svi(v) => (ViewportVariant::Small, ViewportUnit::Vi, v), + Self::Lvi(v) => (ViewportVariant::Large, ViewportUnit::Vi, v), + Self::Dvi(v) => (ViewportVariant::Dynamic, ViewportUnit::Vi, v), + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::ViewportPercentageLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Vw(one), &Vw(other)) => Vw(op(one, other)), + (&Svw(one), &Svw(other)) => Svw(op(one, other)), + (&Lvw(one), &Lvw(other)) => Lvw(op(one, other)), + (&Dvw(one), &Dvw(other)) => Dvw(op(one, other)), + (&Vh(one), &Vh(other)) => Vh(op(one, other)), + (&Svh(one), &Svh(other)) => Svh(op(one, other)), + (&Lvh(one), &Lvh(other)) => Lvh(op(one, other)), + (&Dvh(one), &Dvh(other)) => Dvh(op(one, other)), + (&Vmin(one), &Vmin(other)) => Vmin(op(one, other)), + (&Svmin(one), &Svmin(other)) => Svmin(op(one, other)), + (&Lvmin(one), &Lvmin(other)) => Lvmin(op(one, other)), + (&Dvmin(one), &Dvmin(other)) => Dvmin(op(one, other)), + (&Vmax(one), &Vmax(other)) => Vmax(op(one, other)), + (&Svmax(one), &Svmax(other)) => Svmax(op(one, other)), + (&Lvmax(one), &Lvmax(other)) => Lvmax(op(one, other)), + (&Dvmax(one), &Dvmax(other)) => Dvmax(op(one, other)), + (&Vb(one), &Vb(other)) => Vb(op(one, other)), + (&Svb(one), &Svb(other)) => Svb(op(one, other)), + (&Lvb(one), &Lvb(other)) => Lvb(op(one, other)), + (&Dvb(one), &Dvb(other)) => Dvb(op(one, other)), + (&Vi(one), &Vi(other)) => Vi(op(one, other)), + (&Svi(one), &Svi(other)) => Svi(op(one, other)), + (&Lvi(one), &Lvi(other)) => Lvi(op(one, other)), + (&Dvi(one), &Dvi(other)) => Dvi(op(one, other)), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) | + Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) | + Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) | + Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + match self { + Self::Vw(x) => Self::Vw(op(*x)), + Self::Svw(x) => Self::Svw(op(*x)), + Self::Lvw(x) => Self::Lvw(op(*x)), + Self::Dvw(x) => Self::Dvw(op(*x)), + Self::Vh(x) => Self::Vh(op(*x)), + Self::Svh(x) => Self::Svh(op(*x)), + Self::Lvh(x) => Self::Lvh(op(*x)), + Self::Dvh(x) => Self::Dvh(op(*x)), + Self::Vmin(x) => Self::Vmin(op(*x)), + Self::Svmin(x) => Self::Svmin(op(*x)), + Self::Lvmin(x) => Self::Lvmin(op(*x)), + Self::Dvmin(x) => Self::Dvmin(op(*x)), + Self::Vmax(x) => Self::Vmax(op(*x)), + Self::Svmax(x) => Self::Svmax(op(*x)), + Self::Lvmax(x) => Self::Lvmax(op(*x)), + Self::Dvmax(x) => Self::Dvmax(op(*x)), + Self::Vb(x) => Self::Vb(op(*x)), + Self::Svb(x) => Self::Svb(op(*x)), + Self::Lvb(x) => Self::Lvb(op(*x)), + Self::Dvb(x) => Self::Dvb(op(*x)), + Self::Vi(x) => Self::Vi(op(*x)), + Self::Svi(x) => Self::Svi(op(*x)), + Self::Lvi(x) => Self::Lvi(op(*x)), + Self::Dvi(x) => Self::Dvi(op(*x)), + } + } + + /// Computes the given viewport-relative length for the given viewport size. + pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + let (variant, unit, factor) = self.unpack(); + let size = context.viewport_size_for_viewport_unit_resolution(variant); + let length = match unit { + ViewportUnit::Vw => size.width, + ViewportUnit::Vh => size.height, + ViewportUnit::Vmin => cmp::min(size.width, size.height), + ViewportUnit::Vmax => cmp::max(size.width, size.height), + ViewportUnit::Vi | ViewportUnit::Vb => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if (unit == ViewportUnit::Vb) == context.style().writing_mode.is_vertical() { + size.width + } else { + size.height + } + }, + }; + + // FIXME: Bug 1396535, we need to fix the extremely small viewport length for transform. + // See bug 989802. We truncate so that adding multiple viewport units + // that add up to 100 does not overflow due to rounding differences. + // We convert appUnits to CSS px manually here to avoid premature clamping by + // going through the Au type. + let trunc_scaled = + ((length.0 as f64 * factor as f64 / 100.).trunc() / AU_PER_PX as f64) as f32; + CSSPixelLength::new(crate::values::normalize(trunc_scaled)) + } +} + +/// HTML5 "character width", as defined in HTML5 § 14.5.4. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub struct CharacterWidth(pub i32); + +impl CharacterWidth { + /// Computes the given character width. + pub fn to_computed_value(&self, reference_font_size: computed::Length) -> computed::Length { + // This applies the *converting a character width to pixels* algorithm + // as specified in HTML5 § 14.5.4. + // + // TODO(pcwalton): Find these from the font. + let average_advance = reference_font_size * 0.5; + let max_advance = reference_font_size; + (average_advance * (self.0 as CSSFloat - 1.0) + max_advance).finite() + } +} + +/// Represents an absolute length with its unit +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum AbsoluteLength { + /// An absolute length in pixels (px) + #[css(dimension)] + Px(CSSFloat), + /// An absolute length in inches (in) + #[css(dimension)] + In(CSSFloat), + /// An absolute length in centimeters (cm) + #[css(dimension)] + Cm(CSSFloat), + /// An absolute length in millimeters (mm) + #[css(dimension)] + Mm(CSSFloat), + /// An absolute length in quarter-millimeters (q) + #[css(dimension)] + Q(CSSFloat), + /// An absolute length in points (pt) + #[css(dimension)] + Pt(CSSFloat), + /// An absolute length in pica (pc) + #[css(dimension)] + Pc(CSSFloat), +} + +impl AbsoluteLength { + /// Return the unitless, raw value. + fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Px(v) | + Self::In(v) | + Self::Cm(v) | + Self::Mm(v) | + Self::Q(v) | + Self::Pt(v) | + Self::Pc(v) => v, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Px(_) => "px", + Self::In(_) => "in", + Self::Cm(_) => "cm", + Self::Mm(_) => "mm", + Self::Q(_) => "q", + Self::Pt(_) => "pt", + Self::Pc(_) => "pc", + } + } + + /// Convert this into a pixel value. + #[inline] + pub fn to_px(&self) -> CSSFloat { + match *self { + Self::Px(value) => value, + Self::In(value) => value * PX_PER_IN, + Self::Cm(value) => value * PX_PER_CM, + Self::Mm(value) => value * PX_PER_MM, + Self::Q(value) => value * PX_PER_Q, + Self::Pt(value) => value * PX_PER_PT, + Self::Pc(value) => value * PX_PER_PC, + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + Ok(Self::Px(op(self.to_px(), other.to_px()))) + } + + fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + Self::Px(op(self.to_px())) + } +} + +impl ToComputedValue for AbsoluteLength { + type ComputedValue = CSSPixelLength; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + CSSPixelLength::new(context.builder.effective_zoom().zoom(self.to_px())).finite() + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Px(computed.px()) + } +} + +impl PartialOrd for AbsoluteLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + self.to_px().partial_cmp(&other.to_px()) + } +} + +/// A container query length. +/// +/// <https://drafts.csswg.org/css-contain-3/#container-lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum ContainerRelativeLength { + /// 1% of query container's width + #[css(dimension)] + Cqw(CSSFloat), + /// 1% of query container's height + #[css(dimension)] + Cqh(CSSFloat), + /// 1% of query container's inline size + #[css(dimension)] + Cqi(CSSFloat), + /// 1% of query container's block size + #[css(dimension)] + Cqb(CSSFloat), + /// The smaller value of `cqi` or `cqb` + #[css(dimension)] + Cqmin(CSSFloat), + /// The larger value of `cqi` or `cqb` + #[css(dimension)] + Cqmax(CSSFloat), +} + +impl ContainerRelativeLength { + fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Cqw(v) | + Self::Cqh(v) | + Self::Cqi(v) | + Self::Cqb(v) | + Self::Cqmin(v) | + Self::Cqmax(v) => v, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Cqw(_) => "cqw", + Self::Cqh(_) => "cqh", + Self::Cqi(_) => "cqi", + Self::Cqb(_) => "cqb", + Self::Cqmin(_) => "cqmin", + Self::Cqmax(_) => "cqmax", + } + } + + pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::ContainerRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Cqw(one), &Cqw(other)) => Cqw(op(one, other)), + (&Cqh(one), &Cqh(other)) => Cqh(op(one, other)), + (&Cqi(one), &Cqi(other)) => Cqi(op(one, other)), + (&Cqb(one), &Cqb(other)) => Cqb(op(one, other)), + (&Cqmin(one), &Cqmin(other)) => Cqmin(op(one, other)), + (&Cqmax(one), &Cqmax(other)) => Cqmax(op(one, other)), + + // See https://github.com/rust-lang/rust/issues/68867, then + // https://github.com/rust-lang/rust/pull/95161. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + match self { + Self::Cqw(x) => Self::Cqw(op(*x)), + Self::Cqh(x) => Self::Cqh(op(*x)), + Self::Cqi(x) => Self::Cqi(op(*x)), + Self::Cqb(x) => Self::Cqb(op(*x)), + Self::Cqmin(x) => Self::Cqmin(op(*x)), + Self::Cqmax(x) => Self::Cqmax(op(*x)), + } + } + + /// Computes the given container-relative length. + pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + if context.for_non_inherited_property { + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + context + .builder + .add_flags(ComputedValueFlags::USES_CONTAINER_UNITS); + + let size = context.get_container_size_query(); + let (factor, container_length) = match *self { + Self::Cqw(v) => (v, size.get_container_width(context)), + Self::Cqh(v) => (v, size.get_container_height(context)), + Self::Cqi(v) => (v, size.get_container_inline_size(context)), + Self::Cqb(v) => (v, size.get_container_block_size(context)), + Self::Cqmin(v) => ( + v, + cmp::min( + size.get_container_inline_size(context), + size.get_container_block_size(context), + ), + ), + Self::Cqmax(v) => ( + v, + cmp::max( + size.get_container_inline_size(context), + size.get_container_block_size(context), + ), + ), + }; + CSSPixelLength::new((container_length.to_f64_px() * factor as f64 / 100.0) as f32).finite() + } +} + +#[cfg(feature = "gecko")] +fn are_container_queries_enabled() -> bool { + static_prefs::pref!("layout.css.container-queries.enabled") +} +#[cfg(feature = "servo")] +fn are_container_queries_enabled() -> bool { + false +} + +/// A `<length>` without taking `calc` expressions into account +/// +/// <https://drafts.csswg.org/css-values/#lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum NoCalcLength { + /// An absolute length + /// + /// <https://drafts.csswg.org/css-values/#absolute-length> + Absolute(AbsoluteLength), + + /// A font-relative length: + /// + /// <https://drafts.csswg.org/css-values/#font-relative-lengths> + FontRelative(FontRelativeLength), + + /// A viewport-relative length. + /// + /// <https://drafts.csswg.org/css-values/#viewport-relative-lengths> + ViewportPercentage(ViewportPercentageLength), + + /// A container query length. + /// + /// <https://drafts.csswg.org/css-contain-3/#container-lengths> + ContainerRelative(ContainerRelativeLength), + /// HTML5 "character width", as defined in HTML5 § 14.5.4. + /// + /// This cannot be specified by the user directly and is only generated by + /// `Stylist::synthesize_rules_for_legacy_attributes()`. + ServoCharacterWidth(CharacterWidth), +} + +impl NoCalcLength { + /// Return the unitless, raw value. + pub fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Absolute(v) => v.unitless_value(), + Self::FontRelative(v) => v.unitless_value(), + Self::ViewportPercentage(v) => v.unitless_value(), + Self::ContainerRelative(v) => v.unitless_value(), + Self::ServoCharacterWidth(c) => c.0 as f32, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Absolute(v) => v.unit(), + Self::FontRelative(v) => v.unit(), + Self::ViewportPercentage(v) => v.unit(), + Self::ContainerRelative(v) => v.unit(), + Self::ServoCharacterWidth(_) => "", + } + } + + /// Returns whether the value of this length without unit is less than zero. + pub fn is_negative(&self) -> bool { + self.unitless_value().is_sign_negative() + } + + /// Returns whether the value of this length without unit is equal to zero. + pub fn is_zero(&self) -> bool { + self.unitless_value() == 0.0 + } + + /// Returns whether the value of this length without unit is infinite. + pub fn is_infinite(&self) -> bool { + self.unitless_value().is_infinite() + } + + /// Returns whether the value of this length without unit is NaN. + pub fn is_nan(&self) -> bool { + self.unitless_value().is_nan() + } + + /// Whether text-only zoom should be applied to this length. + /// + /// Generally, font-dependent/relative units don't get text-only-zoomed, + /// because the font they're relative to should be zoomed already. + pub fn should_zoom_text(&self) -> bool { + match *self { + Self::Absolute(..) | Self::ViewportPercentage(..) | Self::ContainerRelative(..) => true, + Self::ServoCharacterWidth(..) | Self::FontRelative(..) => false, + } + } + + /// Parse a given absolute or relative dimension. + pub fn parse_dimension( + context: &ParserContext, + value: CSSFloat, + unit: &str, + ) -> Result<Self, ()> { + Ok(match_ignore_ascii_case! { unit, + "px" => Self::Absolute(AbsoluteLength::Px(value)), + "in" => Self::Absolute(AbsoluteLength::In(value)), + "cm" => Self::Absolute(AbsoluteLength::Cm(value)), + "mm" => Self::Absolute(AbsoluteLength::Mm(value)), + "q" => Self::Absolute(AbsoluteLength::Q(value)), + "pt" => Self::Absolute(AbsoluteLength::Pt(value)), + "pc" => Self::Absolute(AbsoluteLength::Pc(value)), + // font-relative + "em" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Em(value)), + "ex" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ex(value)), + "ch" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ch(value)), + "cap" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Cap(value)), + "ic" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ic(value)), + "rem" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rem(value)), + "lh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Lh(value)), + "rlh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rlh(value)), + // viewport percentages + "vw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vw(value)) + }, + "svw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svw(value)) + }, + "lvw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvw(value)) + }, + "dvw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvw(value)) + }, + "vh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vh(value)) + }, + "svh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svh(value)) + }, + "lvh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvh(value)) + }, + "dvh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvh(value)) + }, + "vmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vmin(value)) + }, + "svmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svmin(value)) + }, + "lvmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvmin(value)) + }, + "dvmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvmin(value)) + }, + "vmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vmax(value)) + }, + "svmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svmax(value)) + }, + "lvmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvmax(value)) + }, + "dvmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvmax(value)) + }, + "vb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vb(value)) + }, + "svb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svb(value)) + }, + "lvb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvb(value)) + }, + "dvb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvb(value)) + }, + "vi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vi(value)) + }, + "svi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svi(value)) + }, + "lvi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvi(value)) + }, + "dvi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvi(value)) + }, + // Container query lengths. Inherit the limitation from viewport units since + // we may fall back to them. + "cqw" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqw(value)) + }, + "cqh" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqh(value)) + }, + "cqi" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqi(value)) + }, + "cqb" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqb(value)) + }, + "cqmin" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqmin(value)) + }, + "cqmax" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqmax(value)) + }, + _ => return Err(()), + }) + } + + pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::NoCalcLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Absolute(ref one), &Absolute(ref other)) => Absolute(one.try_op(other, op)?), + (&FontRelative(ref one), &FontRelative(ref other)) => { + FontRelative(one.try_op(other, op)?) + }, + (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => { + ViewportPercentage(one.try_op(other, op)?) + }, + (&ContainerRelative(ref one), &ContainerRelative(ref other)) => { + ContainerRelative(one.try_op(other, op)?) + }, + (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => { + ServoCharacterWidth(CharacterWidth(op(one.0 as f32, other.0 as f32) as i32)) + }, + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Absolute(..) | + FontRelative(..) | + ViewportPercentage(..) | + ContainerRelative(..) | + ServoCharacterWidth(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + use self::NoCalcLength::*; + + match self { + Absolute(ref one) => Absolute(one.map(op)), + FontRelative(ref one) => FontRelative(one.map(op)), + ViewportPercentage(ref one) => ViewportPercentage(one.map(op)), + ContainerRelative(ref one) => ContainerRelative(one.map(op)), + ServoCharacterWidth(ref one) => { + ServoCharacterWidth(CharacterWidth(op(one.0 as f32) as i32)) + }, + } + } + + /// Get a px value without context (so only absolute units can be handled). + #[inline] + pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> { + match *self { + Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()), + _ => Err(()), + } + } + + /// Get a px value without a full style context; this can handle either + /// absolute or (if a font metrics getter is provided) font-relative units. + #[inline] + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>, + ) -> Result<CSSFloat, ()> { + match *self { + Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()), + Self::FontRelative(fr) => { + if let Some(getter) = get_font_metrics { + fr.to_computed_pixel_length_with_font_metrics(getter) + } else { + Err(()) + } + }, + _ => Err(()), + } + } + + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> NoCalcLength { + NoCalcLength::Absolute(AbsoluteLength::Px(px_value)) + } +} + +impl ToCss for NoCalcLength { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.unitless_value(), + self.unit(), + false, + dest, + ) + } +} + +impl SpecifiedValueInfo for NoCalcLength {} + +impl PartialOrd for NoCalcLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::NoCalcLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Absolute(ref one), &Absolute(ref other)) => one.to_px().partial_cmp(&other.to_px()), + (&FontRelative(ref one), &FontRelative(ref other)) => one.partial_cmp(other), + (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => { + one.partial_cmp(other) + }, + (&ContainerRelative(ref one), &ContainerRelative(ref other)) => one.partial_cmp(other), + (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => { + one.0.partial_cmp(&other.0) + }, + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Absolute(..) | + FontRelative(..) | + ViewportPercentage(..) | + ContainerRelative(..) | + ServoCharacterWidth(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Zero for NoCalcLength { + fn zero() -> Self { + NoCalcLength::Absolute(AbsoluteLength::Px(0.)) + } + + fn is_zero(&self) -> bool { + NoCalcLength::is_zero(self) + } +} + +/// An extension to `NoCalcLength` to parse `calc` expressions. +/// This is commonly used for the `<length>` values. +/// +/// <https://drafts.csswg.org/css-values/#lengths> +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum Length { + /// The internal length type that cannot parse `calc` + NoCalc(NoCalcLength), + /// A calc expression. + /// + /// <https://drafts.csswg.org/css-values/#calc-notation> + Calc(Box<CalcLengthPercentage>), +} + +impl From<NoCalcLength> for Length { + #[inline] + fn from(len: NoCalcLength) -> Self { + Length::NoCalc(len) + } +} + +impl PartialOrd for FontRelativeLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::FontRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Em(ref one), &Em(ref other)) => one.partial_cmp(other), + (&Ex(ref one), &Ex(ref other)) => one.partial_cmp(other), + (&Ch(ref one), &Ch(ref other)) => one.partial_cmp(other), + (&Cap(ref one), &Cap(ref other)) => one.partial_cmp(other), + (&Ic(ref one), &Ic(ref other)) => one.partial_cmp(other), + (&Rem(ref one), &Rem(ref other)) => one.partial_cmp(other), + (&Lh(ref one), &Lh(ref other)) => one.partial_cmp(other), + (&Rlh(ref one), &Rlh(ref other)) => one.partial_cmp(other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl PartialOrd for ContainerRelativeLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::ContainerRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Cqw(ref one), &Cqw(ref other)) => one.partial_cmp(other), + (&Cqh(ref one), &Cqh(ref other)) => one.partial_cmp(other), + (&Cqi(ref one), &Cqi(ref other)) => one.partial_cmp(other), + (&Cqb(ref one), &Cqb(ref other)) => one.partial_cmp(other), + (&Cqmin(ref one), &Cqmin(ref other)) => one.partial_cmp(other), + (&Cqmax(ref one), &Cqmax(ref other)) => one.partial_cmp(other), + + // See https://github.com/rust-lang/rust/issues/68867, then + // https://github.com/rust-lang/rust/pull/95161. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {}, + } + debug_unreachable!("Forgot to handle unit in partial_cmp()") + }, + } + } +} + +impl PartialOrd for ViewportPercentageLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::ViewportPercentageLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Vw(ref one), &Vw(ref other)) => one.partial_cmp(other), + (&Svw(ref one), &Svw(ref other)) => one.partial_cmp(other), + (&Lvw(ref one), &Lvw(ref other)) => one.partial_cmp(other), + (&Dvw(ref one), &Dvw(ref other)) => one.partial_cmp(other), + (&Vh(ref one), &Vh(ref other)) => one.partial_cmp(other), + (&Svh(ref one), &Svh(ref other)) => one.partial_cmp(other), + (&Lvh(ref one), &Lvh(ref other)) => one.partial_cmp(other), + (&Dvh(ref one), &Dvh(ref other)) => one.partial_cmp(other), + (&Vmin(ref one), &Vmin(ref other)) => one.partial_cmp(other), + (&Svmin(ref one), &Svmin(ref other)) => one.partial_cmp(other), + (&Lvmin(ref one), &Lvmin(ref other)) => one.partial_cmp(other), + (&Dvmin(ref one), &Dvmin(ref other)) => one.partial_cmp(other), + (&Vmax(ref one), &Vmax(ref other)) => one.partial_cmp(other), + (&Svmax(ref one), &Svmax(ref other)) => one.partial_cmp(other), + (&Lvmax(ref one), &Lvmax(ref other)) => one.partial_cmp(other), + (&Dvmax(ref one), &Dvmax(ref other)) => one.partial_cmp(other), + (&Vb(ref one), &Vb(ref other)) => one.partial_cmp(other), + (&Svb(ref one), &Svb(ref other)) => one.partial_cmp(other), + (&Lvb(ref one), &Lvb(ref other)) => one.partial_cmp(other), + (&Dvb(ref one), &Dvb(ref other)) => one.partial_cmp(other), + (&Vi(ref one), &Vi(ref other)) => one.partial_cmp(other), + (&Svi(ref one), &Svi(ref other)) => one.partial_cmp(other), + (&Lvi(ref one), &Lvi(ref other)) => one.partial_cmp(other), + (&Dvi(ref one), &Dvi(ref other)) => one.partial_cmp(other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) | + Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) | + Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) | + Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Length { + #[inline] + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Dimension { + value, ref unit, .. + } if num_context.is_ok(context.parsing_mode, value) => { + NoCalcLength::parse_dimension(context, value, unit) + .map(Length::NoCalc) + .map_err(|()| location.new_unexpected_token_error(token.clone())) + }, + Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => { + if value != 0. && + !context.parsing_mode.allows_unitless_lengths() && + !allow_quirks.allowed(context.quirks_mode) + { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(Length::NoCalc(NoCalcLength::Absolute(AbsoluteLength::Px( + value, + )))) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let calc = CalcNode::parse_length(context, input, num_context, function)?; + Ok(Length::Calc(Box::new(calc))) + }, + ref token => return Err(location.new_unexpected_token_error(token.clone())), + } + } + + /// Parse a non-negative length + #[inline] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_non_negative_quirky(context, input, AllowQuirks::No) + } + + /// Parse a non-negative length, allowing quirks. + #[inline] + pub fn parse_non_negative_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal( + context, + input, + AllowedNumericType::NonNegative, + allow_quirks, + ) + } + + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> Length { + Length::NoCalc(NoCalcLength::from_px(px_value)) + } + + /// Get a px value without context. + pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> { + match *self { + Self::NoCalc(ref l) => l.to_computed_pixel_length_without_context(), + Self::Calc(ref l) => l.to_computed_pixel_length_without_context(), + } + } + + /// Get a px value, with an optional GeckoFontMetrics getter to resolve font-relative units. + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>, + ) -> Result<CSSFloat, ()> { + match *self { + Self::NoCalc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics), + Self::Calc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics), + } + } +} + +impl Parse for Length { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl Zero for Length { + fn zero() -> Self { + Length::NoCalc(NoCalcLength::zero()) + } + + fn is_zero(&self) -> bool { + // FIXME(emilio): Seems a bit weird to treat calc() unconditionally as + // non-zero here? + match *self { + Length::NoCalc(ref l) => l.is_zero(), + Length::Calc(..) => false, + } + } +} + +impl Length { + /// Parses a length, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks) + } +} + +/// A wrapper of Length, whose value must be >= 0. +pub type NonNegativeLength = NonNegative<Length>; + +impl Parse for NonNegativeLength { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Length::parse_non_negative(context, input)?)) + } +} + +impl From<NoCalcLength> for NonNegativeLength { + #[inline] + fn from(len: NoCalcLength) -> Self { + NonNegative(Length::NoCalc(len)) + } +} + +impl From<Length> for NonNegativeLength { + #[inline] + fn from(len: Length) -> Self { + NonNegative(len) + } +} + +impl NonNegativeLength { + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> Self { + Length::from_px(px_value.max(0.)).into() + } + + /// Parses a non-negative length, optionally with quirks. + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Length::parse_non_negative_quirky( + context, + input, + allow_quirks, + )?)) + } +} + +/// A `<length-percentage>` value. This can be either a `<length>`, a +/// `<percentage>`, or a combination of both via `calc()`. +/// +/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage +#[allow(missing_docs)] +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum LengthPercentage { + Length(NoCalcLength), + Percentage(computed::Percentage), + Calc(Box<CalcLengthPercentage>), +} + +impl From<Length> for LengthPercentage { + fn from(len: Length) -> LengthPercentage { + match len { + Length::NoCalc(l) => LengthPercentage::Length(l), + Length::Calc(l) => LengthPercentage::Calc(l), + } + } +} + +impl From<NoCalcLength> for LengthPercentage { + #[inline] + fn from(len: NoCalcLength) -> Self { + LengthPercentage::Length(len) + } +} + +impl From<Percentage> for LengthPercentage { + #[inline] + fn from(pc: Percentage) -> Self { + if let Some(clamping_mode) = pc.calc_clamping_mode() { + LengthPercentage::Calc(Box::new(CalcLengthPercentage { + clamping_mode, + node: CalcNode::Leaf(calc::Leaf::Percentage(pc.get())), + })) + } else { + LengthPercentage::Percentage(computed::Percentage(pc.get())) + } + } +} + +impl From<computed::Percentage> for LengthPercentage { + #[inline] + fn from(pc: computed::Percentage) -> Self { + LengthPercentage::Percentage(pc) + } +} + +impl Parse for LengthPercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl LengthPercentage { + #[inline] + /// Returns a `0%` value. + pub fn zero_percent() -> LengthPercentage { + LengthPercentage::Percentage(computed::Percentage::zero()) + } + + #[inline] + /// Returns a `100%` value. + pub fn hundred_percent() -> LengthPercentage { + LengthPercentage::Percentage(computed::Percentage::hundred()) + } + + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Dimension { + value, ref unit, .. + } if num_context.is_ok(context.parsing_mode, value) => { + return NoCalcLength::parse_dimension(context, value, unit) + .map(LengthPercentage::Length) + .map_err(|()| location.new_unexpected_token_error(token.clone())); + }, + Token::Percentage { unit_value, .. } + if num_context.is_ok(context.parsing_mode, unit_value) => + { + return Ok(LengthPercentage::Percentage(computed::Percentage( + unit_value, + ))); + }, + Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => { + if value != 0. && + !context.parsing_mode.allows_unitless_lengths() && + !allow_quirks.allowed(context.quirks_mode) + { + return Err(location.new_unexpected_token_error(token.clone())); + } else { + return Ok(LengthPercentage::Length(NoCalcLength::from_px(value))); + } + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let calc = + CalcNode::parse_length_or_percentage(context, input, num_context, function)?; + Ok(LengthPercentage::Calc(Box::new(calc))) + }, + _ => return Err(location.new_unexpected_token_error(token.clone())), + } + } + + /// Parses allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks) + } + + /// Parse a non-negative length. + /// + /// FIXME(emilio): This should be not public and we should use + /// NonNegativeLengthPercentage instead. + #[inline] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_non_negative_quirky(context, input, AllowQuirks::No) + } + + /// Parse a non-negative length, with quirks. + #[inline] + pub fn parse_non_negative_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal( + context, + input, + AllowedNumericType::NonNegative, + allow_quirks, + ) + } + + /// Returns self as specified::calc::CalcNode. + /// Note that this expect the clamping_mode is AllowedNumericType::All for Calc. The caller + /// should take care about it when using this function. + fn to_calc_node(self) -> CalcNode { + match self { + LengthPercentage::Length(l) => CalcNode::Leaf(calc::Leaf::Length(l)), + LengthPercentage::Percentage(p) => CalcNode::Leaf(calc::Leaf::Percentage(p.0)), + LengthPercentage::Calc(p) => p.node, + } + } + + /// Construct the value representing `calc(100% - self)`. + pub fn hundred_percent_minus(self, clamping_mode: AllowedNumericType) -> Self { + let mut sum = smallvec::SmallVec::<[CalcNode; 2]>::new(); + sum.push(CalcNode::Leaf(calc::Leaf::Percentage(1.0))); + + let mut node = self.to_calc_node(); + node.negate(); + sum.push(node); + + let calc = CalcNode::Sum(sum.into_boxed_slice().into()); + LengthPercentage::Calc(Box::new( + calc.into_length_or_percentage(clamping_mode).unwrap(), + )) + } +} + +impl Zero for LengthPercentage { + fn zero() -> Self { + LengthPercentage::Length(NoCalcLength::zero()) + } + + fn is_zero(&self) -> bool { + match *self { + LengthPercentage::Length(l) => l.is_zero(), + LengthPercentage::Percentage(p) => p.0 == 0.0, + LengthPercentage::Calc(_) => false, + } + } +} + +impl ZeroNoPercent for LengthPercentage { + fn is_zero_no_percent(&self) -> bool { + match *self { + LengthPercentage::Percentage(_) => false, + _ => self.is_zero(), + } + } +} + +/// A specified type for `<length-percentage> | auto`. +pub type LengthPercentageOrAuto = generics::LengthPercentageOrAuto<LengthPercentage>; + +impl LengthPercentageOrAuto { + /// Returns a value representing `0%`. + #[inline] + pub fn zero_percent() -> Self { + generics::LengthPercentageOrAuto::LengthPercentage(LengthPercentage::zero_percent()) + } + + /// Parses a length or a percentage, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + LengthPercentage::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0. +pub type NonNegativeLengthPercentageOrAuto = + generics::LengthPercentageOrAuto<NonNegativeLengthPercentage>; + +impl NonNegativeLengthPercentageOrAuto { + /// Returns a value representing `0%`. + #[inline] + pub fn zero_percent() -> Self { + generics::LengthPercentageOrAuto::LengthPercentage( + NonNegativeLengthPercentage::zero_percent(), + ) + } + + /// Parses a non-negative length-percentage, allowing the unitless length + /// quirk. + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// A wrapper of LengthPercentage, whose value must be >= 0. +pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>; + +/// Either a NonNegativeLengthPercentage or the `normal` keyword. +pub type NonNegativeLengthPercentageOrNormal = + GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>; + +impl From<NoCalcLength> for NonNegativeLengthPercentage { + #[inline] + fn from(len: NoCalcLength) -> Self { + NonNegative(LengthPercentage::from(len)) + } +} + +impl Parse for NonNegativeLengthPercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl NonNegativeLengthPercentage { + #[inline] + /// Returns a `0%` value. + pub fn zero_percent() -> Self { + NonNegative(LengthPercentage::zero_percent()) + } + + /// Parses a length or a percentage, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + LengthPercentage::parse_non_negative_quirky(context, input, allow_quirks).map(NonNegative) + } +} + +/// Either a `<length>` or the `auto` keyword. +/// +/// Note that we use LengthPercentage just for convenience, since it pretty much +/// is everything we care about, but we could just add a similar LengthOrAuto +/// instead if we think getting rid of this weirdness is worth it. +pub type LengthOrAuto = generics::LengthPercentageOrAuto<Length>; + +impl LengthOrAuto { + /// Parses a length, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + Length::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// Either a non-negative `<length>` or the `auto` keyword. +pub type NonNegativeLengthOrAuto = generics::LengthPercentageOrAuto<NonNegativeLength>; + +/// Either a `<length>` or a `<number>`. +pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>; + +/// A specified value for `min-width`, `min-height`, `width` or `height` property. +pub type Size = GenericSize<NonNegativeLengthPercentage>; + +impl Parse for Size { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size::parse_quirky(context, input, AllowQuirks::No) + } +} + +macro_rules! parse_size_non_length { + ($size:ident, $input:expr, $auto_or_none:expr => $auto_or_none_ident:ident) => {{ + let size = $input.try_parse(|input| { + Ok(try_match_ident_ignore_ascii_case! { input, + #[cfg(feature = "gecko")] + "min-content" | "-moz-min-content" => $size::MinContent, + #[cfg(feature = "gecko")] + "max-content" | "-moz-max-content" => $size::MaxContent, + #[cfg(feature = "gecko")] + "fit-content" | "-moz-fit-content" => $size::FitContent, + #[cfg(feature = "gecko")] + "-moz-available" => $size::MozAvailable, + $auto_or_none => $size::$auto_or_none_ident, + }) + }); + if size.is_ok() { + return size; + } + }}; +} + +#[cfg(feature = "gecko")] +fn is_fit_content_function_enabled() -> bool { + static_prefs::pref!("layout.css.fit-content-function.enabled") +} +#[cfg(feature = "servo")] +fn is_fit_content_function_enabled() -> bool { + false +} + +macro_rules! parse_fit_content_function { + ($size:ident, $input:expr, $context:expr, $allow_quirks:expr) => { + if is_fit_content_function_enabled() { + if let Ok(length) = $input.try_parse(|input| { + input.expect_function_matching("fit-content")?; + input.parse_nested_block(|i| { + NonNegativeLengthPercentage::parse_quirky($context, i, $allow_quirks) + }) + }) { + return Ok($size::FitContentFunction(length)); + } + } + }; +} + +impl Size { + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + parse_size_non_length!(Size, input, "auto" => Auto); + parse_fit_content_function!(Size, input, context, allow_quirks); + + let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?; + Ok(GenericSize::LengthPercentage(length)) + } + + /// Returns `0%`. + #[inline] + pub fn zero_percent() -> Self { + GenericSize::LengthPercentage(NonNegativeLengthPercentage::zero_percent()) + } +} + +/// A specified value for `max-width` or `max-height` property. +pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>; + +impl Parse for MaxSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + MaxSize::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl MaxSize { + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + parse_size_non_length!(MaxSize, input, "none" => None); + parse_fit_content_function!(MaxSize, input, context, allow_quirks); + + let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?; + Ok(GenericMaxSize::LengthPercentage(length)) + } +} + +/// A specified non-negative `<length>` | `<number>`. +pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>; diff --git a/servo/components/style/values/specified/list.rs b/servo/components/style/values/specified/list.rs new file mode 100644 index 0000000000..693471e478 --- /dev/null +++ b/servo/components/style/values/specified/list.rs @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `list` specified values. + +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::values::generics::CounterStyle; +#[cfg(feature = "gecko")] +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// Specified and computed `list-style-type` property. +#[cfg(feature = "gecko")] +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum ListStyleType { + /// `none` + None, + /// <counter-style> + CounterStyle(CounterStyle), + /// <string> + String(String), +} + +#[cfg(feature = "gecko")] +impl ListStyleType { + /// Initial specified value for `list-style-type`. + #[inline] + pub fn disc() -> Self { + ListStyleType::CounterStyle(CounterStyle::disc()) + } + + /// Convert from gecko keyword to list-style-type. + /// + /// This should only be used for mapping type attribute to + /// list-style-type, and thus only values possible in that + /// attribute is considered here. + pub fn from_gecko_keyword(value: u32) -> Self { + use crate::gecko_bindings::structs; + let v8 = value as u8; + + if v8 == structs::ListStyle_None { + return ListStyleType::None; + } + + ListStyleType::CounterStyle(CounterStyle::Name(CustomIdent(match v8 { + structs::ListStyle_Disc => atom!("disc"), + structs::ListStyle_Circle => atom!("circle"), + structs::ListStyle_Square => atom!("square"), + structs::ListStyle_Decimal => atom!("decimal"), + structs::ListStyle_LowerRoman => atom!("lower-roman"), + structs::ListStyle_UpperRoman => atom!("upper-roman"), + structs::ListStyle_LowerAlpha => atom!("lower-alpha"), + structs::ListStyle_UpperAlpha => atom!("upper-alpha"), + _ => unreachable!("Unknown counter style keyword value"), + }))) + } + + /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`) + #[inline] + pub fn is_bullet(&self) -> bool { + match self { + ListStyleType::CounterStyle(ref style) => style.is_bullet(), + _ => false, + } + } +} + +#[cfg(feature = "gecko")] +impl Parse for ListStyleType { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(style) = input.try_parse(|i| CounterStyle::parse(context, i)) { + return Ok(ListStyleType::CounterStyle(style)); + } + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ListStyleType::None); + } + Ok(ListStyleType::String( + input.expect_string()?.as_ref().to_owned(), + )) + } +} + +/// A quote pair. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct QuotePair { + /// The opening quote. + pub opening: crate::OwnedStr, + + /// The closing quote. + pub closing: crate::OwnedStr, +} + +/// List of quote pairs for the specified/computed value of `quotes` property. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct QuoteList( + #[css(iterable, if_empty = "none")] + #[ignore_malloc_size_of = "Arc"] + pub crate::ArcSlice<QuotePair>, +); + +/// Specified and computed `quotes` property: `auto`, `none`, or a list +/// of characters. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub enum Quotes { + /// list of quote pairs + QuoteList(QuoteList), + /// auto (use lang-dependent quote marks) + Auto, +} + +impl Parse for Quotes { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Quotes, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(Quotes::Auto); + } + + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(Quotes::QuoteList(QuoteList::default())); + } + + let mut quotes = Vec::new(); + loop { + let location = input.current_source_location(); + let opening = match input.next() { + Ok(&Token::QuotedString(ref value)) => value.as_ref().to_owned().into(), + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(_) => break, + }; + + let closing = input.expect_string()?.as_ref().to_owned().into(); + quotes.push(QuotePair { opening, closing }); + } + + if !quotes.is_empty() { + Ok(Quotes::QuoteList(QuoteList(crate::ArcSlice::from_iter( + quotes.into_iter(), + )))) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs new file mode 100644 index 0000000000..7fc76b3c07 --- /dev/null +++ b/servo/components/style/values/specified/mod.rs @@ -0,0 +1,992 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified values. +//! +//! TODO(emilio): Enhance docs. + +use super::computed::transform::DirectionVector; +use super::computed::{Context, ToComputedValue}; +use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks; +use super::generics::grid::{GridLine as GenericGridLine, TrackBreadth as GenericTrackBreadth}; +use super::generics::grid::{TrackList as GenericTrackList, TrackSize as GenericTrackSize}; +use super::generics::transform::IsParallelTo; +use super::generics::{self, GreaterThanOrEqualToOne, NonNegative}; +use super::{CSSFloat, CSSInteger}; +use crate::context::QuirksMode; +use crate::parser::{Parse, ParserContext}; +use crate::values::specified::calc::CalcNode; +use crate::values::{serialize_atom_identifier, serialize_number, AtomString}; +use crate::{Atom, Namespace, One, Prefix, Zero}; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use std::ops::Add; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(feature = "gecko")] +pub use self::align::{AlignContent, AlignItems, AlignSelf, AlignTracks, ContentDistribution}; +#[cfg(feature = "gecko")] +pub use self::align::{JustifyContent, JustifyItems, JustifySelf, JustifyTracks, SelfAlignment}; +pub use self::angle::{AllowUnitlessZeroAngle, Angle}; +pub use self::animation::{ + AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState, + AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis, + ScrollTimelineName, TransitionProperty, ViewTimelineInset +}; +pub use self::background::{BackgroundRepeat, BackgroundSize}; +pub use self::basic_shape::FillRule; +pub use self::border::{ + BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice, + BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle, LineWidth, +}; +pub use self::box_::{ + Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize, + ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow, + OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign, + ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, + TouchAction, VerticalAlign, WillChange, Zoom, +}; +pub use self::color::{ + Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust, +}; +pub use self::column::ColumnCount; +pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet}; +pub use self::easing::TimingFunction; +pub use self::effects::{BoxShadow, Filter, SimpleShadow}; +pub use self::flex::FlexBasis; +pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle}; +pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; +pub use self::font::{ + FontSize, FontSizeAdjust, FontSizeAdjustFactor, FontSizeKeyword, FontStretch, FontSynthesis, +}; +pub use self::font::{FontVariantAlternates, FontWeight}; +pub use self::font::{FontVariantEastAsian, FontVariationSettings, LineHeight}; +pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale}; +pub use self::image::{EndingShape as GradientEndingShape, Gradient, Image, ImageRendering}; +pub use self::length::{AbsoluteLength, CalcLengthPercentage, CharacterWidth}; +pub use self::length::{FontRelativeLength, Length, LengthOrNumber, NonNegativeLengthOrNumber}; +pub use self::length::{LengthOrAuto, LengthPercentage, LengthPercentageOrAuto}; +pub use self::length::{MaxSize, Size}; +pub use self::length::{NoCalcLength, ViewportPercentageLength, ViewportVariant}; +pub use self::length::{ + NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +#[cfg(feature = "gecko")] +pub use self::list::ListStyleType; +pub use self::list::Quotes; +pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate}; +pub use self::outline::OutlineStyle; +pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize}; +pub use self::percentage::{NonNegativePercentage, Percentage}; +pub use self::position::AspectRatio; +pub use self::position::{GridAutoFlow, GridTemplateAreas, Position, PositionOrAuto}; +pub use self::position::{MasonryAutoFlow, MasonryItemOrder, MasonryPlacement}; +pub use self::position::{PositionComponent, ZIndex}; +pub use self::ratio::Ratio; +pub use self::rect::NonNegativeLengthOrNumberRect; +pub use self::resolution::Resolution; +pub use self::svg::{DProperty, MozContextProperties}; +pub use self::svg::{SVGLength, SVGOpacity, SVGPaint}; +pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth}; +pub use self::svg_path::SVGPathData; +pub use self::text::HyphenateCharacter; +pub use self::text::RubyPosition; +pub use self::text::TextAlignLast; +pub use self::text::TextUnderlinePosition; +pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextAlign, TextIndent}; +pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak}; +pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing}; +pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify, TextTransform}; +pub use self::time::Time; +pub use self::transform::{Rotate, Scale, Transform}; +pub use self::transform::{TransformBox, TransformOrigin, TransformStyle, Translate}; +#[cfg(feature = "gecko")] +pub use self::ui::CursorImage; +pub use self::ui::{BoolInteger, Cursor, UserSelect}; +pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent; + +#[cfg(feature = "gecko")] +pub mod align; +pub mod angle; +pub mod animation; +pub mod background; +pub mod basic_shape; +pub mod border; +#[path = "box.rs"] +pub mod box_; +pub mod calc; +pub mod color; +pub mod column; +pub mod counters; +pub mod easing; +pub mod effects; +pub mod flex; +pub mod font; +#[cfg(feature = "gecko")] +pub mod gecko; +pub mod grid; +pub mod image; +pub mod length; +pub mod list; +pub mod motion; +pub mod outline; +pub mod page; +pub mod percentage; +pub mod position; +pub mod ratio; +pub mod rect; +pub mod resolution; +pub mod source_size_list; +pub mod svg; +pub mod svg_path; +pub mod table; +pub mod text; +pub mod time; +pub mod transform; +pub mod ui; +pub mod url; + +/// <angle> | <percentage> +/// https://drafts.csswg.org/css-values/#typedef-angle-percentage +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AngleOrPercentage { + Percentage(Percentage), + Angle(Angle), +} + +impl AngleOrPercentage { + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_unitless_zero: AllowUnitlessZeroAngle, + ) -> Result<Self, ParseError<'i>> { + if let Ok(per) = input.try_parse(|i| Percentage::parse(context, i)) { + return Ok(AngleOrPercentage::Percentage(per)); + } + + Angle::parse_internal(context, input, allow_unitless_zero).map(AngleOrPercentage::Angle) + } + + /// Allow unitless angles, used for conic-gradients as specified by the spec. + /// https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle + pub fn parse_with_unitless<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::Yes) + } +} + +impl Parse for AngleOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::No) + } +} + +/// Parse a `<number>` value, with a given clamping mode. +fn parse_number_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, +) -> Result<Number, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => { + Ok(Number { + value, + calc_clamping_mode: None, + }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let value = CalcNode::parse_number(context, input, function)?; + Ok(Number { + value, + calc_clamping_mode: Some(clamping_mode), + }) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } +} + +/// A CSS `<number>` specified value. +/// +/// https://drafts.csswg.org/css-values-3/#number-value +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialOrd, ToShmem)] +pub struct Number { + /// The numeric value itself. + value: CSSFloat, + /// If this number came from a calc() expression, this tells how clamping + /// should be done on the value. + calc_clamping_mode: Option<AllowedNumericType>, +} + +impl Parse for Number { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl PartialEq<Number> for Number { + fn eq(&self, other: &Number) -> bool { + if self.calc_clamping_mode != other.calc_clamping_mode { + return false; + } + + self.value == other.value || (self.value.is_nan() && other.value.is_nan()) + } +} + +impl Number { + /// Returns a new number with the value `val`. + #[inline] + fn new_with_clamping_mode( + value: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Self { + value, + calc_clamping_mode, + } + } + + /// Returns this percentage as a number. + pub fn to_percentage(&self) -> Percentage { + Percentage::new_with_clamping_mode(self.value, self.calc_clamping_mode) + } + + /// Returns a new number with the value `val`. + #[inline] + pub fn new(val: CSSFloat) -> Self { + Self::new_with_clamping_mode(val, None) + } + + /// Returns whether this number came from a `calc()` expression. + #[inline] + pub fn was_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + /// Returns the numeric value, clamped if needed. + #[inline] + pub fn get(&self) -> f32 { + crate::values::normalize( + self.calc_clamping_mode + .map_or(self.value, |mode| mode.clamp(self.value)), + ) + .min(f32::MAX) + .max(f32::MIN) + } + + #[allow(missing_docs)] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Number, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + #[allow(missing_docs)] + pub fn parse_at_least_one<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Number, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) + } + + /// Clamp to 1.0 if the value is over 1.0. + #[inline] + pub fn clamp_to_one(self) -> Self { + Number { + value: self.value.min(1.), + calc_clamping_mode: self.calc_clamping_mode, + } + } +} + +impl ToComputedValue for Number { + type ComputedValue = CSSFloat; + + #[inline] + fn to_computed_value(&self, _: &Context) -> CSSFloat { + self.get() + } + + #[inline] + fn from_computed_value(computed: &CSSFloat) -> Self { + Number { + value: *computed, + calc_clamping_mode: None, + } + } +} + +impl ToCss for Number { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_number(self.value, self.calc_clamping_mode.is_some(), dest) + } +} + +impl IsParallelTo for (Number, Number, Number) { + fn is_parallel_to(&self, vector: &DirectionVector) -> bool { + use euclid::approxeq::ApproxEq; + // If a and b is parallel, the angle between them is 0deg, so + // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0. + let self_vector = DirectionVector::new(self.0.get(), self.1.get(), self.2.get()); + self_vector + .cross(*vector) + .square_length() + .approx_eq(&0.0f32) + } +} + +impl SpecifiedValueInfo for Number {} + +impl Add for Number { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self::new(self.get() + other.get()) + } +} + +impl Zero for Number { + #[inline] + fn zero() -> Self { + Self::new(0.) + } + + #[inline] + fn is_zero(&self) -> bool { + self.get() == 0. + } +} + +impl From<Number> for f32 { + #[inline] + fn from(n: Number) -> Self { + n.get() + } +} + +impl From<Number> for f64 { + #[inline] + fn from(n: Number) -> Self { + n.get() as f64 + } +} + +/// A Number which is >= 0.0. +pub type NonNegativeNumber = NonNegative<Number>; + +impl Parse for NonNegativeNumber { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + .map(NonNegative::<Number>) + } +} + +impl One for NonNegativeNumber { + #[inline] + fn one() -> Self { + NonNegativeNumber::new(1.0) + } + + #[inline] + fn is_one(&self) -> bool { + self.get() == 1.0 + } +} + +impl NonNegativeNumber { + /// Returns a new non-negative number with the value `val`. + pub fn new(val: CSSFloat) -> Self { + NonNegative::<Number>(Number::new(val.max(0.))) + } + + /// Returns the numeric value. + #[inline] + pub fn get(&self) -> f32 { + self.0.get() + } +} + +/// An Integer which is >= 0. +pub type NonNegativeInteger = NonNegative<Integer>; + +impl Parse for NonNegativeInteger { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Integer::parse_non_negative(context, input)?)) + } +} + +/// A Number which is >= 1.0. +pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<Number>; + +impl Parse for GreaterThanOrEqualToOneNumber { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) + .map(GreaterThanOrEqualToOne::<Number>) + } +} + +/// <number> | <percentage> +/// +/// Accepts only non-negative numbers. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum NumberOrPercentage { + Percentage(Percentage), + Number(Number), +} + +impl NumberOrPercentage { + fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + type_: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + if let Ok(per) = + input.try_parse(|i| Percentage::parse_with_clamping_mode(context, i, type_)) + { + return Ok(NumberOrPercentage::Percentage(per)); + } + + parse_number_with_clamping_mode(context, input, type_).map(NumberOrPercentage::Number) + } + + /// Parse a non-negative number or percentage. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + /// Convert the number or the percentage to a number. + pub fn to_percentage(self) -> Percentage { + match self { + Self::Percentage(p) => p, + Self::Number(n) => n.to_percentage(), + } + } + + /// Convert the number or the percentage to a number. + pub fn to_number(self) -> Number { + match self { + Self::Percentage(p) => p.to_number(), + Self::Number(n) => n, + } + } +} + +impl Parse for NumberOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +/// A non-negative <number> | <percentage>. +pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>; + +impl NonNegativeNumberOrPercentage { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + NonNegative(NumberOrPercentage::Percentage(Percentage::hundred())) + } + + /// Return a particular number. + #[inline] + pub fn new_number(n: f32) -> Self { + NonNegative(NumberOrPercentage::Number(Number::new(n))) + } +} + +impl Parse for NonNegativeNumberOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(NumberOrPercentage::parse_non_negative( + context, input, + )?)) + } +} + +/// The value of Opacity is <alpha-value>, which is "<number> | <percentage>". +/// However, we serialize the specified value as number, so it's ok to store +/// the Opacity as Number. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub struct Opacity(Number); + +impl Parse for Opacity { + /// Opacity accepts <number> | <percentage>, so we parse it as NumberOrPercentage, + /// and then convert into an Number if it's a Percentage. + /// https://drafts.csswg.org/cssom/#serializing-css-values + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let number = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(Opacity(number)) + } +} + +impl ToComputedValue for Opacity { + type ComputedValue = CSSFloat; + + #[inline] + fn to_computed_value(&self, context: &Context) -> CSSFloat { + let value = self.0.to_computed_value(context); + if context.for_smil_animation { + // SMIL expects to be able to interpolate between out-of-range + // opacity values. + value + } else { + value.min(1.0).max(0.0) + } + } + + #[inline] + fn from_computed_value(computed: &CSSFloat) -> Self { + Opacity(Number::from_computed_value(computed)) + } +} + +/// A specified `<integer>`, optionally coming from a `calc()` expression. +/// +/// <https://drafts.csswg.org/css-values/#integers> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, ToShmem)] +pub struct Integer { + value: CSSInteger, + was_calc: bool, +} + +impl Zero for Integer { + #[inline] + fn zero() -> Self { + Self::new(0) + } + + #[inline] + fn is_zero(&self) -> bool { + self.value() == 0 + } +} + +impl One for Integer { + #[inline] + fn one() -> Self { + Self::new(1) + } + + #[inline] + fn is_one(&self) -> bool { + self.value() == 1 + } +} + +impl PartialEq<i32> for Integer { + fn eq(&self, value: &i32) -> bool { + self.value() == *value + } +} + +impl Integer { + /// Trivially constructs a new `Integer` value. + pub fn new(val: CSSInteger) -> Self { + Integer { + value: val, + was_calc: false, + } + } + + /// Returns the integer value associated with this value. + pub fn value(&self) -> CSSInteger { + self.value + } + + /// Trivially constructs a new integer value from a `calc()` expression. + fn from_calc(val: CSSInteger) -> Self { + Integer { + value: val, + was_calc: true, + } + } +} + +impl Parse for Integer { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { + int_value: Some(v), .. + } => Ok(Integer::new(v)), + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let result = CalcNode::parse_integer(context, input, function)?; + Ok(Integer::from_calc(result)) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Integer { + /// Parse an integer value which is at least `min`. + pub fn parse_with_minimum<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + min: i32, + ) -> Result<Integer, ParseError<'i>> { + let value = Integer::parse(context, input)?; + // FIXME(emilio): The spec asks us to avoid rejecting it at parse + // time except until computed value time. + // + // It's not totally clear it's worth it though, and no other browser + // does this. + if value.value() < min { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(value) + } + + /// Parse a non-negative integer. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Integer, ParseError<'i>> { + Integer::parse_with_minimum(context, input, 0) + } + + /// Parse a positive integer (>= 1). + pub fn parse_positive<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Integer, ParseError<'i>> { + Integer::parse_with_minimum(context, input, 1) + } +} + +impl ToComputedValue for Integer { + type ComputedValue = i32; + + #[inline] + fn to_computed_value(&self, _: &Context) -> i32 { + self.value + } + + #[inline] + fn from_computed_value(computed: &i32) -> Self { + Integer::new(*computed) + } +} + +impl ToCss for Integer { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.was_calc { + dest.write_str("calc(")?; + } + self.value.to_css(dest)?; + if self.was_calc { + dest.write_char(')')?; + } + Ok(()) + } +} + +impl SpecifiedValueInfo for Integer {} + +/// A wrapper of Integer, with value >= 1. +pub type PositiveInteger = GreaterThanOrEqualToOne<Integer>; + +impl Parse for PositiveInteger { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne) + } +} + +/// The specified value of a grid `<track-breadth>` +pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>; + +/// The specified value of a grid `<track-size>` +pub type TrackSize = GenericTrackSize<LengthPercentage>; + +/// The specified value of a grid `<track-size>+` +pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>; + +/// The specified value of a grid `<track-list>` +/// (could also be `<auto-track-list>` or `<explicit-track-list>`) +pub type TrackList = GenericTrackList<LengthPercentage, Integer>; + +/// The specified value of a `<grid-line>`. +pub type GridLine = GenericGridLine<Integer>; + +/// `<grid-template-rows> | <grid-template-columns>` +pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>; + +/// rect(...) +pub type ClipRect = generics::GenericClipRect<LengthOrAuto>; + +impl Parse for ClipRect { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ClipRect { + /// Parses a rect(<top>, <left>, <bottom>, <right>), allowing quirks. + fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("rect")?; + + fn parse_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<LengthOrAuto, ParseError<'i>> { + LengthOrAuto::parse_quirky(context, input, allow_quirks) + } + + input.parse_nested_block(|input| { + let top = parse_argument(context, input, allow_quirks)?; + let right; + let bottom; + let left; + + if input.try_parse(|input| input.expect_comma()).is_ok() { + right = parse_argument(context, input, allow_quirks)?; + input.expect_comma()?; + bottom = parse_argument(context, input, allow_quirks)?; + input.expect_comma()?; + left = parse_argument(context, input, allow_quirks)?; + } else { + right = parse_argument(context, input, allow_quirks)?; + bottom = parse_argument(context, input, allow_quirks)?; + left = parse_argument(context, input, allow_quirks)?; + } + + Ok(ClipRect { + top, + right, + bottom, + left, + }) + }) + } +} + +/// rect(...) | auto +pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>; + +impl ClipRectOrAuto { + /// Parses a ClipRect or Auto, allowing quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if let Ok(v) = input.try_parse(|i| ClipRect::parse_quirky(context, i, allow_quirks)) { + return Ok(generics::GenericClipRectOrAuto::Rect(v)); + } + input.expect_ident_matching("auto")?; + Ok(generics::GenericClipRectOrAuto::Auto) + } +} + +/// Whether quirks are allowed in this context. +#[derive(Clone, Copy, PartialEq)] +pub enum AllowQuirks { + /// Quirks are not allowed. + No, + /// Quirks are allowed, in quirks mode. + Yes, + /// Quirks are always allowed, used for SVG lengths. + Always, +} + +impl AllowQuirks { + /// Returns `true` if quirks are allowed in this context. + pub fn allowed(self, quirks_mode: QuirksMode) -> bool { + match self { + AllowQuirks::Always => true, + AllowQuirks::No => false, + AllowQuirks::Yes => quirks_mode == QuirksMode::Quirks, + } + } +} + +/// An attr(...) rule +/// +/// `[namespace? `|`]? ident` +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(function)] +#[repr(C)] +pub struct Attr { + /// Optional namespace prefix. + pub namespace_prefix: Prefix, + /// Optional namespace URL. + pub namespace_url: Namespace, + /// Attribute name + pub attribute: Atom, + /// Fallback value + pub fallback: AtomString, +} + +impl Parse for Attr { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Attr, ParseError<'i>> { + input.expect_function_matching("attr")?; + input.parse_nested_block(|i| Attr::parse_function(context, i)) + } +} + +/// Get the Namespace for a given prefix from the namespace map. +fn get_namespace_for_prefix(prefix: &Prefix, context: &ParserContext) -> Option<Namespace> { + context.namespaces.prefixes.get(prefix).cloned() +} + +/// Try to parse a namespace and return it if parsed, or none if there was not one present +fn parse_namespace<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<(Prefix, Namespace), ParseError<'i>> { + let ns_prefix = match input.next()? { + Token::Ident(ref prefix) => Some(Prefix::from(prefix.as_ref())), + Token::Delim('|') => None, + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }; + + if ns_prefix.is_some() && !matches!(*input.next_including_whitespace()?, Token::Delim('|')) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if let Some(prefix) = ns_prefix { + let ns = match get_namespace_for_prefix(&prefix, context) { + Some(ns) => ns, + None => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }; + Ok((prefix, ns)) + } else { + Ok((Prefix::default(), Namespace::default())) + } +} + +impl Attr { + /// Parse contents of attr() assuming we have already parsed `attr` and are + /// within a parse_nested_block() + pub fn parse_function<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Attr, ParseError<'i>> { + // Syntax is `[namespace? '|']? ident [',' fallback]?` + let namespace = input + .try_parse(|input| parse_namespace(context, input)) + .ok(); + let namespace_is_some = namespace.is_some(); + let (namespace_prefix, namespace_url) = namespace.unwrap_or_default(); + + // If there is a namespace, ensure no whitespace following '|' + let attribute = Atom::from(if namespace_is_some { + let location = input.current_source_location(); + match *input.next_including_whitespace()? { + Token::Ident(ref ident) => ident.as_ref(), + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } else { + input.expect_ident()?.as_ref() + }); + + // Fallback will always be a string value for now as we do not support + // attr() types yet. + let fallback = input + .try_parse(|input| -> Result<AtomString, ParseError<'i>> { + input.expect_comma()?; + Ok(input.expect_string()?.as_ref().into()) + }) + .unwrap_or_default(); + + Ok(Attr { + namespace_prefix, + namespace_url, + attribute, + fallback, + }) + } +} + +impl ToCss for Attr { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("attr(")?; + if !self.namespace_prefix.is_empty() { + serialize_atom_identifier(&self.namespace_prefix, dest)?; + dest.write_char('|')?; + } + serialize_atom_identifier(&self.attribute, dest)?; + + if !self.fallback.is_empty() { + dest.write_str(", ")?; + self.fallback.to_css(dest)?; + } + + dest.write_char(')') + } +} diff --git a/servo/components/style/values/specified/motion.rs b/servo/components/style/values/specified/motion.rs new file mode 100644 index 0000000000..98858c712c --- /dev/null +++ b/servo/components/style/values/specified/motion.rs @@ -0,0 +1,343 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values that are related to motion path. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::motion::OffsetRotate as ComputedOffsetRotate; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::motion as generics; +use crate::values::specified::basic_shape::BasicShape; +use crate::values::specified::position::{HorizontalPosition, VerticalPosition}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, Position}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// The specified value of ray() function. +pub type RayFunction = generics::GenericRayFunction<Angle, Position>; + +/// The specified value of <offset-path>. +pub type OffsetPathFunction = + generics::GenericOffsetPathFunction<BasicShape, RayFunction, SpecifiedUrl>; + +/// The specified value of `offset-path`. +pub type OffsetPath = generics::GenericOffsetPath<OffsetPathFunction>; + +/// The specified value of `offset-position`. +pub type OffsetPosition = generics::GenericOffsetPosition<HorizontalPosition, VerticalPosition>; + +/// The <coord-box> value, which defines the box that the <offset-path> sizes into. +/// https://drafts.fxtf.org/motion-1/#valdef-offset-path-coord-box +/// +/// <coord-box> = content-box | padding-box | border-box | fill-box | stroke-box | view-box +/// https://drafts.csswg.org/css-box-4/#typedef-coord-box +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum CoordBox { + ContentBox, + PaddingBox, + BorderBox, + FillBox, + StrokeBox, + ViewBox, +} + +impl CoordBox { + /// Returns true if it is default value, border-box. + #[inline] + pub fn is_default(&self) -> bool { + matches!(*self, Self::BorderBox) + } +} + +impl Parse for RayFunction { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if !static_prefs::pref!("layout.css.motion-path-ray.enabled") { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + input.expect_function_matching("ray")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl RayFunction { + /// Parse the inner arguments of a `ray` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::PositionOrAuto; + + let mut angle = None; + let mut size = None; + let mut contain = false; + let mut position = None; + loop { + if angle.is_none() { + angle = input.try_parse(|i| Angle::parse(context, i)).ok(); + } + + if size.is_none() { + size = input.try_parse(generics::RaySize::parse).ok(); + if size.is_some() { + continue; + } + } + + if !contain { + contain = input + .try_parse(|i| i.expect_ident_matching("contain")) + .is_ok(); + if contain { + continue; + } + } + + if position.is_none() { + if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + let pos = Position::parse(context, input)?; + position = Some(PositionOrAuto::Position(pos)); + } + + if position.is_some() { + continue; + } + } + break; + } + + if angle.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(RayFunction { + angle: angle.unwrap(), + // If no <ray-size> is specified it defaults to closest-side. + size: size.unwrap_or(generics::RaySize::ClosestSide), + contain, + position: position.unwrap_or(PositionOrAuto::auto()), + }) + } +} + +impl Parse for OffsetPathFunction { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::basic_shape::{AllowedBasicShapes, ShapeType}; + + // <offset-path> = <ray()> | <url> | <basic-shape> + // https://drafts.fxtf.org/motion-1/#typedef-offset-path + + if static_prefs::pref!("layout.css.motion-path-ray.enabled") { + if let Ok(ray) = input.try_parse(|i| RayFunction::parse(context, i)) { + return Ok(OffsetPathFunction::Ray(ray)); + } + } + + if static_prefs::pref!("layout.css.motion-path-url.enabled") { + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(OffsetPathFunction::Url(url)); + } + } + + let allowed_shapes = if static_prefs::pref!("layout.css.motion-path-basic-shapes.enabled") { + AllowedBasicShapes::ALL + } else { + AllowedBasicShapes::PATH + }; + + BasicShape::parse(context, input, allowed_shapes, ShapeType::Outline) + .map(OffsetPathFunction::Shape) + } +} + +impl Parse for OffsetPath { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Parse none. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(OffsetPath::none()); + } + + let mut path = None; + let mut coord_box = None; + loop { + if path.is_none() { + path = input + .try_parse(|i| OffsetPathFunction::parse(context, i)) + .ok(); + } + + if static_prefs::pref!("layout.css.motion-path-coord-box.enabled") && + coord_box.is_none() + { + coord_box = input.try_parse(CoordBox::parse).ok(); + if coord_box.is_some() { + continue; + } + } + break; + } + + if let Some(p) = path { + return Ok(OffsetPath::OffsetPath { + path: Box::new(p), + coord_box: coord_box.unwrap_or(CoordBox::BorderBox), + }); + } + + match coord_box { + Some(c) => Ok(OffsetPath::CoordBox(c)), + None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} + +/// The direction of offset-rotate. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum OffsetRotateDirection { + /// Unspecified direction keyword. + #[css(skip)] + None, + /// 0deg offset (face forward). + Auto, + /// 180deg offset (face backward). + Reverse, +} + +impl OffsetRotateDirection { + /// Returns true if it is none (i.e. the keyword is not specified). + #[inline] + fn is_none(&self) -> bool { + *self == OffsetRotateDirection::None + } +} + +#[inline] +fn direction_specified_and_angle_is_zero(direction: &OffsetRotateDirection, angle: &Angle) -> bool { + !direction.is_none() && angle.is_zero() +} + +/// The specified offset-rotate. +/// The syntax is: "[ auto | reverse ] || <angle>" +/// +/// https://drafts.fxtf.org/motion-1/#offset-rotate-property +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct OffsetRotate { + /// [auto | reverse]. + #[css(skip_if = "OffsetRotateDirection::is_none")] + direction: OffsetRotateDirection, + /// <angle>. + /// If direction is None, this is a fixed angle which indicates a + /// constant clockwise rotation transformation applied to it by this + /// specified rotation angle. Otherwise, the angle will be added to + /// the angle of the direction in layout. + #[css(contextual_skip_if = "direction_specified_and_angle_is_zero")] + angle: Angle, +} + +impl OffsetRotate { + /// Returns the initial value, auto. + #[inline] + pub fn auto() -> Self { + OffsetRotate { + direction: OffsetRotateDirection::Auto, + angle: Angle::zero(), + } + } + + /// Returns true if self is auto 0deg. + #[inline] + pub fn is_auto(&self) -> bool { + self.direction == OffsetRotateDirection::Auto && self.angle.is_zero() + } +} + +impl Parse for OffsetRotate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let mut direction = input.try_parse(OffsetRotateDirection::parse); + let angle = input.try_parse(|i| Angle::parse(context, i)); + if direction.is_err() { + // The direction and angle could be any order, so give it a change to parse + // direction again. + direction = input.try_parse(OffsetRotateDirection::parse); + } + + if direction.is_err() && angle.is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(OffsetRotate { + direction: direction.unwrap_or(OffsetRotateDirection::None), + angle: angle.unwrap_or(Zero::zero()), + }) + } +} + +impl ToComputedValue for OffsetRotate { + type ComputedValue = ComputedOffsetRotate; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::Angle as ComputedAngle; + + ComputedOffsetRotate { + auto: !self.direction.is_none(), + angle: if self.direction == OffsetRotateDirection::Reverse { + // The computed value should always convert "reverse" into "auto". + // e.g. "reverse calc(20deg + 10deg)" => "auto 210deg" + self.angle.to_computed_value(context) + ComputedAngle::from_degrees(180.0) + } else { + self.angle.to_computed_value(context) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + OffsetRotate { + direction: if computed.auto { + OffsetRotateDirection::Auto + } else { + OffsetRotateDirection::None + }, + angle: ToComputedValue::from_computed_value(&computed.angle), + } + } +} diff --git a/servo/components/style/values/specified/outline.rs b/servo/components/style/values/specified/outline.rs new file mode 100644 index 0000000000..6e5382d4c2 --- /dev/null +++ b/servo/components/style/values/specified/outline.rs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified values for outline properties + +use crate::parser::{Parse, ParserContext}; +use crate::values::specified::BorderStyle; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use style_traits::ParseError; + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Ord, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// <https://drafts.csswg.org/css-ui/#propdef-outline-style> +pub enum OutlineStyle { + /// auto + Auto, + /// <border-style> + BorderStyle(BorderStyle), +} + +impl OutlineStyle { + #[inline] + /// Get default value as None + pub fn none() -> OutlineStyle { + OutlineStyle::BorderStyle(BorderStyle::None) + } + + #[inline] + /// Get value for None or Hidden + pub fn none_or_hidden(&self) -> bool { + match *self { + OutlineStyle::Auto => false, + OutlineStyle::BorderStyle(ref style) => style.none_or_hidden(), + } + } +} + +impl Parse for OutlineStyle { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<OutlineStyle, ParseError<'i>> { + if let Ok(border_style) = input.try_parse(BorderStyle::parse) { + if let BorderStyle::Hidden = border_style { + return Err(input + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent("hidden".into()))); + } + + return Ok(OutlineStyle::BorderStyle(border_style)); + } + + input.expect_ident_matching("auto")?; + Ok(OutlineStyle::Auto) + } +} diff --git a/servo/components/style/values/specified/page.rs b/servo/components/style/values/specified/page.rs new file mode 100644 index 0000000000..76d9105e8f --- /dev/null +++ b/servo/components/style/values/specified/page.rs @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified @page at-rule properties and named-page style properties + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::size::Size2D; +use crate::values::specified::length::NonNegativeLength; +use crate::values::{generics, CustomIdent}; +use cssparser::Parser; +use style_traits::ParseError; + +pub use generics::page::PageOrientation; +pub use generics::page::PageSizeOrientation; +pub use generics::page::PaperSize; +/// Specified value of the @page size descriptor +pub type PageSize = generics::page::PageSize<Size2D<NonNegativeLength>>; + +impl Parse for PageSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Try to parse as <page-size> [ <orientation> ] + if let Ok(paper_size) = input.try_parse(PaperSize::parse) { + let orientation = input + .try_parse(PageSizeOrientation::parse) + .unwrap_or(PageSizeOrientation::Portrait); + return Ok(PageSize::PaperSize(paper_size, orientation)); + } + // Try to parse as <orientation> [ <page-size> ] + if let Ok(orientation) = input.try_parse(PageSizeOrientation::parse) { + if let Ok(paper_size) = input.try_parse(PaperSize::parse) { + return Ok(PageSize::PaperSize(paper_size, orientation)); + } + return Ok(PageSize::Orientation(orientation)); + } + // Try to parse dimensions + if let Ok(size) = + input.try_parse(|i| Size2D::parse_with(context, i, NonNegativeLength::parse)) + { + return Ok(PageSize::Size(size)); + } + // auto value + input.expect_ident_matching("auto")?; + Ok(PageSize::Auto) + } +} + +/// Page name value. +/// +/// https://drafts.csswg.org/css-page-3/#using-named-pages +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum PageName { + /// `auto` value. + Auto, + /// Page name value + PageName(CustomIdent), +} + +impl Parse for PageName { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + Ok(match_ignore_ascii_case! { ident, + "auto" => PageName::auto(), + _ => PageName::PageName(CustomIdent::from_ident(location, ident, &[])?), + }) + } +} + +impl PageName { + /// `auto` value. + #[inline] + pub fn auto() -> Self { + PageName::Auto + } + + /// Whether this is the `auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, PageName::Auto) + } +} diff --git a/servo/components/style/values/specified/percentage.rs b/servo/components/style/values/specified/percentage.rs new file mode 100644 index 0000000000..ccf16d6463 --- /dev/null +++ b/servo/components/style/values/specified/percentage.rs @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified percentages. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::percentage::Percentage as ComputedPercentage; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::NonNegative; +use crate::values::specified::calc::CalcNode; +use crate::values::specified::Number; +use crate::values::{normalize, serialize_percentage, CSSFloat}; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss}; + +/// A percentage value. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +pub struct Percentage { + /// The percentage value as a float. + /// + /// [0 .. 100%] maps to [0.0 .. 1.0] + value: CSSFloat, + /// If this percentage came from a calc() expression, this tells how + /// clamping should be done on the value. + calc_clamping_mode: Option<AllowedNumericType>, +} + +impl ToCss for Percentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.calc_clamping_mode.is_some() { + dest.write_str("calc(")?; + } + + serialize_percentage(self.value, dest)?; + + if self.calc_clamping_mode.is_some() { + dest.write_char(')')?; + } + Ok(()) + } +} + +impl Percentage { + /// Creates a percentage from a numeric value. + pub(super) fn new_with_clamping_mode( + value: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Self { + value, + calc_clamping_mode, + } + } + + /// Creates a percentage from a numeric value. + pub fn new(value: CSSFloat) -> Self { + Self::new_with_clamping_mode(value, None) + } + + /// `0%` + #[inline] + pub fn zero() -> Self { + Percentage { + value: 0., + calc_clamping_mode: None, + } + } + + /// `100%` + #[inline] + pub fn hundred() -> Self { + Percentage { + value: 1., + calc_clamping_mode: None, + } + } + + /// Gets the underlying value for this float. + pub fn get(&self) -> CSSFloat { + self.calc_clamping_mode + .map_or(self.value, |mode| mode.clamp(self.value)) + } + + /// Returns this percentage as a number. + pub fn to_number(&self) -> Number { + Number::new_with_clamping_mode(self.value, self.calc_clamping_mode) + } + + /// Returns the calc() clamping mode for this percentage. + pub fn calc_clamping_mode(&self) -> Option<AllowedNumericType> { + self.calc_clamping_mode + } + + /// Reverses this percentage, preserving calc-ness. + /// + /// For example: If it was 20%, convert it into 80%. + pub fn reverse(&mut self) { + let new_value = 1. - self.value; + self.value = new_value; + } + + /// Parses a specific kind of percentage. + pub fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Percentage { unit_value, .. } + if num_context.is_ok(context.parsing_mode, unit_value) => + { + Ok(Percentage::new(unit_value)) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let value = CalcNode::parse_percentage(context, input, function)?; + Ok(Percentage { + value, + calc_clamping_mode: Some(num_context), + }) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parses a percentage token, but rejects it if it's negative. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + /// Parses a percentage token, but rejects it if it's negative or more than + /// 100%. + pub fn parse_zero_to_a_hundred<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::ZeroToOne) + } + + /// Clamp to 100% if the value is over 100%. + #[inline] + pub fn clamp_to_hundred(self) -> Self { + Percentage { + value: self.value.min(1.), + calc_clamping_mode: self.calc_clamping_mode, + } + } +} + +impl Parse for Percentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl ToComputedValue for Percentage { + type ComputedValue = ComputedPercentage; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + ComputedPercentage(normalize(self.get())) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Percentage::new(computed.0) + } +} + +impl SpecifiedValueInfo for Percentage {} + +/// Turns the percentage into a plain float. +pub trait ToPercentage { + /// Returns whether this percentage used to be a calc(). + fn is_calc(&self) -> bool { + false + } + /// Turns the percentage into a plain float. + fn to_percentage(&self) -> CSSFloat; +} + +impl ToPercentage for Percentage { + fn is_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + fn to_percentage(&self) -> CSSFloat { + self.get() + } +} + +/// A wrapper of Percentage, whose value must be >= 0. +pub type NonNegativePercentage = NonNegative<Percentage>; + +impl Parse for NonNegativePercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Percentage::parse_non_negative(context, input)?)) + } +} + +impl NonNegativePercentage { + /// Convert to ComputedPercentage, for FontFaceRule size-adjust getter. + #[inline] + pub fn compute(&self) -> ComputedPercentage { + ComputedPercentage(self.0.get()) + } +} diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs new file mode 100644 index 0000000000..bab853d972 --- /dev/null +++ b/servo/components/style/values/specified/position.rs @@ -0,0 +1,955 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the specified value of +//! [`position`][position]s +//! +//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position + +use crate::parser::{Parse, ParserContext}; +use crate::selector_map::PrecomputedHashMap; +use crate::str::HTML_SPACE_CHARACTERS; +use crate::values::computed::LengthPercentage as ComputedLengthPercentage; +use crate::values::computed::{Context, Percentage, ToComputedValue}; +use crate::values::generics::position::AspectRatio as GenericAspectRatio; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::position::PositionComponent as GenericPositionComponent; +use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto; +use crate::values::generics::position::ZIndex as GenericZIndex; +use crate::values::specified::{AllowQuirks, Integer, LengthPercentage, NonNegativeNumber}; +use crate::{Atom, Zero}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use servo_arc::Arc; +use std::collections::hash_map::Entry; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// The specified value of a CSS `<position>` +pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>; + +/// The specified value of an `auto | <position>`. +pub type PositionOrAuto = GenericPositionOrAuto<Position>; + +/// The specified value of a horizontal position. +pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>; + +/// The specified value of a vertical position. +pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>; + +/// The specified value of a component of a CSS `<position>`. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum PositionComponent<S> { + /// `center` + Center, + /// `<length-percentage>` + Length(LengthPercentage), + /// `<side> <length-percentage>?` + Side(S, Option<LengthPercentage>), +} + +/// A keyword for the X direction. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum HorizontalPositionKeyword { + Left, + Right, +} + +/// A keyword for the Y direction. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum VerticalPositionKeyword { + Top, + Bottom, +} + +impl Parse for Position { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let position = Self::parse_three_value_quirky(context, input, AllowQuirks::No)?; + if position.is_three_value_syntax() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(position) + } +} + +impl Position { + /// Parses a `<bg-position>`, with quirks. + pub fn parse_three_value_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + match input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) { + Ok(x_pos @ PositionComponent::Center) => { + if let Ok(y_pos) = + input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) + { + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = input + .try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) + .unwrap_or(x_pos); + let y_pos = PositionComponent::Center; + return Ok(Self::new(x_pos, y_pos)); + }, + Ok(PositionComponent::Side(x_keyword, lp)) => { + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + let x_pos = PositionComponent::Side(x_keyword, lp); + let y_pos = PositionComponent::Center; + return Ok(Self::new(x_pos, y_pos)); + } + if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { + let y_lp = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + let x_pos = PositionComponent::Side(x_keyword, lp); + let y_pos = PositionComponent::Side(y_keyword, y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = PositionComponent::Side(x_keyword, None); + let y_pos = lp.map_or(PositionComponent::Center, PositionComponent::Length); + return Ok(Self::new(x_pos, y_pos)); + }, + Ok(x_pos @ PositionComponent::Length(_)) => { + if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { + let y_pos = PositionComponent::Side(y_keyword, None); + return Ok(Self::new(x_pos, y_pos)); + } + if let Ok(y_lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + { + let y_pos = PositionComponent::Length(y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let y_pos = PositionComponent::Center; + let _ = input.try_parse(|i| i.expect_ident_matching("center")); + return Ok(Self::new(x_pos, y_pos)); + }, + Err(_) => {}, + } + let y_keyword = VerticalPositionKeyword::parse(input)?; + let lp_and_x_pos: Result<_, ParseError> = input.try_parse(|i| { + let y_lp = i + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) { + let x_lp = i + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + let x_pos = PositionComponent::Side(x_keyword, x_lp); + return Ok((y_lp, x_pos)); + }; + i.expect_ident_matching("center")?; + let x_pos = PositionComponent::Center; + Ok((y_lp, x_pos)) + }); + if let Ok((y_lp, x_pos)) = lp_and_x_pos { + let y_pos = PositionComponent::Side(y_keyword, y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = PositionComponent::Center; + let y_pos = PositionComponent::Side(y_keyword, None); + Ok(Self::new(x_pos, y_pos)) + } + + /// `center center` + #[inline] + pub fn center() -> Self { + Self::new(PositionComponent::Center, PositionComponent::Center) + } + + /// Returns true if this uses a 3 value syntax. + #[inline] + fn is_three_value_syntax(&self) -> bool { + self.horizontal.component_count() != self.vertical.component_count() + } +} + +impl ToCss for Position { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match (&self.horizontal, &self.vertical) { + ( + x_pos @ &PositionComponent::Side(_, Some(_)), + &PositionComponent::Length(ref y_lp), + ) => { + x_pos.to_css(dest)?; + dest.write_str(" top ")?; + y_lp.to_css(dest) + }, + ( + &PositionComponent::Length(ref x_lp), + y_pos @ &PositionComponent::Side(_, Some(_)), + ) => { + dest.write_str("left ")?; + x_lp.to_css(dest)?; + dest.write_char(' ')?; + y_pos.to_css(dest) + }, + (x_pos, y_pos) => { + x_pos.to_css(dest)?; + dest.write_char(' ')?; + y_pos.to_css(dest) + }, + } + } +} + +impl<S: Parse> Parse for PositionComponent<S> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl<S: Parse> PositionComponent<S> { + /// Parses a component of a CSS position, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + return Ok(PositionComponent::Center); + } + if let Ok(lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + { + return Ok(PositionComponent::Length(lp)); + } + let keyword = S::parse(context, input)?; + let lp = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + Ok(PositionComponent::Side(keyword, lp)) + } +} + +impl<S> GenericPositionComponent for PositionComponent<S> { + fn is_center(&self) -> bool { + match *self { + PositionComponent::Center => true, + PositionComponent::Length(LengthPercentage::Percentage(ref per)) => per.0 == 0.5, + // 50% from any side is still the center. + PositionComponent::Side(_, Some(LengthPercentage::Percentage(ref per))) => per.0 == 0.5, + _ => false, + } + } +} + +impl<S> PositionComponent<S> { + /// `0%` + pub fn zero() -> Self { + PositionComponent::Length(LengthPercentage::Percentage(Percentage::zero())) + } + + /// Returns the count of this component. + fn component_count(&self) -> usize { + match *self { + PositionComponent::Length(..) | PositionComponent::Center => 1, + PositionComponent::Side(_, ref lp) => { + if lp.is_some() { + 2 + } else { + 1 + } + }, + } + } +} + +impl<S: Side> ToComputedValue for PositionComponent<S> { + type ComputedValue = ComputedLengthPercentage; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + PositionComponent::Center => ComputedLengthPercentage::new_percent(Percentage(0.5)), + PositionComponent::Side(ref keyword, None) => { + let p = Percentage(if keyword.is_start() { 0. } else { 1. }); + ComputedLengthPercentage::new_percent(p) + }, + PositionComponent::Side(ref keyword, Some(ref length)) if !keyword.is_start() => { + let length = length.to_computed_value(context); + // We represent `<end-side> <length>` as `calc(100% - <length>)`. + ComputedLengthPercentage::hundred_percent_minus(length, AllowedNumericType::All) + }, + PositionComponent::Side(_, Some(ref length)) | + PositionComponent::Length(ref length) => length.to_computed_value(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + PositionComponent::Length(ToComputedValue::from_computed_value(computed)) + } +} + +impl<S: Side> PositionComponent<S> { + /// The initial specified value of a position component, i.e. the start side. + pub fn initial_specified_value() -> Self { + PositionComponent::Side(S::start(), None) + } +} + +/// Represents a side, either horizontal or vertical, of a CSS position. +pub trait Side { + /// Returns the start side. + fn start() -> Self; + + /// Returns whether this side is the start side. + fn is_start(&self) -> bool; +} + +impl Side for HorizontalPositionKeyword { + #[inline] + fn start() -> Self { + HorizontalPositionKeyword::Left + } + + #[inline] + fn is_start(&self) -> bool { + *self == Self::start() + } +} + +impl Side for VerticalPositionKeyword { + #[inline] + fn start() -> Self { + VerticalPositionKeyword::Top + } + + #[inline] + fn is_start(&self) -> bool { + *self == Self::start() + } +} + +/// Controls how the auto-placement algorithm works specifying exactly how auto-placed items +/// get flowed into the grid. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "row,column,dense")] +#[repr(C)] +pub struct GridAutoFlow(u8); +bitflags! { + impl GridAutoFlow: u8 { + /// 'row' - mutually exclusive with 'column' + const ROW = 1 << 0; + /// 'column' - mutually exclusive with 'row' + const COLUMN = 1 << 1; + /// 'dense' + const DENSE = 1 << 2; + } +} + +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Masonry auto-placement algorithm packing. +pub enum MasonryPlacement { + /// Place the item in the track(s) with the smallest extent so far. + Pack, + /// Place the item after the last item, from start to end. + Next, +} + +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Masonry auto-placement algorithm item sorting option. +pub enum MasonryItemOrder { + /// Place all items with a definite placement before auto-placed items. + DefiniteFirst, + /// Place items in `order-modified document order`. + Ordered, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Controls how the Masonry layout algorithm works +/// specifying exactly how auto-placed items get flowed in the masonry axis. +pub struct MasonryAutoFlow { + /// Specify how to pick a auto-placement track. + #[css(contextual_skip_if = "is_pack_with_non_default_order")] + pub placement: MasonryPlacement, + /// Specify how to pick an item to place. + #[css(skip_if = "is_item_order_definite_first")] + pub order: MasonryItemOrder, +} + +#[inline] +fn is_pack_with_non_default_order(placement: &MasonryPlacement, order: &MasonryItemOrder) -> bool { + *placement == MasonryPlacement::Pack && *order != MasonryItemOrder::DefiniteFirst +} + +#[inline] +fn is_item_order_definite_first(order: &MasonryItemOrder) -> bool { + *order == MasonryItemOrder::DefiniteFirst +} + +impl MasonryAutoFlow { + #[inline] + /// Get initial `masonry-auto-flow` value. + pub fn initial() -> MasonryAutoFlow { + MasonryAutoFlow { + placement: MasonryPlacement::Pack, + order: MasonryItemOrder::DefiniteFirst, + } + } +} + +impl Parse for MasonryAutoFlow { + /// [ definite-first | ordered ] || [ pack | next ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MasonryAutoFlow, ParseError<'i>> { + let mut value = MasonryAutoFlow::initial(); + let mut got_placement = false; + let mut got_order = false; + while !input.is_exhausted() { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let success = match_ignore_ascii_case! { &ident, + "pack" if !got_placement => { + got_placement = true; + true + }, + "next" if !got_placement => { + value.placement = MasonryPlacement::Next; + got_placement = true; + true + }, + "definite-first" if !got_order => { + got_order = true; + true + }, + "ordered" if !got_order => { + value.order = MasonryItemOrder::Ordered; + got_order = true; + true + }, + _ => false + }; + if !success { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + } + } + + if got_placement || got_order { + Ok(value) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +// TODO: Can be derived with some care. +impl Parse for GridAutoFlow { + /// [ row | column ] || dense + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<GridAutoFlow, ParseError<'i>> { + let mut track = None; + let mut dense = GridAutoFlow::empty(); + + while !input.is_exhausted() { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let success = match_ignore_ascii_case! { &ident, + "row" if track.is_none() => { + track = Some(GridAutoFlow::ROW); + true + }, + "column" if track.is_none() => { + track = Some(GridAutoFlow::COLUMN); + true + }, + "dense" if dense.is_empty() => { + dense = GridAutoFlow::DENSE; + true + }, + _ => false, + }; + if !success { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + } + } + + if track.is_some() || !dense.is_empty() { + Ok(track.unwrap_or(GridAutoFlow::ROW) | dense) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for GridAutoFlow { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if *self == GridAutoFlow::ROW { + return dest.write_str("row"); + } + + if *self == GridAutoFlow::COLUMN { + return dest.write_str("column"); + } + + if *self == GridAutoFlow::ROW | GridAutoFlow::DENSE { + return dest.write_str("dense"); + } + + if *self == GridAutoFlow::COLUMN | GridAutoFlow::DENSE { + return dest.write_str("column dense"); + } + + debug_assert!(false, "Unknown or invalid grid-autoflow value"); + Ok(()) + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// https://drafts.csswg.org/css-grid/#named-grid-area +pub struct TemplateAreas { + /// `named area` containing for each template area + #[css(skip)] + pub areas: crate::OwnedSlice<NamedArea>, + /// The simplified CSS strings for serialization purpose. + /// https://drafts.csswg.org/css-grid/#serialize-template + // Note: We also use the length of `strings` when computing the explicit grid end line number + // (i.e. row number). + #[css(iterable)] + pub strings: crate::OwnedSlice<crate::OwnedStr>, + /// The number of columns of the grid. + #[css(skip)] + pub width: u32, +} + +/// Parser for grid template areas. +#[derive(Default)] +pub struct TemplateAreasParser { + areas: Vec<NamedArea>, + area_indices: PrecomputedHashMap<Atom, usize>, + strings: Vec<crate::OwnedStr>, + width: u32, + row: u32, +} + +impl TemplateAreasParser { + /// Parse a single string. + pub fn try_parse_string<'i>( + &mut self, + input: &mut Parser<'i, '_>, + ) -> Result<(), ParseError<'i>> { + input.try_parse(|input| { + self.parse_string(input.expect_string()?) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }) + } + + /// Parse a single string. + fn parse_string(&mut self, string: &str) -> Result<(), ()> { + self.row += 1; + let mut simplified_string = String::new(); + let mut current_area_index: Option<usize> = None; + let mut column = 0u32; + for token in TemplateAreasTokenizer(string) { + column += 1; + if column > 1 { + simplified_string.push(' '); + } + let name = if let Some(token) = token? { + simplified_string.push_str(token); + Atom::from(token) + } else { + if let Some(index) = current_area_index.take() { + if self.areas[index].columns.end != column { + return Err(()); + } + } + simplified_string.push('.'); + continue; + }; + if let Some(index) = current_area_index { + if self.areas[index].name == name { + if self.areas[index].rows.start == self.row { + self.areas[index].columns.end += 1; + } + continue; + } + if self.areas[index].columns.end != column { + return Err(()); + } + } + match self.area_indices.entry(name) { + Entry::Occupied(ref e) => { + let index = *e.get(); + if self.areas[index].columns.start != column || + self.areas[index].rows.end != self.row + { + return Err(()); + } + self.areas[index].rows.end += 1; + current_area_index = Some(index); + }, + Entry::Vacant(v) => { + let index = self.areas.len(); + let name = v.key().clone(); + v.insert(index); + self.areas.push(NamedArea { + name, + columns: UnsignedRange { + start: column, + end: column + 1, + }, + rows: UnsignedRange { + start: self.row, + end: self.row + 1, + }, + }); + current_area_index = Some(index); + }, + } + } + if column == 0 { + // Each string must produce a valid token. + // https://github.com/w3c/csswg-drafts/issues/5110 + return Err(()); + } + if let Some(index) = current_area_index { + if self.areas[index].columns.end != column + 1 { + debug_assert_ne!(self.areas[index].rows.start, self.row); + return Err(()); + } + } + if self.row == 1 { + self.width = column; + } else if self.width != column { + return Err(()); + } + + self.strings.push(simplified_string.into()); + Ok(()) + } + + /// Return the parsed template areas. + pub fn finish(self) -> Result<TemplateAreas, ()> { + if self.strings.is_empty() { + return Err(()); + } + Ok(TemplateAreas { + areas: self.areas.into(), + strings: self.strings.into(), + width: self.width, + }) + } +} + +impl TemplateAreas { + fn parse_internal(input: &mut Parser) -> Result<Self, ()> { + let mut parser = TemplateAreasParser::default(); + while parser.try_parse_string(input).is_ok() {} + parser.finish() + } +} + +impl Parse for TemplateAreas { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(input) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// Arc type for `Arc<TemplateAreas>` +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct TemplateAreasArc(#[ignore_malloc_size_of = "Arc"] pub Arc<TemplateAreas>); + +impl Parse for TemplateAreasArc { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let parsed = TemplateAreas::parse(context, input)?; + Ok(TemplateAreasArc(Arc::new(parsed))) + } +} + +/// A range of rows or columns. Using this instead of std::ops::Range for FFI +/// purposes. +#[repr(C)] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct UnsignedRange { + /// The start of the range. + pub start: u32, + /// The end of the range. + pub end: u32, +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Not associated with any particular grid item, but can be referenced from the +/// grid-placement properties. +pub struct NamedArea { + /// Name of the `named area` + pub name: Atom, + /// Rows of the `named area` + pub rows: UnsignedRange, + /// Columns of the `named area` + pub columns: UnsignedRange, +} + +/// Tokenize the string into a list of the tokens, +/// using longest-match semantics +struct TemplateAreasTokenizer<'a>(&'a str); + +impl<'a> Iterator for TemplateAreasTokenizer<'a> { + type Item = Result<Option<&'a str>, ()>; + + fn next(&mut self) -> Option<Self::Item> { + let rest = self.0.trim_start_matches(HTML_SPACE_CHARACTERS); + if rest.is_empty() { + return None; + } + if rest.starts_with('.') { + self.0 = &rest[rest.find(|c| c != '.').unwrap_or(rest.len())..]; + return Some(Ok(None)); + } + if !rest.starts_with(is_name_code_point) { + return Some(Err(())); + } + let token_len = rest.find(|c| !is_name_code_point(c)).unwrap_or(rest.len()); + let token = &rest[..token_len]; + self.0 = &rest[token_len..]; + Some(Ok(Some(token))) + } +} + +fn is_name_code_point(c: char) -> bool { + c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z' || + c >= '\u{80}' || + c == '_' || + c >= '0' && c <= '9' || + c == '-' +} + +/// This property specifies named grid areas. +/// +/// The syntax of this property also provides a visualization of the structure +/// of the grid, making the overall layout of the grid container easier to +/// understand. +#[repr(C, u8)] +#[derive( + Clone, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum GridTemplateAreas { + /// The `none` value. + None, + /// The actual value. + Areas(TemplateAreasArc), +} + +impl GridTemplateAreas { + #[inline] + /// Get default value as `none` + pub fn none() -> GridTemplateAreas { + GridTemplateAreas::None + } +} + +/// A specified value for the `z-index` property. +pub type ZIndex = GenericZIndex<Integer>; + +/// A specified value for the `aspect-ratio` property. +pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>; + +impl Parse for AspectRatio { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::generics::position::PreferredRatio; + use crate::values::specified::Ratio; + + let location = input.current_source_location(); + let mut auto = input.try_parse(|i| i.expect_ident_matching("auto")); + let ratio = input.try_parse(|i| Ratio::parse(context, i)); + if auto.is_err() { + auto = input.try_parse(|i| i.expect_ident_matching("auto")); + } + + if auto.is_err() && ratio.is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(AspectRatio { + auto: auto.is_ok(), + ratio: match ratio { + Ok(ratio) => PreferredRatio::Ratio(ratio), + Err(..) => PreferredRatio::None, + }, + }) + } +} + +impl AspectRatio { + /// Returns Self by a valid ratio. + pub fn from_mapped_ratio(w: f32, h: f32) -> Self { + use crate::values::generics::position::PreferredRatio; + use crate::values::generics::ratio::Ratio; + AspectRatio { + auto: true, + ratio: PreferredRatio::Ratio(Ratio( + NonNegativeNumber::new(w), + NonNegativeNumber::new(h), + )), + } + } +} diff --git a/servo/components/style/values/specified/ratio.rs b/servo/components/style/values/specified/ratio.rs new file mode 100644 index 0000000000..4cdddd452e --- /dev/null +++ b/servo/components/style/values/specified/ratio.rs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for <ratio>. +//! +//! [ratio]: https://drafts.csswg.org/css-values/#ratios + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::ratio::Ratio as GenericRatio; +use crate::values::specified::NonNegativeNumber; +use crate::One; +use cssparser::Parser; +use style_traits::ParseError; + +/// A specified <ratio> value. +pub type Ratio = GenericRatio<NonNegativeNumber>; + +impl Parse for Ratio { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let a = NonNegativeNumber::parse(context, input)?; + let b = match input.try_parse(|input| input.expect_delim('/')) { + Ok(()) => NonNegativeNumber::parse(context, input)?, + _ => One::one(), + }; + + Ok(GenericRatio(a, b)) + } +} diff --git a/servo/components/style/values/specified/rect.rs b/servo/components/style/values/specified/rect.rs new file mode 100644 index 0000000000..7955ecaa48 --- /dev/null +++ b/servo/components/style/values/specified/rect.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS borders. + +use crate::values::generics::rect::Rect; +use crate::values::specified::length::NonNegativeLengthOrNumber; + +/// A specified rectangle made of four `<length-or-number>` values. +pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>; diff --git a/servo/components/style/values/specified/resolution.rs b/servo/components/style/values/specified/resolution.rs new file mode 100644 index 0000000000..74f100972a --- /dev/null +++ b/servo/components/style/values/specified/resolution.rs @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Resolution values: +//! +//! https://drafts.csswg.org/css-values/#resolution + +use crate::parser::{Parse, ParserContext}; +use crate::values::specified::CalcNode; +use crate::values::CSSFloat; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A specified resolution. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +pub struct Resolution { + value: CSSFloat, + unit: ResolutionUnit, + was_calc: bool, +} + +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +enum ResolutionUnit { + /// Dots per inch. + Dpi, + /// An alias unit for dots per pixel. + X, + /// Dots per pixel. + Dppx, + /// Dots per centimeter. + Dpcm, +} + +impl ResolutionUnit { + fn as_str(self) -> &'static str { + match self { + Self::Dpi => "dpi", + Self::X => "x", + Self::Dppx => "dppx", + Self::Dpcm => "dpcm", + } + } +} + +impl Resolution { + /// Returns a resolution value from dppx units. + pub fn from_dppx(value: CSSFloat) -> Self { + Self { + value, + unit: ResolutionUnit::Dppx, + was_calc: false, + } + } + + /// Returns a resolution value from dppx units. + pub fn from_x(value: CSSFloat) -> Self { + Self { + value, + unit: ResolutionUnit::X, + was_calc: false, + } + } + + /// Returns a resolution value from dppx units. + pub fn from_dppx_calc(value: CSSFloat) -> Self { + Self { + value, + unit: ResolutionUnit::Dppx, + was_calc: true, + } + } + + /// Convert this resolution value to dppx units. + pub fn dppx(&self) -> CSSFloat { + match self.unit { + ResolutionUnit::X | ResolutionUnit::Dppx => self.value, + _ => self.dpi() / 96.0, + } + } + + /// Convert this resolution value to dpi units. + pub fn dpi(&self) -> CSSFloat { + match self.unit { + ResolutionUnit::Dpi => self.value, + ResolutionUnit::X | ResolutionUnit::Dppx => self.value * 96.0, + ResolutionUnit::Dpcm => self.value * 2.54, + } + } + + /// Parse a resolution given a value and unit. + pub fn parse_dimension<'i, 't>(value: CSSFloat, unit: &str) -> Result<Self, ()> { + let unit = match_ignore_ascii_case! { &unit, + "dpi" => ResolutionUnit::Dpi, + "dppx" => ResolutionUnit::Dppx, + "dpcm" => ResolutionUnit::Dpcm, + "x" => ResolutionUnit::X, + _ => return Err(()) + }; + Ok(Self { + value, + unit, + was_calc: false, + }) + } +} + +impl ToCss for Resolution { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.value, + self.unit.as_str(), + self.was_calc, + dest, + ) + } +} + +impl Parse for Resolution { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Dimension { + value, ref unit, .. + } if value >= 0. => Self::parse_dimension(value, unit) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse_resolution(context, input, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } +} diff --git a/servo/components/style/values/specified/source_size_list.rs b/servo/components/style/values/specified/source_size_list.rs new file mode 100644 index 0000000000..ac47461cc4 --- /dev/null +++ b/servo/components/style/values/specified/source_size_list.rs @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! https://html.spec.whatwg.org/multipage/#source-size-list + +use crate::media_queries::Device; +use crate::parser::{Parse, ParserContext}; +use crate::queries::{FeatureType, QueryCondition}; +use crate::values::computed::{self, ToComputedValue}; +use crate::values::specified::{Length, NoCalcLength, ViewportPercentageLength}; +use app_units::Au; +use cssparser::{Delimiter, Parser, Token}; +use selectors::context::QuirksMode; +use style_traits::ParseError; + +/// A value for a `<source-size>`: +/// +/// https://html.spec.whatwg.org/multipage/#source-size +#[derive(Debug)] +pub struct SourceSize { + condition: QueryCondition, + value: Length, +} + +impl Parse for SourceSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let condition = QueryCondition::parse(context, input, FeatureType::Media)?; + let value = Length::parse_non_negative(context, input)?; + Ok(Self { condition, value }) + } +} + +/// A value for a `<source-size-list>`: +/// +/// https://html.spec.whatwg.org/multipage/#source-size-list +#[derive(Debug)] +pub struct SourceSizeList { + source_sizes: Vec<SourceSize>, + value: Option<Length>, +} + +impl SourceSizeList { + /// Create an empty `SourceSizeList`, which can be used as a fall-back. + pub fn empty() -> Self { + Self { + source_sizes: vec![], + value: None, + } + } + + /// Evaluate this <source-size-list> to get the final viewport length. + pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> Au { + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + let matching_source_size = self.source_sizes.iter().find(|source_size| { + source_size + .condition + .matches(context) + .to_bool(/* unknown = */ false) + }); + + match matching_source_size { + Some(source_size) => source_size.value.to_computed_value(context), + None => match self.value { + Some(ref v) => v.to_computed_value(context), + None => Length::NoCalc(NoCalcLength::ViewportPercentage( + ViewportPercentageLength::Vw(100.), + )) + .to_computed_value(context), + }, + } + }) + .into() + } +} + +enum SourceSizeOrLength { + SourceSize(SourceSize), + Length(Length), +} + +impl Parse for SourceSizeOrLength { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(size) = input.try_parse(|input| SourceSize::parse(context, input)) { + return Ok(SourceSizeOrLength::SourceSize(size)); + } + + let length = Length::parse_non_negative(context, input)?; + Ok(SourceSizeOrLength::Length(length)) + } +} + +impl SourceSizeList { + /// NOTE(emilio): This doesn't match the grammar in the spec, see: + /// + /// https://html.spec.whatwg.org/multipage/#parsing-a-sizes-attribute + pub fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Self { + let mut source_sizes = vec![]; + + loop { + let result = input.parse_until_before(Delimiter::Comma, |input| { + SourceSizeOrLength::parse(context, input) + }); + + match result { + Ok(SourceSizeOrLength::Length(value)) => { + return Self { + source_sizes, + value: Some(value), + }; + }, + Ok(SourceSizeOrLength::SourceSize(source_size)) => { + source_sizes.push(source_size); + }, + Err(..) => {}, + } + + match input.next() { + Ok(&Token::Comma) => {}, + Err(..) => break, + _ => unreachable!(), + } + } + + SourceSizeList { + source_sizes, + value: None, + } + } +} diff --git a/servo/components/style/values/specified/svg.rs b/servo/components/style/values/specified/svg.rs new file mode 100644 index 0000000000..8ab2dbb223 --- /dev/null +++ b/servo/components/style/values/specified/svg.rs @@ -0,0 +1,404 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for SVG properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::svg as generic; +use crate::values::specified::color::Color; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::AllowQuirks; +use crate::values::specified::LengthPercentage; +use crate::values::specified::SVGPathData; +use crate::values::specified::{NonNegativeLengthPercentage, Opacity}; +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CommaWithSpace, CssWriter, ParseError, Separator}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// Specified SVG Paint value +pub type SVGPaint = generic::GenericSVGPaint<Color, SpecifiedUrl>; + +/// <length> | <percentage> | <number> | context-value +pub type SVGLength = generic::GenericSVGLength<LengthPercentage>; + +/// A non-negative version of SVGLength. +pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>; + +/// [ <length> | <percentage> | <number> ]# | context-value +pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>; + +/// Whether the `context-value` value is enabled. +#[cfg(feature = "gecko")] +pub fn is_context_value_enabled() -> bool { + static_prefs::pref!("gfx.font_rendering.opentype_svg.enabled") +} + +/// Whether the `context-value` value is enabled. +#[cfg(not(feature = "gecko"))] +pub fn is_context_value_enabled() -> bool { + false +} + +macro_rules! parse_svg_length { + ($ty:ty, $lp:ty) => { + impl Parse for $ty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(lp) = + input.try_parse(|i| <$lp>::parse_quirky(context, i, AllowQuirks::Always)) + { + return Ok(generic::SVGLength::LengthPercentage(lp)); + } + + try_match_ident_ignore_ascii_case! { input, + "context-value" if is_context_value_enabled() => { + Ok(generic::SVGLength::ContextValue) + }, + } + } + } + }; +} + +parse_svg_length!(SVGLength, LengthPercentage); +parse_svg_length!(SVGWidth, NonNegativeLengthPercentage); + +impl Parse for SVGStrokeDashArray { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(values) = input.try_parse(|i| { + CommaWithSpace::parse(i, |i| { + NonNegativeLengthPercentage::parse_quirky(context, i, AllowQuirks::Always) + }) + }) { + return Ok(generic::SVGStrokeDashArray::Values(values.into())); + } + + try_match_ident_ignore_ascii_case! { input, + "context-value" if is_context_value_enabled() => { + Ok(generic::SVGStrokeDashArray::ContextValue) + }, + "none" => Ok(generic::SVGStrokeDashArray::Values(Default::default())), + } + } +} + +/// <opacity-value> | context-fill-opacity | context-stroke-opacity +pub type SVGOpacity = generic::SVGOpacity<Opacity>; + +/// The specified value for a single CSS paint-order property. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, ToCss)] +pub enum PaintOrder { + /// `normal` variant + Normal = 0, + /// `fill` variant + Fill = 1, + /// `stroke` variant + Stroke = 2, + /// `markers` variant + Markers = 3, +} + +/// Number of non-normal components +pub const PAINT_ORDER_COUNT: u8 = 3; + +/// Number of bits for each component +pub const PAINT_ORDER_SHIFT: u8 = 2; + +/// Mask with above bits set +pub const PAINT_ORDER_MASK: u8 = 0b11; + +/// The specified value is tree `PaintOrder` values packed into the +/// bitfields below, as a six-bit field, of 3 two-bit pairs +/// +/// Each pair can be set to FILL, STROKE, or MARKERS +/// Lowest significant bit pairs are highest priority. +/// `normal` is the empty bitfield. The three pairs are +/// never zero in any case other than `normal`. +/// +/// Higher priority values, i.e. the values specified first, +/// will be painted first (and may be covered by paintings of lower priority) +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct SVGPaintOrder(pub u8); + +impl SVGPaintOrder { + /// Get default `paint-order` with `0` + pub fn normal() -> Self { + SVGPaintOrder(0) + } + + /// Get variant of `paint-order` + pub fn order_at(&self, pos: u8) -> PaintOrder { + match (self.0 >> pos * PAINT_ORDER_SHIFT) & PAINT_ORDER_MASK { + 0 => PaintOrder::Normal, + 1 => PaintOrder::Fill, + 2 => PaintOrder::Stroke, + 3 => PaintOrder::Markers, + _ => unreachable!("this cannot happen"), + } + } +} + +impl Parse for SVGPaintOrder { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SVGPaintOrder, ParseError<'i>> { + if let Ok(()) = input.try_parse(|i| i.expect_ident_matching("normal")) { + return Ok(SVGPaintOrder::normal()); + } + + let mut value = 0; + // bitfield representing what we've seen so far + // bit 1 is fill, bit 2 is stroke, bit 3 is markers + let mut seen = 0; + let mut pos = 0; + + loop { + let result: Result<_, ParseError> = input.try_parse(|input| { + try_match_ident_ignore_ascii_case! { input, + "fill" => Ok(PaintOrder::Fill), + "stroke" => Ok(PaintOrder::Stroke), + "markers" => Ok(PaintOrder::Markers), + } + }); + + match result { + Ok(val) => { + if (seen & (1 << val as u8)) != 0 { + // don't parse the same ident twice + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + value |= (val as u8) << (pos * PAINT_ORDER_SHIFT); + seen |= 1 << (val as u8); + pos += 1; + }, + Err(_) => break, + } + } + + if value == 0 { + // Couldn't find any keyword + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // fill in rest + for i in pos..PAINT_ORDER_COUNT { + for paint in 1..(PAINT_ORDER_COUNT + 1) { + // if not seen, set bit at position, mark as seen + if (seen & (1 << paint)) == 0 { + seen |= 1 << paint; + value |= paint << (i * PAINT_ORDER_SHIFT); + break; + } + } + } + + Ok(SVGPaintOrder(value)) + } +} + +impl ToCss for SVGPaintOrder { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.0 == 0 { + return dest.write_str("normal"); + } + + let mut last_pos_to_serialize = 0; + for i in (1..PAINT_ORDER_COUNT).rev() { + let component = self.order_at(i); + let earlier_component = self.order_at(i - 1); + if component < earlier_component { + last_pos_to_serialize = i - 1; + break; + } + } + + for pos in 0..last_pos_to_serialize + 1 { + if pos != 0 { + dest.write_char(' ')? + } + self.order_at(pos).to_css(dest)?; + } + Ok(()) + } +} + +/// The context properties we understand. +#[derive( + Clone, + Copy, + Eq, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ContextPropertyBits(u8); +bitflags! { + impl ContextPropertyBits: u8 { + /// `fill` + const FILL = 1 << 0; + /// `stroke` + const STROKE = 1 << 1; + /// `fill-opacity` + const FILL_OPACITY = 1 << 2; + /// `stroke-opacity` + const STROKE_OPACITY = 1 << 3; + } +} + +/// Specified MozContextProperties value. +/// Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-context-properties) +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct MozContextProperties { + #[css(iterable, if_empty = "none")] + #[ignore_malloc_size_of = "Arc"] + idents: crate::ArcSlice<CustomIdent>, + #[css(skip)] + bits: ContextPropertyBits, +} + +impl Parse for MozContextProperties { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozContextProperties, ParseError<'i>> { + let mut values = vec![]; + let mut bits = ContextPropertyBits::empty(); + loop { + { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + if ident.eq_ignore_ascii_case("none") && values.is_empty() { + return Ok(Self::default()); + } + + let ident = CustomIdent::from_ident(location, ident, &["all", "none", "auto"])?; + + if ident.0 == atom!("fill") { + bits.insert(ContextPropertyBits::FILL); + } else if ident.0 == atom!("stroke") { + bits.insert(ContextPropertyBits::STROKE); + } else if ident.0 == atom!("fill-opacity") { + bits.insert(ContextPropertyBits::FILL_OPACITY); + } else if ident.0 == atom!("stroke-opacity") { + bits.insert(ContextPropertyBits::STROKE_OPACITY); + } + + values.push(ident); + } + + let location = input.current_source_location(); + match input.next() { + Ok(&Token::Comma) => continue, + Err(..) => break, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + } + } + + if values.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(MozContextProperties { + idents: crate::ArcSlice::from_iter(values.into_iter()), + bits, + }) + } +} + +/// The svg d property type. +/// +/// https://svgwg.org/svg2-draft/paths.html#TheDProperty +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum DProperty { + /// Path value for path(<string>) or just a <string>. + #[css(function)] + Path(SVGPathData), + /// None value. + #[animation(error)] + None, +} + +impl DProperty { + /// return none. + #[inline] + pub fn none() -> Self { + DProperty::None + } +} + +impl Parse for DProperty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Parse none. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(DProperty::none()); + } + + // Parse possible functions. + input.expect_function_matching("path")?; + let path_data = input.parse_nested_block(|i| Parse::parse(context, i))?; + Ok(DProperty::Path(path_data)) + } +} diff --git a/servo/components/style/values/specified/svg_path.rs b/servo/components/style/values/specified/svg_path.rs new file mode 100644 index 0000000000..1eb9866dd1 --- /dev/null +++ b/servo/components/style/values/specified/svg_path.rs @@ -0,0 +1,1029 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for SVG Path. + +use crate::parser::{Parse, ParserContext}; +use crate::values::animated::{lists, Animate, Procedure, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::CSSFloat; +use cssparser::Parser; +use std::fmt::{self, Write}; +use std::iter::{Cloned, Peekable}; +use std::slice; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// Whether to allow empty string in the parser. +#[derive(Clone, Debug, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum AllowEmpty { + Yes, + No, +} + +/// The SVG path data. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct SVGPathData( + // TODO(emilio): Should probably measure this somehow only from the + // specified values. + #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice<PathCommand>, +); + +impl SVGPathData { + /// Get the array of PathCommand. + #[inline] + pub fn commands(&self) -> &[PathCommand] { + &self.0 + } + + /// Create a normalized copy of this path by converting each relative + /// command to an absolute command. + pub fn normalize(&self) -> Self { + let mut state = PathTraversalState { + subpath_start: CoordPair::new(0.0, 0.0), + pos: CoordPair::new(0.0, 0.0), + }; + let iter = self.0.iter().map(|seg| seg.normalize(&mut state)); + SVGPathData(crate::ArcSlice::from_iter(iter)) + } + + // FIXME: Bug 1714238, we may drop this once we use the same data structure for both SVG and + // CSS. + /// Decode the svg path raw data from Gecko. + #[cfg(feature = "gecko")] + pub fn decode_from_f32_array(path: &[f32]) -> Result<Self, ()> { + use crate::gecko_bindings::structs::dom::SVGPathSeg_Binding::*; + + let mut result: Vec<PathCommand> = Vec::new(); + let mut i: usize = 0; + while i < path.len() { + // See EncodeType() and DecodeType() in SVGPathSegUtils.h. + // We are using reinterpret_cast<> to encode and decode between u32 and f32, so here we + // use to_bits() to decode the type. + let seg_type = path[i].to_bits() as u16; + i = i + 1; + match seg_type { + PATHSEG_CLOSEPATH => result.push(PathCommand::ClosePath), + PATHSEG_MOVETO_ABS | PATHSEG_MOVETO_REL => { + debug_assert!(i + 1 < path.len()); + result.push(PathCommand::MoveTo { + point: CoordPair::new(path[i], path[i + 1]), + absolute: IsAbsolute::new(seg_type == PATHSEG_MOVETO_ABS), + }); + i = i + 2; + }, + PATHSEG_LINETO_ABS | PATHSEG_LINETO_REL => { + debug_assert!(i + 1 < path.len()); + result.push(PathCommand::LineTo { + point: CoordPair::new(path[i], path[i + 1]), + absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_ABS), + }); + i = i + 2; + }, + PATHSEG_CURVETO_CUBIC_ABS | PATHSEG_CURVETO_CUBIC_REL => { + debug_assert!(i + 5 < path.len()); + result.push(PathCommand::CurveTo { + control1: CoordPair::new(path[i], path[i + 1]), + control2: CoordPair::new(path[i + 2], path[i + 3]), + point: CoordPair::new(path[i + 4], path[i + 5]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_ABS), + }); + i = i + 6; + }, + PATHSEG_CURVETO_QUADRATIC_ABS | PATHSEG_CURVETO_QUADRATIC_REL => { + debug_assert!(i + 3 < path.len()); + result.push(PathCommand::QuadBezierCurveTo { + control1: CoordPair::new(path[i], path[i + 1]), + point: CoordPair::new(path[i + 2], path[i + 3]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_ABS), + }); + i = i + 4; + }, + PATHSEG_ARC_ABS | PATHSEG_ARC_REL => { + debug_assert!(i + 6 < path.len()); + result.push(PathCommand::EllipticalArc { + rx: path[i], + ry: path[i + 1], + angle: path[i + 2], + large_arc_flag: ArcFlag(path[i + 3] != 0.0f32), + sweep_flag: ArcFlag(path[i + 4] != 0.0f32), + point: CoordPair::new(path[i + 5], path[i + 6]), + absolute: IsAbsolute::new(seg_type == PATHSEG_ARC_ABS), + }); + i = i + 7; + }, + PATHSEG_LINETO_HORIZONTAL_ABS | PATHSEG_LINETO_HORIZONTAL_REL => { + debug_assert!(i < path.len()); + result.push(PathCommand::HorizontalLineTo { + x: path[i], + absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_HORIZONTAL_ABS), + }); + i = i + 1; + }, + PATHSEG_LINETO_VERTICAL_ABS | PATHSEG_LINETO_VERTICAL_REL => { + debug_assert!(i < path.len()); + result.push(PathCommand::VerticalLineTo { + y: path[i], + absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_VERTICAL_ABS), + }); + i = i + 1; + }, + PATHSEG_CURVETO_CUBIC_SMOOTH_ABS | PATHSEG_CURVETO_CUBIC_SMOOTH_REL => { + debug_assert!(i + 3 < path.len()); + result.push(PathCommand::SmoothCurveTo { + control2: CoordPair::new(path[i], path[i + 1]), + point: CoordPair::new(path[i + 2], path[i + 3]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_SMOOTH_ABS), + }); + i = i + 4; + }, + PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS | PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL => { + debug_assert!(i + 1 < path.len()); + result.push(PathCommand::SmoothQuadBezierCurveTo { + point: CoordPair::new(path[i], path[i + 1]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS), + }); + i = i + 2; + }, + PATHSEG_UNKNOWN | _ => return Err(()), + } + } + + Ok(SVGPathData(crate::ArcSlice::from_iter(result.into_iter()))) + } + + /// Parse this SVG path string with the argument that indicates whether we should allow the + /// empty string. + // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make + // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.) + // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident + // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable + // str::Char iterator to check each character. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + allow_empty: AllowEmpty, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let path_string = input.expect_string()?.as_ref(); + + // Parse the svg path string as multiple sub-paths. + let mut path_parser = PathParser::new(path_string); + while skip_wsp(&mut path_parser.chars) { + if path_parser.parse_subpath().is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + // The css-shapes-1 says a path data string that does conform but defines an empty path is + // invalid and causes the entire path() to be invalid, so we use the argement to decide + // whether we should allow the empty string. + // https://drafts.csswg.org/css-shapes-1/#typedef-basic-shape + if matches!(allow_empty, AllowEmpty::No) && path_parser.path.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(SVGPathData(crate::ArcSlice::from_iter( + path_parser.path.into_iter(), + ))) + } +} + +impl ToCss for SVGPathData { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + dest.write_char('"')?; + { + let mut writer = SequenceWriter::new(dest, " "); + for command in self.commands() { + writer.item(command)?; + } + } + dest.write_char('"') + } +} + +impl Parse for SVGPathData { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Note that the EBNF allows the path data string in the d property to be empty, so we + // don't reject empty SVG path data. + // https://svgwg.org/svg2-draft/single-page.html#paths-PathDataBNF + SVGPathData::parse(input, AllowEmpty::Yes) + } +} + +impl Animate for SVGPathData { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.0.len() != other.0.len() { + return Err(()); + } + + // FIXME(emilio): This allocates three copies of the path, that's not + // great! Specially, once we're normalized once, we don't need to + // re-normalize again. + let left = self.normalize(); + let right = other.normalize(); + + let items: Vec<_> = lists::by_computed_value::animate(&left.0, &right.0, procedure)?; + Ok(SVGPathData(crate::ArcSlice::from_iter(items.into_iter()))) + } +} + +impl ComputeSquaredDistance for SVGPathData { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.0.len() != other.0.len() { + return Err(()); + } + let left = self.normalize(); + let right = other.normalize(); + lists::by_computed_value::squared_distance(&left.0, &right.0) + } +} + +/// The SVG path command. +/// The fields of these commands are self-explanatory, so we skip the documents. +/// Note: the index of the control points, e.g. control1, control2, are mapping to the control +/// points of the Bézier curve in the spec. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(C, u8)] +pub enum PathCommand { + /// The unknown type. + /// https://www.w3.org/TR/SVG/paths.html#__svg__SVGPathSeg__PATHSEG_UNKNOWN + Unknown, + /// The "moveto" command. + MoveTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The "lineto" command. + LineTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The horizontal "lineto" command. + HorizontalLineTo { x: CSSFloat, absolute: IsAbsolute }, + /// The vertical "lineto" command. + VerticalLineTo { y: CSSFloat, absolute: IsAbsolute }, + /// The cubic Bézier curve command. + CurveTo { + control1: CoordPair, + control2: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The smooth curve command. + SmoothCurveTo { + control2: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The quadratic Bézier curve command. + QuadBezierCurveTo { + control1: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The smooth quadratic Bézier curve command. + SmoothQuadBezierCurveTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The elliptical arc curve command. + EllipticalArc { + rx: CSSFloat, + ry: CSSFloat, + angle: CSSFloat, + large_arc_flag: ArcFlag, + sweep_flag: ArcFlag, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The "closepath" command. + ClosePath, +} + +/// For internal SVGPath normalization. +#[allow(missing_docs)] +struct PathTraversalState { + subpath_start: CoordPair, + pos: CoordPair, +} + +impl PathCommand { + /// Create a normalized copy of this PathCommand. Absolute commands will be copied as-is while + /// for relative commands an equivalent absolute command will be returned. + /// + /// See discussion: https://github.com/w3c/svgwg/issues/321 + fn normalize(&self, state: &mut PathTraversalState) -> Self { + use self::PathCommand::*; + match *self { + Unknown => Unknown, + ClosePath => { + state.pos = state.subpath_start; + ClosePath + }, + MoveTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + state.subpath_start = point; + MoveTo { + point, + absolute: IsAbsolute::Yes, + } + }, + LineTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + LineTo { + point, + absolute: IsAbsolute::Yes, + } + }, + HorizontalLineTo { mut x, absolute } => { + if !absolute.is_yes() { + x += state.pos.x; + } + state.pos.x = x; + HorizontalLineTo { + x, + absolute: IsAbsolute::Yes, + } + }, + VerticalLineTo { mut y, absolute } => { + if !absolute.is_yes() { + y += state.pos.y; + } + state.pos.y = y; + VerticalLineTo { + y, + absolute: IsAbsolute::Yes, + } + }, + CurveTo { + mut control1, + mut control2, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control1 += state.pos; + control2 += state.pos; + point += state.pos; + } + state.pos = point; + CurveTo { + control1, + control2, + point, + absolute: IsAbsolute::Yes, + } + }, + SmoothCurveTo { + mut control2, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control2 += state.pos; + point += state.pos; + } + state.pos = point; + SmoothCurveTo { + control2, + point, + absolute: IsAbsolute::Yes, + } + }, + QuadBezierCurveTo { + mut control1, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control1 += state.pos; + point += state.pos; + } + state.pos = point; + QuadBezierCurveTo { + control1, + point, + absolute: IsAbsolute::Yes, + } + }, + SmoothQuadBezierCurveTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + SmoothQuadBezierCurveTo { + point, + absolute: IsAbsolute::Yes, + } + }, + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + point, + absolute: IsAbsolute::Yes, + } + }, + } + } +} + +impl ToCss for PathCommand { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + use self::PathCommand::*; + match *self { + Unknown => dest.write_char('X'), + ClosePath => dest.write_char('Z'), + MoveTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'M' } else { 'm' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + LineTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'L' } else { 'l' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + CurveTo { + control1, + control2, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'C' } else { 'c' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + QuadBezierCurveTo { + control1, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'Q' } else { 'q' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'A' } else { 'a' })?; + dest.write_char(' ')?; + rx.to_css(dest)?; + dest.write_char(' ')?; + ry.to_css(dest)?; + dest.write_char(' ')?; + angle.to_css(dest)?; + dest.write_char(' ')?; + large_arc_flag.to_css(dest)?; + dest.write_char(' ')?; + sweep_flag.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + HorizontalLineTo { x, absolute } => { + dest.write_char(if absolute.is_yes() { 'H' } else { 'h' })?; + dest.write_char(' ')?; + x.to_css(dest) + }, + VerticalLineTo { y, absolute } => { + dest.write_char(if absolute.is_yes() { 'V' } else { 'v' })?; + dest.write_char(' ')?; + y.to_css(dest) + }, + SmoothCurveTo { + control2, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'S' } else { 's' })?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + SmoothQuadBezierCurveTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'T' } else { 't' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + } + } +} + +/// The path command absolute type. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum IsAbsolute { + Yes, + No, +} + +impl IsAbsolute { + /// Return true if this is IsAbsolute::Yes. + #[inline] + pub fn is_yes(&self) -> bool { + *self == IsAbsolute::Yes + } + + /// Return Yes if value is true. Otherwise, return No. + #[inline] + fn new(value: bool) -> Self { + if value { + IsAbsolute::Yes + } else { + IsAbsolute::No + } + } +} + +/// The path coord type. +#[allow(missing_docs)] +#[derive( + AddAssign, + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CoordPair { + x: CSSFloat, + y: CSSFloat, +} + +impl CoordPair { + /// Create a CoordPair. + #[inline] + pub fn new(x: CSSFloat, y: CSSFloat) -> Self { + CoordPair { x, y } + } +} + +/// The EllipticalArc flag type. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ArcFlag(bool); + +impl ToCss for ArcFlag { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + (self.0 as i32).to_css(dest) + } +} + +impl Animate for ArcFlag { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + (self.0 as i32) + .animate(&(other.0 as i32), procedure) + .map(|v| ArcFlag(v > 0)) + } +} + +impl ComputeSquaredDistance for ArcFlag { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + (self.0 as i32).compute_squared_distance(&(other.0 as i32)) + } +} + +impl ToAnimatedZero for ArcFlag { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + // The 2 ArcFlags in EllipticalArc determine which one of the 4 different arcs will be + // used. (i.e. From 4 combinations). In other words, if we change the flag, we get a + // different arc. Therefore, we return *self. + // https://svgwg.org/svg2-draft/paths.html#PathDataEllipticalArcCommands + Ok(*self) + } +} + +/// SVG Path parser. +struct PathParser<'a> { + chars: Peekable<Cloned<slice::Iter<'a, u8>>>, + path: Vec<PathCommand>, +} + +macro_rules! parse_arguments { + ( + $parser:ident, + $abs:ident, + $enum:ident, + [ $para:ident => $func:ident $(, $other_para:ident => $other_func:ident)* ] + ) => { + { + loop { + let $para = $func(&mut $parser.chars)?; + $( + skip_comma_wsp(&mut $parser.chars); + let $other_para = $other_func(&mut $parser.chars)?; + )* + $parser.path.push(PathCommand::$enum { $para $(, $other_para)*, $abs }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut $parser.chars) || + $parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut $parser.chars); + } + Ok(()) + } + } +} + +impl<'a> PathParser<'a> { + /// Return a PathParser. + #[inline] + fn new(string: &'a str) -> Self { + PathParser { + chars: string.as_bytes().iter().cloned().peekable(), + path: Vec::new(), + } + } + + /// Parse a sub-path. + fn parse_subpath(&mut self) -> Result<(), ()> { + // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path + // (i.e. not a valid moveto-drawto-command-group). + self.parse_moveto()?; + + // Handle other commands. + loop { + skip_wsp(&mut self.chars); + if self.chars.peek().map_or(true, |&m| m == b'M' || m == b'm') { + break; + } + + let command = self.chars.next().unwrap(); + let abs = if command.is_ascii_uppercase() { + IsAbsolute::Yes + } else { + IsAbsolute::No + }; + + skip_wsp(&mut self.chars); + match command { + b'Z' | b'z' => self.parse_closepath(), + b'L' | b'l' => self.parse_lineto(abs), + b'H' | b'h' => self.parse_h_lineto(abs), + b'V' | b'v' => self.parse_v_lineto(abs), + b'C' | b'c' => self.parse_curveto(abs), + b'S' | b's' => self.parse_smooth_curveto(abs), + b'Q' | b'q' => self.parse_quadratic_bezier_curveto(abs), + b'T' | b't' => self.parse_smooth_quadratic_bezier_curveto(abs), + b'A' | b'a' => self.parse_elliptical_arc(abs), + _ => return Err(()), + }?; + } + Ok(()) + } + + /// Parse "moveto" command. + fn parse_moveto(&mut self) -> Result<(), ()> { + let command = match self.chars.next() { + Some(c) if c == b'M' || c == b'm' => c, + _ => return Err(()), + }; + + skip_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + let absolute = if command == b'M' { + IsAbsolute::Yes + } else { + IsAbsolute::No + }; + self.path.push(PathCommand::MoveTo { point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) + { + return Ok(()); + } + skip_comma_wsp(&mut self.chars); + + // If a moveto is followed by multiple pairs of coordinates, the subsequent + // pairs are treated as implicit lineto commands. + self.parse_lineto(absolute) + } + + /// Parse "closepath" command. + fn parse_closepath(&mut self) -> Result<(), ()> { + self.path.push(PathCommand::ClosePath); + Ok(()) + } + + /// Parse "lineto" command. + fn parse_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, LineTo, [ point => parse_coord ]) + } + + /// Parse horizontal "lineto" command. + fn parse_h_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, HorizontalLineTo, [ x => parse_number ]) + } + + /// Parse vertical "lineto" command. + fn parse_v_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, VerticalLineTo, [ y => parse_number ]) + } + + /// Parse cubic Bézier curve command. + fn parse_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, CurveTo, [ + control1 => parse_coord, control2 => parse_coord, point => parse_coord + ]) + } + + /// Parse smooth "curveto" command. + fn parse_smooth_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, SmoothCurveTo, [ + control2 => parse_coord, point => parse_coord + ]) + } + + /// Parse quadratic Bézier curve command. + fn parse_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, QuadBezierCurveTo, [ + control1 => parse_coord, point => parse_coord + ]) + } + + /// Parse smooth quadratic Bézier curveto command. + fn parse_smooth_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, SmoothQuadBezierCurveTo, [ point => parse_coord ]) + } + + /// Parse elliptical arc curve command. + fn parse_elliptical_arc(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + // Parse a flag whose value is '0' or '1'; otherwise, return Err(()). + let parse_flag = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() { + Some(c) if c == b'0' || c == b'1' => Ok(ArcFlag(c == b'1')), + _ => Err(()), + }; + parse_arguments!(self, absolute, EllipticalArc, [ + rx => parse_number, + ry => parse_number, + angle => parse_number, + large_arc_flag => parse_flag, + sweep_flag => parse_flag, + point => parse_coord + ]) + } +} + +/// Parse a pair of numbers into CoordPair. +fn parse_coord(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CoordPair, ()> { + let x = parse_number(iter)?; + skip_comma_wsp(iter); + let y = parse_number(iter)?; + Ok(CoordPair::new(x, y)) +} + +/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed +/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating +/// point number. In other words, the logic here is similar with that of +/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the +/// input is a Peekable and we only accept an integer of a floating point number. +/// +/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF +fn parse_number(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CSSFloat, ()> { + // 1. Check optional sign. + let sign = if iter + .peek() + .map_or(false, |&sign| sign == b'+' || sign == b'-') + { + if iter.next().unwrap() == b'-' { + -1. + } else { + 1. + } + } else { + 1. + }; + + // 2. Check integer part. + let mut integral_part: f64 = 0.; + let got_dot = if !iter.peek().map_or(false, |&n| n == b'.') { + // If the first digit in integer part is neither a dot nor a digit, this is not a number. + if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { + return Err(()); + } + + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + integral_part = integral_part * 10. + (iter.next().unwrap() - b'0') as f64; + } + + iter.peek().map_or(false, |&n| n == b'.') + } else { + true + }; + + // 3. Check fractional part. + let mut fractional_part: f64 = 0.; + if got_dot { + // Consume '.'. + iter.next(); + // If the first digit in fractional part is not a digit, this is not a number. + if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { + return Err(()); + } + + let mut factor = 0.1; + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + fractional_part += (iter.next().unwrap() - b'0') as f64 * factor; + factor *= 0.1; + } + } + + let mut value = sign * (integral_part + fractional_part); + + // 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to + // treat the numbers after 'E' or 'e' are in the exponential part. + if iter.peek().map_or(false, |&exp| exp == b'E' || exp == b'e') { + // Consume 'E' or 'e'. + iter.next(); + let exp_sign = if iter + .peek() + .map_or(false, |&sign| sign == b'+' || sign == b'-') + { + if iter.next().unwrap() == b'-' { + -1. + } else { + 1. + } + } else { + 1. + }; + + let mut exp: f64 = 0.; + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + exp = exp * 10. + (iter.next().unwrap() - b'0') as f64; + } + + value *= f64::powf(10., exp * exp_sign); + } + + if value.is_finite() { + Ok(value.min(f32::MAX as f64).max(f32::MIN as f64) as CSSFloat) + } else { + Err(()) + } +} + +/// Skip all svg whitespaces, and return true if |iter| hasn't finished. +#[inline] +fn skip_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { + // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}. + // However, SVG 2 has one extra whitespace: \u{C}. + // Therefore, we follow the newest spec for the definition of whitespace, + // i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}. + while iter.peek().map_or(false, |c| c.is_ascii_whitespace()) { + iter.next(); + } + iter.peek().is_some() +} + +/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished. +#[inline] +fn skip_comma_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { + if !skip_wsp(iter) { + return false; + } + + if *iter.peek().unwrap() != b',' { + return true; + } + iter.next(); + + skip_wsp(iter) +} diff --git a/servo/components/style/values/specified/table.rs b/servo/components/style/values/specified/table.rs new file mode 100644 index 0000000000..88f917ac78 --- /dev/null +++ b/servo/components/style/values/specified/table.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to tables. + +/// Specified values for the `caption-side` property. +/// +/// Note that despite having "physical" names, these are actually interpreted +/// according to the table's writing-mode: Top and Bottom are treated as +/// block-start and -end respectively. +/// +/// https://drafts.csswg.org/css-tables/#propdef-caption-side +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Ord, + Parse, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum CaptionSide { + Top, + Bottom, +} diff --git a/servo/components/style/values/specified/text.rs b/servo/components/style/values/specified/text.rs new file mode 100644 index 0000000000..0e70bd26ac --- /dev/null +++ b/servo/components/style/values/specified/text.rs @@ -0,0 +1,1193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for text properties. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; +use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle; +use crate::values::computed::text::TextOverflow as ComputedTextOverflow; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::text::InitialLetter as GenericInitialLetter; +use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing}; +use crate::values::specified::length::{Length, LengthPercentage}; +use crate::values::specified::{AllowQuirks, Integer, Number}; +use cssparser::{Parser, Token}; +use icu_segmenter::GraphemeClusterSegmenter; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use style_traits::{KeywordsCollectFn, SpecifiedValueInfo}; + +/// A specified type for the `initial-letter` property. +pub type InitialLetter = GenericInitialLetter<Number, Integer>; + +/// A specified value for the `letter-spacing` property. +pub type LetterSpacing = Spacing<Length>; + +/// A specified value for the `word-spacing` property. +pub type WordSpacing = Spacing<LengthPercentage>; + +/// A value for the `hyphenate-character` property. +#[derive( + Clone, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum HyphenateCharacter { + /// `auto` + Auto, + /// `<string>` + String(crate::OwnedStr), +} + +impl Parse for InitialLetter { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(GenericInitialLetter::Normal); + } + let size = Number::parse_at_least_one(context, input)?; + let sink = input + .try_parse(|i| Integer::parse_positive(context, i)) + .ok(); + Ok(GenericInitialLetter::Specified(size, sink)) + } +} + +impl Parse for LetterSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Spacing::parse_with(context, input, |c, i| { + Length::parse_quirky(c, i, AllowQuirks::Yes) + }) + } +} + +impl Parse for WordSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Spacing::parse_with(context, input, |c, i| { + LengthPercentage::parse_quirky(c, i, AllowQuirks::Yes) + }) + } +} + +/// A generic value for the `text-overflow` property. +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum TextOverflowSide { + /// Clip inline content. + Clip, + /// Render ellipsis to represent clipped inline content. + Ellipsis, + /// Render a given string to represent clipped inline content. + String(crate::OwnedStr), +} + +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// text-overflow. Specifies rendering when inline content overflows its line box edge. +pub struct TextOverflow { + /// First value. Applies to end line box edge if no second is supplied; line-left edge otherwise. + pub first: TextOverflowSide, + /// Second value. Applies to the line-right edge if supplied. + pub second: Option<TextOverflowSide>, +} + +impl Parse for TextOverflow { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TextOverflow, ParseError<'i>> { + let first = TextOverflowSide::parse(context, input)?; + let second = input + .try_parse(|input| TextOverflowSide::parse(context, input)) + .ok(); + Ok(TextOverflow { first, second }) + } +} + +impl ToComputedValue for TextOverflow { + type ComputedValue = ComputedTextOverflow; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + if let Some(ref second) = self.second { + Self::ComputedValue { + first: self.first.clone(), + second: second.clone(), + sides_are_logical: false, + } + } else { + Self::ComputedValue { + first: TextOverflowSide::Clip, + second: self.first.clone(), + sides_are_logical: true, + } + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if computed.sides_are_logical { + assert_eq!(computed.first, TextOverflowSide::Clip); + TextOverflow { + first: computed.second.clone(), + second: None, + } + } else { + TextOverflow { + first: computed.first.clone(), + second: Some(computed.second.clone()), + } + } + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + Serialize, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags(single = "none", mixed = "underline,overline,line-through,blink"))] +#[repr(C)] +/// Specified keyword values for the text-decoration-line property. +pub struct TextDecorationLine(u8); +bitflags! { + impl TextDecorationLine: u8 { + /// No text decoration line is specified. + const NONE = 0; + /// underline + const UNDERLINE = 1 << 0; + /// overline + const OVERLINE = 1 << 1; + /// line-through + const LINE_THROUGH = 1 << 2; + /// blink + const BLINK = 1 << 3; + /// Only set by presentation attributes + /// + /// Setting this will mean that text-decorations use the color + /// specified by `color` in quirks mode. + /// + /// For example, this gives <a href=foo><font color="red">text</font></a> + /// a red text decoration + #[cfg(feature = "gecko")] + const COLOR_OVERRIDE = 0x10; + } +} + +impl Default for TextDecorationLine { + fn default() -> Self { + TextDecorationLine::NONE + } +} + +impl TextDecorationLine { + #[inline] + /// Returns the initial value of text-decoration-line + pub fn none() -> Self { + TextDecorationLine::NONE + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Specified value of the text-transform property, stored in two parts: +/// the case-related transforms (mutually exclusive, only one may be in effect), and others (non-exclusive). +pub struct TextTransform { + /// Case transform, if any. + pub case_: TextTransformCase, + /// Non-case transforms. + pub other_: TextTransformOther, +} + +impl TextTransform { + #[inline] + /// Returns the initial value of text-transform + pub fn none() -> Self { + TextTransform { + case_: TextTransformCase::None, + other_: TextTransformOther::empty(), + } + } + #[inline] + /// Returns whether the value is 'none' + pub fn is_none(&self) -> bool { + self.case_ == TextTransformCase::None && self.other_.is_empty() + } +} + +// TODO: This can be simplified by deriving it. +impl Parse for TextTransform { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = TextTransform::none(); + + // Case keywords are mutually exclusive; other transforms may co-occur. + loop { + let location = input.current_source_location(); + let ident = match input.next() { + Ok(&Token::Ident(ref ident)) => ident, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + Err(..) => break, + }; + + match_ignore_ascii_case! { ident, + "none" if result.is_none() => { + return Ok(result); + }, + "uppercase" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Uppercase + }, + "lowercase" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Lowercase + }, + "capitalize" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Capitalize + }, + "math-auto" if result.case_ == TextTransformCase::None && + result.other_.is_empty() => { + result.case_ = TextTransformCase::MathAuto; + return Ok(result); + }, + "full-width" if !result.other_.intersects(TextTransformOther::FULL_WIDTH) => { + result.other_.insert(TextTransformOther::FULL_WIDTH) + }, + "full-size-kana" if !result.other_.intersects(TextTransformOther::FULL_SIZE_KANA) => { + result.other_.insert(TextTransformOther::FULL_SIZE_KANA) + }, + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + } + } + + if result.is_none() { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(result) + } + } +} + +impl ToCss for TextTransform { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_none() { + return dest.write_str("none"); + } + + if self.case_ != TextTransformCase::None { + self.case_.to_css(dest)?; + if !self.other_.is_empty() { + dest.write_char(' ')?; + } + } + + self.other_.to_css(dest) + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.) +pub enum TextTransformCase { + /// No case transform. + None, + /// All uppercase. + Uppercase, + /// All lowercase. + Lowercase, + /// Capitalize each word. + Capitalize, + /// Automatic italicization of math variables. + MathAuto, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + Serialize, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags(mixed = "full-width,full-size-kana"))] +#[repr(C)] +/// Specified keyword values for non-case transforms in the text-transform property. (Non-exclusive.) +pub struct TextTransformOther(u8); +bitflags! { + impl TextTransformOther: u8 { + /// full-width + const FULL_WIDTH = 1 << 0; + /// full-size-kana + const FULL_SIZE_KANA = 1 << 1; + } +} + +/// Specified and computed value of text-align-last. +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum TextAlignLast { + Auto, + Start, + End, + Left, + Right, + Center, + Justify, +} + +/// Specified value of text-align keyword value. +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum TextAlignKeyword { + Start, + Left, + Right, + Center, + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + Justify, + #[css(skip)] + #[cfg(feature = "gecko")] + Char, + End, + #[cfg(feature = "gecko")] + MozCenter, + #[cfg(feature = "gecko")] + MozLeft, + #[cfg(feature = "gecko")] + MozRight, + #[cfg(feature = "servo-layout-2013")] + ServoCenter, + #[cfg(feature = "servo-layout-2013")] + ServoLeft, + #[cfg(feature = "servo-layout-2013")] + ServoRight, +} + +/// Specified value of text-align property. +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum TextAlign { + /// Keyword value of text-align property. + Keyword(TextAlignKeyword), + /// `match-parent` value of text-align property. It has a different handling + /// unlike other keywords. + #[cfg(feature = "gecko")] + MatchParent, + /// This is how we implement the following HTML behavior from + /// https://html.spec.whatwg.org/#tables-2: + /// + /// User agents are expected to have a rule in their user agent style sheet + /// that matches th elements that have a parent node whose computed value + /// for the 'text-align' property is its initial value, whose declaration + /// block consists of just a single declaration that sets the 'text-align' + /// property to the value 'center'. + /// + /// Since selectors can't depend on the ancestor styles, we implement it with a + /// magic value that computes to the right thing. Since this is an + /// implementation detail, it shouldn't be exposed to web content. + #[cfg(feature = "gecko")] + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozCenterOrInherit, +} + +impl ToComputedValue for TextAlign { + type ComputedValue = TextAlignKeyword; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + match *self { + TextAlign::Keyword(key) => key, + #[cfg(feature = "gecko")] + TextAlign::MatchParent => { + // on the root <html> element we should still respect the dir + // but the parent dir of that element is LTR even if it's <html dir=rtl> + // and will only be RTL if certain prefs have been set. + // In that case, the default behavior here will set it to left, + // but we want to set it to right -- instead set it to the default (`start`), + // which will do the right thing in this case (but not the general case) + if _context.builder.is_root_element { + return TextAlignKeyword::Start; + } + let parent = _context + .builder + .get_parent_inherited_text() + .clone_text_align(); + let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr(); + match (parent, ltr) { + (TextAlignKeyword::Start, true) => TextAlignKeyword::Left, + (TextAlignKeyword::Start, false) => TextAlignKeyword::Right, + (TextAlignKeyword::End, true) => TextAlignKeyword::Right, + (TextAlignKeyword::End, false) => TextAlignKeyword::Left, + _ => parent, + } + }, + #[cfg(feature = "gecko")] + TextAlign::MozCenterOrInherit => { + let parent = _context + .builder + .get_parent_inherited_text() + .clone_text_align(); + if parent == TextAlignKeyword::Start { + TextAlignKeyword::Center + } else { + parent + } + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + TextAlign::Keyword(*computed) + } +} + +fn fill_mode_is_default_and_shape_exists( + fill: &TextEmphasisFillMode, + shape: &Option<TextEmphasisShapeKeyword>, +) -> bool { + shape.is_some() && fill.is_filled() +} + +/// Specified value of text-emphasis-style property. +/// +/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum TextEmphasisStyle { + /// [ <fill> || <shape> ] + Keyword { + #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")] + fill: TextEmphasisFillMode, + shape: Option<TextEmphasisShapeKeyword>, + }, + /// `none` + None, + /// `<string>` (of which only the first grapheme cluster will be used). + String(crate::OwnedStr), +} + +/// Fill mode for the text-emphasis-style property +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TextEmphasisFillMode { + /// `filled` + Filled, + /// `open` + Open, +} + +impl TextEmphasisFillMode { + /// Whether the value is `filled`. + #[inline] + pub fn is_filled(&self) -> bool { + matches!(*self, TextEmphasisFillMode::Filled) + } +} + +/// Shape keyword for the text-emphasis-style property +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TextEmphasisShapeKeyword { + /// `dot` + Dot, + /// `circle` + Circle, + /// `double-circle` + DoubleCircle, + /// `triangle` + Triangle, + /// `sesame` + Sesame, +} + +impl ToComputedValue for TextEmphasisStyle { + type ComputedValue = ComputedTextEmphasisStyle; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + TextEmphasisStyle::Keyword { fill, shape } => { + let shape = shape.unwrap_or_else(|| { + // FIXME(emilio, bug 1572958): This should set the + // rule_cache_conditions properly. + // + // Also should probably use WritingMode::is_vertical rather + // than the computed value of the `writing-mode` property. + if context.style().get_inherited_box().clone_writing_mode() == + SpecifiedWritingMode::HorizontalTb + { + TextEmphasisShapeKeyword::Circle + } else { + TextEmphasisShapeKeyword::Sesame + } + }); + ComputedTextEmphasisStyle::Keyword { fill, shape } + }, + TextEmphasisStyle::None => ComputedTextEmphasisStyle::None, + TextEmphasisStyle::String(ref s) => { + // FIXME(emilio): Doing this at computed value time seems wrong. + // The spec doesn't say that this should be a computed-value + // time operation. This is observable from getComputedStyle(). + // + // Note that the first grapheme cluster boundary should always be the start of the string. + let first_grapheme_end = GraphemeClusterSegmenter::new().segment_str(s).nth(1).unwrap_or(0); + ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword { + fill, + shape: Some(shape), + }, + ComputedTextEmphasisStyle::None => TextEmphasisStyle::None, + ComputedTextEmphasisStyle::String(ref string) => { + TextEmphasisStyle::String(string.clone()) + }, + } + } +} + +impl Parse for TextEmphasisStyle { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(TextEmphasisStyle::None); + } + + if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) { + // Handle <string> + return Ok(TextEmphasisStyle::String(s.into())); + } + + // Handle a pair of keywords + let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); + let fill = input.try_parse(TextEmphasisFillMode::parse).ok(); + if shape.is_none() { + shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); + } + + if shape.is_none() && fill.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // If a shape keyword is specified but neither filled nor open is + // specified, filled is assumed. + let fill = fill.unwrap_or(TextEmphasisFillMode::Filled); + + // We cannot do the same because the default `<shape>` depends on the + // computed writing-mode. + Ok(TextEmphasisStyle::Keyword { fill, shape }) + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + Serialize, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[css(bitflags( + mixed = "over,under,left,right", + validate_mixed = "Self::validate_and_simplify" +))] +/// Values for text-emphasis-position: +/// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property> +pub struct TextEmphasisPosition(u8); +bitflags! { + impl TextEmphasisPosition: u8 { + /// Draws marks to the right of the text in vertical writing mode. + const OVER = 1 << 0; + /// Draw marks under the text in horizontal writing mode. + const UNDER = 1 << 1; + /// Draw marks to the left of the text in vertical writing mode. + const LEFT = 1 << 2; + /// Draws marks to the right of the text in vertical writing mode. + const RIGHT = 1 << 3; + } +} + +impl TextEmphasisPosition { + fn validate_and_simplify(&mut self) -> bool { + if self.intersects(Self::OVER) == self.intersects(Self::UNDER) { + return false; + } + + if self.intersects(Self::LEFT) { + return !self.intersects(Self::RIGHT); + } + + self.remove(Self::RIGHT); // Right is the default + true + } +} + +/// Values for the `word-break` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum WordBreak { + Normal, + BreakAll, + KeepAll, + /// The break-word value, needed for compat. + /// + /// Specifying `word-break: break-word` makes `overflow-wrap` behave as + /// `anywhere`, and `word-break` behave like `normal`. + #[cfg(feature = "gecko")] + BreakWord, +} + +/// Values for the `text-justify` CSS property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum TextJustify { + Auto, + None, + InterWord, + // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute + // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias. + #[parse(aliases = "distribute")] + InterCharacter, +} + +/// Values for the `-moz-control-character-visibility` CSS property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum MozControlCharacterVisibility { + Hidden, + Visible, +} + +impl Default for MozControlCharacterVisibility { + fn default() -> Self { + if static_prefs::pref!("layout.css.control-characters.visible") { + Self::Visible + } else { + Self::Hidden + } + } +} + +/// Values for the `line-break` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum LineBreak { + Auto, + Loose, + Normal, + Strict, + Anywhere, +} + +/// Values for the `overflow-wrap` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum OverflowWrap { + Normal, + BreakWord, + Anywhere, +} + +/// A specified value for the `text-indent` property +/// which takes the grammar of [<length-percentage>] && hanging? && each-line? +/// +/// https://drafts.csswg.org/css-text/#propdef-text-indent +pub type TextIndent = GenericTextIndent<LengthPercentage>; + +impl Parse for TextIndent { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut length = None; + let mut hanging = false; + let mut each_line = false; + + // The length-percentage and the two possible keywords can occur in any order. + while !input.is_exhausted() { + // If we haven't seen a length yet, try to parse one. + if length.is_none() { + if let Ok(len) = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) + { + length = Some(len); + continue; + } + } + + if static_prefs::pref!("layout.css.text-indent-keywords.enabled") { + // Check for the keywords (boolean flags). + try_match_ident_ignore_ascii_case! { input, + "hanging" if !hanging => hanging = true, + "each-line" if !each_line => each_line = true, + } + continue; + } + + // If we reach here, there must be something that we failed to parse; + // just break and let the caller deal with it. + break; + } + + // The length-percentage value is required for the declaration to be valid. + if let Some(length) = length { + Ok(Self { + length, + hanging, + each_line, + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// Implements text-decoration-skip-ink which takes the keywords auto | none | all +/// +/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property +#[repr(u8)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum TextDecorationSkipInk { + Auto, + None, + All, +} + +/// Implements type for `text-decoration-thickness` property +pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>; + +impl TextDecorationLength { + /// `Auto` value. + #[inline] + pub fn auto() -> Self { + GenericTextDecorationLength::Auto + } + + /// Whether this is the `Auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, GenericTextDecorationLength::Auto) + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "auto,from-font,under,left,right")] +#[repr(C)] +/// Specified keyword values for the text-underline-position property. +/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives +/// `auto | [ from-font | under ] || [ left | right ]`.) +/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property +pub struct TextUnderlinePosition(u8); +bitflags! { + impl TextUnderlinePosition: u8 { + /// Use automatic positioning below the alphabetic baseline. + const AUTO = 0; + /// Use underline position from the first available font. + const FROM_FONT = 1 << 0; + /// Below the glyph box. + const UNDER = 1 << 1; + /// In vertical mode, place to the left of the text. + const LEFT = 1 << 2; + /// In vertical mode, place to the right of the text. + const RIGHT = 1 << 3; + } +} + +// TODO: This can be derived with some care. +impl Parse for TextUnderlinePosition { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TextUnderlinePosition, ParseError<'i>> { + let mut result = TextUnderlinePosition::empty(); + + loop { + let location = input.current_source_location(); + let ident = match input.next() { + Ok(&Token::Ident(ref ident)) => ident, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + Err(..) => break, + }; + + match_ignore_ascii_case! { ident, + "auto" if result.is_empty() => { + return Ok(result); + }, + "from-font" if !result.intersects(TextUnderlinePosition::FROM_FONT | + TextUnderlinePosition::UNDER) => { + result.insert(TextUnderlinePosition::FROM_FONT); + }, + "under" if !result.intersects(TextUnderlinePosition::FROM_FONT | + TextUnderlinePosition::UNDER) => { + result.insert(TextUnderlinePosition::UNDER); + }, + "left" if !result.intersects(TextUnderlinePosition::LEFT | + TextUnderlinePosition::RIGHT) => { + result.insert(TextUnderlinePosition::LEFT); + }, + "right" if !result.intersects(TextUnderlinePosition::LEFT | + TextUnderlinePosition::RIGHT) => { + result.insert(TextUnderlinePosition::RIGHT); + }, + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + } + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for TextUnderlinePosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("auto"); + } + + let mut writer = SequenceWriter::new(dest, " "); + let mut any = false; + + macro_rules! maybe_write { + ($ident:ident => $str:expr) => { + if self.contains(TextUnderlinePosition::$ident) { + any = true; + writer.raw_item($str)?; + } + }; + } + + maybe_write!(FROM_FONT => "from-font"); + maybe_write!(UNDER => "under"); + maybe_write!(LEFT => "left"); + maybe_write!(RIGHT => "right"); + + debug_assert!(any); + + Ok(()) + } +} + +/// Values for `ruby-position` property +#[repr(u8)] +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[allow(missing_docs)] +pub enum RubyPosition { + AlternateOver, + AlternateUnder, + Over, + Under, +} + +impl Parse for RubyPosition { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<RubyPosition, ParseError<'i>> { + // Parse alternate before + let alternate = input + .try_parse(|i| i.expect_ident_matching("alternate")) + .is_ok(); + if alternate && input.is_exhausted() { + return Ok(RubyPosition::AlternateOver); + } + // Parse over / under + let over = try_match_ident_ignore_ascii_case! { input, + "over" => true, + "under" => false, + }; + // Parse alternate after + let alternate = alternate || + input + .try_parse(|i| i.expect_ident_matching("alternate")) + .is_ok(); + + Ok(match (over, alternate) { + (true, true) => RubyPosition::AlternateOver, + (false, true) => RubyPosition::AlternateUnder, + (true, false) => RubyPosition::Over, + (false, false) => RubyPosition::Under, + }) + } +} + +impl ToCss for RubyPosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(match self { + RubyPosition::AlternateOver => "alternate", + RubyPosition::AlternateUnder => "alternate under", + RubyPosition::Over => "over", + RubyPosition::Under => "under", + }) + } +} + +impl SpecifiedValueInfo for RubyPosition { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["alternate", "over", "under"]) + } +} diff --git a/servo/components/style/values/specified/time.rs b/servo/components/style/values/specified/time.rs new file mode 100644 index 0000000000..3061ebddcc --- /dev/null +++ b/servo/components/style/values/specified/time.rs @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified time values. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::time::Time as ComputedTime; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::CSSFloat; +use crate::Zero; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +/// A time value according to CSS-VALUES § 6.2. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Time { + seconds: CSSFloat, + unit: TimeUnit, + calc_clamping_mode: Option<AllowedNumericType>, +} + +/// A time unit. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum TimeUnit { + /// `s` + Second, + /// `ms` + Millisecond, +} + +impl Time { + /// Returns a time value that represents `seconds` seconds. + pub fn from_seconds_with_calc_clamping_mode( + seconds: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Time { + seconds, + unit: TimeUnit::Second, + calc_clamping_mode, + } + } + + /// Returns a time value that represents `seconds` seconds. + pub fn from_seconds(seconds: CSSFloat) -> Self { + Self::from_seconds_with_calc_clamping_mode(seconds, None) + } + + /// Returns the time in fractional seconds. + pub fn seconds(self) -> CSSFloat { + self.seconds + } + + /// Returns the unit of the time. + #[inline] + pub fn unit(&self) -> &'static str { + match self.unit { + TimeUnit::Second => "s", + TimeUnit::Millisecond => "ms", + } + } + + #[inline] + fn unitless_value(&self) -> CSSFloat { + match self.unit { + TimeUnit::Second => self.seconds, + TimeUnit::Millisecond => self.seconds * 1000., + } + } + + /// Parses a time according to CSS-VALUES § 6.2. + pub fn parse_dimension(value: CSSFloat, unit: &str) -> Result<Time, ()> { + let (seconds, unit) = match_ignore_ascii_case! { unit, + "s" => (value, TimeUnit::Second), + "ms" => (value / 1000.0, TimeUnit::Millisecond), + _ => return Err(()) + }; + + Ok(Time { + seconds, + unit, + calc_clamping_mode: None, + }) + } + + fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + use style_traits::ParsingMode; + + let location = input.current_source_location(); + match *input.next()? { + // Note that we generally pass ParserContext to is_ok() to check + // that the ParserMode of the ParserContext allows all numeric + // values for SMIL regardless of clamping_mode, but in this Time + // value case, the value does not animate for SMIL at all, so we use + // ParsingMode::DEFAULT directly. + Token::Dimension { + value, ref unit, .. + } if clamping_mode.is_ok(ParsingMode::DEFAULT, value) => { + Time::parse_dimension(value, unit) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse_time(context, input, clamping_mode, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parses a non-negative time value. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } +} + +impl Zero for Time { + #[inline] + fn zero() -> Self { + Self::from_seconds(0.0) + } + + #[inline] + fn is_zero(&self) -> bool { + // The unit doesn't matter, i.e. `s` and `ms` are the same for zero. + self.seconds == 0.0 && self.calc_clamping_mode.is_none() + } +} + +impl ToComputedValue for Time { + type ComputedValue = ComputedTime; + + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + let seconds = self + .calc_clamping_mode + .map_or(self.seconds(), |mode| mode.clamp(self.seconds())); + + ComputedTime::from_seconds(crate::values::normalize(seconds)) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Time { + seconds: computed.seconds(), + unit: TimeUnit::Second, + calc_clamping_mode: None, + } + } +} + +impl Parse for Time { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl ToCss for Time { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.unitless_value(), + self.unit(), + self.calc_clamping_mode.is_some(), + dest, + ) + } +} + +impl SpecifiedValueInfo for Time {} diff --git a/servo/components/style/values/specified/transform.rs b/servo/components/style/values/specified/transform.rs new file mode 100644 index 0000000000..ec5e286bc2 --- /dev/null +++ b/servo/components/style/values/specified/transform.rs @@ -0,0 +1,530 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values that are related to transformations. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Context, LengthPercentage as ComputedLengthPercentage}; +use crate::values::computed::{Percentage as ComputedPercentage, ToComputedValue}; +use crate::values::generics::transform as generic; +use crate::values::generics::transform::{Matrix, Matrix3D}; +use crate::values::specified::position::{ + HorizontalPositionKeyword, Side, VerticalPositionKeyword, +}; +use crate::values::specified::{ + self, Angle, Integer, Length, LengthPercentage, Number, NumberOrPercentage, +}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +pub use crate::values::generics::transform::TransformStyle; + +/// A single operation in a specified CSS `transform` +pub type TransformOperation = + generic::TransformOperation<Angle, Number, Length, Integer, LengthPercentage>; + +/// A specified CSS `transform` +pub type Transform = generic::Transform<TransformOperation>; + +/// The specified value of a CSS `<transform-origin>` +pub type TransformOrigin = generic::TransformOrigin< + OriginComponent<HorizontalPositionKeyword>, + OriginComponent<VerticalPositionKeyword>, + Length, +>; + +#[cfg(feature = "gecko")] +fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool { + static_prefs::pref!("layout.css.transform-box-content-stroke.enabled") +} + +#[cfg(feature = "servo")] +fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool { + false +} + +/// The specified value of `transform-box`. +/// https://drafts.csswg.org/css-transforms-1/#transform-box +// Note: Once we ship everything, we can drop this and just use single_keyword for tranform-box. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TransformBox { + #[parse(condition = "all_transform_boxes_are_enabled")] + ContentBox, + BorderBox, + FillBox, + #[parse(condition = "all_transform_boxes_are_enabled")] + StrokeBox, + ViewBox, +} + +impl TransformOrigin { + /// Returns the initial specified value for `transform-origin`. + #[inline] + pub fn initial_value() -> Self { + Self::new( + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))), + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))), + Length::zero(), + ) + } + + /// Returns the `0 0` value. + pub fn zero_zero() -> Self { + Self::new( + OriginComponent::Length(LengthPercentage::zero()), + OriginComponent::Length(LengthPercentage::zero()), + Length::zero(), + ) + } +} + +impl Transform { + /// Internal parse function for deciding if we wish to accept prefixed values or not + /// + /// `transform` allows unitless zero angles as an exception, see: + /// https://github.com/w3c/csswg-drafts/issues/1162 + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use style_traits::{Separator, Space}; + + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(generic::Transform::none()); + } + + Ok(generic::Transform( + Space::parse(input, |input| { + let function = input.expect_function()?.clone(); + input.parse_nested_block(|input| { + let location = input.current_source_location(); + let result = match_ignore_ascii_case! { &function, + "matrix" => { + let a = Number::parse(context, input)?; + input.expect_comma()?; + let b = Number::parse(context, input)?; + input.expect_comma()?; + let c = Number::parse(context, input)?; + input.expect_comma()?; + let d = Number::parse(context, input)?; + input.expect_comma()?; + // Standard matrix parsing. + let e = Number::parse(context, input)?; + input.expect_comma()?; + let f = Number::parse(context, input)?; + Ok(generic::TransformOperation::Matrix(Matrix { a, b, c, d, e, f })) + }, + "matrix3d" => { + let m11 = Number::parse(context, input)?; + input.expect_comma()?; + let m12 = Number::parse(context, input)?; + input.expect_comma()?; + let m13 = Number::parse(context, input)?; + input.expect_comma()?; + let m14 = Number::parse(context, input)?; + input.expect_comma()?; + let m21 = Number::parse(context, input)?; + input.expect_comma()?; + let m22 = Number::parse(context, input)?; + input.expect_comma()?; + let m23 = Number::parse(context, input)?; + input.expect_comma()?; + let m24 = Number::parse(context, input)?; + input.expect_comma()?; + let m31 = Number::parse(context, input)?; + input.expect_comma()?; + let m32 = Number::parse(context, input)?; + input.expect_comma()?; + let m33 = Number::parse(context, input)?; + input.expect_comma()?; + let m34 = Number::parse(context, input)?; + input.expect_comma()?; + // Standard matrix3d parsing. + let m41 = Number::parse(context, input)?; + input.expect_comma()?; + let m42 = Number::parse(context, input)?; + input.expect_comma()?; + let m43 = Number::parse(context, input)?; + input.expect_comma()?; + let m44 = Number::parse(context, input)?; + Ok(generic::TransformOperation::Matrix3D(Matrix3D { + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44, + })) + }, + "translate" => { + let sx = specified::LengthPercentage::parse(context, input)?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + let sy = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::Translate(sx, sy)) + } else { + Ok(generic::TransformOperation::Translate(sx, Zero::zero())) + } + }, + "translatex" => { + let tx = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::TranslateX(tx)) + }, + "translatey" => { + let ty = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::TranslateY(ty)) + }, + "translatez" => { + let tz = specified::Length::parse(context, input)?; + Ok(generic::TransformOperation::TranslateZ(tz)) + }, + "translate3d" => { + let tx = specified::LengthPercentage::parse(context, input)?; + input.expect_comma()?; + let ty = specified::LengthPercentage::parse(context, input)?; + input.expect_comma()?; + let tz = specified::Length::parse(context, input)?; + Ok(generic::TransformOperation::Translate3D(tx, ty, tz)) + }, + "scale" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + if input.try_parse(|input| input.expect_comma()).is_ok() { + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::Scale(sx, sy)) + } else { + Ok(generic::TransformOperation::Scale(sx, sx)) + } + }, + "scalex" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleX(sx)) + }, + "scaley" => { + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleY(sy)) + }, + "scalez" => { + let sz = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleZ(sz)) + }, + "scale3d" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + input.expect_comma()?; + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + input.expect_comma()?; + let sz = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::Scale3D(sx, sy, sz)) + }, + "rotate" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::Rotate(theta)) + }, + "rotatex" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateX(theta)) + }, + "rotatey" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateY(theta)) + }, + "rotatez" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateZ(theta)) + }, + "rotate3d" => { + let ax = Number::parse(context, input)?; + input.expect_comma()?; + let ay = Number::parse(context, input)?; + input.expect_comma()?; + let az = Number::parse(context, input)?; + input.expect_comma()?; + let theta = specified::Angle::parse_with_unitless(context, input)?; + // TODO(gw): Check that the axis can be normalized. + Ok(generic::TransformOperation::Rotate3D(ax, ay, az, theta)) + }, + "skew" => { + let ax = specified::Angle::parse_with_unitless(context, input)?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + let ay = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::Skew(ax, ay)) + } else { + Ok(generic::TransformOperation::Skew(ax, Zero::zero())) + } + }, + "skewx" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::SkewX(theta)) + }, + "skewy" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::SkewY(theta)) + }, + "perspective" => { + let p = match input.try_parse(|input| specified::Length::parse_non_negative(context, input)) { + Ok(p) => generic::PerspectiveFunction::Length(p), + Err(..) => { + input.expect_ident_matching("none")?; + generic::PerspectiveFunction::None + } + }; + Ok(generic::TransformOperation::Perspective(p)) + }, + _ => Err(()), + }; + result.map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnexpectedFunction( + function.clone(), + )) + }) + }) + })? + .into(), + )) + } +} + +impl Parse for Transform { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Transform::parse_internal(context, input) + } +} + +/// The specified value of a component of a CSS `<transform-origin>`. +#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum OriginComponent<S> { + /// `center` + Center, + /// `<length-percentage>` + Length(LengthPercentage), + /// `<side>` + Side(S), +} + +impl Parse for TransformOrigin { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let parse_depth = |input: &mut Parser| { + input + .try_parse(|i| Length::parse(context, i)) + .unwrap_or(Length::zero()) + }; + match input.try_parse(|i| OriginComponent::parse(context, i)) { + Ok(x_origin @ OriginComponent::Center) => { + if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) { + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let y_origin = OriginComponent::Center; + if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) { + let x_origin = OriginComponent::Side(x_keyword); + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let depth = Length::from_px(0.); + return Ok(Self::new(x_origin, y_origin, depth)); + }, + Ok(x_origin) => { + if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) { + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let y_origin = OriginComponent::Center; + let depth = Length::from_px(0.); + return Ok(Self::new(x_origin, y_origin, depth)); + }, + Err(_) => {}, + } + let y_keyword = VerticalPositionKeyword::parse(input)?; + let y_origin = OriginComponent::Side(y_keyword); + if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) { + let x_origin = OriginComponent::Side(x_keyword); + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + let x_origin = OriginComponent::Center; + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let x_origin = OriginComponent::Center; + let depth = Length::from_px(0.); + Ok(Self::new(x_origin, y_origin, depth)) + } +} + +impl<S> ToComputedValue for OriginComponent<S> +where + S: Side, +{ + type ComputedValue = ComputedLengthPercentage; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + OriginComponent::Center => { + ComputedLengthPercentage::new_percent(ComputedPercentage(0.5)) + }, + OriginComponent::Length(ref length) => length.to_computed_value(context), + OriginComponent::Side(ref keyword) => { + let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. }); + ComputedLengthPercentage::new_percent(p) + }, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + OriginComponent::Length(ToComputedValue::from_computed_value(computed)) + } +} + +impl<S> OriginComponent<S> { + /// `0%` + pub fn zero() -> Self { + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage::zero())) + } +} + +/// A specified CSS `rotate` +pub type Rotate = generic::Rotate<Number, Angle>; + +impl Parse for Rotate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Rotate::None); + } + + // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>. + // + // The rotate axis and angle could be in any order, so we parse angle twice to cover + // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}` + let angle = input + .try_parse(|i| specified::Angle::parse(context, i)) + .ok(); + let axis = input + .try_parse(|i| { + Ok(try_match_ident_ignore_ascii_case! { i, + "x" => (Number::new(1.), Number::new(0.), Number::new(0.)), + "y" => (Number::new(0.), Number::new(1.), Number::new(0.)), + "z" => (Number::new(0.), Number::new(0.), Number::new(1.)), + }) + }) + .or_else(|_: ParseError| -> Result<_, ParseError> { + input.try_parse(|i| { + Ok(( + Number::parse(context, i)?, + Number::parse(context, i)?, + Number::parse(context, i)?, + )) + }) + }) + .ok(); + let angle = match angle { + Some(a) => a, + None => specified::Angle::parse(context, input)?, + }; + + Ok(match axis { + Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle), + None => generic::Rotate::Rotate(angle), + }) + } +} + +/// A specified CSS `translate` +pub type Translate = generic::Translate<LengthPercentage, Length>; + +impl Parse for Translate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Translate::None); + } + + let tx = specified::LengthPercentage::parse(context, input)?; + if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) { + if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) { + // 'translate: <length-percentage> <length-percentage> <length>' + return Ok(generic::Translate::Translate(tx, ty, tz)); + } + + // translate: <length-percentage> <length-percentage>' + return Ok(generic::Translate::Translate( + tx, + ty, + specified::Length::zero(), + )); + } + + // 'translate: <length-percentage> ' + Ok(generic::Translate::Translate( + tx, + specified::LengthPercentage::zero(), + specified::Length::zero(), + )) + } +} + +/// A specified CSS `scale` +pub type Scale = generic::Scale<Number>; + +impl Parse for Scale { + /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage, + /// and then convert into an Number if it's a Percentage. + /// https://github.com/w3c/csswg-drafts/pull/4396 + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Scale::None); + } + + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + if let Ok(sy) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) { + let sy = sy.to_number(); + if let Ok(sz) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) { + // 'scale: <number> <number> <number>' + return Ok(generic::Scale::Scale(sx, sy, sz.to_number())); + } + + // 'scale: <number> <number>' + return Ok(generic::Scale::Scale(sx, sy, Number::new(1.0))); + } + + // 'scale: <number>' + Ok(generic::Scale::Scale(sx, sx, Number::new(1.0))) + } +} diff --git a/servo/components/style/values/specified/ui.rs b/servo/components/style/values/specified/ui.rs new file mode 100644 index 0000000000..2237335ec4 --- /dev/null +++ b/servo/components/style/values/specified/ui.rs @@ -0,0 +1,257 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for UI properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::ui as generics; +use crate::values::specified::color::Color; +use crate::values::specified::image::Image; +use crate::values::specified::Number; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{ + CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss, +}; + +/// A specified value for the `cursor` property. +pub type Cursor = generics::GenericCursor<CursorImage>; + +/// A specified value for item of `image cursors`. +pub type CursorImage = generics::GenericCursorImage<Image, Number>; + +impl Parse for Cursor { + /// cursor: [<url> [<number> <number>]?]# [auto | default | ...] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut images = vec![]; + loop { + match input.try_parse(|input| CursorImage::parse(context, input)) { + Ok(image) => images.push(image), + Err(_) => break, + } + input.expect_comma()?; + } + Ok(Self { + images: images.into(), + keyword: CursorKind::parse(input)?, + }) + } +} + +impl Parse for CursorImage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::Zero; + + let image = Image::parse_only_url(context, input)?; + let mut has_hotspot = false; + let mut hotspot_x = Number::zero(); + let mut hotspot_y = Number::zero(); + + if let Ok(x) = input.try_parse(|input| Number::parse(context, input)) { + has_hotspot = true; + hotspot_x = x; + hotspot_y = Number::parse(context, input)?; + } + + Ok(Self { + image, + has_hotspot, + hotspot_x, + hotspot_y, + }) + } +} + +// This trait is manually implemented because we don't support the whole <image> +// syntax for cursors +impl SpecifiedValueInfo for CursorImage { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["url", "image-set"]); + } +} +/// Specified value of `-moz-force-broken-image-icon` +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct BoolInteger(pub bool); + +impl BoolInteger { + /// Returns 0 + #[inline] + pub fn zero() -> Self { + Self(false) + } +} + +impl Parse for BoolInteger { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // We intentionally don't support calc values here. + match input.expect_integer()? { + 0 => Ok(Self(false)), + 1 => Ok(Self(true)), + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} + +impl ToCss for BoolInteger { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(if self.0 { "1" } else { "0" }) + } +} + +/// A specified value for `scrollbar-color` property +pub type ScrollbarColor = generics::ScrollbarColor<Color>; + +impl Parse for ScrollbarColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(generics::ScrollbarColor::Auto); + } + Ok(generics::ScrollbarColor::Colors { + thumb: Color::parse(context, input)?, + track: Color::parse(context, input)?, + }) + } +} + +/// The specified value for the `user-select` property. +/// +/// https://drafts.csswg.org/css-ui-4/#propdef-user-select +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum UserSelect { + Auto, + Text, + #[parse(aliases = "-moz-none")] + None, + /// Force selection of all children. + All, +} + +/// The keywords allowed in the Cursor property. +/// +/// https://drafts.csswg.org/css-ui-4/#propdef-cursor +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum CursorKind { + None, + Default, + Pointer, + ContextMenu, + Help, + Progress, + Wait, + Cell, + Crosshair, + Text, + VerticalText, + Alias, + Copy, + Move, + NoDrop, + NotAllowed, + #[parse(aliases = "-moz-grab")] + Grab, + #[parse(aliases = "-moz-grabbing")] + Grabbing, + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, + AllScroll, + #[parse(aliases = "-moz-zoom-in")] + ZoomIn, + #[parse(aliases = "-moz-zoom-out")] + ZoomOut, + Auto, +} + +/// The keywords allowed in the -moz-theme property. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum MozTheme { + /// Choose the default (maybe native) rendering. + Auto, + /// Choose the non-native rendering. + NonNative, +} diff --git a/servo/components/style/values/specified/url.rs b/servo/components/style/values/specified/url.rs new file mode 100644 index 0000000000..17ecbe0d5e --- /dev/null +++ b/servo/components/style/values/specified/url.rs @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common handling for the specified value CSS url() values. + +use crate::values::generics::url::GenericUrlOrNone; + +#[cfg(feature = "gecko")] +pub use crate::gecko::url::{SpecifiedImageUrl, SpecifiedUrl}; +#[cfg(feature = "servo")] +pub use crate::servo::url::{SpecifiedImageUrl, SpecifiedUrl}; + +/// Specified <url> | <none> +pub type UrlOrNone = GenericUrlOrNone<SpecifiedUrl>; diff --git a/servo/components/style_derive/Cargo.toml b/servo/components/style_derive/Cargo.toml new file mode 100644 index 0000000000..35f36b04ef --- /dev/null +++ b/servo/components/style_derive/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "style_derive" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +[lib] +path = "lib.rs" +proc-macro = true + +[dependencies] +darling = { version = "0.20", default-features = false } +derive_common = { path = "../derive_common" } +proc-macro2 = "1" +quote = "1" +syn = { version = "2", default-features = false, features = ["clone-impls", "derive", "parsing"] } +synstructure = "0.13" diff --git a/servo/components/style_derive/animate.rs b/servo/components/style_derive/animate.rs new file mode 100644 index 0000000000..9549100ad0 --- /dev/null +++ b/servo/components/style_derive/animate.rs @@ -0,0 +1,135 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use darling::util::PathList; +use derive_common::cg; +use proc_macro2::TokenStream; +use quote::TokenStreamExt; +use syn::{DeriveInput, WhereClause}; +use synstructure::{Structure, VariantInfo}; + +pub fn derive(mut input: DeriveInput) -> TokenStream { + let animation_input_attrs = cg::parse_input_attrs::<AnimationInputAttrs>(&input); + + let no_bound = animation_input_attrs.no_bound.unwrap_or_default(); + let mut where_clause = input.generics.where_clause.take(); + for param in input.generics.type_params() { + if !no_bound.iter().any(|name| name.is_ident(¶m.ident)) { + cg::add_predicate( + &mut where_clause, + parse_quote!(#param: crate::values::animated::Animate), + ); + } + } + let (mut match_body, needs_catchall_branch) = { + let s = Structure::new(&input); + let needs_catchall_branch = s.variants().len() > 1; + let match_body = s.variants().iter().fold(quote!(), |body, variant| { + let arm = derive_variant_arm(variant, &mut where_clause); + quote! { #body #arm } + }); + (match_body, needs_catchall_branch) + }; + + input.generics.where_clause = where_clause; + + if needs_catchall_branch { + // This ideally shouldn't be needed, but see + // https://github.com/rust-lang/rust/issues/68867 + match_body.append_all(quote! { _ => unsafe { debug_unreachable!() } }); + } + + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + quote! { + impl #impl_generics crate::values::animated::Animate for #name #ty_generics #where_clause { + #[allow(unused_variables, unused_imports)] + #[inline] + fn animate( + &self, + other: &Self, + procedure: crate::values::animated::Procedure, + ) -> Result<Self, ()> { + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + match (self, other) { + #match_body + } + } + } + } +} + +fn derive_variant_arm( + variant: &VariantInfo, + where_clause: &mut Option<WhereClause>, +) -> TokenStream { + let variant_attrs = cg::parse_variant_attrs_from_ast::<AnimationVariantAttrs>(&variant.ast()); + let (this_pattern, this_info) = cg::ref_pattern(&variant, "this"); + let (other_pattern, other_info) = cg::ref_pattern(&variant, "other"); + + if variant_attrs.error { + return quote! { + (&#this_pattern, &#other_pattern) => Err(()), + }; + } + + let (result_value, result_info) = cg::value(&variant, "result"); + let mut computations = quote!(); + let iter = result_info.iter().zip(this_info.iter().zip(&other_info)); + computations.append_all(iter.map(|(result, (this, other))| { + let field_attrs = cg::parse_field_attrs::<AnimationFieldAttrs>(&result.ast()); + if field_attrs.field_bound { + let ty = &this.ast().ty; + cg::add_predicate( + where_clause, + parse_quote!(#ty: crate::values::animated::Animate), + ); + } + if field_attrs.constant { + quote! { + if #this != #other { + return Err(()); + } + let #result = std::clone::Clone::clone(#this); + } + } else { + quote! { + let #result = + crate::values::animated::Animate::animate(#this, #other, procedure)?; + } + } + })); + + quote! { + (&#this_pattern, &#other_pattern) => { + #computations + Ok(#result_value) + } + } +} + +#[derive(Default, FromDeriveInput)] +#[darling(attributes(animation), default)] +pub struct AnimationInputAttrs { + pub no_bound: Option<PathList>, +} + +#[derive(Default, FromVariant)] +#[darling(attributes(animation), default)] +pub struct AnimationVariantAttrs { + pub error: bool, + // Only here because of structs, where the struct definition acts as a + // variant itself. + pub no_bound: Option<PathList>, +} + +#[derive(Default, FromField)] +#[darling(attributes(animation), default)] +pub struct AnimationFieldAttrs { + pub constant: bool, + pub field_bound: bool, +} diff --git a/servo/components/style_derive/compute_squared_distance.rs b/servo/components/style_derive/compute_squared_distance.rs new file mode 100644 index 0000000000..022ab115ee --- /dev/null +++ b/servo/components/style_derive/compute_squared_distance.rs @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::animate::{AnimationFieldAttrs, AnimationInputAttrs, AnimationVariantAttrs}; +use derive_common::cg; +use proc_macro2::TokenStream; +use quote::TokenStreamExt; +use syn::{DeriveInput, WhereClause}; +use synstructure; + +pub fn derive(mut input: DeriveInput) -> TokenStream { + let animation_input_attrs = cg::parse_input_attrs::<AnimationInputAttrs>(&input); + let no_bound = animation_input_attrs.no_bound.unwrap_or_default(); + let mut where_clause = input.generics.where_clause.take(); + for param in input.generics.type_params() { + if !no_bound.iter().any(|name| name.is_ident(¶m.ident)) { + cg::add_predicate( + &mut where_clause, + parse_quote!(#param: crate::values::distance::ComputeSquaredDistance), + ); + } + } + + let (mut match_body, needs_catchall_branch) = { + let s = synstructure::Structure::new(&input); + let needs_catchall_branch = s.variants().len() > 1; + + let match_body = s.variants().iter().fold(quote!(), |body, variant| { + let arm = derive_variant_arm(variant, &mut where_clause); + quote! { #body #arm } + }); + + (match_body, needs_catchall_branch) + }; + + input.generics.where_clause = where_clause; + + if needs_catchall_branch { + // This ideally shouldn't be needed, but see: + // https://github.com/rust-lang/rust/issues/68867 + match_body.append_all(quote! { _ => unsafe { debug_unreachable!() } }); + } + + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + quote! { + impl #impl_generics crate::values::distance::ComputeSquaredDistance for #name #ty_generics #where_clause { + #[allow(unused_variables, unused_imports)] + #[inline] + fn compute_squared_distance( + &self, + other: &Self, + ) -> Result<crate::values::distance::SquaredDistance, ()> { + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + match (self, other) { + #match_body + } + } + } + } +} + +fn derive_variant_arm( + variant: &synstructure::VariantInfo, + mut where_clause: &mut Option<WhereClause>, +) -> TokenStream { + let variant_attrs = cg::parse_variant_attrs_from_ast::<AnimationVariantAttrs>(&variant.ast()); + let (this_pattern, this_info) = cg::ref_pattern(&variant, "this"); + let (other_pattern, other_info) = cg::ref_pattern(&variant, "other"); + + if variant_attrs.error { + return quote! { + (&#this_pattern, &#other_pattern) => Err(()), + }; + } + + let sum = if this_info.is_empty() { + quote! { crate::values::distance::SquaredDistance::from_sqrt(0.) } + } else { + let mut sum = quote!(); + sum.append_separated(this_info.iter().zip(&other_info).map(|(this, other)| { + let field_attrs = cg::parse_field_attrs::<DistanceFieldAttrs>(&this.ast()); + if field_attrs.field_bound { + let ty = &this.ast().ty; + cg::add_predicate( + &mut where_clause, + parse_quote!(#ty: crate::values::distance::ComputeSquaredDistance), + ); + } + + let animation_field_attrs = + cg::parse_field_attrs::<AnimationFieldAttrs>(&this.ast()); + + if animation_field_attrs.constant { + quote! { + { + if #this != #other { + return Err(()); + } + crate::values::distance::SquaredDistance::from_sqrt(0.) + } + } + } else { + quote! { + crate::values::distance::ComputeSquaredDistance::compute_squared_distance(#this, #other)? + } + } + }), quote!(+)); + sum + }; + + return quote! { + (&#this_pattern, &#other_pattern) => Ok(#sum), + }; +} + +#[derive(Default, FromField)] +#[darling(attributes(distance), default)] +struct DistanceFieldAttrs { + field_bound: bool, +} diff --git a/servo/components/style_derive/lib.rs b/servo/components/style_derive/lib.rs new file mode 100644 index 0000000000..079db00c5a --- /dev/null +++ b/servo/components/style_derive/lib.rs @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![recursion_limit = "128"] + +#[macro_use] +extern crate darling; +extern crate derive_common; +extern crate proc_macro; +extern crate proc_macro2; +#[macro_use] +extern crate quote; +#[macro_use] +extern crate syn; +extern crate synstructure; + +use proc_macro::TokenStream; + +mod animate; +mod compute_squared_distance; +mod parse; +mod specified_value_info; +mod to_animated_value; +mod to_animated_zero; +mod to_computed_value; +mod to_css; +mod to_resolved_value; + +#[proc_macro_derive(Animate, attributes(animate, animation))] +pub fn derive_animate(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + animate::derive(input).into() +} + +#[proc_macro_derive(ComputeSquaredDistance, attributes(animation, distance))] +pub fn derive_compute_squared_distance(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + compute_squared_distance::derive(input).into() +} + +#[proc_macro_derive(ToAnimatedValue)] +pub fn derive_to_animated_value(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + to_animated_value::derive(input).into() +} + +#[proc_macro_derive(Parse, attributes(css, parse))] +pub fn derive_parse(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + parse::derive(input).into() +} + +#[proc_macro_derive(ToAnimatedZero, attributes(animation, zero))] +pub fn derive_to_animated_zero(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + to_animated_zero::derive(input).into() +} + +#[proc_macro_derive(ToComputedValue, attributes(compute))] +pub fn derive_to_computed_value(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + to_computed_value::derive(input).into() +} + +#[proc_macro_derive(ToResolvedValue, attributes(resolve))] +pub fn derive_to_resolved_value(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + to_resolved_value::derive(input).into() +} + +#[proc_macro_derive(ToCss, attributes(css))] +pub fn derive_to_css(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + to_css::derive(input).into() +} + +#[proc_macro_derive(SpecifiedValueInfo, attributes(css, parse, value_info))] +pub fn derive_specified_value_info(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + specified_value_info::derive(input).into() +} diff --git a/servo/components/style_derive/parse.rs b/servo/components/style_derive/parse.rs new file mode 100644 index 0000000000..b1a1213435 --- /dev/null +++ b/servo/components/style_derive/parse.rs @@ -0,0 +1,323 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::to_css::{CssBitflagAttrs, CssVariantAttrs}; +use derive_common::cg; +use proc_macro2::{Span, TokenStream}; +use quote::TokenStreamExt; +use syn::{self, DeriveInput, Ident, Path}; +use synstructure::{Structure, VariantInfo}; + +#[derive(Default, FromVariant)] +#[darling(attributes(parse), default)] +pub struct ParseVariantAttrs { + pub aliases: Option<String>, + pub condition: Option<Path>, +} + +#[derive(Default, FromField)] +#[darling(attributes(parse), default)] +pub struct ParseFieldAttrs { + field_bound: bool, +} + +fn parse_bitflags(bitflags: &CssBitflagAttrs) -> TokenStream { + let mut match_arms = TokenStream::new(); + for (rust_name, css_name) in bitflags.single_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + match_arms.append_all(quote! { + #css_name if result.is_empty() => { + single_flag = true; + Self::#rust_ident + }, + }); + } + + for (rust_name, css_name) in bitflags.mixed_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + match_arms.append_all(quote! { + #css_name => Self::#rust_ident, + }); + } + + let mut validate_condition = quote! { !result.is_empty() }; + if let Some(ref function) = bitflags.validate_mixed { + validate_condition.append_all(quote! { + && #function(&mut result) + }); + } + + // NOTE(emilio): this loop has this weird structure because we run this code + // to parse stuff like text-decoration-line in the text-decoration + // shorthand, so we need to be a bit careful that we don't error if we don't + // consume the whole thing because we find an invalid identifier or other + // kind of token. Instead, we should leave it unconsumed. + quote! { + let mut result = Self::empty(); + loop { + let mut single_flag = false; + let flag: Result<_, style_traits::ParseError<'i>> = input.try_parse(|input| { + Ok(try_match_ident_ignore_ascii_case! { input, + #match_arms + }) + }); + + let flag = match flag { + Ok(flag) => flag, + Err(..) => break, + }; + + if single_flag { + return Ok(flag); + } + + if result.intersects(flag) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + result.insert(flag); + } + if #validate_condition { + Ok(result) + } else { + Err(input.new_custom_error(style_traits::StyleParseErrorKind::UnspecifiedError)) + } + } +} + +fn parse_non_keyword_variant( + where_clause: &mut Option<syn::WhereClause>, + name: &syn::Ident, + variant: &VariantInfo, + variant_attrs: &CssVariantAttrs, + parse_attrs: &ParseVariantAttrs, + skip_try: bool, +) -> TokenStream { + let bindings = variant.bindings(); + assert!(parse_attrs.aliases.is_none()); + assert!(variant_attrs.function.is_none()); + assert!(variant_attrs.keyword.is_none()); + assert_eq!( + bindings.len(), + 1, + "We only support deriving parse for simple variants" + ); + let variant_name = &variant.ast().ident; + let binding_ast = &bindings[0].ast(); + let ty = &binding_ast.ty; + + if let Some(ref bitflags) = variant_attrs.bitflags { + assert!(skip_try, "Should be the only variant"); + assert!( + parse_attrs.condition.is_none(), + "Should be the only variant" + ); + assert!(where_clause.is_none(), "Generic bitflags?"); + return parse_bitflags(bitflags); + } + + let field_attrs = cg::parse_field_attrs::<ParseFieldAttrs>(binding_ast); + if field_attrs.field_bound { + cg::add_predicate(where_clause, parse_quote!(#ty: crate::parser::Parse)); + } + + let mut parse = if skip_try { + quote! { + let v = <#ty as crate::parser::Parse>::parse(context, input)?; + return Ok(#name::#variant_name(v)); + } + } else { + quote! { + if let Ok(v) = input.try(|i| <#ty as crate::parser::Parse>::parse(context, i)) { + return Ok(#name::#variant_name(v)); + } + } + }; + + if let Some(ref condition) = parse_attrs.condition { + parse = quote! { + if #condition(context) { + #parse + } + }; + + if skip_try { + // We're the last variant and we can fail to parse due to the + // condition clause. If that happens, we need to return an error. + parse = quote! { + #parse + Err(input.new_custom_error(style_traits::StyleParseErrorKind::UnspecifiedError)) + }; + } + } + + parse +} + +pub fn derive(mut input: DeriveInput) -> TokenStream { + let mut where_clause = input.generics.where_clause.take(); + for param in input.generics.type_params() { + cg::add_predicate( + &mut where_clause, + parse_quote!(#param: crate::parser::Parse), + ); + } + + let name = &input.ident; + let s = Structure::new(&input); + + let mut saw_condition = false; + let mut match_keywords = quote! {}; + let mut non_keywords = vec![]; + + let mut effective_variants = 0; + for variant in s.variants().iter() { + let css_variant_attrs = cg::parse_variant_attrs_from_ast::<CssVariantAttrs>(&variant.ast()); + if css_variant_attrs.skip { + continue; + } + effective_variants += 1; + + let parse_attrs = cg::parse_variant_attrs_from_ast::<ParseVariantAttrs>(&variant.ast()); + + saw_condition |= parse_attrs.condition.is_some(); + + if !variant.bindings().is_empty() { + non_keywords.push((variant, css_variant_attrs, parse_attrs)); + continue; + } + + let identifier = cg::to_css_identifier( + &css_variant_attrs + .keyword + .unwrap_or_else(|| variant.ast().ident.to_string()), + ); + let ident = &variant.ast().ident; + + let condition = match parse_attrs.condition { + Some(ref p) => quote! { if #p(context) }, + None => quote! {}, + }; + + match_keywords.extend(quote! { + #identifier #condition => Ok(#name::#ident), + }); + + let aliases = match parse_attrs.aliases { + Some(aliases) => aliases, + None => continue, + }; + + for alias in aliases.split(',') { + match_keywords.extend(quote! { + #alias #condition => Ok(#name::#ident), + }); + } + } + + let needs_context = saw_condition || !non_keywords.is_empty(); + + let context_ident = if needs_context { + quote! { context } + } else { + quote! { _ } + }; + + let has_keywords = non_keywords.len() != effective_variants; + + let mut parse_non_keywords = quote! {}; + for (i, (variant, css_attrs, parse_attrs)) in non_keywords.iter().enumerate() { + let skip_try = !has_keywords && i == non_keywords.len() - 1; + let parse_variant = parse_non_keyword_variant( + &mut where_clause, + name, + variant, + css_attrs, + parse_attrs, + skip_try, + ); + parse_non_keywords.extend(parse_variant); + } + + let parse_body = if needs_context { + let parse_keywords = if has_keywords { + quote! { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + match_ignore_ascii_case! { &ident, + #match_keywords + _ => Err(location.new_unexpected_token_error( + cssparser::Token::Ident(ident.clone()) + )) + } + } + } else { + quote! {} + }; + + quote! { + #parse_non_keywords + #parse_keywords + } + } else { + quote! { Self::parse(input) } + }; + + let has_non_keywords = !non_keywords.is_empty(); + + input.generics.where_clause = where_clause; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let parse_trait_impl = quote! { + impl #impl_generics crate::parser::Parse for #name #ty_generics #where_clause { + #[inline] + fn parse<'i, 't>( + #context_ident: &crate::parser::ParserContext, + input: &mut cssparser::Parser<'i, 't>, + ) -> Result<Self, style_traits::ParseError<'i>> { + #parse_body + } + } + }; + + if needs_context { + return parse_trait_impl; + } + + assert!(!has_non_keywords); + + // TODO(emilio): It'd be nice to get rid of these, but that makes the + // conversion harder... + let methods_impl = quote! { + impl #name { + /// Parse this keyword. + #[inline] + pub fn parse<'i, 't>( + input: &mut cssparser::Parser<'i, 't>, + ) -> Result<Self, style_traits::ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + Self::from_ident(ident.as_ref()).map_err(|()| { + location.new_unexpected_token_error( + cssparser::Token::Ident(ident.clone()) + ) + }) + } + + /// Parse this keyword from a string slice. + #[inline] + pub fn from_ident(ident: &str) -> Result<Self, ()> { + match_ignore_ascii_case! { ident, + #match_keywords + _ => Err(()), + } + } + } + }; + + quote! { + #parse_trait_impl + #methods_impl + } +} diff --git a/servo/components/style_derive/specified_value_info.rs b/servo/components/style_derive/specified_value_info.rs new file mode 100644 index 0000000000..9a07ab49a6 --- /dev/null +++ b/servo/components/style_derive/specified_value_info.rs @@ -0,0 +1,195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::parse::ParseVariantAttrs; +use crate::to_css::{CssFieldAttrs, CssInputAttrs, CssVariantAttrs}; +use derive_common::cg; +use proc_macro2::TokenStream; +use quote::TokenStreamExt; +use syn::{Data, DeriveInput, Fields, Ident, Type}; + +pub fn derive(mut input: DeriveInput) -> TokenStream { + let css_attrs = cg::parse_input_attrs::<CssInputAttrs>(&input); + let mut types = vec![]; + let mut values = vec![]; + + let input_ident = &input.ident; + let input_name = || cg::to_css_identifier(&input_ident.to_string()); + if let Some(function) = css_attrs.function { + values.push(function.explicit().unwrap_or_else(input_name)); + // If the whole value is wrapped in a function, value types of + // its fields should not be propagated. + } else { + let mut where_clause = input.generics.where_clause.take(); + for param in input.generics.type_params() { + cg::add_predicate( + &mut where_clause, + parse_quote!(#param: style_traits::SpecifiedValueInfo), + ); + } + input.generics.where_clause = where_clause; + + match input.data { + Data::Enum(ref e) => { + for v in e.variants.iter() { + let css_attrs = cg::parse_variant_attrs::<CssVariantAttrs>(&v); + let info_attrs = cg::parse_variant_attrs::<ValueInfoVariantAttrs>(&v); + let parse_attrs = cg::parse_variant_attrs::<ParseVariantAttrs>(&v); + if css_attrs.skip { + continue; + } + if let Some(aliases) = parse_attrs.aliases { + for alias in aliases.split(',') { + values.push(alias.to_string()); + } + } + if let Some(other_values) = info_attrs.other_values { + for value in other_values.split(',') { + values.push(value.to_string()); + } + } + let ident = &v.ident; + let variant_name = || cg::to_css_identifier(&ident.to_string()); + if info_attrs.starts_with_keyword { + values.push(variant_name()); + continue; + } + if let Some(keyword) = css_attrs.keyword { + values.push(keyword); + continue; + } + if let Some(function) = css_attrs.function { + values.push(function.explicit().unwrap_or_else(variant_name)); + } else if !derive_struct_fields(&v.fields, &mut types, &mut values) { + values.push(variant_name()); + } + } + }, + Data::Struct(ref s) => { + if let Some(ref bitflags) = css_attrs.bitflags { + for (_rust_name, css_name) in bitflags.single_flags() { + values.push(css_name) + } + for (_rust_name, css_name) in bitflags.mixed_flags() { + values.push(css_name) + } + } else if !derive_struct_fields(&s.fields, &mut types, &mut values) { + values.push(input_name()); + } + }, + Data::Union(_) => unreachable!("union is not supported"), + } + } + + let info_attrs = cg::parse_input_attrs::<ValueInfoInputAttrs>(&input); + if let Some(other_values) = info_attrs.other_values { + for value in other_values.split(',') { + values.push(value.to_string()); + } + } + + let mut types_value = quote!(0); + types_value.append_all(types.iter().map(|ty| { + quote! { + | <#ty as style_traits::SpecifiedValueInfo>::SUPPORTED_TYPES + } + })); + + let mut nested_collects = quote!(); + nested_collects.append_all(types.iter().map(|ty| { + quote! { + <#ty as style_traits::SpecifiedValueInfo>::collect_completion_keywords(_f); + } + })); + + if let Some(ty) = info_attrs.ty { + types_value.append_all(quote! { + | style_traits::CssType::#ty + }); + } + + let append_values = if values.is_empty() { + quote!() + } else { + let mut value_list = quote!(); + value_list.append_separated(values.iter(), quote! { , }); + quote! { _f(&[#value_list]); } + }; + + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + quote! { + impl #impl_generics style_traits::SpecifiedValueInfo for #name #ty_generics + #where_clause + { + const SUPPORTED_TYPES: u8 = #types_value; + + fn collect_completion_keywords(_f: &mut FnMut(&[&'static str])) { + #nested_collects + #append_values + } + } + } +} + +/// Derive from the given fields. Return false if the fields is a Unit, +/// true otherwise. +fn derive_struct_fields<'a>( + fields: &'a Fields, + types: &mut Vec<&'a Type>, + values: &mut Vec<String>, +) -> bool { + let fields = match *fields { + Fields::Unit => return false, + Fields::Named(ref fields) => fields.named.iter(), + Fields::Unnamed(ref fields) => fields.unnamed.iter(), + }; + types.extend(fields.filter_map(|field| { + let info_attrs = cg::parse_field_attrs::<ValueInfoFieldAttrs>(field); + if let Some(other_values) = info_attrs.other_values { + for value in other_values.split(',') { + values.push(value.to_string()); + } + } + let css_attrs = cg::parse_field_attrs::<CssFieldAttrs>(field); + if css_attrs.represents_keyword { + let ident = field + .ident + .as_ref() + .expect("only named field should use represents_keyword"); + values.push(cg::to_css_identifier(&ident.to_string()).replace("_", "-")); + return None; + } + if let Some(if_empty) = css_attrs.if_empty { + values.push(if_empty); + } + if !css_attrs.skip { + Some(&field.ty) + } else { + None + } + })); + true +} + +#[derive(Default, FromDeriveInput)] +#[darling(attributes(value_info), default)] +struct ValueInfoInputAttrs { + ty: Option<Ident>, + other_values: Option<String>, +} + +#[derive(Default, FromVariant)] +#[darling(attributes(value_info), default)] +struct ValueInfoVariantAttrs { + starts_with_keyword: bool, + other_values: Option<String>, +} + +#[derive(Default, FromField)] +#[darling(attributes(value_info), default)] +struct ValueInfoFieldAttrs { + other_values: Option<String>, +} diff --git a/servo/components/style_derive/to_animated_value.rs b/servo/components/style_derive/to_animated_value.rs new file mode 100644 index 0000000000..45282f0c44 --- /dev/null +++ b/servo/components/style_derive/to_animated_value.rs @@ -0,0 +1,35 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use proc_macro2::TokenStream; +use syn::DeriveInput; +use synstructure::BindStyle; +use to_computed_value; + +pub fn derive(input: DeriveInput) -> TokenStream { + let trait_impl = |from_body, to_body| { + quote! { + #[inline] + fn from_animated_value(from: Self::AnimatedValue) -> Self { + #from_body + } + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + #to_body + } + } + }; + + to_computed_value::derive_to_value( + input, + parse_quote!(crate::values::animated::ToAnimatedValue), + parse_quote!(AnimatedValue), + BindStyle::Move, + |_| Default::default(), + |binding| quote!(crate::values::animated::ToAnimatedValue::from_animated_value(#binding)), + |binding| quote!(crate::values::animated::ToAnimatedValue::to_animated_value(#binding)), + trait_impl, + ) +} diff --git a/servo/components/style_derive/to_animated_zero.rs b/servo/components/style_derive/to_animated_zero.rs new file mode 100644 index 0000000000..008e94cbcf --- /dev/null +++ b/servo/components/style_derive/to_animated_zero.rs @@ -0,0 +1,65 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use crate::animate::{AnimationFieldAttrs, AnimationInputAttrs, AnimationVariantAttrs}; +use derive_common::cg; +use proc_macro2::TokenStream; +use quote::TokenStreamExt; +use syn; +use synstructure; + +pub fn derive(mut input: syn::DeriveInput) -> TokenStream { + let animation_input_attrs = cg::parse_input_attrs::<AnimationInputAttrs>(&input); + let no_bound = animation_input_attrs.no_bound.unwrap_or_default(); + let mut where_clause = input.generics.where_clause.take(); + for param in input.generics.type_params() { + if !no_bound.iter().any(|name| name.is_ident(¶m.ident)) { + cg::add_predicate( + &mut where_clause, + parse_quote!(#param: crate::values::animated::ToAnimatedZero), + ); + } + } + + let to_body = synstructure::Structure::new(&input).each_variant(|variant| { + let attrs = cg::parse_variant_attrs_from_ast::<AnimationVariantAttrs>(&variant.ast()); + if attrs.error { + return Some(quote! { Err(()) }); + } + let (mapped, mapped_bindings) = cg::value(variant, "mapped"); + let bindings_pairs = variant.bindings().iter().zip(mapped_bindings); + let mut computations = quote!(); + computations.append_all(bindings_pairs.map(|(binding, mapped_binding)| { + let field_attrs = cg::parse_field_attrs::<AnimationFieldAttrs>(&binding.ast()); + if field_attrs.constant { + quote! { + let #mapped_binding = std::clone::Clone::clone(#binding); + } + } else { + quote! { + let #mapped_binding = + crate::values::animated::ToAnimatedZero::to_animated_zero(#binding)?; + } + } + })); + computations.append_all(quote! { Ok(#mapped) }); + Some(computations) + }); + input.generics.where_clause = where_clause; + + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + quote! { + impl #impl_generics crate::values::animated::ToAnimatedZero for #name #ty_generics #where_clause { + #[allow(unused_variables)] + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + match *self { + #to_body + } + } + } + } +} diff --git a/servo/components/style_derive/to_computed_value.rs b/servo/components/style_derive/to_computed_value.rs new file mode 100644 index 0000000000..5e0f595c6b --- /dev/null +++ b/servo/components/style_derive/to_computed_value.rs @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use derive_common::cg; +use proc_macro2::TokenStream; +use syn::{DeriveInput, Ident, Path}; +use synstructure::{BindStyle, BindingInfo}; + +pub fn derive_to_value( + mut input: DeriveInput, + trait_path: Path, + output_type_name: Ident, + bind_style: BindStyle, + // Returns whether to apply the field bound for a given item. + mut binding_attrs: impl FnMut(&BindingInfo) -> ToValueAttrs, + // Returns a token stream of the form: trait_path::from_foo(#binding) + mut call_from: impl FnMut(&BindingInfo) -> TokenStream, + mut call_to: impl FnMut(&BindingInfo) -> TokenStream, + // Returns a tokenstream of the form: + // fn from_function_syntax(foobar) -> Baz { + // #first_arg + // } + // + // fn to_function_syntax(foobar) -> Baz { + // #second_arg + // } + mut trait_impl: impl FnMut(TokenStream, TokenStream) -> TokenStream, +) -> TokenStream { + let name = &input.ident; + + let mut where_clause = input.generics.where_clause.take(); + cg::propagate_clauses_to_output_type( + &mut where_clause, + &input.generics, + &trait_path, + &output_type_name, + ); + + let moves = match bind_style { + BindStyle::Move | BindStyle::MoveMut => true, + BindStyle::Ref | BindStyle::RefMut => false, + }; + + let params = input.generics.type_params().collect::<Vec<_>>(); + for param in ¶ms { + cg::add_predicate(&mut where_clause, parse_quote!(#param: #trait_path)); + } + + let computed_value_type = cg::fmap_trait_output(&input, &trait_path, &output_type_name); + + let mut add_field_bound = |binding: &BindingInfo| { + let ty = &binding.ast().ty; + + let output_type = cg::map_type_params( + ty, + ¶ms, + &computed_value_type, + &mut |ident| parse_quote!(<#ident as #trait_path>::#output_type_name), + ); + + cg::add_predicate( + &mut where_clause, + parse_quote!( + #ty: #trait_path<#output_type_name = #output_type> + ), + ); + }; + + let (to_body, from_body) = if params.is_empty() { + let mut s = synstructure::Structure::new(&input); + s.variants_mut().iter_mut().for_each(|v| { + v.bind_with(|_| bind_style); + }); + + for variant in s.variants() { + for binding in variant.bindings() { + let attrs = binding_attrs(&binding); + assert!( + !attrs.field_bound, + "It is default on a non-generic implementation", + ); + if !attrs.no_field_bound { + // Add field bounds to all bindings except the manually + // excluded. This ensures the correctness of the clone() / + // move based implementation. + add_field_bound(binding); + } + } + } + + let to_body = if moves { + quote! { self } + } else { + quote! { std::clone::Clone::clone(self) } + }; + + let from_body = if moves { + quote! { from } + } else { + quote! { std::clone::Clone::clone(from) } + }; + + (to_body, from_body) + } else { + let to_body = cg::fmap_match(&input, bind_style, |binding| { + let attrs = binding_attrs(&binding); + assert!( + !attrs.no_field_bound, + "It doesn't make sense on a generic implementation" + ); + if attrs.field_bound { + add_field_bound(&binding); + } + call_to(&binding) + }); + + let from_body = cg::fmap_match(&input, bind_style, |binding| call_from(&binding)); + + let self_ = if moves { + quote! { self } + } else { + quote! { *self } + }; + let from_ = if moves { + quote! { from } + } else { + quote! { *from } + }; + + let to_body = quote! { + match #self_ { + #to_body + } + }; + + let from_body = quote! { + match #from_ { + #from_body + } + }; + + (to_body, from_body) + }; + + input.generics.where_clause = where_clause; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let impl_ = trait_impl(from_body, to_body); + + quote! { + impl #impl_generics #trait_path for #name #ty_generics #where_clause { + type #output_type_name = #computed_value_type; + + #impl_ + } + } +} + +pub fn derive(input: DeriveInput) -> TokenStream { + let trait_impl = |from_body, to_body| { + quote! { + #[inline] + fn from_computed_value(from: &Self::ComputedValue) -> Self { + #from_body + } + + #[allow(unused_variables)] + #[inline] + fn to_computed_value(&self, context: &crate::values::computed::Context) -> Self::ComputedValue { + #to_body + } + } + }; + + derive_to_value( + input, + parse_quote!(crate::values::computed::ToComputedValue), + parse_quote!(ComputedValue), + BindStyle::Ref, + |binding| { + let attrs = cg::parse_field_attrs::<ComputedValueAttrs>(&binding.ast()); + ToValueAttrs { + field_bound: attrs.field_bound, + no_field_bound: attrs.no_field_bound, + } + }, + |binding| quote!(crate::values::computed::ToComputedValue::from_computed_value(#binding)), + |binding| quote!(crate::values::computed::ToComputedValue::to_computed_value(#binding, context)), + trait_impl, + ) +} + +#[derive(Default)] +pub struct ToValueAttrs { + pub field_bound: bool, + pub no_field_bound: bool, +} + +#[derive(Default, FromField)] +#[darling(attributes(compute), default)] +struct ComputedValueAttrs { + field_bound: bool, + no_field_bound: bool, +} diff --git a/servo/components/style_derive/to_css.rs b/servo/components/style_derive/to_css.rs new file mode 100644 index 0000000000..aa33536648 --- /dev/null +++ b/servo/components/style_derive/to_css.rs @@ -0,0 +1,396 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use darling::util::Override; +use derive_common::cg; +use proc_macro2::{Span, TokenStream}; +use quote::{ToTokens, TokenStreamExt}; +use syn::{self, Data, Ident, Path, WhereClause}; +use synstructure::{BindingInfo, Structure, VariantInfo}; + +fn derive_bitflags(input: &syn::DeriveInput, bitflags: &CssBitflagAttrs) -> TokenStream { + let name = &input.ident; + let mut body = TokenStream::new(); + for (rust_name, css_name) in bitflags.single_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + body.append_all(quote! { + if *self == Self::#rust_ident { + return dest.write_str(#css_name); + } + }); + } + + body.append_all(quote! { + let mut has_any = false; + }); + + if bitflags.overlapping_bits { + body.append_all(quote! { + let mut serialized = Self::empty(); + }); + } + + for (rust_name, css_name) in bitflags.mixed_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + let serialize = quote! { + if has_any { + dest.write_char(' ')?; + } + has_any = true; + dest.write_str(#css_name)?; + }; + if bitflags.overlapping_bits { + body.append_all(quote! { + if self.contains(Self::#rust_ident) && !serialized.intersects(Self::#rust_ident) { + #serialize + serialized.insert(Self::#rust_ident); + } + }); + } else { + body.append_all(quote! { + if self.intersects(Self::#rust_ident) { + #serialize + } + }); + } + } + + body.append_all(quote! { + Ok(()) + }); + + quote! { + impl style_traits::ToCss for #name { + #[allow(unused_variables)] + #[inline] + fn to_css<W>( + &self, + dest: &mut style_traits::CssWriter<W>, + ) -> std::fmt::Result + where + W: std::fmt::Write, + { + #body + } + } + } +} + +pub fn derive(mut input: syn::DeriveInput) -> TokenStream { + let mut where_clause = input.generics.where_clause.take(); + for param in input.generics.type_params() { + cg::add_predicate(&mut where_clause, parse_quote!(#param: style_traits::ToCss)); + } + + let input_attrs = cg::parse_input_attrs::<CssInputAttrs>(&input); + if matches!(input.data, Data::Enum(..)) || input_attrs.bitflags.is_some() { + assert!( + input_attrs.function.is_none(), + "#[css(function)] is not allowed on enums or bitflags" + ); + assert!( + !input_attrs.comma, + "#[css(comma)] is not allowed on enums or bitflags" + ); + } + + if let Some(ref bitflags) = input_attrs.bitflags { + assert!( + !input_attrs.derive_debug, + "Bitflags can derive debug on their own" + ); + assert!(where_clause.is_none(), "Generic bitflags?"); + return derive_bitflags(&input, bitflags); + } + + let match_body = { + let s = Structure::new(&input); + s.each_variant(|variant| derive_variant_arm(variant, &mut where_clause)) + }; + input.generics.where_clause = where_clause; + + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let mut impls = quote! { + impl #impl_generics style_traits::ToCss for #name #ty_generics #where_clause { + #[allow(unused_variables)] + #[inline] + fn to_css<W>( + &self, + dest: &mut style_traits::CssWriter<W>, + ) -> std::fmt::Result + where + W: std::fmt::Write, + { + match *self { + #match_body + } + } + } + }; + + if input_attrs.derive_debug { + impls.append_all(quote! { + impl #impl_generics std::fmt::Debug for #name #ty_generics #where_clause { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + style_traits::ToCss::to_css( + self, + &mut style_traits::CssWriter::new(f), + ) + } + } + }); + } + + impls +} + +fn derive_variant_arm(variant: &VariantInfo, generics: &mut Option<WhereClause>) -> TokenStream { + let bindings = variant.bindings(); + let identifier = cg::to_css_identifier(&variant.ast().ident.to_string()); + let ast = variant.ast(); + let variant_attrs = cg::parse_variant_attrs_from_ast::<CssVariantAttrs>(&ast); + let separator = if variant_attrs.comma { ", " } else { " " }; + + if variant_attrs.skip { + return quote!(Ok(())); + } + if variant_attrs.dimension { + assert_eq!(bindings.len(), 1); + assert!( + variant_attrs.function.is_none() && variant_attrs.keyword.is_none(), + "That makes no sense" + ); + } + + let mut expr = if let Some(keyword) = variant_attrs.keyword { + assert!(bindings.is_empty()); + quote! { + std::fmt::Write::write_str(dest, #keyword) + } + } else if !bindings.is_empty() { + derive_variant_fields_expr(bindings, generics, separator) + } else { + quote! { + std::fmt::Write::write_str(dest, #identifier) + } + }; + + if variant_attrs.dimension { + expr = quote! { + #expr?; + std::fmt::Write::write_str(dest, #identifier) + } + } else if let Some(function) = variant_attrs.function { + let mut identifier = function.explicit().map_or(identifier, |name| name); + identifier.push('('); + expr = quote! { + std::fmt::Write::write_str(dest, #identifier)?; + #expr?; + std::fmt::Write::write_str(dest, ")") + } + } + expr +} + +fn derive_variant_fields_expr( + bindings: &[BindingInfo], + where_clause: &mut Option<WhereClause>, + separator: &str, +) -> TokenStream { + let mut iter = bindings + .iter() + .filter_map(|binding| { + let attrs = cg::parse_field_attrs::<CssFieldAttrs>(&binding.ast()); + if attrs.skip { + return None; + } + Some((binding, attrs)) + }) + .peekable(); + + let (first, attrs) = match iter.next() { + Some(pair) => pair, + None => return quote! { Ok(()) }, + }; + if attrs.field_bound { + let ty = &first.ast().ty; + // TODO(emilio): IntoIterator might not be enough for every type of + // iterable thing (like ArcSlice<> or what not). We might want to expose + // an `item = "T"` attribute to handle that in the future. + let predicate = if attrs.iterable { + parse_quote!(<#ty as IntoIterator>::Item: style_traits::ToCss) + } else { + parse_quote!(#ty: style_traits::ToCss) + }; + cg::add_predicate(where_clause, predicate); + } + if !attrs.iterable && iter.peek().is_none() { + let mut expr = quote! { style_traits::ToCss::to_css(#first, dest) }; + if let Some(condition) = attrs.skip_if { + expr = quote! { + if !#condition(#first) { + #expr + } + } + } + + if let Some(condition) = attrs.contextual_skip_if { + expr = quote! { + if !#condition(#(#bindings), *) { + #expr + } + } + } + return expr; + } + + let mut expr = derive_single_field_expr(first, attrs, where_clause, bindings); + for (binding, attrs) in iter { + derive_single_field_expr(binding, attrs, where_clause, bindings).to_tokens(&mut expr) + } + + quote! {{ + let mut writer = style_traits::values::SequenceWriter::new(dest, #separator); + #expr + Ok(()) + }} +} + +fn derive_single_field_expr( + field: &BindingInfo, + attrs: CssFieldAttrs, + where_clause: &mut Option<WhereClause>, + bindings: &[BindingInfo], +) -> TokenStream { + let mut expr = if attrs.iterable { + if let Some(if_empty) = attrs.if_empty { + return quote! { + { + let mut iter = #field.iter().peekable(); + if iter.peek().is_none() { + writer.raw_item(#if_empty)?; + } else { + for item in iter { + writer.item(&item)?; + } + } + } + }; + } + quote! { + for item in #field.iter() { + writer.item(&item)?; + } + } + } else if attrs.represents_keyword { + let ident = field + .ast() + .ident + .as_ref() + .expect("Unnamed field with represents_keyword?"); + let ident = cg::to_css_identifier(&ident.to_string()).replace("_", "-"); + quote! { + if *#field { + writer.raw_item(#ident)?; + } + } + } else { + if attrs.field_bound { + let ty = &field.ast().ty; + cg::add_predicate(where_clause, parse_quote!(#ty: style_traits::ToCss)); + } + quote! { writer.item(#field)?; } + }; + + if let Some(condition) = attrs.skip_if { + expr = quote! { + if !#condition(#field) { + #expr + } + } + } + + if let Some(condition) = attrs.contextual_skip_if { + expr = quote! { + if !#condition(#(#bindings), *) { + #expr + } + } + } + + expr +} + +#[derive(Default, FromMeta)] +#[darling(default)] +pub struct CssBitflagAttrs { + /// Flags that can only go on their own, comma-separated. + pub single: Option<String>, + /// Flags that can go mixed with each other, comma-separated. + pub mixed: Option<String>, + /// Extra validation of the resulting mixed flags. + pub validate_mixed: Option<Path>, + /// Whether there are overlapping bits we need to take care of when + /// serializing. + pub overlapping_bits: bool, +} + +impl CssBitflagAttrs { + /// Returns a vector of (rust_name, css_name) of a given flag list. + fn names(s: &Option<String>) -> Vec<(String, String)> { + let s = match s { + Some(s) => s, + None => return vec![], + }; + s.split(',') + .map(|css_name| (cg::to_scream_case(css_name), css_name.to_owned())) + .collect() + } + + pub fn single_flags(&self) -> Vec<(String, String)> { + Self::names(&self.single) + } + + pub fn mixed_flags(&self) -> Vec<(String, String)> { + Self::names(&self.mixed) + } +} + +#[derive(Default, FromDeriveInput)] +#[darling(attributes(css), default)] +pub struct CssInputAttrs { + pub derive_debug: bool, + // Here because structs variants are also their whole type definition. + pub function: Option<Override<String>>, + // Here because structs variants are also their whole type definition. + pub comma: bool, + pub bitflags: Option<CssBitflagAttrs>, +} + +#[derive(Default, FromVariant)] +#[darling(attributes(css), default)] +pub struct CssVariantAttrs { + pub function: Option<Override<String>>, + // Here because structs variants are also their whole type definition. + pub derive_debug: bool, + pub comma: bool, + pub bitflags: Option<CssBitflagAttrs>, + pub dimension: bool, + pub keyword: Option<String>, + pub skip: bool, +} + +#[derive(Default, FromField)] +#[darling(attributes(css), default)] +pub struct CssFieldAttrs { + pub if_empty: Option<String>, + pub field_bound: bool, + pub iterable: bool, + pub skip: bool, + pub represents_keyword: bool, + pub contextual_skip_if: Option<Path>, + pub skip_if: Option<Path>, +} diff --git a/servo/components/style_derive/to_resolved_value.rs b/servo/components/style_derive/to_resolved_value.rs new file mode 100644 index 0000000000..e049f91152 --- /dev/null +++ b/servo/components/style_derive/to_resolved_value.rs @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use derive_common::cg; +use proc_macro2::TokenStream; +use syn::DeriveInput; +use synstructure::BindStyle; +use to_computed_value; + +pub fn derive(input: DeriveInput) -> TokenStream { + let trait_impl = |from_body, to_body| { + quote! { + #[inline] + fn from_resolved_value(from: Self::ResolvedValue) -> Self { + #from_body + } + + #[inline] + fn to_resolved_value( + self, + context: &crate::values::resolved::Context, + ) -> Self::ResolvedValue { + #to_body + } + } + }; + + to_computed_value::derive_to_value( + input, + parse_quote!(crate::values::resolved::ToResolvedValue), + parse_quote!(ResolvedValue), + BindStyle::Move, + |binding| { + let attrs = cg::parse_field_attrs::<ResolvedValueAttrs>(&binding.ast()); + to_computed_value::ToValueAttrs { + field_bound: attrs.field_bound, + no_field_bound: attrs.no_field_bound, + } + }, + |binding| quote!(crate::values::resolved::ToResolvedValue::from_resolved_value(#binding)), + |binding| quote!(crate::values::resolved::ToResolvedValue::to_resolved_value(#binding, context)), + trait_impl, + ) +} + +#[derive(Default, FromField)] +#[darling(attributes(resolve), default)] +struct ResolvedValueAttrs { + field_bound: bool, + no_field_bound: bool, +} diff --git a/servo/components/style_traits/Cargo.toml b/servo/components/style_traits/Cargo.toml new file mode 100644 index 0000000000..81d6e2bdf4 --- /dev/null +++ b/servo/components/style_traits/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "style_traits" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +[lib] +name = "style_traits" +path = "lib.rs" + +[features] +servo = ["servo_atoms", "cssparser/serde", "webrender_api", "servo_url", "euclid/serde"] +gecko = ["nsstring"] + +[dependencies] +app_units = "0.7" +bitflags = "2" +cssparser = "0.33" +euclid = "0.22" +lazy_static = "1" +malloc_size_of = { path = "../malloc_size_of" } +malloc_size_of_derive = { path = "../../../xpcom/rust/malloc_size_of_derive" } +nsstring = {path = "../../../xpcom/rust/nsstring/", optional = true} +selectors = { path = "../selectors" } +serde = "1.0" +servo_arc = { path = "../servo_arc" } +servo_atoms = { path = "../atoms", optional = true } +servo_url = { path = "../url", optional = true } +to_shmem = { path = "../to_shmem" } +to_shmem_derive = { path = "../to_shmem_derive" } +webrender_api = { git = "https://github.com/servo/webrender", optional = true } diff --git a/servo/components/style_traits/arc_slice.rs b/servo/components/style_traits/arc_slice.rs new file mode 100644 index 0000000000..1721a33f48 --- /dev/null +++ b/servo/components/style_traits/arc_slice.rs @@ -0,0 +1,162 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! A thin atomically-reference-counted slice. + +use serde::de::{Deserialize, Deserializer}; +use serde::ser::{Serialize, Serializer}; +use servo_arc::ThinArc; +use std::ops::Deref; +use std::ptr::NonNull; +use std::{iter, mem}; + +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalSizeOf}; + +/// A canary that we stash in ArcSlices. +/// +/// Given we cannot use a zero-sized-type for the header, since well, C++ +/// doesn't have zsts, and we want to use cbindgen for this type, we may as well +/// assert some sanity at runtime. +/// +/// We use an u64, to guarantee that we can use a single singleton for every +/// empty slice, even if the types they hold are aligned differently. +const ARC_SLICE_CANARY: u64 = 0xf3f3f3f3f3f3f3f3; + +/// A wrapper type for a refcounted slice using ThinArc. +#[repr(C)] +#[derive(Debug, Eq, PartialEq, ToShmem)] +pub struct ArcSlice<T>(#[shmem(field_bound)] ThinArc<u64, T>); + +impl<T> Deref for ArcSlice<T> { + type Target = [T]; + + #[inline] + fn deref(&self) -> &Self::Target { + debug_assert_eq!(self.0.header, ARC_SLICE_CANARY); + self.0.slice() + } +} + +impl<T> Clone for ArcSlice<T> { + fn clone(&self) -> Self { + ArcSlice(self.0.clone()) + } +} + +lazy_static! { + // ThinArc doesn't support alignments greater than align_of::<u64>. + static ref EMPTY_ARC_SLICE: ArcSlice<u64> = { + ArcSlice::from_iter_leaked(iter::empty()) + }; +} + +impl<T> Default for ArcSlice<T> { + #[allow(unsafe_code)] + fn default() -> Self { + debug_assert!( + mem::align_of::<T>() <= mem::align_of::<u64>(), + "Need to increase the alignment of EMPTY_ARC_SLICE" + ); + unsafe { + let empty: ArcSlice<_> = EMPTY_ARC_SLICE.clone(); + let empty: Self = mem::transmute(empty); + debug_assert_eq!(empty.len(), 0); + empty + } + } +} + +impl<T: Serialize> Serialize for ArcSlice<T> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + self.deref().serialize(serializer) + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for ArcSlice<T> { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let r = Vec::deserialize(deserializer)?; + Ok(ArcSlice::from_iter(r.into_iter())) + } +} + +impl<T> ArcSlice<T> { + /// Creates an Arc for a slice using the given iterator to generate the + /// slice. + #[inline] + pub fn from_iter<I>(items: I) -> Self + where + I: Iterator<Item = T> + ExactSizeIterator, + { + if items.len() == 0 { + return Self::default(); + } + ArcSlice(ThinArc::from_header_and_iter(ARC_SLICE_CANARY, items)) + } + + /// Creates an Arc for a slice using the given iterator to generate the + /// slice, and marks the arc as intentionally leaked from the refcount + /// logging point of view. + #[inline] + pub fn from_iter_leaked<I>(items: I) -> Self + where + I: Iterator<Item = T> + ExactSizeIterator, + { + let arc = ThinArc::from_header_and_iter(ARC_SLICE_CANARY, items); + arc.mark_as_intentionally_leaked(); + ArcSlice(arc) + } + + /// Creates a value that can be passed via FFI, and forgets this value + /// altogether. + #[inline] + #[allow(unsafe_code)] + pub fn forget(self) -> ForgottenArcSlicePtr<T> { + let ret = unsafe { + ForgottenArcSlicePtr(NonNull::new_unchecked( + self.0.raw_ptr() as *const _ as *mut _ + )) + }; + mem::forget(self); + ret + } + + /// Leaks an empty arc slice pointer, and returns it. Only to be used to + /// construct ArcSlices from FFI. + #[inline] + pub fn leaked_empty_ptr() -> *mut std::os::raw::c_void { + let empty: ArcSlice<_> = EMPTY_ARC_SLICE.clone(); + let ptr = empty.0.raw_ptr(); + std::mem::forget(empty); + ptr as *mut _ + } + + /// Returns whether there's only one reference to this ArcSlice. + pub fn is_unique(&self) -> bool { + self.0.is_unique() + } +} + +impl<T: MallocSizeOf> MallocUnconditionalSizeOf for ArcSlice<T> { + #[allow(unsafe_code)] + fn unconditional_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + let mut size = unsafe { ops.malloc_size_of(self.0.heap_ptr()) }; + for el in self.iter() { + size += el.size_of(ops); + } + size + } +} + +/// The inner pointer of an ArcSlice<T>, to be sent via FFI. +/// The type of the pointer is a bit of a lie, we just want to preserve the type +/// but these pointers cannot be constructed outside of this crate, so we're +/// good. +#[repr(C)] +pub struct ForgottenArcSlicePtr<T>(NonNull<T>); diff --git a/servo/components/style_traits/dom.rs b/servo/components/style_traits/dom.rs new file mode 100644 index 0000000000..03d5264abf --- /dev/null +++ b/servo/components/style_traits/dom.rs @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Types used to access the DOM from style calculation. + +/// An opaque handle to a node, which, unlike UnsafeNode, cannot be transformed +/// back into a non-opaque representation. The only safe operation that can be +/// performed on this node is to compare it to another opaque handle or to another +/// OpaqueNode. +/// +/// Layout and Graphics use this to safely represent nodes for comparison purposes. +/// Because the script task's GC does not trace layout, node data cannot be safely stored in layout +/// data structures. Also, layout code tends to be faster when the DOM is not being accessed, for +/// locality reasons. Using `OpaqueNode` enforces this invariant. +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct OpaqueNode(pub usize); + +impl OpaqueNode { + /// Returns the address of this node, for debugging purposes. + #[inline] + pub fn id(&self) -> usize { + self.0 + } +} diff --git a/servo/components/style_traits/lib.rs b/servo/components/style_traits/lib.rs new file mode 100644 index 0000000000..9bb2b3c655 --- /dev/null +++ b/servo/components/style_traits/lib.rs @@ -0,0 +1,295 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! This module contains shared types and messages for use by devtools/script. +//! The traits are here instead of in script so that the devtools crate can be +//! modified independently of the rest of Servo. + +#![crate_name = "style_traits"] +#![crate_type = "rlib"] +#![deny(unsafe_code, missing_docs)] + +extern crate app_units; +#[macro_use] +extern crate bitflags; +extern crate cssparser; +extern crate euclid; +#[macro_use] +extern crate lazy_static; +extern crate malloc_size_of; +#[macro_use] +extern crate malloc_size_of_derive; +extern crate nsstring; +extern crate selectors; +#[macro_use] +extern crate serde; +extern crate servo_arc; +#[cfg(feature = "servo")] +extern crate servo_atoms; +#[cfg(feature = "servo")] +extern crate servo_url; +extern crate to_shmem; +#[macro_use] +extern crate to_shmem_derive; +#[cfg(feature = "servo")] +extern crate webrender_api; +#[cfg(feature = "servo")] +pub use webrender_api::units::DevicePixel; + +use cssparser::{CowRcStr, Token}; +use selectors::parser::SelectorParseErrorKind; +#[cfg(feature = "servo")] +use servo_atoms::Atom; + +/// One hardware pixel. +/// +/// This unit corresponds to the smallest addressable element of the display hardware. +#[cfg(not(feature = "servo"))] +#[derive(Clone, Copy, Debug)] +pub enum DevicePixel {} + +/// Represents a mobile style pinch zoom factor. +/// TODO(gw): Once WR supports pinch zoom, use a type directly from webrender_api. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize, MallocSizeOf))] +pub struct PinchZoomFactor(f32); + +impl PinchZoomFactor { + /// Construct a new pinch zoom factor. + pub fn new(scale: f32) -> PinchZoomFactor { + PinchZoomFactor(scale) + } + + /// Get the pinch zoom factor as an untyped float. + pub fn get(&self) -> f32 { + self.0 + } +} + +/// One CSS "px" in the coordinate system of the "initial viewport": +/// <http://www.w3.org/TR/css-device-adapt/#initial-viewport> +/// +/// `CSSPixel` is equal to `DeviceIndependentPixel` times a "page zoom" factor controlled by the user. This is +/// the desktop-style "full page" zoom that enlarges content but then reflows the layout viewport +/// so it still exactly fits the visible area. +/// +/// At the default zoom level of 100%, one `CSSPixel` is equal to one `DeviceIndependentPixel`. However, if the +/// document is zoomed in or out then this scale may be larger or smaller. +#[derive(Clone, Copy, Debug)] +pub enum CSSPixel {} + +// In summary, the hierarchy of pixel units and the factors to convert from one to the next: +// +// DevicePixel +// / hidpi_ratio => DeviceIndependentPixel +// / desktop_zoom => CSSPixel + +pub mod arc_slice; +pub mod dom; +pub mod specified_value_info; +#[macro_use] +pub mod values; +pub mod owned_slice; +pub mod owned_str; + +pub use crate::specified_value_info::{CssType, KeywordsCollectFn, SpecifiedValueInfo}; +pub use crate::values::{ + Comma, CommaWithSpace, CssWriter, OneOrMoreSeparated, Separator, Space, ToCss, +}; + +/// The error type for all CSS parsing routines. +pub type ParseError<'i> = cssparser::ParseError<'i, StyleParseErrorKind<'i>>; + +/// Error in property value parsing +pub type ValueParseError<'i> = cssparser::ParseError<'i, ValueParseErrorKind<'i>>; + +#[derive(Clone, Debug, PartialEq)] +/// Errors that can be encountered while parsing CSS values. +pub enum StyleParseErrorKind<'i> { + /// A bad URL token in a DVB. + BadUrlInDeclarationValueBlock(CowRcStr<'i>), + /// A bad string token in a DVB. + BadStringInDeclarationValueBlock(CowRcStr<'i>), + /// Unexpected closing parenthesis in a DVB. + UnbalancedCloseParenthesisInDeclarationValueBlock, + /// Unexpected closing bracket in a DVB. + UnbalancedCloseSquareBracketInDeclarationValueBlock, + /// Unexpected closing curly bracket in a DVB. + UnbalancedCloseCurlyBracketInDeclarationValueBlock, + /// A property declaration value had input remaining after successfully parsing. + PropertyDeclarationValueNotExhausted, + /// An unexpected dimension token was encountered. + UnexpectedDimension(CowRcStr<'i>), + /// Missing or invalid media feature name. + MediaQueryExpectedFeatureName(CowRcStr<'i>), + /// Missing or invalid media feature value. + MediaQueryExpectedFeatureValue, + /// A media feature range operator was not expected. + MediaQueryUnexpectedOperator, + /// min- or max- properties must have a value. + RangedExpressionWithNoValue, + /// A function was encountered that was not expected. + UnexpectedFunction(CowRcStr<'i>), + /// Error encountered parsing a @property's `syntax` descriptor + PropertySyntaxField(PropertySyntaxParseError), + /// @namespace must be before any rule but @charset and @import + UnexpectedNamespaceRule, + /// @import must be before any rule but @charset + UnexpectedImportRule, + /// @import rules are disallowed in the parser. + DisallowedImportRule, + /// Unexpected @charset rule encountered. + UnexpectedCharsetRule, + /// The @property `<custom-property-name>` must start with `--` + UnexpectedIdent(CowRcStr<'i>), + /// A placeholder for many sources of errors that require more specific variants. + UnspecifiedError, + /// An unexpected token was found within a namespace rule. + UnexpectedTokenWithinNamespace(Token<'i>), + /// An error was encountered while parsing a property value. + ValueError(ValueParseErrorKind<'i>), + /// An error was encountered while parsing a selector + SelectorError(SelectorParseErrorKind<'i>), + /// The property declaration was for an unknown property. + UnknownProperty(CowRcStr<'i>), + /// The property declaration was for a disabled experimental property. + ExperimentalProperty, + /// The property declaration contained an invalid color value. + InvalidColor(CowRcStr<'i>, Token<'i>), + /// The property declaration contained an invalid filter value. + InvalidFilter(CowRcStr<'i>, Token<'i>), + /// The property declaration contained an invalid value. + OtherInvalidValue(CowRcStr<'i>), + /// The declaration contained an animation property, and we were parsing + /// this as a keyframe block (so that property should be ignored). + /// + /// See: https://drafts.csswg.org/css-animations/#keyframes + AnimationPropertyInKeyframeBlock, + /// The property is not allowed within a page rule. + NotAllowedInPageRule, +} + +impl<'i> From<ValueParseErrorKind<'i>> for StyleParseErrorKind<'i> { + fn from(this: ValueParseErrorKind<'i>) -> Self { + StyleParseErrorKind::ValueError(this) + } +} + +impl<'i> From<SelectorParseErrorKind<'i>> for StyleParseErrorKind<'i> { + fn from(this: SelectorParseErrorKind<'i>) -> Self { + StyleParseErrorKind::SelectorError(this) + } +} + +/// Specific errors that can be encountered while parsing property values. +#[derive(Clone, Debug, PartialEq)] +pub enum ValueParseErrorKind<'i> { + /// An invalid token was encountered while parsing a color value. + InvalidColor(Token<'i>), + /// An invalid filter value was encountered. + InvalidFilter(Token<'i>), +} + +impl<'i> StyleParseErrorKind<'i> { + /// Create an InvalidValue parse error + pub fn new_invalid<S>(name: S, value_error: ParseError<'i>) -> ParseError<'i> + where + S: Into<CowRcStr<'i>>, + { + let name = name.into(); + let variant = match value_error.kind { + cssparser::ParseErrorKind::Custom(StyleParseErrorKind::ValueError(e)) => match e { + ValueParseErrorKind::InvalidColor(token) => { + StyleParseErrorKind::InvalidColor(name, token) + }, + ValueParseErrorKind::InvalidFilter(token) => { + StyleParseErrorKind::InvalidFilter(name, token) + }, + }, + _ => StyleParseErrorKind::OtherInvalidValue(name), + }; + cssparser::ParseError { + kind: cssparser::ParseErrorKind::Custom(variant), + location: value_error.location, + } + } +} + +/// Errors that can be encountered while parsing the @property rule's syntax descriptor. +#[derive(Clone, Debug, PartialEq)] +pub enum PropertySyntaxParseError { + /// The string's length was 0. + EmptyInput, + /// A non-whitespace, non-pipe character was fount after parsing a component. + ExpectedPipeBetweenComponents, + /// The start of an identifier was expected but not found. + /// + /// <https://drafts.csswg.org/css-syntax-3/#name-start-code-point> + InvalidNameStart, + /// The name is not a valid `<ident>`. + InvalidName, + /// The data type name was not closed. + /// + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#consume-data-type-name> + UnclosedDataTypeName, + /// The next byte was expected while parsing, but EOF was found instead. + UnexpectedEOF, + /// The data type is not a supported syntax component name. + /// + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#supported-names> + UnknownDataTypeName, +} + +bitflags! { + /// The mode to use when parsing values. + #[derive(Clone, Copy, Eq, PartialEq)] + #[repr(C)] + pub struct ParsingMode: u8 { + /// In CSS; lengths must have units, except for zero values, where the unit can be omitted. + /// <https://www.w3.org/TR/css3-values/#lengths> + const DEFAULT = 0; + /// In SVG; a coordinate or length value without a unit identifier (e.g., "25") is assumed + /// to be in user units (px). + /// <https://www.w3.org/TR/SVG/coords.html#Units> + const ALLOW_UNITLESS_LENGTH = 1; + /// In SVG; out-of-range values are not treated as an error in parsing. + /// <https://www.w3.org/TR/SVG/implnote.html#RangeClamping> + const ALLOW_ALL_NUMERIC_VALUES = 1 << 1; + /// In CSS Properties and Values, the initial value must be computationally + /// independent. + /// <https://drafts.css-houdini.org/css-properties-values-api-1/#ref-for-computationally-independent%E2%91%A0> + const DISALLOW_FONT_RELATIVE = 1 << 2; + } +} + +impl ParsingMode { + /// Whether the parsing mode allows unitless lengths for non-zero values to be intpreted as px. + #[inline] + pub fn allows_unitless_lengths(&self) -> bool { + self.intersects(ParsingMode::ALLOW_UNITLESS_LENGTH) + } + + /// Whether the parsing mode allows all numeric values. + #[inline] + pub fn allows_all_numeric_values(&self) -> bool { + self.intersects(ParsingMode::ALLOW_ALL_NUMERIC_VALUES) + } + + /// Whether the parsing mode allows font-relative units. + #[inline] + pub fn allows_font_relative_lengths(&self) -> bool { + !self.intersects(ParsingMode::DISALLOW_FONT_RELATIVE) + } +} + +#[cfg(feature = "servo")] +/// Speculatively execute paint code in the worklet thread pool. +pub trait SpeculativePainter: Send + Sync { + /// <https://drafts.css-houdini.org/css-paint-api/#draw-a-paint-image> + fn speculatively_draw_a_paint_image( + &self, + properties: Vec<(Atom, String)>, + arguments: Vec<String>, + ); +} diff --git a/servo/components/style_traits/owned_slice.rs b/servo/components/style_traits/owned_slice.rs new file mode 100644 index 0000000000..36ba3162e5 --- /dev/null +++ b/servo/components/style_traits/owned_slice.rs @@ -0,0 +1,198 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! A replacement for `Box<[T]>` that cbindgen can understand. + +use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps}; +use serde::de::{Deserialize, Deserializer}; +use serde::ser::{Serialize, Serializer}; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::ptr::NonNull; +use std::{fmt, iter, mem, slice}; +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +/// A struct that basically replaces a `Box<[T]>`, but which cbindgen can +/// understand. +/// +/// We could rely on the struct layout of `Box<[T]>` per: +/// +/// https://github.com/rust-lang/unsafe-code-guidelines/blob/master/reference/src/layout/pointers.md +/// +/// But handling fat pointers with cbindgen both in structs and argument +/// positions more generally is a bit tricky. +/// +/// cbindgen:derive-eq=false +/// cbindgen:derive-neq=false +#[repr(C)] +pub struct OwnedSlice<T: Sized> { + ptr: NonNull<T>, + len: usize, + _phantom: PhantomData<T>, +} + +impl<T: Sized> Default for OwnedSlice<T> { + #[inline] + fn default() -> Self { + Self { + len: 0, + ptr: NonNull::dangling(), + _phantom: PhantomData, + } + } +} + +impl<T: Sized> Drop for OwnedSlice<T> { + #[inline] + fn drop(&mut self) { + if self.len != 0 { + let _ = mem::replace(self, Self::default()).into_vec(); + } + } +} + +unsafe impl<T: Sized + Send> Send for OwnedSlice<T> {} +unsafe impl<T: Sized + Sync> Sync for OwnedSlice<T> {} + +impl<T: Clone> Clone for OwnedSlice<T> { + #[inline] + fn clone(&self) -> Self { + Self::from_slice(&**self) + } +} + +impl<T: fmt::Debug> fmt::Debug for OwnedSlice<T> { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + self.deref().fmt(formatter) + } +} + +impl<T: PartialEq> PartialEq for OwnedSlice<T> { + fn eq(&self, other: &Self) -> bool { + self.deref().eq(other.deref()) + } +} + +impl<T: Eq> Eq for OwnedSlice<T> {} + +impl<T: Sized> OwnedSlice<T> { + /// Convert the OwnedSlice into a boxed slice. + #[inline] + pub fn into_box(self) -> Box<[T]> { + self.into_vec().into_boxed_slice() + } + + /// Convert the OwnedSlice into a Vec. + #[inline] + pub fn into_vec(self) -> Vec<T> { + let ret = unsafe { Vec::from_raw_parts(self.ptr.as_ptr(), self.len, self.len) }; + mem::forget(self); + ret + } + + /// Convert the regular slice into an owned slice. + #[inline] + pub fn from_slice(s: &[T]) -> Self + where + T: Clone, + { + Self::from(s.to_vec()) + } +} + +impl<T> IntoIterator for OwnedSlice<T> { + type Item = T; + type IntoIter = <Vec<T> as IntoIterator>::IntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.into_vec().into_iter() + } +} + +impl<T> Deref for OwnedSlice<T> { + type Target = [T]; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) } + } +} + +impl<T> DerefMut for OwnedSlice<T> { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) } + } +} + +impl<T> From<Box<[T]>> for OwnedSlice<T> { + #[inline] + fn from(mut b: Box<[T]>) -> Self { + let len = b.len(); + let ptr = unsafe { NonNull::new_unchecked(b.as_mut_ptr()) }; + mem::forget(b); + Self { + len, + ptr, + _phantom: PhantomData, + } + } +} + +impl<T> From<Vec<T>> for OwnedSlice<T> { + #[inline] + fn from(b: Vec<T>) -> Self { + Self::from(b.into_boxed_slice()) + } +} + +impl<T: Sized> MallocShallowSizeOf for OwnedSlice<T> { + fn shallow_size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + unsafe { ops.malloc_size_of(self.ptr.as_ptr()) } + } +} + +impl<T: MallocSizeOf + Sized> MallocSizeOf for OwnedSlice<T> { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + self.shallow_size_of(ops) + (**self).size_of(ops) + } +} + +impl<T: ToShmem + Sized> ToShmem for OwnedSlice<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + unsafe { + let dest = to_shmem::to_shmem_slice(self.iter(), builder)?; + Ok(mem::ManuallyDrop::new(Self::from(Box::from_raw(dest)))) + } + } +} + +impl<T> iter::FromIterator<T> for OwnedSlice<T> { + #[inline] + fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self { + Vec::from_iter(iter).into() + } +} + +impl<T: Serialize> Serialize for OwnedSlice<T> { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + self.deref().serialize(serializer) + } +} + +impl<'de, T: Deserialize<'de>> Deserialize<'de> for OwnedSlice<T> { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: Deserializer<'de>, + { + let r = Box::<[T]>::deserialize(deserializer)?; + Ok(r.into()) + } +} diff --git a/servo/components/style_traits/owned_str.rs b/servo/components/style_traits/owned_str.rs new file mode 100644 index 0000000000..ebfdcd5e06 --- /dev/null +++ b/servo/components/style_traits/owned_str.rs @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![allow(unsafe_code)] + +//! A replacement for `Box<str>` that has a defined layout for FFI. + +use crate::owned_slice::OwnedSlice; +use std::fmt; +use std::ops::{Deref, DerefMut}; + +/// A struct that basically replaces a Box<str>, but with a defined layout, +/// suitable for FFI. +#[repr(C)] +#[derive(Clone, Default, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub struct OwnedStr(OwnedSlice<u8>); + +impl fmt::Debug for OwnedStr { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + self.deref().fmt(formatter) + } +} + +impl Deref for OwnedStr { + type Target = str; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + unsafe { std::str::from_utf8_unchecked(&*self.0) } + } +} + +impl DerefMut for OwnedStr { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + unsafe { std::str::from_utf8_unchecked_mut(&mut *self.0) } + } +} + +impl OwnedStr { + /// Convert the OwnedStr into a boxed str. + #[inline] + pub fn into_box(self) -> Box<str> { + self.into_string().into_boxed_str() + } + + /// Convert the OwnedStr into a `String`. + #[inline] + pub fn into_string(self) -> String { + unsafe { String::from_utf8_unchecked(self.0.into_vec()) } + } +} + +impl From<OwnedStr> for String { + #[inline] + fn from(b: OwnedStr) -> Self { + b.into_string() + } +} + +impl From<OwnedStr> for Box<str> { + #[inline] + fn from(b: OwnedStr) -> Self { + b.into_box() + } +} + +impl From<Box<str>> for OwnedStr { + #[inline] + fn from(b: Box<str>) -> Self { + Self::from(b.into_string()) + } +} + +impl From<String> for OwnedStr { + #[inline] + fn from(s: String) -> Self { + OwnedStr(s.into_bytes().into()) + } +} diff --git a/servo/components/style_traits/specified_value_info.rs b/servo/components/style_traits/specified_value_info.rs new file mode 100644 index 0000000000..1dd368d36e --- /dev/null +++ b/servo/components/style_traits/specified_value_info.rs @@ -0,0 +1,138 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Value information for devtools. + +use crate::arc_slice::ArcSlice; +use crate::owned_slice::OwnedSlice; +use servo_arc::Arc; +use std::ops::Range; +use std::sync::Arc as StdArc; + +/// Type of value that a property supports. This is used by Gecko's +/// devtools to make sense about value it parses, and types listed +/// here should match InspectorPropertyType in InspectorUtils.webidl. +/// +/// XXX This should really be a bitflags rather than a namespace mod, +/// but currently we cannot use bitflags in const. +#[allow(non_snake_case)] +pub mod CssType { + /// <color> + pub const COLOR: u8 = 1 << 0; + /// <gradient> + pub const GRADIENT: u8 = 1 << 1; + /// <timing-function> + pub const TIMING_FUNCTION: u8 = 1 << 2; +} + +/// See SpecifiedValueInfo::collect_completion_keywords. +pub type KeywordsCollectFn<'a> = &'a mut dyn FnMut(&[&'static str]); + +/// Information of values of a given specified value type. +/// +/// This trait is derivable with `#[derive(SpecifiedValueInfo)]`. +/// +/// The algorithm traverses the type definition. For `SUPPORTED_TYPES`, +/// it puts an or'ed value of `SUPPORTED_TYPES` of all types it finds. +/// For `collect_completion_keywords`, it recursively invokes this +/// method on types found, and lists all keyword values and function +/// names following the same rule as `ToCss` in that method. +/// +/// Some attributes of `ToCss` can affect the behavior, specifically: +/// * If `#[css(function)]` is found, the content inside the annotated +/// variant (or the whole type) isn't traversed, only the function +/// name is listed in `collect_completion_keywords`. +/// * If `#[css(skip)]` is found, the content inside the variant or +/// field is ignored. +/// * Values listed in `#[css(if_empty)]`, `#[parse(aliases)]`, and +/// `#[css(keyword)]` are added into `collect_completion_keywords`. +/// +/// In addition to `css` attributes, it also has `value_info` helper +/// attributes, including: +/// * `#[value_info(ty = "TYPE")]` can be used to specify a constant +/// from `CssType` to `SUPPORTED_TYPES`. +/// * `#[value_info(other_values = "value1,value2")]` can be used to +/// add other values related to a field, variant, or the type itself +/// into `collect_completion_keywords`. +/// * `#[value_info(starts_with_keyword)]` can be used on variants to +/// add the name of a non-unit variant (serialized like `ToCss`) into +/// `collect_completion_keywords`. +pub trait SpecifiedValueInfo { + /// Supported CssTypes by the given value type. + /// + /// XXX This should be typed CssType when that becomes a bitflags. + /// Currently we cannot do so since bitflags cannot be used in constant. + const SUPPORTED_TYPES: u8 = 0; + + /// Collect value starting words for the given specified value type. + /// This includes keyword and function names which can appear at the + /// beginning of a value of this type. + /// + /// Caller should pass in a callback function to accept the list of + /// values. The callback function can be called multiple times, and + /// some values passed to the callback may be duplicate. + fn collect_completion_keywords(_f: KeywordsCollectFn) {} +} + +impl SpecifiedValueInfo for bool {} +impl SpecifiedValueInfo for f32 {} +impl SpecifiedValueInfo for i8 {} +impl SpecifiedValueInfo for i32 {} +impl SpecifiedValueInfo for u8 {} +impl SpecifiedValueInfo for u16 {} +impl SpecifiedValueInfo for u32 {} +impl SpecifiedValueInfo for usize {} +impl SpecifiedValueInfo for str {} +impl SpecifiedValueInfo for String {} +impl SpecifiedValueInfo for crate::owned_str::OwnedStr {} + +#[cfg(feature = "servo")] +impl SpecifiedValueInfo for ::servo_atoms::Atom {} +#[cfg(feature = "servo")] +impl SpecifiedValueInfo for ::servo_url::ServoUrl {} + +impl<T: SpecifiedValueInfo + ?Sized> SpecifiedValueInfo for Box<T> { + const SUPPORTED_TYPES: u8 = T::SUPPORTED_TYPES; + fn collect_completion_keywords(f: KeywordsCollectFn) { + T::collect_completion_keywords(f); + } +} + +impl<T: SpecifiedValueInfo> SpecifiedValueInfo for [T] { + const SUPPORTED_TYPES: u8 = T::SUPPORTED_TYPES; + fn collect_completion_keywords(f: KeywordsCollectFn) { + T::collect_completion_keywords(f); + } +} + +macro_rules! impl_generic_specified_value_info { + ($ty:ident<$param:ident>) => { + impl<$param: SpecifiedValueInfo> SpecifiedValueInfo for $ty<$param> { + const SUPPORTED_TYPES: u8 = $param::SUPPORTED_TYPES; + fn collect_completion_keywords(f: KeywordsCollectFn) { + $param::collect_completion_keywords(f); + } + } + }; +} +impl_generic_specified_value_info!(Option<T>); +impl_generic_specified_value_info!(OwnedSlice<T>); +impl_generic_specified_value_info!(Vec<T>); +impl_generic_specified_value_info!(Arc<T>); +impl_generic_specified_value_info!(StdArc<T>); +impl_generic_specified_value_info!(ArcSlice<T>); +impl_generic_specified_value_info!(Range<Idx>); + +impl<T1, T2> SpecifiedValueInfo for (T1, T2) +where + T1: SpecifiedValueInfo, + T2: SpecifiedValueInfo, +{ + const SUPPORTED_TYPES: u8 = T1::SUPPORTED_TYPES | T2::SUPPORTED_TYPES; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + T1::collect_completion_keywords(f); + T2::collect_completion_keywords(f); + } +} diff --git a/servo/components/style_traits/values.rs b/servo/components/style_traits/values.rs new file mode 100644 index 0000000000..a004b577c1 --- /dev/null +++ b/servo/components/style_traits/values.rs @@ -0,0 +1,569 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Helper types and traits for the handling of CSS values. + +use app_units::Au; +use cssparser::ToCss as CssparserToCss; +use cssparser::{serialize_string, ParseError, Parser, Token, UnicodeRange}; +use nsstring::nsCString; +use servo_arc::Arc; +use std::fmt::{self, Write}; + +/// Serialises a value according to its CSS representation. +/// +/// This trait is implemented for `str` and its friends, serialising the string +/// contents as a CSS quoted string. +/// +/// This trait is derivable with `#[derive(ToCss)]`, with the following behaviour: +/// * unit variants get serialised as the `snake-case` representation +/// of their name; +/// * unit variants whose name starts with "Moz" or "Webkit" are prepended +/// with a "-"; +/// * if `#[css(comma)]` is found on a variant, its fields are separated by +/// commas, otherwise, by spaces; +/// * if `#[css(function)]` is found on a variant, the variant name gets +/// serialised like unit variants and its fields are surrounded by parentheses; +/// * if `#[css(iterable)]` is found on a function variant, that variant needs +/// to have a single member, and that member needs to be iterable. The +/// iterable will be serialized as the arguments for the function; +/// * an iterable field can also be annotated with `#[css(if_empty = "foo")]` +/// to print `"foo"` if the iterator is empty; +/// * if `#[css(dimension)]` is found on a variant, that variant needs +/// to have a single member. The variant would be serialized as a CSS +/// dimension token, like: <member><identifier>; +/// * if `#[css(skip)]` is found on a field, the `ToCss` call for that field +/// is skipped; +/// * if `#[css(skip_if = "function")]` is found on a field, the `ToCss` call +/// for that field is skipped if `function` returns true. This function is +/// provided the field as an argument; +/// * if `#[css(contextual_skip_if = "function")]` is found on a field, the +/// `ToCss` call for that field is skipped if `function` returns true. This +/// function is given all the fields in the current struct or variant as an +/// argument; +/// * `#[css(represents_keyword)]` can be used on bool fields in order to +/// serialize the field name if the field is true, or nothing otherwise. It +/// also collects those keywords for `SpecifiedValueInfo`. +/// * `#[css(bitflags(single="", mixed="", validate="", overlapping_bits)]` can +/// be used to derive parse / serialize / etc on bitflags. The rules for parsing +/// bitflags are the following: +/// +/// * `single` flags can only appear on their own. It's common that bitflags +/// properties at least have one such value like `none` or `auto`. +/// * `mixed` properties can appear mixed together, but not along any other +/// flag that shares a bit with itself. For example, if you have three +/// bitflags like: +/// +/// FOO = 1 << 0; +/// BAR = 1 << 1; +/// BAZ = 1 << 2; +/// BAZZ = BAR | BAZ; +/// +/// Then the following combinations won't be valid: +/// +/// * foo foo: (every flag shares a bit with itself) +/// * bar bazz: (bazz shares a bit with bar) +/// +/// But `bar baz` will be valid, as they don't share bits, and so would +/// `foo` with any other flag, or `bazz` on its own. +/// * `overlapping_bits` enables some tracking during serialization of mixed +/// flags to avoid serializing variants that can subsume other variants. +/// In the example above, you could do: +/// mixed="foo,bazz,bar,baz", overlapping_bits +/// to ensure that if bazz is serialized, bar and baz aren't, even though +/// their bits are set. Note that the serialization order is canonical, +/// and thus depends on the order you specify the flags in. +/// +/// * finally, one can put `#[css(derive_debug)]` on the whole type, to +/// implement `Debug` by a single call to `ToCss::to_css`. +pub trait ToCss { + /// Serialize `self` in CSS syntax, writing to `dest`. + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write; + + /// Serialize `self` in CSS syntax and return a string. + /// + /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) + #[inline] + fn to_css_string(&self) -> String { + let mut s = String::new(); + self.to_css(&mut CssWriter::new(&mut s)).unwrap(); + s + } + + /// Serialize `self` in CSS syntax and return a nsCString. + /// + /// (This is a convenience wrapper for `to_css` and probably should not be overridden.) + #[inline] + fn to_css_nscstring(&self) -> nsCString { + let mut s = nsCString::new(); + self.to_css(&mut CssWriter::new(&mut s)).unwrap(); + s + } +} + +impl<'a, T> ToCss for &'a T +where + T: ToCss + ?Sized, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + (*self).to_css(dest) + } +} + +impl ToCss for crate::owned_str::OwnedStr { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_string(self, dest) + } +} + +impl ToCss for str { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_string(self, dest) + } +} + +impl ToCss for String { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_string(self, dest) + } +} + +impl<T> ToCss for Option<T> +where + T: ToCss, +{ + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.as_ref().map_or(Ok(()), |value| value.to_css(dest)) + } +} + +impl ToCss for () { + #[inline] + fn to_css<W>(&self, _: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + Ok(()) + } +} + +/// A writer tailored for serialising CSS. +/// +/// Coupled with SequenceWriter, this allows callers to transparently handle +/// things like comma-separated values etc. +pub struct CssWriter<'w, W: 'w> { + inner: &'w mut W, + prefix: Option<&'static str>, +} + +impl<'w, W> CssWriter<'w, W> +where + W: Write, +{ + /// Creates a new `CssWriter`. + #[inline] + pub fn new(inner: &'w mut W) -> Self { + Self { + inner, + prefix: Some(""), + } + } +} + +impl<'w, W> Write for CssWriter<'w, W> +where + W: Write, +{ + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + if s.is_empty() { + return Ok(()); + } + if let Some(prefix) = self.prefix.take() { + // We are going to write things, but first we need to write + // the prefix that was set by `SequenceWriter::item`. + if !prefix.is_empty() { + self.inner.write_str(prefix)?; + } + } + self.inner.write_str(s) + } + + #[inline] + fn write_char(&mut self, c: char) -> fmt::Result { + if let Some(prefix) = self.prefix.take() { + // See comment in `write_str`. + if !prefix.is_empty() { + self.inner.write_str(prefix)?; + } + } + self.inner.write_char(c) + } +} + +/// Convenience wrapper to serialise CSS values separated by a given string. +pub struct SequenceWriter<'a, 'b: 'a, W: 'b> { + inner: &'a mut CssWriter<'b, W>, + separator: &'static str, +} + +impl<'a, 'b, W> SequenceWriter<'a, 'b, W> +where + W: Write + 'b, +{ + /// Create a new sequence writer. + #[inline] + pub fn new(inner: &'a mut CssWriter<'b, W>, separator: &'static str) -> Self { + if inner.prefix.is_none() { + // See comment in `item`. + inner.prefix = Some(""); + } + Self { inner, separator } + } + + #[inline] + fn write_item<F>(&mut self, f: F) -> fmt::Result + where + F: FnOnce(&mut CssWriter<'b, W>) -> fmt::Result, + { + // Separate non-generic functions so that this code is not repeated + // in every monomorphization with a different type `F` or `W`. + // https://github.com/servo/servo/issues/26713 + fn before( + prefix: &mut Option<&'static str>, + separator: &'static str, + ) -> Option<&'static str> { + let old_prefix = *prefix; + if old_prefix.is_none() { + // If there is no prefix in the inner writer, a previous + // call to this method produced output, which means we need + // to write the separator next time we produce output again. + *prefix = Some(separator); + } + old_prefix + } + fn after( + old_prefix: Option<&'static str>, + prefix: &mut Option<&'static str>, + separator: &'static str, + ) { + match (old_prefix, *prefix) { + (_, None) => { + // This call produced output and cleaned up after itself. + }, + (None, Some(p)) => { + // Some previous call to `item` produced output, + // but this one did not, prefix should be the same as + // the one we set. + debug_assert_eq!(separator, p); + // We clean up here even though it's not necessary just + // to be able to do all these assertion checks. + *prefix = None; + }, + (Some(old), Some(new)) => { + // No previous call to `item` produced output, and this one + // either. + debug_assert_eq!(old, new); + }, + } + } + + let old_prefix = before(&mut self.inner.prefix, self.separator); + f(self.inner)?; + after(old_prefix, &mut self.inner.prefix, self.separator); + Ok(()) + } + + /// Serialises a CSS value, writing any separator as necessary. + /// + /// The separator is never written before any `item` produces any output, + /// and is written in subsequent calls only if the `item` produces some + /// output on its own again. This lets us handle `Option<T>` fields by + /// just not printing anything on `None`. + #[inline] + pub fn item<T>(&mut self, item: &T) -> fmt::Result + where + T: ToCss, + { + self.write_item(|inner| item.to_css(inner)) + } + + /// Writes a string as-is (i.e. not escaped or wrapped in quotes) + /// with any separator as necessary. + /// + /// See SequenceWriter::item. + #[inline] + pub fn raw_item(&mut self, item: &str) -> fmt::Result { + self.write_item(|inner| inner.write_str(item)) + } +} + +/// Type used as the associated type in the `OneOrMoreSeparated` trait on a +/// type to indicate that a serialized list of elements of this type is +/// separated by commas. +pub struct Comma; + +/// Type used as the associated type in the `OneOrMoreSeparated` trait on a +/// type to indicate that a serialized list of elements of this type is +/// separated by spaces. +pub struct Space; + +/// Type used as the associated type in the `OneOrMoreSeparated` trait on a +/// type to indicate that a serialized list of elements of this type is +/// separated by commas, but spaces without commas are also allowed when +/// parsing. +pub struct CommaWithSpace; + +/// A trait satisfied by the types corresponding to separators. +pub trait Separator { + /// The separator string that the satisfying separator type corresponds to. + fn separator() -> &'static str; + + /// Parses a sequence of values separated by this separator. + /// + /// The given closure is called repeatedly for each item in the sequence. + /// + /// Successful results are accumulated in a vector. + /// + /// This method returns `Err(_)` the first time a closure does or if + /// the separators aren't correct. + fn parse<'i, 't, F, T, E>( + parser: &mut Parser<'i, 't>, + parse_one: F, + ) -> Result<Vec<T>, ParseError<'i, E>> + where + F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>; +} + +impl Separator for Comma { + fn separator() -> &'static str { + ", " + } + + fn parse<'i, 't, F, T, E>( + input: &mut Parser<'i, 't>, + parse_one: F, + ) -> Result<Vec<T>, ParseError<'i, E>> + where + F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + input.parse_comma_separated(parse_one) + } +} + +impl Separator for Space { + fn separator() -> &'static str { + " " + } + + fn parse<'i, 't, F, T, E>( + input: &mut Parser<'i, 't>, + mut parse_one: F, + ) -> Result<Vec<T>, ParseError<'i, E>> + where + F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less. + let mut results = vec![parse_one(input)?]; + loop { + input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less. + if let Ok(item) = input.try(&mut parse_one) { + results.push(item); + } else { + return Ok(results); + } + } + } +} + +impl Separator for CommaWithSpace { + fn separator() -> &'static str { + ", " + } + + fn parse<'i, 't, F, T, E>( + input: &mut Parser<'i, 't>, + mut parse_one: F, + ) -> Result<Vec<T>, ParseError<'i, E>> + where + F: for<'tt> FnMut(&mut Parser<'i, 'tt>) -> Result<T, ParseError<'i, E>>, + { + input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less. + let mut results = vec![parse_one(input)?]; + loop { + input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less. + let comma_location = input.current_source_location(); + let comma = input.try(|i| i.expect_comma()).is_ok(); + input.skip_whitespace(); // Unnecessary for correctness, but may help try() rewind less. + if let Ok(item) = input.try(&mut parse_one) { + results.push(item); + } else if comma { + return Err(comma_location.new_unexpected_token_error(Token::Comma)); + } else { + break; + } + } + Ok(results) + } +} + +/// Marker trait on T to automatically implement ToCss for Vec<T> when T's are +/// separated by some delimiter `delim`. +pub trait OneOrMoreSeparated { + /// Associated type indicating which separator is used. + type S: Separator; +} + +impl OneOrMoreSeparated for UnicodeRange { + type S = Comma; +} + +impl<T> ToCss for Vec<T> +where + T: ToCss + OneOrMoreSeparated, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let mut iter = self.iter(); + iter.next().unwrap().to_css(dest)?; + for item in iter { + dest.write_str(<T as OneOrMoreSeparated>::S::separator())?; + item.to_css(dest)?; + } + Ok(()) + } +} + +impl<T> ToCss for Box<T> +where + T: ?Sized + ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + (**self).to_css(dest) + } +} + +impl<T> ToCss for Arc<T> +where + T: ?Sized + ToCss, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + (**self).to_css(dest) + } +} + +impl ToCss for Au { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.to_f64_px().to_css(dest)?; + dest.write_str("px") + } +} + +macro_rules! impl_to_css_for_predefined_type { + ($name: ty) => { + impl<'a> ToCss for $name { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + ::cssparser::ToCss::to_css(self, dest) + } + } + }; +} + +impl_to_css_for_predefined_type!(f32); +impl_to_css_for_predefined_type!(i8); +impl_to_css_for_predefined_type!(i32); +impl_to_css_for_predefined_type!(u16); +impl_to_css_for_predefined_type!(u32); +impl_to_css_for_predefined_type!(::cssparser::Token<'a>); +impl_to_css_for_predefined_type!(::cssparser::UnicodeRange); + +/// Helper types for the handling of specified values. +pub mod specified { + use crate::ParsingMode; + + /// Whether to allow negative lengths or not. + #[repr(u8)] + #[derive( + Clone, Copy, Debug, Deserialize, Eq, MallocSizeOf, PartialEq, PartialOrd, Serialize, ToShmem, + )] + pub enum AllowedNumericType { + /// Allow all kind of numeric values. + All, + /// Allow only non-negative numeric values. + NonNegative, + /// Allow only numeric values greater or equal to 1.0. + AtLeastOne, + /// Allow only numeric values from 0 to 1.0. + ZeroToOne, + } + + impl Default for AllowedNumericType { + #[inline] + fn default() -> Self { + AllowedNumericType::All + } + } + + impl AllowedNumericType { + /// Whether the value fits the rules of this numeric type. + #[inline] + pub fn is_ok(&self, parsing_mode: ParsingMode, val: f32) -> bool { + if parsing_mode.allows_all_numeric_values() { + return true; + } + match *self { + AllowedNumericType::All => true, + AllowedNumericType::NonNegative => val >= 0.0, + AllowedNumericType::AtLeastOne => val >= 1.0, + AllowedNumericType::ZeroToOne => val >= 0.0 && val <= 1.0, + } + } + + /// Clamp the value following the rules of this numeric type. + #[inline] + pub fn clamp(&self, val: f32) -> f32 { + match *self { + AllowedNumericType::All => val, + AllowedNumericType::NonNegative => val.max(0.), + AllowedNumericType::AtLeastOne => val.max(1.), + AllowedNumericType::ZeroToOne => val.max(0.).min(1.), + } + } + } +} diff --git a/servo/components/to_shmem/Cargo.toml b/servo/components/to_shmem/Cargo.toml new file mode 100644 index 0000000000..09e78f57d2 --- /dev/null +++ b/servo/components/to_shmem/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "to_shmem" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +[lib] +name = "to_shmem" +path = "lib.rs" + +[features] +servo = ["cssparser/serde", "string_cache"] +gecko = [] + +[dependencies] +cssparser = "0.33" +servo_arc = { path = "../servo_arc" } +smallbitvec = "2.1.1" +smallvec = "1.0" +string_cache = { version = "0.8", optional = true } +thin-vec = { version = "0.2.1", features = ["gecko-ffi"] } diff --git a/servo/components/to_shmem/lib.rs b/servo/components/to_shmem/lib.rs new file mode 100644 index 0000000000..54fee3a420 --- /dev/null +++ b/servo/components/to_shmem/lib.rs @@ -0,0 +1,618 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Trait for cloning data into a shared memory buffer. +//! +//! This module contains the SharedMemoryBuilder type and ToShmem trait. +//! +//! We put them here (and not in style_traits) so that we can derive ToShmem +//! from the selectors and style crates. + +#![crate_name = "to_shmem"] +#![crate_type = "rlib"] + +extern crate cssparser; +extern crate servo_arc; +extern crate smallbitvec; +extern crate smallvec; +#[cfg(feature = "string_cache")] +extern crate string_cache; +extern crate thin_vec; + +use servo_arc::{Arc, ArcUnion, ArcUnionBorrow, HeaderSlice}; +use smallbitvec::{InternalStorage, SmallBitVec}; +use smallvec::{Array, SmallVec}; +use std::alloc::Layout; +use std::collections::HashSet; +use std::ffi::CString; +use std::isize; +use std::marker::PhantomData; +use std::mem::{self, ManuallyDrop}; +use std::num::Wrapping; +use std::ops::Range; +use std::os::raw::c_char; +#[cfg(debug_assertions)] +use std::os::raw::c_void; +use std::ptr::{self, NonNull}; +use std::slice; +use std::str; +use thin_vec::ThinVec; + +/// Result type for ToShmem::to_shmem. +/// +/// The String is an error message describing why the call failed. +pub type Result<T> = std::result::Result<ManuallyDrop<T>, String>; + +// Various pointer arithmetic functions in this file can be replaced with +// functions on `Layout` once they have stabilized: +// +// https://github.com/rust-lang/rust/issues/55724 + +/// A builder object that transforms and copies values into a fixed size buffer. +pub struct SharedMemoryBuilder { + /// The buffer into which values will be copied. + buffer: *mut u8, + /// The size of the buffer. + capacity: usize, + /// The current position in the buffer, where the next value will be written + /// at. + index: usize, + /// Pointers to every shareable value that we store in the shared memory + /// buffer. We use this to assert against encountering the same value + /// twice, e.g. through another Arc reference, so that we don't + /// inadvertently store duplicate copies of values. + #[cfg(debug_assertions)] + shared_values: HashSet<*const c_void>, +} + +/// Amount of padding needed after `size` bytes to ensure that the following +/// address will satisfy `align`. +fn padding_needed_for(size: usize, align: usize) -> usize { + padded_size(size, align).wrapping_sub(size) +} + +/// Rounds up `size` so that the following address will satisfy `align`. +fn padded_size(size: usize, align: usize) -> usize { + size.wrapping_add(align).wrapping_sub(1) & !align.wrapping_sub(1) +} + +impl SharedMemoryBuilder { + /// Creates a new SharedMemoryBuilder using the specified buffer. + pub unsafe fn new(buffer: *mut u8, capacity: usize) -> SharedMemoryBuilder { + SharedMemoryBuilder { + buffer, + capacity, + index: 0, + #[cfg(debug_assertions)] + shared_values: HashSet::new(), + } + } + + /// Returns the number of bytes currently used in the buffer. + #[inline] + pub fn len(&self) -> usize { + self.index + } + + /// Writes a value into the shared memory buffer and returns a pointer to + /// it in the buffer. + /// + /// The value is cloned and converted into a form suitable for placing into + /// a shared memory buffer by calling ToShmem::to_shmem on it. + /// + /// Panics if there is insufficient space in the buffer. + pub fn write<T: ToShmem>(&mut self, value: &T) -> std::result::Result<*mut T, String> { + // Reserve space for the value. + let dest: *mut T = self.alloc_value(); + + // Make a clone of the value with all of its heap allocations + // placed in the shared memory buffer. + let value = value.to_shmem(self)?; + + unsafe { + // Copy the value into the buffer. + ptr::write(dest, ManuallyDrop::into_inner(value)); + } + + // Return a pointer to the shared value. + Ok(dest) + } + + /// Reserves space in the shared memory buffer to fit a value of type T, + /// and returns a pointer to that reserved space. + /// + /// Panics if there is insufficient space in the buffer. + pub fn alloc_value<T>(&mut self) -> *mut T { + self.alloc(Layout::new::<T>()) + } + + /// Reserves space in the shared memory buffer to fit an array of values of + /// type T, and returns a pointer to that reserved space. + /// + /// Panics if there is insufficient space in the buffer. + pub fn alloc_array<T>(&mut self, len: usize) -> *mut T { + if len == 0 { + return NonNull::dangling().as_ptr(); + } + + let size = mem::size_of::<T>(); + let align = mem::align_of::<T>(); + + self.alloc(Layout::from_size_align(padded_size(size, align) * len, align).unwrap()) + } + + /// Reserves space in the shared memory buffer that conforms to the + /// specified layout, and returns a pointer to that reserved space. + /// + /// Panics if there is insufficient space in the buffer. + pub fn alloc<T>(&mut self, layout: Layout) -> *mut T { + // Amount of padding to align the value. + // + // The addition can't overflow, since self.index <= self.capacity, and + // for us to have successfully allocated the buffer, `buffer + capacity` + // can't overflow. + let padding = padding_needed_for(self.buffer as usize + self.index, layout.align()); + + // Reserve space for the padding. + let start = self.index.checked_add(padding).unwrap(); + assert!(start <= std::isize::MAX as usize); // for the cast below + + // Reserve space for the value. + let end = start.checked_add(layout.size()).unwrap(); + assert!(end <= self.capacity); + + self.index = end; + unsafe { self.buffer.add(start) as *mut T } + } +} + +/// A type that can be copied into a SharedMemoryBuilder. +pub trait ToShmem: Sized { + /// Clones this value into a form suitable for writing into a + /// SharedMemoryBuilder. + /// + /// If this value owns any heap allocations, they should be written into + /// `builder` so that the return value of this function can point to the + /// copy in the shared memory buffer. + /// + /// The return type is wrapped in ManuallyDrop to make it harder to + /// accidentally invoke the destructor of the value that is produced. + /// + /// Returns a Result so that we can gracefully recover from unexpected + /// content. + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self>; +} + +#[macro_export] +macro_rules! impl_trivial_to_shmem { + ($($ty:ty),*) => { + $( + impl $crate::ToShmem for $ty { + fn to_shmem( + &self, + _builder: &mut $crate::SharedMemoryBuilder, + ) -> $crate::Result<Self> { + $crate::Result::Ok(::std::mem::ManuallyDrop::new(*self)) + } + } + )* + }; +} + +impl_trivial_to_shmem!( + (), + bool, + f32, + f64, + i8, + i16, + i32, + i64, + u8, + u16, + u32, + u64, + isize, + usize, + std::num::NonZeroUsize +); + +impl_trivial_to_shmem!( + cssparser::SourceLocation, + cssparser::SourcePosition, + cssparser::TokenSerializationType +); + +impl<T> ToShmem for PhantomData<T> { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> Result<Self> { + Ok(ManuallyDrop::new(*self)) + } +} + +impl<T: ToShmem> ToShmem for Range<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + Ok(ManuallyDrop::new(Range { + start: ManuallyDrop::into_inner(self.start.to_shmem(builder)?), + end: ManuallyDrop::into_inner(self.end.to_shmem(builder)?), + })) + } +} + +impl ToShmem for cssparser::UnicodeRange { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> Result<Self> { + Ok(ManuallyDrop::new(cssparser::UnicodeRange { + start: self.start, + end: self.end, + })) + } +} + +impl<T: ToShmem, U: ToShmem> ToShmem for (T, U) { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + Ok(ManuallyDrop::new(( + ManuallyDrop::into_inner(self.0.to_shmem(builder)?), + ManuallyDrop::into_inner(self.1.to_shmem(builder)?), + ))) + } +} + +impl<T: ToShmem> ToShmem for Wrapping<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + Ok(ManuallyDrop::new(Wrapping(ManuallyDrop::into_inner( + self.0.to_shmem(builder)?, + )))) + } +} + +impl<T: ToShmem> ToShmem for Box<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + // Reserve space for the boxed value. + let dest: *mut T = builder.alloc_value(); + + // Make a clone of the boxed value with all of its heap allocations + // placed in the shared memory buffer. + let value = (**self).to_shmem(builder)?; + + unsafe { + // Copy the value into the buffer. + ptr::write(dest, ManuallyDrop::into_inner(value)); + + Ok(ManuallyDrop::new(Box::from_raw(dest))) + } + } +} + +/// Converts all the items in `src` into shared memory form, writes them into +/// the specified buffer, and returns a pointer to the slice. +unsafe fn to_shmem_slice_ptr<'a, T, I>( + src: I, + dest: *mut T, + builder: &mut SharedMemoryBuilder, +) -> std::result::Result<*mut [T], String> +where + T: 'a + ToShmem, + I: ExactSizeIterator<Item = &'a T>, +{ + let dest = slice::from_raw_parts_mut(dest, src.len()); + + // Make a clone of each element from the iterator with its own heap + // allocations placed in the buffer, and copy that clone into the buffer. + for (src, dest) in src.zip(dest.iter_mut()) { + ptr::write(dest, ManuallyDrop::into_inner(src.to_shmem(builder)?)); + } + + Ok(dest) +} + +/// Writes all the items in `src` into a slice in the shared memory buffer and +/// returns a pointer to the slice. +pub unsafe fn to_shmem_slice<'a, T, I>( + src: I, + builder: &mut SharedMemoryBuilder, +) -> std::result::Result<*mut [T], String> +where + T: 'a + ToShmem, + I: ExactSizeIterator<Item = &'a T>, +{ + let dest = builder.alloc_array(src.len()); + to_shmem_slice_ptr(src, dest, builder) +} + +impl<T: ToShmem> ToShmem for Box<[T]> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + unsafe { + let dest = to_shmem_slice(self.iter(), builder)?; + Ok(ManuallyDrop::new(Box::from_raw(dest))) + } + } +} + +impl ToShmem for Box<str> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + // Reserve space for the string bytes. + let dest: *mut u8 = builder.alloc_array(self.len()); + + unsafe { + // Copy the value into the buffer. + ptr::copy(self.as_ptr(), dest, self.len()); + + Ok(ManuallyDrop::new(Box::from_raw( + str::from_utf8_unchecked_mut(slice::from_raw_parts_mut(dest, self.len())), + ))) + } + } +} + +impl ToShmem for String { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + // Reserve space for the string bytes. + let dest: *mut u8 = builder.alloc_array(self.len()); + + unsafe { + // Copy the value into the buffer. + ptr::copy(self.as_ptr(), dest, self.len()); + + Ok(ManuallyDrop::new(String::from_raw_parts( + dest, + self.len(), + self.len(), + ))) + } + } +} + +impl ToShmem for CString { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + let len = self.as_bytes_with_nul().len(); + + // Reserve space for the string bytes. + let dest: *mut c_char = builder.alloc_array(len); + + unsafe { + // Copy the value into the buffer. + ptr::copy(self.as_ptr(), dest, len); + + Ok(ManuallyDrop::new(CString::from_raw(dest))) + } + } +} + +impl<T: ToShmem> ToShmem for Vec<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + unsafe { + let dest = to_shmem_slice(self.iter(), builder)? as *mut T; + let dest_vec = Vec::from_raw_parts(dest, self.len(), self.len()); + Ok(ManuallyDrop::new(dest_vec)) + } + } +} + +impl<T: ToShmem, A: Array<Item = T>> ToShmem for SmallVec<A> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + let dest_vec = unsafe { + if self.spilled() { + // Place the items in a separate allocation in the shared memory + // buffer. + let dest = to_shmem_slice(self.iter(), builder)? as *mut T; + SmallVec::from_raw_parts(dest, self.len(), self.len()) + } else { + // Place the items inline. + let mut s = SmallVec::new(); + to_shmem_slice_ptr(self.iter(), s.as_mut_ptr(), builder)?; + s.set_len(self.len()); + s + } + }; + + Ok(ManuallyDrop::new(dest_vec)) + } +} + +impl<T: ToShmem> ToShmem for Option<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + let v = match self { + Some(v) => Some(ManuallyDrop::into_inner(v.to_shmem(builder)?)), + None => None, + }; + + Ok(ManuallyDrop::new(v)) + } +} + +impl<T: ToShmem, S> ToShmem for HashSet<T, S> +where + Self: Default, +{ + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> Result<Self> { + if !self.is_empty() { + return Err(format!( + "ToShmem failed for HashSet: We only support empty sets \ + (we don't expect custom properties in UA sheets, they're observable by content)", + )); + } + Ok(ManuallyDrop::new(Self::default())) + } +} + +impl<A: 'static, B: 'static> ToShmem for ArcUnion<A, B> +where + Arc<A>: ToShmem, + Arc<B>: ToShmem, +{ + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + Ok(ManuallyDrop::new(match self.borrow() { + ArcUnionBorrow::First(first) => Self::from_first(ManuallyDrop::into_inner( + first.with_arc(|a| a.to_shmem(builder))?, + )), + ArcUnionBorrow::Second(second) => Self::from_second(ManuallyDrop::into_inner( + second.with_arc(|a| a.to_shmem(builder))?, + )), + })) + } +} + +impl<T: ToShmem> ToShmem for Arc<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + // Assert that we don't encounter any shared references to values we + // don't expect. + #[cfg(debug_assertions)] + assert!( + !builder.shared_values.contains(&self.heap_ptr()), + "ToShmem failed for Arc<{}>: encountered a value with multiple \ + references.", + std::any::type_name::<T>() + ); + + // Make a clone of the Arc-owned value with all of its heap allocations + // placed in the shared memory buffer. + let value = (**self).to_shmem(builder)?; + + // Create a new Arc with the shared value and have it place its + // ArcInner in the shared memory buffer. + unsafe { + let static_arc = Arc::new_static( + |layout| builder.alloc(layout), + ManuallyDrop::into_inner(value), + ); + + #[cfg(debug_assertions)] + builder.shared_values.insert(self.heap_ptr()); + + Ok(ManuallyDrop::new(static_arc)) + } + } +} + +impl<H: ToShmem, T: ToShmem> ToShmem for Arc<HeaderSlice<H, T>> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + // We don't currently have any shared ThinArc values in stylesheets, + // so don't support them for now. + #[cfg(debug_assertions)] + assert!( + !builder.shared_values.contains(&self.heap_ptr()), + "ToShmem failed for ThinArc<T>: encountered a value with multiple references, which \ + is not currently supported", + ); + + // Make a clone of the Arc-owned header and slice values with all of + // their heap allocations placed in the shared memory buffer. + let header = self.header.to_shmem(builder)?; + let mut values = Vec::with_capacity(self.len()); + for v in self.slice().iter() { + values.push(v.to_shmem(builder)?); + } + + // Create a new ThinArc with the shared value and have it place + // its ArcInner in the shared memory buffer. + let len = values.len(); + let static_arc = Self::from_header_and_iter_alloc( + |layout| builder.alloc(layout), + ManuallyDrop::into_inner(header), + values.into_iter().map(ManuallyDrop::into_inner), + len, + /* is_static = */ true, + ); + + #[cfg(debug_assertions)] + builder.shared_values.insert(self.heap_ptr()); + + Ok(ManuallyDrop::new(static_arc)) + } +} + +impl<T: ToShmem> ToShmem for ThinVec<T> { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + assert_eq!(mem::size_of::<Self>(), mem::size_of::<*const ()>()); + + // NOTE: We need to do the work of allocating the header in shared memory even if the + // length is zero, because an empty ThinVec, even though it doesn't allocate, references + // static memory which will not be mapped to other processes, see bug 1841011. + let len = self.len(); + + // nsTArrayHeader size. + // FIXME: Would be nice not to hard-code this, but in practice thin-vec crate also relies + // on this. + let header_size = 2 * mem::size_of::<u32>(); + let header_align = mem::size_of::<u32>(); + + let item_size = mem::size_of::<T>(); + let item_align = mem::align_of::<T>(); + + // We don't need to support underalignment for now, this could be supported if needed. + assert!(item_align >= header_align); + + // This is explicitly unsupported by ThinVec, see: + // https://searchfox.org/mozilla-central/rev/ad732108b073742d7324f998c085f459674a6846/third_party/rust/thin-vec/src/lib.rs#375-386 + assert!(item_align <= header_size); + let header_padding = 0; + + let layout = Layout::from_size_align( + header_size + header_padding + padded_size(item_size, item_align) * len, + item_align, + ) + .unwrap(); + + let shmem_header_ptr = builder.alloc::<u8>(layout); + let shmem_data_ptr = unsafe { shmem_header_ptr.add(header_size + header_padding) }; + + let data_ptr = self.as_ptr() as *const T as *const u8; + let header_ptr = unsafe { data_ptr.sub(header_size + header_padding) }; + + unsafe { + // Copy the header. Note this might copy a wrong capacity, but it doesn't matter, + // because shared memory ptrs are immutable anyways, and we can't relocate. + ptr::copy(header_ptr, shmem_header_ptr, header_size); + // ToShmem + copy the contents into the shared buffer. + to_shmem_slice_ptr(self.iter(), shmem_data_ptr as *mut T, builder)?; + // Return the new ThinVec, which is just a pointer to the shared memory buffer. + let shmem_thinvec: Self = mem::transmute(shmem_header_ptr); + + // Sanity-check that the ptr and length match. + debug_assert_eq!(shmem_thinvec.as_ptr(), shmem_data_ptr as *const T); + debug_assert_eq!(shmem_thinvec.len(), len); + + Ok(ManuallyDrop::new(shmem_thinvec)) + } + } +} + +impl ToShmem for SmallBitVec { + fn to_shmem(&self, builder: &mut SharedMemoryBuilder) -> Result<Self> { + let storage = match self.clone().into_storage() { + InternalStorage::Spilled(vs) => { + // Reserve space for the boxed slice values. + let len = vs.len(); + let dest: *mut usize = builder.alloc_array(len); + + unsafe { + // Copy the value into the buffer. + let src = vs.as_ptr() as *const usize; + ptr::copy(src, dest, len); + + let dest_slice = + Box::from_raw(slice::from_raw_parts_mut(dest, len) as *mut [usize]); + InternalStorage::Spilled(dest_slice) + } + }, + InternalStorage::Inline(x) => InternalStorage::Inline(x), + }; + Ok(ManuallyDrop::new(unsafe { + SmallBitVec::from_storage(storage) + })) + } +} + +#[cfg(feature = "string_cache")] +impl<Static: string_cache::StaticAtomSet> ToShmem for string_cache::Atom<Static> { + fn to_shmem(&self, _: &mut SharedMemoryBuilder) -> Result<Self> { + // NOTE(emilio): In practice, this can be implemented trivially if + // string_cache could expose the implementation detail of static atoms + // being an index into the static table (and panicking in the + // non-static, non-inline cases). + unimplemented!( + "If servo wants to share stylesheets across processes, \ + then ToShmem for Atom needs to be implemented" + ) + } +} diff --git a/servo/components/to_shmem_derive/Cargo.toml b/servo/components/to_shmem_derive/Cargo.toml new file mode 100644 index 0000000000..a3fd73249e --- /dev/null +++ b/servo/components/to_shmem_derive/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "to_shmem_derive" +version = "0.0.1" +authors = ["The Servo Project Developers"] +license = "MPL-2.0" +publish = false + +[lib] +path = "lib.rs" +proc-macro = true + +[dependencies] +darling = { version = "0.20", default-features = false } +derive_common = { path = "../derive_common" } +proc-macro2 = "1" +quote = "1" +syn = { version = "2", default-features = false, features = ["derive", "parsing"] } +synstructure = "0.13" diff --git a/servo/components/to_shmem_derive/lib.rs b/servo/components/to_shmem_derive/lib.rs new file mode 100644 index 0000000000..b820e7f85d --- /dev/null +++ b/servo/components/to_shmem_derive/lib.rs @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![recursion_limit = "128"] + +#[macro_use] +extern crate darling; +extern crate derive_common; +extern crate proc_macro; +extern crate proc_macro2; +#[macro_use] +extern crate quote; +#[macro_use] +extern crate syn; +extern crate synstructure; + +use proc_macro::TokenStream; + +mod to_shmem; + +#[proc_macro_derive(ToShmem, attributes(shmem))] +pub fn derive_to_shmem(stream: TokenStream) -> TokenStream { + let input = syn::parse(stream).unwrap(); + to_shmem::derive(input).into() +} diff --git a/servo/components/to_shmem_derive/to_shmem.rs b/servo/components/to_shmem_derive/to_shmem.rs new file mode 100644 index 0000000000..157730c5a5 --- /dev/null +++ b/servo/components/to_shmem_derive/to_shmem.rs @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use derive_common::cg; +use proc_macro2::TokenStream; +use syn; +use synstructure::{BindStyle, Structure}; + +pub fn derive(mut input: syn::DeriveInput) -> TokenStream { + let mut where_clause = input.generics.where_clause.take(); + let attrs = cg::parse_input_attrs::<ShmemInputAttrs>(&input); + if !attrs.no_bounds { + for param in input.generics.type_params() { + cg::add_predicate(&mut where_clause, parse_quote!(#param: ::to_shmem::ToShmem)); + } + } + for variant in Structure::new(&input).variants() { + for binding in variant.bindings() { + let attrs = cg::parse_field_attrs::<ShmemFieldAttrs>(&binding.ast()); + if attrs.field_bound { + let ty = &binding.ast().ty; + cg::add_predicate(&mut where_clause, parse_quote!(#ty: ::to_shmem::ToShmem)) + } + } + } + + input.generics.where_clause = where_clause; + + // Do all of the `to_shmem()?` calls before the `ManuallyDrop::into_inner()` + // calls, so that we don't drop a value in the shared memory buffer if one + // of the `to_shmem`s fails. + let match_body = cg::fmap2_match( + &input, + BindStyle::Ref, + |binding| { + quote! { + ::to_shmem::ToShmem::to_shmem(#binding, builder)? + } + }, + |binding| { + Some(quote! { + ::std::mem::ManuallyDrop::into_inner(#binding) + }) + }, + ); + + let name = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + quote! { + impl #impl_generics ::to_shmem::ToShmem for #name #ty_generics #where_clause { + #[allow(unused_variables, unreachable_code)] + fn to_shmem( + &self, + builder: &mut ::to_shmem::SharedMemoryBuilder, + ) -> ::to_shmem::Result<Self> { + Ok(::std::mem::ManuallyDrop::new( + match *self { + #match_body + } + )) + } + } + } +} + +#[derive(Default, FromDeriveInput)] +#[darling(attributes(shmem), default)] +pub struct ShmemInputAttrs { + pub no_bounds: bool, +} + +#[derive(Default, FromField)] +#[darling(attributes(shmem), default)] +pub struct ShmemFieldAttrs { + pub field_bound: bool, +} |