diff options
Diffstat (limited to '')
101 files changed, 32684 insertions, 0 deletions
diff --git a/servo/components/style/values/animated/color.rs b/servo/components/style/values/animated/color.rs new file mode 100644 index 0000000000..9f4fa5c52b --- /dev/null +++ b/servo/components/style/values/animated/color.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/. */ + +//! Animated types for CSS colors. + +use crate::values::animated::{Animate, Procedure, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::color::{Color as GenericColor, ComplexColorRatios}; + +/// An animated RGBA color. +/// +/// Unlike in computed values, each component value may exceed the +/// range `[0.0, 1.0]`. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedZero)] +pub struct RGBA { + /// The red component. + pub red: f32, + /// The green component. + pub green: f32, + /// The blue component. + pub blue: f32, + /// The alpha component. + pub alpha: f32, +} + +impl RGBA { + /// Returns a transparent color. + #[inline] + pub fn transparent() -> Self { + Self::new(0., 0., 0., 0.) + } + + /// Returns a new color. + #[inline] + pub fn new(red: f32, green: f32, blue: f32, alpha: f32) -> Self { + RGBA { + red: red, + green: green, + blue: blue, + alpha: alpha, + } + } +} + +/// Unlike Animate for computed colors, we don't clamp any component values. +/// +/// FIXME(nox): Why do computed colors even implement Animate? +impl Animate for RGBA { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + let mut alpha = self.alpha.animate(&other.alpha, procedure)?; + if alpha <= 0. { + // Ideally we should return color value that only alpha component is + // 0, but this is what current gecko does. + return Ok(RGBA::transparent()); + } + + alpha = alpha.min(1.); + let red = + (self.red * self.alpha).animate(&(other.red * other.alpha), procedure)? * 1. / alpha; + let green = (self.green * self.alpha).animate(&(other.green * other.alpha), procedure)? * + 1. / + alpha; + let blue = + (self.blue * self.alpha).animate(&(other.blue * other.alpha), procedure)? * 1. / alpha; + + Ok(RGBA::new(red, green, blue, alpha)) + } +} + +impl ComputeSquaredDistance for RGBA { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + let start = [ + self.alpha, + self.red * self.alpha, + self.green * self.alpha, + self.blue * self.alpha, + ]; + let end = [ + other.alpha, + other.red * other.alpha, + other.green * other.alpha, + other.blue * other.alpha, + ]; + start + .iter() + .zip(&end) + .map(|(this, other)| this.compute_squared_distance(other)) + .sum() + } +} + +/// An animated value for `<color>`. +pub type Color = GenericColor<RGBA>; + +impl Color { + fn effective_intermediate_rgba(&self) -> RGBA { + match *self { + GenericColor::Numeric(color) => color, + GenericColor::CurrentColor => RGBA::transparent(), + GenericColor::Complex { color, ratios } => RGBA { + alpha: color.alpha * ratios.bg, + ..color.clone() + }, + } + } + + fn effective_ratios(&self) -> ComplexColorRatios { + match *self { + GenericColor::Numeric(..) => ComplexColorRatios::NUMERIC, + GenericColor::CurrentColor => ComplexColorRatios::CURRENT_COLOR, + GenericColor::Complex { ratios, .. } => ratios, + } + } +} + +impl Animate for Color { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use self::GenericColor::*; + + // Common cases are interpolating between two numeric colors, + // two currentcolors, and a numeric color and a currentcolor. + let (this_weight, other_weight) = procedure.weights(); + + Ok(match (*self, *other, procedure) { + // Any interpolation of currentcolor with currentcolor returns currentcolor. + (CurrentColor, CurrentColor, Procedure::Interpolate { .. }) => CurrentColor, + // Animating two numeric colors. + (Numeric(c1), Numeric(c2), _) => Numeric(c1.animate(&c2, procedure)?), + // Combinations of numeric color and currentcolor + (CurrentColor, Numeric(color), _) => Self::with_ratios( + color, + ComplexColorRatios { + bg: other_weight as f32, + fg: this_weight as f32, + }, + ), + (Numeric(color), CurrentColor, _) => Self::with_ratios( + color, + ComplexColorRatios { + bg: this_weight as f32, + fg: other_weight as f32, + }, + ), + + // Any other animation of currentcolor with currentcolor. + (CurrentColor, CurrentColor, _) => Self::with_ratios( + RGBA::transparent(), + ComplexColorRatios { + bg: 0., + fg: (this_weight + other_weight) as f32, + }, + ), + + // Defer to complex calculations + _ => { + // Compute the "scaled" contribution for `color`. + fn scaled_rgba(color: &Color) -> RGBA { + match *color { + GenericColor::Numeric(color) => color, + GenericColor::CurrentColor => RGBA::transparent(), + GenericColor::Complex { color, ratios } => RGBA { + red: color.red * ratios.bg, + green: color.green * ratios.bg, + blue: color.blue * ratios.bg, + alpha: color.alpha * ratios.bg, + }, + } + } + + // Each `Color`, represents a complex combination of foreground color and + // background color where fg and bg represent the overall + // contributions. ie: + // + // color = { bg * mColor, fg * foreground } + // = { bg_color , fg_color } + // = bg_color + fg_color + // + // where `foreground` is `currentcolor`, and `bg_color`, + // `fg_color` are the scaled background and foreground + // contributions. + // + // Each operation, lerp, addition, or accumulate, can be + // represented as a scaled-addition each complex color. ie: + // + // p * col1 + q * col2 + // + // where p = (1 - a), q = a for lerp(a), p = 1, q = 1 for + // addition, etc. + // + // Therefore: + // + // col1 op col2 + // = p * col1 + q * col2 + // = p * { bg_color1, fg_color1 } + q * { bg_color2, fg_color2 } + // = p * (bg_color1 + fg_color1) + q * (bg_color2 + fg_color2) + // = p * bg_color1 + p * fg_color1 + q * bg_color2 + p * fg_color2 + // = (p * bg_color1 + q * bg_color2) + (p * fg_color1 + q * fg_color2) + // = (bg_color1 op bg_color2) + (fg_color1 op fg_color2) + // + // fg_color1 op fg_color2 is equivalent to (fg1 op fg2) * foreground, + // so the final color is: + // + // = { bg_color, fg_color } + // = { 1 * (bg_color1 op bg_color2), (fg1 op fg2) * foreground } + + // To perform the operation on two complex colors, we need to + // generate the scaled contributions of each background color + // component. + let bg_color1 = scaled_rgba(self); + let bg_color2 = scaled_rgba(other); + // Perform bg_color1 op bg_color2 + let bg_color = bg_color1.animate(&bg_color2, procedure)?; + + // Calculate the final foreground color ratios; perform + // animation on effective fg ratios. + let ComplexColorRatios { fg: fg1, .. } = self.effective_ratios(); + let ComplexColorRatios { fg: fg2, .. } = other.effective_ratios(); + // Perform fg1 op fg2 + let fg = fg1.animate(&fg2, procedure)?; + + Self::with_ratios(bg_color, ComplexColorRatios { bg: 1., fg }) + }, + }) + } +} + +impl ComputeSquaredDistance for Color { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + use self::GenericColor::*; + + // All comments from the Animate impl also applies here. + Ok(match (*self, *other) { + (CurrentColor, CurrentColor) => SquaredDistance::from_sqrt(0.), + (Numeric(c1), Numeric(c2)) => c1.compute_squared_distance(&c2)?, + (CurrentColor, Numeric(color)) | (Numeric(color), CurrentColor) => { + // `computed_squared_distance` is symmetric. + color.compute_squared_distance(&RGBA::transparent())? + + SquaredDistance::from_sqrt(1.) + }, + (_, _) => { + let self_color = self.effective_intermediate_rgba(); + let other_color = other.effective_intermediate_rgba(); + let self_ratios = self.effective_ratios(); + let other_ratios = other.effective_ratios(); + + self_color.compute_squared_distance(&other_color)? + + self_ratios.bg.compute_squared_distance(&other_ratios.bg)? + + self_ratios.fg.compute_squared_distance(&other_ratios.fg)? + }, + }) + } +} + +impl ToAnimatedZero for Color { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(RGBA::transparent().into()) + } +} 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..a3eb1fe1e1 --- /dev/null +++ b/servo/components/style/values/animated/font.rs @@ -0,0 +1,158 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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, FontWeight}; +use crate::values::computed::Number; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::font::{FontSettings as GenericFontSettings, FontTag, VariationValue}; + +impl ToAnimatedZero for FontWeight { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Ok(FontWeight::normal()) + } +} + +/// <https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def> +impl Animate for FontVariationSettings { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + FontSettingTagIter::new(self, other)? + .map(|r| r.and_then(|(st, ot)| st.animate(&ot, procedure))) + .collect::<Result<Vec<ComputedVariationValue>, ()>>() + .map(|v| GenericFontSettings(v.into_boxed_slice())) + } +} + +impl ComputeSquaredDistance for FontVariationSettings { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + FontSettingTagIter::new(self, other)? + .map(|r| r.and_then(|(st, ot)| st.compute_squared_distance(&ot))) + .sum() + } +} + +impl ToAnimatedZero for FontVariationSettings { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +type ComputedVariationValue = VariationValue<Number>; + +// FIXME: Could do a rename, this is only used for font variations. +struct FontSettingTagIterState<'a> { + tags: Vec<&'a ComputedVariationValue>, + index: usize, + prev_tag: FontTag, +} + +impl<'a> FontSettingTagIterState<'a> { + fn new(tags: Vec<&'a ComputedVariationValue>) -> FontSettingTagIterState<'a> { + FontSettingTagIterState { + index: tags.len(), + tags, + prev_tag: FontTag(0), + } + } +} + +/// Iterator for font-variation-settings tag lists +/// +/// [CSS fonts level 4](https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-variation-settings) +/// defines the animation of font-variation-settings as follows: +/// +/// Two declarations of font-feature-settings[sic] can be animated between if +/// they are "like". "Like" declarations are ones where the same set of +/// properties appear (in any order). Because succesive[sic] duplicate +/// properties are applied instead of prior duplicate properties, two +/// declarations can be "like" even if they have differing number of +/// properties. If two declarations are "like" then animation occurs pairwise +/// between corresponding values in the declarations. +/// +/// In other words if we have the following lists: +/// +/// "wght" 1.4, "wdth" 5, "wght" 2 +/// "wdth" 8, "wght" 4, "wdth" 10 +/// +/// We should animate between: +/// +/// "wdth" 5, "wght" 2 +/// "wght" 4, "wdth" 10 +/// +/// This iterator supports this by sorting the two lists, then iterating them in +/// reverse, and skipping entries with repeated tag names. It will return +/// Some(Err()) if it reaches the end of one list before the other, or if the +/// tag names do not match. +/// +/// For the above example, this iterator would return: +/// +/// Some(Ok("wght" 2, "wght" 4)) +/// Some(Ok("wdth" 5, "wdth" 10)) +/// None +/// +struct FontSettingTagIter<'a> { + a_state: FontSettingTagIterState<'a>, + b_state: FontSettingTagIterState<'a>, +} + +impl<'a> FontSettingTagIter<'a> { + fn new( + a_settings: &'a FontVariationSettings, + b_settings: &'a FontVariationSettings, + ) -> Result<FontSettingTagIter<'a>, ()> { + if a_settings.0.is_empty() || b_settings.0.is_empty() { + return Err(()); + } + + fn as_new_sorted_tags(tags: &[ComputedVariationValue]) -> Vec<&ComputedVariationValue> { + use std::iter::FromIterator; + let mut sorted_tags = Vec::from_iter(tags.iter()); + sorted_tags.sort_by_key(|k| k.tag.0); + sorted_tags + }; + + Ok(FontSettingTagIter { + a_state: FontSettingTagIterState::new(as_new_sorted_tags(&a_settings.0)), + b_state: FontSettingTagIterState::new(as_new_sorted_tags(&b_settings.0)), + }) + } + + fn next_tag(state: &mut FontSettingTagIterState<'a>) -> Option<&'a ComputedVariationValue> { + if state.index == 0 { + return None; + } + + state.index -= 1; + let tag = state.tags[state.index]; + if tag.tag == state.prev_tag { + FontSettingTagIter::next_tag(state) + } else { + state.prev_tag = tag.tag; + Some(tag) + } + } +} + +impl<'a> Iterator for FontSettingTagIter<'a> { + type Item = Result<(&'a ComputedVariationValue, &'a ComputedVariationValue), ()>; + + fn next( + &mut self, + ) -> Option<Result<(&'a ComputedVariationValue, &'a ComputedVariationValue), ()>> { + match ( + FontSettingTagIter::next_tag(&mut self.a_state), + FontSettingTagIter::next_tag(&mut self.b_state), + ) { + (Some(at), Some(bt)) if at.tag == bt.tag => Some(Ok((at, bt))), + (None, None) => None, + _ => Some(Err(())), // Mismatch number of unique tags or tag names. + } + } +} diff --git a/servo/components/style/values/animated/grid.rs b/servo/components/style/values/animated/grid.rs new file mode 100644 index 0000000000..7b9417a08c --- /dev/null +++ b/servo/components/style/values/animated/grid.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/. */ + +//! 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 { + Err(()) + } +} + +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(()), + } + + // The length of track_sizes should be matched. + if self.track_sizes.len() != other.track_sizes.len() { + return Err(()); + } + + let count = self.count; + let track_sizes = self + .track_sizes + .iter() + .zip(other.track_sizes.iter()) + .map(|(a, b)| a.animate(b, procedure)) + .collect::<Result<Vec<_>, _>>()?; + + // 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: track_sizes.into(), + }) + } +} + +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 = self + .values + .iter() + .zip(other.values.iter()) + .map(|(a, b)| a.animate(b, procedure)) + .collect::<Result<Vec<_>, _>>()?; + // 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: values.into(), + 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/mod.rs b/servo/components/style/values/animated/mod.rs new file mode 100644 index 0000000000..226c01a9a4 --- /dev/null +++ b/servo/components/style/values/animated/mod.rs @@ -0,0 +1,488 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::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; +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::Shorthand(..) | PropertyId::ShorthandAlias(..) => { + PropertyCategory::Shorthand + }, + PropertyId::Longhand(id) | PropertyId::LonghandAlias(id, ..) => { + if id.is_logical() { + PropertyCategory::LogicalLonghand + } else { + PropertyCategory::PhysicalLonghand + } + }, + 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, ()> { + use std::f32; + + 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, ()> { + use std::f64; + + 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); +// 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..13ad10174b --- /dev/null +++ b/servo/components/style/values/animated/svg.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/. */ + +//! Animation implementations for various SVG-related types. + +use super::{Animate, Procedure}; +use crate::properties::animated_properties::ListAnimation; +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(this.animate_repeatable_list(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)) => { + this.squared_distance_repeatable_list(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..621ae60084 --- /dev/null +++ b/servo/components/style/values/animated/transform.rs @@ -0,0 +1,1456 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::properties::animated_properties::ListAnimation; +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; + +// ------------------------------------ +// 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); + Ok(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 from = decompose_2d_matrix(&(*self).into()); + let to = decompose_2d_matrix(&(*other).into()); + match (from, to) { + (Ok(from), Ok(to)) => Matrix3D::from(from.animate(&to, procedure)?).into_2d(), + // 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(()), + } + } +} + +/// 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" + ); + // 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 * (angle / 2.).sin(), + vector.y as f64 * (angle / 2.).sin(), + vector.z as f64 * (angle / 2.).sin(), + (angle / 2.).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 Animate for Quaternion { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + use std::f64; + + 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, + )); + } + + // Straight from gfxQuaternion::Slerp. + // + // Dot product, clamped between -1 and 1. + let dot = (self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3) + .min(1.0) + .max(-1.0); + + if dot.abs() == 1.0 { + return Ok(*self); + } + + let theta = dot.acos(); + let rsintheta = 1.0 / (1.0 - dot * dot).sqrt(); + + let right_weight = (other_weight * theta).sin() * rsintheta; + let left_weight = (other_weight * theta).cos() - dot * right_weight; + + let left = self.scale(left_weight); + let right = other.scale(right_weight); + + Ok(Quaternion( + left.0 + right.0, + left.1 + right.1, + left.2 + right.2, + left.3 + right.3, + )) + } +} + +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, + }) +} + +/// Decompose a 2D matrix for Gecko. +// Use the algorithm from nsStyleTransformMatrix::Decompose2DMatrix() in Gecko. +#[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)) + }; + match (from, to) { + (Ok(from), Ok(to)) => Ok(Matrix3D::from(from.animate(&to, 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(()), + } + } +} + +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)) => match procedure { + Procedure::Add => { + debug_assert!(false, "Should have already dealt with add by the point"); + return Err(()); + }, + Procedure::Interpolate { progress } => { + result.push(TransformOperation::InterpolateMatrix { + from_list: Transform(this_remainder.to_vec().into()), + to_list: Transform(other_remainder.to_vec().into()), + progress: Percentage(progress as f32), + }); + }, + Procedure::Accumulate { count } => { + result.push(TransformOperation::AccumulateMatrix { + from_list: Transform(this_remainder.to_vec().into()), + to_list: Transform(other_remainder.to_vec().into()), + count: cmp::min(count, i32::max_value() as u64) as i32, + }); + }, + }, + // 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 { + // We can't interpolate/accumulate ___Matrix types directly with a + // matrix. Instead we need to wrap it in another ___Matrix type. + TransformOperation::AccumulateMatrix { .. } | + TransformOperation::InterpolateMatrix { .. } => { + let transform_list = Transform(vec![transform.clone()].into()); + let identity_list = Transform(vec![identity].into()); + let (from_list, to_list) = if fill_right { + (transform_list, identity_list) + } else { + (identity_list, transform_list) + }; + + match procedure { + Procedure::Add => Err(()), + Procedure::Interpolate { progress } => { + Ok(TransformOperation::InterpolateMatrix { + from_list, + to_list, + progress: Percentage(progress as f32), + }) + }, + Procedure::Accumulate { count } => { + Ok(TransformOperation::AccumulateMatrix { + from_list, + to_list, + count: cmp::min(count, i32::max_value() as u64) + as i32, + }) + }, + } + }, + _ => { + 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 = self.0.squared_distance_with_zero(&other.0); + + // Roll back to matrix interpolation if there is any Err(()) in the + // transform lists, such as mismatched transform functions. + if squared_dist.is_err() { + let matrix1: Matrix3D = self.to_transform_3d_matrix(None)?.0.into(); + let matrix2: Matrix3D = other.to_transform_3d_matrix(None)?.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.px()); + let to = create_perspective_matrix(td.px()); + + let interpolated = Matrix3D::from(from).animate(&Matrix3D::from(to), procedure)?; + + let decomposed = decompose_3d_matrix(interpolated)?; + let perspective_z = decomposed.perspective.2; + let used_value = if perspective_z == 0. { + 0. + } else { + -1. / perspective_z + }; + Ok(TransformOperation::Perspective(CSSPixelLength::new( + 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(()), + } + } +} + +// 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.compute_squared_distance(td), + (&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(); + if p.px() > 0. { + p_matrix.m34 = -1. / p.px(); + } + 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, ()> { + 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(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + let (mut fx, mut fy, mut fz, fa) = + transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); + let (mut tx, mut ty, mut tz, ta) = + transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3); + + if fa == Angle::from_degrees(0.) { + fx = tx; + fy = ty; + fz = tz; + } else if ta == Angle::from_degrees(0.) { + tx = fx; + ty = fy; + tz = fz; + } + + if (fx, fy, fz) == (tx, ty, tz) { + return Ok(Rotate::Rotate3D(fx, fy, fz, fa.animate(&ta, procedure)?)); + } + + let fv = DirectionVector::new(fx, fy, fz); + let tv = DirectionVector::new(tx, ty, tz); + let fq = Quaternion::from_direction_and_angle(&fv, fa.radians64()); + let tq = Quaternion::from_direction_and_angle(&tv, ta.radians64()); + + let rq = Quaternion::animate(&fq, &tq, procedure)?; + 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, ()> { + 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 == Angle::zero() { + fx = tx; + fy = ty; + fz = tz; + } else if angle2 == Angle::zero() { + tx = fx; + ty = fy; + tz = fz; + } + + if (fx, fy, fz) == (tx, ty, tz) { + angle1.compute_squared_distance(&angle2) + } else { + let v1 = DirectionVector::new(fx, fy, fz); + let v2 = DirectionVector::new(tx, ty, tz); + 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/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..fa30220157 --- /dev/null +++ b/servo/components/style/values/computed/basic_shape.rs @@ -0,0 +1,42 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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}; +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< + LengthPercentage, + LengthPercentage, + LengthPercentage, + NonNegativeLengthPercentage, +>; + +/// The computed value of `inset()` +pub type InsetRect = generic::InsetRect<LengthPercentage, NonNegativeLengthPercentage>; + +/// A computed circle. +pub type Circle = generic::Circle<LengthPercentage, LengthPercentage, NonNegativeLengthPercentage>; + +/// A computed ellipse. +pub type Ellipse = + generic::Ellipse<LengthPercentage, LengthPercentage, 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..abdc28ad84 --- /dev/null +++ b/servo/components/style/values/computed/border.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/. */ + +//! 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 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..22f5e30697 --- /dev/null +++ b/servo/components/style/values/computed/box.rs @@ -0,0 +1,246 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::computed::length::{LengthPercentage, NonNegativeLength}; +use crate::values::computed::{Context, Number, ToComputedValue}; +use crate::values::generics::box_::AnimationIterationCount as GenericAnimationIterationCount; +use crate::values::generics::box_::Perspective as GenericPerspective; +use crate::values::generics::box_::VerticalAlign as GenericVerticalAlign; +use crate::values::specified::box_ as specified; + +pub use crate::values::specified::box_::Clear as SpecifiedClear; +pub use crate::values::specified::box_::{AnimationName, Appearance, BreakBetween, BreakWithin}; +pub use crate::values::specified::box_::{Contain, Display, Float as SpecifiedFloat, Overflow}; +pub use crate::values::specified::box_::{OverflowAnchor, OverflowClipBox, OverscrollBehavior}; +pub use crate::values::specified::box_::{ + ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStrictness, ScrollSnapType, +}; +pub use crate::values::specified::box_::{TouchAction, TransitionProperty, WillChange}; + +/// A computed value for the `vertical-align` property. +pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>; + +/// A computed value for the `animation-iteration-count` property. +pub type AnimationIterationCount = GenericAnimationIterationCount<Number>; + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + GenericAnimationIterationCount::Number(1.0) + } +} + +/// 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. +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, + } + } +} diff --git a/servo/components/style/values/computed/color.rs b/servo/components/style/values/computed/color.rs new file mode 100644 index 0000000000..6098ea4590 --- /dev/null +++ b/servo/components/style/values/computed/color.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/. */ + +//! Computed color values. + +use crate::values::animated::color::RGBA as AnimatedRGBA; +use crate::values::animated::ToAnimatedValue; +use crate::values::generics::color::{Color as GenericColor, ColorOrAuto as GenericColorOrAuto}; +use cssparser::{Color as CSSParserColor, RGBA}; +use std::fmt; +use style_traits::{CssWriter, ToCss}; + +/// The computed value of the `color` property. +pub type ColorPropertyValue = RGBA; + +/// The computed value of `-moz-font-smoothing-background-color`. +pub type MozFontSmoothingBackgroundColor = RGBA; + +/// A computed value for `<color>`. +pub type Color = GenericColor<RGBA>; + +impl Color { + /// Returns a complex color value representing transparent. + pub fn transparent() -> Color { + Color::rgba(RGBA::transparent()) + } + + /// Combine this complex color with the given foreground color into + /// a numeric RGBA color. It currently uses linear blending. + pub fn to_rgba(&self, fg_color: RGBA) -> RGBA { + let (color, ratios) = match *self { + // Common cases that the complex color is either pure numeric + // color or pure currentcolor. + GenericColor::Numeric(color) => return color, + GenericColor::CurrentColor => return fg_color, + GenericColor::Complex { color, ratios } => (color, ratios), + }; + + // For the more complicated case that the alpha value differs, + // we use the following formula to compute the components: + // alpha = self_alpha * bg_ratio + fg_alpha * fg_ratio + // color = (self_color * self_alpha * bg_ratio + + // fg_color * fg_alpha * fg_ratio) / alpha + + let p1 = ratios.bg; + let a1 = color.alpha_f32(); + let r1 = a1 * color.red_f32(); + let g1 = a1 * color.green_f32(); + let b1 = a1 * color.blue_f32(); + + let p2 = ratios.fg; + let a2 = fg_color.alpha_f32(); + let r2 = a2 * fg_color.red_f32(); + let g2 = a2 * fg_color.green_f32(); + let b2 = a2 * fg_color.blue_f32(); + + let a = p1 * a1 + p2 * a2; + if a <= 0. { + return RGBA::transparent(); + } + let a = f32::min(a, 1.); + + let inverse_a = 1. / a; + let r = (p1 * r1 + p2 * r2) * inverse_a; + let g = (p1 * g1 + p2 * g2) * inverse_a; + let b = (p1 * b1 + p2 * b2) * inverse_a; + return RGBA::from_floats(r, g, b, a); + } +} + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + match *self { + GenericColor::Numeric(color) => color.to_css(dest), + GenericColor::CurrentColor => CSSParserColor::CurrentColor.to_css(dest), + _ => Ok(()), + } + } +} + +impl ToAnimatedValue for RGBA { + type AnimatedValue = AnimatedRGBA; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + AnimatedRGBA::new( + self.red_f32(), + self.green_f32(), + self.blue_f32(), + self.alpha_f32(), + ) + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + // RGBA::from_floats clamps each component values. + RGBA::from_floats(animated.red, animated.green, animated.blue, animated.alpha) + } +} + +/// auto | <color> +pub type ColorOrAuto = GenericColorOrAuto<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..40cfe46efa --- /dev/null +++ b/servo/components/style/values/computed/counters.rs @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::url::ComputedImageUrl; +use crate::values::generics::counters as generics; +use crate::values::generics::counters::CounterIncrement as GenericCounterIncrement; +use crate::values::generics::counters::CounterSetOrReset as GenericCounterSetOrReset; + +/// A computed value for the `counter-increment` property. +pub type CounterIncrement = GenericCounterIncrement<i32>; + +/// A computed value for the `counter-set` and `counter-reset` properties. +pub type CounterSetOrReset = GenericCounterSetOrReset<i32>; + +/// A computed value for the `content` property. +pub type Content = generics::GenericContent<ComputedImageUrl>; + +/// A computed content item. +pub type ContentItem = generics::GenericContentItem<ComputedImageUrl>; diff --git a/servo/components/style/values/computed/easing.rs b/servo/components/style/values/computed/easing.rs new file mode 100644 index 0000000000..877810de6d --- /dev/null +++ b/servo/components/style/values/computed/easing.rs @@ -0,0 +1,14 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 crate::values::computed::{Integer, Number}; +use crate::values::generics::easing; + +/// A computed timing function. +pub type ComputedTimingFunction = easing::TimingFunction<Integer, Number>; + +/// An alias of the computed timing function. +pub type TimingFunction = ComputedTimingFunction; 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..df6a8ed667 --- /dev/null +++ b/servo/components/style/values/computed/font.rs @@ -0,0 +1,987 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 + +#[cfg(feature = "gecko")] +use crate::gecko_bindings::sugar::refptr::RefPtr; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::{bindings, structs}; +use crate::values::animated::{ToAnimatedValue, ToAnimatedZero}; +use crate::values::computed::{ + Angle, Context, Integer, Length, NonNegativeLength, NonNegativePercentage, +}; +use crate::values::computed::{Number, Percentage, ToComputedValue}; +use crate::values::generics::font::{FeatureTagValue, FontSettings, VariationValue}; +use crate::values::generics::{font as generics, NonNegative}; +use crate::values::specified::font::{ + self as specified, KeywordInfo, MAX_FONT_WEIGHT, MIN_FONT_WEIGHT, +}; +use crate::values::specified::length::{FontBaseSize, NoCalcLength}; +use crate::values::CSSFloat; +use crate::Atom; +use cssparser::{serialize_identifier, CssStringWriter, Parser}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use std::fmt::{self, Write}; +use std::hash::{Hash, Hasher}; +#[cfg(feature = "gecko")] +use std::mem::{self, ManuallyDrop}; +#[cfg(feature = "servo")] +use std::slice; +use style_traits::{CssWriter, ParseError, ToCss}; +#[cfg(feature = "gecko")] +use to_shmem::{self, SharedMemoryBuilder, ToShmem}; + +pub use crate::values::computed::Length as MozScriptMinSize; +pub use crate::values::specified::font::{FontSynthesis, MozScriptSizeMultiplier}; +pub use crate::values::specified::font::{XLang, XTextZoom}; +pub use crate::values::specified::Integer as SpecifiedInteger; + +/// A value for the font-weight property per: +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight +/// +/// This is effectively just a `Number`. +#[derive( + Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct FontWeight(pub Number); + +impl Hash for FontWeight { + fn hash<H: Hasher>(&self, hasher: &mut H) { + hasher.write_u64((self.0 * 10000.).trunc() as u64); + } +} + +impl ToAnimatedValue for FontWeight { + type AnimatedValue = Number; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontWeight(animated.max(MIN_FONT_WEIGHT).min(MAX_FONT_WEIGHT)) + } +} + +#[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 size. + pub size: NonNegativeLength, + /// If derived from a keyword, the keyword and additional transformations applied to it + #[css(skip)] + pub keyword_info: KeywordInfo, +} + +impl FontWeight { + /// Value for normal + pub fn normal() -> Self { + FontWeight(400.) + } + + /// Value for bold + pub fn bold() -> Self { + FontWeight(700.) + } + + /// Convert from an Gecko weight + #[cfg(feature = "gecko")] + pub fn from_gecko_weight(weight: structs::FontWeight) -> Self { + // we allow a wider range of weights than is parseable + // because system fonts may provide custom values + let weight = unsafe { bindings::Gecko_FontWeight_ToFloat(weight) }; + FontWeight(weight) + } + + /// Weither this weight is bold + pub fn is_bold(&self) -> bool { + self.0 > 500. + } + + /// Return the bolder weight. + /// + /// See the table in: + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + pub fn bolder(self) -> Self { + if self.0 < 350. { + FontWeight(400.) + } else if self.0 < 550. { + FontWeight(700.) + } else { + FontWeight(self.0.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 { + if self.0 < 550. { + FontWeight(self.0.min(100.)) + } else if self.0 < 750. { + FontWeight(400.) + } else { + FontWeight(700.) + } + } +} + +impl FontSize { + /// The actual computed font size. + #[inline] + pub fn size(&self) -> Length { + self.size.0 + } + + #[inline] + /// Get default value of font size. + pub fn medium() -> Self { + Self { + 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.size.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontSize { + 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. +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, +} + +impl FontFamily { + #[inline] + /// Get default font family as `serif` which is a generic font-family + pub fn serif() -> Self { + FontFamily { + families: FontFamilyList::new(Box::new([SingleFontFamily::Generic( + GenericFontFamily::Serif, + )])), + is_system_font: false, + } + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for FontFamily { + fn size_of(&self, _ops: &mut MallocSizeOfOps) -> usize { + // SharedFontList objects are generally shared from the pointer + // stored in the specified value. So only count this if the + // SharedFontList is unshared. + let shared_font_list = self.families.shared_font_list().get(); + unsafe { bindings::Gecko_SharedFontList_SizeOfIncludingThisIfUnshared(shared_font_list) } + } +} + +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(); + iter.next().unwrap().to_css(dest)?; + for family in iter { + dest.write_str(", ")?; + family.to_css(dest)?; + } + Ok(()) + } +} + +#[derive( + Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +/// The name of a font family of choice +pub struct FamilyName { + /// Name of the font family + pub name: Atom, + /// Syntax of the font family + pub syntax: FontFamilyNameSyntax, +} + +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, +} + +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize, Hash))] +/// A set of faces that vary in weight, width or slope. +pub enum SingleFontFamily { + /// The name of a font family of choice. + FamilyName(FamilyName), + /// Generic family name. + Generic(GenericFontFamily), +} + +/// 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. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + Parse, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(u8)] +#[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, + /// An internal value for emoji font selection. + #[css(skip)] + #[cfg(feature = "gecko")] + MozEmoji, +} + +impl SingleFontFamily { + /// Parse a font-family value. + pub fn parse<'i, 't>(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, + })); + } + + let first_ident = input.expect_ident_cloned()?; + if let Ok(generic) = GenericFontFamily::from_ident(&first_ident) { + return Ok(SingleFontFamily::Generic(generic)); + } + + 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")] + /// 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, + }) + } + + #[cfg(feature = "gecko")] + /// Get the corresponding font-family with family name + fn from_font_family_name(family: &structs::FontFamilyName) -> SingleFontFamily { + if family.mName.mRawPtr.is_null() { + debug_assert_ne!(family.mGeneric, GenericFontFamily::None); + return SingleFontFamily::Generic(family.mGeneric); + } + let name = unsafe { Atom::from_raw(family.mName.mRawPtr) }; + SingleFontFamily::FamilyName(FamilyName { + name, + syntax: family.mSyntax, + }) + } +} + +#[cfg(feature = "servo")] +#[derive( + Clone, + Debug, + Deserialize, + Eq, + Hash, + MallocSizeOf, + PartialEq, + Serialize, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +/// A list of SingleFontFamily +pub struct FontFamilyList(Box<[SingleFontFamily]>); + +#[cfg(feature = "gecko")] +#[derive(Clone, Debug, ToComputedValue, ToResolvedValue)] +/// A list of SingleFontFamily +pub enum FontFamilyList { + /// A strong reference to a Gecko SharedFontList object. + SharedFontList( + #[compute(no_field_bound)] + #[resolve(no_field_bound)] + RefPtr<structs::SharedFontList>, + ), + /// A font-family generic ID. + Generic(GenericFontFamily), +} + +#[cfg(feature = "gecko")] +impl ToShmem for FontFamilyList { + fn to_shmem(&self, _builder: &mut SharedMemoryBuilder) -> to_shmem::Result<Self> { + // In practice, the only SharedFontList objects we create from shared + // style sheets are ones with a single generic entry. + Ok(ManuallyDrop::new(match *self { + FontFamilyList::SharedFontList(ref r) => { + if !(r.mNames.len() == 1 && r.mNames[0].mName.mRawPtr.is_null()) { + return Err(String::from( + "ToShmem failed for FontFamilyList: cannot handle non-generic families", + )); + } + FontFamilyList::Generic(r.mNames[0].mGeneric) + }, + FontFamilyList::Generic(t) => FontFamilyList::Generic(t), + })) + } +} + +#[cfg(feature = "gecko")] +impl PartialEq for FontFamilyList { + fn eq(&self, other: &FontFamilyList) -> bool { + let self_list = self.shared_font_list(); + let other_list = other.shared_font_list(); + + if self_list.mNames.len() != other_list.mNames.len() { + return false; + } + for (a, b) in self_list.mNames.iter().zip(other_list.mNames.iter()) { + if a.mSyntax != b.mSyntax || + a.mName.mRawPtr != b.mName.mRawPtr || + a.mGeneric != b.mGeneric + { + return false; + } + } + true + } +} + +#[cfg(feature = "gecko")] +impl Eq for FontFamilyList {} + +impl FontFamilyList { + /// Return FontFamilyList with a vector of SingleFontFamily + #[cfg(feature = "servo")] + pub fn new(families: Box<[SingleFontFamily]>) -> FontFamilyList { + FontFamilyList(families) + } + + /// Return FontFamilyList with a vector of SingleFontFamily + #[cfg(feature = "gecko")] + pub fn new(families: Box<[SingleFontFamily]>) -> FontFamilyList { + let fontlist; + let names; + unsafe { + fontlist = bindings::Gecko_SharedFontList_Create(); + names = &mut (*fontlist).mNames; + names.ensure_capacity(families.len()); + }; + + for family in families.iter() { + match *family { + SingleFontFamily::FamilyName(ref f) => unsafe { + bindings::Gecko_nsTArray_FontFamilyName_AppendNamed( + names, + f.name.as_ptr(), + f.syntax, + ); + }, + SingleFontFamily::Generic(family) => unsafe { + bindings::Gecko_nsTArray_FontFamilyName_AppendGeneric(names, family); + }, + } + } + + FontFamilyList::SharedFontList(unsafe { RefPtr::from_addrefed(fontlist) }) + } + + /// Return iterator of SingleFontFamily + #[cfg(feature = "servo")] + pub fn iter(&self) -> slice::Iter<SingleFontFamily> { + self.0.iter() + } + + /// Return iterator of SingleFontFamily + #[cfg(feature = "gecko")] + pub fn iter(&self) -> FontFamilyNameIter { + FontFamilyNameIter { + names: &self.shared_font_list().mNames, + cur: 0, + } + } + + /// 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.clone()); + } + } + None + } + + /// Return a reference to the Gecko SharedFontList. + #[cfg(feature = "gecko")] + pub fn shared_font_list(&self) -> &RefPtr<structs::SharedFontList> { + match *self { + FontFamilyList::SharedFontList(ref r) => r, + FontFamilyList::Generic(t) => { + unsafe { + // TODO(heycam): Should really add StaticRefPtr sugar. + let index = t as usize; + mem::transmute::< + &structs::StaticRefPtr<structs::SharedFontList>, + &RefPtr<structs::SharedFontList>, + >(&structs::SharedFontList_sSingleGenerics[index]) + } + }, + } + } +} + +/// Iterator of FontFamily +#[cfg(feature = "gecko")] +pub struct FontFamilyNameIter<'a> { + names: &'a structs::nsTArray<structs::FontFamilyName>, + cur: usize, +} + +#[cfg(feature = "gecko")] +impl<'a> Iterator for FontFamilyNameIter<'a> { + type Item = SingleFontFamily; + + fn next(&mut self) -> Option<Self::Item> { + if self.cur < self.names.len() { + let item = SingleFontFamily::from_font_family_name(&self.names[self.cur]); + self.cur += 1; + Some(item) + } else { + None + } + } +} + +/// Preserve the readability of text when font fallback occurs +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToCss, + ToResolvedValue, +)] +pub enum FontSizeAdjust { + #[animation(error)] + /// None variant + None, + /// Number variant + Number(CSSFloat), +} + +impl FontSizeAdjust { + #[inline] + /// Default value of font-size-adjust + pub fn none() -> Self { + FontSizeAdjust::None + } + + /// Get font-size-adjust with float number + pub fn from_gecko_adjust(gecko: f32) -> Self { + if gecko == -1.0 { + FontSizeAdjust::None + } else { + FontSizeAdjust::Number(gecko) + } + } +} + +impl ToAnimatedZero for FontSizeAdjust { + #[inline] + // FIXME(emilio): why? + fn to_animated_zero(&self) -> Result<Self, ()> { + Err(()) + } +} + +impl ToAnimatedValue for FontSizeAdjust { + type AnimatedValue = Self; + + #[inline] + fn to_animated_value(self) -> Self { + self + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + match animated { + FontSizeAdjust::Number(number) => FontSizeAdjust::Number(number.max(0.)), + _ => animated, + } + } +} + +/// Use VariantAlternatesList as computed type of FontVariantAlternates +pub type FontVariantAlternates = specified::VariantAlternatesList; + +impl FontVariantAlternates { + /// Get initial value with VariantAlternatesList + #[inline] + pub fn get_initial_value() -> Self { + Self::default() + } +} + +/// Use VariantEastAsian as computed type of FontVariantEastAsian +pub type FontVariantEastAsian = specified::VariantEastAsian; + +/// Use VariantLigatures as computed type of FontVariantLigatures +pub type FontVariantLigatures = specified::VariantLigatures; + +/// Use VariantNumeric as computed type of FontVariantNumeric +pub type FontVariantNumeric = specified::VariantNumeric; + +/// 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>>; + +/// font-language-override can only have a single three-letter +/// 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, ToResolvedValue)] +#[repr(C)] +pub struct FontLanguageOverride(pub u32); + +impl FontLanguageOverride { + #[inline] + /// Get computed default value of `font-language-override` with 0 + pub fn zero() -> 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 computing + let slice = if cfg!(debug_assertions) { + std::str::from_utf8(&storage[..]).unwrap() + } else { + unsafe { std::str::from_utf8_unchecked(&storage[..]) } + }; + slice.trim_end() + } + + /// Parses a str, return `Self::zero()` if the input isn't a valid OpenType + /// "language system" tag. + #[inline] + pub fn from_str(lang: &str) -> Self { + if lang.is_empty() || lang.len() > 4 { + return Self::zero(); + } + let mut bytes = [b' '; 4]; + for (byte, lang_byte) in bytes.iter_mut().zip(lang.as_bytes()) { + if !lang_byte.is_ascii() { + return Self::zero(); + } + *byte = *lang_byte; + } + Self(u32::from_be_bytes(bytes)) + } + + /// 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; + match self.0 { + NoCalcLength::FontRelative(value) => value.to_computed_value(cx, base_size), + NoCalcLength::ServoCharacterWidth(value) => { + value.to_computed_value(base_size.resolve(cx)) + }, + 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 + 1 + } else { + parent + } + }, + specified::MathDepth::Add(rel) => { + let parent = cx.builder.get_parent_font().clone_math_depth(); + parent as i32 + 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)) + } +} + +/// A wrapper over an `Angle`, that handles clamping to the appropriate range +/// for `font-style` animation. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct FontStyleAngle(pub Angle); + +impl ToAnimatedValue for FontStyleAngle { + type AnimatedValue = Angle; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontStyleAngle(Angle::from_degrees( + animated + .degrees() + .min(specified::FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES) + .max(specified::FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES), + )) + } +} + +impl Hash for FontStyleAngle { + fn hash<H: Hasher>(&self, hasher: &mut H) { + hasher.write_u64((self.0.degrees() * 10000.).trunc() as u64); + } +} + +/// The computed value of `font-style`. +/// +/// FIXME(emilio): Angle should be a custom type to handle clamping during +/// animation. +pub type FontStyle = generics::FontStyle<FontStyleAngle>; + +impl FontStyle { + /// The `normal` value. + #[inline] + pub fn normal() -> Self { + generics::FontStyle::Normal + } + + /// The default angle for font-style: oblique. This is 20deg per spec: + /// + /// https://drafts.csswg.org/css-fonts-4/#valdef-font-style-oblique-angle + #[inline] + pub fn default_angle() -> FontStyleAngle { + FontStyleAngle(Angle::from_degrees( + specified::DEFAULT_FONT_STYLE_OBLIQUE_ANGLE_DEGREES, + )) + } + + /// Get the font style from Gecko's nsFont struct. + #[cfg(feature = "gecko")] + pub fn from_gecko(style: structs::FontSlantStyle) -> Self { + let mut angle = 0.; + let mut italic = false; + let mut normal = false; + unsafe { + bindings::Gecko_FontSlantStyle_Get(style, &mut normal, &mut italic, &mut angle); + } + if normal { + return generics::FontStyle::Normal; + } + if italic { + return generics::FontStyle::Italic; + } + generics::FontStyle::Oblique(FontStyleAngle(Angle::from_degrees(angle))) + } +} + +impl ToCss for FontStyle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::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")?; + // Use `degrees` instead of just comparing Angle because + // `degrees` can return slightly different values due to + // floating point conversions. + if angle.0.degrees() != Self::default_angle().0.degrees() { + dest.write_char(' ')?; + angle.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +/// A value for the font-stretch property per: +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch +#[derive( + Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct FontStretch(pub NonNegativePercentage); + +impl FontStretch { + /// 100% + pub fn hundred() -> Self { + FontStretch(NonNegativePercentage::hundred()) + } + + /// The float value of the percentage + #[inline] + pub fn value(&self) -> CSSFloat { + ((self.0).0).0 + } +} + +impl ToAnimatedValue for FontStretch { + type AnimatedValue = Percentage; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0.to_animated_value() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontStretch(NonNegativePercentage::from_animated_value(animated)) + } +} + +impl Hash for FontStretch { + fn hash<H: Hasher>(&self, hasher: &mut H) { + hasher.write_u64((self.value() * 10000.).trunc() as u64); + } +} diff --git a/servo/components/style/values/computed/image.rs b/servo/components/style/values/computed/image.rs new file mode 100644 index 0000000000..52cb9cd493 --- /dev/null +++ b/servo/components/style/values/computed/image.rs @@ -0,0 +1,196 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; +#[cfg(feature = "gecko")] +use crate::values::computed::NumberOrPercentage; +use crate::values::computed::{Angle, Color, Context}; +use crate::values::computed::{ + AngleOrPercentage, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage, + ToComputedValue, Resolution, +}; +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}; + +/// Computed values for an image according to CSS-IMAGES. +/// <https://drafts.csswg.org/css-images/#image-values> +pub type Image = generic::GenericImage<Gradient, MozImageRect, ComputedImageUrl, Color, Percentage, Resolution>; + +/// 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 percentage or nothing. +pub type PercentOrNone = generic::PercentOrNone<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 selected_index = 0; + let mut selected_resolution = items[0].resolution.dppx(); + for (i, item) in items.iter().enumerate().skip(1) { + let candidate_resolution = item.resolution.dppx(); + + // 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 + }; + + if better_candidate() { + selected_index = i; + selected_resolution = candidate_resolution; + } + } + + ImageSet { + selected_index, + items, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self { + selected_index: 0, + items: ToComputedValue::from_computed_value(&computed.items), + } + } +} + + +/// Computed values for `-moz-image-rect(...)`. +#[cfg(feature = "gecko")] +pub type MozImageRect = generic::GenericMozImageRect<NumberOrPercentage, ComputedImageUrl>; + +/// Empty enum on non-gecko +#[cfg(not(feature = "gecko"))] +pub type MozImageRect = specified::MozImageRect; + +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_str(" ")?; + 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..f4108b35bc --- /dev/null +++ b/servo/components/style/values/computed/length.rs @@ -0,0 +1,518 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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}; +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 { + match *self { + specified::NoCalcLength::Absolute(length) => length.to_computed_value(context), + specified::NoCalcLength::FontRelative(length) => { + length.to_computed_value(context, FontBaseSize::CurrentStyle) + }, + specified::NoCalcLength::ViewportPercentage(length) => { + length.to_computed_value(context.viewport_size_for_viewport_unit_resolution()) + }, + specified::NoCalcLength::ServoCharacterWidth(length) => { + length.to_computed_value(context.style().get_font().clone_font_size().size()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + specified::NoCalcLength::Absolute(AbsoluteLength::Px(computed.px())) + } +} + +impl ToComputedValue for specified::Length { + type ComputedValue = Length; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + specified::Length::NoCalc(l) => l.to_computed_value(context), + specified::Length::Calc(ref calc) => { + calc.to_computed_value(context).to_length().unwrap() + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + specified::Length::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 { + generics::GenericLengthPercentageOrAuto::Auto => None, + generics::GenericLengthPercentageOrAuto::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 { + GenericSize::Auto => false, + GenericSize::LengthPercentage(ref lp) => lp.is_definitely_zero(), + #[cfg(feature = "gecko")] + GenericSize::ExtremumLength(..) => 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)) + } + + /// 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 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<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 type for possible values for min- and max- flavors of width, height, +/// block-size, and inline-size. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ExtremumLength { + #[parse(aliases = "-moz-max-content")] + MaxContent, + #[parse(aliases = "-moz-min-content")] + MinContent, + MozFitContent, + MozAvailable, +} + +/// 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..22fd9b5f8c --- /dev/null +++ b/servo/components/style/values/computed/length_percentage.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/. */ + +//! `<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::values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::{calc, NonNegative}; +use crate::values::specified::length::FontBaseSize; +use crate::values::{specified, CSSFloat}; +use crate::Zero; +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) + } + + /// 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)))) + }, + } + }, + _ => 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 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), +} + +impl CalcLengthPercentageLeaf { + fn is_zero_length(&self) -> bool { + match *self { + Self::Length(ref l) => l.is_zero(), + Self::Percentage(..) => false, + } + } +} + +impl PartialOrd for CalcLengthPercentageLeaf { + fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { + use self::CalcLengthPercentageLeaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Length(ref one), &Length(ref other)) => one.partial_cmp(other), + (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other), + _ => { + match *self { + Length(..) | Percentage(..) => {}, + } + unsafe { + debug_unreachable!("Forgot a branch?"); + } + }, + } + } +} + +impl calc::CalcNodeLeaf for CalcLengthPercentageLeaf { + fn is_negative(&self) -> bool { + match *self { + Self::Length(ref l) => l.px() < 0., + Self::Percentage(ref p) => p.0 < 0., + } + } + + 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(()); + } + + 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; + }, + _ => return Err(()), + } + + Ok(()) + } + + fn mul_by(&mut self, scalar: f32) { + match *self { + Self::Length(ref mut l) => *l = *l * scalar, + Self::Percentage(ref mut p) => p.0 *= scalar, + } + } + + fn simplify(&mut self) {} + + fn sort_key(&self) -> calc::SortKey { + match *self { + Self::Length(..) => calc::SortKey::Px, + Self::Percentage(..) => calc::SortKey::Percentage, + } + } +} + +/// 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] + fn resolve(&self, basis: Length) -> Length { + // unwrap() is fine because the conversion below is infallible. + let px = self + .node + .resolve(|l| { + Ok(match *l { + CalcLengthPercentageLeaf::Length(l) => l.px(), + CalcLengthPercentageLeaf::Percentage(ref p) => basis.px() * p.0, + }) + }) + .unwrap(); + Length::new(self.clamping_mode.clamp(px)).normalized() + } +} + +// 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, + ) -> LengthPercentage + where + F: Fn(Length) -> Length, + { + use crate::values::specified::calc::Leaf; + use crate::values::specified::length::NoCalcLength; + + let node = self.node.map_leaves(|leaf| match *leaf { + Leaf::Percentage(p) => CalcLengthPercentageLeaf::Percentage(Percentage(p)), + Leaf::Length(l) => CalcLengthPercentageLeaf::Length(match l { + NoCalcLength::Absolute(ref abs) => zoom_fn(abs.to_computed_value(context)), + NoCalcLength::FontRelative(ref fr) => fr.to_computed_value(context, base_size), + other => other.to_computed_value(context), + }), + Leaf::Number(..) | Leaf::Angle(..) | Leaf::Time(..) => { + 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, + ) -> LengthPercentage { + self.to_computed_value_with_zoom( + context, + |abs| context.maybe_zoom_text(abs.into()), + base_size, + ) + } + + /// 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 calc using the current font-size (and without text-zoom). + pub fn to_computed_value(&self, context: &Context) -> LengthPercentage { + self.to_computed_value_with_zoom(context, |abs| abs, FontBaseSize::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), + }), + } + } +} + +/// 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)?) + }, + _ => { + let mut one = self.to_calc_node().into_owned(); + let mut other = other.to_calc_node().into_owned(); + let (l, r) = procedure.weights(); + + one.mul_by(l as f32); + other.mul_by(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..2ae3776041 --- /dev/null +++ b/servo/components/style/values/computed/list.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/. */ + +//! `list` computed values. + +#[cfg(feature = "gecko")] +pub use crate::values::specified::list::ListStyleType; +pub use crate::values::specified::list::MozListReversed; +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..d37dc25497 --- /dev/null +++ b/servo/components/style/values/computed/mod.rs @@ -0,0 +1,816 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::context::QuirksMode; +use crate::font_metrics::{get_metrics_provider_for_product, FontMetricsProvider}; +use crate::media_queries::Device; +#[cfg(feature = "gecko")] +use crate::properties; +use crate::properties::{ComputedValues, LonghandId, StyleBuilder}; +use crate::rule_cache::RuleCacheConditions; +use crate::{ArcSlice, Atom, One}; +use euclid::default::Size2D; +use servo_arc::Arc; +use std::cell::RefCell; +use std::cmp; +use std::f32; + +#[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::background::{BackgroundRepeat, BackgroundSize}; +pub use self::basic_shape::FillRule; +pub use self::border::{BorderCornerRadius, BorderRadius, BorderSpacing}; +pub use self::border::{BorderImageRepeat, BorderImageSideWidth}; +pub use self::border::{BorderImageSlice, BorderImageWidth}; +pub use self::box_::{AnimationIterationCount, AnimationName, Contain}; +pub use self::box_::{Appearance, BreakBetween, BreakWithin, Clear, Float}; +pub use self::box_::{Display, Overflow, OverflowAnchor, TransitionProperty}; +pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize}; +pub use self::box_::{ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStrictness, ScrollSnapType}; +pub use self::box_::{TouchAction, VerticalAlign, WillChange}; +pub use self::color::{Color, ColorOrAuto, ColorPropertyValue}; +pub use self::column::ColumnCount; +pub use self::counters::{Content, ContentItem, CounterIncrement, CounterSetOrReset}; +pub use self::easing::TimingFunction; +pub use self::effects::{BoxShadow, Filter, SimpleShadow}; +pub use self::flex::FlexBasis; +pub use self::font::{FontFamily, FontLanguageOverride, FontStyle}; +pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; +pub use self::font::{FontSize, FontSizeAdjust, FontStretch, FontSynthesis}; +pub use self::font::{FontVariantAlternates, FontWeight}; +pub use self::font::{FontVariantEastAsian, FontVariationSettings}; +pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextZoom}; +pub use self::image::{Gradient, Image, LineDirection, MozImageRect}; +pub use self::length::{CSSPixelLength, ExtremumLength, 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::MozListReversed; +pub use self::list::Quotes; +pub use self::motion::{OffsetPath, OffsetRotate}; +pub use self::outline::OutlineStyle; +pub use self::percentage::{NonNegativePercentage, Percentage}; +pub use self::position::AspectRatio; +pub use self::position::{ + GridAutoFlow, GridTemplateAreas, MasonryAutoFlow, Position, PositionOrAuto, ZIndex, +}; +pub use self::rect::NonNegativeLengthOrNumberRect; +pub use self::resolution::Resolution; +pub use self::svg::MozContextProperties; +pub use self::svg::{SVGLength, SVGOpacity, SVGPaint, SVGPaintKind}; +pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth}; +pub use self::text::TextUnderlinePosition; +pub use self::text::{InitialLetter, LetterSpacing, LineBreak, LineHeight}; +pub use self::text::{OverflowWrap, TextOverflow, WordBreak, WordSpacing}; +pub use self::text::{TextAlign, TextAlignLast, TextEmphasisPosition, TextEmphasisStyle}; +pub use self::text::{TextDecorationLength, TextDecorationSkipInk}; +pub use self::time::Time; +pub use self::transform::{Rotate, Scale, Transform, TransformOperation}; +pub use self::transform::{TransformOrigin, TransformStyle, Translate}; +#[cfg(feature = "gecko")] +pub use self::ui::CursorImage; +pub use self::ui::{Cursor, MozForceBrokenImageIcon, UserSelect}; +pub use super::specified::TextTransform; +pub use super::specified::{BorderStyle, TextDecorationLine}; +pub use super::{Auto, Either, None_}; +pub use app_units::Au; + +#[cfg(feature = "gecko")] +pub mod align; +pub mod angle; +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 percentage; +pub mod position; +pub mod rect; +pub mod resolution; +pub mod svg; +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<()>, + + /// A font metrics provider, used to access font metrics to implement + /// font-relative units. + pub font_metrics_provider: &'a dyn FontMetricsProvider, + + /// Whether or not we are computing the media list in a media query + pub in_media_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, + + /// The property we are computing a value for, if it is a non-inherited + /// property. None 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: Option<LonghandId>, + + /// The conditions to cache a rule node on the rule cache. + /// + /// FIXME(emilio): Drop the refcell. + pub rule_cache_conditions: RefCell<&'a mut RuleCacheConditions>, +} + +impl<'a> Context<'a> { + /// 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 provider = get_metrics_provider_for_product(); + + let context = Context { + builder: StyleBuilder::for_inheritance(device, None, None), + font_metrics_provider: &provider, + cached_system_font: None, + in_media_query: true, + quirks_mode, + for_smil_animation: false, + for_non_inherited_property: None, + rule_cache_conditions: RefCell::new(&mut conditions), + }; + + f(&context) + } + + /// The current device. + pub fn device(&self) -> &Device { + self.builder.device + } + + /// The current viewport size, used to resolve viewport units. + pub fn viewport_size_for_viewport_unit_resolution(&self) -> Size2D<Au> { + self.builder + .device + .au_viewport_size_for_viewport_unit_resolution() + } + + /// 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 { + // We disable zoom for <svg:text> by unsetting the + // -x-text-zoom property, which leads to a false value + // in mAllowZoomAndMinSize + if self.style().get_font().gecko.mAllowZoomAndMinSize { + 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: cx, + values: 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 Size2D<T> +where + T: ToComputedValue, +{ + type ComputedValue = 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); +#[cfg(feature = "servo")] +trivial_to_computed_value!(html5ever::Namespace); +#[cfg(feature = "servo")] +trivial_to_computed_value!(html5ever::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); + +#[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) + } +} + +/// A computed positive `<integer>` value or `none`. +pub type PositiveIntegerOrNone = Either<PositiveInteger, None_>; + +/// 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>; diff --git a/servo/components/style/values/computed/motion.rs b/servo/components/style/values/computed/motion.rs new file mode 100644 index 0000000000..e2565ff346 --- /dev/null +++ b/servo/components/style/values/computed/motion.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/. */ + +//! Computed types for CSS values that are related to motion path. + +use crate::values::computed::Angle; +use crate::values::generics::motion::GenericOffsetPath; +use crate::Zero; + +/// The computed value of `offset-path`. +pub type OffsetPath = GenericOffsetPath<Angle>; + +#[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/percentage.rs b/servo/components/style/values/computed/percentage.rs new file mode 100644 index 0000000000..7430d82d47 --- /dev/null +++ b/servo/components/style/values/computed/percentage.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/. */ + +//! Computed percentages. + +use crate::values::animated::ToAnimatedValue; +use crate::values::generics::NonNegative; +use crate::values::{serialize_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 std::ops::AddAssign for Percentage { + fn add_assign(&mut self, other: Self) { + self.0 += other.0 + } +} + +impl ToCss for Percentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + serialize_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..f5e586fdfa --- /dev/null +++ b/servo/components/style/values/computed/position.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/. */ + +//! 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::Ratio as GenericRatio; +use crate::values::generics::position::ZIndex as GenericZIndex; +pub use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas, MasonryAutoFlow}; +use crate::{One, Zero}; +use std::cmp::{Ordering, PartialOrd}; +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_str(" ")?; + 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 <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), + ) + } +} + +impl Ratio { + /// Returns a new Ratio. + 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 + } + } +} + +/// A computed value for the `aspect-ratio` property. +pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>; 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..72580635b8 --- /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(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(self.to_dppx()) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + specified::Resolution::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..a348d071ab --- /dev/null +++ b/servo/components/style/values/computed/svg.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 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::values::RGBA; +use crate::Zero; + +pub use crate::values::specified::{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 fn black() -> Self { + let rgba = RGBA::from_floats(0., 0., 0., 1.).into(); + SVGPaint { + kind: generic::SVGPaintKind::Color(rgba), + 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/text.rs b/servo/components/style/values/computed/text.rs new file mode 100644 index 0000000000..b77695e06c --- /dev/null +++ b/servo/components/style/values/computed/text.rs @@ -0,0 +1,218 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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, NonNegativeLength, NonNegativeNumber, ToComputedValue}; +use crate::values::generics::text::InitialLetter as GenericInitialLetter; +use crate::values::generics::text::LineHeight as GenericLineHeight; +use crate::values::generics::text::{GenericTextDecorationLength, 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::{TextAlignLast, TextUnderlinePosition}; +pub use crate::values::specified::{LineBreak, OverflowWrap, WordBreak}; +pub use crate::values::specified::{TextDecorationLine, TextEmphasisPosition}; +pub use crate::values::specified::{TextDecorationSkipInk, 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; + +/// 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)) + } +} + +/// A computed value for the `line-height` property. +pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLength>; + +#[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_str(" ")?; + 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..5681f6fab5 --- /dev/null +++ b/servo/components/style/values/computed/time.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 time values. + +use crate::values::CSSFloat; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A computed `<time>` value. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct Time { + seconds: CSSFloat, +} + +impl Time { + /// Creates a time value from a seconds amount. + pub fn from_seconds(seconds: CSSFloat) -> Self { + Time { seconds: 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_str("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..46c471b7e2 --- /dev/null +++ b/servo/components/style/values/computed/transform.rs @@ -0,0 +1,555 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; + +/// 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>; + +/// 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(ref l) => Ok( + generic::TransformOperation::Perspective(l.to_animated_zero()?), + ), + 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..ae12dfcdae --- /dev/null +++ b/servo/components/style/values/computed/ui.rs @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::url::ComputedImageUrl; +use crate::values::computed::Number; +use crate::values::generics::ui as generics; + +pub use crate::values::specified::ui::CursorKind; +pub use crate::values::specified::ui::{MozForceBrokenImageIcon, 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<ComputedImageUrl, 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..4815281a3b --- /dev/null +++ b/servo/components/style/values/computed/url.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/. */ + +//! 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>; + +/// Computed image <url> | <none> +pub type ImageUrlOrNone = GenericUrlOrNone<ComputedImageUrl>; diff --git a/servo/components/style/values/distance.rs b/servo/components/style/values/distance.rs new file mode 100644 index 0000000000..a4259ce8c6 --- /dev/null +++ b/servo/components/style/values/distance.rs @@ -0,0 +1,131 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 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/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..60619b46e2 --- /dev/null +++ b/servo/components/style/values/generics/basic_shape.rs @@ -0,0 +1,535 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::{Animate, Procedure, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::border::GenericBorderRadius; +use crate::values::generics::position::GenericPosition; +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 + } +} + +/// 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)] +#[animation(no_bound(U))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum GenericClipPath<BasicShape, U> { + #[animation(error)] + None, + #[animation(error)] + Url(U), + #[css(function)] + Path(Path), + Shape( + Box<BasicShape>, + #[css(skip_if = "is_default")] ShapeGeometryBox, + ), + #[animation(error)] + Box(ShapeGeometryBox), +} + +pub use self::GenericClipPath as ClipPath; + +/// A value for the `shape-outside` property. +#[allow(missing_docs)] +#[animation(no_bound(I))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[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; + +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericBasicShape<H, V, LengthPercentage, NonNegativeLengthPercentage> { + Inset( + #[css(field_bound)] + #[shmem(field_bound)] + InsetRect<LengthPercentage, NonNegativeLengthPercentage>, + ), + Circle( + #[css(field_bound)] + #[shmem(field_bound)] + Circle<H, V, NonNegativeLengthPercentage>, + ), + Ellipse( + #[css(field_bound)] + #[shmem(field_bound)] + Ellipse<H, V, NonNegativeLengthPercentage>, + ), + Polygon(GenericPolygon<LengthPercentage>), +} + +pub use self::GenericBasicShape as BasicShape; + +/// <https://drafts.csswg.org/css-shapes/#funcdef-inset> +#[allow(missing_docs)] +#[css(function = "inset")] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct InsetRect<LengthPercentage, NonNegativeLengthPercentage> { + pub rect: Rect<LengthPercentage>, + #[shmem(field_bound)] + pub round: GenericBorderRadius<NonNegativeLengthPercentage>, +} + +/// <https://drafts.csswg.org/css-shapes/#funcdef-circle> +#[allow(missing_docs)] +#[css(function)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Circle<H, V, NonNegativeLengthPercentage> { + pub position: GenericPosition<H, V>, + pub radius: GenericShapeRadius<NonNegativeLengthPercentage>, +} + +/// <https://drafts.csswg.org/css-shapes/#funcdef-ellipse> +#[allow(missing_docs)] +#[css(function)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Ellipse<H, V, NonNegativeLengthPercentage> { + pub position: GenericPosition<H, V>, + 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, + MallocSizeOf, + Parse, + PartialEq, + 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> +#[css(comma, function = "polygon")] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[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( + Clone, + Debug, + MallocSizeOf, + PartialEq, + 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( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + 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 +#[css(comma)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Path { + /// The filling rule for the svg path. + #[css(skip_if = "is_default")] + #[animation(constant)] + 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_str(")") + } +} + +impl<H, V, NonNegativeLengthPercentage> ToCss for Circle<H, V, NonNegativeLengthPercentage> +where + GenericPosition<H, V>: ToCss, + NonNegativeLengthPercentage: ToCss + PartialEq, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("circle(")?; + if self.radius != Default::default() { + self.radius.to_css(dest)?; + dest.write_str(" ")?; + } + dest.write_str("at ")?; + self.position.to_css(dest)?; + dest.write_str(")") + } +} + +impl<H, V, NonNegativeLengthPercentage> ToCss for Ellipse<H, V, NonNegativeLengthPercentage> +where + GenericPosition<H, V>: ToCss, + NonNegativeLengthPercentage: ToCss + PartialEq, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("ellipse(")?; + if self.semiaxis_x != Default::default() || self.semiaxis_y != Default::default() { + self.semiaxis_x.to_css(dest)?; + dest.write_str(" ")?; + self.semiaxis_y.to_css(dest)?; + dest.write_str(" ")?; + } + dest.write_str("at ")?; + self.position.to_css(dest)?; + dest.write_str(")") + } +} + +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(()); + } + if self.coordinates.len() != other.coordinates.len() { + return Err(()); + } + let coordinates = self + .coordinates + .iter() + .zip(other.coordinates.iter()) + .map(|(this, other)| { + Ok(PolygonCoord( + this.0.animate(&other.0, procedure)?, + this.1.animate(&other.1, procedure)?, + )) + }) + .collect::<Result<Vec<_>, _>>()? + .into(); + 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(()); + } + if self.coordinates.len() != other.coordinates.len() { + return Err(()); + } + self.coordinates + .iter() + .zip(other.coordinates.iter()) + .map(|(this, other)| { + let d1 = this.0.compute_squared_distance(&other.0)?; + let d2 = this.1.compute_squared_distance(&other.1)?; + Ok(d1 + d2) + }) + .sum() + } +} + +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..37d91464b0 --- /dev/null +++ b/servo/components/style/values/generics/border.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/. */ + +//! 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, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + 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`, `outline-radius` and `inset()`. +/// +/// <https://drafts.csswg.org/css-backgrounds-3/#border-radius> +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + 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..5d6e8e2503 --- /dev/null +++ b/servo/components/style/values/generics/box.rs @@ -0,0 +1,132 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; + +#[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-animations/#animation-iteration-count +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum AnimationIterationCount<Number> { + /// A `<number>` value. + Number(Number), + /// The `infinite` keyword. + Infinite, +} + +/// 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..0d791e5d00 --- /dev/null +++ b/servo/components/style/values/generics/calc.rs @@ -0,0 +1,580 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::Zero; +use smallvec::SmallVec; +use std::fmt::{self, Write}; +use std::ops::Add; +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, +} + +/// 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, + Ch, + Deg, + Em, + Ex, + Px, + Rem, + Sec, + Vh, + 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 sum node, representing `a + b + c` where a, b, and c are the + /// arguments. + Sum(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>>, + }, +} + +pub use self::GenericCalcNode as CalcNode; + +/// A trait that represents all the stuff a valid leaf of a calc expression. +pub trait CalcNodeLeaf: Clone + Sized + PartialOrd + PartialEq + ToCss { + /// Whether this value is known-negative. + fn is_negative(&self) -> bool; + + /// Tries to merge one sum to another, that is, perform `x` + `y`. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()>; + + /// Multiplies the leaf by a given scalar number. + fn mul_by(&mut self, scalar: f32); + + /// Negates the leaf. + fn negate(&mut self) { + self.mul_by(-1.); + } + + /// Canonicalizes the expression if necessary. + fn simplify(&mut self); + + /// Returns the sort key for simplification. + fn sort_key(&self) -> SortKey; +} + +impl<L: CalcNodeLeaf> CalcNode<L> { + /// Negates the node. + pub fn negate(&mut self) { + self.mul_by(-1.); + } + + fn sort_key(&self) -> SortKey { + match *self { + Self::Leaf(ref l) => l.sort_key(), + _ => SortKey::Other, + } + } + + /// Tries to merge one sum to another, 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(()), + } + } + + /// 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::Sum(ref c) => CalcNode::Sum(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 } + }, + } + } + + /// Resolves the expression returning a value of `O`, given a function to + /// turn a leaf into the relevant value. + pub fn resolve<O>( + &self, + mut leaf_to_output_fn: impl FnMut(&L) -> Result<O, ()>, + ) -> Result<O, ()> + where + O: PartialOrd + PartialEq + Add<Output = O> + Zero, + { + self.resolve_internal(&mut leaf_to_output_fn) + } + + fn resolve_internal<O, F>(&self, leaf_to_output_fn: &mut F) -> Result<O, ()> + where + O: PartialOrd + PartialEq + Add<Output = O> + Zero, + F: FnMut(&L) -> Result<O, ()>, + { + Ok(match *self { + Self::Leaf(ref l) => return leaf_to_output_fn(l), + Self::Sum(ref c) => { + let mut result = Zero::zero(); + for child in &**c { + result = result + child.resolve_internal(leaf_to_output_fn)?; + } + result + }, + Self::MinMax(ref nodes, op) => { + let mut result = nodes[0].resolve_internal(leaf_to_output_fn)?; + for node in nodes.iter().skip(1) { + let candidate = node.resolve_internal(leaf_to_output_fn)?; + let candidate_wins = match op { + MinMaxOp::Min => candidate < result, + MinMaxOp::Max => candidate > result, + }; + if candidate_wins { + result = candidate; + } + } + result + }, + Self::Clamp { + ref min, + ref center, + ref 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)?; + + let mut result = center; + if result > max { + result = max; + } + if result < min { + result = min + } + result + }, + }) + } + + fn is_negative_leaf(&self) -> bool { + match *self { + Self::Leaf(ref l) => l.is_negative(), + _ => false, + } + } + + /// Multiplies the node by a scalar. + pub fn mul_by(&mut self, scalar: f32) { + match *self { + Self::Leaf(ref mut l) => l.mul_by(scalar), + // Multiplication is distributive across this. + Self::Sum(ref mut children) => { + for node in &mut **children { + node.mul_by(scalar); + } + }, + // This one is a bit trickier. + Self::MinMax(ref mut children, ref mut op) => { + for node in &mut **children { + node.mul_by(scalar); + } + + // For negatives we need to invert the operation. + if scalar < 0. { + *op = match *op { + MinMaxOp::Min => MinMaxOp::Max, + MinMaxOp::Max => MinMaxOp::Min, + } + } + }, + // This one is slightly tricky too. + Self::Clamp { + ref mut min, + ref mut center, + ref mut max, + } => { + min.mul_by(scalar); + center.mul_by(scalar); + max.mul_by(scalar); + // For negatives we need to swap min / max. + if scalar < 0. { + mem::swap(min, max); + } + }, + } + } + + /// 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::Sum(ref mut children) | Self::MinMax(ref mut children, _) => { + for child in &mut **children { + child.visit_depth_first_internal(f); + } + }, + Self::Leaf(..) => {}, + } + f(self); + } + + /// Simplifies and sorts the calculation of a given node. All the nodes + /// below it should be simplified already, this only takes care of + /// simplifying directly nested nodes. So, probably should always be used in + /// combination with `visit_depth_first()`. + /// + /// This is only needed if it's going to be preserved after parsing (so, for + /// `<length-percentage>`). Otherwise we can just evaluate it using + /// `resolve()`, and we'll come up with a simplified value anyways. + pub fn simplify_and_sort_direct_children(&mut self) { + macro_rules! replace_self_with { + ($slot:expr) => {{ + let dummy = Self::MinMax(Default::default(), MinMaxOp::Max); + let result = mem::replace($slot, dummy); + *self = result; + }}; + } + 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.partial_cmp(¢er) { + 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) { + return replace_self_with!(&mut **min); + } + + // Otherwise try with max. + let max_cmp_center = match max.partial_cmp(¢er) { + 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.partial_cmp(&min) { + 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) { + return replace_self_with!(&mut **min); + } + + return replace_self_with!(&mut **max); + } + + // Otherwise we're the center node. + return replace_self_with!(&mut **center); + }, + 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].partial_cmp(&children[result]) { + // 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 { + return replace_self_with!(&mut children_slot[0]); + } + + let mut children = mem::replace(children_slot, Default::default()).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::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>, is_outermost: bool) -> 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 + }, + _ => { + if is_outermost { + dest.write_str("calc(")?; + } + is_outermost + }, + }; + + match *self { + Self::MinMax(ref children, _) => { + let mut first = true; + for child in &**children { + if !first { + dest.write_str(", ")?; + } + first = false; + child.to_css_impl(dest, false)?; + } + }, + Self::Sum(ref children) => { + let mut first = true; + for child in &**children { + if !first { + if child.is_negative_leaf() { + dest.write_str(" - ")?; + let mut c = child.clone(); + c.negate(); + c.to_css_impl(dest, false)?; + } else { + dest.write_str(" + ")?; + child.to_css_impl(dest, false)?; + } + } else { + first = false; + child.to_css_impl(dest, false)?; + } + } + }, + Self::Clamp { + ref min, + ref center, + ref max, + } => { + min.to_css_impl(dest, false)?; + dest.write_str(", ")?; + center.to_css_impl(dest, false)?; + dest.write_str(", ")?; + max.to_css_impl(dest, false)?; + }, + Self::Leaf(ref l) => l.to_css(dest)?, + } + + if write_closing_paren { + dest.write_str(")")?; + } + Ok(()) + } +} + +impl<L: CalcNodeLeaf> PartialOrd for CalcNode<L> { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + match (self, other) { + (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => one.partial_cmp(other), + _ => None, + } + } +} + +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, /* is_outermost = */ true) + } +} diff --git a/servo/components/style/values/generics/color.rs b/servo/components/style/values/generics/color.rs new file mode 100644 index 0000000000..b4f2e7445e --- /dev/null +++ b/servo/components/style/values/generics/color.rs @@ -0,0 +1,112 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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. + +/// Ratios representing the contribution of color and currentcolor to +/// the final color value. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] +#[repr(C)] +pub struct ComplexColorRatios { + /// Numeric color contribution. + pub bg: f32, + /// currentcolor contribution. + pub fg: f32, +} + +impl ComplexColorRatios { + /// Ratios representing a `Numeric` color. + pub const NUMERIC: ComplexColorRatios = ComplexColorRatios { bg: 1., fg: 0. }; + /// Ratios representing the `CurrentColor` color. + pub const CURRENT_COLOR: ComplexColorRatios = ComplexColorRatios { bg: 0., fg: 1. }; +} + +/// This enum represents a combined color from a numeric color and +/// the current foreground color (currentcolor keyword). +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)] +#[repr(C, u8)] +pub enum GenericColor<RGBA> { + /// Numeric RGBA color. + Numeric(RGBA), + + /// The current foreground color. + CurrentColor, + + /// A linear combination of numeric color and currentcolor. + /// The formula is: `color * ratios.bg + currentcolor * ratios.fg`. + Complex { + /// The actual numeric color. + color: RGBA, + /// The ratios of mixing between numeric and currentcolor. + ratios: ComplexColorRatios, + }, +} + +pub use self::GenericColor as Color; + +impl<RGBA> Color<RGBA> { + /// Create a color based upon the specified ratios. + pub fn with_ratios(color: RGBA, ratios: ComplexColorRatios) -> Self { + if ratios == ComplexColorRatios::NUMERIC { + Color::Numeric(color) + } else if ratios == ComplexColorRatios::CURRENT_COLOR { + Color::CurrentColor + } else { + Color::Complex { color, ratios } + } + } + + /// Returns a numeric color representing the given RGBA value. + pub fn rgba(color: RGBA) -> Self { + Color::Numeric(color) + } + + /// Returns a complex color value representing currentcolor. + pub fn currentcolor() -> Self { + Color::CurrentColor + } + + /// Whether it is a numeric color (no currentcolor component). + pub fn is_numeric(&self) -> bool { + matches!(*self, Color::Numeric(..)) + } + + /// Whether it is a currentcolor value (no numeric color component). + pub fn is_currentcolor(&self) -> bool { + matches!(*self, Color::CurrentColor) + } +} + +impl<RGBA> From<RGBA> for Color<RGBA> { + fn from(color: RGBA) -> Self { + Self::rgba(color) + } +} + +/// Either `<color>` or `auto`. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericColorOrAuto<C> { + /// A `<color>`. + Color(C), + /// `auto` + Auto, +} + +pub use self::GenericColorOrAuto as ColorOrAuto; 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..fa7e77a8b5 --- /dev/null +++ b/servo/components/style/values/generics/counters.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 counters-related CSS values. + +#[cfg(feature = "servo")] +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::ops::Deref; + +/// A name / value pair for counters. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + 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, +} +pub use self::GenericCounterPair as CounterPair; + +/// 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>(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` and `counter-reset` properties. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct GenericCounterSetOrReset<I>(pub GenericCounters<I>); +pub use self::GenericCounterSetOrReset as CounterSetOrReset; + +impl<I> CounterSetOrReset<I> { + /// Returns a new value for `counter-set` / `counter-reset`. + #[inline] + pub fn new(counters: Vec<CounterPair<I>>) -> Self { + CounterSetOrReset(Counters(counters.into())) + } +} + +impl<I> Deref for CounterSetOrReset<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(iterable, if_empty = "none")] crate::OwnedSlice<GenericCounterPair<I>>, +); +pub use self::GenericCounters as Counters; + +#[cfg(feature = "servo")] +type CounterStyleType = ListStyleType; + +#[cfg(feature = "gecko")] +type CounterStyleType = CounterStyle; + +#[cfg(feature = "servo")] +#[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<ImageUrl> { + /// `normal` reserved keyword. + Normal, + /// `none` reserved keyword. + None, + /// Content items. + Items(#[css(iterable)] crate::OwnedSlice<GenericContentItem<ImageUrl>>), +} + +pub use self::GenericContent as Content; + +impl<ImageUrl> Content<ImageUrl> { + /// 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, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum GenericContentItem<ImageUrl> { + /// Literal string content. + String(crate::OwnedStr), + /// `counter(name, style)`. + #[css(comma, function)] + Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyleType), + /// `counters(name, separator, style)`. + #[css(comma, function)] + Counters( + CustomIdent, + crate::OwnedStr, + #[css(skip_if = "is_decimal")] CounterStyleType, + ), + /// `open-quote`. + OpenQuote, + /// `close-quote`. + CloseQuote, + /// `no-open-quote`. + NoOpenQuote, + /// `no-close-quote`. + NoCloseQuote, + /// `-moz-alt-content`. + #[cfg(feature = "gecko")] + MozAltContent, + /// `attr([namespace? `|`]? ident)` + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + Attr(Attr), + /// `url(url)` + Url(ImageUrl), +} + +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..b234b6a54e --- /dev/null +++ b/servo/components/style/values/generics/easing.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/. */ + +//! Generic types for CSS Easing Functions. +//! https://drafts.csswg.org/css-easing/#timing-functions + +use crate::parser::ParserContext; +use crate::values::CSSFloat; + +/// A generic easing function. +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[value_info(ty = "TIMING_FUNCTION")] +#[repr(u8, C)] +pub enum TimingFunction<Integer, Number> { + /// `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), +} + +#[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 TimingKeyword { + Linear, + Ease, + EaseIn, + EaseOut, + EaseInOut, +} + +#[cfg(feature = "gecko")] +fn step_position_jump_enabled(_context: &ParserContext) -> bool { + static_prefs::pref!("layout.css.step-position-jump.enabled") +} + +#[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, +)] +#[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> TimingFunction<Integer, Number> { + /// `ease` + #[inline] + pub fn ease() -> Self { + TimingFunction::Keyword(TimingKeyword::Ease) + } +} + +impl TimingKeyword { + /// Returns the keyword as a quadruplet of Bezier point coordinates + /// `(x1, y1, x2, y2)`. + #[inline] + pub fn to_bezier(self) -> (CSSFloat, CSSFloat, CSSFloat, CSSFloat) { + match self { + TimingKeyword::Linear => (0., 0., 1., 1.), + TimingKeyword::Ease => (0.25, 0.1, 0.25, 1.), + TimingKeyword::EaseIn => (0.42, 0., 1., 1.), + TimingKeyword::EaseOut => (0., 0., 0.58, 1.), + TimingKeyword::EaseInOut => (0.42, 0., 0.58, 1.), + } + } +} diff --git a/servo/components/style/values/generics/effects.rs b/servo/components/style/values/generics/effects.rs new file mode 100644 index 0000000000..dd9da8759b --- /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))] +#[animation(no_bound(U))] +#[derive( + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[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..e19c796d17 --- /dev/null +++ b/servo/components/style/values/generics/font.rs @@ -0,0 +1,204 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::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}; + +/// 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<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, +} + +/// A value both for font-variation-settings and font-feature-settings. +#[css(comma)] +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +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), +} diff --git a/servo/components/style/values/generics/grid.rs b/servo/components/style/values/generics/grid.rs new file mode 100644 index 0000000000..7995bf044a --- /dev/null +++ b/servo/components/style/values/generics/grid.rs @@ -0,0 +1,819 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::specified::grid::parse_line_names; +use crate::values::{CSSFloat, CustomIdent}; +use crate::{Atom, 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: Atom, + /// 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: atom!(""), + } + } + + /// Check whether this `<grid-line>` represents an `auto` value. + pub fn is_auto(&self) -> bool { + self.ident == 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 != 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, +{ + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_auto() { + return dest.write_str("auto"); + } + + if self.is_span { + dest.write_str("span")?; + } + + if !self.line_num.is_zero() { + if self.is_span { + dest.write_str(" ")?; + } + self.line_num.to_css(dest)?; + } + + if self.ident != atom!("") { + if self.is_span || !self.line_num.is_zero() { + dest.write_str(" ")?; + } + CustomIdent(self.ident.clone()).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 != 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| i.expect_ident_cloned()) { + if val_before_span || grid_line.ident != 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 = CustomIdent::from_ident(location, &name, &["auto"])?.0; + } 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 == 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_str(")") + }, + TrackSize::FitContent(ref lp) => { + dest.write_str("fit-content(")?; + lp.to_css(dest)?; + dest.write_str(")") + }, + } + } +} + +/// 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, 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. +/// +/// It can also hold `repeat()` function parameters, which expands into the respective +/// values in its computed form. +#[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_str(" ")?; + } + + 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_str(")")?; + + 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_str(" ")?; + } + + 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_str(" ")?; + } + } + + Ok(()) + } +} + +/// The `<line-name-list>` for subgrids. +/// +/// `subgrid [ <line-names> | repeat(<positive-integer> | auto-fill, <line-names>+) ]+` +/// +/// https://drafts.csswg.org/css-grid-2/#typedef-line-name-list +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct LineNameList { + /// The optional `<line-name-list>` + pub names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>, + /// Indicates the starting line names that requires `auto-fill`, if in bounds. + pub fill_start: usize, + /// Indicates the number of line names in the auto-fill + pub fill_len: usize, +} + +impl Parse for LineNameList { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_ident_matching("subgrid")?; + let mut line_names = vec![]; + let mut fill_data = None; + + loop { + let repeat_parse_result = input.try_parse(|input| { + input.expect_function_matching("repeat")?; + input.parse_nested_block(|input| { + let count = RepeatCount::parse(context, input)?; + input.expect_comma()?; + let mut names_list = vec![]; + names_list.push(parse_line_names(input)?); // there should be at least one + while let Ok(names) = input.try_parse(parse_line_names) { + names_list.push(names); + } + Ok((names_list, count)) + }) + }); + if let Ok((names_list, count)) = repeat_parse_result { + match count { + // FIXME(emilio): we shouldn't expand repeat() at + // parse time for subgrid. (bug 1583429) + RepeatCount::Number(num) => line_names.extend( + names_list + .iter() + .cloned() + .cycle() + .take(num.value() as usize * names_list.len()), + ), + RepeatCount::AutoFill if fill_data.is_none() => { + let fill_idx = line_names.len(); + let fill_len = names_list.len(); + fill_data = Some((fill_idx, fill_len)); + line_names.extend(names_list.into_iter()); + }, + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } else if let Ok(names) = input.try_parse(parse_line_names) { + line_names.push(names); + } else { + break; + } + } + + if line_names.len() > MAX_GRID_LINE as usize { + line_names.truncate(MAX_GRID_LINE as usize); + } + + let (fill_start, fill_len) = fill_data.unwrap_or((0, 0)); + + Ok(LineNameList { + names: line_names.into(), + fill_start: fill_start, + fill_len: fill_len, + }) + } +} + +impl ToCss for LineNameList { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("subgrid")?; + let fill_start = self.fill_start; + let fill_len = self.fill_len; + for (i, names) in self.names.iter().enumerate() { + if fill_len > 0 && i == fill_start { + dest.write_str(" repeat(auto-fill,")?; + } + + dest.write_str(" [")?; + + if let Some((ref first, rest)) = names.split_first() { + first.to_css(dest)?; + for name in rest { + dest.write_str(" ")?; + name.to_css(dest)?; + } + } + + dest.write_str("]")?; + if fill_len > 0 && i == fill_start + fill_len - 1 { + dest.write_str(")")?; + } + } + + Ok(()) + } +} + +/// Variants for `<grid-template-rows> | <grid-template-columns>` +#[derive( + Animate, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[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<LineNameList>), + /// `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..c9b8f8c0f8 --- /dev/null +++ b/servo/components/style/values/generics/image.rs @@ -0,0 +1,602 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::custom_properties; +use crate::values::generics::position::PositionComponent; +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, MozImageRect, 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-image-rect` image. Also fairly large and rare. + // not cfg’ed out on non-Gecko to avoid `error[E0392]: parameter `MozImageRect` is never used` + // Instead we make MozImageRect an empty enum + Rect(Box<MozImageRect>), + + /// 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> +#[css(comma, function = "cross-fade")] +#[derive( + Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem, ToCss, ToComputedValue, +)] +#[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>>, +} + +/// A `<percent> | none` value. Represents optional percentage values +/// assosicated with cross-fade images. +#[derive( + Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, ToCss, +)] +#[repr(C, u8)] +pub enum PercentOrNone<Percentage> { + /// `none` variant. + #[css(skip)] + None, + /// A percentage variant. + Percent(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: PercentOrNone<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 +#[css(comma, function = "image-set")] +#[derive( + Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem, ToCss, +)] +#[repr(C)] +pub struct GenericImageSet<Image, Resolution> { + /// The index of the selected candidate. Zero for specified values. + #[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, ToCss, +)] +#[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, + // TODO: type() function. +} + +pub use self::GenericImageSet as ImageSet; +pub use self::GenericImageSetItem as ImageSetItem; + +/// 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, + /// The color stops and interpolation hints. + items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>, + /// True if this is a repeating gradient. + repeating: bool, + /// Compatibility mode. + compat_mode: GradientCompatMode, + }, + /// A radial gradient. + Radial { + /// Shape of gradient + shape: GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>, + /// Center of gradient + position: Position, + /// The color stops and interpolation hints. + items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>, + /// True if this is a repeating gradient. + repeating: bool, + /// Compatibility mode. + compat_mode: GradientCompatMode, + }, + /// A conic gradient. + Conic { + /// Start angle of gradient + angle: Angle, + /// Center of gradient + position: Position, + /// The color stops and interpolation hints. + items: crate::OwnedSlice<GenericGradientItem<Color, AngleOrPercentage>>, + /// True if this is a repeating gradient. + repeating: bool, + }, +} + +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_str(")") + } +} + +/// Values for `moz-image-rect`. +/// +/// `-moz-image-rect(<uri>, top, right, bottom, left);` +#[allow(missing_docs)] +#[css(comma, function = "-moz-image-rect")] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericMozImageRect<NumberOrPercentage, MozImageRectUrl> { + pub url: MozImageRectUrl, + pub top: NumberOrPercentage, + pub right: NumberOrPercentage, + pub bottom: NumberOrPercentage, + pub left: NumberOrPercentage, +} + +pub use self::GenericMozImageRect as MozImageRect; + +impl<G, R, U, C, P, Resolution> fmt::Debug for Image<G, R, U, C, P, Resolution> +where + Image<G, R, U, C, P, Resolution>: ToCss, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(&mut CssWriter::new(f)) + } +} + +impl<G, R, U, C, P, Resolution> ToCss for Image<G, R, U, C, P, Resolution> +where + G: ToCss, + R: 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), + Image::Rect(ref rect) => rect.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_str(")") + }, + 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) = match *self { + Gradient::Linear { + compat_mode, + repeating, + .. + } => (compat_mode, repeating), + Gradient::Radial { + compat_mode, + repeating, + .. + } => (compat_mode, repeating), + Gradient::Conic { repeating, .. } => (GradientCompatMode::Modern, repeating), + }; + + 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 items, + compat_mode, + .. + } => { + dest.write_str("linear-gradient(")?; + let mut skip_comma = if !direction.points_downwards(compat_mode) { + direction.to_css(dest, compat_mode)?; + false + } else { + true + }; + for item in &**items { + if !skip_comma { + dest.write_str(", ")?; + } + skip_comma = false; + item.to_css(dest)?; + } + }, + Gradient::Radial { + ref shape, + ref position, + 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_str(" ")?; + } + } + 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)?; + } + } + let mut skip_comma = omit_shape && omit_position; + for item in &**items { + if !skip_comma { + dest.write_str(", ")?; + } + skip_comma = false; + item.to_css(dest)?; + } + }, + Gradient::Conic { + ref angle, + ref position, + 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_str(" ")?; + } + } + if !omit_position { + dest.write_str("at ")?; + position.to_css(dest)?; + } + let mut skip_comma = omit_angle && omit_position; + for item in &**items { + if !skip_comma { + dest.write_str(", ")?; + } + skip_comma = false; + item.to_css(dest)?; + } + }, + } + dest.write_str(")") + } +} + +/// 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..3ac358e781 --- /dev/null +++ b/servo/components/style/values/generics/length.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/. */ + +//! Generic types for CSS values related to length. + +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::values::computed::ExtremumLength; +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 Fn() -> 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, + #[cfg(feature = "gecko")] + #[animation(error)] + ExtremumLength(ExtremumLength), +} + +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, + #[cfg(feature = "gecko")] + #[animation(error)] + ExtremumLength(ExtremumLength), +} + +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 `-moz-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..09a99432e1 --- /dev/null +++ b/servo/components/style/values/generics/mod.rs @@ -0,0 +1,294 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind}; + +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 position; +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"))) + } +} + +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, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + 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) + } +} diff --git a/servo/components/style/values/generics/motion.rs b/servo/components/style/values/generics/motion.rs new file mode 100644 index 0000000000..25ba5873ad --- /dev/null +++ b/servo/components/style/values/generics/motion.rs @@ -0,0 +1,117 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::specified::SVGPathData; + +/// The <size> in ray() function. +/// +/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-size +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum RaySize { + ClosestSide, + ClosestCorner, + FarthestSide, + FarthestCorner, + Sides, +} + +/// The `ray()` function, `ray( [ <angle> && <size> && contain? ] )` +/// +/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-ray +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct RayFunction<Angle> { + /// 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. + #[animation(constant)] + pub size: RaySize, + /// Clamp `offset-distance` so that the box is entirely contained + /// within the path. + #[animation(constant)] + #[css(represents_keyword)] + pub contain: bool, +} + +/// The offset-path value. +/// +/// https://drafts.fxtf.org/motion-1/#offset-path-property +/// cbindgen:private-default-tagged-enum-constructor=false +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum GenericOffsetPath<Angle> { + // We could merge SVGPathData into ShapeSource, so we could reuse them. However, + // we don't want to support other value for offset-path, so use SVGPathData only for now. + /// Path value for path(<string>). + #[css(function)] + Path(SVGPathData), + /// ray() function, which defines a path in the polar coordinate system. + #[css(function)] + Ray(RayFunction<Angle>), + /// None value. + #[animation(error)] + None, + // Bug 1186329: Implement <basic-shape>, <geometry-box>, and <url>. +} + +pub use self::GenericOffsetPath as OffsetPath; + +impl<Angle> OffsetPath<Angle> { + /// Return None. + #[inline] + pub fn none() -> Self { + OffsetPath::None + } +} diff --git a/servo/components/style/values/generics/position.rs b/servo/components/style/values/generics/position.rs new file mode 100644 index 0000000000..45605b1b8c --- /dev/null +++ b/servo/components/style/values/generics/position.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/. */ + +//! Generic types for CSS handling of specified and computed values of +//! [`position`](https://drafts.csswg.org/css-backgrounds-3/#position) + +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// 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 +/// cbindgen:private-default-tagged-enum-constructor=false +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + 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 + } +} + +/// 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, + } + } +} + +/// A generic value for the `<ratio>` value. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedZero, + 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(()) + } +} + +/// Ratio or None. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum PreferredRatio<N> { + /// Without specified ratio + #[css(skip)] + None, + /// With specified ratio + Ratio(#[css(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, + ToAnimatedZero, + 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. + #[css(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, + } + } +} diff --git a/servo/components/style/values/generics/rect.rs b/servo/components/style/values/generics/rect.rs new file mode 100644 index 0000000000..dee21459a7 --- /dev/null +++ b/servo/components/style/values/generics/rect.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/. */ + +//! 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, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + 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)) + } +} + +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_str(" ")?; + self.1.to_css(dest)?; + if same_vertical && same_horizontal { + return Ok(()); + } + dest.write_str(" ")?; + self.2.to_css(dest)?; + if same_horizontal { + return Ok(()); + } + dest.write_str(" ")?; + 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..e00eef28b9 --- /dev/null +++ b/servo/components/style/values/generics/size.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/. */ + +//! 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, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + 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_str(" ")?; + 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..26a33228e8 --- /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> +#[animation(no_bound(Url))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[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. +#[animation(no_bound(U))] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[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..0062f30470 --- /dev/null +++ b/servo/components/style/values/generics/text.rs @@ -0,0 +1,157 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::values::animated::ToAnimatedZero; +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) + } +} + +#[cfg(feature = "gecko")] +fn line_height_moz_block_height_enabled(context: &ParserContext) -> bool { + context.in_ua_sheet() || + static_prefs::pref!("layout.css.line-height-moz-block-height.content.enabled") +} + +/// A generic value for the `line-height` property. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToCss, + ToShmem, + ToResolvedValue, + Parse, +)] +#[repr(C, u8)] +pub enum GenericLineHeight<N, L> { + /// `normal` + Normal, + /// `-moz-block-height` + #[cfg(feature = "gecko")] + #[parse(condition = "line_height_moz_block_height_enabled")] + 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 + } +} + +/// 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, +} diff --git a/servo/components/style/values/generics/transform.rs b/servo/components/style/values/generics/transform.rs new file mode 100644 index 0000000000..07ae2fa0dc --- /dev/null +++ b/servo/components/style/values/generics/transform.rs @@ -0,0 +1,830 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; +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)] +#[css(comma, function = "matrix3d")] +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[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 +} + +#[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, + 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 = "Zero::is_zero")] 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(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, + 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, ()> { + match containing_len { + Some(relative_len) => Ok(self.resolve(relative_len).px()), + // If we don't have reference box, we cannot resolve the used value, + // so only retrieve the length part. This will be used for computing + // distance without any layout info. + // + // FIXME(emilio): This looks wrong. + None => Ok(self.resolve(Zero::zero()).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, +{ + #[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::*; + use std::f64; + + 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 d) => { + let m = create_perspective_matrix(d.to_pixel_length(None)?); + m.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), ()> { + 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.to_transform_3d_matrix_f64(reference_box)?; + Ok((cast_3d_transform(m), is_3d)) + } + + /// Same as Transform::to_transform_3d_matrix but a f64 version. + pub fn to_transform_3d_matrix_f64( + &self, + 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 &*self.0 { + let matrix = operation.to_3d_matrix(reference_box)?; + 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> { + // TODO(gw): The transforms spec says that perspective length must + // be positive. However, there is some confusion between the spec + // and browser implementations as to handling the case of 0 for the + // perspective value. Until the spec bug is resolved, at least ensure + // that a provided perspective value of <= 0.0 doesn't cause panics + // and behaves as it does in other browsers. + // See https://lists.w3.org/Archives/Public/www-style/2016Jan/0020.html for more details. + if d <= 0.0 { + Transform3D::identity() + } else { + Transform3D::perspective(d) + } +} + +/// 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> +/// cbindgen:private-default-tagged-enum-constructor=false +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, + 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 a 3d rotation is specified, the property must serialize with an axis + // specified. If the axis is parallel with the x, y, or z axises, it must + // serialize as the appropriate keyword. + // https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization + let v = (x, y, z); + if v.is_parallel_to(&DirectionVector::new(1., 0., 0.)) { + dest.write_char('x')?; + } else if v.is_parallel_to(&DirectionVector::new(0., 1., 0.)) { + dest.write_char('y')?; + } else if v.is_parallel_to(&DirectionVector::new(0., 0., 1.)) { + dest.write_char('z')?; + } else { + 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> +/// cbindgen:private-default-tagged-enum-constructor=false +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, Length: Zero>( + _: &LengthPercentage, + y: &LengthPercentage, + z: &Length, +) -> bool { + y.is_zero() && 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). +/// +/// 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> +/// cbindgen:private-default-tagged-enum-constructor=false +pub enum GenericTranslate<LengthPercentage, Length> +where + LengthPercentage: Zero, + 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..04c2951c70 --- /dev/null +++ b/servo/components/style/values/generics/ui.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/. */ + +//! 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, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct GenericCursorImage<ImageUrl, Number> { + /// The url to parse images from. + pub url: ImageUrl, + /// 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<ImageUrl: ToCss, Number: ToCss> ToCss for CursorImage<ImageUrl, Number> { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.url.to_css(dest)?; + if self.has_hotspot { + dest.write_str(" ")?; + self.hotspot_x.to_css(dest)?; + dest.write_str(" ")?; + 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..9587b52648 --- /dev/null +++ b/servo/components/style/values/mod.rs @@ -0,0 +1,425 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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, RGBA}; +use precomputed_hash::PrecomputedHash; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Debug, Write}; +use std::hash; +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; + +define_keyword_type!(None_, "none"); +define_keyword_type!(Auto, "auto"); + +/// 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) +} + +/// 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); + +impl cssparser::ToCss for AtomString { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: Write, + { + self.0 + .with_str(|s| cssparser::CssStringWriter::new(dest).write_str(s)) + } +} + +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 CSS `<ident>` stored as an `Atom`. +#[repr(transparent)] +#[derive( + Clone, + Debug, + Default, + Deref, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct AtomIdent(pub Atom); + +impl cssparser::ToCss for AtomIdent { + fn to_css<W>(&self, dest: &mut W) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +impl PrecomputedHash for AtomIdent { + #[inline] + fn precomputed_hash(&self) -> u32 { + self.0.precomputed_hash() + } +} + +impl<'a> From<&'a str> for AtomIdent { + #[inline] + fn from(string: &str) -> Self { + Self(Atom::from(string)) + } +} + +impl AtomIdent { + /// Like `Atom::with` but for `AtomIdent`. + #[cfg(feature = "gecko")] + 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) + }) + } +} + +#[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 normalized value into percentage. +pub fn serialize_percentage<W>(value: CSSFloat, dest: &mut CssWriter<W>) -> fmt::Result +where + W: Write, +{ + (value * 100.).to_css(dest)?; + dest.write_str("%") +} + +/// 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, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CustomIdent(pub Atom); + +impl CustomIdent { + /// Parse an already-tokenizer identifier + pub fn from_ident<'i>( + location: SourceLocation, + ident: &CowRcStr<'i>, + excluding: &[&str], + ) -> Result<Self, ParseError<'i>> { + let valid = match_ignore_ascii_case! { ident, + "initial" | "inherit" | "unset" | "default" | "revert" => false, + _ => true + }; + if !valid { + 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()))) + } + } +} + +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://drafts.csswg.org/css-animations/#typedef-keyframes-name> +#[derive( + Clone, Debug, MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, +)] +pub enum KeyframesName { + /// <custom-ident> + Ident(CustomIdent), + /// <string> + QuotedString(Atom), +} + +impl KeyframesName { + /// <https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name> + pub fn from_ident(value: &str) -> Self { + let location = SourceLocation { line: 0, column: 0 }; + let custom_ident = CustomIdent::from_ident(location, &value.into(), &["none"]).ok(); + match custom_ident { + Some(ident) => KeyframesName::Ident(ident), + None => KeyframesName::QuotedString(value.into()), + } + } + + /// Create a new KeyframesName from Atom. + #[cfg(feature = "gecko")] + pub fn from_atom(atom: Atom) -> Self { + debug_assert_ne!(atom, atom!("")); + + // FIXME: We might want to preserve <string>, but currently Gecko + // stores both of <custom-ident> and <string> into nsAtom, so + // we can't tell it. + KeyframesName::Ident(CustomIdent(atom)) + } + + /// The name as an Atom + pub fn as_atom(&self) -> &Atom { + match *self { + KeyframesName::Ident(ref ident) => &ident.0, + KeyframesName::QuotedString(ref atom) => atom, + } + } +} + +impl Eq for KeyframesName {} + +/// A trait that returns whether a given type is the `auto` value or not. So far +/// only needed for background-size serialization, which special-cases `auto`. +pub trait IsAuto { + /// Returns whether the value is the `auto` value. + fn is_auto(&self) -> bool; +} + +impl PartialEq for KeyframesName { + fn eq(&self, other: &Self) -> bool { + self.as_atom() == other.as_atom() + } +} + +impl hash::Hash for KeyframesName { + fn hash<H>(&self, state: &mut H) + where + H: hash::Hasher, + { + self.as_atom().hash(state) + } +} + +impl Parse for KeyframesName { + 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::Ident(ref s) => Ok(KeyframesName::Ident(CustomIdent::from_ident( + location, + s, + &["none"], + )?)), + Token::QuotedString(ref s) => Ok(KeyframesName::QuotedString(Atom::from(s.as_ref()))), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl ToCss for KeyframesName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + KeyframesName::Ident(ref ident) => ident.to_css(dest), + KeyframesName::QuotedString(ref atom) => atom.to_string().to_css(dest), + } + } +} diff --git a/servo/components/style/values/resolved/color.rs b/servo/components/style/values/resolved/color.rs new file mode 100644 index 0000000000..1b845c58e9 --- /dev/null +++ b/servo/components/style/values/resolved/color.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/. */ + +//! Resolved color values. + +use super::{Context, ToResolvedValue}; + +use crate::values::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 = cssparser::RGBA; + + #[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::Numeric(resolved) + } +} + +impl ToResolvedValue for computed::ColorOrAuto { + // A resolved caret-color value is an rgba color, with auto resolving to + // currentcolor. + type ResolvedValue = cssparser::RGBA; + + #[inline] + fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue { + let color = match self { + 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::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..bf6490f740 --- /dev/null +++ b/servo/components/style/values/resolved/counters.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/. */ + +//! 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_before_or_after = context + .style + .pseudo() + .map_or(false, |p| p.is_before_or_after()); + + match self { + Self::Normal if is_before_or_after => Self::None, + // For now, make `content: none` compute to `normal` on other + // elements, as we don't respect it. + // + // FIXME(emilio, bug 1605473): for marker this should be preserved + // and respected, probably. + Self::None if !is_before_or_after => 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..45cc5cbd8c --- /dev/null +++ b/servo/components/style/values/resolved/mod.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/. */ + +//! Resolved values. These are almost always computed values, but in some cases +//! there are used values. + +use crate::properties::ComputedValues; +use crate::ArcSlice; +use cssparser; +use servo_arc::Arc; +use smallvec::SmallVec; + +mod color; +mod counters; + +use crate::values::computed; + +/// 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, + // TODO(emilio): Add layout box information, and maybe property-specific + // information? +} + +/// 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!(i32); +trivial_to_resolved_value!(u8); +trivial_to_resolved_value!(i8); +trivial_to_resolved_value!(u16); +trivial_to_resolved_value!(u32); +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!(cssparser::RGBA); +trivial_to_resolved_value!(crate::Atom); +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!(html5ever::Namespace); +#[cfg(feature = "servo")] +trivial_to_resolved_value!(html5ever::Prefix); +trivial_to_resolved_value!(computed::LengthPercentage); +trivial_to_resolved_value!(style_traits::values::specified::AllowedNumericType); + +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..ebb6f63834 --- /dev/null +++ b/servo/components/style/values/specified/align.rs @@ -0,0 +1,817 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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}; + +bitflags! { + /// Constants shared by multiple CSS Box Alignment properties + #[derive(MallocSizeOf, ToComputedValue, ToResolvedValue, ToShmem)] + #[repr(C)] + pub struct 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..fa60f66507 --- /dev/null +++ b/servo/components/style/values/specified/angle.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/. */ + +//! 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 { + match *self { + AngleDimension::Deg(ref f) | + AngleDimension::Grad(ref f) | + AngleDimension::Rad(ref f) | + AngleDimension::Turn(ref f) => *f == 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, + } + } +} + +/// 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, + { + if self.was_calc { + dest.write_str("calc(")?; + } + self.value.to_css(dest)?; + if self.was_calc { + dest.write_str(")")?; + } + Ok(()) + } +} + +impl ToComputedValue for Angle { + type ComputedValue = ComputedAngle; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + ComputedAngle::from_degrees(self.degrees()) + } + + #[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, + } + } + + /// 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() + } + + /// 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, + } + } +} + +/// 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(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/background.rs b/servo/components/style/values/specified/background.rs new file mode 100644 index 0000000000..29f6d6b352 --- /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_str(" ")?; + 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..37aaae2ec2 --- /dev/null +++ b/servo/components/style/values/specified/basic_shape.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/. */ + +//! 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::generics::basic_shape as generic; +use crate::values::generics::basic_shape::{Path, PolygonCoord}; +use crate::values::generics::rect::Rect; +use crate::values::specified::border::BorderRadius; +use crate::values::specified::image::Image; +use crate::values::specified::position::{HorizontalPosition, Position, VerticalPosition}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::SVGPathData; +use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// 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 basic shape. +pub type BasicShape = generic::GenericBasicShape< + HorizontalPosition, + VerticalPosition, + LengthPercentage, + NonNegativeLengthPercentage, +>; + +/// The specified value of `inset()` +pub type InsetRect = generic::InsetRect<LengthPercentage, NonNegativeLengthPercentage>; + +/// A specified circle. +pub type Circle = + generic::Circle<HorizontalPosition, VerticalPosition, NonNegativeLengthPercentage>; + +/// A specified ellipse. +pub type Ellipse = + generic::Ellipse<HorizontalPosition, VerticalPosition, NonNegativeLengthPercentage>; + +/// The specified value of `ShapeRadius` +pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>; + +/// The specified value of `Polygon` +pub type Polygon = generic::GenericPolygon<LengthPercentage>; + +#[cfg(feature = "gecko")] +fn is_clip_path_path_enabled(context: &ParserContext) -> bool { + context.chrome_rules_enabled() || static_prefs::pref!("layout.css.clip-path-path.enabled") +} +#[cfg(feature = "servo")] +fn is_clip_path_path_enabled(_: &ParserContext) -> bool { + false +} + +/// 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, +) -> Result<R, ParseError<'i>> +where + ReferenceBox: Default + Parse, +{ + fn parse_component<U: Parse>( + context: &ParserContext, + input: &mut Parser, + component: &mut Option<U>, + ) -> bool { + if component.is_some() { + return false; // already parsed this component + } + + *component = input.try_parse(|i| U::parse(context, i)).ok(); + component.is_some() + } + + let mut shape = None; + let mut ref_box = None; + + while parse_component(context, input, &mut shape) || + parse_component(context, input, &mut ref_box) + { + // + } + + 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 is_clip_path_path_enabled(context) { + if let Ok(p) = input.try_parse(|i| Path::parse(context, i)) { + return Ok(ClipPath::Path(p)); + } + } + + 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) + } +} + +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) + } +} + +impl Parse for BasicShape { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> 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" => return InsetRect::parse_function_arguments(context, i).map(generic::BasicShape::Inset), + "circle" => return Circle::parse_function_arguments(context, i).map(generic::BasicShape::Circle), + "ellipse" => return Ellipse::parse_function_arguments(context, i).map(generic::BasicShape::Ellipse), + "polygon" => return Polygon::parse_function_arguments(context, i).map(generic::BasicShape::Polygon), + _ => Err(()) + }).map_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)) + } +} + +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 = if input + .try_parse(|i| i.expect_ident_matching("round")) + .is_ok() + { + BorderRadius::parse(context, input)? + } else { + BorderRadius::zero() + }; + Ok(generic::InsetRect { rect, round }) + } +} + +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 = if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + Position::parse(context, input)? + } else { + Position::center() + }; + + 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 (a, b) = input + .try_parse(|i| -> Result<_, ParseError> { + Ok(( + ShapeRadius::parse(context, i)?, + ShapeRadius::parse(context, i)?, + )) + }) + .unwrap_or_default(); + let position = if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + Position::parse(context, input)? + } else { + Position::center() + }; + + Ok(generic::Ellipse { + semiaxis_x: a, + semiaxis_y: b, + position: position, + }) + } +} + +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)) + } +} + +impl Polygon { + /// Parse the inner arguments of a `polygon` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let fill = input + .try_parse(|i| -> Result<_, ParseError> { + let fill = FillRule::parse(i)?; + i.expect_comma()?; // only eat the comma if there is something before it + Ok(fill) + }) + .unwrap_or_default(); + + let coordinates = input + .parse_comma_separated(|i| { + Ok(PolygonCoord( + LengthPercentage::parse(context, i)?, + LengthPercentage::parse(context, i)?, + )) + })? + .into(); + + Ok(Polygon { fill, coordinates }) + } +} + +impl Parse for Path { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("path")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Path { + /// Parse the inner arguments of a `path` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let fill = input + .try_parse(|i| -> Result<_, ParseError> { + let fill = FillRule::parse(i)?; + i.expect_comma()?; + Ok(fill) + }) + .unwrap_or_default(); + let path = SVGPathData::parse(context, input)?; + Ok(Path { fill, path }) + } +} diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs new file mode 100644 index 0000000000..817a5aaddd --- /dev/null +++ b/servo/components/style/values/specified/border.rs @@ -0,0 +1,311 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::{self, 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::{NonNegativeLength, NonNegativeLengthPercentage}; +use crate::values::specified::{AllowQuirks, NonNegativeNumber, NonNegativeNumberOrPercentage}; +use crate::Zero; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{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 a single side of the `border-width` property. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum BorderSideWidth { + /// `thin` + Thin, + /// `medium` + Medium, + /// `thick` + Thick, + /// `<length>` + Length(NonNegativeLength), +} + +/// 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, + } + } +} + +impl BorderSideWidth { + /// Returns the `0px` value. + #[inline] + pub fn zero() -> Self { + BorderSideWidth::Length(NonNegativeLength::zero()) + } + + /// Parses, with quirks. + pub 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(BorderSideWidth::Length(length)); + } + try_match_ident_ignore_ascii_case! { input, + "thin" => Ok(BorderSideWidth::Thin), + "medium" => Ok(BorderSideWidth::Medium), + "thick" => Ok(BorderSideWidth::Thick), + } + } +} + +impl Parse for BorderSideWidth { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for BorderSideWidth { + type ComputedValue = computed::NonNegativeLength; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + // We choose the pixel length of the keyword values the same as both spec and gecko. + // Spec: https://drafts.csswg.org/css-backgrounds-3/#line-width + // Gecko: https://bugzilla.mozilla.org/show_bug.cgi?id=1312155#c0 + match *self { + BorderSideWidth::Thin => NonNegativeLength::from_px(1.).to_computed_value(context), + BorderSideWidth::Medium => NonNegativeLength::from_px(3.).to_computed_value(context), + BorderSideWidth::Thick => NonNegativeLength::from_px(5.).to_computed_value(context), + BorderSideWidth::Length(ref length) => length.to_computed_value(context), + } + .into() + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + BorderSideWidth::Length(ToComputedValue::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_str(" ")?; + 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), + )) + } +} diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs new file mode 100644 index 0000000000..6511418870 --- /dev/null +++ b/servo/components/style/values/specified/box.rs @@ -0,0 +1,1996 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::custom_properties::Name as CustomPropertyName; +use crate::parser::{Parse, ParserContext}; +use crate::properties::{LonghandId, PropertyDeclarationId, PropertyFlags}; +use crate::properties::{PropertyId, ShorthandId}; +use crate::values::generics::box_::AnimationIterationCount as GenericAnimationIterationCount; +use crate::values::generics::box_::Perspective as GenericPerspective; +use crate::values::generics::box_::{GenericVerticalAlign, VerticalAlignKeyword}; +use crate::values::specified::length::{LengthPercentage, NonNegativeLength}; +use crate::values::specified::{AllowQuirks, Number}; +use crate::values::{CustomIdent, KeyframesName}; +use crate::Atom; +use cssparser::Parser; +use num_traits::FromPrimitive; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(feature = "gecko")] +fn moz_display_values_enabled(context: &ParserContext) -> bool { + context.in_ua_or_chrome_sheet() || + static_prefs::pref!("layout.css.xul-display-values.content.enabled") +} + +#[cfg(feature = "gecko")] +fn moz_box_display_values_enabled(context: &ParserContext) -> bool { + context.in_ua_or_chrome_sheet() || + static_prefs::pref!("layout.css.xul-box-display-values.content.enabled") +} + +fn flexbox_enabled() -> bool { + #[cfg(feature = "servo-layout-2020")] + { + return servo_config::prefs::pref_map() + .get("layout.flexbox.enabled") + .as_bool() + .unwrap_or(false); + } + + true +} + +/// 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, + #[cfg(feature = "gecko")] + XUL, +} + +#[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, + #[cfg(feature = "gecko")] + MozBox, + #[cfg(feature = "gecko")] + MozStack, + #[cfg(feature = "gecko")] + MozDeck, + #[cfg(feature = "gecko")] + MozPopup, +} + +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +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 + const LIST_ITEM_BIT: u16 = 0x8000; //^ + const DISPLAY_OUTSIDE_BITS: u16 = 7; // ^^^^^^^ + const DISPLAY_INSIDE_BITS: u16 = 8; // ^^^^^^^^ + + /// https://drafts.csswg.org/css-display/#the-display-properties + pub const None: Self = Self::new(DisplayOutside::None, DisplayInside::None); + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + pub const Contents: Self = Self::new(DisplayOutside::None, DisplayInside::Contents); + pub const Inline: Self = Self::new(DisplayOutside::Inline, DisplayInside::Flow); + pub const InlineBlock: Self = Self::new(DisplayOutside::Inline, DisplayInside::FlowRoot); + pub const Block: Self = Self::new(DisplayOutside::Block, DisplayInside::Flow); + #[cfg(feature = "gecko")] + pub const FlowRoot: Self = Self::new(DisplayOutside::Block, DisplayInside::FlowRoot); + pub const Flex: Self = Self::new(DisplayOutside::Block, DisplayInside::Flex); + pub const InlineFlex: Self = Self::new(DisplayOutside::Inline, DisplayInside::Flex); + #[cfg(feature = "gecko")] + pub const Grid: Self = Self::new(DisplayOutside::Block, DisplayInside::Grid); + #[cfg(feature = "gecko")] + pub const InlineGrid: Self = Self::new(DisplayOutside::Inline, DisplayInside::Grid); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const Table: Self = Self::new(DisplayOutside::Block, DisplayInside::Table); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const InlineTable: Self = Self::new(DisplayOutside::Inline, DisplayInside::Table); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCaption: Self = Self::new(DisplayOutside::TableCaption, DisplayInside::Flow); + #[cfg(feature = "gecko")] + pub const Ruby: Self = Self::new(DisplayOutside::Inline, DisplayInside::Ruby); + #[cfg(feature = "gecko")] + pub const WebkitBox: Self = Self::new(DisplayOutside::Block, DisplayInside::WebkitBox); + #[cfg(feature = "gecko")] + pub const WebkitInlineBox: Self = Self::new(DisplayOutside::Inline, DisplayInside::WebkitBox); + + // Internal table boxes. + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRowGroup: Self = + Self::new(DisplayOutside::InternalTable, DisplayInside::TableRowGroup); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableHeaderGroup: Self = Self::new( + DisplayOutside::InternalTable, + DisplayInside::TableHeaderGroup, + ); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableFooterGroup: Self = Self::new( + DisplayOutside::InternalTable, + DisplayInside::TableFooterGroup, + ); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumn: Self = + Self::new(DisplayOutside::InternalTable, DisplayInside::TableColumn); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumnGroup: Self = Self::new( + DisplayOutside::InternalTable, + DisplayInside::TableColumnGroup, + ); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRow: Self = Self::new(DisplayOutside::InternalTable, DisplayInside::TableRow); + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCell: Self = Self::new(DisplayOutside::InternalTable, DisplayInside::TableCell); + + /// Internal ruby boxes. + #[cfg(feature = "gecko")] + pub const RubyBase: Self = Self::new(DisplayOutside::InternalRuby, DisplayInside::RubyBase); + #[cfg(feature = "gecko")] + pub const RubyBaseContainer: Self = Self::new( + DisplayOutside::InternalRuby, + DisplayInside::RubyBaseContainer, + ); + #[cfg(feature = "gecko")] + pub const RubyText: Self = Self::new(DisplayOutside::InternalRuby, DisplayInside::RubyText); + #[cfg(feature = "gecko")] + pub const RubyTextContainer: Self = Self::new( + DisplayOutside::InternalRuby, + DisplayInside::RubyTextContainer, + ); + + /// XUL boxes. + #[cfg(feature = "gecko")] + pub const MozBox: Self = Self::new(DisplayOutside::Block, DisplayInside::MozBox); + #[cfg(feature = "gecko")] + pub const MozInlineBox: Self = Self::new(DisplayOutside::Inline, DisplayInside::MozBox); + #[cfg(feature = "gecko")] + pub const MozStack: Self = Self::new(DisplayOutside::XUL, DisplayInside::MozStack); + #[cfg(feature = "gecko")] + pub const MozDeck: Self = Self::new(DisplayOutside::XUL, DisplayInside::MozDeck); + #[cfg(feature = "gecko")] + pub const MozPopup: Self = Self::new(DisplayOutside::XUL, DisplayInside::MozPopup); + + /// Make a raw display value from <display-outside> and <display-inside> values. + #[inline] + const fn new(outside: DisplayOutside, inside: DisplayInside) -> Self { + let o: u16 = ((outside as u8) as u16) << Self::DISPLAY_INSIDE_BITS; + let i: u16 = (inside as u8) as u16; + Self(o | i) + } + + /// 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_BIT) + } + + /// Accessor for the <display-inside> value. + #[inline] + pub fn inside(&self) -> DisplayInside { + DisplayInside::from_u16(self.0 & ((1 << Self::DISPLAY_INSIDE_BITS) - 1)).unwrap() + } + + /// Accessor for the <display-outside> value. + #[inline] + pub fn outside(&self) -> DisplayOutside { + DisplayOutside::from_u16( + (self.0 >> Self::DISPLAY_INSIDE_BITS) & ((1 << Self::DISPLAY_OUTSIDE_BITS) - 1), + ) + .unwrap() + } + + /// 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_BIT) != 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. +#[allow(non_upper_case_globals)] +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 { + match *self { + Display::Inline => true, + #[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 + } +} + +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(feature = "gecko")] + Display::MozInlineBox => dest.write_str("-moz-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_str(" ")?; + } + if inside != DisplayInside::Flow { + inside.to_css(dest)?; + dest.write_str(" ")?; + } + dest.write_str("list-item") + } else { + inside.to_css(dest) + } + }, + }, + } + } +} + +/// <display-inside> = flow | flow-root | table | flex | grid | ruby +/// https://drafts.csswg.org/css-display/#typedef-display-inside +fn parse_display_inside<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<DisplayInside, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "flow" => DisplayInside::Flow, + "flex" if flexbox_enabled() => DisplayInside::Flex, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "flow-root" => DisplayInside::FlowRoot, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table" => DisplayInside::Table, + #[cfg(feature = "gecko")] + "grid" => DisplayInside::Grid, + #[cfg(feature = "gecko")] + "ruby" => DisplayInside::Ruby, + }) +} + +/// <display-outside> = block | inline | run-in +/// https://drafts.csswg.org/css-display/#typedef-display-outside +fn parse_display_outside<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<DisplayOutside, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "block" => DisplayOutside::Block, + "inline" => DisplayOutside::Inline, + // FIXME(bug 2056): not supported in layout yet: + //"run-in" => DisplayOutside::RunIn, + }) +} + +/// (flow | flow-root)? +fn parse_display_inside_for_list_item<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<DisplayInside, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "flow" => DisplayInside::Flow, + #[cfg(feature = "gecko")] + "flow-root" => DisplayInside::FlowRoot, + }) +} +/// Test a <display-inside> Result for same values as above. +fn is_valid_inside_for_list_item<'i>(inside: &Result<DisplayInside, ParseError<'i>>) -> bool { + match inside { + Ok(DisplayInside::Flow) => true, + #[cfg(feature = "gecko")] + Ok(DisplayInside::FlowRoot) => true, + _ => false, + } +} + +/// Parse `list-item`. +fn parse_list_item<'i, 't>(input: &mut Parser<'i, 't>) -> Result<(), ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "list-item" => (), + }) +} + +impl Parse for Display { + #[allow(unused)] // `context` isn't used for servo-2020 for now + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Display, ParseError<'i>> { + // Parse all combinations of <display-inside/outside>? and `list-item`? first. + let mut got_list_item = input.try_parse(parse_list_item).is_ok(); + let mut inside = if got_list_item { + input.try_parse(parse_display_inside_for_list_item) + } else { + input.try_parse(parse_display_inside) + }; + // <display-listitem> = <display-outside>? && [ flow | flow-root ]? && list-item + // https://drafts.csswg.org/css-display/#typedef-display-listitem + if !got_list_item && is_valid_inside_for_list_item(&inside) { + got_list_item = input.try_parse(parse_list_item).is_ok(); + } + let outside = input.try_parse(parse_display_outside); + if outside.is_ok() { + if !got_list_item && (inside.is_err() || is_valid_inside_for_list_item(&inside)) { + got_list_item = input.try_parse(parse_list_item).is_ok(); + } + if inside.is_err() { + inside = if got_list_item { + input.try_parse(parse_display_inside_for_list_item) + } else { + input.try_parse(parse_display_inside) + }; + if !got_list_item && is_valid_inside_for_list_item(&inside) { + got_list_item = input.try_parse(parse_list_item).is_ok(); + } + } + } + if got_list_item || inside.is_ok() || outside.is_ok() { + let inside = inside.unwrap_or(DisplayInside::Flow); + let outside = outside.unwrap_or(match inside { + // "If <display-outside> is omitted, the element’s outside display type + // defaults to block — except for ruby, which defaults to inline." + // https://drafts.csswg.org/css-display/#inside-model + #[cfg(feature = "gecko")] + DisplayInside::Ruby => DisplayOutside::Inline, + _ => DisplayOutside::Block, + }); + return Ok(Display::from3(outside, inside, got_list_item)); + } + + // Now parse the single-keyword `display` values. + Ok(try_match_ident_ignore_ascii_case! { input, + "none" => Display::None, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "contents" => Display::Contents, + "inline-block" => Display::InlineBlock, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "inline-table" => Display::InlineTable, + "-webkit-flex" if flexbox_enabled() => Display::Flex, + "inline-flex" | "-webkit-inline-flex" if flexbox_enabled() => Display::InlineFlex, + #[cfg(feature = "gecko")] + "inline-grid" => Display::InlineGrid, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-caption" => Display::TableCaption, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row-group" => Display::TableRowGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-header-group" => Display::TableHeaderGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-footer-group" => Display::TableFooterGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column" => Display::TableColumn, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column-group" => Display::TableColumnGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row" => Display::TableRow, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-cell" => Display::TableCell, + #[cfg(feature = "gecko")] + "ruby-base" => Display::RubyBase, + #[cfg(feature = "gecko")] + "ruby-base-container" => Display::RubyBaseContainer, + #[cfg(feature = "gecko")] + "ruby-text" => Display::RubyText, + #[cfg(feature = "gecko")] + "ruby-text-container" => Display::RubyTextContainer, + #[cfg(feature = "gecko")] + "-webkit-box" => Display::WebkitBox, + #[cfg(feature = "gecko")] + "-webkit-inline-box" => Display::WebkitInlineBox, + #[cfg(feature = "gecko")] + "-moz-box" if moz_box_display_values_enabled(context) => Display::MozBox, + #[cfg(feature = "gecko")] + "-moz-inline-box" if moz_box_display_values_enabled(context) => Display::MozInlineBox, + #[cfg(feature = "gecko")] + "-moz-stack" if moz_display_values_enabled(context) => Display::MozStack, + #[cfg(feature = "gecko")] + "-moz-deck" if moz_display_values_enabled(context) => Display::MozDeck, + #[cfg(feature = "gecko")] + "-moz-popup" if moz_display_values_enabled(context) => Display::MozPopup, + }) + } +} + +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 `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, + )?)) + } +} + +/// https://drafts.csswg.org/css-animations/#animation-iteration-count +pub type AnimationIterationCount = GenericAnimationIterationCount<Number>; + +impl Parse for AnimationIterationCount { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut ::cssparser::Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("infinite")) + .is_ok() + { + return Ok(GenericAnimationIterationCount::Infinite); + } + + let number = Number::parse_non_negative(context, input)?; + Ok(GenericAnimationIterationCount::Number(number)) + } +} + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + GenericAnimationIterationCount::Number(Number::new(1.0)) + } +} + +/// A value for the `animation-name` property. +#[derive( + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "none")] +pub struct AnimationName(pub Option<KeyframesName>); + +impl AnimationName { + /// Get the name of the animation as an `Atom`. + pub fn as_atom(&self) -> Option<&Atom> { + self.0.as_ref().map(|n| n.as_atom()) + } + + /// Returns the `none` value. + pub fn none() -> Self { + AnimationName(None) + } +} + +impl ToCss for AnimationName { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match self.0 { + Some(ref name) => name.to_css(dest), + None => dest.write_str("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(Some(name))); + } + + input.expect_ident_matching("none")?; + Ok(AnimationName(None)) + } +} + +/// 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_str(" ")?; + 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_str(" ")?; + 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 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() + } +} + +bitflags! { + /// The change bits that we care about. + #[derive(Default, MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[repr(C)] + pub struct WillChangeBits: u8 { + /// Whether the stacking context will change. + const STACKING_CONTEXT = 1 << 0; + /// Whether `transform` will change. + const TRANSFORM = 1 << 1; + /// Whether `scroll-position` will change. + const SCROLL = 1 << 2; + /// Whether `opacity` will change. + const OPACITY = 1 << 3; + /// Fixed pos containing block. + const FIXPOS_CB = 1 << 4; + /// Abs pos containing block. + const ABSPOS_CB = 1 << 5; + } +} + +fn change_bits_for_longhand(longhand: LonghandId) -> WillChangeBits { + let mut flags = match longhand { + LonghandId::Opacity => WillChangeBits::OPACITY, + LonghandId::Transform => WillChangeBits::TRANSFORM, + #[cfg(feature = "gecko")] + LonghandId::Translate | LonghandId::Rotate | LonghandId::Scale | LonghandId::OffsetPath => { + WillChangeBits::TRANSFORM + }, + _ => WillChangeBits::empty(), + }; + + let property_flags = longhand.flags(); + if property_flags.contains(PropertyFlags::CREATES_STACKING_CONTEXT) { + flags |= WillChangeBits::STACKING_CONTEXT; + } + if property_flags.contains(PropertyFlags::FIXPOS_CB) { + flags |= WillChangeBits::FIXPOS_CB; + } + if property_flags.contains(PropertyFlags::ABSPOS_CB) { + flags |= WillChangeBits::ABSPOS_CB; + } + flags +} + +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 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, + }) + } +} + +bitflags! { + /// Values for the `touch-action` property. + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + /// These constants match Gecko's `NS_STYLE_TOUCH_ACTION_*` constants. + #[value_info(other_values = "auto,none,manipulation,pan-x,pan-y,pinch-zoom")] + #[repr(C)] + pub struct 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 + } +} + +impl ToCss for TouchAction { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.contains(TouchAction::AUTO) { + return dest.write_str("auto"); + } + if self.contains(TouchAction::NONE) { + return dest.write_str("none"); + } + if self.contains(TouchAction::MANIPULATION) { + return dest.write_str("manipulation"); + } + + let mut has_any = false; + macro_rules! maybe_write_value { + ($ident:path => $str:expr) => { + if self.contains($ident) { + if has_any { + dest.write_str(" ")?; + } + has_any = true; + dest.write_str($str)?; + } + }; + } + maybe_write_value!(TouchAction::PAN_X => "pan-x"); + maybe_write_value!(TouchAction::PAN_Y => "pan-y"); + maybe_write_value!(TouchAction::PINCH_ZOOM => "pinch-zoom"); + + debug_assert!(has_any); + Ok(()) + } +} + +impl Parse for TouchAction { + /// auto | none | [ pan-x || pan-y || pinch-zoom ] | manipulation + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TouchAction, ParseError<'i>> { + let mut result = TouchAction::empty(); + while let Ok(name) = input.try_parse(|i| i.expect_ident_cloned()) { + let flag = match_ignore_ascii_case! { &name, + "pan-x" => Some(TouchAction::PAN_X), + "pan-y" => Some(TouchAction::PAN_Y), + "pinch-zoom" => Some(TouchAction::PINCH_ZOOM), + "none" if result.is_empty() => return Ok(TouchAction::NONE), + "manipulation" if result.is_empty() => return Ok(TouchAction::MANIPULATION), + "auto" if result.is_empty() => return Ok(TouchAction::AUTO), + _ => None + }; + + let flag = match flag { + Some(flag) if !result.contains(flag) => flag, + _ => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name)) + ); + }, + }; + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +bitflags! { + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[value_info(other_values = "none,strict,content,size,layout,paint")] + #[repr(C)] + /// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property + pub struct Contain: u8 { + /// `none` variant, just for convenience. + const NONE = 0; + /// 'size' variant, turns on size containment + const SIZE = 1 << 0; + /// `layout` variant, turns on layout containment + const LAYOUT = 1 << 1; + /// `paint` variant, turns on paint containment + const PAINT = 1 << 2; + /// `strict` variant, turns on all types of containment + const STRICT = 1 << 3; + /// 'content' variant, turns on layout and paint containment + const CONTENT = 1 << 4; + /// variant with all the bits that contain: strict turns on + const STRICT_BITS = Contain::LAYOUT.bits | Contain::PAINT.bits | Contain::SIZE.bits; + /// variant with all the bits that contain: content turns on + const CONTENT_BITS = Contain::LAYOUT.bits | Contain::PAINT.bits; + } +} + +impl ToCss for Contain { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("none"); + } + if self.contains(Contain::STRICT) { + return dest.write_str("strict"); + } + if self.contains(Contain::CONTENT) { + return dest.write_str("content"); + } + + let mut has_any = false; + macro_rules! maybe_write_value { + ($ident:path => $str:expr) => { + if self.contains($ident) { + if has_any { + dest.write_str(" ")?; + } + has_any = true; + dest.write_str($str)?; + } + }; + } + maybe_write_value!(Contain::SIZE => "size"); + maybe_write_value!(Contain::LAYOUT => "layout"); + maybe_write_value!(Contain::PAINT => "paint"); + + debug_assert!(has_any); + Ok(()) + } +} + +impl Parse for Contain { + /// none | strict | content | [ size || layout || paint ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Contain, ParseError<'i>> { + let mut result = Contain::empty(); + while let Ok(name) = input.try_parse(|i| i.expect_ident_cloned()) { + let flag = match_ignore_ascii_case! { &name, + "size" => Some(Contain::SIZE), + "layout" => Some(Contain::LAYOUT), + "paint" => Some(Contain::PAINT), + "strict" if result.is_empty() => return Ok(Contain::STRICT | Contain::STRICT_BITS), + "content" if result.is_empty() => return Ok(Contain::CONTENT | Contain::CONTENT_BITS), + "none" if result.is_empty() => return Ok(result), + _ => None + }; + + let flag = match flag { + Some(flag) if !result.contains(flag) => flag, + _ => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name)) + ); + }, + }; + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// A specified value for the `perspective` property. +pub type Perspective = GenericPerspective<NonNegativeLength>; + +/// 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, +)] +pub enum TransitionProperty { + /// A shorthand. + Shorthand(ShorthandId), + /// A longhand transitionable property. + Longhand(LonghandId), + /// A custom property. + Custom(CustomPropertyName), + /// 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, + { + use crate::values::serialize_atom_name; + match *self { + TransitionProperty::Shorthand(ref s) => s.to_css(dest), + TransitionProperty::Longhand(ref l) => l.to_css(dest), + TransitionProperty::Custom(ref name) => { + dest.write_str("--")?; + 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(..) => { + return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident( + location, + ident, + &["none"], + )?)); + }, + }; + + Ok(match id.as_shorthand() { + Ok(s) => TransitionProperty::Shorthand(s), + Err(longhand_or_custom) => match longhand_or_custom { + PropertyDeclarationId::Longhand(id) => TransitionProperty::Longhand(id), + PropertyDeclarationId::Custom(custom) => TransitionProperty::Custom(custom.clone()), + }, + }) + } +} + +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 `all`. + #[inline] + pub fn all() -> Self { + TransitionProperty::Shorthand(ShorthandId::All) + } + + /// Convert TransitionProperty to nsCSSPropertyID. + #[cfg(feature = "gecko")] + pub fn to_nscsspropertyid( + &self, + ) -> Result<crate::gecko_bindings::structs::nsCSSPropertyID, ()> { + Ok(match *self { + TransitionProperty::Shorthand(ShorthandId::All) => { + crate::gecko_bindings::structs::nsCSSPropertyID::eCSSPropertyExtra_all_properties + }, + TransitionProperty::Shorthand(ref id) => id.to_nscsspropertyid(), + TransitionProperty::Longhand(ref id) => id.to_nscsspropertyid(), + TransitionProperty::Custom(..) | TransitionProperty::Unsupported(..) => return Err(()), + }) + } +} + +#[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/css-box/#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>. + #[parse(aliases = "textfield-multiline")] + Textarea, + /// A checkbox element. + Checkbox, + /// A radio element within a radio group. + Radio, + /// A dropdown list. + Menulist, + /// List boxes. + Listbox, + /// A horizontal meter bar. + #[parse(aliases = "meterbar")] + Meter, + /// A horizontal progress bar. + #[parse(aliases = "progressbar")] + 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::in_ua_or_chrome_sheet")] + ButtonArrowDown, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonArrowNext, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonArrowPrevious, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonArrowUp, + /// The focus outline box inside of a button. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ButtonFocus, + /// A dual toolbar button (e.g., a Back button with a dropdown) + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Dualbutton, + /// A groupbox. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Groupbox, + /// Menu Bar background + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menubar, + /// <menu> and <menuitem> appearances + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuitem, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Checkmenuitem, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Radiomenuitem, + /// For text on non-iconic menuitems only + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuitemtext, + /// The text part of a dropdown list, to left of button. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MenulistText, + /// Menu Popup background. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menupopup, + /// menu checkbox/radio appearances + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menucheckbox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuradio, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuseparator, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuarrow, + /// An image in the menu gutter, like in bookmarks or history. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Menuimage, + /// The meter bar's meter indicator. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Meterchunk, + /// The "arrowed" part of the dropdown button that open up a dropdown list. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMenulistArrowButton, + /// For HTML's <input type=number> + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + NumberInput, + /// The progress bar's progress indicator + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Progresschunk, + /// A generic container that always repaints on state changes. This is a + /// hack to make XUL checkboxes and radio buttons work. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + CheckboxContainer, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + RadioContainer, + /// The label part of a checkbox or radio button, used for painting a focus + /// outline. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + CheckboxLabel, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + RadioLabel, + /// nsRangeFrame and its subparts + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Range, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + RangeThumb, + /// The resizer background area in a status bar for the resizer widget in + /// the corner of a window. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Resizerpanel, + /// The resizer itself. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Resizer, + /// The scrollbar slider + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarHorizontal, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + 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::in_ua_or_chrome_sheet")] + ScrollbarbuttonUp, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarbuttonDown, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarbuttonLeft, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarbuttonRight, + /// The scrollbar thumb. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarthumbHorizontal, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbarthumbVertical, + /// The scrollbar track. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbartrackHorizontal, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ScrollbartrackVertical, + /// The scroll corner + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Scrollcorner, + /// A separator. Can be horizontal or vertical. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Separator, + /// A spin control (up/down control for time/date pickers). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Spinner, + /// The up button of a spin control. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + SpinnerUpbutton, + /// The down button of a spin control. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + SpinnerDownbutton, + /// The textfield of a spin control + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + SpinnerTextfield, + /// A splitter. Can be horizontal or vertical. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Splitter, + /// A status bar in a main application window. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Statusbar, + /// A single pane of a status bar. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Statusbarpanel, + /// A single tab in a tab widget. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tab, + /// A single pane (inside the tabpanels container). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tabpanel, + /// The tab panels container. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tabpanels, + /// The tabs scroll arrows (left/right). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + TabScrollArrowBack, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + TabScrollArrowForward, + /// A toolbar in an application window. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbar, + /// A single toolbar button (with no associated dropdown). + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbarbutton, + /// The dropdown portion of a toolbar button + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + ToolbarbuttonDropdown, + /// The gripper for a toolbar. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbargripper, + /// The toolbox that contains the toolbars. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Toolbox, + /// A tooltip. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Tooltip, + /// A listbox or tree widget header + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeheader, + /// An individual header cell + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeheadercell, + /// The sort arrow for a header. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeheadersortarrow, + /// A tree item. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeitem, + /// A tree widget branch line + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeline, + /// A tree widget twisty. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treetwisty, + /// Open tree widget twisty. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treetwistyopen, + /// A tree widget. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Treeview, + /// Window and dialog backgrounds. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Window, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + Dialog, + + /// Vista Rebars. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinCommunicationsToolbox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinMediaToolbox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinBrowsertabbarToolbox, + /// Vista glass. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinGlass, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinBorderlessGlass, + /// -moz-apperance style used in setting proper glass margins. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWinExcludeGlass, + + /// Titlebar elements on the Mac. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacFullscreenButton, + /// Mac help button. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacHelpButton, + + /// Windows themed window frame elements. + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonBox, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonBoxMaximized, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonClose, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonMaximize, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonMinimize, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowButtonRestore, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowFrameBottom, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowFrameLeft, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowFrameRight, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowTitlebar, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozWindowTitlebarMaximized, + + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozGtkInfoBar, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacActiveSourceListSelection, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacDisclosureButtonClosed, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacDisclosureButtonOpen, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacSourceList, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacSourceListSelection, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrancyDark, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrancyLight, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrantTitlebarDark, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozMacVibrantTitlebarLight, + + /// A non-disappearing scrollbar. + #[css(skip)] + ScrollbarNonDisappearing, + + /// 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-*`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + #[inline] + pub fn parse_legacy<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let break_value = match BreakBetween::from_ident(ident) { + Ok(v) => v, + Err(()) => { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + }, + }; + match break_value { + BreakBetween::Always => Ok(BreakBetween::Page), + BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => { + Ok(break_value) + }, + BreakBetween::Page => { + Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + }, + } + } + + /// Serialize a legacy break-between value for `page-break-*`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + pub 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, +} + +/// The value for the `overflow-x` / `overflow-y` properties. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Overflow { + Visible, + Hidden, + Scroll, + Auto, + #[cfg(feature = "gecko")] + #[parse(aliases = "-moz-hidden-unscrollable")] + 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, + } + } +} diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs new file mode 100644 index 0000000000..eb8b2fe421 --- /dev/null +++ b/servo/components/style/values/specified/calc.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/. */ + +//! [Calc expressions][calc]. +//! +//! [calc]: https://drafts.csswg.org/css-values/#calc-notation + +use crate::parser::ParserContext; +use crate::values::generics::calc as generic; +use crate::values::generics::calc::{MinMaxOp, SortKey}; +use crate::values::specified::length::ViewportPercentageLength; +use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength}; +use crate::values::specified::{self, Angle, Time}; +use crate::values::{CSSFloat, CSSInteger}; +use cssparser::{AngleOrNumber, CowRcStr, NumberOrPercentage, 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)] +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, +} + +/// 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), + /// `<percentage>` + Percentage(CSSFloat), + /// `<number>` + Number(CSSFloat), +} + +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(ref n) => n.to_css(dest), + Self::Percentage(p) => crate::values::serialize_percentage(p, dest), + Self::Angle(ref a) => a.to_css(dest), + Self::Time(ref t) => t.to_css(dest), + } + } +} + +/// An expected unit we intend to parse within a `calc()` expression. +/// +/// This is used as a hint for the parser to fast-reject invalid expressions. +#[derive(Clone, Copy, PartialEq)] +enum CalcUnit { + /// `<number>` + Number, + /// `<length>` + Length, + /// `<percentage>` + Percentage, + /// `<length> | <percentage>` + LengthPercentage, + /// `<angle>` + Angle, + /// `<time>` + Time, +} + +/// 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 SpecifiedValueInfo for CalcLengthPercentage {} + +impl PartialOrd for Leaf { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Length(ref one), &Length(ref other)) => one.partial_cmp(other), + (&Percentage(ref one), &Percentage(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()), + (&Number(ref one), &Number(ref other)) => one.partial_cmp(other), + _ => { + match *self { + Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) => {}, + } + unsafe { + debug_unreachable!("Forgot a branch?"); + } + }, + } + } +} + +impl generic::CalcNodeLeaf for Leaf { + fn is_negative(&self) -> bool { + match *self { + Self::Length(ref l) => l.is_negative(), + Self::Percentage(n) | Self::Number(n) => n < 0., + Self::Angle(ref a) => a.degrees() < 0., + Self::Time(ref t) => t.seconds() < 0., + } + } + + fn mul_by(&mut self, scalar: f32) { + match *self { + Self::Length(ref mut l) => { + // FIXME: For consistency this should probably convert absolute + // lengths into pixels. + *l = *l * scalar; + }, + Self::Number(ref mut n) => { + *n *= scalar; + }, + Self::Angle(ref mut a) => { + *a = Angle::from_calc(a.degrees() * scalar); + }, + Self::Time(ref mut t) => { + *t = Time::from_calc(t.seconds() * scalar); + }, + Self::Percentage(ref mut p) => { + *p *= scalar; + }, + } + } + + fn sort_key(&self) -> SortKey { + match *self { + Self::Number(..) => SortKey::Number, + Self::Percentage(..) => SortKey::Percentage, + Self::Time(..) => SortKey::Sec, + 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::Rem(..) => SortKey::Rem, + }, + NoCalcLength::ViewportPercentage(ref vp) => match *vp { + ViewportPercentageLength::Vh(..) => SortKey::Vh, + ViewportPercentageLength::Vw(..) => SortKey::Vw, + ViewportPercentageLength::Vmax(..) => SortKey::Vmax, + ViewportPercentageLength::Vmin(..) => SortKey::Vmin, + }, + 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_calc(one.seconds() + other.seconds()); + }, + (&mut Length(ref mut one), &Length(ref other)) => { + *one = one.try_sum(other)?; + }, + _ => { + match *other { + Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) => {}, + } + unsafe { + debug_unreachable!(); + } + }, + } + + Ok(()) + } +} + +/// 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>`, according to + /// `expected_unit`. + /// + /// 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>, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match (input.next()?, expected_unit) { + (&Token::Number { value, .. }, _) => Ok(CalcNode::Leaf(Leaf::Number(value))), + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::Length, + ) | + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::LengthPercentage, + ) => match NoCalcLength::parse_dimension(context, value, unit) { + Ok(l) => Ok(CalcNode::Leaf(Leaf::Length(l))), + Err(()) => Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }, + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::Angle, + ) => { + match Angle::parse_dimension(value, unit, /* from_calc = */ true) { + Ok(a) => Ok(CalcNode::Leaf(Leaf::Angle(a))), + Err(()) => { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + }, + ( + &Token::Dimension { + value, ref unit, .. + }, + CalcUnit::Time, + ) => { + match Time::parse_dimension(value, unit, /* from_calc = */ true) { + Ok(t) => Ok(CalcNode::Leaf(Leaf::Time(t))), + Err(()) => { + Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + }, + (&Token::Percentage { unit_value, .. }, CalcUnit::LengthPercentage) | + (&Token::Percentage { unit_value, .. }, CalcUnit::Percentage) => { + Ok(CalcNode::Leaf(Leaf::Percentage(unit_value))) + }, + (&Token::ParenthesisBlock, _) => input.parse_nested_block(|input| { + CalcNode::parse_argument(context, input, expected_unit) + }), + (&Token::Function(ref name), _) => { + let function = CalcNode::math_function(name, location)?; + CalcNode::parse(context, input, function, expected_unit) + }, + (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, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + // TODO: Do something different based on the function name. In + // particular, for non-calc function we need to take a list of + // comma-separated arguments and such. + input.parse_nested_block(|input| { + match function { + MathFunction::Calc => Self::parse_argument(context, input, expected_unit), + MathFunction::Clamp => { + let min = Self::parse_argument(context, input, expected_unit)?; + input.expect_comma()?; + let center = Self::parse_argument(context, input, expected_unit)?; + input.expect_comma()?; + let max = Self::parse_argument(context, input, expected_unit)?; + Ok(Self::Clamp { + min: Box::new(min), + center: Box::new(center), + max: Box::new(max), + }) + }, + 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, expected_unit) + })?; + + let op = match function { + MathFunction::Min => MinMaxOp::Min, + MathFunction::Max => MinMaxOp::Max, + _ => unreachable!(), + }; + + Ok(Self::MinMax(arguments.into(), op)) + }, + } + }) + } + + fn parse_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + let mut sum = SmallVec::<[CalcNode; 1]>::new(); + sum.push(Self::parse_product(context, input, expected_unit)?); + + 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, expected_unit)?); + }, + Token::Delim('-') => { + let mut rhs = Self::parse_product(context, input, expected_unit)?; + rhs.negate(); + sum.push(rhs); + }, + ref t => { + let t = t.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + } + }, + _ => { + 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>, + expected_unit: CalcUnit, + ) -> Result<Self, ParseError<'i>> { + let mut node = Self::parse_one(context, input, expected_unit)?; + + loop { + let start = input.state(); + match input.next() { + Ok(&Token::Delim('*')) => { + let rhs = Self::parse_one(context, input, expected_unit)?; + if let Ok(rhs) = rhs.to_number() { + node.mul_by(rhs); + } else if let Ok(number) = node.to_number() { + node = rhs; + node.mul_by(number); + } else { + // One of the two parts of the multiplication has to be + // a number, at least until we implement unit math. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + }, + Ok(&Token::Delim('/')) => { + let rhs = Self::parse_one(context, input, expected_unit)?; + // Dividing by units is not ok. + // + // TODO(emilio): Eventually it should be. + let number = match rhs.to_number() { + Ok(n) if n != 0. => n, + _ => { + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + }, + }; + node.mul_by(1. / number); + }, + _ => { + input.reset(&start); + break; + }, + } + } + + Ok(node) + } + + /// Tries to simplify this expression into a `<length>` or `<percentage>` + /// value. + fn into_length_or_percentage( + mut self, + clamping_mode: AllowedNumericType, + ) -> Result<CalcLengthPercentage, ()> { + // Keep track of whether there's any invalid member of the calculation, + // so as to reject the calculation properly at parse-time. + let mut any_invalid = false; + self.visit_depth_first(|node| { + if let CalcNode::Leaf(ref l) = *node { + any_invalid |= !matches!(*l, Leaf::Percentage(..) | Leaf::Length(..)); + } + node.simplify_and_sort_direct_children(); + }); + + if any_invalid { + return Err(()); + } + + Ok(CalcLengthPercentage { + clamping_mode, + node: self, + }) + } + + /// Tries to simplify this expression into a `<time>` value. + fn to_time(&self) -> Result<Time, ()> { + let seconds = self.resolve(|leaf| match *leaf { + Leaf::Time(ref t) => Ok(t.seconds()), + _ => Err(()), + })?; + Ok(Time::from_calc(crate::values::normalize(seconds))) + } + + /// Tries to simplify this expression into an `Angle` value. + fn to_angle(&self) -> Result<Angle, ()> { + let degrees = self.resolve(|leaf| match *leaf { + Leaf::Angle(ref angle) => Ok(angle.degrees()), + _ => Err(()), + })?; + Ok(Angle::from_calc(crate::values::normalize(degrees))) + } + + /// Tries to simplify this expression into a `<number>` value. + fn to_number(&self) -> Result<CSSFloat, ()> { + self.resolve(|leaf| match *leaf { + Leaf::Number(n) => Ok(n), + _ => Err(()), + }) + } + + /// Tries to simplify this expression into a `<percentage>` value. + fn to_percentage(&self) -> Result<CSSFloat, ()> { + self.resolve(|leaf| match *leaf { + Leaf::Percentage(p) => Ok(p), + _ => 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>( + name: &CowRcStr<'i>, + location: cssparser::SourceLocation, + ) -> Result<MathFunction, ParseError<'i>> { + Ok(match_ignore_ascii_case! { &*name, + "calc" => MathFunction::Calc, + "min" => MathFunction::Min, + "max" => MathFunction::Max, + "clamp" => MathFunction::Clamp, + _ => return Err(location.new_unexpected_token_error(Token::Function(name.clone()))), + }) + } + + /// 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.round() 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, CalcUnit::LengthPercentage)? + .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, CalcUnit::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, CalcUnit::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, CalcUnit::Number)? + .to_number() + .map(crate::values::normalize) + .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, CalcUnit::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>, + function: MathFunction, + ) -> Result<Time, ParseError<'i>> { + Self::parse(context, input, function, CalcUnit::Time)? + .to_time() + .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, CalcUnit::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, CalcUnit::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..e44568fb5b --- /dev/null +++ b/servo/components/style/values/specified/color.rs @@ -0,0 +1,665 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; +#[cfg(feature = "gecko")] +use crate::gecko_bindings::structs::nscolor; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; +use crate::values::generics::color::{Color as GenericColor, ColorOrAuto as GenericColorOrAuto}; +use crate::values::specified::calc::CalcNode; +use cssparser::{AngleOrNumber, Color as CSSParserColor, Parser, Token, RGBA}; +use cssparser::{BasicParseErrorKind, NumberOrPercentage, ParseErrorKind}; +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}; + +/// Specified color value +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum Color { + /// The 'currentColor' keyword + CurrentColor, + /// A specific RGBA color + Numeric { + /// Parsed RGBA color + parsed: RGBA, + /// Authored representation + authored: Option<Box<str>>, + }, + /// A complex color value from computed value + Complex(ComputedColor), + /// A system color + #[cfg(feature = "gecko")] + System(SystemColor), + /// Quirksmode-only rule for inheriting color from the body + #[cfg(feature = "gecko")] + InheritFromBodyQuirk, +} + +/// System colors. +#[allow(missing_docs)] +#[cfg(feature = "gecko")] +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum SystemColor { + #[css(skip)] + WindowBackground, + #[css(skip)] + WindowForeground, + #[css(skip)] + WidgetBackground, + #[css(skip)] + WidgetForeground, + #[css(skip)] + WidgetSelectBackground, + #[css(skip)] + WidgetSelectForeground, + #[css(skip)] + Widget3DHighlight, + #[css(skip)] + Widget3DShadow, + #[css(skip)] + TextBackground, + #[css(skip)] + TextForeground, + #[css(skip)] + TextSelectBackground, + #[css(skip)] + TextSelectForeground, + #[css(skip)] + TextSelectForegroundCustom, + #[css(skip)] + TextSelectBackgroundDisabled, + #[css(skip)] + TextSelectBackgroundAttention, + #[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, + Activeborder, + Activecaption, + Appworkspace, + Background, + Buttonface, + Buttonhighlight, + Buttonshadow, + Buttontext, + Captiontext, + #[parse(aliases = "-moz-field")] + Field, + #[parse(aliases = "-moz-fieldtext")] + Fieldtext, + Graytext, + Highlight, + Highlighttext, + Inactiveborder, + Inactivecaption, + Inactivecaptiontext, + Infobackground, + Infotext, + Menu, + Menutext, + Scrollbar, + Threeddarkshadow, + Threedface, + Threedhighlight, + Threedlightshadow, + Threedshadow, + Window, + Windowframe, + Windowtext, + MozButtondefault, + #[parse(aliases = "-moz-default-color")] + Canvastext, + #[parse(aliases = "-moz-default-background-color")] + Canvas, + MozDialog, + MozDialogtext, + /// Used to highlight valid regions to drop something onto. + MozDragtargetzone, + /// Used for selected but not focused cell backgrounds. + MozCellhighlight, + /// Used for selected but not focused cell text. + MozCellhighlighttext, + /// Used for selected but not focused html cell backgrounds. + MozHtmlCellhighlight, + /// Used for selected but not focused html cell text. + MozHtmlCellhighlighttext, + /// 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 text when hovered. + MozMenuhovertext, + /// Used for menubar item text. + MozMenubartext, + /// 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::in_ua_or_chrome_sheet")] + MozGtkButtonactivetext, + + /// Used for button text when pressed. + MozMacButtonactivetext, + /// Background color of chrome toolbars in active windows. + MozMacChromeActive, + /// Background color of chrome toolbars in inactive windows. + MozMacChromeInactive, + /// Foreground color of default buttons. + MozMacDefaultbuttontext, + /// Ring color around text fields and lists. + MozMacFocusring, + /// Color used when mouse is over a menu item. + MozMacMenuselect, + /// Color used to do shadows on menu items. + MozMacMenushadow, + /// Color used to display text for disabled menu items. + MozMacMenutextdisable, + /// Color used to display text while mouse is over a menu item. + MozMacMenutextselect, + /// Text color of disabled text on toolbars. + MozMacDisabledtoolbartext, + /// Inactive light hightlight + MozMacSecondaryhighlight, + + /// Font smoothing background colors needed by the Mac OS X theme, based on + /// -moz-appearance names. + MozMacVibrancyLight, + MozMacVibrancyDark, + MozMacVibrantTitlebarLight, + MozMacVibrantTitlebarDark, + MozMacMenupopup, + MozMacMenuitem, + MozMacActiveMenuitem, + MozMacSourceList, + MozMacSourceListSelection, + MozMacActiveSourceListSelection, + MozMacTooltip, + + /// Accent color for title bar. + MozWinAccentcolor, + /// Color from drawing text over the accent color. + MozWinAccentcolortext, + /// Media rebar text. + MozWinMediatext, + /// Communications rebar text. + MozWinCommunicationstext, + + /// 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. + MozNativehyperlinktext, + + #[parse(aliases = "-moz-hyperlinktext")] + Linktext, + #[parse(aliases = "-moz-activehyperlinktext")] + Activetext, + #[parse(aliases = "-moz-visitedhyperlinktext")] + Visitedtext, + + /// Combobox widgets + MozComboboxtext, + MozCombobox, + + MozGtkInfoBarText, + + /// Color of tree column headers + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozColheadertext, + #[parse(condition = "ParserContext::in_ua_or_chrome_sheet")] + MozColheaderhovertext, + + #[css(skip)] + End, // Just for array-indexing purposes. +} + +#[cfg(feature = "gecko")] +impl SystemColor { + #[inline] + fn compute(&self, cx: &Context) -> ComputedColor { + use crate::gecko_bindings::bindings; + + let prefs = cx.device().pref_sheet_prefs(); + + convert_nscolor_to_computedcolor(match *self { + SystemColor::Canvastext => prefs.mDefaultColor, + SystemColor::Canvas => prefs.mDefaultBackgroundColor, + SystemColor::Linktext => prefs.mLinkColor, + SystemColor::Activetext => prefs.mActiveLinkColor, + SystemColor::Visitedtext => prefs.mVisitedLinkColor, + + _ => unsafe { + bindings::Gecko_GetLookAndFeelSystemColor(*self as i32, cx.device().document()) + }, + }) + } +} + +impl From<RGBA> for Color { + fn from(value: RGBA) -> Self { + Color::rgba(value) + } +} + +struct ColorComponentParser<'a, 'b: 'a>(&'a ParserContext<'b>); +impl<'a, 'b: 'a, 'i: 'a> ::cssparser::ColorComponentParser<'i> for ColorComponentParser<'a, 'b> { + 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(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>> { + use crate::values::specified::Percentage; + + 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(name, location)?; + CalcNode::parse_number_or_percentage(self.0, input, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Parse for Color { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // 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); + + let compontent_parser = ColorComponentParser(&*context); + match input.try_parse(|i| CSSParserColor::parse_with(&compontent_parser, i)) { + Ok(value) => Ok(match value { + CSSParserColor::CurrentColor => Color::CurrentColor, + CSSParserColor::RGBA(rgba) => Color::Numeric { + parsed: rgba, + authored: authored.map(|s| s.to_ascii_lowercase().into_boxed_str()), + }, + }), + Err(e) => { + #[cfg(feature = "gecko")] + { + if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) { + return Ok(Color::System(system)); + } + } + + match e.kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => { + Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( + ValueParseErrorKind::InvalidColor(t), + ))) + }, + _ => Err(e), + } + }, + } + } +} + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Color::CurrentColor => CSSParserColor::CurrentColor.to_css(dest), + Color::Numeric { + authored: Some(ref authored), + .. + } => dest.write_str(authored), + Color::Numeric { + parsed: ref rgba, .. + } => rgba.to_css(dest), + Color::Complex(_) => Ok(()), + #[cfg(feature = "gecko")] + Color::System(system) => system.to_css(dest), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => Ok(()), + } + } +} + +/// A wrapper of cssparser::Color::parse_hash. +/// +/// That function should never return CurrentColor, so it makes no sense to +/// handle a cssparser::Color here. This should really be done in cssparser +/// directly rather than here. +fn parse_hash_color(value: &[u8]) -> Result<RGBA, ()> { + CSSParserColor::parse_hash(value).map(|color| match color { + CSSParserColor::RGBA(rgba) => rgba, + CSSParserColor::CurrentColor => unreachable!("parse_hash should never return currentcolor"), + }) +} + +impl Color { + /// Returns currentcolor value. + #[inline] + pub fn currentcolor() -> Color { + Color::CurrentColor + } + + /// Returns transparent value. + #[inline] + pub fn transparent() -> Color { + // We should probably set authored to "transparent", but maybe it doesn't matter. + Color::rgba(RGBA::transparent()) + } + + /// Returns a numeric RGBA color value. + #[inline] + pub fn rgba(rgba: RGBA) -> Self { + Color::Numeric { + parsed: rgba, + 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(Color::rgba) + .map_err(|_| e) + }) + } + + /// 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<RGBA, 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 parse_hash_color(ident.as_bytes()).map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnspecifiedError) + }); + }, + 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; + written += itoa::write(&mut serialization[written..], value).unwrap(); + if let Some(unit) = unit { + written += (&mut serialization[written..]) + .write(unit.as_bytes()) + .unwrap(); + } + debug_assert_eq!(written, 6); + parse_hash_color(&serialization) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[cfg(feature = "gecko")] +fn convert_nscolor_to_computedcolor(color: nscolor) -> ComputedColor { + use crate::gecko::values::convert_nscolor_to_rgba; + ComputedColor::rgba(convert_nscolor_to_rgba(color)) +} + +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::Numeric { ref parsed, .. } => ComputedColor::rgba(*parsed), + Color::Complex(ref complex) => *complex, + #[cfg(feature = "gecko")] + Color::System(system) => system.compute(_context?), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => { + ComputedColor::rgba(_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 { + GenericColor::Numeric(color) => Color::rgba(color), + GenericColor::CurrentColor => Color::currentcolor(), + GenericColor::Complex { .. } => Color::Complex(*computed), + } + } +} + +/// Specified color value for `-moz-font-smoothing-background-color`. +/// +/// This property does not support `currentcolor`. We could drop it at +/// parse-time, but it's not exposed to the web so it doesn't really matter. +/// +/// We resolve it to `transparent` instead. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct MozFontSmoothingBackgroundColor(pub Color); + +impl Parse for MozFontSmoothingBackgroundColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Color::parse(context, input).map(MozFontSmoothingBackgroundColor) + } +} + +impl ToComputedValue for MozFontSmoothingBackgroundColor { + type ComputedValue = RGBA; + + fn to_computed_value(&self, context: &Context) -> RGBA { + self.0 + .to_computed_value(context) + .to_rgba(RGBA::transparent()) + } + + fn from_computed_value(computed: &RGBA) -> Self { + MozFontSmoothingBackgroundColor(Color::rgba(*computed)) + } +} + +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", "currentColor", "transparent"]); + } +} + +/// 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 = RGBA; + + #[inline] + fn to_computed_value(&self, context: &Context) -> RGBA { + self.0 + .to_computed_value(context) + .to_rgba(context.builder.get_parent_inherited_text().clone_color()) + } + + #[inline] + fn from_computed_value(computed: &RGBA) -> Self { + ColorPropertyValue(Color::rgba(*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>; 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..ac097c91a9 --- /dev/null +++ b/servo/components/style/values/specified/counters.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/. */ + +//! Specified types for counter properties. + +#[cfg(feature = "servo")] +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::url::SpecifiedImageUrl; +#[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}; +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// 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, 1)?)) + } +} + +/// A specified value for the `counter-set` and `counter-reset` properties. +pub type CounterSetOrReset = generics::GenericCounterSetOrReset<Integer>; + +impl Parse for CounterSetOrReset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters(context, input, 0)?)) + } +} + +fn parse_counters<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + default_value: i32, +) -> 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 = match input.next() { + Ok(&Token::Ident(ref ident)) => CustomIdent::from_ident(location, ident, &["none"])?, + Ok(t) => { + let t = t.clone(); + return Err(location.new_unexpected_token_error(t)); + }, + Err(_) => break, + }; + + let value = input + .try_parse(|input| Integer::parse(context, input)) + .unwrap_or(Integer::new(default_value)); + counters.push(CounterPair { name, value }); + } + + 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<SpecifiedImageUrl>; + +/// The specified value for a content item in the `content` property. +pub type ContentItem = generics::GenericContentItem<SpecifiedImageUrl>; + +impl Content { + #[cfg(feature = "servo")] + 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(url) = input.try_parse(|i| SpecifiedImageUrl::parse(context, i)) { + content.push(generics::ContentItem::Url(url)); + 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, + "counter" => input.parse_nested_block(|input| { + let location = input.current_source_location(); + let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?; + let style = Content::parse_counter_style(context, input); + Ok(generics::ContentItem::Counter(name, style)) + }), + "counters" => input.parse_nested_block(|input| { + let location = input.current_source_location(); + let name = CustomIdent::from_ident(location, input.expect_ident()?, &[])?; + 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)?)) + }), + _ => { + let name = name.clone(); + return Err(input.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(name), + )) + } + }?; + content.push(result); + }, + 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 + }, + _ =>{ + 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..26b921ebdc --- /dev/null +++ b/servo/components/style/values/specified/easing.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/. */ + +//! Specified types for CSS Easing functions. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::easing::TimingFunction as ComputedTimingFunction; +use crate::values::generics::easing::TimingFunction as GenericTimingFunction; +use crate::values::generics::easing::{StepPosition, TimingKeyword}; +use crate::values::specified::{Integer, Number}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified timing function. +pub type TimingFunction = GenericTimingFunction<Integer, Number>; + +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" => { + let x1 = Number::parse(context, i)?; + i.expect_comma()?; + let y1 = Number::parse(context, i)?; + i.expect_comma()?; + let x2 = Number::parse(context, i)?; + i.expect_comma()?; + let y2 = Number::parse(context, i)?; + + if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 }) + }, + "steps" => { + let steps = Integer::parse_positive(context, i)?; + let position = i.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 && 2 > steps.value() { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(GenericTimingFunction::Steps(steps, position)) + }, + _ => Err(()), + }) + .map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone())) + }) + }) + } +} + +// 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), + } + } +} diff --git a/servo/components/style/values/specified/effects.rs b/servo/components/style/values/specified/effects.rs new file mode 100644 index 0000000000..fd92e6ee95 --- /dev/null +++ b/servo/components/style/values/specified/effects.rs @@ -0,0 +1,362 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; +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 { + 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, + } + } +} + +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..fb59a53cef --- /dev/null +++ b/servo/components/style/values/specified/font.rs @@ -0,0 +1,2385 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 + +#[cfg(feature = "gecko")] +use crate::gecko_bindings::bindings; +use crate::parser::{Parse, ParserContext}; +use crate::properties::longhands::system_font::SystemFont; +use crate::values::computed::font::{FamilyName, FontFamilyList, FontStyleAngle, SingleFontFamily}; +use crate::values::computed::{font as computed, Length, NonNegativeLength}; +use crate::values::computed::{Angle as ComputedAngle, Percentage as ComputedPercentage}; +use crate::values::computed::{CSSPixelLength, Context, ToComputedValue}; +use crate::values::generics::font::VariationValue; +use crate::values::generics::font::{self as generics, FeatureTagValue, FontSettings, FontTag}; +use crate::values::generics::NonNegative; +use crate::values::specified::length::{FontBaseSize, AU_PER_PT, AU_PER_PX}; +use crate::values::specified::{AllowQuirks, Angle, Integer, LengthPercentage}; +use crate::values::specified::{NoCalcLength, NonNegativeNumber, Number, Percentage}; +use crate::values::CustomIdent; +use crate::Atom; +use cssparser::{Parser, Token}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +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 + } + } + }; +} + +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.0, + ))) + } +} + +/// 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(weight.get().max(MIN_FONT_WEIGHT).min(MAX_FONT_WEIGHT)) + }, + 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 { + generics::FontStyle::Normal => generics::FontStyle::Normal, + generics::FontStyle::Italic => generics::FontStyle::Italic, + generics::FontStyle::Oblique(ref angle) => { + generics::FontStyle::Oblique(FontStyleAngle(Self::compute_angle(angle))) + }, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + generics::FontStyle::Normal => generics::FontStyle::Normal, + generics::FontStyle::Italic => generics::FontStyle::Italic, + generics::FontStyle::Oblique(ref angle) => { + generics::FontStyle::Oblique(Angle::from_computed_value(&angle.0)) + }, + } + } +} + +/// The default angle for `font-style: oblique`. +/// +/// NOTE(emilio): As of right now this diverges from the spec, which specifies +/// 20, because it's not updated yet to account for the resolution in: +/// +/// https://github.com/w3c/csswg-drafts/issues/2295 +pub const DEFAULT_FONT_STYLE_OBLIQUE_ANGLE_DEGREES: f32 = 14.; + +/// 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) + } + + fn compute_angle(angle: &Angle) -> ComputedAngle { + ComputedAngle::from_degrees(Self::compute_angle_degrees(angle)) + } + + /// 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( + DEFAULT_FONT_STYLE_OBLIQUE_ANGLE_DEGREES, + /* 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 +/// +/// TODO(emilio): We could derive Parse if we had NonNegativePercentage. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[repr(u8)] +pub enum FontStretch { + Stretch(Percentage), + 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 { + /// Resolves the value of the keyword as specified in: + /// + /// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop + pub fn compute(&self) -> ComputedPercentage { + use self::FontStretchKeyword::*; + ComputedPercentage(match *self { + UltraCondensed => 0.5, + ExtraCondensed => 0.625, + Condensed => 0.75, + SemiCondensed => 0.875, + Normal => 1., + SemiExpanded => 1.125, + Expanded => 1.25, + ExtraExpanded => 1.5, + UltraExpanded => 2., + }) + } + + /// Does the opposite operation to `compute`, in order to serialize keywords + /// if possible. + pub fn from_percentage(percentage: f32) -> Option<Self> { + use self::FontStretchKeyword::*; + // NOTE(emilio): Can't use `match` because of rust-lang/rust#41620. + if percentage == 0.5 { + return Some(UltraCondensed); + } + if percentage == 0.625 { + return Some(ExtraCondensed); + } + if percentage == 0.75 { + return Some(Condensed); + } + if percentage == 0.875 { + return Some(SemiCondensed); + } + if percentage == 1. { + return Some(Normal); + } + if percentage == 1.125 { + return Some(SemiExpanded); + } + if percentage == 1.25 { + return Some(Expanded); + } + if percentage == 1.5 { + return Some(ExtraExpanded); + } + if percentage == 2. { + return Some(UltraExpanded); + } + None + } +} + +impl FontStretch { + /// `normal`. + pub fn normal() -> Self { + FontStretch::Keyword(FontStretchKeyword::Normal) + } + + system_font_methods!(FontStretch, font_stretch); +} + +impl Parse for FontStretch { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // From https://drafts.csswg.org/css-fonts-4/#font-stretch-prop: + // + // Values less than 0% are not allowed and are treated as parse + // errors. + if let Ok(percentage) = + input.try_parse(|input| Percentage::parse_non_negative(context, input)) + { + return Ok(FontStretch::Stretch(percentage)); + } + + Ok(FontStretch::Keyword(FontStretchKeyword::parse(input)?)) + } +} + +impl ToComputedValue for FontStretch { + type ComputedValue = computed::FontStretch; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStretch::Stretch(ref percentage) => { + computed::FontStretch(NonNegative(percentage.to_computed_value(context))) + }, + FontStretch::Keyword(ref kw) => computed::FontStretch(NonNegative(kw.compute())), + FontStretch::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStretch::Stretch(Percentage::from_computed_value(&(computed.0).0)) + } +} + +/// 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, + #[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); + 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); + + /// Parse a specified font-family value + pub fn parse_specified<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(SingleFontFamily::parse)?; + Ok(FontFamily::Values(FontFamilyList::new( + values.into_boxed_slice(), + ))) + } +} + +impl ToComputedValue for FontFamily { + type ComputedValue = computed::FontFamily; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontFamily::Values(ref v) => computed::FontFamily { + families: v.clone(), + is_system_font: 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 a SharedFontList object is refcounted, we always + // attribute its size to the specified value, as long as it's + // not a value in SharedFontList::sSingleGenerics. + if matches!(v, FontFamilyList::SharedFontList(_)) { + let ptr = v.shared_font_list().get(); + unsafe { bindings::Gecko_SharedFontList_SizeOfIncludingThis(ptr) } + } else { + 0 + } + }, + FontFamily::System(_) => 0, + } + } +} + +impl Parse for FontFamily { + /// <family-name># + /// <family-name> = <string> | [ <ident>+ ] + /// TODO: <generic-family> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontFamily, ParseError<'i>> { + FontFamily::parse_specified(input) + } +} + +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>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + match SingleFontFamily::parse(input) { + Ok(SingleFontFamily::FamilyName(name)) => Ok(name), + Ok(SingleFontFamily::Generic(_)) => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Err(e) => Err(e), + } + } +} + +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Preserve the readability of text when font fallback occurs +pub enum FontSizeAdjust { + /// None variant + None, + /// Number variant + Number(NonNegativeNumber), + /// system font + #[css(skip)] + System(SystemFont), +} + +impl FontSizeAdjust { + #[inline] + /// Default value of font-size-adjust + pub fn none() -> Self { + FontSizeAdjust::None + } + + system_font_methods!(FontSizeAdjust, font_size_adjust); +} + +impl ToComputedValue for FontSizeAdjust { + type ComputedValue = computed::FontSizeAdjust; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontSizeAdjust::None => computed::FontSizeAdjust::None, + FontSizeAdjust::Number(ref n) => { + // The computed version handles clamping of animated values + // itself. + computed::FontSizeAdjust::Number(n.to_computed_value(context).0) + }, + FontSizeAdjust::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &computed::FontSizeAdjust) -> Self { + match *computed { + computed::FontSizeAdjust::None => FontSizeAdjust::None, + computed::FontSizeAdjust::Number(v) => { + FontSizeAdjust::Number(NonNegativeNumber::from_computed_value(&v.into())) + }, + } + } +} + +/// 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; + +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::None => unreachable!(), + }) + } + + #[cfg(feature = "gecko")] + #[inline] + fn to_length(&self, cx: &Context) -> NonNegativeLength { + use crate::context::QuirksMode; + + // 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 ref gecko_font = cx.style().get_font().gecko(); + let base_size = unsafe { + Atom::with(gecko_font.mLanguage.mRawPtr, |atom| { + cx.font_metrics_provider + .get_size(atom, gecko_font.mGenericID) + }) + }; + + 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 cx.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, + ) -> computed::FontSize { + use crate::values::specified::length::FontRelativeLength; + + 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(NoCalcLength::FontRelative(value))) => { + 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); + } + value.to_computed_value(context, base_size) + }, + FontSize::Length(LengthPercentage::Length(NoCalcLength::ServoCharacterWidth( + value, + ))) => value.to_computed_value(base_size.resolve(context)), + FontSize::Length(LengthPercentage::Length(NoCalcLength::Absolute(ref l))) => { + context.maybe_zoom_text(l.to_computed_value(context)) + }, + FontSize::Length(LengthPercentage::Length(ref l)) => l.to_computed_value(context), + 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) * pc.0).normalized() + }, + FontSize::Length(LengthPercentage::Calc(ref calc)) => { + let calc = calc.to_computed_value_zoomed(context, base_size); + calc.resolve(base_size.resolve(context)) + }, + FontSize::Keyword(i) => { + // 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) + }, + FontSize::Larger => { + info = compose_keyword(LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(LARGER_FONT_SIZE_RATIO).to_computed_value(context, base_size) + }, + + FontSize::System(_) => { + #[cfg(feature = "servo")] + { + unreachable!() + } + #[cfg(feature = "gecko")] + { + context + .cached_system_font + .as_ref() + .unwrap() + .font_size + .size + .0 + } + }, + }; + computed::FontSize { + 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) + } + + #[inline] + fn from_computed_value(computed: &computed::FontSize) -> Self { + FontSize::Length(LengthPercentage::Length( + ToComputedValue::from_computed_value(&computed.size.0), + )) + } +} + +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(FontSizeKeyword::parse) { + 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! { + #[cfg_attr(feature = "servo", derive(MallocSizeOf))] + /// 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, 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, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +/// List of Variant Alternates +pub struct VariantAlternatesList( + #[css(if_empty = "normal", iterable)] crate::OwnedSlice<VariantAlternates>, +); + +impl VariantAlternatesList { + /// 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, + }) + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Control over the selection of these alternate glyphs +pub enum FontVariantAlternates { + /// Use alternative glyph from value + Value(VariantAlternatesList), + /// Use system font glyph + #[css(skip)] + System(SystemFont), +} + +impl FontVariantAlternates { + #[inline] + /// Get initial specified value with VariantAlternatesList + pub fn get_initial_specified_value() -> Self { + FontVariantAlternates::Value(Default::default()) + } + + system_font_methods!(FontVariantAlternates, font_variant_alternates); +} + +impl ToComputedValue for FontVariantAlternates { + type ComputedValue = computed::FontVariantAlternates; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantAlternates { + match *self { + FontVariantAlternates::Value(ref v) => v.clone(), + FontVariantAlternates::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantAlternates) -> Self { + FontVariantAlternates::Value(other.clone()) + } +} + +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(FontVariantAlternates::Value(Default::default())); + } + + let mut alternates = Vec::new(); + 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); + alternates.push(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 location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Swash(ident)); + Ok(()) + }, + "stylistic" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLISTIC); + let location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Stylistic(ident)); + Ok(()) + }, + "ornaments" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ORNAMENTS); + let location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Ornaments(ident)); + Ok(()) + }, + "annotation" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ANNOTATION); + let location = i.current_source_location(); + let ident = CustomIdent::from_ident(location, i.expect_ident()?, &[])?; + alternates.push(VariantAlternates::Annotation(ident)); + Ok(()) + }, + "styleset" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLESET); + let idents = i.parse_comma_separated(|i| { + let location = i.current_source_location(); + CustomIdent::from_ident(location, i.expect_ident()?, &[]) + })?; + alternates.push(VariantAlternates::Styleset(idents.into())); + Ok(()) + }, + "character-variant" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::CHARACTER_VARIANT); + let idents = i.parse_comma_separated(|i| { + let location = i.current_source_location(); + CustomIdent::from_ident(location, i.expect_ident()?, &[]) + })?; + alternates.push(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)); + } + Ok(FontVariantAlternates::Value(VariantAlternatesList( + alternates.into(), + ))) + } +} + +macro_rules! impl_variant_east_asian { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + bitflags! { + #[derive(MallocSizeOf, ToResolvedValue, ToShmem)] + /// Vairants for east asian variant + pub struct VariantEastAsian: u16 { + /// None of the features + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for VariantEastAsian { + 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(VariantEastAsian::$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, VariantEastAsian::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for VariantEastAsian { + 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 VariantEastAsian { + /// 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!(VariantEastAsian, u16); + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Allows control of glyph substitution and sizing in East Asian text. +pub enum FontVariantEastAsian { + /// Value variant with `variant-east-asian` + Value(VariantEastAsian), + /// System font variant + #[css(skip)] + System(SystemFont), +} + +impl FontVariantEastAsian { + #[inline] + /// Get default `font-variant-east-asian` with `empty` variant + pub fn empty() -> Self { + FontVariantEastAsian::Value(VariantEastAsian::empty()) + } + + system_font_methods!(FontVariantEastAsian, font_variant_east_asian); +} + +impl ToComputedValue for FontVariantEastAsian { + type ComputedValue = computed::FontVariantEastAsian; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantEastAsian { + match *self { + FontVariantEastAsian::Value(ref v) => v.clone(), + FontVariantEastAsian::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantEastAsian) -> Self { + FontVariantEastAsian::Value(other.clone()) + } +} + +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<FontVariantEastAsian, ParseError<'i>> { + let mut result = VariantEastAsian::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontVariantEastAsian::Value(result)); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "jis78" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS78), + "jis83" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS83), + "jis90" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS90), + "jis04" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::JIS04), + "simplified" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::SIMPLIFIED), + "traditional" => + exclusive_value!((result, VariantEastAsian::JIS78 | VariantEastAsian::JIS83 | + VariantEastAsian::JIS90 | VariantEastAsian::JIS04 | + VariantEastAsian::SIMPLIFIED | VariantEastAsian::TRADITIONAL + ) => VariantEastAsian::TRADITIONAL), + "full-width" => + exclusive_value!((result, VariantEastAsian::FULL_WIDTH | + VariantEastAsian::PROPORTIONAL_WIDTH + ) => VariantEastAsian::FULL_WIDTH), + "proportional-width" => + exclusive_value!((result, VariantEastAsian::FULL_WIDTH | + VariantEastAsian::PROPORTIONAL_WIDTH + ) => VariantEastAsian::PROPORTIONAL_WIDTH), + "ruby" => + exclusive_value!((result, VariantEastAsian::RUBY) => VariantEastAsian::RUBY), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(FontVariantEastAsian::Value(result)) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_ligatures { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + bitflags! { + #[derive(MallocSizeOf, ToResolvedValue, ToShmem)] + /// Variants of ligatures + pub struct VariantLigatures: u16 { + /// Specifies that common default features are enabled + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for VariantLigatures { + 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(VariantLigatures::NONE) { + return dest.write_str("none"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(VariantLigatures::$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, VariantLigatures::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for VariantLigatures { + 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 VariantLigatures { + /// 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!(VariantLigatures, u16); + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Ligatures and contextual forms are ways of combining glyphs +/// to produce more harmonized forms +pub enum FontVariantLigatures { + /// Value variant with `variant-ligatures` + Value(VariantLigatures), + /// System font variant + #[css(skip)] + System(SystemFont), +} + +impl FontVariantLigatures { + system_font_methods!(FontVariantLigatures, font_variant_ligatures); + + /// Default value of `font-variant-ligatures` as `empty` + #[inline] + pub fn empty() -> FontVariantLigatures { + FontVariantLigatures::Value(VariantLigatures::empty()) + } + + #[inline] + /// Get `none` variant of `font-variant-ligatures` + pub fn none() -> FontVariantLigatures { + FontVariantLigatures::Value(VariantLigatures::NONE) + } +} + +impl ToComputedValue for FontVariantLigatures { + type ComputedValue = computed::FontVariantLigatures; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantLigatures { + match *self { + FontVariantLigatures::Value(ref v) => v.clone(), + FontVariantLigatures::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantLigatures) -> Self { + FontVariantLigatures::Value(other.clone()) + } +} + +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<FontVariantLigatures, ParseError<'i>> { + let mut result = VariantLigatures::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontVariantLigatures::Value(result)); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(FontVariantLigatures::Value(VariantLigatures::NONE)); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "common-ligatures" => + exclusive_value!((result, VariantLigatures::COMMON_LIGATURES | + VariantLigatures::NO_COMMON_LIGATURES + ) => VariantLigatures::COMMON_LIGATURES), + "no-common-ligatures" => + exclusive_value!((result, VariantLigatures::COMMON_LIGATURES | + VariantLigatures::NO_COMMON_LIGATURES + ) => VariantLigatures::NO_COMMON_LIGATURES), + "discretionary-ligatures" => + exclusive_value!((result, VariantLigatures::DISCRETIONARY_LIGATURES | + VariantLigatures::NO_DISCRETIONARY_LIGATURES + ) => VariantLigatures::DISCRETIONARY_LIGATURES), + "no-discretionary-ligatures" => + exclusive_value!((result, VariantLigatures::DISCRETIONARY_LIGATURES | + VariantLigatures::NO_DISCRETIONARY_LIGATURES + ) => VariantLigatures::NO_DISCRETIONARY_LIGATURES), + "historical-ligatures" => + exclusive_value!((result, VariantLigatures::HISTORICAL_LIGATURES | + VariantLigatures::NO_HISTORICAL_LIGATURES + ) => VariantLigatures::HISTORICAL_LIGATURES), + "no-historical-ligatures" => + exclusive_value!((result, VariantLigatures::HISTORICAL_LIGATURES | + VariantLigatures::NO_HISTORICAL_LIGATURES + ) => VariantLigatures::NO_HISTORICAL_LIGATURES), + "contextual" => + exclusive_value!((result, VariantLigatures::CONTEXTUAL | + VariantLigatures::NO_CONTEXTUAL + ) => VariantLigatures::CONTEXTUAL), + "no-contextual" => + exclusive_value!((result, VariantLigatures::CONTEXTUAL | + VariantLigatures::NO_CONTEXTUAL + ) => VariantLigatures::NO_CONTEXTUAL), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(FontVariantLigatures::Value(result)) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_numeric { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + bitflags! { + #[derive(MallocSizeOf, ToResolvedValue, ToShmem)] + /// Vairants of numeric values + pub struct VariantNumeric: u8 { + /// None of other variants are enabled. + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for VariantNumeric { + 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(VariantNumeric::$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, VariantNumeric::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for VariantNumeric { + 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 VariantNumeric { + /// 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!(VariantNumeric, u8); + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Specifies control over numerical forms. +pub enum FontVariantNumeric { + /// Value variant with `variant-numeric` + Value(VariantNumeric), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontVariantNumeric { + #[inline] + /// Default value of `font-variant-numeric` as `empty` + pub fn empty() -> FontVariantNumeric { + FontVariantNumeric::Value(VariantNumeric::empty()) + } + + system_font_methods!(FontVariantNumeric, font_variant_numeric); +} + +impl ToComputedValue for FontVariantNumeric { + type ComputedValue = computed::FontVariantNumeric; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariantNumeric { + match *self { + FontVariantNumeric::Value(ref v) => v.clone(), + FontVariantNumeric::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariantNumeric) -> Self { + FontVariantNumeric::Value(other.clone()) + } +} + +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<FontVariantNumeric, ParseError<'i>> { + let mut result = VariantNumeric::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontVariantNumeric::Value(result)); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "ordinal" => + exclusive_value!((result, VariantNumeric::ORDINAL) => VariantNumeric::ORDINAL), + "slashed-zero" => + exclusive_value!((result, VariantNumeric::SLASHED_ZERO) => VariantNumeric::SLASHED_ZERO), + "lining-nums" => + exclusive_value!((result, VariantNumeric::LINING_NUMS | + VariantNumeric::OLDSTYLE_NUMS + ) => VariantNumeric::LINING_NUMS), + "oldstyle-nums" => + exclusive_value!((result, VariantNumeric::LINING_NUMS | + VariantNumeric::OLDSTYLE_NUMS + ) => VariantNumeric::OLDSTYLE_NUMS), + "proportional-nums" => + exclusive_value!((result, VariantNumeric::PROPORTIONAL_NUMS | + VariantNumeric::TABULAR_NUMS + ) => VariantNumeric::PROPORTIONAL_NUMS), + "tabular-nums" => + exclusive_value!((result, VariantNumeric::PROPORTIONAL_NUMS | + VariantNumeric::TABULAR_NUMS + ) => VariantNumeric::TABULAR_NUMS), + "diagonal-fractions" => + exclusive_value!((result, VariantNumeric::DIAGONAL_FRACTIONS | + VariantNumeric::STACKED_FRACTIONS + ) => VariantNumeric::DIAGONAL_FRACTIONS), + "stacked-fractions" => + exclusive_value!((result, VariantNumeric::DIAGONAL_FRACTIONS | + VariantNumeric::STACKED_FRACTIONS + ) => VariantNumeric::STACKED_FRACTIONS), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(FontVariantNumeric::Value(result)) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// This property provides low-level control over OpenType or TrueType font features. +pub type SpecifiedFontFeatureSettings = FontSettings<FeatureTagValue<Integer>>; + +/// Define initial settings that apply when the font defined by an @font-face +/// rule is rendered. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum FontFeatureSettings { + /// Value of `FontSettings` + Value(SpecifiedFontFeatureSettings), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontFeatureSettings { + #[inline] + /// Get default value of `font-feature-settings` as normal + pub fn normal() -> FontFeatureSettings { + FontFeatureSettings::Value(FontSettings::normal()) + } + + system_font_methods!(FontFeatureSettings, font_feature_settings); +} + +impl ToComputedValue for FontFeatureSettings { + type ComputedValue = computed::FontFeatureSettings; + + fn to_computed_value(&self, context: &Context) -> computed::FontFeatureSettings { + match *self { + FontFeatureSettings::Value(ref v) => v.to_computed_value(context), + FontFeatureSettings::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontFeatureSettings) -> Self { + FontFeatureSettings::Value(ToComputedValue::from_computed_value(other)) + } +} + +impl Parse for FontFeatureSettings { + /// normal | <feature-tag-value># + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontFeatureSettings, ParseError<'i>> { + SpecifiedFontFeatureSettings::parse(context, input).map(FontFeatureSettings::Value) + } +} + +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +/// Whether user agents are allowed to synthesize bold or oblique font faces +/// when a font family lacks bold or italic faces +pub struct FontSynthesis { + /// If a `font-weight` is requested that the font family does not contain, + /// the user agent may synthesize the requested weight from the weights + /// that do exist in the font family. + #[css(represents_keyword)] + pub weight: bool, + /// If a font-style is requested that the font family does not contain, + /// the user agent may synthesize the requested style from the normal face in the font family. + #[css(represents_keyword)] + pub style: bool, +} + +impl FontSynthesis { + #[inline] + /// Get the default value of font-synthesis + pub fn get_initial_value() -> Self { + FontSynthesis { + weight: true, + style: true, + } + } +} + +impl Parse for FontSynthesis { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontSynthesis, ParseError<'i>> { + let mut result = FontSynthesis { + weight: false, + style: false, + }; + try_match_ident_ignore_ascii_case! { input, + "none" => Ok(result), + "weight" => { + result.weight = true; + if input.try_parse(|input| input.expect_ident_matching("style")).is_ok() { + result.style = true; + } + Ok(result) + }, + "style" => { + result.style = true; + if input.try_parse(|input| input.expect_ident_matching("weight")).is_ok() { + result.weight = true; + } + Ok(result) + }, + } + } +} + +impl ToCss for FontSynthesis { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.weight && self.style { + dest.write_str("weight style") + } else if self.style { + dest.write_str("style") + } else if self.weight { + dest.write_str("weight") + } else { + dest.write_str("none") + } + } +} + +#[cfg(feature = "gecko")] +impl From<u8> for FontSynthesis { + fn from(bits: u8) -> FontSynthesis { + use crate::gecko_bindings::structs; + + FontSynthesis { + weight: bits & structs::NS_FONT_SYNTHESIS_WEIGHT as u8 != 0, + style: bits & structs::NS_FONT_SYNTHESIS_STYLE as u8 != 0, + } + } +} + +#[cfg(feature = "gecko")] +impl From<FontSynthesis> for u8 { + fn from(v: FontSynthesis) -> u8 { + use crate::gecko_bindings::structs; + + let mut bits: u8 = 0; + if v.weight { + bits |= structs::NS_FONT_SYNTHESIS_WEIGHT as u8; + } + if v.style { + bits |= structs::NS_FONT_SYNTHESIS_STYLE as u8; + } + bits + } +} + +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Allows authors to explicitly specify the language system of the font, +/// overriding the language system implied by the content language +pub enum FontLanguageOverride { + /// When rendering with OpenType fonts, + /// the content language of the element is + /// used to infer the OpenType language system + Normal, + /// Single three-letter case-sensitive OpenType language system tag, + /// specifies the OpenType language system to be used instead of + /// the language system implied by the language of the element + Override(Box<str>), + /// Use system font + #[css(skip)] + System(SystemFont), +} + +impl FontLanguageOverride { + #[inline] + /// Get default value with `normal` + pub fn normal() -> FontLanguageOverride { + FontLanguageOverride::Normal + } + + /// The ToComputedValue implementation for non-system-font + /// FontLanguageOverride, used for @font-face descriptors. + #[inline] + pub fn compute_non_system(&self) -> computed::FontLanguageOverride { + match *self { + FontLanguageOverride::Normal => computed::FontLanguageOverride::zero(), + FontLanguageOverride::Override(ref lang) => { + computed::FontLanguageOverride::from_str(lang) + }, + FontLanguageOverride::System(..) => unreachable!(), + } + } + + system_font_methods!(FontLanguageOverride, font_language_override); +} + +impl ToComputedValue for FontLanguageOverride { + type ComputedValue = computed::FontLanguageOverride; + + #[inline] + fn to_computed_value(&self, context: &Context) -> computed::FontLanguageOverride { + match *self { + FontLanguageOverride::System(_) => self.compute_system(context), + _ => self.compute_non_system(), + } + } + #[inline] + fn from_computed_value(computed: &computed::FontLanguageOverride) -> Self { + if *computed == computed::FontLanguageOverride::zero() { + return FontLanguageOverride::Normal; + } + FontLanguageOverride::Override(computed.to_str(&mut [0; 4]).into()) + } +} + +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()?; + Ok(FontLanguageOverride::Override( + string.as_ref().to_owned().into_boxed_str(), + )) + } +} + +/// This property provides low-level control over OpenType or TrueType font +/// variations. +pub type SpecifiedFontVariationSettings = FontSettings<VariationValue<Number>>; + +/// Define initial settings that apply when the font defined by an @font-face +/// rule is rendered. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum FontVariationSettings { + /// Value of `FontSettings` + Value(SpecifiedFontVariationSettings), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontVariationSettings { + #[inline] + /// Get default value of `font-variation-settings` as normal + pub fn normal() -> FontVariationSettings { + FontVariationSettings::Value(FontSettings::normal()) + } + + system_font_methods!(FontVariationSettings, font_variation_settings); +} + +impl ToComputedValue for FontVariationSettings { + type ComputedValue = computed::FontVariationSettings; + + fn to_computed_value(&self, context: &Context) -> computed::FontVariationSettings { + match *self { + FontVariationSettings::Value(ref v) => v.to_computed_value(context), + FontVariationSettings::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontVariationSettings) -> Self { + FontVariationSettings::Value(ToComputedValue::from_computed_value(other)) + } +} + +impl Parse for FontVariationSettings { + /// normal | <variation-tag-value># + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariationSettings, ParseError<'i>> { + SpecifiedFontVariationSettings::parse(context, input).map(FontVariationSettings::Value) + } +} + +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 }) + } +} + +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// text-zoom. Enable if true, disable if false +pub struct XTextZoom(#[css(skip)] pub bool); + +impl Parse for XTextZoom { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<XTextZoom, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[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 * (AU_PER_PT / AU_PER_PX)) + } +} + +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 + } +} diff --git a/servo/components/style/values/specified/gecko.rs b/servo/components/style/values/specified/gecko.rs new file mode 100644 index 0000000000..3e3085c884 --- /dev/null +++ b/servo/components/style/values/specified/gecko.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/. */ + +//! 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>> { + 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..cfcba5dca3 --- /dev/null +++ b/servo/components/style/values/specified/grid.rs @@ -0,0 +1,349 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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, TrackBreadth, TrackRepeat, TrackSize}; +use crate::values::generics::grid::{TrackList, TrackListValue}; +use crate::values::specified::{Integer, LengthPercentage}; +use crate::values::{CSSFloat, CustomIdent}; +use cssparser::{ParseError as CssParseError, 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((loc, ident)) = input.try_parse(|i| -> Result<_, CssParseError<()>> { + Ok((i.current_source_location(), i.expect_ident_cloned()?)) + }) { + let ident = CustomIdent::from_ident(loc, &ident, &["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 { + static_prefs::pref!("layout.css.grid-template-subgrid-value.enabled") +} + +#[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))) + } +} diff --git a/servo/components/style/values/specified/image.rs b/servo/components/style/values/specified/image.rs new file mode 100644 index 0000000000..2fa8d0bb1e --- /dev/null +++ b/servo/components/style/values/specified/image.rs @@ -0,0 +1,1170 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::custom_properties::SpecifiedValue; +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::CorsMode; +use crate::values::generics::image::PaintWorklet; +use crate::values::generics::image::{ + self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent, +}; +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}; + +/// Specified values for an image according to CSS-IMAGES. +/// <https://drafts.csswg.org/css-images/#image-values> +pub type Image = generic::Image<Gradient, MozImageRect, SpecifiedImageUrl, Color, Percentage, Resolution>; + +/// 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>; +/// A specified percentage or nothing. +pub type PercentOrNone = generic::PercentOrNone<Percentage>; + +/// `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>>; + +#[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 +} + +#[cfg(feature = "gecko")] +fn image_set_enabled() -> bool { + static_prefs::pref!("layout.css.image-set.enabled") +} + +#[cfg(feature = "servo")] +fn image_set_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) { + if image_set_enabled() { + 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>; + +/// Specified values for `moz-image-rect` +/// -moz-image-rect(<uri>, top, right, bottom, left); +#[cfg(all(feature = "gecko", not(feature = "cbindgen")))] +pub type MozImageRect = generic::GenericMozImageRect<NumberOrPercentage, SpecifiedImageUrl>; + +#[cfg(not(feature = "gecko"))] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Empty enum on non-Gecko +pub enum MozImageRect {} + +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, /* allow_none = */ true) + } +} + +impl Image { + fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + allow_none: bool, + ) -> Result<Image, ParseError<'i>> { + if allow_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 let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) { + return Ok(generic::Image::Gradient(Box::new(gradient))); + } + if image_set_enabled() { + if let Ok(is) = input.try_parse(|input| ImageSet::parse(context, input, cors_mode)) { + return Ok(generic::Image::ImageSet(Box::new(is))); + } + } + if cross_fade_enabled() { + if let Ok(cf) = input.try_parse(|input| CrossFade::parse(context, input, cors_mode)) { + return Ok(generic::Image::CrossFade(Box::new(cf))); + } + } + #[cfg(feature = "servo-layout-2013")] + { + if let Ok(paint_worklet) = input.try_parse(|i| PaintWorklet::parse(context, i)) { + return Ok(generic::Image::PaintWorklet(paint_worklet)); + } + } + #[cfg(feature = "gecko")] + { + if let Ok(image_rect) = input.try_parse(|input| MozImageRect::parse(context, input, cors_mode)) { + return Ok(generic::Image::Rect(Box::new(image_rect))); + } + Ok(generic::Image::Element(Image::parse_element(input)?)) + } + #[cfg(not(feature = "gecko"))] + Err(input.new_error_for_next_token()) + } +} + +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, 't>(input: &mut Parser<'i, 't>) -> Result<Atom, ParseError<'i>> { + input.try_parse(|i| i.expect_function_matching("-moz-element"))?; + let location = input.current_source_location(); + input.parse_nested_block(|i| match *i.next()? { + Token::IDHash(ref id) => Ok(Atom::from(id.as_ref())), + ref t => 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, /* allow_none = */ true) + } +} + +impl CrossFade { + /// cross-fade() = cross-fade( <cf-image># ) + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("cross-fade")?; + let elements = input.parse_nested_block(|input| { + input.parse_comma_separated(|input| CrossFadeElement::parse(context, input, cors_mode)) + })?; + let elements = crate::OwnedSlice::from(elements); + Ok(Self { elements }) + } +} + +impl CrossFadeElement { + /// <cf-image> = <percentage>? && [ <image> | <color> ] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + // Try and parse a leading percent sign. + let mut percent = PercentOrNone::parse_or_none(context, input); + // Parse the image + let image = CrossFadeImage::parse(context, input, cors_mode)?; + // Try and parse a trailing percent sign. + if percent == PercentOrNone::None { + percent = PercentOrNone::parse_or_none(context, input); + } + Ok(Self { percent, image }) + } +} + +impl CrossFadeImage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + if let Ok(image) = input.try_parse(|input| Image::parse_with_cors_mode(context, input, cors_mode, /* allow_none = */ false)) { + return Ok(Self::Image(image)) + } + Ok(Self::Color(Color::parse(context, input)?)) + } +} + +impl PercentOrNone { + fn parse_or_none<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Self { + // 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> + if let Ok(percent) = input.try_parse(|input| Percentage::parse_non_negative(context, input)) + { + Self::Percent(percent.clamp_to_hundred()) + } else { + Self::None + } + } +} + +impl ImageSet { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("image-set")?; + let items = input.parse_nested_block(|input| { + input.parse_comma_separated(|input| ImageSetItem::parse(context, input, cors_mode)) + })?; + Ok(Self { + selected_index: 0, + items: items.into() + }) + } +} + +impl ImageSetItem { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> 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, /* allow_none = */ false)?, + }; + let resolution = input.try_parse(|input| Resolution::parse(context, input)).unwrap_or(Resolution::X(1.0)); + Ok(Self { + image, + resolution, + }) + } +} + +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, + items, + repeating: false, + 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, + items, + repeating: false, + 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().into(), + position: Percentage::zero().into(), + }, + generic::GradientItem::ComplexColorStop { + color: Color::transparent().into(), + position: Percentage::hundred().into(), + }, + ]; + } 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) + } + + /// 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 direction = if let Ok(d) = + input.try_parse(|i| LineDirection::parse(context, i, &mut compat_mode)) + { + input.expect_comma()?; + d + } else { + match compat_mode { + GradientCompatMode::Modern => { + LineDirection::Vertical(VerticalPositionKeyword::Bottom) + }, + _ => LineDirection::Vertical(VerticalPositionKeyword::Top), + } + }; + let items = Gradient::parse_stops(context, input)?; + + Ok(Gradient::Linear { + direction, + items, + repeating, + 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 (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()) + }, + }; + + if shape.is_ok() || position.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)?; + + Ok(Gradient::Radial { + shape, + position, + items, + repeating, + compat_mode, + }) + } + fn parse_conic<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + ) -> Result<Self, ParseError<'i>> { + 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) + }); + if angle.is_ok() || position.is_ok() { + 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)); + } + + Ok(Gradient::Conic { + angle, + position, + items, + repeating, + }) + } +} + +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_str(" ")?; + 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 Parse for PaintWorklet { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("paint")?; + input.parse_nested_block(|input| { + let name = Atom::from(&**input.expect_ident()?); + let arguments = input + .try_parse(|input| { + input.expect_comma()?; + input.parse_comma_separated(|input| SpecifiedValue::parse(input)) + }) + .unwrap_or(vec![]); + Ok(PaintWorklet { name, arguments }) + }) + } +} + +impl MozImageRect { + #[cfg(not(feature = "gecko"))] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + _cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + Err(input.new_error_for_next_token()) + } + + #[cfg(feature = "gecko")] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| i.expect_function_matching("-moz-image-rect"))?; + input.parse_nested_block(|i| { + let string = i.expect_url_or_string()?; + let url = SpecifiedImageUrl::parse_from_string( + string.as_ref().to_owned(), + context, + cors_mode, + ); + i.expect_comma()?; + let top = NumberOrPercentage::parse_non_negative(context, i)?; + i.expect_comma()?; + let right = NumberOrPercentage::parse_non_negative(context, i)?; + i.expect_comma()?; + let bottom = NumberOrPercentage::parse_non_negative(context, i)?; + i.expect_comma()?; + let left = NumberOrPercentage::parse_non_negative(context, i)?; + Ok(MozImageRect { + url, + top, + right, + bottom, + left, + }) + }) + } +} diff --git a/servo/components/style/values/specified/length.rs b/servo/components/style/values/specified/length.rs new file mode 100644 index 0000000000..2eafa9fbd6 --- /dev/null +++ b/servo/components/style/values/specified/length.rs @@ -0,0 +1,1285 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::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; +use app_units::Au; +use cssparser::{Parser, Token}; +use euclid::default::Size2D; +use std::cmp; +use std::ops::{Add, Mul}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{ParseError, SpecifiedValueInfo, StyleParseErrorKind}; + +pub use super::image::Image; +pub use super::image::{EndingShape as GradientEndingShape, Gradient}; +pub use crate::values::specified::calc::CalcLengthPercentage; + +/// Number of app units per pixel +pub const AU_PER_PX: CSSFloat = 60.; +/// Number of app units per inch +pub const AU_PER_IN: CSSFloat = AU_PER_PX * 96.; +/// Number of app units per centimeter +pub const AU_PER_CM: CSSFloat = AU_PER_IN / 2.54; +/// Number of app units per millimeter +pub const AU_PER_MM: CSSFloat = AU_PER_IN / 25.4; +/// Number of app units per quarter +pub const AU_PER_Q: CSSFloat = AU_PER_MM / 4.; +/// Number of app units per point +pub const AU_PER_PT: CSSFloat = AU_PER_IN / 72.; +/// Number of app units per pica +pub const AU_PER_PC: CSSFloat = AU_PER_PT * 12.; + +/// A font relative length. +#[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 "rem" value: https://drafts.csswg.org/css-values/#rem + #[css(dimension)] + Rem(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, +} + +impl FontBaseSize { + /// Calculate the actual size for a given context + pub fn resolve(&self, context: &Context) -> computed::Length { + match *self { + FontBaseSize::CurrentStyle => context.style().get_font().clone_font_size().size(), + FontBaseSize::InheritedStyle => { + context.style().get_parent_font().clone_font_size().size() + }, + } + } +} + +impl FontRelativeLength { + /// Return true if this is a zero value. + fn is_zero(&self) -> bool { + match *self { + FontRelativeLength::Em(v) | + FontRelativeLength::Ex(v) | + FontRelativeLength::Ch(v) | + FontRelativeLength::Rem(v) => v == 0., + } + } + + fn is_negative(&self) -> bool { + match *self { + FontRelativeLength::Em(v) | + FontRelativeLength::Ex(v) | + FontRelativeLength::Ch(v) | + FontRelativeLength::Rem(v) => v < 0., + } + } + + fn try_sum(&self, other: &Self) -> Result<Self, ()> { + use self::FontRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Em(one), &Em(other)) => Em(one + other), + (&Ex(one), &Ex(other)) => Ex(one + other), + (&Ch(one), &Ch(other)) => Ch(one + other), + (&Rem(one), &Rem(other)) => Rem(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(..) | Rem(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum()") + }, + }) + } + + /// Computes the font-relative length. + pub fn to_computed_value( + &self, + context: &Context, + base_size: FontBaseSize, + ) -> computed::Length { + let (reference_size, length) = self.reference_font_size_and_length(context, base_size); + (reference_size * length).normalized() + } + + /// 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, + ) -> (computed::Length, CSSFloat) { + fn query_font_metrics( + context: &Context, + base_size: FontBaseSize, + orientation: FontMetricsOrientation, + ) -> FontMetrics { + context + .font_metrics_provider + .query(context, base_size, orientation) + } + + let reference_font_size = base_size.resolve(context); + let font_metrics_flag = match base_size { + FontBaseSize::CurrentStyle => ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS, + FontBaseSize::InheritedStyle => ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS, + }; + match *self { + FontRelativeLength::Em(length) => { + if context.for_non_inherited_property.is_some() { + if base_size == FontBaseSize::CurrentStyle { + context + .rule_cache_conditions + .borrow_mut() + .set_font_size_dependency(reference_font_size.into()); + } + } + + (reference_font_size, length) + }, + FontRelativeLength::Ex(length) => { + if context.for_non_inherited_property.is_some() { + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + context.builder.add_flags(font_metrics_flag); + // 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. + // + reference_font_size * 0.5 + }); + (reference_size, length) + }, + FontRelativeLength::Ch(length) => { + if context.for_non_inherited_property.is_some() { + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + context.builder.add_flags(font_metrics_flag); + // 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::MatchContext); + 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). + // + let wm = context.style().writing_mode; + if wm.is_vertical() && wm.is_upright() { + reference_font_size + } else { + reference_font_size * 0.5 + } + }); + (reference_size, length) + }, + FontRelativeLength::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 + } else { + context.device().root_font_size() + }; + (reference_size, length) + }, + } + } +} + +/// A viewport-relative length. +/// +/// <https://drafts.csswg.org/css-values/#viewport-relative-lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum ViewportPercentageLength { + /// A vw unit: https://drafts.csswg.org/css-values/#vw + #[css(dimension)] + Vw(CSSFloat), + /// A vh unit: https://drafts.csswg.org/css-values/#vh + #[css(dimension)] + Vh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#vmin> + #[css(dimension)] + Vmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#vmax> + #[css(dimension)] + Vmax(CSSFloat), +} + +impl ViewportPercentageLength { + /// Return true if this is a zero value. + fn is_zero(&self) -> bool { + match *self { + ViewportPercentageLength::Vw(v) | + ViewportPercentageLength::Vh(v) | + ViewportPercentageLength::Vmin(v) | + ViewportPercentageLength::Vmax(v) => v == 0., + } + } + + fn is_negative(&self) -> bool { + match *self { + ViewportPercentageLength::Vw(v) | + ViewportPercentageLength::Vh(v) | + ViewportPercentageLength::Vmin(v) | + ViewportPercentageLength::Vmax(v) => v < 0., + } + } + + fn try_sum(&self, other: &Self) -> Result<Self, ()> { + use self::ViewportPercentageLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Vw(one), &Vw(other)) => Vw(one + other), + (&Vh(one), &Vh(other)) => Vh(one + other), + (&Vmin(one), &Vmin(other)) => Vmin(one + other), + (&Vmax(one), &Vmax(other)) => Vmax(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(..) | Vh(..) | Vmin(..) | Vmax(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum()") + }, + }) + } + + /// Computes the given viewport-relative length for the given viewport size. + pub fn to_computed_value(&self, viewport_size: Size2D<Au>) -> CSSPixelLength { + let (factor, length) = match *self { + ViewportPercentageLength::Vw(length) => (length, viewport_size.width), + ViewportPercentageLength::Vh(length) => (length, viewport_size.height), + ViewportPercentageLength::Vmin(length) => { + (length, cmp::min(viewport_size.width, viewport_size.height)) + }, + ViewportPercentageLength::Vmax(length) => { + (length, cmp::max(viewport_size.width, viewport_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 + let trunc_scaled = ((length.0 as f64) * factor as f64 / 100.).trunc(); + Au::from_f64_au(trunc_scaled).into() + } +} + +/// 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 + } +} + +/// 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 { + fn is_zero(&self) -> bool { + match *self { + AbsoluteLength::Px(v) | + AbsoluteLength::In(v) | + AbsoluteLength::Cm(v) | + AbsoluteLength::Mm(v) | + AbsoluteLength::Q(v) | + AbsoluteLength::Pt(v) | + AbsoluteLength::Pc(v) => v == 0., + } + } + + fn is_negative(&self) -> bool { + match *self { + AbsoluteLength::Px(v) | + AbsoluteLength::In(v) | + AbsoluteLength::Cm(v) | + AbsoluteLength::Mm(v) | + AbsoluteLength::Q(v) | + AbsoluteLength::Pt(v) | + AbsoluteLength::Pc(v) => v < 0., + } + } + + /// Convert this into a pixel value. + #[inline] + pub fn to_px(&self) -> CSSFloat { + use std::f32; + + let pixel = match *self { + AbsoluteLength::Px(value) => value, + AbsoluteLength::In(value) => value * (AU_PER_IN / AU_PER_PX), + AbsoluteLength::Cm(value) => value * (AU_PER_CM / AU_PER_PX), + AbsoluteLength::Mm(value) => value * (AU_PER_MM / AU_PER_PX), + AbsoluteLength::Q(value) => value * (AU_PER_Q / AU_PER_PX), + AbsoluteLength::Pt(value) => value * (AU_PER_PT / AU_PER_PX), + AbsoluteLength::Pc(value) => value * (AU_PER_PC / AU_PER_PX), + }; + pixel.min(f32::MAX).max(f32::MIN) + } +} + +impl ToComputedValue for AbsoluteLength { + type ComputedValue = CSSPixelLength; + + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + CSSPixelLength::new(self.to_px()) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + AbsoluteLength::Px(computed.px()) + } +} + +impl PartialOrd for AbsoluteLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + self.to_px().partial_cmp(&other.to_px()) + } +} + +impl Mul<CSSFloat> for AbsoluteLength { + type Output = AbsoluteLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> AbsoluteLength { + match self { + AbsoluteLength::Px(v) => AbsoluteLength::Px(v * scalar), + AbsoluteLength::In(v) => AbsoluteLength::In(v * scalar), + AbsoluteLength::Cm(v) => AbsoluteLength::Cm(v * scalar), + AbsoluteLength::Mm(v) => AbsoluteLength::Mm(v * scalar), + AbsoluteLength::Q(v) => AbsoluteLength::Q(v * scalar), + AbsoluteLength::Pt(v) => AbsoluteLength::Pt(v * scalar), + AbsoluteLength::Pc(v) => AbsoluteLength::Pc(v * scalar), + } + } +} + +impl Add<AbsoluteLength> for AbsoluteLength { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self { + match (self, rhs) { + (AbsoluteLength::Px(x), AbsoluteLength::Px(y)) => AbsoluteLength::Px(x + y), + (AbsoluteLength::In(x), AbsoluteLength::In(y)) => AbsoluteLength::In(x + y), + (AbsoluteLength::Cm(x), AbsoluteLength::Cm(y)) => AbsoluteLength::Cm(x + y), + (AbsoluteLength::Mm(x), AbsoluteLength::Mm(y)) => AbsoluteLength::Mm(x + y), + (AbsoluteLength::Q(x), AbsoluteLength::Q(y)) => AbsoluteLength::Q(x + y), + (AbsoluteLength::Pt(x), AbsoluteLength::Pt(y)) => AbsoluteLength::Pt(x + y), + (AbsoluteLength::Pc(x), AbsoluteLength::Pc(y)) => AbsoluteLength::Pc(x + y), + _ => AbsoluteLength::Px(self.to_px() + rhs.to_px()), + } + } +} + +/// A `<length>` without taking `calc` expressions into account +/// +/// <https://drafts.csswg.org/css-values/#lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, 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), + + /// 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()`. + #[css(function)] + ServoCharacterWidth(CharacterWidth), +} + +impl Mul<CSSFloat> for NoCalcLength { + type Output = NoCalcLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> NoCalcLength { + match self { + NoCalcLength::Absolute(v) => NoCalcLength::Absolute(v * scalar), + NoCalcLength::FontRelative(v) => NoCalcLength::FontRelative(v * scalar), + NoCalcLength::ViewportPercentage(v) => NoCalcLength::ViewportPercentage(v * scalar), + NoCalcLength::ServoCharacterWidth(_) => panic!("Can't multiply ServoCharacterWidth!"), + } + } +} + +impl NoCalcLength { + /// Returns whether the value of this length without unit is less than zero. + pub fn is_negative(&self) -> bool { + match *self { + NoCalcLength::Absolute(v) => v.is_negative(), + NoCalcLength::FontRelative(v) => v.is_negative(), + NoCalcLength::ViewportPercentage(v) => v.is_negative(), + NoCalcLength::ServoCharacterWidth(c) => c.0 < 0, + } + } + + /// 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" => NoCalcLength::Absolute(AbsoluteLength::Px(value)), + "in" => NoCalcLength::Absolute(AbsoluteLength::In(value)), + "cm" => NoCalcLength::Absolute(AbsoluteLength::Cm(value)), + "mm" => NoCalcLength::Absolute(AbsoluteLength::Mm(value)), + "q" => NoCalcLength::Absolute(AbsoluteLength::Q(value)), + "pt" => NoCalcLength::Absolute(AbsoluteLength::Pt(value)), + "pc" => NoCalcLength::Absolute(AbsoluteLength::Pc(value)), + // font-relative + "em" => NoCalcLength::FontRelative(FontRelativeLength::Em(value)), + "ex" => NoCalcLength::FontRelative(FontRelativeLength::Ex(value)), + "ch" => NoCalcLength::FontRelative(FontRelativeLength::Ch(value)), + "rem" => NoCalcLength::FontRelative(FontRelativeLength::Rem(value)), + // viewport percentages + "vw" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vw(value)) + }, + "vh" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vh(value)) + }, + "vmin" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmin(value)) + }, + "vmax" if !context.in_page_rule() => { + NoCalcLength::ViewportPercentage(ViewportPercentageLength::Vmax(value)) + }, + _ => return Err(()), + }) + } + + /// Try to sume two lengths if compatible into the left hand side. + pub(crate) fn try_sum(&self, other: &Self) -> Result<Self, ()> { + 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 + *other), + (&FontRelative(ref one), &FontRelative(ref other)) => FontRelative(one.try_sum(other)?), + (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => { + ViewportPercentage(one.try_sum(other)?) + }, + (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => { + ServoCharacterWidth(CharacterWidth(one.0 + 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(..) | + ServoCharacterWidth(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum()") + }, + }) + } + + /// Get a px value without context. + #[inline] + pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> { + match *self { + NoCalcLength::Absolute(len) => Ok(len.to_px()), + _ => 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 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) + }, + (&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(..) | + 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 { + match *self { + NoCalcLength::Absolute(v) => v.is_zero(), + NoCalcLength::FontRelative(v) => v.is_zero(), + NoCalcLength::ViewportPercentage(v) => v.is_zero(), + NoCalcLength::ServoCharacterWidth(v) => v.0 == 0, + } + } +} + +/// 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 Mul<CSSFloat> for Length { + type Output = Length; + + #[inline] + fn mul(self, scalar: CSSFloat) -> Length { + match self { + Length::NoCalc(inner) => Length::NoCalc(inner * scalar), + Length::Calc(..) => panic!("Can't multiply Calc!"), + } + } +} + +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), + (&Rem(ref one), &Rem(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(..) | Rem(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Mul<CSSFloat> for FontRelativeLength { + type Output = FontRelativeLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> FontRelativeLength { + match self { + FontRelativeLength::Em(v) => FontRelativeLength::Em(v * scalar), + FontRelativeLength::Ex(v) => FontRelativeLength::Ex(v * scalar), + FontRelativeLength::Ch(v) => FontRelativeLength::Ch(v * scalar), + FontRelativeLength::Rem(v) => FontRelativeLength::Rem(v * scalar), + } + } +} + +impl Mul<CSSFloat> for ViewportPercentageLength { + type Output = ViewportPercentageLength; + + #[inline] + fn mul(self, scalar: CSSFloat) -> ViewportPercentageLength { + match self { + ViewportPercentageLength::Vw(v) => ViewportPercentageLength::Vw(v * scalar), + ViewportPercentageLength::Vh(v) => ViewportPercentageLength::Vh(v * scalar), + ViewportPercentageLength::Vmin(v) => ViewportPercentageLength::Vmin(v * scalar), + ViewportPercentageLength::Vmax(v) => ViewportPercentageLength::Vmax(v * scalar), + } + } +} + +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), + (&Vh(ref one), &Vh(ref other)) => one.partial_cmp(other), + (&Vmin(ref one), &Vmin(ref other)) => one.partial_cmp(other), + (&Vmax(ref one), &Vmax(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(..) | Vh(..) | Vmin(..) | Vmax(..) => {}, + } + 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(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)) + } +} + +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 pc.is_calc() { + // FIXME(emilio): Hard-coding the clamping mode is suspect. + LengthPercentage::Calc(Box::new(CalcLengthPercentage { + clamping_mode: AllowedNumericType::All, + 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()) + } + + 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(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, + ) + } +} + +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, + } + } +} + +/// 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) + } +} + +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>> { + #[cfg(feature = "gecko")] + { + if let Ok(l) = input.try_parse(computed::ExtremumLength::parse) { + return Ok(GenericSize::ExtremumLength(l)); + } + } + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(GenericSize::Auto); + } + + 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>> { + #[cfg(feature = "gecko")] + { + if let Ok(l) = input.try_parse(computed::ExtremumLength::parse) { + return Ok(GenericMaxSize::ExtremumLength(l)); + } + } + + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(GenericMaxSize::None); + } + + 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..5d31c0b34d --- /dev/null +++ b/servo/components/style/values/specified/list.rs @@ -0,0 +1,216 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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; + + if value == structs::NS_STYLE_LIST_STYLE_NONE { + return ListStyleType::None; + } + + ListStyleType::CounterStyle(CounterStyle::Name(CustomIdent(match value { + structs::NS_STYLE_LIST_STYLE_DISC => atom!("disc"), + structs::NS_STYLE_LIST_STYLE_CIRCLE => atom!("circle"), + structs::NS_STYLE_LIST_STYLE_SQUARE => atom!("square"), + structs::NS_STYLE_LIST_STYLE_DECIMAL => atom!("decimal"), + structs::NS_STYLE_LIST_STYLE_LOWER_ROMAN => atom!("lower-roman"), + structs::NS_STYLE_LIST_STYLE_UPPER_ROMAN => atom!("upper-roman"), + structs::NS_STYLE_LIST_STYLE_LOWER_ALPHA => atom!("lower-alpha"), + structs::NS_STYLE_LIST_STYLE_UPPER_ALPHA => atom!("upper-alpha"), + _ => unreachable!("Unknown counter style keyword value"), + }))) + } +} + +#[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)) + } + } +} + +/// Specified and computed `-moz-list-reversed` property (for UA sheets only). +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum MozListReversed { + /// the initial value + False, + /// exclusively used for <ol reversed> in our html.css UA sheet + True, +} diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs new file mode 100644 index 0000000000..d6d763686e --- /dev/null +++ b/servo/components/style/values/specified/mod.rs @@ -0,0 +1,933 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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, Either, None_}; +use crate::context::QuirksMode; +use crate::parser::{Parse, ParserContext}; +use crate::values::serialize_atom_identifier; +use crate::values::specified::calc::CalcNode; +use crate::{Atom, Namespace, One, Prefix, Zero}; +use cssparser::{Parser, Token}; +use std::f32; +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::background::{BackgroundRepeat, BackgroundSize}; +pub use self::basic_shape::FillRule; +pub use self::border::{BorderCornerRadius, BorderImageSlice, BorderImageWidth}; +pub use self::border::{BorderImageRepeat, BorderImageSideWidth}; +pub use self::border::{BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle}; +pub use self::box_::{AnimationIterationCount, AnimationName, Contain, Display}; +pub use self::box_::{Appearance, BreakBetween, BreakWithin}; +pub use self::box_::{Clear, Float, Overflow, OverflowAnchor}; +pub use self::box_::{OverflowClipBox, OverscrollBehavior, Perspective, Resize}; +pub use self::box_::{ScrollSnapAlign, ScrollSnapAxis, ScrollSnapStrictness, ScrollSnapType}; +pub use self::box_::{TouchAction, TransitionProperty, VerticalAlign, WillChange}; +pub use self::color::{Color, ColorOrAuto, ColorPropertyValue}; +pub use self::column::ColumnCount; +pub use self::counters::{Content, ContentItem, CounterIncrement, CounterSetOrReset}; +pub use self::easing::TimingFunction; +pub use self::effects::{BoxShadow, Filter, SimpleShadow}; +pub use self::flex::FlexBasis; +pub use self::font::{FontFamily, FontLanguageOverride, FontStyle}; +pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; +pub use self::font::{FontSize, FontSizeAdjust, FontSizeKeyword, FontStretch, FontSynthesis}; +pub use self::font::{FontVariantAlternates, FontWeight}; +pub use self::font::{FontVariantEastAsian, FontVariationSettings}; +pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextZoom}; +pub use self::image::{EndingShape as GradientEndingShape, Gradient}; +pub use self::image::{Image, MozImageRect}; +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}; +pub use self::length::{ + NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +#[cfg(feature = "gecko")] +pub use self::list::ListStyleType; +pub use self::list::MozListReversed; +pub use self::list::Quotes; +pub use self::motion::{OffsetPath, OffsetRotate}; +pub use self::outline::OutlineStyle; +pub use self::percentage::Percentage; +pub use self::position::AspectRatio; +pub use self::position::{ + GridAutoFlow, GridTemplateAreas, MasonryAutoFlow, Position, PositionOrAuto, +}; +pub use self::position::{PositionComponent, ZIndex}; +pub use self::rect::NonNegativeLengthOrNumberRect; +pub use self::resolution::Resolution; +pub use self::svg::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::TextAlignLast; +pub use self::text::TextUnderlinePosition; +pub use self::text::{InitialLetter, LetterSpacing, LineBreak, LineHeight, TextAlign}; +pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak}; +pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing}; +pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextTransform}; +pub use self::time::Time; +pub use self::transform::{Rotate, Scale, Transform}; +pub use self::transform::{TransformOrigin, TransformStyle, Translate}; +#[cfg(feature = "gecko")] +pub use self::ui::CursorImage; +pub use self::ui::{Cursor, MozForceBrokenImageIcon, UserSelect}; +pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent; + +#[cfg(feature = "gecko")] +pub mod align; +pub mod angle; +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 percentage; +pub mod position; +pub mod rect; +pub mod resolution; +pub mod source_size_list; +pub mod svg; +pub mod svg_path; +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: value.min(f32::MAX).max(f32::MIN), + calc_clamping_mode: None, + }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let result = CalcNode::parse_number(context, input, function)?; + Ok(Number { + value: result.min(f32::MAX).max(f32::MIN), + 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, PartialEq, 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 Number { + /// Returns a new number with the value `val`. + 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`. + 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 { + self.calc_clamping_mode + .map_or(self.value, |mode| mode.clamp(self.value)) + } + + #[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, + { + if self.calc_clamping_mode.is_some() { + dest.write_str("calc(")?; + } + self.value.to_css(dest)?; + if self.calc_clamping_mode.is_some() { + dest.write_str(")")?; + } + Ok(()) + } +} + +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() + } +} + +/// 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())) + } +} + +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 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(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_str(")")?; + } + 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) + } +} + +/// A specified positive `<integer>` value or `none`. +pub type PositiveIntegerOrNone = Either<PositiveInteger, None_>; + +/// 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, +} + +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 + .as_ref()? + .prefixes + .get(prefix) + .map(|x| x.clone()) +} + +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` + // no spaces allowed + let first = input.try_parse(|i| i.expect_ident_cloned()).ok(); + if let Ok(token) = input.try_parse(|i| i.next_including_whitespace().map(|t| t.clone())) { + match token { + Token::Delim('|') => { + let location = input.current_source_location(); + // must be followed by an ident + let second_token = match *input.next_including_whitespace()? { + Token::Ident(ref second) => second, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + + let (namespace_prefix, namespace_url) = if let Some(ns) = first { + let prefix = Prefix::from(ns.as_ref()); + let ns = match get_namespace_for_prefix(&prefix, context) { + Some(ns) => ns, + None => { + return Err(location + .new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }, + }; + (prefix, ns) + } else { + (Prefix::default(), Namespace::default()) + }; + return Ok(Attr { + namespace_prefix, + namespace_url, + attribute: Atom::from(second_token.as_ref()), + }); + }, + // In the case of attr(foobar ) we don't want to error out + // because of the trailing whitespace. + Token::WhiteSpace(..) => {}, + ref t => return Err(input.new_unexpected_token_error(t.clone())), + } + } + + if let Some(first) = first { + Ok(Attr { + namespace_prefix: Prefix::default(), + namespace_url: Namespace::default(), + attribute: Atom::from(first.as_ref()), + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +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_str("|")?; + } + serialize_atom_identifier(&self.attribute, dest)?; + dest.write_str(")") + } +} diff --git a/servo/components/style/values/specified/motion.rs b/servo/components/style/values/specified/motion.rs new file mode 100644 index 0000000000..7831b78afe --- /dev/null +++ b/servo/components/style/values/specified/motion.rs @@ -0,0 +1,220 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::{GenericOffsetPath, RayFunction, RaySize}; +use crate::values::specified::{Angle, SVGPathData}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// The specified value of `offset-path`. +pub type OffsetPath = GenericOffsetPath<Angle>; + +#[cfg(feature = "gecko")] +fn is_ray_enabled() -> bool { + static_prefs::pref!("layout.css.motion-path-ray.enabled") +} +#[cfg(feature = "servo")] +fn is_ray_enabled() -> bool { + false +} + +impl Parse for RayFunction<Angle> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if !is_ray_enabled() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut angle = None; + let mut size = None; + let mut contain = false; + loop { + if angle.is_none() { + angle = input.try_parse(|i| Angle::parse(context, i)).ok(); + } + + if size.is_none() { + size = input.try_parse(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; + } + } + break; + } + + if angle.is_none() || size.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(RayFunction { + angle: angle.unwrap(), + size: size.unwrap(), + contain, + }) + } +} + +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()); + } + + // 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, + // Bug 1186329: Implement the parser for <basic-shape>, <geometry-box>, + // and <url>. + "path" => SVGPathData::parse(context, i).map(GenericOffsetPath::Path), + "ray" => RayFunction::parse(context, i).map(GenericOffsetPath::Ray), + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + }) + } +} + +/// 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/percentage.rs b/servo/components/style/values/specified/percentage.rs new file mode 100644 index 0000000000..eac4d91cf8 --- /dev/null +++ b/servo/components/style/values/specified/percentage.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/. */ + +//! Specified percentages. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::percentage::Percentage as ComputedPercentage; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::specified::Number; +use crate::values::{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_str(")")?; + } + 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 whether this percentage is a `calc()` value. + pub fn is_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + /// 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(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) + } + + /// 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(self.get()) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Percentage::new(computed.0) + } +} + +impl SpecifiedValueInfo for Percentage {} diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs new file mode 100644 index 0000000000..9810959f3a --- /dev/null +++ b/servo/components/style/values/specified/position.rs @@ -0,0 +1,956 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::Ratio as GenericRatio; +use crate::values::generics::position::ZIndex as GenericZIndex; +use crate::values::specified::{AllowQuirks, Integer, LengthPercentage, NonNegativeNumber}; +use crate::{Atom, One, Zero}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use servo_arc::Arc; +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_str(" ")?; + y_pos.to_css(dest) + }, + (x_pos, y_pos) => { + x_pos.to_css(dest)?; + dest.write_str(" ")?; + 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() + } +} + +bitflags! { + /// Controls how the auto-placement algorithm works + /// specifying exactly how auto-placed items get flowed into the grid + #[derive( + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem + )] + #[value_info(other_values = "row,column,dense")] + #[repr(C)] + pub struct 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; + } +} + +#[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, +} + +#[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, +)] +/// 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)) + } + } +} + +#[cfg(feature = "gecko")] +impl From<u8> for MasonryAutoFlow { + fn from(bits: u8) -> MasonryAutoFlow { + use crate::gecko_bindings::structs; + let mut value = MasonryAutoFlow::initial(); + if bits & structs::NS_STYLE_MASONRY_PLACEMENT_PACK as u8 == 0 { + value.placement = MasonryPlacement::Next; + } + if bits & structs::NS_STYLE_MASONRY_ORDER_DEFINITE_FIRST as u8 == 0 { + value.order = MasonryItemOrder::Ordered; + } + value + } +} + +#[cfg(feature = "gecko")] +impl From<MasonryAutoFlow> for u8 { + fn from(v: MasonryAutoFlow) -> u8 { + use crate::gecko_bindings::structs; + + let mut result: u8 = 0; + if v.placement == MasonryPlacement::Pack { + result |= structs::NS_STYLE_MASONRY_PLACEMENT_PACK as u8; + } + if v.order == MasonryItemOrder::DefiniteFirst { + result |= structs::NS_STYLE_MASONRY_ORDER_DEFINITE_FIRST as u8; + } + result + } +} + +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 original CSS string value of each template area + #[css(iterable)] + pub strings: crate::OwnedSlice<crate::OwnedStr>, + /// The number of columns of the grid. + #[css(skip)] + pub width: u32, +} + +impl TemplateAreas { + /// Transform `vector` of str into `template area` + pub fn from_vec(strings: Vec<crate::OwnedStr>) -> Result<Self, ()> { + if strings.is_empty() { + return Err(()); + } + let mut areas: Vec<NamedArea> = vec![]; + let mut width = 0; + { + let mut row = 0u32; + let mut area_indices = PrecomputedHashMap::<Atom, usize>::default(); + for string in &strings { + let mut current_area_index: Option<usize> = None; + row += 1; + let mut column = 0u32; + for token in TemplateAreasTokenizer(string) { + column += 1; + let name = if let Some(token) = token? { + Atom::from(token) + } else { + if let Some(index) = current_area_index.take() { + if areas[index].columns.end != column { + return Err(()); + } + } + continue; + }; + if let Some(index) = current_area_index { + if areas[index].name == name { + if areas[index].rows.start == row { + areas[index].columns.end += 1; + } + continue; + } + if areas[index].columns.end != column { + return Err(()); + } + } + if let Some(index) = area_indices.get(&name).cloned() { + if areas[index].columns.start != column || areas[index].rows.end != row { + return Err(()); + } + areas[index].rows.end += 1; + current_area_index = Some(index); + continue; + } + let index = areas.len(); + assert!(area_indices.insert(name.clone(), index).is_none()); + areas.push(NamedArea { + name, + columns: UnsignedRange { + start: column, + end: column + 1, + }, + rows: UnsignedRange { + start: row, + end: 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 areas[index].columns.end != column + 1 { + assert_ne!(areas[index].rows.start, row); + return Err(()); + } + } + if row == 1 { + width = column; + } else if width != column { + return Err(()); + } + } + } + Ok(TemplateAreas { + areas: areas.into(), + strings: strings.into(), + width, + }) + } +} + +impl Parse for TemplateAreas { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut strings = vec![]; + while let Ok(string) = + input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned().into())) + { + strings.push(string); + } + + TemplateAreas::from_vec(strings) + .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; + + 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; + AspectRatio { + auto: true, + ratio: PreferredRatio::Ratio(GenericRatio( + NonNegativeNumber::new(w), + NonNegativeNumber::new(h), + )), + } + } +} + +/// A specified <ratio> value. +pub type Ratio = GenericRatio<NonNegativeNumber>; + +// https://drafts.csswg.org/css-values-4/#ratios +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..e8674cffaa --- /dev/null +++ b/servo/components/style/values/specified/resolution.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/. */ + +//! Resolution values: +//! +//! https://drafts.csswg.org/css-values/#resolution + +use crate::parser::{Parse, ParserContext}; +use crate::values::CSSFloat; +use cssparser::{Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified resolution. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem, SpecifiedValueInfo)] +pub enum Resolution { + /// Dots per inch. + #[css(dimension)] + Dpi(CSSFloat), + /// An alias unit for dots per pixel. + #[css(dimension)] + X(CSSFloat), + /// Dots per pixel. + #[css(dimension)] + Dppx(CSSFloat), + /// Dots per centimeter. + #[css(dimension)] + Dpcm(CSSFloat), +} + +impl Resolution { + /// Convert this resolution value to dppx units. + pub fn to_dppx(&self) -> CSSFloat { + match *self { + Resolution::X(f) | Resolution::Dppx(f) => f, + _ => self.to_dpi() / 96.0, + } + } + + /// Convert this resolution value to dpi units. + pub fn to_dpi(&self) -> CSSFloat { + match *self { + Resolution::Dpi(f) => f, + Resolution::X(f) | Resolution::Dppx(f) => f * 96.0, + Resolution::Dpcm(f) => f * 2.54, + } + } +} + +impl Parse for Resolution { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let (value, unit) = match *input.next()? { + Token::Dimension { + value, ref unit, .. + } => (value, unit), + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }; + + if value <= 0. { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + match_ignore_ascii_case! { &unit, + "dpi" => Ok(Resolution::Dpi(value)), + "dppx" => Ok(Resolution::Dppx(value)), + "dpcm" => Ok(Resolution::Dpcm(value)), + "x" => Ok(Resolution::X(value)), + _ => Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedDimension(unit.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..edb2c09f73 --- /dev/null +++ b/servo/components/style/values/specified/source_size_list.rs @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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 + +#[cfg(feature = "gecko")] +use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI}; +use crate::media_queries::{Device, MediaCondition}; +use crate::parser::{Parse, ParserContext}; +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: MediaCondition, + value: Length, +} + +impl Parse for SourceSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let condition = MediaCondition::parse(context, input)?; + 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 { + let matching_source_size = self + .source_sizes + .iter() + .find(|source_size| source_size.condition.matches(device, quirks_mode)); + + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + 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, + } + } +} + +#[cfg(feature = "gecko")] +unsafe impl HasFFI for SourceSizeList { + type FFIType = crate::gecko_bindings::structs::RawServoSourceSizeList; +} +#[cfg(feature = "gecko")] +unsafe impl HasSimpleFFI for SourceSizeList {} +#[cfg(feature = "gecko")] +unsafe impl HasBoxFFI for SourceSizeList {} diff --git a/servo/components/style/values/specified/svg.rs b/servo/components/style/values/specified/svg.rs new file mode 100644 index 0000000000..abc6587239 --- /dev/null +++ b/servo/components/style/values/specified/svg.rs @@ -0,0 +1,336 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::{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_str(" ")? + } + self.order_at(pos).to_css(dest)?; + } + Ok(()) + } +} + +bitflags! { + /// The context properties we understand. + #[derive(Default, MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[repr(C)] + pub struct 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, + }) + } +} 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..9f1c5c5e32 --- /dev/null +++ b/servo/components/style/values/specified/svg_path.rs @@ -0,0 +1,896 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::{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}; + +/// 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 result = self + .0 + .iter() + .map(|seg| seg.normalize(&mut state)) + .collect::<Vec<_>>(); + + SVGPathData(crate::ArcSlice::from_iter(result.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 { + // 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. + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> 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)); + } + } + + Ok(SVGPathData(crate::ArcSlice::from_iter( + path_parser.path.into_iter(), + ))) + } +} + +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 result = self + .normalize() + .0 + .iter() + .zip(other.normalize().0.iter()) + .map(|(a, b)| a.animate(&b, procedure)) + .collect::<Result<Vec<_>, _>>()?; + + Ok(SVGPathData(crate::ArcSlice::from_iter(result.into_iter()))) + } +} + +impl ComputeSquaredDistance for SVGPathData { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.0.len() != other.0.len() { + return Err(()); + } + self.normalize() + .0 + .iter() + .zip(other.normalize().0.iter()) + .map(|(this, other)| this.compute_squared_distance(&other)) + .sum() + } +} + +/// 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.0; + } + state.pos.0 = x; + HorizontalLineTo { + x, + absolute: IsAbsolute::Yes, + } + }, + VerticalLineTo { mut y, absolute } => { + if !absolute.is_yes() { + y += state.pos.1; + } + state.pos.1 = 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 + } +} + +/// The path coord type. +#[derive( + AddAssign, + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CoordPair(CSSFloat, 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(::std::f32::MAX as f64) + .max(::std::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/text.rs b/servo/components/style/values/specified/text.rs new file mode 100644 index 0000000000..7c5c712eb3 --- /dev/null +++ b/servo/components/style/values/specified/text.rs @@ -0,0 +1,1195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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::LineHeight as ComputedLineHeight; +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::LineHeight as GenericLineHeight; +use crate::values::generics::text::{GenericTextDecorationLength, Spacing}; +use crate::values::specified::length::NonNegativeLengthPercentage; +use crate::values::specified::length::{FontRelativeLength, Length}; +use crate::values::specified::length::{LengthPercentage, NoCalcLength}; +use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumber, Number}; +use cssparser::{Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use unicode_segmentation::UnicodeSegmentation; + +/// 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 specified value for the `line-height` property. +pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>; + +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) + }) + } +} + +impl ToComputedValue for LineHeight { + type ComputedValue = ComputedLineHeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::specified::length::FontBaseSize; + 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) => length.to_computed_value(context), + LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0) + .to_computed_value(context, FontBaseSize::CurrentStyle), + LengthPercentage::Calc(ref calc) => { + let computed_calc = + calc.to_computed_value_zoomed(context, FontBaseSize::CurrentStyle); + let base = context.style().get_font().clone_font_size().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()) + }, + } + } +} + +/// 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()), + } + } + } +} + +bitflags! { + #[derive(MallocSizeOf, Serialize, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[value_info(other_values = "none,underline,overline,line-through,blink")] + #[repr(C)] + /// Specified keyword values for the text-decoration-line property. + pub struct 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 Parse for TextDecorationLine { + /// none | [ underline || overline || line-through || blink ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = TextDecorationLine::empty(); + + // NOTE(emilio): this loop has this weird structure because we run this + // code to parse the text-decoration shorthand as well, so we need to + // ensure we don't return an error if we don't consume the whole thing + // because we find an invalid identifier or other kind of token. + loop { + let flag: Result<_, ParseError<'i>> = input.try_parse(|input| { + let flag = try_match_ident_ignore_ascii_case! { input, + "none" if result.is_empty() => TextDecorationLine::NONE, + "underline" => TextDecorationLine::UNDERLINE, + "overline" => TextDecorationLine::OVERLINE, + "line-through" => TextDecorationLine::LINE_THROUGH, + "blink" => TextDecorationLine::BLINK, + }; + + Ok(flag) + }); + + let flag = match flag { + Ok(flag) => flag, + Err(..) => break, + }; + + if flag.is_empty() { + return Ok(TextDecorationLine::NONE); + } + + if result.contains(flag) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + result.insert(flag) + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for TextDecorationLine { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("none"); + } + + #[cfg(feature = "gecko")] + { + if *self == TextDecorationLine::COLOR_OVERRIDE { + return Ok(()); + } + } + + let mut writer = SequenceWriter::new(dest, " "); + let mut any = false; + + macro_rules! maybe_write { + ($ident:ident => $str:expr) => { + if self.contains(TextDecorationLine::$ident) { + any = true; + writer.raw_item($str)?; + } + }; + } + + maybe_write!(UNDERLINE => "underline"); + maybe_write!(OVERLINE => "overline"); + maybe_write!(LINE_THROUGH => "line-through"); + maybe_write!(BLINK => "blink"); + + debug_assert!(any); + + Ok(()) + } +} + +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() + } +} + +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 + }, + "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_str(" ")?; + } + } + + 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, +} + +bitflags! { + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] + #[value_info(other_values = "none,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 { + /// full-width + const FULL_WIDTH = 1 << 0; + /// full-size-kana + const FULL_SIZE_KANA = 1 << 1; + } +} + +impl ToCss for TextTransformOther { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let mut writer = SequenceWriter::new(dest, " "); + let mut any = false; + macro_rules! maybe_write { + ($ident:ident => $str:expr) => { + if self.contains(TextTransformOther::$ident) { + writer.raw_item($str)?; + any = true; + } + }; + } + + maybe_write!(FULL_WIDTH => "full-width"); + maybe_write!(FULL_SIZE_KANA => "full-size-kana"); + + debug_assert!(any || self.is_empty()); + + Ok(()) + } +} + +/// 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, + /// `MozCenterOrInherit` value of text-align property. It cannot be parsed, + /// only set directly on the elements and it has a different handling + /// unlike other values. + #[cfg(feature = "gecko")] + #[css(skip)] + 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) => { + // Passing `true` to iterate over extended grapheme clusters, following + // recommendation at http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries + // + // 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(). + let s = s.graphemes(true).next().unwrap_or("").to_string(); + ComputedTextEmphasisStyle::String(s.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 }) + } +} + +/// The allowed horizontal values for the `text-emphasis-position` property. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum TextEmphasisHorizontalWritingModeValue { + /// Draw marks over the text in horizontal writing mode. + Over, + /// Draw marks under the text in horizontal writing mode. + Under, +} + +/// The allowed vertical values for the `text-emphasis-position` property. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum TextEmphasisVerticalWritingModeValue { + /// Draws marks to the right of the text in vertical writing mode. + Right, + /// Draw marks to the left of the text in vertical writing mode. + Left, +} + +/// Specified value of `text-emphasis-position` property. +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct TextEmphasisPosition( + pub TextEmphasisHorizontalWritingModeValue, + pub TextEmphasisVerticalWritingModeValue, +); + +impl TextEmphasisPosition { + #[inline] + /// Returns the initial value of `text-emphasis-position` + pub fn over_right() -> Self { + TextEmphasisPosition( + TextEmphasisHorizontalWritingModeValue::Over, + TextEmphasisVerticalWritingModeValue::Right, + ) + } + + #[cfg(feature = "gecko")] + /// Converts an enumerated value coming from Gecko to a `TextEmphasisPosition`. + pub fn from_gecko_keyword(kw: u32) -> Self { + use crate::gecko_bindings::structs; + + let vert = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT != 0 { + TextEmphasisVerticalWritingModeValue::Right + } else { + debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT != 0); + TextEmphasisVerticalWritingModeValue::Left + }; + let horiz = if kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER != 0 { + TextEmphasisHorizontalWritingModeValue::Over + } else { + debug_assert!(kw & structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER != 0); + TextEmphasisHorizontalWritingModeValue::Under + }; + TextEmphasisPosition(horiz, vert) + } +} + +impl Parse for TextEmphasisPosition { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(horizontal) = + input.try_parse(|input| TextEmphasisHorizontalWritingModeValue::parse(input)) + { + let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?; + Ok(TextEmphasisPosition(horizontal, vertical)) + } else { + let vertical = TextEmphasisVerticalWritingModeValue::parse(input)?; + let horizontal = TextEmphasisHorizontalWritingModeValue::parse(input)?; + Ok(TextEmphasisPosition(horizontal, vertical)) + } + } +} + +#[cfg(feature = "gecko")] +impl From<u8> for TextEmphasisPosition { + fn from(bits: u8) -> Self { + TextEmphasisPosition::from_gecko_keyword(bits as u32) + } +} + +#[cfg(feature = "gecko")] +impl From<TextEmphasisPosition> for u8 { + fn from(v: TextEmphasisPosition) -> u8 { + use crate::gecko_bindings::structs; + + let mut result = match v.0 { + TextEmphasisHorizontalWritingModeValue::Over => { + structs::NS_STYLE_TEXT_EMPHASIS_POSITION_OVER + }, + TextEmphasisHorizontalWritingModeValue::Under => { + structs::NS_STYLE_TEXT_EMPHASIS_POSITION_UNDER + }, + }; + match v.1 { + TextEmphasisVerticalWritingModeValue::Right => { + result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_RIGHT; + }, + TextEmphasisVerticalWritingModeValue::Left => { + result |= structs::NS_STYLE_TEXT_EMPHASIS_POSITION_LEFT; + }, + }; + result as u8 + } +} + +/// 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 `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, +} + +/// 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) + } +} + +bitflags! { + #[derive(MallocSizeOf, 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 { + /// 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; + } +} + +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(()) + } +} diff --git a/servo/components/style/values/specified/time.rs b/servo/components/style/values/specified/time.rs new file mode 100644 index 0000000000..aba3f0a828 --- /dev/null +++ b/servo/components/style/values/specified/time.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/. */ + +//! 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 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, + was_calc: bool, +} + +/// 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(seconds: CSSFloat) -> Self { + Time { + seconds, + unit: TimeUnit::Second, + was_calc: false, + } + } + + /// Returns `0s`. + pub fn zero() -> Self { + Self::from_seconds(0.0) + } + + /// Returns the time in fractional seconds. + pub fn seconds(self) -> CSSFloat { + self.seconds + } + + /// Parses a time according to CSS-VALUES § 6.2. + pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> 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, + was_calc, + }) + } + + /// Returns a `Time` value from a CSS `calc()` expression. + pub fn from_calc(seconds: CSSFloat) -> Self { + Time { + seconds, + unit: TimeUnit::Second, + was_calc: true, + } + } + + 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, /* from_calc = */ false) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(name, location)?; + let time = CalcNode::parse_time(context, input, function)?; + + // FIXME(emilio): Rejecting calc() at parse time is wrong, + // was_calc should probably be replaced by calc_clamping_mode or + // something like we do for numbers, or we should do the + // clamping here instead (simpler, but technically incorrect, + // though still more correct than this!). + if !clamping_mode.is_ok(ParsingMode::DEFAULT, time.seconds) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(time) + }, + 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 ToComputedValue for Time { + type ComputedValue = ComputedTime; + + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + ComputedTime::from_seconds(self.seconds()) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Time { + seconds: computed.seconds(), + unit: TimeUnit::Second, + was_calc: false, + } + } +} + +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, + { + if self.was_calc { + dest.write_str("calc(")?; + } + match self.unit { + TimeUnit::Second => { + self.seconds.to_css(dest)?; + dest.write_str("s")?; + }, + TimeUnit::Millisecond => { + (self.seconds * 1000.).to_css(dest)?; + dest.write_str("ms")?; + }, + } + if self.was_calc { + dest.write_str(")")?; + } + Ok(()) + } +} + +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..ba2be4ee8d --- /dev/null +++ b/servo/components/style/values/specified/transform.rs @@ -0,0 +1,481 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.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, +>; + +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 d = specified::Length::parse_non_negative(context, input)?; + Ok(generic::TransformOperation::Perspective(d)) + }, + _ => 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..74440a6720 --- /dev/null +++ b/servo/components/style/values/specified/ui.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/. */ + +//! 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::url::SpecifiedImageUrl; +use crate::values::specified::Number; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, 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<SpecifiedImageUrl, 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 url = SpecifiedImageUrl::parse(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 { + url, + has_hotspot, + hotspot_x, + hotspot_y, + }) + } +} + +/// Specified value of `-moz-force-broken-image-icon` +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct MozForceBrokenImageIcon(pub bool); + +impl MozForceBrokenImageIcon { + /// Return initial value of -moz-force-broken-image-icon which is false. + #[inline] + pub fn false_value() -> MozForceBrokenImageIcon { + MozForceBrokenImageIcon(false) + } +} + +impl Parse for MozForceBrokenImageIcon { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozForceBrokenImageIcon, ParseError<'i>> { + // We intentionally don't support calc values here. + match input.expect_integer()? { + 0 => Ok(MozForceBrokenImageIcon(false)), + 1 => Ok(MozForceBrokenImageIcon(true)), + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} + +impl ToCss for MozForceBrokenImageIcon { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(if self.0 { "1" } else { "0" }) + } +} + +impl From<u8> for MozForceBrokenImageIcon { + fn from(bits: u8) -> MozForceBrokenImageIcon { + MozForceBrokenImageIcon(bits == 1) + } +} + +impl From<MozForceBrokenImageIcon> for u8 { + fn from(v: MozForceBrokenImageIcon) -> u8 { + if v.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, +} diff --git a/servo/components/style/values/specified/url.rs b/servo/components/style/values/specified/url.rs new file mode 100644 index 0000000000..b0a69b362d --- /dev/null +++ b/servo/components/style/values/specified/url.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/. */ + +//! 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>; + +/// Specified image <url> | <none> +pub type ImageUrlOrNone = GenericUrlOrNone<SpecifiedImageUrl>; |