From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- servo/components/style/values/animated/color.rs | 88 + servo/components/style/values/animated/effects.rs | 27 + servo/components/style/values/animated/font.rs | 37 + servo/components/style/values/animated/grid.rs | 165 ++ servo/components/style/values/animated/lists.rs | 141 ++ servo/components/style/values/animated/mod.rs | 487 +++++ servo/components/style/values/animated/svg.rs | 46 + .../components/style/values/animated/transform.rs | 1667 +++++++++++++++ servo/components/style/values/computed/align.rs | 91 + servo/components/style/values/computed/angle.rs | 101 + .../components/style/values/computed/animation.rs | 70 + .../components/style/values/computed/background.rs | 13 + .../style/values/computed/basic_shape.rs | 37 + servo/components/style/values/computed/border.rs | 84 + servo/components/style/values/computed/box.rs | 388 ++++ servo/components/style/values/computed/color.rs | 95 + servo/components/style/values/computed/column.rs | 11 + servo/components/style/values/computed/counters.rs | 26 + servo/components/style/values/computed/easing.rs | 109 + servo/components/style/values/computed/effects.rs | 44 + servo/components/style/values/computed/flex.rs | 19 + servo/components/style/values/computed/font.rs | 1369 ++++++++++++ servo/components/style/values/computed/image.rs | 205 ++ servo/components/style/values/computed/length.rs | 531 +++++ .../style/values/computed/length_percentage.rs | 1055 ++++++++++ servo/components/style/values/computed/list.rs | 17 + servo/components/style/values/computed/mod.rs | 1035 +++++++++ servo/components/style/values/computed/motion.rs | 70 + servo/components/style/values/computed/outline.rs | 7 + servo/components/style/values/computed/page.rs | 75 + .../components/style/values/computed/percentage.rs | 136 ++ servo/components/style/values/computed/position.rs | 74 + servo/components/style/values/computed/ratio.rs | 115 + servo/components/style/values/computed/rect.rs | 11 + .../components/style/values/computed/resolution.rs | 56 + servo/components/style/values/computed/svg.rs | 66 + servo/components/style/values/computed/table.rs | 7 + servo/components/style/values/computed/text.rs | 228 ++ servo/components/style/values/computed/time.rs | 45 + .../components/style/values/computed/transform.rs | 559 +++++ servo/components/style/values/computed/ui.rs | 21 + servo/components/style/values/computed/url.rs | 15 + servo/components/style/values/distance.rs | 138 ++ .../components/style/values/generics/animation.rs | 140 ++ .../components/style/values/generics/background.rs | 54 + .../style/values/generics/basic_shape.rs | 567 +++++ servo/components/style/values/generics/border.rs | 261 +++ servo/components/style/values/generics/box.rs | 211 ++ servo/components/style/values/generics/calc.rs | 1820 ++++++++++++++++ servo/components/style/values/generics/color.rs | 209 ++ servo/components/style/values/generics/column.rs | 45 + servo/components/style/values/generics/counters.rs | 295 +++ servo/components/style/values/generics/easing.rs | 143 ++ servo/components/style/values/generics/effects.rs | 121 ++ servo/components/style/values/generics/flex.rs | 33 + servo/components/style/values/generics/font.rs | 316 +++ servo/components/style/values/generics/grid.rs | 867 ++++++++ servo/components/style/values/generics/image.rs | 631 ++++++ servo/components/style/values/generics/length.rs | 304 +++ servo/components/style/values/generics/mod.rs | 388 ++++ servo/components/style/values/generics/motion.rs | 270 +++ servo/components/style/values/generics/page.rs | 162 ++ servo/components/style/values/generics/position.rs | 238 +++ servo/components/style/values/generics/ratio.rs | 50 + servo/components/style/values/generics/rect.rs | 146 ++ servo/components/style/values/generics/size.rs | 101 + servo/components/style/values/generics/svg.rs | 221 ++ servo/components/style/values/generics/text.rs | 148 ++ .../components/style/values/generics/transform.rs | 879 ++++++++ servo/components/style/values/generics/ui.rs | 129 ++ servo/components/style/values/generics/url.rs | 47 + servo/components/style/values/mod.rs | 796 +++++++ servo/components/style/values/resolved/color.rs | 48 + servo/components/style/values/resolved/counters.rs | 51 + servo/components/style/values/resolved/mod.rs | 275 +++ servo/components/style/values/specified/align.rs | 820 ++++++++ servo/components/style/values/specified/angle.rs | 276 +++ .../components/style/values/specified/animation.rs | 463 ++++ .../style/values/specified/background.rs | 143 ++ .../style/values/specified/basic_shape.rs | 719 +++++++ servo/components/style/values/specified/border.rs | 398 ++++ servo/components/style/values/specified/box.rs | 1945 +++++++++++++++++ servo/components/style/values/specified/calc.rs | 1086 ++++++++++ servo/components/style/values/specified/color.rs | 1175 +++++++++++ servo/components/style/values/specified/column.rs | 11 + .../components/style/values/specified/counters.rs | 279 +++ servo/components/style/values/specified/easing.rs | 192 ++ servo/components/style/values/specified/effects.rs | 453 ++++ servo/components/style/values/specified/flex.rs | 25 + servo/components/style/values/specified/font.rs | 2222 ++++++++++++++++++++ servo/components/style/values/specified/gecko.rs | 82 + servo/components/style/values/specified/grid.rs | 441 ++++ servo/components/style/values/specified/image.rs | 1340 ++++++++++++ servo/components/style/values/specified/length.rs | 2031 ++++++++++++++++++ servo/components/style/values/specified/list.rs | 202 ++ servo/components/style/values/specified/mod.rs | 992 +++++++++ servo/components/style/values/specified/motion.rs | 343 +++ servo/components/style/values/specified/outline.rs | 71 + servo/components/style/values/specified/page.rs | 99 + .../style/values/specified/percentage.rs | 225 ++ .../components/style/values/specified/position.rs | 955 +++++++++ servo/components/style/values/specified/ratio.rs | 32 + servo/components/style/values/specified/rect.rs | 11 + .../style/values/specified/resolution.rs | 141 ++ .../style/values/specified/source_size_list.rs | 136 ++ servo/components/style/values/specified/svg.rs | 404 ++++ .../components/style/values/specified/svg_path.rs | 1029 +++++++++ servo/components/style/values/specified/table.rs | 36 + servo/components/style/values/specified/text.rs | 1193 +++++++++++ servo/components/style/values/specified/time.rs | 183 ++ .../components/style/values/specified/transform.rs | 530 +++++ servo/components/style/values/specified/ui.rs | 257 +++ servo/components/style/values/specified/url.rs | 15 + 113 files changed, 40502 insertions(+) create mode 100644 servo/components/style/values/animated/color.rs create mode 100644 servo/components/style/values/animated/effects.rs create mode 100644 servo/components/style/values/animated/font.rs create mode 100644 servo/components/style/values/animated/grid.rs create mode 100644 servo/components/style/values/animated/lists.rs create mode 100644 servo/components/style/values/animated/mod.rs create mode 100644 servo/components/style/values/animated/svg.rs create mode 100644 servo/components/style/values/animated/transform.rs create mode 100644 servo/components/style/values/computed/align.rs create mode 100644 servo/components/style/values/computed/angle.rs create mode 100644 servo/components/style/values/computed/animation.rs create mode 100644 servo/components/style/values/computed/background.rs create mode 100644 servo/components/style/values/computed/basic_shape.rs create mode 100644 servo/components/style/values/computed/border.rs create mode 100644 servo/components/style/values/computed/box.rs create mode 100644 servo/components/style/values/computed/color.rs create mode 100644 servo/components/style/values/computed/column.rs create mode 100644 servo/components/style/values/computed/counters.rs create mode 100644 servo/components/style/values/computed/easing.rs create mode 100644 servo/components/style/values/computed/effects.rs create mode 100644 servo/components/style/values/computed/flex.rs create mode 100644 servo/components/style/values/computed/font.rs create mode 100644 servo/components/style/values/computed/image.rs create mode 100644 servo/components/style/values/computed/length.rs create mode 100644 servo/components/style/values/computed/length_percentage.rs create mode 100644 servo/components/style/values/computed/list.rs create mode 100644 servo/components/style/values/computed/mod.rs create mode 100644 servo/components/style/values/computed/motion.rs create mode 100644 servo/components/style/values/computed/outline.rs create mode 100644 servo/components/style/values/computed/page.rs create mode 100644 servo/components/style/values/computed/percentage.rs create mode 100644 servo/components/style/values/computed/position.rs create mode 100644 servo/components/style/values/computed/ratio.rs create mode 100644 servo/components/style/values/computed/rect.rs create mode 100644 servo/components/style/values/computed/resolution.rs create mode 100644 servo/components/style/values/computed/svg.rs create mode 100644 servo/components/style/values/computed/table.rs create mode 100644 servo/components/style/values/computed/text.rs create mode 100644 servo/components/style/values/computed/time.rs create mode 100644 servo/components/style/values/computed/transform.rs create mode 100644 servo/components/style/values/computed/ui.rs create mode 100644 servo/components/style/values/computed/url.rs create mode 100644 servo/components/style/values/distance.rs create mode 100644 servo/components/style/values/generics/animation.rs create mode 100644 servo/components/style/values/generics/background.rs create mode 100644 servo/components/style/values/generics/basic_shape.rs create mode 100644 servo/components/style/values/generics/border.rs create mode 100644 servo/components/style/values/generics/box.rs create mode 100644 servo/components/style/values/generics/calc.rs create mode 100644 servo/components/style/values/generics/color.rs create mode 100644 servo/components/style/values/generics/column.rs create mode 100644 servo/components/style/values/generics/counters.rs create mode 100644 servo/components/style/values/generics/easing.rs create mode 100644 servo/components/style/values/generics/effects.rs create mode 100644 servo/components/style/values/generics/flex.rs create mode 100644 servo/components/style/values/generics/font.rs create mode 100644 servo/components/style/values/generics/grid.rs create mode 100644 servo/components/style/values/generics/image.rs create mode 100644 servo/components/style/values/generics/length.rs create mode 100644 servo/components/style/values/generics/mod.rs create mode 100644 servo/components/style/values/generics/motion.rs create mode 100644 servo/components/style/values/generics/page.rs create mode 100644 servo/components/style/values/generics/position.rs create mode 100644 servo/components/style/values/generics/ratio.rs create mode 100644 servo/components/style/values/generics/rect.rs create mode 100644 servo/components/style/values/generics/size.rs create mode 100644 servo/components/style/values/generics/svg.rs create mode 100644 servo/components/style/values/generics/text.rs create mode 100644 servo/components/style/values/generics/transform.rs create mode 100644 servo/components/style/values/generics/ui.rs create mode 100644 servo/components/style/values/generics/url.rs create mode 100644 servo/components/style/values/mod.rs create mode 100644 servo/components/style/values/resolved/color.rs create mode 100644 servo/components/style/values/resolved/counters.rs create mode 100644 servo/components/style/values/resolved/mod.rs create mode 100644 servo/components/style/values/specified/align.rs create mode 100644 servo/components/style/values/specified/angle.rs create mode 100644 servo/components/style/values/specified/animation.rs create mode 100644 servo/components/style/values/specified/background.rs create mode 100644 servo/components/style/values/specified/basic_shape.rs create mode 100644 servo/components/style/values/specified/border.rs create mode 100644 servo/components/style/values/specified/box.rs create mode 100644 servo/components/style/values/specified/calc.rs create mode 100644 servo/components/style/values/specified/color.rs create mode 100644 servo/components/style/values/specified/column.rs create mode 100644 servo/components/style/values/specified/counters.rs create mode 100644 servo/components/style/values/specified/easing.rs create mode 100644 servo/components/style/values/specified/effects.rs create mode 100644 servo/components/style/values/specified/flex.rs create mode 100644 servo/components/style/values/specified/font.rs create mode 100644 servo/components/style/values/specified/gecko.rs create mode 100644 servo/components/style/values/specified/grid.rs create mode 100644 servo/components/style/values/specified/image.rs create mode 100644 servo/components/style/values/specified/length.rs create mode 100644 servo/components/style/values/specified/list.rs create mode 100644 servo/components/style/values/specified/mod.rs create mode 100644 servo/components/style/values/specified/motion.rs create mode 100644 servo/components/style/values/specified/outline.rs create mode 100644 servo/components/style/values/specified/page.rs create mode 100644 servo/components/style/values/specified/percentage.rs create mode 100644 servo/components/style/values/specified/position.rs create mode 100644 servo/components/style/values/specified/ratio.rs create mode 100644 servo/components/style/values/specified/rect.rs create mode 100644 servo/components/style/values/specified/resolution.rs create mode 100644 servo/components/style/values/specified/source_size_list.rs create mode 100644 servo/components/style/values/specified/svg.rs create mode 100644 servo/components/style/values/specified/svg_path.rs create mode 100644 servo/components/style/values/specified/table.rs create mode 100644 servo/components/style/values/specified/text.rs create mode 100644 servo/components/style/values/specified/time.rs create mode 100644 servo/components/style/values/specified/transform.rs create mode 100644 servo/components/style/values/specified/ui.rs create mode 100644 servo/components/style/values/specified/url.rs (limited to 'servo/components/style/values') diff --git a/servo/components/style/values/animated/color.rs b/servo/components/style/values/animated/color.rs new file mode 100644 index 0000000000..f608b72e53 --- /dev/null +++ b/servo/components/style/values/animated/color.rs @@ -0,0 +1,88 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for CSS colors. + +use crate::color::mix::ColorInterpolationMethod; +use crate::color::AbsoluteColor; +use crate::values::animated::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::Percentage; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::color::{ColorMixFlags, GenericColor, GenericColorMix}; + +impl Animate for AbsoluteColor { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let (left_weight, right_weight) = procedure.weights(); + Ok(crate::color::mix::mix( + ColorInterpolationMethod::best_interpolation_between(self, other), + self, + left_weight as f32, + other, + right_weight as f32, + ColorMixFlags::empty(), + )) + } +} + +impl ComputeSquaredDistance for AbsoluteColor { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + let start = [ + self.alpha, + self.components.0 * self.alpha, + self.components.1 * self.alpha, + self.components.2 * self.alpha, + ]; + let end = [ + other.alpha, + other.components.0 * other.alpha, + other.components.1 * other.alpha, + other.components.2 * other.alpha, + ]; + start + .iter() + .zip(&end) + .map(|(this, other)| this.compute_squared_distance(other)) + .sum() + } +} + +/// An animated value for ``. +pub type Color = GenericColor; + +/// An animated value for ``. +pub type ColorMix = GenericColorMix; + +impl Animate for Color { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let (left_weight, right_weight) = procedure.weights(); + Ok(Self::from_color_mix(ColorMix { + interpolation: ColorInterpolationMethod::srgb(), + left: self.clone(), + left_percentage: Percentage(left_weight as f32), + right: other.clone(), + right_percentage: Percentage(right_weight as f32), + // See https://github.com/w3c/csswg-drafts/issues/7324 + flags: ColorMixFlags::empty(), + })) + } +} + +impl ComputeSquaredDistance for Color { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + let current_color = AbsoluteColor::TRANSPARENT_BLACK; + self.resolve_to_absolute(¤t_color) + .compute_squared_distance(&other.resolve_to_absolute(¤t_color)) + } +} + +impl ToAnimatedZero for Color { + #[inline] + fn to_animated_zero(&self) -> Result { + Ok(Color::Absolute(AbsoluteColor::TRANSPARENT_BLACK)) + } +} diff --git a/servo/components/style/values/animated/effects.rs b/servo/components/style/values/animated/effects.rs new file mode 100644 index 0000000000..67557e54b7 --- /dev/null +++ b/servo/components/style/values/animated/effects.rs @@ -0,0 +1,27 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for CSS values related to effects. + +use crate::values::animated::color::Color; +use crate::values::computed::length::Length; +#[cfg(feature = "gecko")] +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Angle, Number}; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +#[cfg(not(feature = "gecko"))] +use crate::values::Impossible; + +/// An animated value for the `drop-shadow()` filter. +pub type AnimatedSimpleShadow = GenericSimpleShadow; + +/// An animated value for a single `filter`. +#[cfg(feature = "gecko")] +pub type AnimatedFilter = + GenericFilter; + +/// An animated value for a single `filter`. +#[cfg(not(feature = "gecko"))] +pub type AnimatedFilter = GenericFilter; diff --git a/servo/components/style/values/animated/font.rs b/servo/components/style/values/animated/font.rs new file mode 100644 index 0000000000..63d4a14b2f --- /dev/null +++ b/servo/components/style/values/animated/font.rs @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementation for various font-related types. + +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::font::FontVariationSettings; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; + +/// +/// +/// Note that the ComputedValue implementation will already have sorted and de-dup'd +/// the lists of settings, so we can just iterate over the two lists together and +/// animate their individual values. +impl Animate for FontVariationSettings { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let result: Vec<_> = + super::lists::by_computed_value::animate(&self.0, &other.0, procedure)?; + Ok(Self(result.into_boxed_slice())) + } +} + +impl ComputeSquaredDistance for FontVariationSettings { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + super::lists::by_computed_value::squared_distance(&self.0, &other.0) + } +} + +impl ToAnimatedZero for FontVariationSettings { + #[inline] + fn to_animated_zero(&self) -> Result { + Err(()) + } +} diff --git a/servo/components/style/values/animated/grid.rs b/servo/components/style/values/animated/grid.rs new file mode 100644 index 0000000000..04f1a2fcaa --- /dev/null +++ b/servo/components/style/values/animated/grid.rs @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementation for various grid-related types. + +// Note: we can implement Animate on their generic types directly, but in this case we need to +// make sure two trait bounds, L: Clone and I: PartialEq, are satisfied on almost all the +// grid-related types and their other trait implementations because Animate needs them. So in +// order to avoid adding these two trait bounds (or maybe more..) everywhere, we implement +// Animate for the computed types, instead of the generic types. + +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::Integer; +use crate::values::computed::LengthPercentage; +use crate::values::computed::{GridTemplateComponent, TrackList, TrackSize}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::grid as generics; + +fn discrete(from: &T, to: &T, procedure: Procedure) -> Result { + if let Procedure::Interpolate { progress } = procedure { + Ok(if progress < 0.5 { + from.clone() + } else { + to.clone() + }) + } else { + // The discrete animation is not additive, so per spec [1] we should use the |from|, which + // is the underlying value. However this mismatches our animation mechanism (see + // composite_endpoint() in servo/ports/geckolib/glues.rs), which uses the effect value + // (i.e. |to| value here) [2]. So in order to match the behavior of other properties and + // other browsers, we use |to| value for addition and accumulation, i.e. Vresult = Vb. + // + // [1] https://drafts.csswg.org/css-values-4/#not-additive + // [2] https://github.com/w3c/csswg-drafts/issues/9070 + Ok(to.clone()) + } +} + +fn animate_with_discrete_fallback( + from: &T, + to: &T, + procedure: Procedure, +) -> Result { + from.animate(to, procedure) + .or_else(|_| discrete(from, to, procedure)) +} + +impl Animate for TrackSize { + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + 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 { + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + // If the keyword, auto-fit/fill, is the same it can result in different + // number of tracks. For both auto-fit/fill, the number of columns isn't + // known until you do layout since it depends on the container size, item + // placement and other factors, so we cannot do the correct interpolation + // by computed values. Therefore, return Err(()) if it's keywords. If it + // is Number, we support animation only if the count is the same and the + // length of track_sizes is the same. + // https://github.com/w3c/csswg-drafts/issues/3503 + match (&self.count, &other.count) { + (&generics::RepeatCount::Number(from), &generics::RepeatCount::Number(to)) + if from == to => + { + () + }, + (_, _) => return Err(()), + } + + let count = self.count; + let track_sizes = super::lists::by_computed_value::animate( + &self.track_sizes, + &other.track_sizes, + procedure, + )?; + + // The length of |line_names| is always 0 or N+1, where N is the length + // of |track_sizes|. Besides, is always discrete. + let line_names = discrete(&self.line_names, &other.line_names, procedure)?; + + Ok(generics::TrackRepeat { + count, + line_names, + track_sizes, + }) + } +} + +impl Animate for TrackList { + // Based on https://github.com/w3c/csswg-drafts/issues/3201: + // 1. Check interpolation type per track, so we need to handle discrete animations + // in TrackSize, so any Err(()) returned from TrackSize doesn't make all TrackSize + // fallback to discrete animation. + // 2. line-names is always discrete. + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + 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 . + // This may be updated in the future. + // https://github.com/w3c/csswg-drafts/issues/3503 + if self.has_auto_repeat() || other.has_auto_repeat() { + return Err(()); + } + + let values = + super::lists::by_computed_value::animate(&self.values, &other.values, procedure)?; + + // The length of |line_names| is always 0 or N+1, where N is the length + // of |track_sizes|. Besides, is always discrete. + let line_names = discrete(&self.line_names, &other.line_names, procedure)?; + + Ok(TrackList { + values, + line_names, + auto_repeat_index: self.auto_repeat_index, + }) + } +} + +impl ComputeSquaredDistance for GridTemplateComponent { + #[inline] + fn compute_squared_distance(&self, _other: &Self) -> Result { + // TODO: Bug 1518585, we should implement ComputeSquaredDistance. + Err(()) + } +} + +impl ToAnimatedZero for GridTemplateComponent { + #[inline] + fn to_animated_zero(&self) -> Result { + // It's not clear to get a zero grid track list based on the current definition + // of spec, so we return Err(()) directly. + Err(()) + } +} diff --git a/servo/components/style/values/animated/lists.rs b/servo/components/style/values/animated/lists.rs new file mode 100644 index 0000000000..8b3898c497 --- /dev/null +++ b/servo/components/style/values/animated/lists.rs @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Lists have various ways of being animated, this module implements them. +//! +//! See https://drafts.csswg.org/web-animations-1/#animating-properties + +/// https://drafts.csswg.org/web-animations-1/#by-computed-value +pub mod by_computed_value { + use crate::values::{ + animated::{Animate, Procedure}, + distance::{ComputeSquaredDistance, SquaredDistance}, + }; + use std::iter::FromIterator; + + #[allow(missing_docs)] + pub fn animate(left: &[T], right: &[T], procedure: Procedure) -> Result + where + T: Animate, + C: FromIterator, + { + if left.len() != right.len() { + return Err(()); + } + left.iter() + .zip(right.iter()) + .map(|(left, right)| left.animate(right, procedure)) + .collect() + } + + #[allow(missing_docs)] + pub fn squared_distance(left: &[T], right: &[T]) -> Result + where + T: ComputeSquaredDistance, + { + if left.len() != right.len() { + return Err(()); + } + left.iter() + .zip(right.iter()) + .map(|(left, right)| left.compute_squared_distance(right)) + .sum() + } +} + +/// This is the animation used for some of the types like shadows and filters, where the +/// interpolation happens with the zero value if one of the sides is not present. +/// +/// https://drafts.csswg.org/web-animations-1/#animating-shadow-lists +pub mod with_zero { + use crate::values::animated::ToAnimatedZero; + use crate::values::{ + animated::{Animate, Procedure}, + distance::{ComputeSquaredDistance, SquaredDistance}, + }; + use itertools::{EitherOrBoth, Itertools}; + use std::iter::FromIterator; + + #[allow(missing_docs)] + pub fn animate(left: &[T], right: &[T], procedure: Procedure) -> Result + where + T: Animate + Clone + ToAnimatedZero, + C: FromIterator, + { + if procedure == Procedure::Add { + return Ok(left.iter().chain(right.iter()).cloned().collect()); + } + left.iter() + .zip_longest(right.iter()) + .map(|it| match it { + EitherOrBoth::Both(left, right) => left.animate(right, procedure), + EitherOrBoth::Left(left) => left.animate(&left.to_animated_zero()?, procedure), + EitherOrBoth::Right(right) => right.to_animated_zero()?.animate(right, procedure), + }) + .collect() + } + + #[allow(missing_docs)] + pub fn squared_distance(left: &[T], right: &[T]) -> Result + where + T: ToAnimatedZero + ComputeSquaredDistance, + { + left.iter() + .zip_longest(right.iter()) + .map(|it| match it { + EitherOrBoth::Both(left, right) => left.compute_squared_distance(right), + EitherOrBoth::Left(item) | EitherOrBoth::Right(item) => { + item.to_animated_zero()?.compute_squared_distance(item) + }, + }) + .sum() + } +} + +/// https://drafts.csswg.org/web-animations-1/#repeatable-list +pub mod repeatable_list { + use crate::values::{ + animated::{Animate, Procedure}, + distance::{ComputeSquaredDistance, SquaredDistance}, + }; + use std::iter::FromIterator; + + #[allow(missing_docs)] + pub fn animate(left: &[T], right: &[T], procedure: Procedure) -> Result + where + T: Animate, + C: FromIterator, + { + use num_integer::lcm; + // If the length of either list is zero, the least common multiple is undefined. + if left.is_empty() || right.is_empty() { + return Err(()); + } + let len = lcm(left.len(), right.len()); + left.iter() + .cycle() + .zip(right.iter().cycle()) + .take(len) + .map(|(left, right)| left.animate(right, procedure)) + .collect() + } + + #[allow(missing_docs)] + pub fn squared_distance(left: &[T], right: &[T]) -> Result + where + T: ComputeSquaredDistance, + { + use num_integer::lcm; + if left.is_empty() || right.is_empty() { + return Err(()); + } + let len = lcm(left.len(), right.len()); + left.iter() + .cycle() + .zip(right.iter().cycle()) + .take(len) + .map(|(left, right)| left.compute_squared_distance(right)) + .sum() + } +} diff --git a/servo/components/style/values/animated/mod.rs b/servo/components/style/values/animated/mod.rs new file mode 100644 index 0000000000..31ea206fc0 --- /dev/null +++ b/servo/components/style/values/animated/mod.rs @@ -0,0 +1,487 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated values. +//! +//! Some values, notably colors, cannot be interpolated directly with their +//! computed values and need yet another intermediate representation. This +//! module's raison d'être is to ultimately contain all these types. + +use crate::color::AbsoluteColor; +use crate::properties::PropertyId; +use crate::values::computed::length::LengthPercentage; +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::Angle as ComputedAngle; +use crate::values::computed::Image; +use crate::values::specified::SVGPathData; +use crate::values::CSSFloat; +use app_units::Au; +use smallvec::SmallVec; +use std::cmp; + +pub mod color; +pub mod effects; +mod font; +mod grid; +pub mod lists; +mod svg; +pub mod transform; + +/// The category a property falls into for ordering purposes. +/// +/// https://drafts.csswg.org/web-animations/#calculating-computed-keyframes +#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)] +enum PropertyCategory { + Custom, + PhysicalLonghand, + LogicalLonghand, + Shorthand, +} + +impl PropertyCategory { + fn of(id: &PropertyId) -> Self { + match *id { + PropertyId::NonCustom(id) => match id.longhand_or_shorthand() { + Ok(id) => if id.is_logical() { + PropertyCategory::LogicalLonghand + } else { + PropertyCategory::PhysicalLonghand + }, + Err(..) => PropertyCategory::Shorthand, + }, + PropertyId::Custom(..) => PropertyCategory::Custom, + } + } +} + +/// A comparator to sort PropertyIds such that physical longhands are sorted +/// before logical longhands and shorthands, shorthands with fewer components +/// are sorted before shorthands with more components, and otherwise shorthands +/// are sorted by IDL name as defined by [Web Animations][property-order]. +/// +/// Using this allows us to prioritize values specified by longhands (or smaller +/// shorthand subsets) when longhands and shorthands are both specified on the +/// one keyframe. +/// +/// [property-order] https://drafts.csswg.org/web-animations/#calculating-computed-keyframes +pub fn compare_property_priority(a: &PropertyId, b: &PropertyId) -> cmp::Ordering { + let a_category = PropertyCategory::of(a); + let b_category = PropertyCategory::of(b); + + if a_category != b_category { + return a_category.cmp(&b_category); + } + + if a_category != PropertyCategory::Shorthand { + return cmp::Ordering::Equal; + } + + let a = a.as_shorthand().unwrap(); + let b = b.as_shorthand().unwrap(); + // Within shorthands, sort by the number of subproperties, then by IDL + // name. + let subprop_count_a = a.longhands().count(); + let subprop_count_b = b.longhands().count(); + subprop_count_a + .cmp(&subprop_count_b) + .then_with(|| a.idl_name_sort_order().cmp(&b.idl_name_sort_order())) +} + +/// A helper function to animate two multiplicative factor. +pub fn animate_multiplicative_factor( + this: CSSFloat, + other: CSSFloat, + procedure: Procedure, +) -> Result { + 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; +} + +/// An animation procedure. +/// +/// +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Procedure { + /// + Interpolate { progress: f64 }, + /// + Add, + /// + 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; +} + +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.), + } + } +} + +/// +impl Animate for i32 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + Ok(((*self as f64).animate(&(*other as f64), procedure)? + 0.5).floor() as i32) + } +} + +/// +impl Animate for f32 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let ret = (*self as f64).animate(&(*other as f64), procedure)?; + Ok(ret.min(f32::MAX as f64).max(f32::MIN as f64) as f32) + } +} + +/// +impl Animate for f64 { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let (self_weight, other_weight) = procedure.weights(); + + let ret = *self * self_weight + *other * other_weight; + Ok(ret.min(f64::MAX).max(f64::MIN)) + } +} + +impl Animate for Option +where + T: Animate, +{ + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + 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 { + Ok(Au::new(self.0.animate(&other.0, procedure)?)) + } +} + +impl Animate for Box { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + Ok(Box::new((**self).animate(&other, procedure)?)) + } +} + +impl ToAnimatedValue for Option +where + T: ToAnimatedValue, +{ + type AnimatedValue = Option<::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 ToAnimatedValue for Vec +where + T: ToAnimatedValue, +{ + type AnimatedValue = Vec<::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 ToAnimatedValue for Box +where + T: ToAnimatedValue, +{ + type AnimatedValue = Box<::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 ToAnimatedValue for Box<[T]> +where + T: ToAnimatedValue, +{ + type AnimatedValue = Box<[::AnimatedValue]>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_vec() + .into_iter() + .map(T::to_animated_value) + .collect::>() + .into_boxed_slice() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated + .into_vec() + .into_iter() + .map(T::from_animated_value) + .collect::>() + .into_boxed_slice() + } +} + +impl ToAnimatedValue for crate::OwnedSlice +where + T: ToAnimatedValue, +{ + type AnimatedValue = crate::OwnedSlice<::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 ToAnimatedValue for SmallVec<[T; 1]> +where + T: ToAnimatedValue, +{ + type AnimatedValue = SmallVec<[T::AnimatedValue; 1]>; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.into_iter().map(T::to_animated_value).collect() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated.into_iter().map(T::from_animated_value).collect() + } +} + +macro_rules! trivial_to_animated_value { + ($ty:ty) => { + impl $crate::values::animated::ToAnimatedValue for $ty { + type AnimatedValue = Self; + + #[inline] + fn to_animated_value(self) -> Self { + self + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + animated + } + } + }; +} + +trivial_to_animated_value!(Au); +trivial_to_animated_value!(LengthPercentage); +trivial_to_animated_value!(ComputedAngle); +trivial_to_animated_value!(ComputedUrl); +trivial_to_animated_value!(bool); +trivial_to_animated_value!(f32); +trivial_to_animated_value!(i32); +trivial_to_animated_value!(AbsoluteColor); +trivial_to_animated_value!(crate::values::generics::color::ColorMixFlags); +// Note: This implementation is for ToAnimatedValue of ShapeSource. +// +// SVGPathData uses Box<[T]>. If we want to derive ToAnimatedValue for all the +// types, we have to do "impl ToAnimatedValue for Box<[T]>" first. +// However, the general version of "impl ToAnimatedValue for Box<[T]>" needs to +// clone |T| and convert it into |T::AnimatedValue|. However, for SVGPathData +// that is unnecessary--moving |T| is sufficient. So here, we implement this +// trait manually. +trivial_to_animated_value!(SVGPathData); +// FIXME: Bug 1514342, Image is not animatable, but we still need to implement +// this to avoid adding this derive to generic::Image and all its arms. We can +// drop this after landing Bug 1514342. +trivial_to_animated_value!(Image); + +impl ToAnimatedZero for Au { + #[inline] + fn to_animated_zero(&self) -> Result { + Ok(Au(0)) + } +} + +impl ToAnimatedZero for f32 { + #[inline] + fn to_animated_zero(&self) -> Result { + Ok(0.) + } +} + +impl ToAnimatedZero for f64 { + #[inline] + fn to_animated_zero(&self) -> Result { + Ok(0.) + } +} + +impl ToAnimatedZero for i32 { + #[inline] + fn to_animated_zero(&self) -> Result { + Ok(0) + } +} + +impl ToAnimatedZero for Box +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result { + Ok(Box::new((**self).to_animated_zero()?)) + } +} + +impl ToAnimatedZero for Option +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result { + match *self { + Some(ref value) => Ok(Some(value.to_animated_zero()?)), + None => Ok(None), + } + } +} + +impl ToAnimatedZero for Vec +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl ToAnimatedZero for Box<[T]> +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl ToAnimatedZero for crate::OwnedSlice +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result { + self.iter().map(|v| v.to_animated_zero()).collect() + } +} + +impl ToAnimatedZero for crate::ArcSlice +where + T: ToAnimatedZero, +{ + #[inline] + fn to_animated_zero(&self) -> Result { + let v = self + .iter() + .map(|v| v.to_animated_zero()) + .collect::, _>>()?; + Ok(crate::ArcSlice::from_iter(v.into_iter())) + } +} diff --git a/servo/components/style/values/animated/svg.rs b/servo/components/style/values/animated/svg.rs new file mode 100644 index 0000000000..04e35098ad --- /dev/null +++ b/servo/components/style/values/animated/svg.rs @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animation implementations for various SVG-related types. + +use super::{Animate, Procedure}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::svg::SVGStrokeDashArray; + +/// +impl Animate for SVGStrokeDashArray +where + L: Clone + Animate, +{ + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) { + // Non-additive. + return Err(()); + } + match (self, other) { + (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => { + Ok(SVGStrokeDashArray::Values( + super::lists::repeatable_list::animate(this, other, procedure)?, + )) + }, + _ => Err(()), + } + } +} + +impl ComputeSquaredDistance for SVGStrokeDashArray +where + L: ComputeSquaredDistance, +{ + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + match (self, other) { + (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => { + super::lists::repeatable_list::squared_distance(this, other) + }, + _ => Err(()), + } + } +} diff --git a/servo/components/style/values/animated/transform.rs b/servo/components/style/values/animated/transform.rs new file mode 100644 index 0000000000..b91e3ed8bc --- /dev/null +++ b/servo/components/style/values/animated/transform.rs @@ -0,0 +1,1667 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Animated types for transform. +// There are still some implementation on Matrix3D in animated_properties.mako.rs +// because they still need mako to generate the code. + +use super::animate_multiplicative_factor; +use super::{Animate, Procedure, ToAnimatedZero}; +use crate::values::computed::transform::Rotate as ComputedRotate; +use crate::values::computed::transform::Scale as ComputedScale; +use crate::values::computed::transform::Transform as ComputedTransform; +use crate::values::computed::transform::TransformOperation as ComputedTransformOperation; +use crate::values::computed::transform::Translate as ComputedTranslate; +use crate::values::computed::transform::{DirectionVector, Matrix, Matrix3D}; +use crate::values::computed::Angle; +use crate::values::computed::{Length, LengthPercentage}; +use crate::values::computed::{Number, Percentage}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::transform::{self, Transform, TransformOperation}; +use crate::values::generics::transform::{Rotate, Scale, Translate}; +use crate::values::CSSFloat; +use crate::Zero; +use std::cmp; +use std::ops::Add; + +// ------------------------------------ +// Animations for Matrix/Matrix3D. +// ------------------------------------ +/// A 2d matrix for interpolation. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[allow(missing_docs)] +// FIXME: We use custom derive for ComputeSquaredDistance. However, If possible, we should convert +// the InnerMatrix2D into types with physical meaning. This custom derive computes the squared +// distance from each matrix item, and this makes the result different from that in Gecko if we +// have skew factor in the Matrix3D. +pub struct InnerMatrix2D { + pub m11: CSSFloat, + pub m12: CSSFloat, + pub m21: CSSFloat, + pub m22: CSSFloat, +} + +impl Animate for InnerMatrix2D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + 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 { + 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 { + /// + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + // 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 { + // 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 for MatrixDecomposed2D { + /// Decompose 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 for Matrix3D { + /// Recompose 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 { + let this = Matrix3D::from(*self); + let other = Matrix3D::from(*other); + let this = MatrixDecomposed2D::from(this); + let other = MatrixDecomposed2D::from(other); + Matrix3D::from(this.animate(&other, procedure)?).into_2d() + } + + #[cfg(feature = "gecko")] + // Gecko doesn't exactly follow the spec here; we use a different procedure + // to match it + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let this = Matrix3D::from(*self); + let other = Matrix3D::from(*other); + let from = decompose_2d_matrix(&this)?; + let to = decompose_2d_matrix(&other)?; + Matrix3D::from(from.animate(&to, procedure)?).into_2d() + } +} + +/// A 3d translation. +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)] +pub struct Translate3D(pub f32, pub f32, pub f32); + +/// A 3d scale function. +#[derive(Clone, ComputeSquaredDistance, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Scale3D(pub f32, pub f32, pub f32); + +impl Scale3D { + /// Negate self. + fn negate(&mut self) { + self.0 *= -1.0; + self.1 *= -1.0; + self.2 *= -1.0; + } +} + +impl Animate for Scale3D { + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + 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 { + 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 { + Ok(Perspective( + self.0.animate(&other.0, procedure)?, + self.1.animate(&other.1, procedure)?, + self.2.animate(&other.2, procedure)?, + animate_multiplicative_factor(self.3, other.3, procedure)?, + )) + } +} + +/// A quaternion used to represent a rotation. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +pub struct Quaternion(f64, f64, f64, f64); + +impl Quaternion { + /// Return a quaternion from a unit direction vector and angle (unit: radian). + #[inline] + fn from_direction_and_angle(vector: &DirectionVector, angle: f64) -> Self { + debug_assert!( + (vector.length() - 1.).abs() < 0.0001, + "Only accept an unit direction vector to create a quaternion" + ); + + // Quaternions between the range [360, 720] will treated as rotations at the other + // direction: [-360, 0]. And quaternions between the range [720*k, 720*(k+1)] will be + // treated as rotations [0, 720]. So it does not make sense to use quaternions to rotate + // the element more than ±360deg. Therefore, we have to make sure its range is (-360, 360). + let half_angle = angle + .abs() + .rem_euclid(std::f64::consts::TAU) + .copysign(angle) / + 2.; + + // Reference: + // https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation + // + // if the direction axis is (x, y, z) = xi + yj + zk, + // and the angle is |theta|, this formula can be done using + // an extension of Euler's formula: + // q = cos(theta/2) + (xi + yj + zk)(sin(theta/2)) + // = cos(theta/2) + + // x*sin(theta/2)i + y*sin(theta/2)j + z*sin(theta/2)k + Quaternion( + vector.x as f64 * half_angle.sin(), + vector.y as f64 * half_angle.sin(), + vector.z as f64 * half_angle.sin(), + half_angle.cos(), + ) + } + + /// Calculate the dot product. + #[inline] + fn dot(&self, other: &Self) -> f64 { + self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3 + } + + /// Return the scaled quaternion by a factor. + #[inline] + fn scale(&self, factor: f64) -> Self { + Quaternion( + self.0 * factor, + self.1 * factor, + self.2 * factor, + self.3 * factor, + ) + } +} + +impl Add for Quaternion { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self( + self.0 + other.0, + self.1 + other.1, + self.2 + other.2, + self.3 + other.3, + ) + } +} + +impl Animate for Quaternion { + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + let (this_weight, other_weight) = procedure.weights(); + debug_assert!( + // Doule EPSILON since both this_weight and other_weght have calculation errors + // which are approximately equal to EPSILON. + (this_weight + other_weight - 1.0f64).abs() <= f64::EPSILON * 2.0 || + other_weight == 1.0f64 || + other_weight == 0.0f64, + "animate should only be used for interpolating or accumulating transforms" + ); + + // We take a specialized code path for accumulation (where other_weight + // is 1). + if let Procedure::Accumulate { .. } = procedure { + debug_assert_eq!(other_weight, 1.0); + if this_weight == 0.0 { + return Ok(*other); + } + + let clamped_w = self.3.min(1.0).max(-1.0); + + // Determine the scale factor. + let mut theta = clamped_w.acos(); + let mut scale = if theta == 0.0 { 0.0 } else { 1.0 / theta.sin() }; + theta *= this_weight; + scale *= theta.sin(); + + // Scale the self matrix by this_weight. + let mut scaled_self = *self; + scaled_self.0 *= scale; + scaled_self.1 *= scale; + scaled_self.2 *= scale; + scaled_self.3 = theta.cos(); + + // Multiply scaled-self by other. + let a = &scaled_self; + let b = other; + return Ok(Quaternion( + a.3 * b.0 + a.0 * b.3 + a.1 * b.2 - a.2 * b.1, + a.3 * b.1 - a.0 * b.2 + a.1 * b.3 + a.2 * b.0, + a.3 * b.2 + a.0 * b.1 - a.1 * b.0 + a.2 * b.3, + a.3 * b.3 - a.0 * b.0 - a.1 * b.1 - a.2 * b.2, + )); + } + + // https://drafts.csswg.org/css-transforms-2/#interpolation-of-decomposed-3d-matrix-values + // + // Dot product, clamped between -1 and 1. + let cos_half_theta = + (self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3) + .min(1.0) + .max(-1.0); + + if cos_half_theta.abs() == 1.0 { + return Ok(*self); + } + + let half_theta = cos_half_theta.acos(); + let sin_half_theta = (1.0 - cos_half_theta * cos_half_theta).sqrt(); + + let right_weight = (other_weight * half_theta).sin() / sin_half_theta; + // The spec would like to use + // "(other_weight * half_theta).cos() - cos_half_theta * right_weight". However, this + // formula may produce some precision issues of floating-point number calculation, e.g. + // when the progress is 100% (i.e. |other_weight| is 1), the |left_weight| may not be + // perfectly equal to 0. It could be something like -2.22e-16, which is approximately equal + // to zero, in the test. And after we recompose the Matrix3D, these approximated zeros + // make us failed to treat this Matrix3D as a Matrix2D, when serializating it. + // + // Therefore, we use another formula to calculate |left_weight| here. Blink and WebKit also + // use this formula, which is defined in: + // https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/index.htm + // https://github.com/w3c/csswg-drafts/issues/9338 + let left_weight = (this_weight * half_theta).sin() / sin_half_theta; + + Ok(self.scale(left_weight) + other.scale(right_weight)) + } +} + +impl ComputeSquaredDistance for Quaternion { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + // 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 for Matrix3D { + /// Recompose 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 { + // Combine 2 point. + let combine = |a: [f32; 3], b: [f32; 3], ascl: f32, bscl: f32| { + [ + (ascl * a[0]) + (bscl * b[0]), + (ascl * a[1]) + (bscl * b[1]), + (ascl * a[2]) + (bscl * b[2]), + ] + }; + // Dot product. + let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; + // Cross product. + let cross = |row1: [f32; 3], row2: [f32; 3]| { + [ + row1[1] * row2[2] - row1[2] * row2[1], + row1[2] * row2[0] - row1[0] * row2[2], + row1[0] * row2[1] - row1[1] * row2[0], + ] + }; + + if matrix.m44 == 0.0 { + return Err(()); + } + + let scaling_factor = matrix.m44; + + // Normalize the matrix. + matrix.scale_by_factor(1.0 / scaling_factor); + + // perspective_matrix is used to solve for perspective, but it also provides + // an easy way to test for singularity of the upper 3x3 component. + let mut perspective_matrix = matrix; + + perspective_matrix.m14 = 0.0; + perspective_matrix.m24 = 0.0; + perspective_matrix.m34 = 0.0; + perspective_matrix.m44 = 1.0; + + if perspective_matrix.determinant() == 0.0 { + return Err(()); + } + + // First, isolate perspective. + let perspective = if matrix.m14 != 0.0 || matrix.m24 != 0.0 || matrix.m34 != 0.0 { + let right_hand_side: [f32; 4] = [matrix.m14, matrix.m24, matrix.m34, matrix.m44]; + + perspective_matrix = perspective_matrix.inverse().unwrap().transpose(); + let perspective = perspective_matrix.pre_mul_point4(&right_hand_side); + // NOTE(emilio): Even though the reference algorithm clears the + // fourth column here (matrix.m14..matrix.m44), they're not used below + // so it's not really needed. + Perspective( + perspective[0], + perspective[1], + perspective[2], + perspective[3], + ) + } else { + Perspective(0.0, 0.0, 0.0, 1.0) + }; + + // Next take care of translation (easy). + let translate = Translate3D(matrix.m41, matrix.m42, matrix.m43); + + // Now get scale and shear. 'row' is a 3 element array of 3 component vectors + let mut row = matrix.get_matrix_3x3_part(); + + // Compute X scale factor and normalize first row. + let row0len = (row[0][0] * row[0][0] + row[0][1] * row[0][1] + row[0][2] * row[0][2]).sqrt(); + let mut scale = Scale3D(row0len, 0.0, 0.0); + row[0] = [ + row[0][0] / row0len, + row[0][1] / row0len, + row[0][2] / row0len, + ]; + + // Compute XY shear factor and make 2nd row orthogonal to 1st. + let mut skew = Skew(dot(row[0], row[1]), 0.0, 0.0); + row[1] = combine(row[1], row[0], 1.0, -skew.0); + + // Now, compute Y scale and normalize 2nd row. + let row1len = (row[1][0] * row[1][0] + row[1][1] * row[1][1] + row[1][2] * row[1][2]).sqrt(); + scale.1 = row1len; + row[1] = [ + row[1][0] / row1len, + row[1][1] / row1len, + row[1][2] / row1len, + ]; + skew.0 /= scale.1; + + // Compute XZ and YZ shears, orthogonalize 3rd row + skew.1 = dot(row[0], row[2]); + row[2] = combine(row[2], row[0], 1.0, -skew.1); + skew.2 = dot(row[1], row[2]); + row[2] = combine(row[2], row[1], 1.0, -skew.2); + + // Next, get Z scale and normalize 3rd row. + let row2len = (row[2][0] * row[2][0] + row[2][1] * row[2][1] + row[2][2] * row[2][2]).sqrt(); + scale.2 = row2len; + row[2] = [ + row[2][0] / row2len, + row[2][1] / row2len, + row[2][2] / row2len, + ]; + skew.1 /= scale.2; + skew.2 /= scale.2; + + // At this point, the matrix (in rows) is orthonormal. + // Check for a coordinate system flip. If the determinant + // is -1, then negate the matrix and the scaling factors. + if dot(row[0], cross(row[1], row[2])) < 0.0 { + scale.negate(); + for i in 0..3 { + row[i][0] *= -1.0; + row[i][1] *= -1.0; + row[i][2] *= -1.0; + } + } + + // Now, get the rotations out. + let mut quaternion = Quaternion( + 0.5 * ((1.0 + row[0][0] - row[1][1] - row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 - row[0][0] + row[1][1] - row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 - row[0][0] - row[1][1] + row[2][2]).max(0.0) as f64).sqrt(), + 0.5 * ((1.0 + row[0][0] + row[1][1] + row[2][2]).max(0.0) as f64).sqrt(), + ); + + if row[2][1] > row[1][2] { + quaternion.0 = -quaternion.0 + } + if row[0][2] > row[2][0] { + quaternion.1 = -quaternion.1 + } + if row[1][0] > row[0][1] { + quaternion.2 = -quaternion.2 + } + + Ok(MatrixDecomposed3D { + translate, + scale, + skew, + perspective, + quaternion, + }) +} + +/** + * The relevant section of the transitions specification: + * https://drafts.csswg.org/web-animations-1/#animation-types + * http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types- + * defers all of the details to the 2-D and 3-D transforms specifications. + * For the 2-D transforms specification (all that's relevant for us, right + * now), the relevant section is: + * https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms + * This, in turn, refers to the unmatrix program in Graphics Gems, + * available from http://graphicsgems.org/ , and in + * particular as the file GraphicsGems/gemsii/unmatrix.c + * in http://graphicsgems.org/AllGems.tar.gz + * + * The unmatrix reference is for general 3-D transform matrices (any of the + * 16 components can have any value). + * + * For CSS 2-D transforms, we have a 2-D matrix with the bottom row constant: + * + * [ A C E ] + * [ B D F ] + * [ 0 0 1 ] + * + * For that case, I believe the algorithm in unmatrix reduces to: + * + * (1) If A * D - B * C == 0, the matrix is singular. Fail. + * + * (2) Set translation components (Tx and Ty) to the translation parts of + * the matrix (E and F) and then ignore them for the rest of the time. + * (For us, E and F each actually consist of three constants: a + * length, a multiplier for the width, and a multiplier for the + * height. This actually requires its own decomposition, but I'll + * keep that separate.) + * + * (3) Let the X scale (Sx) be sqrt(A^2 + B^2). Then divide both A and B + * by it. + * + * (4) Let the XY shear (K) be A * C + B * D. From C, subtract A times + * the XY shear. From D, subtract B times the XY shear. + * + * (5) Let the Y scale (Sy) be sqrt(C^2 + D^2). Divide C, D, and the XY + * shear (K) by it. + * + * (6) At this point, A * D - B * C is either 1 or -1. If it is -1, + * negate the XY shear (K), the X scale (Sx), and A, B, C, and D. + * (Alternatively, we could negate the XY shear (K) and the Y scale + * (Sy).) + * + * (7) Let the rotation be R = atan2(B, A). + * + * Then the resulting decomposed transformation is: + * + * translate(Tx, Ty) rotate(R) skewX(atan(K)) scale(Sx, Sy) + * + * An interesting result of this is that all of the simple transform + * functions (i.e., all functions other than matrix()), in isolation, + * decompose back to themselves except for: + * 'skewY(φ)', which is 'matrix(1, tan(φ), 0, 1, 0, 0)', which decomposes + * to 'rotate(φ) skewX(φ) scale(sec(φ), cos(φ))' since (ignoring the + * alternate sign possibilities that would get fixed in step 6): + * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) = + * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) = + * sin(φ). In step 4, the XY shear is sin(φ). Thus, after step 4, C = + * -cos(φ)sin(φ) and D = 1 - sin²(φ) = cos²(φ). Thus, in step 5, the Y scale is + * sqrt(cos²(φ)(sin²(φ) + cos²(φ)) = cos(φ). Thus, after step 5, C = -sin(φ), D + * = cos(φ), and the XY shear is tan(φ). Thus, in step 6, A * D - B * C = + * cos²(φ) + sin²(φ) = 1. In step 7, the rotation is thus φ. + * + * skew(θ, φ), which is matrix(1, tan(φ), tan(θ), 1, 0, 0), which decomposes + * to 'rotate(φ) skewX(θ + φ) scale(sec(φ), cos(φ))' since (ignoring + * the alternate sign possibilities that would get fixed in step 6): + * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) = + * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) = + * sin(φ). In step 4, the XY shear is cos(φ)tan(θ) + sin(φ). Thus, after step 4, + * C = tan(θ) - cos(φ)(cos(φ)tan(θ) + sin(φ)) = tan(θ)sin²(φ) - cos(φ)sin(φ) + * D = 1 - sin(φ)(cos(φ)tan(θ) + sin(φ)) = cos²(φ) - sin(φ)cos(φ)tan(θ) + * Thus, in step 5, the Y scale is sqrt(C² + D²) = + * sqrt(tan²(θ)(sin⁴(φ) + sin²(φ)cos²(φ)) - + * 2 tan(θ)(sin³(φ)cos(φ) + sin(φ)cos³(φ)) + + * (sin²(φ)cos²(φ) + cos⁴(φ))) = + * sqrt(tan²(θ)sin²(φ) - 2 tan(θ)sin(φ)cos(φ) + cos²(φ)) = + * cos(φ) - tan(θ)sin(φ) (taking the negative of the obvious solution so + * we avoid flipping in step 6). + * After step 5, C = -sin(φ) and D = cos(φ), and the XY shear is + * (cos(φ)tan(θ) + sin(φ)) / (cos(φ) - tan(θ)sin(φ)) = + * (dividing both numerator and denominator by cos(φ)) + * (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)) = tan(θ + φ). + * (See http://en.wikipedia.org/wiki/List_of_trigonometric_identities .) + * Thus, in step 6, A * D - B * C = cos²(φ) + sin²(φ) = 1. + * In step 7, the rotation is thus φ. + * + * To check this result, we can multiply things back together: + * + * [ cos(φ) -sin(φ) ] [ 1 tan(θ + φ) ] [ sec(φ) 0 ] + * [ sin(φ) cos(φ) ] [ 0 1 ] [ 0 cos(φ) ] + * + * [ cos(φ) cos(φ)tan(θ + φ) - sin(φ) ] [ sec(φ) 0 ] + * [ sin(φ) sin(φ)tan(θ + φ) + cos(φ) ] [ 0 cos(φ) ] + * + * but since tan(θ + φ) = (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)), + * cos(φ)tan(θ + φ) - sin(φ) + * = cos(φ)(tan(θ) + tan(φ)) - sin(φ) + sin(φ)tan(θ)tan(φ) + * = cos(φ)tan(θ) + sin(φ) - sin(φ) + sin(φ)tan(θ)tan(φ) + * = cos(φ)tan(θ) + sin(φ)tan(θ)tan(φ) + * = tan(θ) (cos(φ) + sin(φ)tan(φ)) + * = tan(θ) sec(φ) (cos²(φ) + sin²(φ)) + * = tan(θ) sec(φ) + * and + * sin(φ)tan(θ + φ) + cos(φ) + * = sin(φ)(tan(θ) + tan(φ)) + cos(φ) - cos(φ)tan(θ)tan(φ) + * = tan(θ) (sin(φ) - sin(φ)) + sin(φ)tan(φ) + cos(φ) + * = sec(φ) (sin²(φ) + cos²(φ)) + * = sec(φ) + * so the above is: + * [ cos(φ) tan(θ) sec(φ) ] [ sec(φ) 0 ] + * [ sin(φ) sec(φ) ] [ 0 cos(φ) ] + * + * [ 1 tan(θ) ] + * [ tan(φ) 1 ] + */ + +/// Decompose a 2D matrix for Gecko. This implements the above decomposition algorithm. +#[cfg(feature = "gecko")] +fn decompose_2d_matrix(matrix: &Matrix3D) -> Result { + // 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 { + 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 { + let (from, to) = if self.is_3d() || other.is_3d() { + (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?) + } else { + (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?) + }; + // Matrices can be undecomposable due to couple reasons, e.g., + // non-invertible matrices. In this case, we should report Err here, + // and let the caller do the fallback procedure. + Ok(Matrix3D::from(from.animate(&to, procedure)?)) + } +} + +impl ComputeSquaredDistance for Matrix3D { + #[inline] + #[cfg(feature = "servo")] + fn compute_squared_distance(&self, other: &Self) -> Result { + 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 { + 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, + } +} + +/// +impl Animate for ComputedTransform { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + 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::, _>>()?; + + // Deal with the remainders + let this_remainder = if this.len() > result.len() { + Some(&this[result.len()..]) + } else { + None + }; + let other_remainder = if other.len() > result.len() { + Some(&other[result.len()..]) + } else { + None + }; + + match (this_remainder, other_remainder) { + // If there is a remainder from *both* lists we must have had mismatched functions. + // => Add the remainders to a suitable ___Matrix function. + (Some(this_remainder), Some(other_remainder)) => { + result.push(TransformOperation::animate_mismatched_transforms( + this_remainder, + other_remainder, + procedure, + )?); + }, + // If there is a remainder from just one list, then one list must be shorter but + // completely match the type of the corresponding functions in the longer list. + // => Interpolate the remainder with identity transforms. + (Some(remainder), None) | (None, Some(remainder)) => { + let fill_right = this_remainder.is_some(); + result.append( + &mut remainder + .iter() + .map(|transform| { + let identity = transform.to_animated_zero().unwrap(); + + match transform { + TransformOperation::AccumulateMatrix { .. } | + TransformOperation::InterpolateMatrix { .. } => { + let (from, to) = if fill_right { + (transform, &identity) + } else { + (&identity, transform) + }; + + TransformOperation::animate_mismatched_transforms( + &[from.clone()], + &[to.clone()], + procedure, + ) + }, + _ => { + let (lhs, rhs) = if fill_right { + (transform, &identity) + } else { + (&identity, transform) + }; + lhs.animate(rhs, procedure) + }, + } + }) + .collect::, _>>()?, + ); + }, + (None, None) => {}, + } + + Ok(Transform(result.into())) + } +} + +impl ComputeSquaredDistance for ComputedTransform { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result { + let squared_dist = super::lists::with_zero::squared_distance(&self.0, &other.0); + + // Roll back to matrix interpolation if there is any Err(()) in the + // transform lists, such as mismatched transform functions. + // + // FIXME: Using a zero size here seems a bit sketchy but matches the + // previous behavior. + if squared_dist.is_err() { + let rect = euclid::Rect::zero(); + let matrix1: Matrix3D = self.to_transform_3d_matrix(Some(&rect))?.0.into(); + let matrix2: Matrix3D = other.to_transform_3d_matrix(Some(&rect))?.0.into(); + return matrix1.compute_squared_distance(&matrix2); + } + + squared_dist + } +} + +/// +impl Animate for ComputedTransformOperation { + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + match (self, other) { + (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => { + Ok(TransformOperation::Matrix3D( + this.animate(other, procedure)?, + )) + }, + (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => { + Ok(TransformOperation::Matrix(this.animate(other, procedure)?)) + }, + ( + &TransformOperation::Skew(ref fx, ref fy), + &TransformOperation::Skew(ref tx, ref ty), + ) => Ok(TransformOperation::Skew( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + )), + (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) => { + Ok(TransformOperation::SkewX(f.animate(t, procedure)?)) + }, + (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => { + Ok(TransformOperation::SkewY(f.animate(t, procedure)?)) + }, + ( + &TransformOperation::Translate3D(ref fx, ref fy, ref fz), + &TransformOperation::Translate3D(ref tx, ref ty, ref tz), + ) => Ok(TransformOperation::Translate3D( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + fz.animate(tz, procedure)?, + )), + ( + &TransformOperation::Translate(ref fx, ref fy), + &TransformOperation::Translate(ref tx, ref ty), + ) => Ok(TransformOperation::Translate( + fx.animate(tx, procedure)?, + fy.animate(ty, procedure)?, + )), + (&TransformOperation::TranslateX(ref f), &TransformOperation::TranslateX(ref t)) => { + Ok(TransformOperation::TranslateX(f.animate(t, procedure)?)) + }, + (&TransformOperation::TranslateY(ref f), &TransformOperation::TranslateY(ref t)) => { + Ok(TransformOperation::TranslateY(f.animate(t, procedure)?)) + }, + (&TransformOperation::TranslateZ(ref f), &TransformOperation::TranslateZ(ref t)) => { + Ok(TransformOperation::TranslateZ(f.animate(t, procedure)?)) + }, + ( + &TransformOperation::Scale3D(ref fx, ref fy, ref fz), + &TransformOperation::Scale3D(ref tx, ref ty, ref tz), + ) => Ok(TransformOperation::Scale3D( + animate_multiplicative_factor(*fx, *tx, procedure)?, + animate_multiplicative_factor(*fy, *ty, procedure)?, + animate_multiplicative_factor(*fz, *tz, procedure)?, + )), + (&TransformOperation::ScaleX(ref f), &TransformOperation::ScaleX(ref t)) => Ok( + TransformOperation::ScaleX(animate_multiplicative_factor(*f, *t, procedure)?), + ), + (&TransformOperation::ScaleY(ref f), &TransformOperation::ScaleY(ref t)) => Ok( + TransformOperation::ScaleY(animate_multiplicative_factor(*f, *t, procedure)?), + ), + (&TransformOperation::ScaleZ(ref f), &TransformOperation::ScaleZ(ref t)) => Ok( + TransformOperation::ScaleZ(animate_multiplicative_factor(*f, *t, procedure)?), + ), + ( + &TransformOperation::Scale(ref fx, ref fy), + &TransformOperation::Scale(ref tx, ref ty), + ) => Ok(TransformOperation::Scale( + animate_multiplicative_factor(*fx, *tx, procedure)?, + animate_multiplicative_factor(*fy, *ty, procedure)?, + )), + ( + &TransformOperation::Rotate3D(fx, fy, fz, fa), + &TransformOperation::Rotate3D(tx, ty, tz, ta), + ) => { + let animated = Rotate::Rotate3D(fx, fy, fz, fa) + .animate(&Rotate::Rotate3D(tx, ty, tz, ta), procedure)?; + let (fx, fy, fz, fa) = ComputedRotate::resolve(&animated); + Ok(TransformOperation::Rotate3D(fx, fy, fz, fa)) + }, + (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) => { + Ok(TransformOperation::RotateX(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) => { + Ok(TransformOperation::RotateY(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) => { + Ok(TransformOperation::RotateZ(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::Rotate(fa), &TransformOperation::RotateZ(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + (&TransformOperation::RotateZ(fa), &TransformOperation::Rotate(ta)) => { + Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?)) + }, + ( + &TransformOperation::Perspective(ref fd), + &TransformOperation::Perspective(ref td), + ) => { + use crate::values::computed::CSSPixelLength; + use crate::values::generics::transform::create_perspective_matrix; + + // From https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions: + // + // The transform functions matrix(), matrix3d() and + // perspective() get converted into 4x4 matrices first and + // interpolated as defined in section Interpolation of + // Matrices afterwards. + // + let from = create_perspective_matrix(fd.infinity_or(|l| l.px())); + let to = create_perspective_matrix(td.infinity_or(|l| l.px())); + + let interpolated = Matrix3D::from(from).animate(&Matrix3D::from(to), procedure)?; + + let decomposed = decompose_3d_matrix(interpolated)?; + let perspective_z = decomposed.perspective.2; + // Clamp results outside of the -1 to 0 range so that we get perspective + // function values between 1 and infinity. + let used_value = if perspective_z >= 0. { + transform::PerspectiveFunction::None + } else { + transform::PerspectiveFunction::Length(CSSPixelLength::new( + if perspective_z <= -1. { + 1. + } else { + -1. / perspective_z + }, + )) + }; + Ok(TransformOperation::Perspective(used_value)) + }, + _ if self.is_translate() && other.is_translate() => self + .to_translate_3d() + .animate(&other.to_translate_3d(), procedure), + _ if self.is_scale() && other.is_scale() => { + self.to_scale_3d().animate(&other.to_scale_3d(), procedure) + }, + _ if self.is_rotate() && other.is_rotate() => self + .to_rotate_3d() + .animate(&other.to_rotate_3d(), procedure), + _ => Err(()), + } + } +} + +impl ComputedTransformOperation { + /// If there are no size dependencies, we try to animate in-place, to avoid + /// creating deeply nested Interpolate* operations. + fn try_animate_mismatched_transforms_in_place( + left: &[Self], + right: &[Self], + procedure: Procedure, + ) -> Result { + let (left, _left_3d) = Transform::components_to_transform_3d_matrix(left, None)?; + let (right, _right_3d) = Transform::components_to_transform_3d_matrix(right, None)?; + Ok(Self::Matrix3D( + Matrix3D::from(left).animate(&Matrix3D::from(right), procedure)?, + )) + } + + fn animate_mismatched_transforms( + left: &[Self], + right: &[Self], + procedure: Procedure, + ) -> Result { + if let Ok(op) = Self::try_animate_mismatched_transforms_in_place(left, right, procedure) { + return Ok(op); + } + let from_list = Transform(left.to_vec().into()); + let to_list = Transform(right.to_vec().into()); + Ok(match procedure { + Procedure::Add => { + debug_assert!(false, "Addition should've been handled earlier"); + return Err(()); + }, + Procedure::Interpolate { progress } => Self::InterpolateMatrix { + from_list, + to_list, + progress: Percentage(progress as f32), + }, + Procedure::Accumulate { count } => Self::AccumulateMatrix { + from_list, + to_list, + count: cmp::min(count, i32::max_value() as u64) as i32, + }, + }) + } +} + +// This might not be the most useful definition of distance. It might be better, for example, +// to trace the distance travelled by a point as its transform is interpolated between the two +// lists. That, however, proves to be quite complicated so we take a simple approach for now. +// See https://bugzilla.mozilla.org/show_bug.cgi?id=1318591#c0. +impl ComputeSquaredDistance for ComputedTransformOperation { + fn compute_squared_distance(&self, other: &Self) -> Result { + match (self, other) { + (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => { + this.compute_squared_distance(other) + }, + (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => { + let this: Matrix3D = (*this).into(); + let other: Matrix3D = (*other).into(); + this.compute_squared_distance(&other) + }, + ( + &TransformOperation::Skew(ref fx, ref fy), + &TransformOperation::Skew(ref tx, ref ty), + ) => Ok(fx.compute_squared_distance(&tx)? + fy.compute_squared_distance(&ty)?), + (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) | + (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => { + f.compute_squared_distance(&t) + }, + ( + &TransformOperation::Translate3D(ref fx, ref fy, ref fz), + &TransformOperation::Translate3D(ref tx, ref ty, ref tz), + ) => { + // For translate, We don't want to require doing layout in order + // to calculate the result, so drop the percentage part. + // + // However, dropping percentage makes us impossible to compute + // the distance for the percentage-percentage case, but Gecko + // uses the same formula, so it's fine for now. + let basis = Length::new(0.); + let fx = fx.resolve(basis).px(); + let fy = fy.resolve(basis).px(); + let tx = tx.resolve(basis).px(); + let ty = ty.resolve(basis).px(); + + Ok(fx.compute_squared_distance(&tx)? + + fy.compute_squared_distance(&ty)? + + fz.compute_squared_distance(&tz)?) + }, + ( + &TransformOperation::Scale3D(ref fx, ref fy, ref fz), + &TransformOperation::Scale3D(ref tx, ref ty, ref tz), + ) => Ok(fx.compute_squared_distance(&tx)? + + fy.compute_squared_distance(&ty)? + + fz.compute_squared_distance(&tz)?), + ( + &TransformOperation::Rotate3D(fx, fy, fz, fa), + &TransformOperation::Rotate3D(tx, ty, tz, ta), + ) => Rotate::Rotate3D(fx, fy, fz, fa) + .compute_squared_distance(&Rotate::Rotate3D(tx, ty, tz, ta)), + (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) | + (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) | + (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) | + (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => { + fa.compute_squared_distance(&ta) + }, + ( + &TransformOperation::Perspective(ref fd), + &TransformOperation::Perspective(ref td), + ) => fd + .infinity_or(|l| l.px()) + .compute_squared_distance(&td.infinity_or(|l| l.px())), + (&TransformOperation::Perspective(ref p), &TransformOperation::Matrix3D(ref m)) | + (&TransformOperation::Matrix3D(ref m), &TransformOperation::Perspective(ref p)) => { + // FIXME(emilio): Is this right? Why interpolating this with + // Perspective but not with anything else? + let mut p_matrix = Matrix3D::identity(); + let p = p.infinity_or(|p| p.px()); + if p >= 0. { + p_matrix.m34 = -1. / p.max(1.); + } + p_matrix.compute_squared_distance(&m) + }, + // Gecko cross-interpolates amongst all translate and all scale + // functions (See ToPrimitive in layout/style/StyleAnimationValue.cpp) + // without falling back to InterpolateMatrix + _ if self.is_translate() && other.is_translate() => self + .to_translate_3d() + .compute_squared_distance(&other.to_translate_3d()), + _ if self.is_scale() && other.is_scale() => self + .to_scale_3d() + .compute_squared_distance(&other.to_scale_3d()), + _ if self.is_rotate() && other.is_rotate() => self + .to_rotate_3d() + .compute_squared_distance(&other.to_rotate_3d()), + _ => Err(()), + } + } +} + +// ------------------------------------ +// Individual transforms. +// ------------------------------------ +/// +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 { + use euclid::approxeq::ApproxEq; + match (self, other) { + (&Rotate::None, &Rotate::None) => Ok(Rotate::None), + (&Rotate::Rotate3D(fx, fy, fz, fa), &Rotate::None) => { + // We always normalize direction vector for rotate3d() first, so we should also + // apply the same rule for rotate property. In other words, we promote none into + // a 3d rotate, and normalize both direction vector first, and then do + // interpolation. + let (fx, fy, fz, fa) = transform::get_normalized_vector_and_angle(fx, fy, fz, fa); + Ok(Rotate::Rotate3D( + fx, + fy, + fz, + fa.animate(&Angle::zero(), procedure)?, + )) + }, + (&Rotate::None, &Rotate::Rotate3D(tx, ty, tz, ta)) => { + // Normalize direction vector first. + let (tx, ty, tz, ta) = transform::get_normalized_vector_and_angle(tx, ty, tz, ta); + Ok(Rotate::Rotate3D( + tx, + ty, + tz, + Angle::zero().animate(&ta, procedure)?, + )) + }, + (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => { + // https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions + + let (from, to) = (self.resolve(), other.resolve()); + // For interpolations with the primitive rotate3d(), the direction vectors of the + // transform functions get normalized first. + let (fx, fy, fz, fa) = + transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); + let (tx, ty, tz, ta) = + transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3); + + // The rotation angle gets interpolated numerically and the rotation vector of the + // non-zero angle is used or (0, 0, 1) if both angles are zero. + // + // Note: the normalization may get two different vectors because of the + // floating-point precision, so we have to use approx_eq to compare two + // vectors. + let fv = DirectionVector::new(fx, fy, fz); + let tv = DirectionVector::new(tx, ty, tz); + if fa.is_zero() || ta.is_zero() || fv.approx_eq(&tv) { + let (x, y, z) = if fa.is_zero() && ta.is_zero() { + (0., 0., 1.) + } else if fa.is_zero() { + (tx, ty, tz) + } else { + // ta.is_zero() or both vectors are equal. + (fx, fy, fz) + }; + return Ok(Rotate::Rotate3D(x, y, z, fa.animate(&ta, procedure)?)); + } + + // Slerp algorithm doesn't work well for Procedure::Add, which makes both + // |this_weight| and |other_weight| be 1.0, and this may make the cosine value of + // the angle be out of the range (i.e. the 4th component of the quaternion vector). + // (See Quaternion::animate() for more details about the Slerp formula.) + // Therefore, if the cosine value is out of range, we get an NaN after applying + // acos() on it, and so the result is invalid. + // Note: This is specialized for `rotate` property. The addition of `transform` + // property has been handled in `ComputedTransform::animate()` by merging two list + // directly. + let rq = if procedure == Procedure::Add { + // In Transform::animate(), it converts two rotations into transform matrices, + // and do matrix multiplication. This match the spec definition for the + // addition. + // https://drafts.csswg.org/css-transforms-2/#combining-transform-lists + let f = ComputedTransformOperation::Rotate3D(fx, fy, fz, fa); + let t = ComputedTransformOperation::Rotate3D(tx, ty, tz, ta); + let v = + Transform(vec![f].into()).animate(&Transform(vec![t].into()), procedure)?; + let (m, _) = v.to_transform_3d_matrix(None)?; + // Decompose the matrix and retrive the quaternion vector. + decompose_3d_matrix(Matrix3D::from(m))?.quaternion + } else { + // If the normalized vectors are not equal and both rotation angles are + // non-zero the transform functions get converted into 4x4 matrices first and + // interpolated as defined in section Interpolation of Matrices afterwards. + // However, per the spec issue [1], we prefer to converting the rotate3D into + // quaternion vectors directly, and then apply Slerp algorithm. + // + // Both ways should be identical, and converting rotate3D into quaternion + // vectors directly can avoid redundant math operations, e.g. the generation of + // the equivalent matrix3D and the unnecessary decomposition parts of + // translation, scale, skew, and persepctive in the matrix3D. + // + // [1] https://github.com/w3c/csswg-drafts/issues/9278 + let fq = Quaternion::from_direction_and_angle(&fv, fa.radians64()); + let tq = Quaternion::from_direction_and_angle(&tv, ta.radians64()); + Quaternion::animate(&fq, &tq, procedure)? + }; + + debug_assert!(rq.3 <= 1.0 && rq.3 >= -1.0, "Invalid cosine value"); + let (x, y, z, angle) = transform::get_normalized_vector_and_angle( + rq.0 as f32, + rq.1 as f32, + rq.2 as f32, + rq.3.acos() as f32 * 2.0, + ); + + Ok(Rotate::Rotate3D(x, y, z, Angle::from_radians(angle))) + }, + (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => { + // If this is a 2D rotation, we just animate the + 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 { + use euclid::approxeq::ApproxEq; + match (self, other) { + (&Rotate::None, &Rotate::None) => Ok(SquaredDistance::from_sqrt(0.)), + (&Rotate::Rotate3D(_, _, _, a), &Rotate::None) | + (&Rotate::None, &Rotate::Rotate3D(_, _, _, a)) => { + a.compute_squared_distance(&Angle::zero()) + }, + (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => { + let (from, to) = (self.resolve(), other.resolve()); + let (mut fx, mut fy, mut fz, angle1) = + transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3); + let (mut tx, mut ty, mut tz, angle2) = + transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3); + + if angle1.is_zero() && angle2.is_zero() { + (fx, fy, fz) = (0., 0., 1.); + (tx, ty, tz) = (0., 0., 1.); + } else if angle1.is_zero() { + (fx, fy, fz) = (tx, ty, tz); + } else if angle2.is_zero() { + (tx, ty, tz) = (fx, fy, fz); + } + + let v1 = DirectionVector::new(fx, fy, fz); + let v2 = DirectionVector::new(tx, ty, tz); + if v1.approx_eq(&v2) { + angle1.compute_squared_distance(&angle2) + } else { + let q1 = Quaternion::from_direction_and_angle(&v1, angle1.radians64()); + let q2 = Quaternion::from_direction_and_angle(&v2, angle2.radians64()); + q1.compute_squared_distance(&q2) + } + }, + (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => self + .resolve() + .3 + .compute_squared_distance(&other.resolve().3), + } + } +} + +/// +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 { + 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 { + 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)?) + } +} + +/// +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 { + 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 { + 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; + + /// + 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(&self, dest: &mut CssWriter) -> 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 { + // Use the formula for calculating the distance between angles defined in SVG: + // https://www.w3.org/TR/SVG/animate.html#complexDistances + self.radians64() + .compute_squared_distance(&other.radians64()) + } +} diff --git a/servo/components/style/values/computed/animation.rs b/servo/components/style/values/computed/animation.rs new file mode 100644 index 0000000000..626dbe5347 --- /dev/null +++ b/servo/components/style/values/computed/animation.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for properties related to animations and transitions + +use crate::values::computed::{Context, LengthPercentage, ToComputedValue}; +use crate::values::generics::animation as generics; +use crate::values::specified::animation as specified; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::animation::{ + AnimationName, ScrollAxis, ScrollTimelineName, TransitionProperty, AnimationComposition, + AnimationDirection, AnimationFillMode, AnimationPlayState, +}; + +/// A computed value for the `animation-iteration-count` property. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)] +#[repr(C)] +pub struct AnimationIterationCount(pub f32); + +impl ToComputedValue for specified::AnimationIterationCount { + type ComputedValue = AnimationIterationCount; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + AnimationIterationCount(match *self { + specified::AnimationIterationCount::Number(n) => n.to_computed_value(context).0, + specified::AnimationIterationCount::Infinite => f32::INFINITY, + }) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + use crate::values::specified::NonNegativeNumber; + if computed.0.is_infinite() { + specified::AnimationIterationCount::Infinite + } else { + specified::AnimationIterationCount::Number(NonNegativeNumber::new(computed.0)) + } + } +} + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + Self(1.0) + } +} + +impl ToCss for AnimationIterationCount { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + if self.0.is_infinite() { + dest.write_str("infinite") + } else { + self.0.to_css(dest) + } + } +} + +/// A computed value for the `animation-timeline` property. +pub type AnimationTimeline = generics::GenericAnimationTimeline; + +/// A computed value for the `view-timeline-inset` property. +pub type ViewTimelineInset = generics::GenericViewTimelineInset; 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; diff --git a/servo/components/style/values/computed/basic_shape.rs b/servo/components/style/values/computed/basic_shape.rs new file mode 100644 index 0000000000..d39110ec1c --- /dev/null +++ b/servo/components/style/values/computed/basic_shape.rs @@ -0,0 +1,37 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [`basic-shape`][basic-shape]s +//! +//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape + +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Image, LengthPercentage, NonNegativeLengthPercentage, Position}; +use crate::values::generics::basic_shape as generic; + +/// A computed alias for FillRule. +pub use crate::values::generics::basic_shape::FillRule; + +/// A computed `clip-path` value. +pub type ClipPath = generic::GenericClipPath; + +/// A computed `shape-outside` value. +pub type ShapeOutside = generic::GenericShapeOutside; + +/// A computed basic shape. +pub type BasicShape = + generic::GenericBasicShape; + +/// The computed value of `inset()`. +pub type InsetRect = generic::GenericInsetRect; + +/// A computed circle. +pub type Circle = generic::Circle; + +/// A computed ellipse. +pub type Ellipse = generic::Ellipse; + +/// The computed value of `ShapeRadius`. +pub type ShapeRadius = generic::GenericShapeRadius; diff --git a/servo/components/style/values/computed/border.rs b/servo/components/style/values/computed/border.rs new file mode 100644 index 0000000000..e073f671b3 --- /dev/null +++ b/servo/components/style/values/computed/border.rs @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to borders. + +use crate::values::computed::length::{NonNegativeLength, NonNegativeLengthPercentage}; +use crate::values::computed::{NonNegativeNumber, NonNegativeNumberOrPercentage}; +use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius; +use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice; +use crate::values::generics::border::BorderRadius as GenericBorderRadius; +use crate::values::generics::border::BorderSpacing as GenericBorderSpacing; +use crate::values::generics::border::GenericBorderImageSideWidth; +use crate::values::generics::rect::Rect; +use crate::values::generics::size::Size2D; +use crate::values::generics::NonNegative; +use crate::Zero; +use app_units::Au; + +pub use crate::values::specified::border::BorderImageRepeat; + +/// A computed value for -webkit-text-stroke-width. +pub type LineWidth = Au; + +/// A computed value for border-width (and the like). +pub type BorderSideWidth = Au; + +/// A computed value for the `border-image-width` property. +pub type BorderImageWidth = Rect; + +/// A computed value for a single side of a `border-image-width` property. +pub type BorderImageSideWidth = + GenericBorderImageSideWidth; + +/// A computed value for the `border-image-slice` property. +pub type BorderImageSlice = GenericBorderImageSlice; + +/// A computed value for the `border-radius` property. +pub type BorderRadius = GenericBorderRadius; + +/// A computed value for the `border-*-radius` longhand properties. +pub type BorderCornerRadius = GenericBorderCornerRadius; + +/// A computed value for the `border-spacing` longhand property. +pub type BorderSpacing = GenericBorderSpacing; + +impl BorderImageSideWidth { + /// Returns `1`. + #[inline] + pub fn one() -> Self { + GenericBorderImageSideWidth::Number(NonNegative(1.)) + } +} + +impl BorderImageSlice { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + GenericBorderImageSlice { + offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()), + fill: false, + } + } +} + +impl BorderSpacing { + /// Returns `0 0`. + pub fn zero() -> Self { + GenericBorderSpacing(Size2D::new( + NonNegativeLength::zero(), + NonNegativeLength::zero(), + )) + } + + /// Returns the horizontal spacing. + pub fn horizontal(&self) -> Au { + Au::from(*self.0.width()) + } + + /// Returns the vertical spacing. + pub fn vertical(&self) -> Au { + Au::from(*self.0.height()) + } +} diff --git a/servo/components/style/values/computed/box.rs b/servo/components/style/values/computed/box.rs new file mode 100644 index 0000000000..62811d9851 --- /dev/null +++ b/servo/components/style/values/computed/box.rs @@ -0,0 +1,388 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for box properties. + +use crate::values::animated::{Animate, Procedure, ToAnimatedValue}; +use crate::values::computed::font::FixedPoint; +use crate::values::computed::length::{LengthPercentage, NonNegativeLength}; +use crate::values::computed::{Context, Integer, Number, ToComputedValue}; +use crate::values::generics::box_::{ + GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign, +}; +use crate::values::specified::box_ as specified; +use std::fmt; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::box_::{ + Appearance, BaselineSource, BreakBetween, BreakWithin, Clear as SpecifiedClear, Contain, + ContainerName, ContainerType, ContentVisibility, Display, Float as SpecifiedFloat, Overflow, + OverflowAnchor, OverflowClipBox, OverscrollBehavior, ScrollSnapAlign, ScrollSnapAxis, + ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, WillChange, +}; + +/// A computed value for the `vertical-align` property. +pub type VerticalAlign = GenericVerticalAlign; + +/// A computed value for the `contain-intrinsic-size` property. +pub type ContainIntrinsicSize = GenericContainIntrinsicSize; + +impl ContainIntrinsicSize { + /// Converts contain-intrinsic-size to auto style. + pub fn add_auto_if_needed(&self) -> Option { + Some(match *self { + Self::None => Self::AutoNone, + Self::Length(ref l) => Self::AutoLength(*l), + Self::AutoNone | Self::AutoLength(..) => return None, + }) + } +} + +/// A computed value for the `line-clamp` property. +pub type LineClamp = GenericLineClamp; + +impl Animate for LineClamp { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + if self.is_none() != other.is_none() { + return Err(()); + } + if self.is_none() { + return Ok(Self::none()); + } + Ok(Self(self.0.animate(&other.0, procedure)?.max(1))) + } +} + +/// A computed value for the `perspective` property. +pub type Perspective = GenericPerspective; + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToResolvedValue, +)] +#[repr(u8)] +/// A computed value for the `float` property. +pub enum Float { + Left, + Right, + None, +} + +impl Float { + /// Returns true if `self` is not `None`. + pub fn is_floating(self) -> bool { + self != Self::None + } +} + +impl ToComputedValue for SpecifiedFloat { + type ComputedValue = Float; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let ltr = context.style().writing_mode.is_bidi_ltr(); + // https://drafts.csswg.org/css-logical-props/#float-clear + match *self { + SpecifiedFloat::InlineStart => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Float::Left + } else { + Float::Right + } + }, + SpecifiedFloat::InlineEnd => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Float::Right + } else { + Float::Left + } + }, + SpecifiedFloat::Left => Float::Left, + SpecifiedFloat::Right => Float::Right, + SpecifiedFloat::None => Float::None, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedFloat { + match *computed { + Float::Left => SpecifiedFloat::Left, + Float::Right => SpecifiedFloat::Right, + Float::None => SpecifiedFloat::None, + } + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToResolvedValue, +)] +/// A computed value for the `clear` property. +#[repr(u8)] +pub enum Clear { + None, + Left, + Right, + Both, +} + +impl ToComputedValue for SpecifiedClear { + type ComputedValue = Clear; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let ltr = context.style().writing_mode.is_bidi_ltr(); + // https://drafts.csswg.org/css-logical-props/#float-clear + match *self { + SpecifiedClear::InlineStart => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Clear::Left + } else { + Clear::Right + } + }, + SpecifiedClear::InlineEnd => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if ltr { + Clear::Right + } else { + Clear::Left + } + }, + SpecifiedClear::None => Clear::None, + SpecifiedClear::Left => Clear::Left, + SpecifiedClear::Right => Clear::Right, + SpecifiedClear::Both => Clear::Both, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedClear { + match *computed { + Clear::None => SpecifiedClear::None, + Clear::Left => SpecifiedClear::Left, + Clear::Right => SpecifiedClear::Right, + Clear::Both => SpecifiedClear::Both, + } + } +} + +/// A computed value for the `resize` property. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, ToCss, ToResolvedValue)] +#[repr(u8)] +pub enum Resize { + None, + Both, + Horizontal, + Vertical, +} + +impl ToComputedValue for specified::Resize { + type ComputedValue = Resize; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Resize { + let is_vertical = context.style().writing_mode.is_vertical(); + match self { + specified::Resize::Inline => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if is_vertical { + Resize::Vertical + } else { + Resize::Horizontal + } + }, + specified::Resize::Block => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if is_vertical { + Resize::Horizontal + } else { + Resize::Vertical + } + }, + specified::Resize::None => Resize::None, + specified::Resize::Both => Resize::Both, + specified::Resize::Horizontal => Resize::Horizontal, + specified::Resize::Vertical => Resize::Vertical, + } + } + + #[inline] + fn from_computed_value(computed: &Resize) -> specified::Resize { + match computed { + Resize::None => specified::Resize::None, + Resize::Both => specified::Resize::Both, + Resize::Horizontal => specified::Resize::Horizontal, + Resize::Vertical => specified::Resize::Vertical, + } + } +} + +/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375). +pub const ZOOM_FRACTION_BITS: u16 = 6; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference workaround. +pub type ZoomFixedPoint = FixedPoint; + +/// The computed `zoom` property value. We store it as a 16-bit fixed point because we need to +/// store it efficiently in the ComputedStyle representation. The assumption being that zooms over +/// 1000 aren't quite useful. +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(C)] +pub struct Zoom(ZoomFixedPoint); + +impl ToComputedValue for specified::Zoom { + type ComputedValue = Zoom; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + let n = match *self { + Self::Normal => return Zoom::ONE, + Self::Document => return Zoom::DOCUMENT, + Self::Value(ref n) => n.0.to_number().get(), + }; + if n == 0.0 { + // For legacy reasons, zoom: 0 (and 0%) computes to 1. ¯\_(ツ)_/¯ + return Zoom::ONE; + } + Zoom(ZoomFixedPoint::from_float(n)) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::new_number(computed.value()) + } +} + +impl ToCss for Zoom { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + use std::fmt::Write; + if *self == Self::DOCUMENT { + return dest.write_str("document"); + } + self.value().to_css(dest) + } +} + +impl ToAnimatedValue for Zoom { + type AnimatedValue = Number; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.value() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Zoom(ZoomFixedPoint::from_float(animated.max(0.0))) + } +} + +impl Zoom { + /// The value 1. This is by far the most common value. + pub const ONE: Zoom = Zoom(ZoomFixedPoint { + value: 1 << ZOOM_FRACTION_BITS, + }); + + /// The `document` value. This can appear in the computed zoom property value, but not in the + /// `effective_zoom` field. + pub const DOCUMENT: Zoom = Zoom(ZoomFixedPoint { value: 0 }); + + /// Returns whether we're the number 1. + #[inline] + pub fn is_one(self) -> bool { + self == Self::ONE + } + + /// Returns the value as a float. + #[inline] + pub fn value(&self) -> f32 { + self.0.to_float() + } + + /// Computes the effective zoom for a given new zoom value in rhs. + pub fn compute_effective(self, specified: Self) -> Self { + if specified == Self::DOCUMENT { + return Self::ONE; + } + if self == Self::ONE { + return specified; + } + if specified == Self::ONE { + return self; + } + Zoom(self.0 * specified.0) + } + + /// Returns the zoomed value. + #[inline] + pub fn zoom(self, value: f32) -> f32 { + if self == Self::ONE { + return value; + } + self.value() * value + } +} diff --git a/servo/components/style/values/computed/color.rs b/servo/components/style/values/computed/color.rs new file mode 100644 index 0000000000..9b5185d923 --- /dev/null +++ b/servo/components/style/values/computed/color.rs @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed color values. + +use crate::color::parsing::Color as CSSParserColor; +use crate::color::AbsoluteColor; +use crate::values::animated::ToAnimatedZero; +use crate::values::computed::percentage::Percentage; +use crate::values::generics::color::{ + GenericCaretColor, GenericColor, GenericColorMix, GenericColorOrAuto, +}; +use std::fmt; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::color::{ColorScheme, ForcedColorAdjust, PrintColorAdjust}; + +/// The computed value of the `color` property. +pub type ColorPropertyValue = AbsoluteColor; + +/// A computed value for ``. +pub type Color = GenericColor; + +/// A computed color-mix(). +pub type ColorMix = GenericColorMix; + +impl ToCss for Color { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + match *self { + Self::Absolute(ref c) => c.to_css(dest), + Self::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest), + Self::ColorMix(ref m) => m.to_css(dest), + } + } +} + +impl Color { + /// A fully transparent color. + pub const TRANSPARENT_BLACK: Self = Self::Absolute(AbsoluteColor::TRANSPARENT_BLACK); + + /// An opaque black color. + pub const BLACK: Self = Self::Absolute(AbsoluteColor::BLACK); + + /// An opaque white color. + pub const WHITE: Self = Self::Absolute(AbsoluteColor::WHITE); + + /// Create a new computed [`Color`] from a given color-mix, simplifying it to an absolute color + /// if possible. + pub fn from_color_mix(color_mix: ColorMix) -> Self { + if let Some(absolute) = color_mix.mix_to_absolute() { + Self::Absolute(absolute) + } else { + Self::ColorMix(Box::new(color_mix)) + } + } + + /// Combine this complex color with the given foreground color into an + /// absolute color. + pub fn resolve_to_absolute(&self, current_color: &AbsoluteColor) -> AbsoluteColor { + use crate::values::specified::percentage::ToPercentage; + + match *self { + Self::Absolute(c) => c, + Self::CurrentColor => *current_color, + Self::ColorMix(ref mix) => { + let left = mix.left.resolve_to_absolute(current_color); + let right = mix.right.resolve_to_absolute(current_color); + crate::color::mix::mix( + mix.interpolation, + &left, + mix.left_percentage.to_percentage(), + &right, + mix.right_percentage.to_percentage(), + mix.flags, + ) + }, + } + } +} + +impl ToAnimatedZero for AbsoluteColor { + fn to_animated_zero(&self) -> Result { + Ok(Self::TRANSPARENT_BLACK) + } +} + +/// auto | +pub type ColorOrAuto = GenericColorOrAuto; + +/// caret-color +pub type CaretColor = GenericCaretColor; 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; diff --git a/servo/components/style/values/computed/counters.rs b/servo/components/style/values/computed/counters.rs new file mode 100644 index 0000000000..fd5e915c4a --- /dev/null +++ b/servo/components/style/values/computed/counters.rs @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for counter properties + +use crate::values::computed::image::Image; +use crate::values::generics::counters as generics; +use crate::values::generics::counters::CounterIncrement as GenericCounterIncrement; +use crate::values::generics::counters::CounterReset as GenericCounterReset; +use crate::values::generics::counters::CounterSet as GenericCounterSet; + +/// A computed value for the `counter-increment` property. +pub type CounterIncrement = GenericCounterIncrement; + +/// A computed value for the `counter-reset` property. +pub type CounterReset = GenericCounterReset; + +/// A computed value for the `counter-set` property. +pub type CounterSet = GenericCounterSet; + +/// A computed value for the `content` property. +pub type Content = generics::GenericContent; + +/// A computed content item. +pub type ContentItem = generics::GenericContentItem; diff --git a/servo/components/style/values/computed/easing.rs b/servo/components/style/values/computed/easing.rs new file mode 100644 index 0000000000..d351b3c71d --- /dev/null +++ b/servo/components/style/values/computed/easing.rs @@ -0,0 +1,109 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS Easing functions. + +use euclid::approxeq::ApproxEq; + +use crate::bezier::Bezier; +use crate::piecewise_linear::PiecewiseLinearFunction; +use crate::values::computed::{Integer, Number}; +use crate::values::generics::easing::{self, BeforeFlag, StepPosition, TimingKeyword}; + +/// A computed timing function. +pub type ComputedTimingFunction = easing::TimingFunction; + +/// An alias of the computed timing function. +pub type TimingFunction = ComputedTimingFunction; + +impl ComputedTimingFunction { + fn calculate_step_output( + steps: i32, + pos: StepPosition, + progress: f64, + before_flag: BeforeFlag, + ) -> f64 { + // User specified values can cause overflow (bug 1706157). Increments/decrements + // should be gravefully handled. + let mut current_step = (progress * (steps as f64)).floor() as i32; + + // Increment current step if it is jump-start or start. + if pos == StepPosition::Start || + pos == StepPosition::JumpStart || + pos == StepPosition::JumpBoth + { + current_step = current_step.checked_add(1).unwrap_or(current_step); + } + + // If the "before flag" is set and we are at a transition point, + // drop back a step + if before_flag == BeforeFlag::Set && + (progress * steps as f64).rem_euclid(1.0).approx_eq(&0.0) + { + current_step = current_step.checked_sub(1).unwrap_or(current_step); + } + + // We should not produce a result outside [0, 1] unless we have an + // input outside that range. This takes care of steps that would otherwise + // occur at boundaries. + if progress >= 0.0 && current_step < 0 { + current_step = 0; + } + + // |jumps| should always be in [1, i32::MAX]. + let jumps = if pos == StepPosition::JumpBoth { + steps.checked_add(1).unwrap_or(steps) + } else if pos == StepPosition::JumpNone { + steps.checked_sub(1).unwrap_or(steps) + } else { + steps + }; + + if progress <= 1.0 && current_step > jumps { + current_step = jumps; + } + + (current_step as f64) / (jumps as f64) + } + + /// The output of the timing function given the progress ratio of this animation. + pub fn calculate_output(&self, progress: f64, before_flag: BeforeFlag, epsilon: f64) -> f64 { + let progress = match self { + TimingFunction::CubicBezier { x1, y1, x2, y2 } => { + Bezier::calculate_bezier_output(progress, epsilon, *x1, *y1, *x2, *y2) + }, + TimingFunction::Steps(steps, pos) => { + Self::calculate_step_output(*steps, *pos, progress, before_flag) + }, + TimingFunction::LinearFunction(function) => function.at(progress as f32).into(), + TimingFunction::Keyword(keyword) => match keyword { + TimingKeyword::Linear => progress, + TimingKeyword::Ease => { + Bezier::calculate_bezier_output(progress, epsilon, 0.25, 0.1, 0.25, 1.) + }, + TimingKeyword::EaseIn => { + Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 1., 1.) + }, + TimingKeyword::EaseOut => { + Bezier::calculate_bezier_output(progress, epsilon, 0., 0., 0.58, 1.) + }, + TimingKeyword::EaseInOut => { + Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 0.58, 1.) + }, + }, + }; + + // The output progress value of an easing function is a real number in the range: + // [-inf, inf]. + // https://drafts.csswg.org/css-easing-1/#output-progress-value + // + // However, we expect to use the finite progress for interpolation and web-animations + // https://drafts.csswg.org/css-values-4/#interpolation + // https://drafts.csswg.org/web-animations-1/#dom-computedeffecttiming-progress + // + // So we clamp the infinite progress, per the spec issue: + // https://github.com/w3c/csswg-drafts/issues/8344 + progress.min(f64::MAX).max(f64::MIN) + } +} diff --git a/servo/components/style/values/computed/effects.rs b/servo/components/style/values/computed/effects.rs new file mode 100644 index 0000000000..b0a92024ca --- /dev/null +++ b/servo/components/style/values/computed/effects.rs @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to effects. + +use crate::values::computed::color::Color; +use crate::values::computed::length::{Length, NonNegativeLength}; +#[cfg(feature = "gecko")] +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Angle, NonNegativeNumber, ZeroToOneNumber}; +use crate::values::generics::effects::BoxShadow as GenericBoxShadow; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +#[cfg(not(feature = "gecko"))] +use crate::values::Impossible; + +/// A computed value for a single shadow of the `box-shadow` property. +pub type BoxShadow = GenericBoxShadow; + +/// 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; 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; + +impl FlexBasis { + /// `auto` + #[inline] + pub fn auto() -> Self { + GenericFlexBasis::Size(Size::auto()) + } +} diff --git a/servo/components/style/values/computed/font.rs b/servo/components/style/values/computed/font.rs new file mode 100644 index 0000000000..de0a5e372b --- /dev/null +++ b/servo/components/style/values/computed/font.rs @@ -0,0 +1,1369 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for font properties + +use crate::parser::{Parse, ParserContext}; +use crate::values::animated::ToAnimatedValue; +use crate::values::computed::{ + Angle, Context, Integer, Length, NonNegativeLength, NonNegativeNumber, Number, Percentage, + ToComputedValue, +}; +use crate::values::generics::font::{ + FeatureTagValue, FontSettings, TaggedFontValue, VariationValue, +}; +use crate::values::generics::{font as generics, NonNegative}; +use crate::values::resolved::{Context as ResolvedContext, ToResolvedValue}; +use crate::values::specified::font::{ + self as specified, KeywordInfo, MAX_FONT_WEIGHT, MIN_FONT_WEIGHT, +}; +use crate::values::specified::length::{FontBaseSize, LineHeightBase, NoCalcLength}; +use crate::Atom; +use cssparser::{serialize_identifier, CssStringWriter, Parser}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use num_traits::abs; +use num_traits::cast::AsPrimitive; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +pub use crate::values::computed::Length as MozScriptMinSize; +pub use crate::values::specified::font::MozScriptSizeMultiplier; +pub use crate::values::specified::font::{FontPalette, FontSynthesis}; +pub use crate::values::specified::font::{ + FontVariantAlternates, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric, XLang, + XTextScale, +}; +pub use crate::values::specified::Integer as SpecifiedInteger; +pub use crate::values::specified::Number as SpecifiedNumber; + +/// Generic template for font property type classes that use a fixed-point +/// internal representation with `FRACTION_BITS` for the fractional part. +/// +/// Values are constructed from and exposed as floating-point, but stored +/// internally as fixed point, so there will be a quantization effect on +/// fractional values, depending on the number of fractional bits used. +/// +/// Using (16-bit) fixed-point types rather than floats for these style +/// attributes reduces the memory footprint of gfxFontEntry and gfxFontStyle; it +/// will also tend to reduce the number of distinct font instances that get +/// created, particularly when styles are animated or set to arbitrary values +/// (e.g. by sliders in the UI), which should reduce pressure on graphics +/// resources and improve cache hit rates. +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[repr(C)] +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +pub struct FixedPoint { + /// The actual representation. + pub value: T, +} + +impl FixedPoint +where + T: AsPrimitive, + f32: AsPrimitive, + u16: AsPrimitive, +{ + const SCALE: u16 = 1 << FRACTION_BITS; + const INVERSE_SCALE: f32 = 1.0 / Self::SCALE as f32; + + /// Returns a fixed-point bit from a floating-point context. + pub fn from_float(v: f32) -> Self { + Self { + value: (v * Self::SCALE as f32).round().as_(), + } + } + + /// Returns the floating-point representation. + pub fn to_float(&self) -> f32 { + self.value.as_() * Self::INVERSE_SCALE + } +} + +// We implement this and mul below only for u16 types, because u32 types might need more care about +// overflow. But it's not hard to implement in either case. +impl std::ops::Div for FixedPoint { + type Output = Self; + fn div(self, rhs: Self) -> Self { + Self { + value: (((self.value as u32) << (FRACTION_BITS as u32)) / (rhs.value as u32)) as u16, + } + } +} +impl std::ops::Mul for FixedPoint { + type Output = Self; + fn mul(self, rhs: Self) -> Self { + Self { + value: (((self.value as u32) * (rhs.value as u32)) >> (FRACTION_BITS as u32)) as u16, + } + } +} + +/// font-weight: range 1..1000, fractional values permitted; keywords +/// 'normal', 'bold' aliased to 400, 700 respectively. +/// +/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375) +pub const FONT_WEIGHT_FRACTION_BITS: u16 = 6; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference +/// workaround. +pub type FontWeightFixedPoint = FixedPoint; + +/// A value for the font-weight property per: +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(C)] +pub struct FontWeight(FontWeightFixedPoint); +impl ToAnimatedValue for FontWeight { + type AnimatedValue = Number; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.value() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontWeight::from_float(animated) + } +} + +impl ToCss for FontWeight { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.value().to_css(dest) + } +} + +impl FontWeight { + /// The `normal` keyword. + pub const NORMAL: FontWeight = FontWeight(FontWeightFixedPoint { + value: 400 << FONT_WEIGHT_FRACTION_BITS, + }); + + /// The `bold` value. + pub const BOLD: FontWeight = FontWeight(FontWeightFixedPoint { + value: 700 << FONT_WEIGHT_FRACTION_BITS, + }); + + /// The threshold from which we consider a font bold. + pub const BOLD_THRESHOLD: FontWeight = FontWeight(FontWeightFixedPoint { + value: 600 << FONT_WEIGHT_FRACTION_BITS, + }); + + /// Returns the `normal` keyword value. + pub fn normal() -> Self { + Self::NORMAL + } + + /// Weither this weight is bold + pub fn is_bold(&self) -> bool { + *self >= Self::BOLD_THRESHOLD + } + + /// Returns the value as a float. + pub fn value(&self) -> f32 { + self.0.to_float() + } + + /// Construct a valid weight from a float value. + pub fn from_float(v: f32) -> Self { + Self(FixedPoint::from_float( + v.max(MIN_FONT_WEIGHT).min(MAX_FONT_WEIGHT), + )) + } + + /// Return the bolder weight. + /// + /// See the table in: + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + pub fn bolder(self) -> Self { + let value = self.value(); + if value < 350. { + return Self::NORMAL; + } + if value < 550. { + return Self::BOLD; + } + Self::from_float(value.max(900.)) + } + + /// Return the lighter weight. + /// + /// See the table in: + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + pub fn lighter(self) -> Self { + let value = self.value(); + if value < 550. { + return Self::from_float(value.min(100.)); + } + if value < 750. { + return Self::NORMAL; + } + Self::BOLD + } +} + +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))] +/// The computed value of font-size +pub struct FontSize { + /// The computed size, that we use to compute ems etc. This accounts for + /// e.g., text-zoom. + pub computed_size: NonNegativeLength, + /// The actual used size. This is the computed font size, potentially + /// constrained by other factors like minimum font-size settings and so on. + #[css(skip)] + pub used_size: NonNegativeLength, + /// If derived from a keyword, the keyword and additional transformations applied to it + #[css(skip)] + pub keyword_info: KeywordInfo, +} + +impl FontSize { + /// The actual computed font size. + #[inline] + pub fn computed_size(&self) -> Length { + self.computed_size.0 + } + + /// The actual used font size. + #[inline] + pub fn used_size(&self) -> Length { + self.used_size.0 + } + + #[inline] + /// Get default value of font size. + pub fn medium() -> Self { + Self { + computed_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)), + used_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)), + keyword_info: KeywordInfo::medium(), + } + } +} + +impl ToAnimatedValue for FontSize { + type AnimatedValue = Length; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.computed_size.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + FontSize { + computed_size: NonNegative(animated.clamp_to_non_negative()), + used_size: NonNegative(animated.clamp_to_non_negative()), + keyword_info: KeywordInfo::none(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, ToComputedValue, ToResolvedValue)] +#[cfg_attr(feature = "servo", derive(Hash, MallocSizeOf, Serialize, Deserialize))] +/// Specifies a prioritized list of font family names or generic family names. +#[repr(C)] +pub struct FontFamily { + /// The actual list of family names. + pub families: FontFamilyList, + /// Whether this font-family came from a specified system-font. + pub is_system_font: bool, + /// Whether this is the initial font-family that might react to language + /// changes. + pub is_initial: bool, +} + +macro_rules! static_font_family { + ($ident:ident, $family:expr) => { + lazy_static! { + static ref $ident: FontFamily = FontFamily { + families: FontFamilyList { + list: crate::ArcSlice::from_iter_leaked(std::iter::once($family)), + }, + is_system_font: false, + is_initial: false, + }; + } + }; +} + +impl FontFamily { + #[inline] + /// Get default font family as `serif` which is a generic font-family + pub fn serif() -> Self { + Self::generic(GenericFontFamily::Serif).clone() + } + + /// Returns the font family for `-moz-bullet-font`. + pub(crate) fn moz_bullet() -> &'static Self { + static_font_family!( + MOZ_BULLET, + SingleFontFamily::FamilyName(FamilyName { + name: atom!("-moz-bullet-font"), + syntax: FontFamilyNameSyntax::Identifiers, + }) + ); + + &*MOZ_BULLET + } + + /// Returns a font family for a single system font. + pub fn for_system_font(name: &str) -> Self { + Self { + families: FontFamilyList { + list: crate::ArcSlice::from_iter(std::iter::once(SingleFontFamily::FamilyName( + FamilyName { + name: Atom::from(name), + syntax: FontFamilyNameSyntax::Identifiers, + }, + ))), + }, + is_system_font: true, + is_initial: false, + } + } + + /// Returns a generic font family. + pub fn generic(generic: GenericFontFamily) -> &'static Self { + macro_rules! generic_font_family { + ($ident:ident, $family:ident) => { + static_font_family!( + $ident, + SingleFontFamily::Generic(GenericFontFamily::$family) + ) + }; + } + + generic_font_family!(SERIF, Serif); + generic_font_family!(SANS_SERIF, SansSerif); + generic_font_family!(MONOSPACE, Monospace); + generic_font_family!(CURSIVE, Cursive); + generic_font_family!(FANTASY, Fantasy); + generic_font_family!(MOZ_EMOJI, MozEmoji); + generic_font_family!(SYSTEM_UI, SystemUi); + + let family = match generic { + GenericFontFamily::None => { + debug_assert!(false, "Bogus caller!"); + &*SERIF + }, + GenericFontFamily::Serif => &*SERIF, + GenericFontFamily::SansSerif => &*SANS_SERIF, + GenericFontFamily::Monospace => &*MONOSPACE, + GenericFontFamily::Cursive => &*CURSIVE, + GenericFontFamily::Fantasy => &*FANTASY, + GenericFontFamily::MozEmoji => &*MOZ_EMOJI, + GenericFontFamily::SystemUi => &*SYSTEM_UI, + }; + debug_assert_eq!( + *family.families.iter().next().unwrap(), + SingleFontFamily::Generic(generic) + ); + family + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for FontFamily { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + use malloc_size_of::MallocUnconditionalSizeOf; + // SharedFontList objects are generally measured from the pointer stored + // in the specified value. So only count this if the SharedFontList is + // unshared. + let shared_font_list = &self.families.list; + if shared_font_list.is_unique() { + shared_font_list.unconditional_size_of(ops) + } else { + 0 + } + } +} + +impl ToCss for FontFamily { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + let mut iter = self.families.iter(); + match iter.next() { + Some(f) => f.to_css(dest)?, + None => return Ok(()), + } + for family in iter { + dest.write_str(", ")?; + family.to_css(dest)?; + } + Ok(()) + } +} + +/// The name of a font family of choice. +#[derive( + Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(C)] +pub struct FamilyName { + /// Name of the font family. + pub name: Atom, + /// Syntax of the font family. + pub syntax: FontFamilyNameSyntax, +} + +impl FamilyName { + fn is_known_icon_font_family(&self) -> bool { + use crate::gecko_bindings::bindings; + unsafe { bindings::Gecko_IsKnownIconFontFamily(self.name.as_ptr()) } + } +} + +impl ToCss for FamilyName { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + match self.syntax { + FontFamilyNameSyntax::Quoted => { + dest.write_char('"')?; + write!(CssStringWriter::new(dest), "{}", self.name)?; + dest.write_char('"') + }, + FontFamilyNameSyntax::Identifiers => { + let mut first = true; + for ident in self.name.to_string().split(' ') { + if first { + first = false; + } else { + dest.write_char(' ')?; + } + debug_assert!( + !ident.is_empty(), + "Family name with leading, \ + trailing, or consecutive white spaces should \ + have been marked quoted by the parser" + ); + serialize_identifier(ident, dest)?; + } + Ok(()) + }, + } + } +} + +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +/// Font family names must either be given quoted as strings, +/// or unquoted as a sequence of one or more identifiers. +#[repr(u8)] +pub enum FontFamilyNameSyntax { + /// The family name was specified in a quoted form, e.g. "Font Name" + /// or 'Font Name'. + Quoted, + + /// The family name was specified in an unquoted form as a sequence of + /// identifiers. + Identifiers, +} + +/// A set of faces that vary in weight, width or slope. +/// cbindgen:derive-mut-casts=true +#[derive( + Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize, Hash))] +#[repr(u8)] +pub enum SingleFontFamily { + /// The name of a font family of choice. + FamilyName(FamilyName), + /// Generic family name. + Generic(GenericFontFamily), +} + +fn system_ui_enabled(_: &ParserContext) -> bool { + static_prefs::pref!("layout.css.system-ui.enabled") +} + +/// A generic font-family name. +/// +/// The order here is important, if you change it make sure that +/// `gfxPlatformFontList.h`s ranged array and `gfxFontFamilyList`'s +/// sSingleGenerics are updated as well. +/// +/// NOTE(emilio): Should be u8, but it's a u32 because of ABI issues between GCC +/// and LLVM see https://bugs.llvm.org/show_bug.cgi?id=44228 / bug 1600735 / +/// bug 1726515. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + Parse, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[repr(u32)] +#[allow(missing_docs)] +pub enum GenericFontFamily { + /// No generic family specified, only for internal usage. + /// + /// NOTE(emilio): Gecko code relies on this variant being zero. + #[css(skip)] + None = 0, + Serif, + SansSerif, + #[parse(aliases = "-moz-fixed")] + Monospace, + Cursive, + Fantasy, + #[parse(condition = "system_ui_enabled")] + SystemUi, + /// An internal value for emoji font selection. + #[css(skip)] + #[cfg(feature = "gecko")] + MozEmoji, +} + +impl GenericFontFamily { + /// When we disallow websites to override fonts, we ignore some generic + /// families that the website might specify, since they're not configured by + /// the user. See bug 789788 and bug 1730098. + pub(crate) fn valid_for_user_font_prioritization(self) -> bool { + match self { + Self::None | Self::Fantasy | Self::Cursive | Self::SystemUi | Self::MozEmoji => false, + + Self::Serif | Self::SansSerif | Self::Monospace => true, + } + } +} + +impl Parse for SingleFontFamily { + /// Parse a font-family value. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) { + return Ok(SingleFontFamily::FamilyName(FamilyName { + name: Atom::from(&*value), + syntax: FontFamilyNameSyntax::Quoted, + })); + } + + if let Ok(generic) = input.try_parse(|i| GenericFontFamily::parse(context, i)) { + return Ok(SingleFontFamily::Generic(generic)); + } + + let first_ident = input.expect_ident_cloned()?; + let reserved = match_ignore_ascii_case! { &first_ident, + // https://drafts.csswg.org/css-fonts/#propdef-font-family + // "Font family names that happen to be the same as a keyword value + // (`inherit`, `serif`, `sans-serif`, `monospace`, `fantasy`, and `cursive`) + // must be quoted to prevent confusion with the keywords with the same names. + // The keywords ‘initial’ and ‘default’ are reserved for future use + // and must also be quoted when used as font names. + // UAs must not consider these keywords as matching the type." + "inherit" | "initial" | "unset" | "revert" | "default" => true, + _ => false, + }; + + let mut value = first_ident.as_ref().to_owned(); + let mut serialize_quoted = value.contains(' '); + + // These keywords are not allowed by themselves. + // The only way this value can be valid with with another keyword. + if reserved { + let ident = input.expect_ident()?; + serialize_quoted = serialize_quoted || ident.contains(' '); + value.push(' '); + value.push_str(&ident); + } + while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { + serialize_quoted = serialize_quoted || ident.contains(' '); + value.push(' '); + value.push_str(&ident); + } + let syntax = if serialize_quoted { + // For font family names which contains special white spaces, e.g. + // `font-family: \ a\ \ b\ \ c\ ;`, it is tricky to serialize them + // as identifiers correctly. Just mark them quoted so we don't need + // to worry about them in serialization code. + FontFamilyNameSyntax::Quoted + } else { + FontFamilyNameSyntax::Identifiers + }; + Ok(SingleFontFamily::FamilyName(FamilyName { + name: Atom::from(value), + syntax, + })) + } +} + +#[cfg(feature = "servo")] +impl SingleFontFamily { + /// Get the corresponding font-family with Atom + pub fn from_atom(input: Atom) -> SingleFontFamily { + match input { + atom!("serif") => return SingleFontFamily::Generic(GenericFontFamily::Serif), + atom!("sans-serif") => return SingleFontFamily::Generic(GenericFontFamily::SansSerif), + atom!("cursive") => return SingleFontFamily::Generic(GenericFontFamily::Cursive), + atom!("fantasy") => return SingleFontFamily::Generic(GenericFontFamily::Fantasy), + atom!("monospace") => return SingleFontFamily::Generic(GenericFontFamily::Monospace), + _ => {}, + } + + match_ignore_ascii_case! { &input, + "serif" => return SingleFontFamily::Generic(GenericFontFamily::Serif), + "sans-serif" => return SingleFontFamily::Generic(GenericFontFamily::SansSerif), + "cursive" => return SingleFontFamily::Generic(GenericFontFamily::Cursive), + "fantasy" => return SingleFontFamily::Generic(GenericFontFamily::Fantasy), + "monospace" => return SingleFontFamily::Generic(GenericFontFamily::Monospace), + _ => {} + } + + // We don't know if it's quoted or not. So we set it to + // quoted by default. + SingleFontFamily::FamilyName(FamilyName { + name: input, + syntax: FontFamilyNameSyntax::Quoted, + }) + } +} + +/// A list of font families. +#[derive(Clone, Debug, ToComputedValue, ToResolvedValue, ToShmem, PartialEq, Eq)] +#[repr(C)] +pub struct FontFamilyList { + /// The actual list of font families specified. + pub list: crate::ArcSlice, +} + +impl FontFamilyList { + /// Return iterator of SingleFontFamily + pub fn iter(&self) -> impl Iterator { + self.list.iter() + } + + /// If there's a generic font family on the list which is suitable for user + /// font prioritization, then move it ahead of the other families in the list, + /// except for any families known to be ligature-based icon fonts, where using a + /// generic instead of the site's specified font may cause substantial breakage. + /// If no suitable generic is found in the list, insert the default generic ahead + /// of all the listed families except for known ligature-based icon fonts. + pub(crate) fn prioritize_first_generic_or_prepend(&mut self, generic: GenericFontFamily) { + let mut index_of_first_generic = None; + let mut target_index = None; + + for (i, f) in self.iter().enumerate() { + match &*f { + SingleFontFamily::Generic(f) => { + if index_of_first_generic.is_none() && f.valid_for_user_font_prioritization() { + // If we haven't found a target position, there's nothing to do; + // this entry is already ahead of everything except any whitelisted + // icon fonts. + if target_index.is_none() { + return; + } + index_of_first_generic = Some(i); + break; + } + // A non-prioritized generic (e.g. cursive, fantasy) becomes the target + // position for prioritization, just like arbitrary named families. + if target_index.is_none() { + target_index = Some(i); + } + }, + SingleFontFamily::FamilyName(fam) => { + // Target position for the first generic is in front of the first + // non-whitelisted icon font family we find. + if target_index.is_none() && !fam.is_known_icon_font_family() { + target_index = Some(i); + } + }, + } + } + + let mut new_list = self.list.iter().cloned().collect::>(); + let first_generic = match index_of_first_generic { + Some(i) => new_list.remove(i), + None => SingleFontFamily::Generic(generic), + }; + + if let Some(i) = target_index { + new_list.insert(i, first_generic); + } else { + new_list.push(first_generic); + } + self.list = crate::ArcSlice::from_iter(new_list.into_iter()); + } + + /// Returns whether we need to prioritize user fonts. + pub(crate) fn needs_user_font_prioritization(&self) -> bool { + self.iter().next().map_or(true, |f| match f { + SingleFontFamily::Generic(f) => !f.valid_for_user_font_prioritization(), + _ => true, + }) + } + + /// Return the generic ID if it is a single generic font + pub fn single_generic(&self) -> Option { + let mut iter = self.iter(); + if let Some(SingleFontFamily::Generic(f)) = iter.next() { + if iter.next().is_none() { + return Some(*f); + } + } + None + } +} + +/// Preserve the readability of text when font fallback occurs. +pub type FontSizeAdjust = generics::GenericFontSizeAdjust; + +impl FontSizeAdjust { + #[inline] + /// Default value of font-size-adjust + pub fn none() -> Self { + FontSizeAdjust::None + } +} + +impl ToComputedValue for specified::FontSizeAdjust { + type ComputedValue = FontSizeAdjust; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::font_metrics::FontMetricsOrientation; + + let font_metrics = |vertical| { + let orient = if vertical { + FontMetricsOrientation::MatchContextPreferVertical + } else { + FontMetricsOrientation::Horizontal + }; + let metrics = context.query_font_metrics(FontBaseSize::CurrentStyle, orient, false); + let font_size = context.style().get_font().clone_font_size().used_size.0; + (metrics, font_size) + }; + + // Macro to resolve a from-font value using the given metric field. If not present, + // returns the fallback value, or if that is negative, resolves using ascent instead + // of the missing field (this is the fallback for cap-height). + macro_rules! resolve { + ($basis:ident, $value:expr, $vertical:expr, $field:ident, $fallback:expr) => {{ + match $value { + specified::FontSizeAdjustFactor::Number(f) => { + FontSizeAdjust::$basis(f.to_computed_value(context)) + }, + specified::FontSizeAdjustFactor::FromFont => { + let (metrics, font_size) = font_metrics($vertical); + let ratio = if let Some(metric) = metrics.$field { + metric / font_size + } else if $fallback >= 0.0 { + $fallback + } else { + metrics.ascent / font_size + }; + if ratio.is_nan() { + FontSizeAdjust::$basis(NonNegative(abs($fallback))) + } else { + FontSizeAdjust::$basis(NonNegative(ratio)) + } + }, + } + }}; + } + + match *self { + Self::None => FontSizeAdjust::None, + Self::ExHeight(val) => resolve!(ExHeight, val, false, x_height, 0.5), + Self::CapHeight(val) => { + resolve!(CapHeight, val, false, cap_height, -1.0 /* fall back to ascent */) + }, + Self::ChWidth(val) => resolve!(ChWidth, val, false, zero_advance_measure, 0.5), + Self::IcWidth(val) => resolve!(IcWidth, val, false, ic_width, 1.0), + Self::IcHeight(val) => resolve!(IcHeight, val, true, ic_width, 1.0), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + macro_rules! case { + ($basis:ident, $val:expr) => { + Self::$basis(specified::FontSizeAdjustFactor::Number( + ToComputedValue::from_computed_value($val), + )) + }; + } + match *computed { + FontSizeAdjust::None => Self::None, + FontSizeAdjust::ExHeight(ref val) => case!(ExHeight, val), + FontSizeAdjust::CapHeight(ref val) => case!(CapHeight, val), + FontSizeAdjust::ChWidth(ref val) => case!(ChWidth, val), + FontSizeAdjust::IcWidth(ref val) => case!(IcWidth, val), + FontSizeAdjust::IcHeight(ref val) => case!(IcHeight, val), + } + } +} + +/// Use FontSettings as computed type of FontFeatureSettings. +pub type FontFeatureSettings = FontSettings>; + +/// The computed value for font-variation-settings. +pub type FontVariationSettings = FontSettings>; + +// The computed value of font-{feature,variation}-settings discards values +// with duplicate tags, keeping only the last occurrence of each tag. +fn dedup_font_settings(settings_list: &mut Vec) +where + T: TaggedFontValue, +{ + if settings_list.len() > 1 { + settings_list.sort_by_key(|k| k.tag().0); + // dedup() keeps the first of any duplicates, but we want the last, + // so we implement it manually here. + let mut prev_tag = settings_list.last().unwrap().tag(); + for i in (0..settings_list.len() - 1).rev() { + let cur_tag = settings_list[i].tag(); + if cur_tag == prev_tag { + settings_list.remove(i); + } + prev_tag = cur_tag; + } + } +} + +impl ToComputedValue for FontSettings +where + T: ToComputedValue, + ::ComputedValue: TaggedFontValue, +{ + type ComputedValue = FontSettings; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let mut v = self + .0 + .iter() + .map(|item| item.to_computed_value(context)) + .collect::>(); + dedup_font_settings(&mut v); + FontSettings(v.into_boxed_slice()) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self( + computed + .0 + .iter() + .map(T::from_computed_value) + .collect::>() + .into_boxed_slice(), + ) + } +} + +/// font-language-override can only have a single 1-4 ASCII character +/// OpenType "language system" tag, so we should be able to compute +/// it and store it as a 32-bit integer +/// (see http://www.microsoft.com/typography/otspec/languagetags.htm). +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[value_info(other_values = "normal")] +pub struct FontLanguageOverride(pub u32); + +impl FontLanguageOverride { + #[inline] + /// Get computed default value of `font-language-override` with 0 + pub fn normal() -> FontLanguageOverride { + FontLanguageOverride(0) + } + + /// Returns this value as a `&str`, backed by `storage`. + #[inline] + pub(crate) fn to_str(self, storage: &mut [u8; 4]) -> &str { + *storage = u32::to_be_bytes(self.0); + // Safe because we ensure it's ASCII during parsing + let slice = if cfg!(debug_assertions) { + std::str::from_utf8(&storage[..]).unwrap() + } else { + unsafe { std::str::from_utf8_unchecked(&storage[..]) } + }; + slice.trim_end() + } + + /// Unsafe because `Self::to_str` requires the value to represent a UTF-8 + /// string. + #[inline] + pub unsafe fn from_u32(value: u32) -> Self { + Self(value) + } +} + +impl ToCss for FontLanguageOverride { + fn to_css(&self, dest: &mut CssWriter) -> 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 for FontLanguageOverride { + fn from(v: u32) -> Self { + unsafe { Self::from_u32(v) } + } +} + +#[cfg(feature = "gecko")] +impl From for u32 { + fn from(v: FontLanguageOverride) -> u32 { + v.0 + } +} + +impl ToComputedValue for specified::MozScriptMinSize { + type ComputedValue = MozScriptMinSize; + + fn to_computed_value(&self, cx: &Context) -> MozScriptMinSize { + // this value is used in the computation of font-size, so + // we use the parent size + let base_size = FontBaseSize::InheritedStyle; + let line_height_base = LineHeightBase::InheritedStyle; + match self.0 { + NoCalcLength::FontRelative(value) => { + value.to_computed_value(cx, base_size, line_height_base) + }, + NoCalcLength::ServoCharacterWidth(value) => { + value.to_computed_value(base_size.resolve(cx).computed_size()) + }, + ref l => l.to_computed_value(cx), + } + } + + fn from_computed_value(other: &MozScriptMinSize) -> Self { + specified::MozScriptMinSize(ToComputedValue::from_computed_value(other)) + } +} + +/// The computed value of the math-depth property. +pub type MathDepth = i8; + +#[cfg(feature = "gecko")] +impl ToComputedValue for specified::MathDepth { + type ComputedValue = MathDepth; + + fn to_computed_value(&self, cx: &Context) -> i8 { + use crate::properties::longhands::math_style::SpecifiedValue as MathStyleValue; + use std::{cmp, i8}; + + let int = match *self { + specified::MathDepth::AutoAdd => { + let parent = cx.builder.get_parent_font().clone_math_depth() as i32; + let style = cx.builder.get_parent_font().clone_math_style(); + if style == MathStyleValue::Compact { + parent.saturating_add(1) + } else { + parent + } + }, + specified::MathDepth::Add(rel) => { + let parent = cx.builder.get_parent_font().clone_math_depth(); + (parent as i32).saturating_add(rel.to_computed_value(cx)) + }, + specified::MathDepth::Absolute(abs) => abs.to_computed_value(cx), + }; + cmp::min(int, i8::MAX as i32) as i8 + } + + fn from_computed_value(other: &i8) -> Self { + let computed_value = *other as i32; + specified::MathDepth::Absolute(SpecifiedInteger::from_computed_value(&computed_value)) + } +} + +/// - Use a signed 8.8 fixed-point value (representable range -128.0..128) +/// +/// Values of below -90 or above 90 not permitted, so we use out of +/// range values to represent normal | oblique +pub const FONT_STYLE_FRACTION_BITS: u16 = 8; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference +/// workaround. +pub type FontStyleFixedPoint = FixedPoint; + +/// The computed value of `font-style`. +/// +/// - Define out of range values min value (-128.0) as meaning 'normal' +/// - Define max value (127.99609375) as 'italic' +/// - Other values represent 'oblique ' +/// - Note that 'oblique 0deg' is distinct from 'normal' (should it be?) +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[derive( + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Hash, + MallocSizeOf, + PartialEq, + PartialOrd, + ToResolvedValue, +)] +#[repr(C)] +pub struct FontStyle(FontStyleFixedPoint); + +impl FontStyle { + /// The normal keyword. + pub const NORMAL: FontStyle = FontStyle(FontStyleFixedPoint { + value: 100 << FONT_STYLE_FRACTION_BITS, + }); + /// The italic keyword. + pub const ITALIC: FontStyle = FontStyle(FontStyleFixedPoint { + value: 101 << FONT_STYLE_FRACTION_BITS, + }); + + /// The default angle for `font-style: oblique`. + /// See also https://github.com/w3c/csswg-drafts/issues/2295 + pub const DEFAULT_OBLIQUE_DEGREES: i16 = 14; + + /// The `oblique` keyword with the default degrees. + pub const OBLIQUE: FontStyle = FontStyle(FontStyleFixedPoint { + value: Self::DEFAULT_OBLIQUE_DEGREES << FONT_STYLE_FRACTION_BITS, + }); + + /// The `normal` value. + #[inline] + pub fn normal() -> Self { + Self::NORMAL + } + + /// Returns the oblique angle for this style. + pub fn oblique(degrees: f32) -> Self { + Self(FixedPoint::from_float( + degrees + .max(specified::FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES) + .min(specified::FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES), + )) + } + + /// Returns the oblique angle for this style. + pub fn oblique_degrees(&self) -> f32 { + debug_assert_ne!(*self, Self::NORMAL); + debug_assert_ne!(*self, Self::ITALIC); + self.0.to_float() + } +} + +impl ToCss for FontStyle { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + if *self == Self::NORMAL { + return dest.write_str("normal"); + } + if *self == Self::ITALIC { + return dest.write_str("italic"); + } + if *self == Self::OBLIQUE { + return dest.write_str("oblique"); + } + dest.write_str("oblique ")?; + let angle = Angle::from_degrees(self.oblique_degrees()); + angle.to_css(dest)?; + Ok(()) + } +} + +impl ToAnimatedValue for FontStyle { + type AnimatedValue = generics::FontStyle; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + if self == Self::NORMAL { + // This allows us to animate between normal and oblique values. Per spec, + // https://drafts.csswg.org/css-fonts-4/#font-style-prop: + // Animation type: by computed value type; 'normal' animates as 'oblique 0deg' + return generics::FontStyle::Oblique(Angle::from_degrees(0.0)); + } + if self == Self::ITALIC { + return generics::FontStyle::Italic; + } + generics::FontStyle::Oblique(Angle::from_degrees(self.oblique_degrees())) + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + match animated { + generics::FontStyle::Normal => Self::NORMAL, + generics::FontStyle::Italic => Self::ITALIC, + generics::FontStyle::Oblique(ref angle) => { + if angle.degrees() == 0.0 { + // Reverse the conversion done in to_animated_value() + Self::NORMAL + } else { + Self::oblique(angle.degrees()) + } + }, + } + } +} + +/// font-stretch is a percentage relative to normal. +/// +/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375) +/// +/// We arbitrarily limit here to 1000%. (If that becomes a problem, we could +/// reduce the number of fractional bits and increase the limit.) +pub const FONT_STRETCH_FRACTION_BITS: u16 = 6; + +/// This is an alias which is useful mostly as a cbindgen / C++ inference +/// workaround. +pub type FontStretchFixedPoint = FixedPoint; + +/// A value for the font-stretch property per: +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch +/// +/// cbindgen:derive-lt +/// cbindgen:derive-lte +/// cbindgen:derive-gt +/// cbindgen:derive-gte +#[derive( + Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue, +)] +#[repr(C)] +pub struct FontStretch(pub FontStretchFixedPoint); + +impl FontStretch { + /// The fraction bits, as an easy-to-access-constant. + pub const FRACTION_BITS: u16 = FONT_STRETCH_FRACTION_BITS; + /// 0.5 in our floating point representation. + pub const HALF: u16 = 1 << (Self::FRACTION_BITS - 1); + + /// The `ultra-condensed` keyword. + pub const ULTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 50 << Self::FRACTION_BITS, + }); + /// The `extra-condensed` keyword. + pub const EXTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: (62 << Self::FRACTION_BITS) + Self::HALF, + }); + /// The `condensed` keyword. + pub const CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 75 << Self::FRACTION_BITS, + }); + /// The `semi-condensed` keyword. + pub const SEMI_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint { + value: (87 << Self::FRACTION_BITS) + Self::HALF, + }); + /// The `normal` keyword. + pub const NORMAL: FontStretch = FontStretch(FontStretchFixedPoint { + value: 100 << Self::FRACTION_BITS, + }); + /// The `semi-expanded` keyword. + pub const SEMI_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: (112 << Self::FRACTION_BITS) + Self::HALF, + }); + /// The `expanded` keyword. + pub const EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 125 << Self::FRACTION_BITS, + }); + /// The `extra-expanded` keyword. + pub const EXTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 150 << Self::FRACTION_BITS, + }); + /// The `ultra-expanded` keyword. + pub const ULTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint { + value: 200 << Self::FRACTION_BITS, + }); + + /// 100% + pub fn hundred() -> Self { + Self::NORMAL + } + + /// Converts to a computed percentage. + #[inline] + pub fn to_percentage(&self) -> Percentage { + Percentage(self.0.to_float() / 100.0) + } + + /// Converts from a computed percentage value. + pub fn from_percentage(p: f32) -> Self { + Self(FixedPoint::from_float((p * 100.).max(0.0).min(1000.0))) + } + + /// Returns a relevant stretch value from a keyword. + /// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop + pub fn from_keyword(kw: specified::FontStretchKeyword) -> Self { + use specified::FontStretchKeyword::*; + match kw { + UltraCondensed => Self::ULTRA_CONDENSED, + ExtraCondensed => Self::EXTRA_CONDENSED, + Condensed => Self::CONDENSED, + SemiCondensed => Self::SEMI_CONDENSED, + Normal => Self::NORMAL, + SemiExpanded => Self::SEMI_EXPANDED, + Expanded => Self::EXPANDED, + ExtraExpanded => Self::EXTRA_EXPANDED, + UltraExpanded => Self::ULTRA_EXPANDED, + } + } + + /// Returns the stretch keyword if we map to one of the relevant values. + pub fn as_keyword(&self) -> Option { + use specified::FontStretchKeyword::*; + // TODO: Can we use match here? + if *self == Self::ULTRA_CONDENSED { + return Some(UltraCondensed); + } + if *self == Self::EXTRA_CONDENSED { + return Some(ExtraCondensed); + } + if *self == Self::CONDENSED { + return Some(Condensed); + } + if *self == Self::SEMI_CONDENSED { + return Some(SemiCondensed); + } + if *self == Self::NORMAL { + return Some(Normal); + } + if *self == Self::SEMI_EXPANDED { + return Some(SemiExpanded); + } + if *self == Self::EXPANDED { + return Some(Expanded); + } + if *self == Self::EXTRA_EXPANDED { + return Some(ExtraExpanded); + } + if *self == Self::ULTRA_EXPANDED { + return Some(UltraExpanded); + } + None + } +} + +impl ToCss for FontStretch { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.to_percentage().to_css(dest) + } +} + +impl ToAnimatedValue for FontStretch { + type AnimatedValue = Percentage; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.to_percentage() + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + Self::from_percentage(animated.0) + } +} + +/// A computed value for the `line-height` property. +pub type LineHeight = generics::GenericLineHeight; + +impl ToResolvedValue for LineHeight { + type ResolvedValue = Self; + + fn to_resolved_value(self, context: &ResolvedContext) -> Self::ResolvedValue { + // Resolve to an absolute based on font size. + if matches!(self, Self::Normal | Self::MozBlockHeight) { + return self; + } + let wm = context.style.writing_mode; + Self::Length(context.device.calc_line_height( + context.style.get_font(), + wm, + Some(context.element_info.element), + )) + } + + #[inline] + fn from_resolved_value(value: Self::ResolvedValue) -> Self { + value + } +} diff --git a/servo/components/style/values/computed/image.rs b/servo/components/style/values/computed/image.rs new file mode 100644 index 0000000000..8a91d95313 --- /dev/null +++ b/servo/components/style/values/computed/image.rs @@ -0,0 +1,205 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [`image`][image]s +//! +//! [image]: https://drafts.csswg.org/css-images/#image-values + +use crate::values::computed::percentage::Percentage; +use crate::values::computed::position::Position; +use crate::values::computed::url::ComputedImageUrl; +use crate::values::computed::{Angle, Color, Context}; +use crate::values::computed::{ + AngleOrPercentage, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage, + Resolution, ToComputedValue, +}; +use crate::values::generics::image::{self as generic, GradientCompatMode}; +use crate::values::specified::image as specified; +use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword}; +use std::f32::consts::PI; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +pub use specified::ImageRendering; + +/// Computed values for an image according to CSS-IMAGES. +/// +pub type Image = generic::GenericImage; + +// Images should remain small, see https://github.com/servo/servo/pull/18430 +size_of_test!(Image, 16); + +/// Computed values for a CSS gradient. +/// +pub type Gradient = generic::GenericGradient< + LineDirection, + LengthPercentage, + NonNegativeLength, + NonNegativeLengthPercentage, + Position, + Angle, + AngleOrPercentage, + Color, +>; + +/// Computed values for CSS cross-fade +/// +pub type CrossFade = generic::CrossFade; + +/// A computed radial gradient ending shape. +pub type EndingShape = generic::GenericEndingShape; + +/// 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; + +impl ToComputedValue for specified::ImageSet { + type ComputedValue = ImageSet; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let items = self.items.to_computed_value(context); + let dpr = context.device().device_pixel_ratio().get(); + + let mut supported_image = false; + let mut selected_index = std::usize::MAX; + let mut selected_resolution = 0.0; + + for (i, item) in items.iter().enumerate() { + if item.has_mime_type && !context.device().is_supported_mime_type(&item.mime_type) { + // If the MIME type is not supported, we discard the ImageSetItem. + continue; + } + + let candidate_resolution = item.resolution.dppx(); + debug_assert!( + candidate_resolution >= 0.0, + "Resolutions should be non-negative" + ); + if candidate_resolution == 0.0 { + // If the resolution is 0, we also treat it as an invalid image. + continue; + } + + // https://drafts.csswg.org/css-images-4/#image-set-notation: + // + // Make a UA-specific choice of which to load, based on whatever criteria deemed + // relevant (such as the resolution of the display, connection speed, etc). + // + // For now, select the lowest resolution greater than display density, otherwise the + // greatest resolution available. + let better_candidate = || { + if selected_resolution < dpr && candidate_resolution > selected_resolution { + return true; + } + if candidate_resolution < selected_resolution && candidate_resolution >= dpr { + return true; + } + false + }; + + // The first item with a supported MIME type is obviously the current best candidate + if !supported_image || better_candidate() { + supported_image = true; + selected_index = i; + selected_resolution = candidate_resolution; + } + } + + ImageSet { + selected_index, + items, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self { + selected_index: std::usize::MAX, + items: ToComputedValue::from_computed_value(&computed.items), + } + } +} + +impl generic::LineDirection for LineDirection { + fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool { + match *self { + LineDirection::Angle(angle) => angle.radians() == PI, + LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { + compat_mode == GradientCompatMode::Modern + }, + LineDirection::Vertical(VerticalPositionKeyword::Top) => { + compat_mode != GradientCompatMode::Modern + }, + _ => false, + } + } + + fn to_css(&self, dest: &mut CssWriter, compat_mode: GradientCompatMode) -> fmt::Result + where + W: Write, + { + match *self { + LineDirection::Angle(ref angle) => angle.to_css(dest), + LineDirection::Horizontal(x) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest) + }, + LineDirection::Vertical(y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + y.to_css(dest) + }, + LineDirection::Corner(x, y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest)?; + dest.write_char(' ')?; + y.to_css(dest) + }, + } + } +} + +impl ToComputedValue for specified::LineDirection { + type ComputedValue = LineDirection; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + specified::LineDirection::Angle(ref angle) => { + LineDirection::Angle(angle.to_computed_value(context)) + }, + specified::LineDirection::Horizontal(x) => LineDirection::Horizontal(x), + specified::LineDirection::Vertical(y) => LineDirection::Vertical(y), + specified::LineDirection::Corner(x, y) => LineDirection::Corner(x, y), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + LineDirection::Angle(ref angle) => { + specified::LineDirection::Angle(ToComputedValue::from_computed_value(angle)) + }, + LineDirection::Horizontal(x) => specified::LineDirection::Horizontal(x), + LineDirection::Vertical(y) => specified::LineDirection::Vertical(y), + LineDirection::Corner(x, y) => specified::LineDirection::Corner(x, y), + } + } +} diff --git a/servo/components/style/values/computed/length.rs b/servo/components/style/values/computed/length.rs new file mode 100644 index 0000000000..e75676a76d --- /dev/null +++ b/servo/components/style/values/computed/length.rs @@ -0,0 +1,531 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `` computed values, and related ones. + +use super::{Context, Number, ToComputedValue}; +use crate::values::animated::ToAnimatedValue; +use crate::values::computed::NonNegativeNumber; +use crate::values::generics::length as generics; +use crate::values::generics::length::{ + GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::length::{AbsoluteLength, FontBaseSize, LineHeightBase}; +use crate::values::{specified, CSSFloat}; +use crate::Zero; +use app_units::Au; +use std::fmt::{self, Write}; +use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub}; +use style_traits::{CSSPixel, CssWriter, ToCss}; + +pub use super::image::Image; +pub use super::length_percentage::{LengthPercentage, NonNegativeLengthPercentage}; +pub use crate::values::specified::url::UrlOrNone; +pub use crate::values::specified::{Angle, BorderStyle, Time}; + +impl ToComputedValue for specified::NoCalcLength { + type ComputedValue = Length; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + self.to_computed_value_with_base_size( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::CurrentStyle, + ) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Absolute(AbsoluteLength::Px(computed.px())) + } +} + +impl specified::NoCalcLength { + /// Computes a length with a given font-relative base size. + pub fn to_computed_value_with_base_size( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> Length { + match *self { + Self::Absolute(length) => length.to_computed_value(context), + Self::FontRelative(length) => { + length.to_computed_value(context, base_size, line_height_base) + }, + Self::ViewportPercentage(length) => length.to_computed_value(context), + Self::ContainerRelative(length) => length.to_computed_value(context), + Self::ServoCharacterWidth(length) => length + .to_computed_value(context.style().get_font().clone_font_size().computed_size()), + } + } +} + +impl ToComputedValue for specified::Length { + type ComputedValue = Length; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + Self::NoCalc(l) => l.to_computed_value(context), + Self::Calc(ref calc) => { + let result = calc.to_computed_value(context); + debug_assert!( + result.to_length().is_some(), + "{:?} didn't resolve to a length: {:?}", + calc, + result, + ); + result.to_length().unwrap_or_else(Length::zero) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::NoCalc(specified::NoCalcLength::from_computed_value(computed)) + } +} + +/// Some boilerplate to share between negative and non-negative +/// length-percentage or auto. +macro_rules! computed_length_percentage_or_auto { + ($inner:ty) => { + /// Returns the used value. + #[inline] + pub fn to_used_value(&self, percentage_basis: Au) -> Option { + match *self { + Self::Auto => None, + Self::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)), + } + } + + /// Returns true if the computed value is absolute 0 or 0%. + #[inline] + pub fn is_definitely_zero(&self) -> bool { + use crate::values::generics::length::LengthPercentageOrAuto::*; + match *self { + LengthPercentage(ref l) => l.is_definitely_zero(), + Auto => false, + } + } + }; +} + +/// A computed type for ` | auto`. +pub type LengthPercentageOrAuto = generics::GenericLengthPercentageOrAuto; + +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) -> 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; + +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 { + 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 { + match *self { + GenericSize::Auto => None, + GenericSize::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)), + } + } + + /// Returns true if the computed value is absolute 0 or 0%. + #[inline] + pub fn is_definitely_zero(&self) -> bool { + match *self { + Self::Auto => false, + Self::LengthPercentage(ref lp) => lp.is_definitely_zero(), + #[cfg(feature = "gecko")] + Self::MinContent | + Self::MaxContent | + Self::FitContent | + Self::MozAvailable | + Self::FitContentFunction(_) => false, + } + } +} + +/// The computed `` value. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Deserialize, + MallocSizeOf, + PartialEq, + PartialOrd, + Serialize, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CSSPixelLength(CSSFloat); + +impl fmt::Debug for CSSPixelLength { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0.fmt(f)?; + f.write_str(" px") + } +} + +impl CSSPixelLength { + /// Return a new CSSPixelLength. + #[inline] + pub fn new(px: CSSFloat) -> Self { + CSSPixelLength(px) + } + + /// Returns a normalized (NaN turned to zero) version of this length. + #[inline] + pub fn normalized(self) -> Self { + Self::new(crate::values::normalize(self.0)) + } + + /// Returns a finite (normalized and clamped to float min and max) version of this length. + #[inline] + pub fn finite(self) -> Self { + Self::new(crate::values::normalize(self.0).min(f32::MAX).max(f32::MIN)) + } + + /// Scale the length by a given amount. + #[inline] + pub fn scale_by(self, scale: CSSFloat) -> Self { + CSSPixelLength(self.0 * scale) + } + + /// Return the containing pixel value. + #[inline] + pub fn px(self) -> CSSFloat { + self.0 + } + + /// Return the length with app_unit i32 type. + #[inline] + pub fn to_i32_au(self) -> i32 { + Au::from(self).0 + } + + /// Return the absolute value of this length. + #[inline] + pub fn abs(self) -> Self { + CSSPixelLength::new(self.0.abs()) + } + + /// Return the clamped value of this length. + #[inline] + pub fn clamp_to_non_negative(self) -> Self { + CSSPixelLength::new(self.0.max(0.)) + } + + /// Returns the minimum between `self` and `other`. + #[inline] + pub fn min(self, other: Self) -> Self { + CSSPixelLength::new(self.0.min(other.0)) + } + + /// Returns the maximum between `self` and `other`. + #[inline] + pub fn max(self, other: Self) -> Self { + CSSPixelLength::new(self.0.max(other.0)) + } + + /// Sets `self` to the maximum between `self` and `other`. + #[inline] + pub fn max_assign(&mut self, other: Self) { + *self = self.max(other); + } + + /// Clamp the value to a lower bound and an optional upper bound. + /// + /// Can be used for example with `min-width` and `max-width`. + #[inline] + pub fn clamp_between_extremums(self, min_size: Self, max_size: Option) -> Self { + self.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 { + 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + dest.write_str("px") + } +} + +impl std::iter::Sum for CSSPixelLength { + fn sum>(iter: I) -> Self { + iter.fold(Length::zero(), Add::add) + } +} + +impl Add for CSSPixelLength { + type Output = Self; + + #[inline] + fn add(self, other: Self) -> Self { + Self::new(self.px() + other.px()) + } +} + +impl AddAssign for CSSPixelLength { + #[inline] + fn add_assign(&mut self, other: Self) { + self.0 += other.0; + } +} + +impl Div for CSSPixelLength { + type Output = CSSFloat; + + #[inline] + fn div(self, other: Self) -> CSSFloat { + self.px() / other.px() + } +} + +impl Div for CSSPixelLength { + type Output = Self; + + #[inline] + fn div(self, other: CSSFloat) -> Self { + Self::new(self.px() / other) + } +} + +impl MulAssign for CSSPixelLength { + #[inline] + fn mul_assign(&mut self, other: CSSFloat) { + self.0 *= other; + } +} + +impl Mul 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 for Au { + #[inline] + fn from(len: CSSPixelLength) -> Self { + Au::from_f32_px(len.0) + } +} + +impl From for CSSPixelLength { + #[inline] + fn from(len: Au) -> Self { + CSSPixelLength::new(len.to_f32_px()) + } +} + +impl From for euclid::Length { + #[inline] + fn from(length: CSSPixelLength) -> Self { + Self::new(length.0) + } +} + +/// An alias of computed `` value. +pub type Length = CSSPixelLength; + +/// Either a computed `` or the `auto` keyword. +pub type LengthOrAuto = generics::GenericLengthPercentageOrAuto; + +/// Either a non-negative `` or the `auto` keyword. +pub type NonNegativeLengthOrAuto = generics::GenericLengthPercentageOrAuto; + +/// Either a computed `` or a `` value. +pub type LengthOrNumber = GenericLengthOrNumber; + +/// A wrapper of Length, whose value must be >= 0. +pub type NonNegativeLength = NonNegative; + +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 for NonNegativeLength { + #[inline] + fn from(len: Length) -> Self { + NonNegative(len) + } +} + +impl From for NonNegativeLength { + #[inline] + fn from(au: Au) -> Self { + NonNegative(au.into()) + } +} + +impl From 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; + +/// Either a non-negative `` or a ``. +pub type NonNegativeLengthOrNumber = GenericLengthOrNumber; + +/// A computed value for `min-width`, `min-height`, `width` or `height` property. +pub type Size = GenericSize; + +/// A computed value for `max-width` or `min-height` property. +pub type MaxSize = GenericMaxSize; diff --git a/servo/components/style/values/computed/length_percentage.rs b/servo/components/style/values/computed/length_percentage.rs new file mode 100644 index 0000000000..898281a7ef --- /dev/null +++ b/servo/components/style/values/computed/length_percentage.rs @@ -0,0 +1,1055 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `` computed values, and related ones. +//! +//! The over-all design is a tagged pointer, with the lower bits of the pointer +//! being non-zero if it is a non-calc value. +//! +//! It is expected to take 64 bits both in x86 and x86-64. This is implemented +//! as a `union`, with 4 different variants: +//! +//! * The length and percentage variants have a { tag, f32 } (effectively) +//! layout. The tag has to overlap with the lower 2 bits of the calc variant. +//! +//! * The `calc()` variant is a { tag, pointer } in x86 (so same as the +//! others), or just a { pointer } in x86-64 (so that the two bits of the tag +//! can be obtained from the lower bits of the pointer). +//! +//! * There's a `tag` variant just to make clear when only the tag is intended +//! to be read. Note that the tag needs to be masked always by `TAG_MASK`, to +//! deal with the pointer variant in x86-64. +//! +//! The assertions in the constructor methods ensure that the tag getter matches +//! our expectations. + +use super::{Context, Length, Percentage, ToComputedValue}; +use crate::gecko_bindings::structs::GeckoFontMetrics; +use crate::values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::calc::{CalcUnits, PositivePercentageBasis}; +use crate::values::generics::{calc, NonNegative}; +use crate::values::specified::length::{FontBaseSize, LineHeightBase}; +use crate::values::{specified, CSSFloat}; +use crate::{Zero, ZeroNoPercent}; +use app_units::Au; +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps}; +use serde::{Deserialize, Serialize}; +use std::borrow::Cow; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ToCss}; + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct LengthVariant { + tag: u8, + length: Length, +} + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct PercentageVariant { + tag: u8, + percentage: Percentage, +} + +// NOTE(emilio): cbindgen only understands the #[cfg] on the top level +// definition. +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +#[cfg(target_pointer_width = "32")] +pub struct CalcVariant { + tag: u8, + ptr: *mut CalcLengthPercentage, +} + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +#[cfg(target_pointer_width = "64")] +pub struct CalcVariant { + ptr: usize, // In little-endian byte order +} + +// `CalcLengthPercentage` is `Send + Sync` as asserted below. +unsafe impl Send for CalcVariant {} +unsafe impl Sync for CalcVariant {} + +#[doc(hidden)] +#[derive(Clone, Copy)] +#[repr(C)] +pub struct TagVariant { + tag: u8, +} + +/// A `` value. This can be either a ``, a +/// ``, 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() {} + std::mem::transmute::(0u64); + std::mem::transmute::(0u64); + std::mem::transmute::(0u64); + std::mem::transmute::(0u64); + assert_send_and_sync::(); + assert_send_and_sync::(); + assert_send_and_sync::(); +} + +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 `` that borrows the `calc()` variant. +#[derive(Clone, Debug, PartialEq, ToCss)] +enum Unpacked<'a> { + Calc(&'a CalcLengthPercentage), + Length(Length), + Percentage(Percentage), +} + +/// An unpacked `` that mutably borrows the `calc()` variant. +enum UnpackedMut<'a> { + Calc(&'a mut CalcLengthPercentage), + Length(Length), + Percentage(Percentage), +} + +/// An unpacked `` 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 { + match self.unpack() { + Unpacked::Length(l) => Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Length(l))), + Unpacked::Percentage(p) => { + Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(p))) + }, + Unpacked::Calc(p) => Cow::Borrowed(&p.node), + } + } + + /// Constructs a length value. + #[inline] + pub fn new_length(length: Length) -> Self { + let length = Self(LengthPercentageUnion { + length: LengthVariant { + tag: LengthPercentageUnion::TAG_LENGTH, + length, + }, + }); + debug_assert_eq!(length.tag(), Tag::Length); + length + } + + /// Constructs a percentage value. + #[inline] + pub fn new_percent(percentage: Percentage) -> Self { + let percent = Self(LengthPercentageUnion { + percentage: PercentageVariant { + tag: LengthPercentageUnion::TAG_PERCENTAGE, + percentage, + }, + }); + debug_assert_eq!(percent.tag(), Tag::Percentage); + percent + } + + /// Given a `LengthPercentage` value `v`, construct the value representing + /// `calc(100% - v)`. + pub fn hundred_percent_minus(v: Self, clamping_mode: AllowedNumericType) -> Self { + // TODO: This could in theory take ownership of the calc node in `v` if + // possible instead of cloning. + let mut node = v.to_calc_node().into_owned(); + node.negate(); + + let new_node = CalcNode::Sum( + vec![ + CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(Percentage::hundred())), + node, + ] + .into(), + ); + + Self::new_calc(new_node, clamping_mode) + } + + /// Given a list of `LengthPercentage` values, construct the value representing + /// `calc(100% - the sum of the list)`. + pub fn hundred_percent_minus_list(list: &[&Self], clamping_mode: AllowedNumericType) -> Self { + let mut new_list = vec![CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage( + Percentage::hundred(), + ))]; + + for lp in list.iter() { + let mut node = lp.to_calc_node().into_owned(); + node.negate(); + new_list.push(node) + } + + Self::new_calc(CalcNode::Sum(new_list.into()), clamping_mode) + } + + /// Constructs a `calc()` value. + #[inline] + pub fn new_calc(mut node: CalcNode, clamping_mode: AllowedNumericType) -> Self { + node.simplify_and_sort(); + + match node { + CalcNode::Leaf(l) => { + return match l { + CalcLengthPercentageLeaf::Length(l) => { + Self::new_length(Length::new(clamping_mode.clamp(l.px())).normalized()) + }, + CalcLengthPercentageLeaf::Percentage(p) => Self::new_percent(Percentage( + clamping_mode.clamp(crate::values::normalize(p.0)), + )), + CalcLengthPercentageLeaf::Number(number) => { + debug_assert!( + false, + "The final result of a should never be a number" + ); + Self::new_length(Length::new(number)) + }, + }; + }, + _ => Self::new_calc_unchecked(Box::new(CalcLengthPercentage { + clamping_mode, + node, + })), + } + } + + /// Private version of new_calc() that constructs a calc() variant without + /// checking. + fn new_calc_unchecked(calc: Box) -> 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 `` if possible. + pub fn to_length(&self) -> Option { + match self.unpack() { + Unpacked::Length(l) => Some(l), + Unpacked::Percentage(..) | Unpacked::Calc(..) => { + debug_assert!(self.has_percentage()); + return None; + }, + } + } + + /// Converts to a `` if possible. + #[inline] + pub fn to_percentage(&self) -> Option { + 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) -> Option { + 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) -> Option { + 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 { + 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 { + // 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.unpack().to_css(dest) + } +} + +impl Zero for LengthPercentage { + fn zero() -> Self { + LengthPercentage::new_length(Length::zero()) + } + + #[inline] + fn is_zero(&self) -> bool { + self.is_definitely_zero() + } +} + +impl ZeroNoPercent for LengthPercentage { + #[inline] + fn is_zero_no_percent(&self) -> bool { + self.is_definitely_zero() && !self.has_percentage() + } +} + +impl Serialize for LengthPercentage { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.to_serializable().serialize(serializer) + } +} + +impl<'de> Deserialize<'de> for LengthPercentage { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + Ok(Self::from_serializable(Serializable::deserialize( + deserializer, + )?)) + } +} + +/// The leaves of a `` calc expression. +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum CalcLengthPercentageLeaf { + Length(Length), + Percentage(Percentage), + Number(f32), +} + +impl CalcLengthPercentageLeaf { + fn is_zero_length(&self) -> bool { + match *self { + Self::Length(ref l) => l.is_zero(), + Self::Percentage(..) => false, + Self::Number(..) => false, + } + } +} + +impl calc::CalcNodeLeaf for CalcLengthPercentageLeaf { + fn unit(&self) -> CalcUnits { + match self { + Self::Length(_) => CalcUnits::LENGTH, + Self::Percentage(_) => CalcUnits::PERCENTAGE, + Self::Number(_) => CalcUnits::empty(), + } + } + + fn unitless_value(&self) -> f32 { + match *self { + Self::Length(ref l) => l.px(), + Self::Percentage(ref p) => p.0, + Self::Number(n) => n, + } + } + + fn new_number(value: f32) -> Self { + Self::Number(value) + } + + fn as_number(&self) -> Option { + match *self { + Self::Length(_) | Self::Percentage(_) => None, + Self::Number(value) => Some(value), + } + } + + fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option { + use self::CalcLengthPercentageLeaf::*; + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) { + return None; + } + + let self_negative = self.is_negative(); + if self_negative != other.is_negative() { + return Some(if self_negative { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater }); + } + + match (self, other) { + (&Length(ref one), &Length(ref other)) => one.partial_cmp(other), + (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other), + (&Number(ref one), &Number(ref other)) => one.partial_cmp(other), + _ => unsafe { + match *self { + Length(..) | Percentage(..) | Number(..) => {}, + } + debug_unreachable!("Forgot to handle unit in compare()") + }, + } + } + + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { + use self::CalcLengthPercentageLeaf::*; + + // 0px plus anything else is equal to the right hand side. + if self.is_zero_length() { + *self = other.clone(); + return Ok(()); + } + + if other.is_zero_length() { + return Ok(()); + } + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + match (self, other) { + (&mut Length(ref mut one), &Length(ref other)) => { + *one += *other; + }, + (&mut Percentage(ref mut one), &Percentage(ref other)) => { + one.0 += other.0; + }, + (&mut Number(ref mut one), &Number(ref other)) => { + *one += *other; + }, + _ => unsafe { + match *other { + Length(..) | Percentage(..) | Number(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_sum_in_place()") + }, + } + + Ok(()) + } + + fn try_product_in_place(&mut self, other: &mut Self) -> bool { + if let Self::Number(ref mut left) = *self { + if let Self::Number(ref right) = *other { + // Both sides are numbers, so we can just modify the left side. + *left *= *right; + true + } else { + // The right side is not a number, so the result should be in the units of the right + // side. + other.map(|v| v * *left); + std::mem::swap(self, other); + true + } + } else if let Self::Number(ref right) = *other { + // The left side is not a number, but the right side is, so the result is the left + // side unit. + self.map(|v| v * *right); + true + } else { + // Neither side is a number, so a product is not possible. + false + } + } + + fn try_op(&self, other: &Self, op: O) -> Result + where + O: Fn(f32, f32) -> f32, + { + use self::CalcLengthPercentageLeaf::*; + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + Ok(match (self, other) { + (&Length(ref one), &Length(ref other)) => { + Length(super::Length::new(op(one.px(), other.px()))) + }, + (&Percentage(one), &Percentage(other)) => { + Self::Percentage(super::Percentage(op(one.0, other.0))) + }, + (&Number(one), &Number(other)) => Self::Number(op(one, other)), + _ => unsafe { + match *self { + Length(..) | Percentage(..) | Number(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + fn map(&mut self, mut op: impl FnMut(f32) -> f32) { + match self { + Self::Length(value) => { + *value = Length::new(op(value.px())); + }, + Self::Percentage(value) => { + *value = Percentage(op(value.0)); + }, + Self::Number(value) => { + *value = op(*value); + }, + } + } + + fn simplify(&mut self) {} + + fn sort_key(&self) -> calc::SortKey { + match *self { + Self::Length(..) => calc::SortKey::Px, + Self::Percentage(..) => calc::SortKey::Percentage, + Self::Number(..) => calc::SortKey::Number, + } + } +} + +/// The computed version of a calc() node for `` values. +pub type CalcNode = calc::GenericCalcNode; + +/// The representation of a calc() function with mixed lengths and percentages. +#[derive( + Clone, Debug, Deserialize, MallocSizeOf, Serialize, ToAnimatedZero, ToResolvedValue, ToCss, +)] +#[repr(C)] +pub struct CalcLengthPercentage { + #[animation(constant)] + #[css(skip)] + clamping_mode: AllowedNumericType, + node: CalcNode, +} + +impl CalcLengthPercentage { + /// Resolves the percentage. + #[inline] + pub fn resolve(&self, basis: Length) -> Length { + // unwrap() is fine because the conversion below is infallible. + if let CalcLengthPercentageLeaf::Length(px) = self + .node + .resolve_map(|leaf| { + Ok(if let CalcLengthPercentageLeaf::Percentage(p) = leaf { + CalcLengthPercentageLeaf::Length(Length::new(basis.px() * p.0)) + } else { + leaf.clone() + }) + }) + .unwrap() + { + Length::new(self.clamping_mode.clamp(px.px())).normalized() + } else { + unreachable!("resolve_map should turn percentages to lengths, and parsing should ensure that we don't end up with a number"); + } + } +} + +// NOTE(emilio): We don't compare `clamping_mode` since we want to preserve the +// invariant that `from_computed_value(length).to_computed_value(..) == length`. +// +// Right now for e.g. a non-negative length, we set clamping_mode to `All` +// unconditionally for non-calc values, and to `NonNegative` for calc. +// +// If we determine that it's sound, from_computed_value() can generate an +// absolute length, which then would get `All` as the clamping mode. +// +// We may want to just eagerly-detect whether we can clamp in +// `LengthPercentage::new` and switch to `AllowedNumericType::NonNegative` then, +// maybe. +impl PartialEq for CalcLengthPercentage { + fn eq(&self, other: &Self) -> bool { + self.node == other.node + } +} + +impl specified::CalcLengthPercentage { + /// Compute the value, zooming any absolute units by the zoom function. + fn to_computed_value_with_zoom( + &self, + context: &Context, + zoom_fn: F, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> LengthPercentage + where + F: Fn(Length) -> Length, + { + use crate::values::specified::calc::Leaf; + + let node = self.node.map_leaves(|leaf| match *leaf { + Leaf::Percentage(p) => CalcLengthPercentageLeaf::Percentage(Percentage(p)), + Leaf::Length(l) => CalcLengthPercentageLeaf::Length({ + let result = + l.to_computed_value_with_base_size(context, base_size, line_height_base); + if l.should_zoom_text() { + zoom_fn(result) + } else { + result + } + }), + Leaf::Number(n) => CalcLengthPercentageLeaf::Number(n), + Leaf::Angle(..) | Leaf::Time(..) | Leaf::Resolution(..) => { + unreachable!("Shouldn't have parsed") + }, + }); + + LengthPercentage::new_calc(node, self.clamping_mode) + } + + /// Compute font-size or line-height taking into account text-zoom if necessary. + pub fn to_computed_value_zoomed( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> LengthPercentage { + self.to_computed_value_with_zoom( + context, + |abs| context.maybe_zoom_text(abs), + base_size, + line_height_base, + ) + } + + /// Compute the value into pixel length as CSSFloat without context, + /// so it returns Err(()) if there is any non-absolute unit. + pub fn to_computed_pixel_length_without_context(&self) -> Result { + use crate::values::specified::calc::Leaf; + use crate::values::specified::length::NoCalcLength; + + // Simplification should've turned this into an absolute length, + // otherwise it wouldn't have been able to. + match self.node { + calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()), + _ => Err(()), + } + } + + /// Compute the value into pixel length as CSSFloat, using the get_font_metrics function + /// if provided to resolve font-relative dimensions. + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: Option GeckoFontMetrics>, + ) -> Result { + use crate::values::specified::calc::Leaf; + use crate::values::specified::length::NoCalcLength; + + match self.node { + calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()), + calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::FontRelative(ref l))) => { + if let Some(getter) = get_font_metrics { + l.to_computed_pixel_length_with_font_metrics(getter) + } else { + Err(()) + } + }, + _ => Err(()), + } + } + + /// Compute the calc using the current font-size and line-height. (and without text-zoom). + pub fn to_computed_value(&self, context: &Context) -> LengthPercentage { + self.to_computed_value_with_zoom( + context, + |abs| abs, + FontBaseSize::CurrentStyle, + LineHeightBase::CurrentStyle, + ) + } + + #[inline] + fn from_computed_value(computed: &CalcLengthPercentage) -> Self { + use crate::values::specified::calc::Leaf; + use crate::values::specified::length::NoCalcLength; + + specified::CalcLengthPercentage { + clamping_mode: computed.clamping_mode, + node: computed.node.map_leaves(|l| match l { + CalcLengthPercentageLeaf::Length(ref l) => { + Leaf::Length(NoCalcLength::from_px(l.px())) + }, + CalcLengthPercentageLeaf::Percentage(ref p) => Leaf::Percentage(p.0), + CalcLengthPercentageLeaf::Number(n) => Leaf::Number(*n), + }), + } + } +} + +/// https://drafts.csswg.org/css-transitions/#animtype-lpcalc +/// https://drafts.csswg.org/css-values-4/#combine-math +/// https://drafts.csswg.org/css-values-4/#combine-mixed +impl Animate for LengthPercentage { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + Ok(match (self.unpack(), other.unpack()) { + (Unpacked::Length(one), Unpacked::Length(other)) => { + Self::new_length(one.animate(&other, procedure)?) + }, + (Unpacked::Percentage(one), Unpacked::Percentage(other)) => { + Self::new_percent(one.animate(&other, procedure)?) + }, + _ => { + use calc::CalcNodeLeaf; + + fn product_with(mut node: CalcNode, product: f32) -> CalcNode { + let mut number = CalcNode::Leaf(CalcLengthPercentageLeaf::new_number(product)); + if !node.try_product_in_place(&mut number) { + CalcNode::Product(vec![node, number].into()) + } else { + node + } + } + + let (l, r) = procedure.weights(); + let one = product_with(self.to_calc_node().into_owned(), l as f32); + let other = product_with(other.to_calc_node().into_owned(), r as f32); + + Self::new_calc( + CalcNode::Sum(vec![one, other].into()), + AllowedNumericType::All, + ) + }, + }) + } +} + +/// A wrapper of LengthPercentage, whose value must be >= 0. +pub type NonNegativeLengthPercentage = NonNegative; + +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) -> Option { + let resolved = self + .0 + .maybe_to_used_value(containing_length.map(|v| v.into()))?; + Some(std::cmp::max(resolved, Au(0))) + } +} diff --git a/servo/components/style/values/computed/list.rs b/servo/components/style/values/computed/list.rs new file mode 100644 index 0000000000..3e5d1eb220 --- /dev/null +++ b/servo/components/style/values/computed/list.rs @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `list` computed values. + +#[cfg(feature = "gecko")] +pub use crate::values::specified::list::ListStyleType; +pub use crate::values::specified::list::Quotes; + +impl Quotes { + /// Initial value for `quotes`. + #[inline] + pub fn get_initial_value() -> Quotes { + Quotes::Auto + } +} diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs new file mode 100644 index 0000000000..de5db2cdab --- /dev/null +++ b/servo/components/style/values/computed/mod.rs @@ -0,0 +1,1035 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values. + +use self::transform::DirectionVector; +use super::animated::ToAnimatedValue; +use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent; +use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks; +use super::generics::grid::{GenericGridLine, GenericTrackBreadth}; +use super::generics::grid::{GenericTrackSize, TrackList as GenericTrackList}; +use super::generics::transform::IsParallelTo; +use super::generics::{self, GreaterThanOrEqualToOne, NonNegative, ZeroToOne}; +use super::specified; +use super::{CSSFloat, CSSInteger}; +use crate::computed_value_flags::ComputedValueFlags; +use crate::context::QuirksMode; +use crate::custom_properties::ComputedCustomProperties; +use crate::font_metrics::{FontMetrics, FontMetricsOrientation}; +use crate::media_queries::Device; +#[cfg(feature = "gecko")] +use crate::properties; +use crate::properties::{ComputedValues, StyleBuilder}; +use crate::rule_cache::RuleCacheConditions; +use crate::stylesheets::container_rule::{ + ContainerInfo, ContainerSizeQuery, ContainerSizeQueryResult, +}; +use crate::stylist::Stylist; +use crate::values::specified::length::FontBaseSize; +use crate::{ArcSlice, Atom, One}; +use euclid::{default, Point2D, Rect, Size2D}; +use servo_arc::Arc; +use std::cell::RefCell; +use std::cmp; +use std::f32; +use std::ops::{Add, Sub}; + +#[cfg(feature = "gecko")] +pub use self::align::{ + AlignContent, AlignItems, AlignTracks, JustifyContent, JustifyItems, JustifyTracks, + SelfAlignment, +}; +#[cfg(feature = "gecko")] +pub use self::align::{AlignSelf, JustifySelf}; +pub use self::angle::Angle; +pub use self::animation::{ + AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState, + AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis, + ScrollTimelineName, TransitionProperty, ViewTimelineInset +}; +pub use self::background::{BackgroundRepeat, BackgroundSize}; +pub use self::basic_shape::FillRule; +pub use self::border::{ + BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice, + BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, LineWidth, +}; +pub use self::box_::{ + Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize, + ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow, + OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign, + ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, + TouchAction, VerticalAlign, WillChange, Zoom, +}; +pub use self::color::{ + Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust, +}; +pub use self::column::ColumnCount; +pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet}; +pub use self::easing::TimingFunction; +pub use self::effects::{BoxShadow, Filter, SimpleShadow}; +pub use self::flex::FlexBasis; +pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle}; +pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; +pub use self::font::{FontSize, FontSizeAdjust, FontStretch, FontSynthesis, LineHeight}; +pub use self::font::{FontVariantAlternates, FontWeight}; +pub use self::font::{FontVariantEastAsian, FontVariationSettings}; +pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale}; +pub use self::image::{Gradient, Image, ImageRendering, LineDirection}; +pub use self::length::{CSSPixelLength, NonNegativeLength}; +pub use self::length::{Length, LengthOrNumber, LengthPercentage, NonNegativeLengthOrNumber}; +pub use self::length::{LengthOrAuto, LengthPercentageOrAuto, MaxSize, Size}; +pub use self::length::{NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto}; +#[cfg(feature = "gecko")] +pub use self::list::ListStyleType; +pub use self::list::Quotes; +pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate}; +pub use self::outline::OutlineStyle; +pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize}; +pub use self::percentage::{NonNegativePercentage, Percentage}; +pub use self::position::AspectRatio; +pub use self::position::{ + GridAutoFlow, GridTemplateAreas, MasonryAutoFlow, Position, PositionOrAuto, ZIndex, +}; +pub use self::ratio::Ratio; +pub use self::rect::NonNegativeLengthOrNumberRect; +pub use self::resolution::Resolution; +pub use self::svg::{DProperty, MozContextProperties}; +pub use self::svg::{SVGLength, SVGOpacity, SVGPaint, SVGPaintKind}; +pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth}; +pub use self::text::HyphenateCharacter; +pub use self::text::TextUnderlinePosition; +pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextIndent}; +pub use self::text::{OverflowWrap, RubyPosition, TextOverflow, WordBreak, WordSpacing}; +pub use self::text::{TextAlign, TextAlignLast, TextEmphasisPosition, TextEmphasisStyle}; +pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify}; +pub use self::time::Time; +pub use self::transform::{Rotate, Scale, Transform, TransformBox, TransformOperation}; +pub use self::transform::{TransformOrigin, TransformStyle, Translate}; +#[cfg(feature = "gecko")] +pub use self::ui::CursorImage; +pub use self::ui::{BoolInteger, Cursor, UserSelect}; +pub use super::specified::TextTransform; +pub use super::specified::ViewportVariant; +pub use super::specified::{BorderStyle, TextDecorationLine}; +pub use app_units::Au; + +#[cfg(feature = "gecko")] +pub mod align; +pub mod angle; +pub mod animation; +pub mod background; +pub mod basic_shape; +pub mod border; +#[path = "box.rs"] +pub mod box_; +pub mod color; +pub mod column; +pub mod counters; +pub mod easing; +pub mod effects; +pub mod flex; +pub mod font; +pub mod image; +pub mod length; +pub mod length_percentage; +pub mod list; +pub mod motion; +pub mod outline; +pub mod page; +pub mod percentage; +pub mod position; +pub mod ratio; +pub mod rect; +pub mod resolution; +pub mod svg; +pub mod table; +pub mod text; +pub mod time; +pub mod transform; +pub mod ui; +pub mod url; + +/// A `Context` is all the data a specified value could ever need to compute +/// itself and be transformed to a computed value. +pub struct Context<'a> { + /// Values accessed through this need to be in the properties "computed + /// early": color, text-decoration, font-size, display, position, float, + /// border-*-style, outline-style, font-family, writing-mode... + pub builder: StyleBuilder<'a>, + + /// A cached computed system font value, for use by gecko. + /// + /// See properties/longhands/font.mako.rs + #[cfg(feature = "gecko")] + pub cached_system_font: Option, + + /// A dummy option for servo so initializing a computed::Context isn't + /// painful. + /// + /// TODO(emilio): Make constructors for Context, and drop this. + #[cfg(feature = "servo")] + pub cached_system_font: Option<()>, + + /// Whether or not we are computing the media list in a media query. + pub in_media_query: bool, + + /// Whether or not we are computing the container query condition. + pub in_container_query: bool, + + /// The quirks mode of this context. + pub quirks_mode: QuirksMode, + + /// Whether this computation is being done for a SMIL animation. + /// + /// This is used to allow certain properties to generate out-of-range + /// values, which SMIL allows. + pub for_smil_animation: bool, + + /// Returns the container information to evaluate a given container query. + pub container_info: Option, + + /// Whether we're computing a value for a non-inherited property. + /// False if we are computed a value for an inherited property or not computing for a property + /// at all (e.g. in a media query evaluation). + pub for_non_inherited_property: bool, + + /// The conditions to cache a rule node on the rule cache. + /// + /// FIXME(emilio): Drop the refcell. + pub rule_cache_conditions: RefCell<&'a mut RuleCacheConditions>, + + /// Container size query for this context. + container_size_query: RefCell>, +} + +impl<'a> Context<'a> { + /// Lazily evaluate the container size query, returning the result. + pub fn get_container_size_query(&self) -> ContainerSizeQueryResult { + let mut resolved = self.container_size_query.borrow_mut(); + resolved.get().clone() + } + + /// Creates a suitable context for media query evaluation, in which + /// font-relative units compute against the system_font, and executes `f` + /// with it. + pub fn for_media_query_evaluation(device: &Device, quirks_mode: QuirksMode, f: F) -> R + where + F: FnOnce(&Context) -> R, + { + let mut conditions = RuleCacheConditions::default(); + let context = Context { + builder: StyleBuilder::for_inheritance(device, None, None, None), + cached_system_font: None, + in_media_query: true, + in_container_query: false, + quirks_mode, + for_smil_animation: false, + container_info: None, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(&mut conditions), + container_size_query: RefCell::new(ContainerSizeQuery::none()), + }; + f(&context) + } + + /// Creates a suitable context for container query evaluation for the style + /// specified. + pub fn for_container_query_evaluation( + device: &Device, + stylist: Option<&Stylist>, + container_info_and_style: Option<(ContainerInfo, Arc)>, + container_size_query: ContainerSizeQuery, + f: F, + ) -> R + where + F: FnOnce(&Context) -> R, + { + let mut conditions = RuleCacheConditions::default(); + + let (container_info, style) = match container_info_and_style { + Some((ci, s)) => (Some(ci), Some(s)), + None => (None, None), + }; + + let style = style.as_ref().map(|s| &**s); + let quirks_mode = device.quirks_mode(); + let context = Context { + builder: StyleBuilder::for_inheritance(device, stylist, style, None), + cached_system_font: None, + in_media_query: false, + in_container_query: true, + quirks_mode, + for_smil_animation: false, + container_info, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(&mut conditions), + container_size_query: RefCell::new(container_size_query), + }; + + f(&context) + } + + /// Creates a context suitable for more general cases. + pub fn new( + builder: StyleBuilder<'a>, + quirks_mode: QuirksMode, + rule_cache_conditions: &'a mut RuleCacheConditions, + container_size_query: ContainerSizeQuery<'a>, + ) -> Self { + Self { + builder, + cached_system_font: None, + in_media_query: false, + in_container_query: false, + quirks_mode, + container_info: None, + for_smil_animation: false, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(rule_cache_conditions), + container_size_query: RefCell::new(container_size_query), + } + } + + /// Creates a context suitable for computing animations. + pub fn new_for_animation( + builder: StyleBuilder<'a>, + for_smil_animation: bool, + quirks_mode: QuirksMode, + rule_cache_conditions: &'a mut RuleCacheConditions, + container_size_query: ContainerSizeQuery<'a>, + ) -> Self { + Self { + builder, + cached_system_font: None, + in_media_query: false, + in_container_query: false, + quirks_mode, + container_info: None, + for_smil_animation, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(rule_cache_conditions), + container_size_query: RefCell::new(container_size_query), + } + } + + /// Creates a context suitable for computing the initial value of @property. + pub fn new_for_initial_at_property_value( + stylist: &'a Stylist, + rule_cache_conditions: &'a mut RuleCacheConditions, + ) -> Self { + Self { + builder: StyleBuilder::new(stylist.device(), Some(stylist), None, None, None, false), + cached_system_font: None, + // Because font-relative values are disallowed in @property initial values, we do not + // need to keep track of whether we're in a media query, whether we're in a container + // query, and so on. + in_media_query: false, + in_container_query: false, + quirks_mode: stylist.quirks_mode(), + container_info: None, + for_smil_animation: false, + for_non_inherited_property: false, + rule_cache_conditions: RefCell::new(rule_cache_conditions), + container_size_query: RefCell::new(ContainerSizeQuery::none()), + } + } + + /// The current device. + pub fn device(&self) -> &Device { + self.builder.device + } + + /// Get the inherited custom properties map. + pub fn inherited_custom_properties(&self) -> &ComputedCustomProperties { + &self.builder.inherited_custom_properties() + } + + /// Whether the style is for the root element. + pub fn is_root_element(&self) -> bool { + self.builder.is_root_element + } + + /// Queries font metrics. + pub fn query_font_metrics( + &self, + base_size: FontBaseSize, + orientation: FontMetricsOrientation, + retrieve_math_scales: bool, + ) -> FontMetrics { + if self.for_non_inherited_property { + self.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + self.builder.add_flags(match base_size { + FontBaseSize::CurrentStyle => ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS, + FontBaseSize::InheritedStyle => ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS, + }); + let size = base_size.resolve(self).used_size(); + let style = self.style(); + + let (wm, font) = match base_size { + FontBaseSize::CurrentStyle => (style.writing_mode, style.get_font()), + // This is only used for font-size computation. + FontBaseSize::InheritedStyle => { + (*style.inherited_writing_mode(), style.get_parent_font()) + }, + }; + + let vertical = match orientation { + FontMetricsOrientation::MatchContextPreferHorizontal => { + wm.is_vertical() && wm.is_upright() + }, + FontMetricsOrientation::MatchContextPreferVertical => wm.is_text_vertical(), + FontMetricsOrientation::Horizontal => false, + }; + self.device().query_font_metrics( + vertical, + font, + size, + self.in_media_or_container_query(), + retrieve_math_scales, + ) + } + + /// The current viewport size, used to resolve viewport units. + pub fn viewport_size_for_viewport_unit_resolution( + &self, + variant: ViewportVariant, + ) -> default::Size2D { + self.builder + .add_flags(ComputedValueFlags::USES_VIEWPORT_UNITS); + self.builder + .device + .au_viewport_size_for_viewport_unit_resolution(variant) + } + + /// Whether we're in a media or container query. + pub fn in_media_or_container_query(&self) -> bool { + self.in_media_query || self.in_container_query + } + + /// The default computed style we're getting our reset style from. + pub fn default_style(&self) -> &ComputedValues { + self.builder.default_style() + } + + /// The current style. + pub fn style(&self) -> &StyleBuilder { + &self.builder + } + + /// Apply text-zoom if enabled. + #[cfg(feature = "gecko")] + pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength { + if self + .style() + .get_font() + .clone__x_text_scale() + .text_zoom_enabled() + { + self.device().zoom_text(size) + } else { + size + } + } + + /// (Servo doesn't do text-zoom) + #[cfg(feature = "servo")] + pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength { + size + } +} + +/// An iterator over a slice of computed values +#[derive(Clone)] +pub struct ComputedVecIter<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> { + cx: &'cx Context<'cx_a>, + values: &'a [S], +} + +impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ComputedVecIter<'a, 'cx, 'cx_a, S> { + /// Construct an iterator from a slice of specified values and a context + pub fn new(cx: &'cx Context<'cx_a>, values: &'a [S]) -> Self { + ComputedVecIter { cx, values } + } +} + +impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ExactSizeIterator + for ComputedVecIter<'a, 'cx, 'cx_a, S> +{ + fn len(&self) -> usize { + self.values.len() + } +} + +impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> Iterator for ComputedVecIter<'a, 'cx, 'cx_a, S> { + type Item = S::ComputedValue; + fn next(&mut self) -> Option { + 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) { + (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 ToComputedValue for (A, B) +where + A: ToComputedValue, + B: ToComputedValue, +{ + type ComputedValue = ( + ::ComputedValue, + ::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 ToComputedValue for Option +where + T: ToComputedValue, +{ + type ComputedValue = Option<::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 ToComputedValue for default::Size2D +where + T: ToComputedValue, +{ + type ComputedValue = default::Size2D<::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 ToComputedValue for Vec +where + T: ToComputedValue, +{ + type ComputedValue = Vec<::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 ToComputedValue for Box +where + T: ToComputedValue, +{ + type ComputedValue = Box<::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 ToComputedValue for Box<[T]> +where + T: ToComputedValue, +{ + type ComputedValue = Box<[::ComputedValue]>; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + self.iter() + .map(|item| item.to_computed_value(context)) + .collect::>() + .into_boxed_slice() + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + computed + .iter() + .map(T::from_computed_value) + .collect::>() + .into_boxed_slice() + } +} + +impl ToComputedValue for crate::OwnedSlice +where + T: ToComputedValue, +{ + type ComputedValue = crate::OwnedSlice<::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 ToComputedValue for Arc +where + T: ToComputedValue, +{ + 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 ToComputedValue for ArcSlice +where + T: ToComputedValue, +{ + type ComputedValue = Self; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self { + self.clone() + } + + #[inline] + fn from_computed_value(computed: &Self) -> Self { + computed.clone() + } +} + +trivial_to_computed_value!(()); +trivial_to_computed_value!(bool); +trivial_to_computed_value!(f32); +trivial_to_computed_value!(i32); +trivial_to_computed_value!(u8); +trivial_to_computed_value!(u16); +trivial_to_computed_value!(u32); +trivial_to_computed_value!(usize); +trivial_to_computed_value!(Atom); +trivial_to_computed_value!(crate::values::AtomIdent); +#[cfg(feature = "servo")] +trivial_to_computed_value!(crate::Namespace); +#[cfg(feature = "servo")] +trivial_to_computed_value!(crate::Prefix); +trivial_to_computed_value!(String); +trivial_to_computed_value!(Box); +trivial_to_computed_value!(crate::OwnedStr); +trivial_to_computed_value!(style_traits::values::specified::AllowedNumericType); +trivial_to_computed_value!(crate::values::generics::color::ColorMixFlags); + +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[repr(C, u8)] +pub enum AngleOrPercentage { + Percentage(Percentage), + Angle(Angle), +} + +impl ToComputedValue for specified::AngleOrPercentage { + type ComputedValue = AngleOrPercentage; + + #[inline] + fn to_computed_value(&self, context: &Context) -> AngleOrPercentage { + match *self { + specified::AngleOrPercentage::Percentage(percentage) => { + AngleOrPercentage::Percentage(percentage.to_computed_value(context)) + }, + specified::AngleOrPercentage::Angle(angle) => { + AngleOrPercentage::Angle(angle.to_computed_value(context)) + }, + } + } + #[inline] + fn from_computed_value(computed: &AngleOrPercentage) -> Self { + match *computed { + AngleOrPercentage::Percentage(percentage) => specified::AngleOrPercentage::Percentage( + ToComputedValue::from_computed_value(&percentage), + ), + AngleOrPercentage::Angle(angle) => { + specified::AngleOrPercentage::Angle(ToComputedValue::from_computed_value(&angle)) + }, + } + } +} + +/// A `` 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; + +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 for NonNegativeNumber { + #[inline] + fn from(number: CSSFloat) -> NonNegativeNumber { + NonNegative::(number) + } +} + +impl From 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; + +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 for ZeroToOneNumber { + #[inline] + fn from(number: CSSFloat) -> Self { + Self(number) + } +} + +/// A wrapper of Number, but the value >= 1. +pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne; + +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 for GreaterThanOrEqualToOneNumber { + #[inline] + fn from(number: CSSFloat) -> GreaterThanOrEqualToOneNumber { + GreaterThanOrEqualToOne::(number) + } +} + +impl From 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 . +pub type NonNegativeNumberOrPercentage = NonNegative; + +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 `` value. +pub type Integer = CSSInteger; + +/// A wrapper of Integer, but only accept a value >= 1. +pub type PositiveInteger = GreaterThanOrEqualToOne; + +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 for PositiveInteger { + #[inline] + fn from(int: CSSInteger) -> PositiveInteger { + GreaterThanOrEqualToOne::(int) + } +} + +/// rect(...) | auto +pub type ClipRect = generics::GenericClipRect; + +/// rect(...) | auto +pub type ClipRectOrAuto = generics::GenericClipRectOrAuto; + +/// The computed value of a grid `` +pub type TrackBreadth = GenericTrackBreadth; + +/// The computed value of a grid `` +pub type TrackSize = GenericTrackSize; + +/// The computed value of a grid `+` +pub type ImplicitGridTracks = GenericImplicitGridTracks; + +/// The computed value of a grid `` +/// (could also be `` or ``) +pub type TrackList = GenericTrackList; + +/// The computed value of a ``. +pub type GridLine = GenericGridLine; + +/// ` | ` +pub type GridTemplateComponent = GenericGridTemplateComponent; + +impl ClipRect { + /// Given a border box, resolves the clip rect against the border box + /// in the same space the border box is in + pub fn for_border_rect + Add + Sub, U>( + &self, + border_box: Rect, + ) -> Rect { + fn extract_clip_component>(p: &LengthOrAuto, or: T) -> T { + match *p { + LengthOrAuto::Auto => or, + LengthOrAuto::LengthPercentage(ref length) => T::from(*length), + } + } + + let clip_origin = Point2D::new( + From::from(self.left.auto_is(|| Length::new(0.))), + From::from(self.top.auto_is(|| Length::new(0.))), + ); + let right = extract_clip_component(&self.right, border_box.size.width); + let bottom = extract_clip_component(&self.bottom, border_box.size.height); + let clip_size = Size2D::new(right - clip_origin.x, bottom - clip_origin.y); + + Rect::new(clip_origin, clip_size).translate(border_box.origin.to_vector()) + } +} diff --git a/servo/components/style/values/computed/motion.rs b/servo/components/style/values/computed/motion.rs new file mode 100644 index 0000000000..37b9f4909e --- /dev/null +++ b/servo/components/style/values/computed/motion.rs @@ -0,0 +1,70 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values that are related to motion path. + +use crate::values::computed::basic_shape::BasicShape; +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{Angle, LengthPercentage, Position}; +use crate::values::generics::motion::{ + GenericOffsetPath, GenericOffsetPathFunction, GenericOffsetPosition, GenericRayFunction, +}; +use crate::Zero; + +/// The computed value of ray() function. +pub type RayFunction = GenericRayFunction; + +/// The computed value of . +pub type OffsetPathFunction = GenericOffsetPathFunction; + +/// The computed value of `offset-path`. +pub type OffsetPath = GenericOffsetPath; + +/// The computed value of `offset-position`. +pub type OffsetPosition = GenericOffsetPosition; + +#[inline] +fn is_auto_zero_angle(auto: &bool, angle: &Angle) -> bool { + *auto && angle.is_zero() +} + +/// A computed offset-rotate. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + ToAnimatedZero, + ToCss, + ToResolvedValue, +)] +#[repr(C)] +pub struct OffsetRotate { + /// If auto is false, this is a fixed angle which indicates a + /// constant clockwise rotation transformation applied to it by this + /// specified rotation angle. Otherwise, the angle will be added to + /// the angle of the direction in layout. + #[animation(constant)] + #[css(represents_keyword)] + pub auto: bool, + /// The angle value. + #[css(contextual_skip_if = "is_auto_zero_angle")] + pub angle: Angle, +} + +impl OffsetRotate { + /// Returns "auto 0deg". + #[inline] + pub fn auto() -> Self { + OffsetRotate { + auto: true, + angle: Zero::zero(), + } + } +} diff --git a/servo/components/style/values/computed/outline.rs b/servo/components/style/values/computed/outline.rs new file mode 100644 index 0000000000..f872176529 --- /dev/null +++ b/servo/components/style/values/computed/outline.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed values for outline properties + +pub use crate::values::specified::OutlineStyle; diff --git a/servo/components/style/values/computed/page.rs b/servo/components/style/values/computed/page.rs new file mode 100644 index 0000000000..6f71c912cf --- /dev/null +++ b/servo/components/style/values/computed/page.rs @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed @page at-rule properties and named-page style properties + +use crate::values::computed::length::NonNegativeLength; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics; +use crate::values::generics::size::Size2D; + +use crate::values::specified::page as specified; +pub use generics::page::GenericPageSize; +pub use generics::page::PageOrientation; +pub use generics::page::PageSizeOrientation; +pub use generics::page::PaperSize; +pub use specified::PageName; + +/// Computed value of the @page size descriptor +/// +/// The spec says that the computed value should be the same as the specified +/// value but with all absolute units, but it's not currently possibly observe +/// the computed value of page-size. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)] +#[repr(C, u8)] +pub enum PageSize { + /// Specified size, paper size, or paper size and orientation. + Size(Size2D), + /// `landscape` or `portrait` value, no specified size. + Orientation(PageSizeOrientation), + /// `auto` value + Auto, +} + +impl ToComputedValue for specified::PageSize { + type ComputedValue = PageSize; + + fn to_computed_value(&self, ctx: &Context) -> Self::ComputedValue { + match &*self { + Self::Size(s) => PageSize::Size(s.to_computed_value(ctx)), + Self::PaperSize(p, PageSizeOrientation::Landscape) => PageSize::Size(Size2D { + width: p.long_edge().to_computed_value(ctx), + height: p.short_edge().to_computed_value(ctx), + }), + Self::PaperSize(p, PageSizeOrientation::Portrait) => PageSize::Size(Size2D { + width: p.short_edge().to_computed_value(ctx), + height: p.long_edge().to_computed_value(ctx), + }), + Self::Orientation(o) => PageSize::Orientation(*o), + Self::Auto => PageSize::Auto, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + PageSize::Size(s) => Self::Size(ToComputedValue::from_computed_value(&s)), + PageSize::Orientation(o) => Self::Orientation(o), + PageSize::Auto => Self::Auto, + } + } +} + +impl PageSize { + /// `auto` value. + #[inline] + pub fn auto() -> Self { + PageSize::Auto + } + + /// Whether this is the `auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, PageSize::Auto) + } +} diff --git a/servo/components/style/values/computed/percentage.rs b/servo/components/style/values/computed/percentage.rs new file mode 100644 index 0000000000..994c01594a --- /dev/null +++ b/servo/components/style/values/computed/percentage.rs @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed percentages. + +use crate::values::animated::ToAnimatedValue; +use crate::values::generics::NonNegative; +use crate::values::specified::percentage::ToPercentage; +use crate::values::{serialize_normalized_percentage, CSSFloat}; +use crate::Zero; +use std::fmt; +use style_traits::{CssWriter, ToCss}; + +/// A computed percentage. +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Default, + Deserialize, + MallocSizeOf, + PartialEq, + PartialOrd, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Percentage(pub CSSFloat); + +impl Percentage { + /// 100% + #[inline] + pub fn hundred() -> Self { + Percentage(1.) + } + + /// Returns the absolute value for this percentage. + #[inline] + pub fn abs(&self) -> Self { + Percentage(self.0.abs()) + } + + /// Clamps this percentage to a non-negative percentage. + #[inline] + pub fn clamp_to_non_negative(self) -> Self { + Percentage(self.0.max(0.)) + } +} + +impl Zero for Percentage { + fn zero() -> Self { + Percentage(0.) + } + + fn is_zero(&self) -> bool { + self.0 == 0. + } +} + +impl ToPercentage for Percentage { + fn to_percentage(&self) -> CSSFloat { + self.0 + } +} + +impl std::ops::AddAssign for Percentage { + fn add_assign(&mut self, other: Self) { + self.0 += other.0 + } +} + +impl std::ops::Add for Percentage { + type Output = Self; + + fn add(self, other: Self) -> Self { + Percentage(self.0 + other.0) + } +} + +impl std::ops::Sub for Percentage { + type Output = Self; + + fn sub(self, other: Self) -> Self { + Percentage(self.0 - other.0) + } +} + +impl std::ops::Rem for Percentage { + type Output = Self; + + fn rem(self, other: Self) -> Self { + Percentage(self.0 % other.0) + } +} + +impl ToCss for Percentage { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + serialize_normalized_percentage(self.0, dest) + } +} + +/// A wrapper over a `Percentage`, whose value should be clamped to 0. +pub type NonNegativePercentage = NonNegative; + +impl NonNegativePercentage { + /// 100% + #[inline] + pub fn hundred() -> Self { + NonNegative(Percentage::hundred()) + } +} + +impl ToAnimatedValue for NonNegativePercentage { + type AnimatedValue = Percentage; + + #[inline] + fn to_animated_value(self) -> Self::AnimatedValue { + self.0 + } + + #[inline] + fn from_animated_value(animated: Self::AnimatedValue) -> Self { + NonNegative(animated.clamp_to_non_negative()) + } +} diff --git a/servo/components/style/values/computed/position.rs b/servo/components/style/values/computed/position.rs new file mode 100644 index 0000000000..5a10c0f23d --- /dev/null +++ b/servo/components/style/values/computed/position.rs @@ -0,0 +1,74 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [`position`][position] values. +//! +//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position + +use crate::values::computed::{Integer, LengthPercentage, NonNegativeNumber, Percentage}; +use crate::values::generics::position::AspectRatio as GenericAspectRatio; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::position::PositionComponent as GenericPositionComponent; +use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto; +use crate::values::generics::position::ZIndex as GenericZIndex; +pub use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas, MasonryAutoFlow}; +use crate::Zero; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// The computed value of a CSS `` +pub type Position = GenericPosition; + +/// The computed value of an `auto | ` +pub type PositionOrAuto = GenericPositionOrAuto; + +/// 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.horizontal.to_css(dest)?; + dest.write_char(' ')?; + self.vertical.to_css(dest) + } +} + +impl GenericPositionComponent for LengthPercentage { + fn is_center(&self) -> bool { + match self.to_percentage() { + Some(Percentage(per)) => per == 0.5, + _ => false, + } + } +} + +/// A computed value for the `z-index` property. +pub type ZIndex = GenericZIndex; + +/// A computed value for the `aspect-ratio` property. +pub type AspectRatio = GenericAspectRatio; diff --git a/servo/components/style/values/computed/ratio.rs b/servo/components/style/values/computed/ratio.rs new file mode 100644 index 0000000000..ae8997cfc0 --- /dev/null +++ b/servo/components/style/values/computed/ratio.rs @@ -0,0 +1,115 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `` computed values. + +use crate::values::animated::{Animate, Procedure}; +use crate::values::computed::NonNegativeNumber; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::generics::ratio::Ratio as GenericRatio; +use crate::{One, Zero}; +use std::cmp::{Ordering, PartialOrd}; + +/// A computed value. +pub type Ratio = GenericRatio; + +impl PartialOrd for Ratio { + fn partial_cmp(&self, other: &Self) -> Option { + f64::partial_cmp( + &((self.0).0 as f64 * (other.1).0 as f64), + &((self.1).0 as f64 * (other.0).0 as f64), + ) + } +} + +/// https://drafts.csswg.org/css-values/#combine-ratio +impl Animate for Ratio { + fn animate(&self, other: &Self, procedure: Procedure) -> Result { + // If either is degenerate, the values cannot be interpolated. + if self.is_degenerate() || other.is_degenerate() { + return Err(()); + } + + // Addition of s is not possible, and based on + // https://drafts.csswg.org/css-values-4/#not-additive, + // we simply use the first value as the result value. + // Besides, the procedure for accumulation should be identical to addition here. + if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) { + return Ok(self.clone()); + } + + // The interpolation of a is defined by converting each to a number by + // dividing the first value by the second (so a ratio of 3 / 2 would become 1.5), taking + // the logarithm of that result (so the 1.5 would become approximately 0.176), then + // interpolating those values. + // + // The result during the interpolation is converted back to a by inverting the + // logarithm, then interpreting the result as a with the result as the first value + // and 1 as the second value. + let start = self.to_f32().ln(); + let end = other.to_f32().ln(); + let e = std::f32::consts::E; + let result = e.powf(start.animate(&end, procedure)?); + // The range of the result is [0, inf), based on the easing function. + if result.is_zero() || result.is_infinite() { + return Err(()); + } + Ok(Ratio::new(result, 1.0f32)) + } +} + +impl ComputeSquaredDistance for Ratio { + fn compute_squared_distance(&self, other: &Self) -> Result { + if self.is_degenerate() || other.is_degenerate() { + return Err(()); + } + // Use the distance of their logarithm values. (This is used by testing, so don't need to + // care about the base. Here we use the same base as that in animate().) + self.to_f32() + .ln() + .compute_squared_distance(&other.to_f32().ln()) + } +} + +impl Zero for Ratio { + fn zero() -> Self { + Self::new(Zero::zero(), One::one()) + } + + fn is_zero(&self) -> bool { + self.0.is_zero() + } +} + +impl Ratio { + /// Returns a new Ratio. + #[inline] + pub fn new(a: f32, b: f32) -> Self { + GenericRatio(a.into(), b.into()) + } + + /// Returns the used value. A ratio of 0/0 behaves as the ratio 1/0. + /// https://drafts.csswg.org/css-values-4/#ratios + pub fn used_value(self) -> Self { + if self.0.is_zero() && self.1.is_zero() { + Ratio::new(One::one(), Zero::zero()) + } else { + self + } + } + + /// Returns true if this is a degenerate ratio. + /// https://drafts.csswg.org/css-values/#degenerate-ratio + #[inline] + pub fn is_degenerate(&self) -> bool { + self.0.is_zero() || self.1.is_zero() + } + + /// Returns the f32 value by dividing the first value by the second one. + #[inline] + fn to_f32(&self) -> f32 { + debug_assert!(!self.is_degenerate()); + (self.0).0 / (self.1).0 + } +} diff --git a/servo/components/style/values/computed/rect.rs b/servo/components/style/values/computed/rect.rs new file mode 100644 index 0000000000..ec44360fc8 --- /dev/null +++ b/servo/components/style/values/computed/rect.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS borders. + +use crate::values::computed::length::NonNegativeLengthOrNumber; +use crate::values::generics::rect::Rect; + +/// A specified rectangle made of four `` values. +pub type NonNegativeLengthOrNumberRect = Rect; diff --git a/servo/components/style/values/computed/resolution.rs b/servo/components/style/values/computed/resolution.rs new file mode 100644 index 0000000000..430c80d211 --- /dev/null +++ b/servo/components/style/values/computed/resolution.rs @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Resolution values: +//! +//! https://drafts.csswg.org/css-values/#resolution + +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified; +use crate::values::CSSFloat; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A computed ``. +#[repr(C)] +#[derive(Animate, Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)] +pub struct Resolution(CSSFloat); + +impl Resolution { + /// Returns this resolution value as dppx. + #[inline] + pub fn dppx(&self) -> CSSFloat { + self.0 + } + + /// Return a computed `resolution` value from a dppx float value. + #[inline] + pub fn from_dppx(dppx: CSSFloat) -> Self { + Resolution(dppx) + } +} + +impl ToComputedValue for specified::Resolution { + type ComputedValue = Resolution; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + Resolution(crate::values::normalize(self.dppx().max(0.0))) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + specified::Resolution::from_dppx(computed.dppx()) + } +} + +impl ToCss for Resolution { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + self.dppx().to_css(dest)?; + dest.write_str("dppx") + } +} diff --git a/servo/components/style/values/computed/svg.rs b/servo/components/style/values/computed/svg.rs new file mode 100644 index 0000000000..6efdfca36b --- /dev/null +++ b/servo/components/style/values/computed/svg.rs @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for SVG properties. + +use crate::values::computed::color::Color; +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::{LengthPercentage, NonNegativeLengthPercentage, Opacity}; +use crate::values::generics::svg as generic; +use crate::Zero; + +pub use crate::values::specified::{DProperty, MozContextProperties, SVGPaintOrder}; + +/// Computed SVG Paint value +pub type SVGPaint = generic::GenericSVGPaint; + +/// Computed SVG Paint Kind value +pub type SVGPaintKind = generic::GenericSVGPaintKind; + +impl SVGPaint { + /// Opaque black color + pub const BLACK: Self = Self { + kind: generic::SVGPaintKind::Color(Color::BLACK), + fallback: generic::SVGPaintFallback::Unset, + }; +} + +/// | | | context-value +pub type SVGLength = generic::GenericSVGLength; + +impl SVGLength { + /// `0px` + pub fn zero() -> Self { + generic::SVGLength::LengthPercentage(LengthPercentage::zero()) + } +} + +/// An non-negative wrapper of SVGLength. +pub type SVGWidth = generic::GenericSVGLength; + +impl SVGWidth { + /// `1px`. + pub fn one() -> Self { + use crate::values::generics::NonNegative; + generic::SVGLength::LengthPercentage(NonNegative(LengthPercentage::one())) + } +} + +/// [ | | ]# | context-value +pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray; + +impl Default for SVGStrokeDashArray { + fn default() -> Self { + generic::SVGStrokeDashArray::Values(Default::default()) + } +} + +/// | context-fill-opacity | context-stroke-opacity +pub type SVGOpacity = generic::GenericSVGOpacity; + +impl Default for SVGOpacity { + fn default() -> Self { + generic::SVGOpacity::Opacity(1.) + } +} diff --git a/servo/components/style/values/computed/table.rs b/servo/components/style/values/computed/table.rs new file mode 100644 index 0000000000..47109e20ec --- /dev/null +++ b/servo/components/style/values/computed/table.rs @@ -0,0 +1,7 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for CSS values related to tables. + +pub use super::specified::table::CaptionSide; diff --git a/servo/components/style/values/computed/text.rs b/servo/components/style/values/computed/text.rs new file mode 100644 index 0000000000..a4fec654a5 --- /dev/null +++ b/servo/components/style/values/computed/text.rs @@ -0,0 +1,228 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed types for text properties. + +#[cfg(feature = "servo")] +use crate::properties::StyleBuilder; +use crate::values::computed::length::{Length, LengthPercentage}; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::text::InitialLetter as GenericInitialLetter; +use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing}; +use crate::values::specified::text::{self as specified, TextOverflowSide}; +use crate::values::specified::text::{TextEmphasisFillMode, TextEmphasisShapeKeyword}; +use crate::values::{CSSFloat, CSSInteger}; +use crate::Zero; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +pub use crate::values::specified::text::{ + MozControlCharacterVisibility, TextAlignLast, TextUnderlinePosition, +}; +pub use crate::values::specified::HyphenateCharacter; +pub use crate::values::specified::{LineBreak, OverflowWrap, RubyPosition, WordBreak}; +pub use crate::values::specified::{TextDecorationLine, TextEmphasisPosition}; +pub use crate::values::specified::{TextDecorationSkipInk, TextJustify, TextTransform}; + +/// A computed value for the `initial-letter` property. +pub type InitialLetter = GenericInitialLetter; + +/// Implements type for `text-decoration-thickness` property. +pub type TextDecorationLength = GenericTextDecorationLength; + +/// The computed value of `text-align`. +pub type TextAlign = specified::TextAlignKeyword; + +/// The computed value of `text-indent`. +pub type TextIndent = GenericTextIndent; + +/// 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + // https://drafts.csswg.org/css-text/#propdef-letter-spacing + // + // For legacy reasons, a computed letter-spacing of zero yields a + // resolved value (getComputedStyle() return value) of normal. + if self.0.is_zero() { + return dest.write_str("normal"); + } + self.0.to_css(dest) + } +} + +impl ToComputedValue for specified::LetterSpacing { + type ComputedValue = LetterSpacing; + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + Spacing::Normal => LetterSpacing(Length::zero()), + Spacing::Value(ref v) => LetterSpacing(v.to_computed_value(context)), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if computed.0.is_zero() { + return Spacing::Normal; + } + Spacing::Value(ToComputedValue::from_computed_value(&computed.0)) + } +} + +/// A computed value for the `word-spacing` property. +pub type WordSpacing = LengthPercentage; + +impl ToComputedValue for specified::WordSpacing { + type ComputedValue = WordSpacing; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + Spacing::Normal => LengthPercentage::zero(), + Spacing::Value(ref v) => v.to_computed_value(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Spacing::Value(ToComputedValue::from_computed_value(computed)) + } +} + +impl WordSpacing { + /// Return the `normal` computed value, which is just zero. + #[inline] + pub fn normal() -> Self { + LengthPercentage::zero() + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue)] +#[repr(C)] +/// text-overflow. +/// When the specified value only has one side, that's the "second" +/// side, and the sides are logical, so "second" means "end". The +/// start side is Clip in that case. +/// +/// When the specified value has two sides, those are our "first" +/// and "second" sides, and they are physical sides ("left" and +/// "right"). +pub struct TextOverflow { + /// First side + pub first: TextOverflowSide, + /// Second side + pub second: TextOverflowSide, + /// True if the specified value only has one side. + pub sides_are_logical: bool, +} + +impl TextOverflow { + /// Returns the initial `text-overflow` value + pub fn get_initial_value() -> TextOverflow { + TextOverflow { + first: TextOverflowSide::Clip, + second: TextOverflowSide::Clip, + sides_are_logical: true, + } + } +} + +impl ToCss for TextOverflow { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + if self.sides_are_logical { + debug_assert_eq!(self.first, TextOverflowSide::Clip); + self.second.to_css(dest)?; + } else { + self.first.to_css(dest)?; + dest.write_char(' ')?; + self.second.to_css(dest)?; + } + Ok(()) + } +} + +/// A struct that represents the _used_ value of the text-decoration property. +/// +/// FIXME(emilio): This is done at style resolution time, though probably should +/// be done at layout time, otherwise we need to account for display: contents +/// and similar stuff when we implement it. +/// +/// FIXME(emilio): Also, should be just a bitfield instead of three bytes. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToResolvedValue)] +pub struct TextDecorationsInEffect { + /// Whether an underline is in effect. + pub underline: bool, + /// Whether an overline decoration is in effect. + pub overline: bool, + /// Whether a line-through style is in effect. + pub line_through: bool, +} + +impl TextDecorationsInEffect { + /// Computes the text-decorations in effect for a given style. + #[cfg(feature = "servo")] + pub fn from_style(style: &StyleBuilder) -> Self { + // Start with no declarations if this is an atomic inline-level box; + // otherwise, start with the declarations in effect and add in the text + // decorations that this block specifies. + let mut result = if style.get_box().clone_display().is_atomic_inline_level() { + Self::default() + } else { + style + .get_parent_inherited_text() + .text_decorations_in_effect + .clone() + }; + + let line = style.get_text().clone_text_decoration_line(); + + result.underline |= line.contains(TextDecorationLine::UNDERLINE); + result.overline |= line.contains(TextDecorationLine::OVERLINE); + result.line_through |= line.contains(TextDecorationLine::LINE_THROUGH); + + result + } +} + +/// Computed value for the text-emphasis-style property +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)] +#[allow(missing_docs)] +#[repr(C, u8)] +pub enum TextEmphasisStyle { + /// [ || ] + Keyword { + #[css(skip_if = "TextEmphasisFillMode::is_filled")] + fill: TextEmphasisFillMode, + shape: TextEmphasisShapeKeyword, + }, + /// `none` + None, + /// `` (of which only the first grapheme cluster will be used). + String(crate::OwnedStr), +} diff --git a/servo/components/style/values/computed/time.rs b/servo/components/style/values/computed/time.rs new file mode 100644 index 0000000000..b81c6e879a --- /dev/null +++ b/servo/components/style/values/computed/time.rs @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Computed time values. + +use crate::values::CSSFloat; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ToCss}; + +/// A computed `::ResolvedValue, + ::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 ToResolvedValue for Option +where + T: ToResolvedValue, +{ + type ResolvedValue = Option<::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 ToResolvedValue for SmallVec<[T; 1]> +where + T: ToResolvedValue, +{ + type ResolvedValue = SmallVec<[::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 ToResolvedValue for Vec +where + T: ToResolvedValue, +{ + type ResolvedValue = Vec<::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 ToResolvedValue for Box +where + T: ToResolvedValue, +{ + type ResolvedValue = Box<::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 ToResolvedValue for Box<[T]> +where + T: ToResolvedValue, +{ + type ResolvedValue = Box<[::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 ToResolvedValue for crate::OwnedSlice +where + T: ToResolvedValue, +{ + type ResolvedValue = crate::OwnedSlice<::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 ToResolvedValue for Arc +where + T: ToResolvedValue, +{ + 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 ToResolvedValue for ArcSlice +where + T: ToResolvedValue, +{ + type ResolvedValue = Self; + + #[inline] + fn to_resolved_value(self, _: &Context) -> Self { + self + } + + #[inline] + fn from_resolved_value(resolved: Self) -> Self { + resolved + } +} diff --git a/servo/components/style/values/specified/align.rs b/servo/components/style/values/specified/align.rs new file mode 100644 index 0000000000..60eca4556b --- /dev/null +++ b/servo/components/style/values/specified/align.rs @@ -0,0 +1,820 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Values for CSS Box Alignment properties +//! +//! https://drafts.csswg.org/css-align/ + +use crate::parser::{Parse, ParserContext}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss}; + +/// Constants shared by multiple CSS Box Alignment properties +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(C)] +pub struct AlignFlags(u8); +bitflags! { + impl AlignFlags: u8 { + // Enumeration stored in the lower 5 bits: + /// {align,justify}-{content,items,self}: 'auto' + const AUTO = 0; + /// 'normal' + const NORMAL = 1; + /// 'start' + const START = 2; + /// 'end' + const END = 3; + /// 'flex-start' + const FLEX_START = 4; + /// 'flex-end' + const FLEX_END = 5; + /// 'center' + const CENTER = 6; + /// 'left' + const LEFT = 7; + /// 'right' + const RIGHT = 8; + /// 'baseline' + const BASELINE = 9; + /// 'last-baseline' + const LAST_BASELINE = 10; + /// 'stretch' + const STRETCH = 11; + /// 'self-start' + const SELF_START = 12; + /// 'self-end' + const SELF_END = 13; + /// 'space-between' + const SPACE_BETWEEN = 14; + /// 'space-around' + const SPACE_AROUND = 15; + /// 'space-evenly' + const SPACE_EVENLY = 16; + + // Additional flags stored in the upper bits: + /// 'legacy' (mutually exclusive w. SAFE & UNSAFE) + const LEGACY = 1 << 5; + /// 'safe' + const SAFE = 1 << 6; + /// 'unsafe' (mutually exclusive w. SAFE) + const UNSAFE = 1 << 7; + + /// Mask for the additional flags above. + const FLAG_BITS = 0b11100000; + } +} + +impl AlignFlags { + /// Returns the enumeration value stored in the lower 5 bits. + #[inline] + fn value(&self) -> Self { + *self & !AlignFlags::FLAG_BITS + } +} + +impl ToCss for AlignFlags { + fn to_css(&self, dest: &mut CssWriter) -> 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. +/// +/// +#[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 . + 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> { + // 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 , but only on the block axis. + if axis == AxisDirection::Block { + if let Ok(value) = input.try_parse(parse_baseline) { + return Ok(ContentDistribution::new(value)); + } + } + + // + if let Ok(value) = input.try_parse(parse_content_distribution) { + return Ok(ContentDistribution::new(value)); + } + + // ? + 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. +/// +/// +#[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> { + // 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. +/// +/// +#[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); + +impl Parse for AlignTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let values = input.parse_comma_separated(|input| AlignContent::parse(context, input))?; + Ok(AlignTracks(values.into())) + } +} + +/// Value for the `justify-content` property. +/// +/// +#[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> { + // 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. +/// +/// +#[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, +); + +impl Parse for JustifyTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let values = input.parse_comma_separated(|input| JustifyContent::parse(context, input))?; + Ok(JustifyTracks(values.into())) + } +} + +/// +#[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> { + // NOTE Please also update the `list_keywords` function below + // when this function is updated. + + // + // + // It's weird that this accepts , 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)); + } + + // ? + 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. +/// +/// +#[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> { + // 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. +/// +/// +#[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> { + // 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 +/// +/// +#[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 | | + // ? + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // + 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)); + } + // ? + 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 +/// +/// +#[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> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // + // + // It's weird that this accepts , 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)); + } + + // ? + 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> { + // 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> { + // 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"]); +} + +// +fn parse_baseline<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + // 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"]); +} + +// +fn parse_content_distribution<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result> { + // 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"]); +} + +// +fn parse_overflow_position<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result> { + // 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"]); +} + +// | left | right in the inline axis. +fn parse_self_position<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, +) -> Result> { + // 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> { + // 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> { + // NOTE Please also update the `list_legacy_keywords` function below + // when this function is updated. + let flags = try_match_ident_ignore_ascii_case! { input, + "legacy" => { + let flags = input.try_parse(parse_left_right_center) + .unwrap_or(AlignFlags::empty()); + + return Ok(AlignFlags::LEGACY | flags) + }, + "left" => AlignFlags::LEFT, + "right" => AlignFlags::RIGHT, + "center" => AlignFlags::CENTER, + }; + + input.expect_ident_matching("legacy")?; + Ok(AlignFlags::LEGACY | flags) +} + +fn list_legacy_keywords(f: KeywordsCollectFn) { + f(&["legacy", "left", "right", "center"]); +} diff --git a/servo/components/style/values/specified/angle.rs b/servo/components/style/values/specified/angle.rs new file mode 100644 index 0000000000..fb4554eb85 --- /dev/null +++ b/servo/components/style/values/specified/angle.rs @@ -0,0 +1,276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified angles. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::angle::Angle as ComputedAngle; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::CSSFloat; +use crate::Zero; +use cssparser::{Parser, Token}; +use std::f32::consts::PI; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss}; + +/// A specified angle dimension. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToCss, ToShmem)] +pub enum AngleDimension { + /// An angle with degree unit. + #[css(dimension)] + Deg(CSSFloat), + /// An angle with gradian unit. + #[css(dimension)] + Grad(CSSFloat), + /// An angle with radian unit. + #[css(dimension)] + Rad(CSSFloat), + /// An angle with turn unit. + #[css(dimension)] + Turn(CSSFloat), +} + +impl Zero for AngleDimension { + fn zero() -> Self { + AngleDimension::Deg(0.) + } + + fn is_zero(&self) -> bool { + self.unitless_value() == 0.0 + } +} + +impl AngleDimension { + /// Returns the amount of degrees this angle represents. + #[inline] + fn degrees(&self) -> CSSFloat { + const DEG_PER_RAD: f32 = 180.0 / PI; + const DEG_PER_TURN: f32 = 360.0; + const DEG_PER_GRAD: f32 = 180.0 / 200.0; + + match *self { + AngleDimension::Deg(d) => d, + AngleDimension::Rad(rad) => rad * DEG_PER_RAD, + AngleDimension::Turn(turns) => turns * DEG_PER_TURN, + AngleDimension::Grad(gradians) => gradians * DEG_PER_GRAD, + } + } + + fn unitless_value(&self) -> CSSFloat { + match *self { + AngleDimension::Deg(v) | + AngleDimension::Rad(v) | + AngleDimension::Turn(v) | + AngleDimension::Grad(v) => v, + } + } + + fn unit(&self) -> &'static str { + match *self { + AngleDimension::Deg(_) => "deg", + AngleDimension::Rad(_) => "rad", + AngleDimension::Turn(_) => "turn", + AngleDimension::Grad(_) => "grad", + } + } +} + +/// A specified Angle value, which is just the angle dimension, plus whether it +/// was specified as `calc()` or not. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Angle { + value: AngleDimension, + was_calc: bool, +} + +impl Zero for Angle { + fn zero() -> Self { + Self { + value: Zero::zero(), + was_calc: false, + } + } + + fn is_zero(&self) -> bool { + self.value.is_zero() + } +} + +impl ToCss for Angle { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.value.unitless_value(), + self.value.unit(), + self.was_calc, + dest, + ) + } +} + +impl ToComputedValue for Angle { + type ComputedValue = ComputedAngle; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + let degrees = self.degrees(); + + // NaN and +-infinity should degenerate to 0: https://github.com/w3c/csswg-drafts/issues/6105 + ComputedAngle::from_degrees(if degrees.is_finite() { degrees } else { 0.0 }) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Angle { + value: AngleDimension::Deg(computed.degrees()), + was_calc: false, + } + } +} + +impl Angle { + /// Creates an angle with the given value in degrees. + #[inline] + pub fn from_degrees(value: CSSFloat, was_calc: bool) -> Self { + Angle { + value: AngleDimension::Deg(value), + was_calc, + } + } + + /// Creates an angle with the given value in radians. + #[inline] + pub fn from_radians(value: CSSFloat) -> Self { + Angle { + value: AngleDimension::Rad(value), + was_calc: false, + } + } + + /// Return `0deg`. + pub fn zero() -> Self { + Self::from_degrees(0.0, false) + } + + /// Returns the value of the angle in degrees, mostly for `calc()`. + #[inline] + pub fn degrees(&self) -> CSSFloat { + self.value.degrees() + } + + /// Returns the value of the angle in radians. + #[inline] + pub fn radians(&self) -> CSSFloat { + const RAD_PER_DEG: f32 = PI / 180.0; + self.value.degrees() * RAD_PER_DEG + } + + /// Whether this specified angle came from a `calc()` expression. + #[inline] + pub fn was_calc(&self) -> bool { + self.was_calc + } + + /// Returns an `Angle` parsed from a `calc()` expression. + pub fn from_calc(degrees: CSSFloat) -> Self { + Angle { + value: AngleDimension::Deg(degrees), + was_calc: true, + } + } + + /// Returns the unit of the angle. + #[inline] + pub fn unit(&self) -> &'static str { + self.value.unit() + } +} + +/// Whether to allow parsing an unitless zero as a valid angle. +/// +/// This should always be `No`, except for exceptions like: +/// +/// https://github.com/w3c/fxtf-drafts/issues/228 +/// +/// See also: https://github.com/w3c/csswg-drafts/issues/1162. +#[allow(missing_docs)] +pub enum AllowUnitlessZeroAngle { + Yes, + No, +} + +impl Parse for Angle { + /// Parses an angle according to CSS-VALUES § 6.1. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Self::parse_internal(context, input, AllowUnitlessZeroAngle::No) + } +} + +impl Angle { + /// Parse an `` value given a value and an unit. + pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result { + 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 `` 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::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> { + let location = input.current_source_location(); + let t = input.next()?; + let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes); + match *t { + Token::Dimension { + value, ref unit, .. + } => { + match Angle::parse_dimension(value, unit, /* from_calc = */ false) { + Ok(angle) => Ok(angle), + Err(()) => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse_angle(context, input, function) + }, + Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()), + ref t => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + } +} + +impl SpecifiedValueInfo for Angle {} diff --git a/servo/components/style/values/specified/animation.rs b/servo/components/style/values/specified/animation.rs new file mode 100644 index 0000000000..e7bbf26fb3 --- /dev/null +++ b/servo/components/style/values/specified/animation.rs @@ -0,0 +1,463 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for properties related to animations and transitions. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::{NonCustomPropertyId, PropertyId, ShorthandId}; +use crate::values::generics::animation as generics; +use crate::values::specified::{LengthPercentage, NonNegativeNumber}; +use crate::values::{CustomIdent, KeyframesName, TimelineName}; +use crate::Atom; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{ + CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss, +}; + +/// A given transition property, that is either `All`, a longhand or shorthand +/// property, or an unsupported or custom property. +#[derive( + Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(u8)] +pub enum TransitionProperty { + /// A non-custom property. + NonCustom(NonCustomPropertyId), + /// A custom property. + Custom(Atom), + /// Unrecognized property which could be any non-transitionable, custom property, or + /// unknown property. + Unsupported(CustomIdent), +} + +impl ToCss for TransitionProperty { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + match *self { + TransitionProperty::NonCustom(ref id) => id.to_css(dest), + TransitionProperty::Custom(ref name) => { + dest.write_str("--")?; + crate::values::serialize_atom_name(name, dest) + }, + TransitionProperty::Unsupported(ref i) => i.to_css(dest), + } + } +} + +impl Parse for TransitionProperty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + let id = match PropertyId::parse_ignoring_rule_type(&ident, context) { + Ok(id) => id, + Err(..) => { + // None is not acceptable as a single transition-property. + return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident( + location, + ident, + &["none"], + )?)); + }, + }; + + Ok(match id { + PropertyId::NonCustom(id) => TransitionProperty::NonCustom(id.unaliased()), + PropertyId::Custom(name) => TransitionProperty::Custom(name), + }) + } +} + +impl SpecifiedValueInfo for TransitionProperty { + fn collect_completion_keywords(f: KeywordsCollectFn) { + // `transition-property` can actually accept all properties and + // arbitrary identifiers, but `all` is a special one we'd like + // to list. + f(&["all"]); + } +} + +impl TransitionProperty { + /// Returns the `none` value. + #[inline] + pub fn none() -> Self { + TransitionProperty::Unsupported(CustomIdent(atom!("none"))) + } + + /// Returns whether we're the `none` value. + #[inline] + pub fn is_none(&self) -> bool { + matches!(*self, TransitionProperty::Unsupported(ref ident) if ident.0 == atom!("none")) + } + + /// Returns `all`. + #[inline] + pub fn all() -> Self { + TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(ShorthandId::All)) + } + + /// Returns true if it is `all`. + #[inline] + pub fn is_all(&self) -> bool { + self == &TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand( + ShorthandId::All, + )) + } +} + +/// https://drafts.csswg.org/css-animations/#animation-iteration-count +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AnimationIterationCount { + /// A `` value. + Number(NonNegativeNumber), + /// The `infinite` keyword. + Infinite, +} + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + Self::Number(NonNegativeNumber::new(1.0)) + } +} + +/// A value for the `animation-name` property. +#[derive( + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "none")] +#[repr(C)] +pub struct AnimationName(pub KeyframesName); + +impl AnimationName { + /// Get the name of the animation as an `Atom`. + pub fn as_atom(&self) -> Option<&Atom> { + if self.is_none() { + return None; + } + Some(self.0.as_atom()) + } + + /// Returns the `none` value. + pub fn none() -> Self { + AnimationName(KeyframesName::none()) + } + + /// Returns whether this is the none value. + pub fn is_none(&self) -> bool { + self.0.is_none() + } +} + +impl Parse for AnimationName { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) { + return Ok(AnimationName(name)); + } + + input.expect_ident_matching("none")?; + Ok(AnimationName(KeyframesName::none())) + } +} + +/// https://drafts.csswg.org/css-animations/#propdef-animation-direction +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationDirection { + Normal, + Reverse, + Alternate, + AlternateReverse, +} + +/// https://drafts.csswg.org/css-animations/#animation-play-state +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationPlayState { + Running, + Paused, +} + +/// https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationFillMode { + None, + Forwards, + Backwards, + Both, +} + +/// https://drafts.csswg.org/css-animations-2/#animation-composition +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationComposition { + Replace, + Add, + Accumulate, +} + +/// A value for the used in scroll(). +/// +/// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller +#[derive( + Copy, + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Scroller { + /// The nearest ancestor scroll container. (Default.) + Nearest, + /// The document viewport as the scroll container. + Root, + /// Specifies to use the element’s own principal box as the scroll container. + #[css(keyword = "self")] + SelfElement, +} + +impl Scroller { + /// Returns true if it is default. + #[inline] + fn is_default(&self) -> bool { + matches!(*self, Self::Nearest) + } +} + +impl Default for Scroller { + fn default() -> Self { + Self::Nearest + } +} + +/// A value for the used in scroll(), or a value for {scroll|view}-timeline-axis. +/// +/// https://drafts.csswg.org/scroll-animations-1/#typedef-axis +/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis +/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis +#[derive( + Copy, + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollAxis { + /// The block axis of the scroll container. (Default.) + Block = 0, + /// The inline axis of the scroll container. + Inline = 1, + /// The vertical block axis of the scroll container. + Vertical = 2, + /// The horizontal axis of the scroll container. + Horizontal = 3, +} + +impl ScrollAxis { + /// Returns true if it is default. + #[inline] + pub fn is_default(&self) -> bool { + matches!(*self, Self::Block) + } +} + +impl Default for ScrollAxis { + fn default() -> Self { + Self::Block + } +} + +/// The scroll() notation. +/// https://drafts.csswg.org/scroll-animations-1/#scroll-notation +#[derive( + Copy, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(function = "scroll")] +#[repr(C)] +pub struct ScrollFunction { + /// The scroll container element whose scroll position drives the progress of the timeline. + #[css(skip_if = "Scroller::is_default")] + pub scroller: Scroller, + /// The axis of scrolling that drives the progress of the timeline. + #[css(skip_if = "ScrollAxis::is_default")] + pub axis: ScrollAxis, +} + +impl ScrollFunction { + /// Parse the inner function arguments of `scroll()`. + fn parse_arguments<'i, 't>(input: &mut Parser<'i, 't>) -> Result> { + // = scroll( [ || ]? ) + // https://drafts.csswg.org/scroll-animations-1/#funcdef-scroll + let mut scroller = None; + let mut axis = None; + loop { + if scroller.is_none() { + scroller = input.try_parse(Scroller::parse).ok(); + } + + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + if axis.is_some() { + continue; + } + } + break; + } + + Ok(Self { + scroller: scroller.unwrap_or_default(), + axis: axis.unwrap_or_default(), + }) + } +} + +impl generics::ViewFunction { + /// Parse the inner function arguments of `view()`. + fn parse_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // = view( [ || <'view-timeline-inset'> ]? ) + // https://drafts.csswg.org/scroll-animations-1/#funcdef-view + let mut axis = None; + let mut inset = None; + loop { + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + } + + if inset.is_none() { + inset = input + .try_parse(|i| ViewTimelineInset::parse(context, i)) + .ok(); + if inset.is_some() { + continue; + } + } + break; + } + + Ok(Self { + inset: inset.unwrap_or_default(), + axis: axis.unwrap_or_default(), + }) + } +} + +/// A specified value for the `animation-timeline` property. +pub type AnimationTimeline = generics::GenericAnimationTimeline; + +impl Parse for AnimationTimeline { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + use crate::values::generics::animation::ViewFunction; + + // = auto | none | | | + // https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(Self::Auto); + } + + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(AnimationTimeline::Timeline(TimelineName::none())); + } + + if let Ok(name) = input.try_parse(|i| TimelineName::parse(context, i)) { + return Ok(AnimationTimeline::Timeline(name)); + } + + // Parse possible functions + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "scroll" => ScrollFunction::parse_arguments(i).map(Self::Scroll), + "view" => ViewFunction::parse_arguments(context, i).map(Self::View), + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + }) + } +} + +/// A value for the scroll-timeline-name or view-timeline-name. +pub type ScrollTimelineName = AnimationName; + +/// A specified value for the `view-timeline-inset` property. +pub type ViewTimelineInset = generics::GenericViewTimelineInset; + +impl Parse for ViewTimelineInset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + use crate::values::specified::LengthPercentageOrAuto; + + let start = LengthPercentageOrAuto::parse(context, input)?; + let end = match input.try_parse(|input| LengthPercentageOrAuto::parse(context, input)) { + Ok(end) => end, + Err(_) => start.clone(), + }; + + Ok(Self { start, end }) + } +} diff --git a/servo/components/style/values/specified/background.rs b/servo/components/style/values/specified/background.rs new file mode 100644 index 0000000000..39a5a85193 --- /dev/null +++ b/servo/components/style/values/specified/background.rs @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to backgrounds. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::background::BackgroundSize as GenericBackgroundSize; +use crate::values::specified::length::{ + NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A specified value for the `background-size` property. +pub type BackgroundSize = GenericBackgroundSize; + +impl Parse for BackgroundSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + match (self.0, self.1) { + (BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat) => { + dest.write_str("repeat-x") + }, + (BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat) => { + dest.write_str("repeat-y") + }, + (horizontal, vertical) => { + horizontal.to_css(dest)?; + if horizontal != vertical { + dest.write_char(' ')?; + vertical.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for BackgroundRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let ident = input.expect_ident_cloned()?; + + match_ignore_ascii_case! { &ident, + "repeat-x" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat)); + }, + "repeat-y" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat)); + }, + _ => {}, + } + + let horizontal = match BackgroundRepeatKeyword::from_ident(&ident) { + Ok(h) => h, + Err(()) => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())) + ); + }, + }; + + let vertical = input.try_parse(BackgroundRepeatKeyword::parse).ok(); + Ok(BackgroundRepeat(horizontal, vertical.unwrap_or(horizontal))) + } +} diff --git a/servo/components/style/values/specified/basic_shape.rs b/servo/components/style/values/specified/basic_shape.rs new file mode 100644 index 0000000000..526296b735 --- /dev/null +++ b/servo/components/style/values/specified/basic_shape.rs @@ -0,0 +1,719 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the specified value of +//! [`basic-shape`][basic-shape]s +//! +//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::basic_shape as generic; +use crate::values::generics::basic_shape::{Path, PolygonCoord}; +use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto}; +use crate::values::generics::rect::Rect; +use crate::values::specified::border::BorderRadius; +use crate::values::specified::image::Image; +use crate::values::specified::length::LengthPercentageOrAuto; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData}; +use crate::Zero; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A specified alias for FillRule. +pub use crate::values::generics::basic_shape::FillRule; + +/// A specified `clip-path` value. +pub type ClipPath = generic::GenericClipPath; + +/// A specified `shape-outside` value. +pub type ShapeOutside = generic::GenericShapeOutside; + +/// A specified value for `at ` in circle() and ellipse(). +// Note: its computed value is the same as computed::position::Position. We just want to always use +// LengthPercentage as the type of its components, for basic shapes. +pub type ShapePosition = GenericPosition; + +/// A specified basic shape. +pub type BasicShape = generic::GenericBasicShape< + ShapePosition, + LengthPercentage, + NonNegativeLengthPercentage, + BasicShapeRect, +>; + +/// The specified value of `inset()`. +pub type InsetRect = generic::GenericInsetRect; + +/// A specified circle. +pub type Circle = generic::Circle; + +/// A specified ellipse. +pub type Ellipse = generic::Ellipse; + +/// The specified value of `ShapeRadius`. +pub type ShapeRadius = generic::ShapeRadius; + +/// The specified value of `Polygon`. +pub type Polygon = generic::GenericPolygon; + +/// The specified value of `xywh()`. +/// Defines a rectangle via offsets from the top and left edge of the reference box, and a +/// specified width and height. +/// +/// The four s define, respectively, the inset from the left edge of the +/// reference box, the inset from the top edge of the reference box, the width of the rectangle, +/// and the height of the rectangle. +/// +/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +pub struct Xywh { + /// The left edge of the reference box. + pub x: LengthPercentage, + /// The top edge of the reference box. + pub y: LengthPercentage, + /// The specified width. + pub width: NonNegativeLengthPercentage, + /// The specified height. + pub height: NonNegativeLengthPercentage, + /// The optional argument(s) define rounded corners for the inset rectangle + /// using the border-radius shorthand syntax. + pub round: BorderRadius, +} + +/// Defines a rectangle via insets from the top and left edges of the reference box. +/// +/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +#[repr(C)] +pub struct ShapeRectFunction { + /// The four s define the position of the top, right, bottom, and left edges + /// of a rectangle, respectively, as insets from the top edge of the reference box (for the + /// first and third values) or the left edge of the reference box (for the second and fourth + /// values). + /// + /// An auto value makes the edge of the box coincide with the corresponding edge of the + /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and + /// equivalent to 100% as the second (right) or third (bottom) value. + pub rect: Rect, + /// The optional argument(s) define rounded corners for the inset rectangle + /// using the border-radius shorthand syntax. + pub round: BorderRadius, +} + +/// The specified value of . +/// = | | +/// +/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum BasicShapeRect { + /// Defines an inset rectangle via insets from each edge of the reference box. + Inset(InsetRect), + /// Defines a xywh function. + #[css(function)] + Xywh(Xywh), + /// Defines a rect function. + #[css(function)] + Rect(ShapeRectFunction), +} + +/// For filled shapes, we use fill-rule, and store it for path() and polygon(). +/// For outline shapes, we should ignore fill-rule. +/// +/// https://github.com/w3c/fxtf-drafts/issues/512 +/// https://github.com/w3c/csswg-drafts/issues/7390 +/// https://github.com/w3c/csswg-drafts/issues/3468 +pub enum ShapeType { + /// The CSS property uses filled shapes. The default behavior. + Filled, + /// The CSS property uses outline shapes. This is especially useful for offset-path. + Outline, +} + +bitflags! { + /// The flags to represent which basic shapes we would like to support. + /// + /// Different properties may use different subsets of : + /// e.g. + /// clip-path: all basic shapes. + /// motion-path: all basic shapes (but ignore fill-rule). + /// shape-outside: inset(), circle(), ellipse(), polygon(). + /// + /// Also there are some properties we don't support for now: + /// shape-inside: inset(), circle(), ellipse(), polygon(). + /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon(). + /// + /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now + /// we use the bitflags to choose the supported basic shapes for each property at the parse + /// time. + /// https://github.com/w3c/csswg-drafts/issues/7390 + #[derive(Clone, Copy)] + #[repr(C)] + pub struct AllowedBasicShapes: u8 { + /// inset(). + const INSET = 1 << 0; + /// xywh(). + const XYWH = 1 << 1; + /// rect(). + const RECT = 1 << 2; + /// circle(). + const CIRCLE = 1 << 3; + /// ellipse(). + const ELLIPSE = 1 << 4; + /// polygon(). + const POLYGON = 1 << 5; + /// path(). + const PATH = 1 << 6; + // TODO: Bug 1823463. Add shape(). + // const SHAPE = 1 << 7; + + /// All flags. + const ALL = + Self::INSET.bits() | + Self::XYWH.bits() | + Self::RECT.bits() | + Self::CIRCLE.bits() | + Self::ELLIPSE.bits() | + Self::POLYGON.bits() | + Self::PATH.bits(); + + /// For shape-outside. + const SHAPE_OUTSIDE = + Self::INSET.bits() | + Self::CIRCLE.bits() | + Self::ELLIPSE.bits() | + Self::POLYGON.bits(); + } +} + +/// A helper for both clip-path and shape-outside parsing of shapes. +fn parse_shape_or_box<'i, 't, R, ReferenceBox>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + to_shape: impl FnOnce(Box, ReferenceBox) -> R, + to_reference_box: impl FnOnce(ReferenceBox) -> R, + flags: AllowedBasicShapes, +) -> Result> +where + ReferenceBox: Default + Parse, +{ + let mut shape = None; + let mut ref_box = None; + loop { + if shape.is_none() { + shape = input + .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled)) + .ok(); + } + + if ref_box.is_none() { + ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok(); + if ref_box.is_some() { + continue; + } + } + break; + } + + if let Some(shp) = shape { + return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default())); + } + + match ref_box { + Some(r) => Ok(to_reference_box(r)), + None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } +} + +impl Parse for ClipPath { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ClipPath::None); + } + + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(ClipPath::Url(url)); + } + + parse_shape_or_box( + context, + input, + ClipPath::Shape, + ClipPath::Box, + AllowedBasicShapes::ALL, + ) + } +} + +impl Parse for ShapeOutside { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // Need to parse this here so that `Image::parse_with_cors_anonymous` + // doesn't parse it. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ShapeOutside::None); + } + + if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) { + debug_assert_ne!(image, Image::None); + return Ok(ShapeOutside::Image(image)); + } + + parse_shape_or_box( + context, + input, + ShapeOutside::Shape, + ShapeOutside::Box, + AllowedBasicShapes::SHAPE_OUTSIDE, + ) + } +} + +impl BasicShape { + /// Parse with some parameters. + /// 1. The supported . + /// 2. The type of shapes. Should we ignore fill-rule? + /// 3. The default value of `at `. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + flags: AllowedBasicShapes, + shape_type: ShapeType, + ) -> Result> { + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "inset" if flags.contains(AllowedBasicShapes::INSET) => { + InsetRect::parse_function_arguments(context, i) + .map(BasicShapeRect::Inset) + .map(BasicShape::Rect) + }, + "xywh" + if flags.contains(AllowedBasicShapes::XYWH) + && static_prefs::pref!("layout.css.basic-shape-xywh.enabled") => + { + Xywh::parse_function_arguments(context, i) + .map(BasicShapeRect::Xywh) + .map(BasicShape::Rect) + }, + "rect" + if flags.contains(AllowedBasicShapes::RECT) + && static_prefs::pref!("layout.css.basic-shape-rect.enabled") => + { + ShapeRectFunction::parse_function_arguments(context, i) + .map(BasicShapeRect::Rect) + .map(BasicShape::Rect) + }, + "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => { + Circle::parse_function_arguments(context, i) + .map(BasicShape::Circle) + }, + "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => { + Ellipse::parse_function_arguments(context, i) + .map(BasicShape::Ellipse) + }, + "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => { + Polygon::parse_function_arguments(context, i, shape_type) + .map(BasicShape::Polygon) + }, + "path" if flags.contains(AllowedBasicShapes::PATH) => { + Path::parse_function_arguments(i, shape_type).map(BasicShape::Path) + }, + _ => Err(location + .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))), + } + }) + } +} + +impl Parse for InsetRect { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_function_matching("inset")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +fn parse_round<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result> { + if input + .try_parse(|i| i.expect_ident_matching("round")) + .is_ok() + { + return BorderRadius::parse(context, input); + } + + Ok(BorderRadius::zero()) +} + +impl InsetRect { + /// Parse the inner function arguments of `inset()` + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let rect = Rect::parse_with(context, input, LengthPercentage::parse)?; + let round = parse_round(context, input)?; + Ok(generic::InsetRect { rect, round }) + } +} + +impl ToCss for ShapePosition { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.horizontal.to_css(dest)?; + dest.write_char(' ')?; + self.vertical.to_css(dest) + } +} + +fn parse_at_position<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result, ParseError<'i>> { + use crate::values::specified::position::{Position, Side}; + use crate::values::specified::{AllowedNumericType, Percentage, PositionComponent}; + + fn convert_to_length_percentage(c: PositionComponent) -> LengthPercentage { + // Convert the value when parsing, to make sure we serialize it properly for both + // specified and computed values. + // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization + match c { + // Since keywords stand in for percentages, keywords without an offset + // turn into percentages. + PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)), + PositionComponent::Side(keyword, None) => { + Percentage::new(if keyword.is_start() { 0. } else { 1. }).into() + }, + // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of + // "avoiding calc() expressions where possible" and "avoiding calc() + // transformations" will be removed from the spec, and we should follow the + // css-values-4 for position, i.e. we make it as length-percentage always. + // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization. + // https://drafts.csswg.org/css-values-4/#typedef-position + PositionComponent::Side(keyword, Some(length)) => { + if keyword.is_start() { + length + } else { + length.hundred_percent_minus(AllowedNumericType::All) + } + }, + PositionComponent::Length(length) => length, + } + } + + if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + Position::parse(context, input).map(|pos| { + GenericPositionOrAuto::Position(ShapePosition::new( + convert_to_length_percentage(pos.horizontal), + convert_to_length_percentage(pos.vertical), + )) + }) + } else { + // `at ` is omitted. + Ok(GenericPositionOrAuto::Auto) + } +} + +impl Parse for Circle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + 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> { + let radius = input + .try_parse(|i| ShapeRadius::parse(context, i)) + .unwrap_or_default(); + let position = parse_at_position(context, input)?; + + Ok(generic::Circle { radius, position }) + } +} + +impl Parse for Ellipse { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + 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> { + let (semiaxis_x, semiaxis_y) = input + .try_parse(|i| -> Result<_, ParseError> { + Ok(( + ShapeRadius::parse(context, i)?, + ShapeRadius::parse(context, i)?, + )) + }) + .unwrap_or_default(); + let position = parse_at_position(context, input)?; + + Ok(generic::Ellipse { + semiaxis_x, + semiaxis_y, + position, + }) + } +} + +fn parse_fill_rule<'i, 't>(input: &mut Parser<'i, 't>, shape_type: ShapeType) -> FillRule { + match shape_type { + // Per [1] and [2], we ignore `` for outline shapes, so always use a default + // value. + // [1] https://github.com/w3c/csswg-drafts/issues/3468 + // [2] https://github.com/w3c/csswg-drafts/issues/7390 + // + // Also, per [3] and [4], we would like the ignore `` from outline shapes, e.g. + // offset-path, which means we don't parse it when setting `ShapeType::Outline`. + // This should be web compatible because the shipped "offset-path:path()" doesn't have + // `` and "offset-path:polygon()" is a new feature and still behind the + // preference. + // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321 + // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929 + ShapeType::Outline => Default::default(), + ShapeType::Filled => input + .try_parse(|i| -> Result<_, ParseError> { + let fill = FillRule::parse(i)?; + i.expect_comma()?; + Ok(fill) + }) + .unwrap_or_default(), + } +} + +impl Parse for Polygon { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + input.expect_function_matching("polygon")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled)) + } +} + +impl Polygon { + /// Parse the inner arguments of a `polygon` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + shape_type: ShapeType, + ) -> Result> { + let fill = parse_fill_rule(input, shape_type); + let coordinates = input + .parse_comma_separated(|i| { + Ok(PolygonCoord( + LengthPercentage::parse(context, i)?, + LengthPercentage::parse(context, i)?, + )) + })? + .into(); + + Ok(Polygon { fill, coordinates }) + } +} + +impl Path { + /// Parse the inner arguments of a `path` function. + fn parse_function_arguments<'i, 't>( + input: &mut Parser<'i, 't>, + shape_type: ShapeType, + ) -> Result> { + use crate::values::specified::svg_path::AllowEmpty; + + let fill = parse_fill_rule(input, shape_type); + let path = SVGPathData::parse(input, AllowEmpty::No)?; + Ok(Path { fill, path }) + } +} + +fn round_to_css(round: &BorderRadius, dest: &mut CssWriter) -> fmt::Result +where + W: Write, +{ + if !round.is_zero() { + dest.write_str(" round ")?; + round.to_css(dest)?; + } + Ok(()) +} + +impl ToCss for Xywh { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.x.to_css(dest)?; + dest.write_char(' ')?; + self.y.to_css(dest)?; + dest.write_char(' ')?; + self.width.to_css(dest)?; + dest.write_char(' ')?; + self.height.to_css(dest)?; + round_to_css(&self.round, dest) + } +} + +impl Xywh { + /// Parse the inner function arguments of `xywh()`. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let x = LengthPercentage::parse(context, input)?; + let y = LengthPercentage::parse(context, input)?; + let width = NonNegativeLengthPercentage::parse(context, input)?; + let height = NonNegativeLengthPercentage::parse(context, input)?; + let round = parse_round(context, input)?; + Ok(Xywh { + x, + y, + width, + height, + round, + }) + } +} + +impl ToCss for ShapeRectFunction { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.rect.0.to_css(dest)?; + dest.write_char(' ')?; + self.rect.1.to_css(dest)?; + dest.write_char(' ')?; + self.rect.2.to_css(dest)?; + dest.write_char(' ')?; + self.rect.3.to_css(dest)?; + round_to_css(&self.round, dest) + } +} + +impl ShapeRectFunction { + /// Parse the inner function arguments of `rect()`. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?; + let round = parse_round(context, input)?; + Ok(ShapeRectFunction { rect, round }) + } +} + +impl ToComputedValue for BasicShapeRect { + type ComputedValue = ComputedInsetRect; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::LengthPercentage; + use crate::values::computed::LengthPercentageOrAuto; + use style_traits::values::specified::AllowedNumericType; + + match self { + Self::Inset(ref inset) => inset.to_computed_value(context), + Self::Xywh(ref xywh) => { + // Given `xywh(x y w h)`, construct the equivalent inset() function, + // `inset(y calc(100% - x - w) calc(100% - y - h) x)`. + // + // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values + // https://github.com/w3c/csswg-drafts/issues/9053 + let x = xywh.x.to_computed_value(context); + let y = xywh.y.to_computed_value(context); + let w = xywh.width.to_computed_value(context); + let h = xywh.height.to_computed_value(context); + // calc(100% - x - w). + let right = LengthPercentage::hundred_percent_minus_list( + &[&x, &w.0], + AllowedNumericType::All, + ); + // calc(100% - y - h). + let bottom = LengthPercentage::hundred_percent_minus_list( + &[&y, &h.0], + AllowedNumericType::All, + ); + + ComputedInsetRect { + rect: Rect::new(y, right, bottom, x), + round: xywh.round.to_computed_value(context), + } + }, + Self::Rect(ref rect) => { + // Given `rect(t r b l)`, the equivalent function is + // `inset(t calc(100% - r) calc(100% - b) l)`. + // + // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values + fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage { + match v { + // it’s equivalent to 0% as the first (top) or fourth (left) value. + // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect + LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), + LengthPercentageOrAuto::LengthPercentage(lp) => lp, + } + } + fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage { + match v { + // It's equivalent to 100% as the second (right) or third (bottom) value. + // So calc(100% - 100%) = 0%. + // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect + LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), + LengthPercentageOrAuto::LengthPercentage(lp) => { + LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All) + }, + } + } + + let round = rect.round.to_computed_value(context); + let rect = rect.rect.to_computed_value(context); + let rect = Rect::new( + compute_top_or_left(rect.0), + compute_bottom_or_right(rect.1), + compute_bottom_or_right(rect.2), + compute_top_or_left(rect.3), + ); + + ComputedInsetRect { rect, round } + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Inset(ToComputedValue::from_computed_value(computed)) + } +} diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs new file mode 100644 index 0000000000..a4660c7f60 --- /dev/null +++ b/servo/components/style/values/specified/border.rs @@ -0,0 +1,398 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to borders. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius; +use crate::values::generics::border::BorderImageSideWidth as GenericBorderImageSideWidth; +use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice; +use crate::values::generics::border::BorderRadius as GenericBorderRadius; +use crate::values::generics::border::BorderSpacing as GenericBorderSpacing; +use crate::values::generics::rect::Rect; +use crate::values::generics::size::Size2D; +use crate::values::specified::length::{Length, NonNegativeLength, NonNegativeLengthPercentage}; +use crate::values::specified::Color; +use crate::values::specified::{AllowQuirks, NonNegativeNumber, NonNegativeNumberOrPercentage}; +use crate::Zero; +use app_units::Au; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{values::SequenceWriter, CssWriter, ParseError, ToCss}; + +/// A specified value for a single side of a `border-style` property. +/// +/// The order here corresponds to the integer values from the border conflict +/// resolution rules in CSS 2.1 § 17.6.2.1. Higher values override lower values. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Ord, + Parse, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BorderStyle { + Hidden, + None, + Inset, + Groove, + Outset, + Ridge, + Dotted, + Dashed, + Solid, + Double, +} + +impl BorderStyle { + /// Whether this border style is either none or hidden. + #[inline] + pub fn none_or_hidden(&self) -> bool { + matches!(*self, BorderStyle::None | BorderStyle::Hidden) + } +} + +/// A specified value for the `border-image-width` property. +pub type BorderImageWidth = Rect; + +/// A specified value for a single side of a `border-image-width` property. +pub type BorderImageSideWidth = + GenericBorderImageSideWidth; + +/// A specified value for the `border-image-slice` property. +pub type BorderImageSlice = GenericBorderImageSlice; + +/// A specified value for the `border-radius` property. +pub type BorderRadius = GenericBorderRadius; + +/// A specified value for the `border-*-radius` longhand properties. +pub type BorderCornerRadius = GenericBorderCornerRadius; + +/// A specified value for the `border-spacing` longhand properties. +pub type BorderSpacing = GenericBorderSpacing; + +impl BorderImageSlice { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + GenericBorderImageSlice { + offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()), + fill: false, + } + } +} + +/// https://drafts.csswg.org/css-backgrounds-3/#typedef-line-width +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum LineWidth { + /// `thin` + Thin, + /// `medium` + Medium, + /// `thick` + Thick, + /// `` + Length(NonNegativeLength), +} + +impl LineWidth { + /// Returns the `0px` value. + #[inline] + pub fn zero() -> Self { + Self::Length(NonNegativeLength::zero()) + } + + fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result> { + if let Ok(length) = + input.try_parse(|i| NonNegativeLength::parse_quirky(context, i, allow_quirks)) + { + return Ok(Self::Length(length)); + } + Ok(try_match_ident_ignore_ascii_case! { input, + "thin" => Self::Thin, + "medium" => Self::Medium, + "thick" => Self::Thick, + }) + } +} + +impl Parse for LineWidth { + fn parse<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for LineWidth { + type ComputedValue = app_units::Au; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + // https://drafts.csswg.org/css-backgrounds-3/#line-width + Self::Thin => Au::from_px(1), + Self::Medium => Au::from_px(3), + Self::Thick => Au::from_px(5), + Self::Length(ref length) => Au::from_f32_px(length.to_computed_value(context).px()), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Length(NonNegativeLength::from_px(computed.to_f32_px())) + } +} + +/// A specified value for a single side of the `border-width` property. The difference between this +/// and LineWidth is whether we snap to device pixels or not. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct BorderSideWidth(LineWidth); + +impl BorderSideWidth { + /// Returns the `medium` value. + pub fn medium() -> Self { + Self(LineWidth::Medium) + } + + /// Returns a bare px value from the argument. + pub fn from_px(px: f32) -> Self { + Self(LineWidth::Length(Length::from_px(px).into())) + } + + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result> { + Ok(Self(LineWidth::parse_quirky(context, input, allow_quirks)?)) + } +} + +impl Parse for BorderSideWidth { + fn parse<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for BorderSideWidth { + type ComputedValue = app_units::Au; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let width = self.0.to_computed_value(context); + // Round `width` down to the nearest device pixel, but any non-zero value that would round + // down to zero is clamped to 1 device pixel. + if width == Au(0) { + return width; + } + + let au_per_dev_px = context.device().app_units_per_device_pixel(); + std::cmp::max( + Au(au_per_dev_px), + Au(width.0 / au_per_dev_px * au_per_dev_px), + ) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self(LineWidth::from_computed_value(computed)) + } +} + +impl BorderImageSideWidth { + /// Returns `1`. + #[inline] + pub fn one() -> Self { + GenericBorderImageSideWidth::Number(NonNegativeNumber::new(1.)) + } +} + +impl Parse for BorderImageSlice { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + 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> { + 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> { + 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> { + 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + if self.0 != self.1 { + dest.write_char(' ')?; + self.1.to_css(dest)?; + } + Ok(()) + } +} + +impl BorderImageRepeat { + /// Returns the `stretch` value. + #[inline] + pub fn stretch() -> Self { + BorderImageRepeat( + BorderImageRepeatKeyword::Stretch, + BorderImageRepeatKeyword::Stretch, + ) + } +} + +impl Parse for BorderImageRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let horizontal = BorderImageRepeatKeyword::parse(input)?; + let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok(); + Ok(BorderImageRepeat( + horizontal, + vertical.unwrap_or(horizontal), + )) + } +} + +/// Serializes a border shorthand value composed of width/style/color. +pub fn serialize_directional_border( + dest: &mut CssWriter, + width: &BorderSideWidth, + style: &BorderStyle, + color: &Color, +) -> fmt::Result +where + W: Write, +{ + let has_style = *style != BorderStyle::None; + let has_color = *color != Color::CurrentColor; + let has_width = *width != BorderSideWidth::medium(); + if !has_style && !has_color && !has_width { + return width.to_css(dest); + } + let mut writer = SequenceWriter::new(dest, " "); + if has_width { + writer.item(width)?; + } + if has_style { + writer.item(style)?; + } + if has_color { + writer.item(color)?; + } + Ok(()) +} diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs new file mode 100644 index 0000000000..8414591c2b --- /dev/null +++ b/servo/components/style/values/specified/box.rs @@ -0,0 +1,1945 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for box properties. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::{LonghandId, PropertyDeclarationId, PropertyId}; +use crate::values::generics::box_::{ + GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign, + VerticalAlignKeyword, +}; +use crate::values::specified::length::{LengthPercentage, NonNegativeLength}; +use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumberOrPercentage}; +use crate::values::CustomIdent; +use cssparser::Parser; +use num_traits::FromPrimitive; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(not(feature = "servo-layout-2020"))] +fn flexbox_enabled() -> bool { + true +} + +#[cfg(feature = "servo-layout-2020")] +fn flexbox_enabled() -> bool { + servo_config::prefs::pref_map() + .get("layout.flexbox.enabled") + .as_bool() + .unwrap_or(false) +} + +/// Defines an element’s display type, which consists of +/// the two basic qualities of how an element generates boxes +/// +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayOutside { + None = 0, + Inline, + Block, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCaption, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + InternalTable, + #[cfg(feature = "gecko")] + InternalRuby, +} + +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayInside { + None = 0, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Contents, + Flow, + FlowRoot, + Flex, + #[cfg(feature = "gecko")] + Grid, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Table, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRowGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumn, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumnGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableHeaderGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableFooterGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRow, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCell, + #[cfg(feature = "gecko")] + Ruby, + #[cfg(feature = "gecko")] + RubyBase, + #[cfg(feature = "gecko")] + RubyBaseContainer, + #[cfg(feature = "gecko")] + RubyText, + #[cfg(feature = "gecko")] + RubyTextContainer, + #[cfg(feature = "gecko")] + WebkitBox, +} + +impl DisplayInside { + fn is_valid_for_list_item(self) -> bool { + match self { + DisplayInside::Flow => true, + #[cfg(feature = "gecko")] + DisplayInside::FlowRoot => true, + _ => false, + } + } + + /// https://drafts.csswg.org/css-display/#inside-model: + /// If is omitted, the element’s outside display type defaults to block + /// — except for ruby, which defaults to inline. + fn default_display_outside(self) -> DisplayOutside { + match self { + #[cfg(feature = "gecko")] + DisplayInside::Ruby => DisplayOutside::Inline, + _ => DisplayOutside::Block, + } + } +} + +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Display(u16); + +/// Gecko-only impl block for Display (shared stuff later in this file): +#[allow(missing_docs)] +#[allow(non_upper_case_globals)] +impl Display { + // Our u16 bits are used as follows: LOOOOOOOIIIIIIII + pub const LIST_ITEM_MASK: u16 = 0b1000000000000000; + pub const OUTSIDE_MASK: u16 = 0b0111111100000000; + pub const INSIDE_MASK: u16 = 0b0000000011111111; + pub const OUTSIDE_SHIFT: u16 = 8; + + /// https://drafts.csswg.org/css-display/#the-display-properties + /// ::new() inlined so cbindgen can use it + pub const None: Self = + Self(((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::None as u16); + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + pub const Contents: Self = Self( + ((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Contents as u16, + ); + pub const Inline: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16); + pub const InlineBlock: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16, + ); + pub const Block: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16); + #[cfg(feature = "gecko")] + pub const FlowRoot: Self = Self( + ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16, + ); + pub const Flex: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16); + pub const InlineFlex: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16); + #[cfg(feature = "gecko")] + pub const Grid: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16); + #[cfg(feature = "gecko")] + pub const InlineGrid: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const Table: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const InlineTable: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCaption: Self = Self( + ((DisplayOutside::TableCaption as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16, + ); + #[cfg(feature = "gecko")] + pub const Ruby: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Ruby as u16); + #[cfg(feature = "gecko")] + pub const WebkitBox: Self = Self( + ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16, + ); + #[cfg(feature = "gecko")] + pub const WebkitInlineBox: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16, + ); + + // Internal table boxes. + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRowGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableRowGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableHeaderGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableHeaderGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableFooterGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableFooterGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumn: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableColumn as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumnGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableColumnGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRow: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableRow as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCell: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableCell as u16, + ); + + /// Internal ruby boxes. + #[cfg(feature = "gecko")] + pub const RubyBase: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyBase as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyBaseContainer: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyBaseContainer as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyText: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyText as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyTextContainer: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyTextContainer as u16, + ); + + /// Make a raw display value from and values. + #[inline] + const fn new(outside: DisplayOutside, inside: DisplayInside) -> Self { + Self((outside as u16) << Self::OUTSIDE_SHIFT | inside as u16) + } + + /// Make a display enum value from and values. + #[inline] + fn from3(outside: DisplayOutside, inside: DisplayInside, list_item: bool) -> Self { + let v = Self::new(outside, inside); + if !list_item { + return v; + } + Self(v.0 | Self::LIST_ITEM_MASK) + } + + /// Accessor for the value. + #[inline] + pub fn inside(&self) -> DisplayInside { + DisplayInside::from_u16(self.0 & Self::INSIDE_MASK).unwrap() + } + + /// Accessor for the value. + #[inline] + pub fn outside(&self) -> DisplayOutside { + DisplayOutside::from_u16((self.0 & Self::OUTSIDE_MASK) >> Self::OUTSIDE_SHIFT).unwrap() + } + + /// Returns the raw underlying u16 value. + #[inline] + pub const fn to_u16(&self) -> u16 { + self.0 + } + + /// Whether this is `display: inline` (or `inline list-item`). + #[inline] + pub fn is_inline_flow(&self) -> bool { + self.outside() == DisplayOutside::Inline && self.inside() == DisplayInside::Flow + } + + /// Returns whether this `display` value is some kind of list-item. + #[inline] + pub const fn is_list_item(&self) -> bool { + (self.0 & Self::LIST_ITEM_MASK) != 0 + } + + /// Returns whether this `display` value is a ruby level container. + pub fn is_ruby_level_container(&self) -> bool { + match *self { + #[cfg(feature = "gecko")] + Display::RubyBaseContainer | Display::RubyTextContainer => true, + _ => false, + } + } + + /// Returns whether this `display` value is one of the types for ruby. + pub fn is_ruby_type(&self) -> bool { + match self.inside() { + #[cfg(feature = "gecko")] + DisplayInside::Ruby | + DisplayInside::RubyBase | + DisplayInside::RubyText | + DisplayInside::RubyBaseContainer | + DisplayInside::RubyTextContainer => true, + _ => false, + } + } +} + +/// Shared Display impl for both Gecko and Servo. +impl Display { + /// The initial display value. + #[inline] + pub fn inline() -> Self { + Display::Inline + } + + /// + #[cfg(feature = "servo")] + #[inline] + pub fn is_atomic_inline_level(&self) -> bool { + match *self { + Display::InlineBlock | Display::InlineFlex => true, + #[cfg(any(feature = "servo-layout-2013"))] + Display::InlineTable => true, + _ => false, + } + } + + /// Returns whether this `display` value is the display of a flex or + /// grid container. + /// + /// This is used to implement various style fixups. + pub fn is_item_container(&self) -> bool { + match self.inside() { + DisplayInside::Flex => true, + #[cfg(feature = "gecko")] + DisplayInside::Grid => true, + _ => false, + } + } + + /// Returns whether an element with this display type is a line + /// participant, which means it may lay its children on the same + /// line as itself. + pub fn is_line_participant(&self) -> bool { + if self.is_inline_flow() { + return true; + } + match *self { + #[cfg(feature = "gecko")] + Display::Contents | Display::Ruby | Display::RubyBaseContainer => true, + _ => false, + } + } + + /// Convert this display into an equivalent block display. + /// + /// Also used for :root style adjustments. + pub fn equivalent_block_display(&self, _is_root_element: bool) -> Self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + { + // Special handling for `contents` and `list-item`s on the root element. + if _is_root_element && (self.is_contents() || self.is_list_item()) { + return Display::Block; + } + } + + match self.outside() { + DisplayOutside::Inline => { + let inside = match self.inside() { + // `inline-block` blockifies to `block` rather than + // `flow-root`, for legacy reasons. + DisplayInside::FlowRoot => DisplayInside::Flow, + inside => inside, + }; + Display::from3(DisplayOutside::Block, inside, self.is_list_item()) + }, + DisplayOutside::Block | DisplayOutside::None => *self, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + _ => Display::Block, + } + } + + /// Convert this display into an equivalent inline-outside display. + /// https://drafts.csswg.org/css-display/#inlinify + #[cfg(feature = "gecko")] + pub fn inlinify(&self) -> Self { + match self.outside() { + DisplayOutside::Block => { + let inside = match self.inside() { + // `display: block` inlinifies to `display: inline-block`, + // rather than `inline`, for legacy reasons. + DisplayInside::Flow => DisplayInside::FlowRoot, + inside => inside, + }; + Display::from3(DisplayOutside::Inline, inside, self.is_list_item()) + }, + _ => *self, + } + } + + /// Returns true if the value is `Contents` + #[inline] + pub fn is_contents(&self) -> bool { + match *self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Display::Contents => true, + _ => false, + } + } + + /// Returns true if the value is `None` + #[inline] + pub fn is_none(&self) -> bool { + *self == Display::None + } +} + +enum DisplayKeyword { + Full(Display), + Inside(DisplayInside), + Outside(DisplayOutside), + ListItem, +} + +impl DisplayKeyword { + fn parse<'i>(input: &mut Parser<'i, '_>) -> Result> { + use self::DisplayKeyword::*; + Ok(try_match_ident_ignore_ascii_case! { input, + "none" => Full(Display::None), + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "contents" => Full(Display::Contents), + "inline-block" => Full(Display::InlineBlock), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "inline-table" => Full(Display::InlineTable), + "-webkit-flex" if flexbox_enabled() => Full(Display::Flex), + "inline-flex" | "-webkit-inline-flex" if flexbox_enabled() => Full(Display::InlineFlex), + #[cfg(feature = "gecko")] + "inline-grid" => Full(Display::InlineGrid), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-caption" => Full(Display::TableCaption), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row-group" => Full(Display::TableRowGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-header-group" => Full(Display::TableHeaderGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-footer-group" => Full(Display::TableFooterGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column" => Full(Display::TableColumn), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column-group" => Full(Display::TableColumnGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row" => Full(Display::TableRow), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-cell" => Full(Display::TableCell), + #[cfg(feature = "gecko")] + "ruby-base" => Full(Display::RubyBase), + #[cfg(feature = "gecko")] + "ruby-base-container" => Full(Display::RubyBaseContainer), + #[cfg(feature = "gecko")] + "ruby-text" => Full(Display::RubyText), + #[cfg(feature = "gecko")] + "ruby-text-container" => Full(Display::RubyTextContainer), + #[cfg(feature = "gecko")] + "-webkit-box" => Full(Display::WebkitBox), + #[cfg(feature = "gecko")] + "-webkit-inline-box" => Full(Display::WebkitInlineBox), + + /// = block | inline | run-in + /// https://drafts.csswg.org/css-display/#typedef-display-outside + "block" => Outside(DisplayOutside::Block), + "inline" => Outside(DisplayOutside::Inline), + + "list-item" => ListItem, + + /// = flow | flow-root | table | flex | grid | ruby + /// https://drafts.csswg.org/css-display/#typedef-display-inside + "flow" => Inside(DisplayInside::Flow), + "flex" if flexbox_enabled() => Inside(DisplayInside::Flex), + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "flow-root" => Inside(DisplayInside::FlowRoot), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table" => Inside(DisplayInside::Table), + #[cfg(feature = "gecko")] + "grid" => Inside(DisplayInside::Grid), + #[cfg(feature = "gecko")] + "ruby" => Inside(DisplayInside::Ruby), + }) + } +} + +impl ToCss for Display { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: fmt::Write, + { + let outside = self.outside(); + let inside = self.inside(); + match *self { + Display::Block | Display::Inline => outside.to_css(dest), + Display::InlineBlock => dest.write_str("inline-block"), + #[cfg(feature = "gecko")] + Display::WebkitInlineBox => dest.write_str("-webkit-inline-box"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Display::TableCaption => dest.write_str("table-caption"), + _ => match (outside, inside) { + #[cfg(feature = "gecko")] + (DisplayOutside::Inline, DisplayInside::Grid) => dest.write_str("inline-grid"), + (DisplayOutside::Inline, DisplayInside::Flex) => dest.write_str("inline-flex"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + (DisplayOutside::Inline, DisplayInside::Table) => dest.write_str("inline-table"), + #[cfg(feature = "gecko")] + (DisplayOutside::Block, DisplayInside::Ruby) => dest.write_str("block ruby"), + (_, inside) => { + if self.is_list_item() { + if outside != DisplayOutside::Block { + outside.to_css(dest)?; + dest.write_char(' ')?; + } + if inside != DisplayInside::Flow { + inside.to_css(dest)?; + dest.write_char(' ')?; + } + dest.write_str("list-item") + } else { + inside.to_css(dest) + } + }, + }, + } + } +} + +impl Parse for Display { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let mut got_list_item = false; + let mut inside = None; + let mut outside = None; + match DisplayKeyword::parse(input)? { + DisplayKeyword::Full(d) => return Ok(d), + DisplayKeyword::Outside(o) => { + outside = Some(o); + }, + DisplayKeyword::Inside(i) => { + inside = Some(i); + }, + DisplayKeyword::ListItem => { + got_list_item = true; + }, + }; + + while let Ok(kw) = input.try_parse(DisplayKeyword::parse) { + match kw { + DisplayKeyword::ListItem if !got_list_item => { + got_list_item = true; + }, + DisplayKeyword::Outside(o) if outside.is_none() => { + outside = Some(o); + }, + DisplayKeyword::Inside(i) if inside.is_none() => { + inside = Some(i); + }, + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + let inside = inside.unwrap_or(DisplayInside::Flow); + let outside = outside.unwrap_or_else(|| inside.default_display_outside()); + if got_list_item && !inside.is_valid_for_list_item() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + return Ok(Display::from3(outside, inside, got_list_item)); + } +} + +impl SpecifiedValueInfo for Display { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&[ + "block", + "contents", + "flex", + "flow-root", + "flow-root list-item", + "grid", + "inline", + "inline-block", + "inline-flex", + "inline-grid", + "inline-table", + "inline list-item", + "inline flow-root list-item", + "list-item", + "none", + "block ruby", + "ruby", + "ruby-base", + "ruby-base-container", + "ruby-text", + "ruby-text-container", + "table", + "table-caption", + "table-cell", + "table-column", + "table-column-group", + "table-footer-group", + "table-header-group", + "table-row", + "table-row-group", + "-webkit-box", + "-webkit-inline-box", + ]); + } +} + +/// A specified value for the `contain-intrinsic-size` property. +pub type ContainIntrinsicSize = GenericContainIntrinsicSize; + +/// A specified value for the `line-clamp` property. +pub type LineClamp = GenericLineClamp; + +/// A specified value for the `vertical-align` property. +pub type VerticalAlign = GenericVerticalAlign; + +impl Parse for VerticalAlign { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if let Ok(lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) + { + return Ok(GenericVerticalAlign::Length(lp)); + } + + Ok(GenericVerticalAlign::Keyword(VerticalAlignKeyword::parse( + input, + )?)) + } +} + +/// A specified value for the `baseline-source` property. +/// https://drafts.csswg.org/css-inline-3/#baseline-source +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToShmem, + ToComputedValue, + ToResolvedValue, +)] +#[repr(u8)] +pub enum BaselineSource { + /// `Last` for `inline-block`, `First` otherwise. + Auto, + /// Use first baseline for alignment. + First, + /// Use last baseline for alignment. + Last, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-axis +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAxis { + X, + Y, + Block, + Inline, + Both, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapStrictness { + #[css(skip)] + None, // Used to represent scroll-snap-type: none. It's not parsed. + Mandatory, + Proximity, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapType { + axis: ScrollSnapAxis, + strictness: ScrollSnapStrictness, +} + +impl ScrollSnapType { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + Self { + axis: ScrollSnapAxis::Both, + strictness: ScrollSnapStrictness::None, + } + } +} + +impl Parse for ScrollSnapType { + /// none | [ x | y | block | inline | both ] [ mandatory | proximity ]? + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + if self.strictness == ScrollSnapStrictness::None { + return dest.write_str("none"); + } + self.axis.to_css(dest)?; + if self.strictness != ScrollSnapStrictness::Proximity { + dest.write_char(' ')?; + self.strictness.to_css(dest)?; + } + Ok(()) + } +} + +/// Specified value of scroll-snap-align keyword value. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAlignKeyword { + None, + Start, + End, + Center, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapAlign { + block: ScrollSnapAlignKeyword, + inline: ScrollSnapAlignKeyword, +} + +impl ScrollSnapAlign { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + ScrollSnapAlign { + block: ScrollSnapAlignKeyword::None, + inline: ScrollSnapAlignKeyword::None, + } + } +} + +impl Parse for ScrollSnapAlign { + /// [ none | start | end | center ]{1,2} + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + 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(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + self.block.to_css(dest)?; + if self.block != self.inline { + dest.write_char(' ')?; + self.inline.to_css(dest)?; + } + Ok(()) + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapStop { + Normal, + Always, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverscrollBehavior { + Auto, + Contain, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowAnchor { + Auto, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowClipBox { + PaddingBox, + ContentBox, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma)] +#[repr(C)] +/// Provides a rendering hint to the user agent, stating what kinds of changes +/// the author expects to perform on the element. +/// +/// `auto` is represented by an empty `features` list. +/// +/// +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, + /// A bitfield with the kind of change that the value will create, based + /// on the above field. + #[css(skip)] + bits: WillChangeBits, +} + +impl WillChange { + #[inline] + /// Get default value of `will-change` as `auto` + pub fn auto() -> Self { + Self::default() + } +} + +/// The change bits that we care about. +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct WillChangeBits(u16); +bitflags! { + impl WillChangeBits: u16 { + /// Whether a property which can create a stacking context **on any + /// box** will change. + const STACKING_CONTEXT_UNCONDITIONAL = 1 << 0; + /// Whether `transform` or related properties will change. + const TRANSFORM = 1 << 1; + /// Whether `scroll-position` will change. + const SCROLL = 1 << 2; + /// Whether `contain` will change. + const CONTAIN = 1 << 3; + /// Whether `opacity` will change. + const OPACITY = 1 << 4; + /// Whether `perspective` will change. + const PERSPECTIVE = 1 << 5; + /// Whether `z-index` will change. + const Z_INDEX = 1 << 6; + /// Whether any property which creates a containing block for non-svg + /// text frames will change. + const FIXPOS_CB_NON_SVG = 1 << 7; + /// Whether the position property will change. + const POSITION = 1 << 8; + } +} + +fn change_bits_for_longhand(longhand: LonghandId) -> WillChangeBits { + match longhand { + LonghandId::Opacity => WillChangeBits::OPACITY, + LonghandId::Contain => WillChangeBits::CONTAIN, + LonghandId::Perspective => WillChangeBits::PERSPECTIVE, + LonghandId::Position => { + WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::POSITION + }, + LonghandId::ZIndex => WillChangeBits::Z_INDEX, + LonghandId::Transform | + LonghandId::TransformStyle | + LonghandId::Translate | + LonghandId::Rotate | + LonghandId::Scale | + LonghandId::OffsetPath => WillChangeBits::TRANSFORM, + LonghandId::BackdropFilter | LonghandId::Filter => { + WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::FIXPOS_CB_NON_SVG + }, + LonghandId::MixBlendMode | + LonghandId::Isolation | + LonghandId::MaskImage | + LonghandId::ClipPath => WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL, + _ => WillChangeBits::empty(), + } +} + +fn change_bits_for_maybe_property(ident: &str, context: &ParserContext) -> WillChangeBits { + let id = match PropertyId::parse_ignoring_rule_type(ident, context) { + Ok(id) => id, + Err(..) => return WillChangeBits::empty(), + }; + + match id.as_shorthand() { + Ok(shorthand) => shorthand + .longhands() + .fold(WillChangeBits::empty(), |flags, p| { + flags | change_bits_for_longhand(p) + }), + Err(PropertyDeclarationId::Longhand(longhand)) => change_bits_for_longhand(longhand), + Err(PropertyDeclarationId::Custom(..)) => WillChangeBits::empty(), + } +} + +impl Parse for WillChange { + /// auto | # + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(Self::default()); + } + + let mut bits = WillChangeBits::empty(); + let custom_idents = input.parse_comma_separated(|i| { + let location = i.current_source_location(); + let parser_ident = i.expect_ident()?; + let ident = CustomIdent::from_ident( + location, + parser_ident, + &["will-change", "none", "all", "auto"], + )?; + + if context.in_ua_sheet() && ident.0 == atom!("-moz-fixed-pos-containing-block") { + bits |= WillChangeBits::FIXPOS_CB_NON_SVG; + } else if ident.0 == atom!("scroll-position") { + bits |= WillChangeBits::SCROLL; + } else { + bits |= change_bits_for_maybe_property(&parser_ident, context); + } + Ok(ident) + })?; + + Ok(Self { + features: custom_idents.into(), + bits, + }) + } +} + +/// Values for the `touch-action` property. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags(single = "none,auto,manipulation", mixed = "pan-x,pan-y,pinch-zoom"))] +#[repr(C)] +pub struct TouchAction(u8); +bitflags! { + impl TouchAction: u8 { + /// `none` variant + const NONE = 1 << 0; + /// `auto` variant + const AUTO = 1 << 1; + /// `pan-x` variant + const PAN_X = 1 << 2; + /// `pan-y` variant + const PAN_Y = 1 << 3; + /// `manipulation` variant + const MANIPULATION = 1 << 4; + /// `pinch-zoom` variant + const PINCH_ZOOM = 1 << 5; + } +} + +impl TouchAction { + #[inline] + /// Get default `touch-action` as `auto` + pub fn auto() -> TouchAction { + TouchAction::AUTO + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags( + single = "none,strict,content", + mixed = "size,layout,style,paint,inline-size", + overlapping_bits +))] +#[repr(C)] +/// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property +pub struct Contain(u8); +bitflags! { + impl Contain: u8 { + /// `none` variant, just for convenience. + const NONE = 0; + /// `inline-size` variant, turns on single-axis inline size containment + const INLINE_SIZE = 1 << 0; + /// `block-size` variant, turns on single-axis block size containment, internal only + const BLOCK_SIZE = 1 << 1; + /// `layout` variant, turns on layout containment + const LAYOUT = 1 << 2; + /// `style` variant, turns on style containment + const STYLE = 1 << 3; + /// `paint` variant, turns on paint containment + const PAINT = 1 << 4; + /// 'size' variant, turns on size containment + const SIZE = 1 << 5 | Contain::INLINE_SIZE.bits() | Contain::BLOCK_SIZE.bits(); + /// `content` variant, turns on layout and paint containment + const CONTENT = 1 << 6 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits(); + /// `strict` variant, turns on all types of containment + const STRICT = 1 << 7 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits() | Contain::SIZE.bits(); + } +} + +impl Parse for ContainIntrinsicSize { + /// none | | auto + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if let Ok(l) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + return Ok(Self::Length(l)); + } + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(Self::AutoNone); + } + + let l = NonNegativeLength::parse(context, input)?; + return Ok(Self::AutoLength(l)); + } + + input.expect_ident_matching("none")?; + Ok(Self::None) + } +} + +impl Parse for LineClamp { + /// none | + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if let Ok(i) = + input.try_parse(|i| crate::values::specified::PositiveInteger::parse(context, i)) + { + return Ok(Self(i.0)); + } + input.expect_ident_matching("none")?; + Ok(Self::none()) + } +} + +/// https://drafts.csswg.org/css-contain-2/#content-visibility +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ContentVisibility { + /// `auto` variant, the element turns on layout containment, style containment, and paint + /// containment. In addition, if the element is not relevant to the user (such as by being + /// offscreen) it also skips its content + Auto, + /// `hidden` variant, the element skips its content + Hidden, + /// 'visible' variant, no effect + Visible, +} + +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + Parse, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +#[allow(missing_docs)] +/// https://drafts.csswg.org/css-contain-3/#container-type +pub enum ContainerType { + /// The `normal` variant. + Normal, + /// The `inline-size` variant. + InlineSize, + /// The `size` variant. + Size, +} + +impl ContainerType { + /// Is this container-type: normal? + pub fn is_normal(self) -> bool { + self == Self::Normal + } + + /// Is this type containing size in any way? + pub fn is_size_container_type(self) -> bool { + !self.is_normal() + } +} + +/// https://drafts.csswg.org/css-contain-3/#container-name +#[repr(transparent)] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct ContainerName(#[css(iterable, if_empty = "none")] pub crate::OwnedSlice); + +impl ContainerName { + /// Return the `none` value. + pub fn none() -> Self { + Self(Default::default()) + } + + /// Returns whether this is the `none` value. + pub fn is_none(&self) -> bool { + self.0.is_empty() + } + + fn parse_internal<'i>( + input: &mut Parser<'i, '_>, + for_query: bool, + ) -> Result> { + let mut idents = vec![]; + let location = input.current_source_location(); + let first = input.expect_ident()?; + if !for_query && first.eq_ignore_ascii_case("none") { + return Ok(Self::none()); + } + const DISALLOWED_CONTAINER_NAMES: &'static [&'static str] = &["none", "not", "or", "and"]; + idents.push(CustomIdent::from_ident( + location, + first, + DISALLOWED_CONTAINER_NAMES, + )?); + if !for_query { + while let Ok(name) = + input.try_parse(|input| CustomIdent::parse(input, DISALLOWED_CONTAINER_NAMES)) + { + idents.push(name); + } + } + Ok(ContainerName(idents.into())) + } + + /// https://github.com/w3c/csswg-drafts/issues/7203 + /// Only a single name allowed in @container rule. + /// Disallow none for container-name in @container rule. + pub fn parse_for_query<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Self::parse_internal(input, /* for_query = */ true) + } +} + +impl Parse for ContainerName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + Self::parse_internal(input, /* for_query = */ false) + } +} + +/// A specified value for the `perspective` property. +pub type Perspective = GenericPerspective; + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css-box/#propdef-float +pub enum Float { + Left, + Right, + None, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css2/#propdef-clear +pub enum Clear { + None, + Left, + Right, + Both, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +/// https://drafts.csswg.org/css-ui/#propdef-resize +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum Resize { + None, + Both, + Horizontal, + Vertical, + // https://drafts.csswg.org/css-logical-1/#resize + Inline, + Block, +} + +/// The value for the `appearance` property. +/// +/// https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Appearance { + /// No appearance at all. + None, + /// Default appearance for the element. + /// + /// This value doesn't make sense for -moz-default-appearance, but we don't bother to guard + /// against parsing it. + Auto, + /// A searchfield. + Searchfield, + /// A multi-line text field, e.g. HTML