summaryrefslogtreecommitdiffstats
path: root/servo/components/style/values
diff options
context:
space:
mode:
Diffstat (limited to 'servo/components/style/values')
-rw-r--r--servo/components/style/values/animated/color.rs88
-rw-r--r--servo/components/style/values/animated/effects.rs27
-rw-r--r--servo/components/style/values/animated/font.rs37
-rw-r--r--servo/components/style/values/animated/grid.rs165
-rw-r--r--servo/components/style/values/animated/lists.rs141
-rw-r--r--servo/components/style/values/animated/mod.rs487
-rw-r--r--servo/components/style/values/animated/svg.rs46
-rw-r--r--servo/components/style/values/animated/transform.rs1667
-rw-r--r--servo/components/style/values/computed/align.rs91
-rw-r--r--servo/components/style/values/computed/angle.rs101
-rw-r--r--servo/components/style/values/computed/animation.rs70
-rw-r--r--servo/components/style/values/computed/background.rs13
-rw-r--r--servo/components/style/values/computed/basic_shape.rs37
-rw-r--r--servo/components/style/values/computed/border.rs84
-rw-r--r--servo/components/style/values/computed/box.rs388
-rw-r--r--servo/components/style/values/computed/color.rs95
-rw-r--r--servo/components/style/values/computed/column.rs11
-rw-r--r--servo/components/style/values/computed/counters.rs26
-rw-r--r--servo/components/style/values/computed/easing.rs109
-rw-r--r--servo/components/style/values/computed/effects.rs44
-rw-r--r--servo/components/style/values/computed/flex.rs19
-rw-r--r--servo/components/style/values/computed/font.rs1369
-rw-r--r--servo/components/style/values/computed/image.rs205
-rw-r--r--servo/components/style/values/computed/length.rs531
-rw-r--r--servo/components/style/values/computed/length_percentage.rs1055
-rw-r--r--servo/components/style/values/computed/list.rs17
-rw-r--r--servo/components/style/values/computed/mod.rs1035
-rw-r--r--servo/components/style/values/computed/motion.rs70
-rw-r--r--servo/components/style/values/computed/outline.rs7
-rw-r--r--servo/components/style/values/computed/page.rs75
-rw-r--r--servo/components/style/values/computed/percentage.rs136
-rw-r--r--servo/components/style/values/computed/position.rs74
-rw-r--r--servo/components/style/values/computed/ratio.rs115
-rw-r--r--servo/components/style/values/computed/rect.rs11
-rw-r--r--servo/components/style/values/computed/resolution.rs56
-rw-r--r--servo/components/style/values/computed/svg.rs66
-rw-r--r--servo/components/style/values/computed/table.rs7
-rw-r--r--servo/components/style/values/computed/text.rs228
-rw-r--r--servo/components/style/values/computed/time.rs45
-rw-r--r--servo/components/style/values/computed/transform.rs559
-rw-r--r--servo/components/style/values/computed/ui.rs21
-rw-r--r--servo/components/style/values/computed/url.rs15
-rw-r--r--servo/components/style/values/distance.rs138
-rw-r--r--servo/components/style/values/generics/animation.rs140
-rw-r--r--servo/components/style/values/generics/background.rs54
-rw-r--r--servo/components/style/values/generics/basic_shape.rs567
-rw-r--r--servo/components/style/values/generics/border.rs261
-rw-r--r--servo/components/style/values/generics/box.rs211
-rw-r--r--servo/components/style/values/generics/calc.rs1820
-rw-r--r--servo/components/style/values/generics/color.rs209
-rw-r--r--servo/components/style/values/generics/column.rs45
-rw-r--r--servo/components/style/values/generics/counters.rs295
-rw-r--r--servo/components/style/values/generics/easing.rs143
-rw-r--r--servo/components/style/values/generics/effects.rs121
-rw-r--r--servo/components/style/values/generics/flex.rs33
-rw-r--r--servo/components/style/values/generics/font.rs316
-rw-r--r--servo/components/style/values/generics/grid.rs867
-rw-r--r--servo/components/style/values/generics/image.rs631
-rw-r--r--servo/components/style/values/generics/length.rs304
-rw-r--r--servo/components/style/values/generics/mod.rs388
-rw-r--r--servo/components/style/values/generics/motion.rs270
-rw-r--r--servo/components/style/values/generics/page.rs162
-rw-r--r--servo/components/style/values/generics/position.rs238
-rw-r--r--servo/components/style/values/generics/ratio.rs50
-rw-r--r--servo/components/style/values/generics/rect.rs146
-rw-r--r--servo/components/style/values/generics/size.rs101
-rw-r--r--servo/components/style/values/generics/svg.rs221
-rw-r--r--servo/components/style/values/generics/text.rs148
-rw-r--r--servo/components/style/values/generics/transform.rs879
-rw-r--r--servo/components/style/values/generics/ui.rs129
-rw-r--r--servo/components/style/values/generics/url.rs47
-rw-r--r--servo/components/style/values/mod.rs796
-rw-r--r--servo/components/style/values/resolved/color.rs48
-rw-r--r--servo/components/style/values/resolved/counters.rs51
-rw-r--r--servo/components/style/values/resolved/mod.rs275
-rw-r--r--servo/components/style/values/specified/align.rs820
-rw-r--r--servo/components/style/values/specified/angle.rs276
-rw-r--r--servo/components/style/values/specified/animation.rs463
-rw-r--r--servo/components/style/values/specified/background.rs143
-rw-r--r--servo/components/style/values/specified/basic_shape.rs719
-rw-r--r--servo/components/style/values/specified/border.rs398
-rw-r--r--servo/components/style/values/specified/box.rs1945
-rw-r--r--servo/components/style/values/specified/calc.rs1086
-rw-r--r--servo/components/style/values/specified/color.rs1175
-rw-r--r--servo/components/style/values/specified/column.rs11
-rw-r--r--servo/components/style/values/specified/counters.rs279
-rw-r--r--servo/components/style/values/specified/easing.rs192
-rw-r--r--servo/components/style/values/specified/effects.rs453
-rw-r--r--servo/components/style/values/specified/flex.rs25
-rw-r--r--servo/components/style/values/specified/font.rs2222
-rw-r--r--servo/components/style/values/specified/gecko.rs82
-rw-r--r--servo/components/style/values/specified/grid.rs441
-rw-r--r--servo/components/style/values/specified/image.rs1340
-rw-r--r--servo/components/style/values/specified/length.rs2031
-rw-r--r--servo/components/style/values/specified/list.rs202
-rw-r--r--servo/components/style/values/specified/mod.rs992
-rw-r--r--servo/components/style/values/specified/motion.rs343
-rw-r--r--servo/components/style/values/specified/outline.rs71
-rw-r--r--servo/components/style/values/specified/page.rs99
-rw-r--r--servo/components/style/values/specified/percentage.rs225
-rw-r--r--servo/components/style/values/specified/position.rs955
-rw-r--r--servo/components/style/values/specified/ratio.rs32
-rw-r--r--servo/components/style/values/specified/rect.rs11
-rw-r--r--servo/components/style/values/specified/resolution.rs141
-rw-r--r--servo/components/style/values/specified/source_size_list.rs136
-rw-r--r--servo/components/style/values/specified/svg.rs404
-rw-r--r--servo/components/style/values/specified/svg_path.rs1029
-rw-r--r--servo/components/style/values/specified/table.rs36
-rw-r--r--servo/components/style/values/specified/text.rs1193
-rw-r--r--servo/components/style/values/specified/time.rs183
-rw-r--r--servo/components/style/values/specified/transform.rs530
-rw-r--r--servo/components/style/values/specified/ui.rs257
-rw-r--r--servo/components/style/values/specified/url.rs15
113 files changed, 40502 insertions, 0 deletions
diff --git a/servo/components/style/values/animated/color.rs b/servo/components/style/values/animated/color.rs
new file mode 100644
index 0000000000..f608b72e53
--- /dev/null
+++ b/servo/components/style/values/animated/color.rs
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Animated types for CSS colors.
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::color::AbsoluteColor;
+use crate::values::animated::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::Percentage;
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::color::{ColorMixFlags, GenericColor, GenericColorMix};
+
+impl Animate for AbsoluteColor {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (left_weight, right_weight) = procedure.weights();
+ Ok(crate::color::mix::mix(
+ ColorInterpolationMethod::best_interpolation_between(self, other),
+ self,
+ left_weight as f32,
+ other,
+ right_weight as f32,
+ ColorMixFlags::empty(),
+ ))
+ }
+}
+
+impl ComputeSquaredDistance for AbsoluteColor {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let start = [
+ self.alpha,
+ self.components.0 * self.alpha,
+ self.components.1 * self.alpha,
+ self.components.2 * self.alpha,
+ ];
+ let end = [
+ other.alpha,
+ other.components.0 * other.alpha,
+ other.components.1 * other.alpha,
+ other.components.2 * other.alpha,
+ ];
+ start
+ .iter()
+ .zip(&end)
+ .map(|(this, other)| this.compute_squared_distance(other))
+ .sum()
+ }
+}
+
+/// An animated value for `<color>`.
+pub type Color = GenericColor<Percentage>;
+
+/// An animated value for `<color-mix>`.
+pub type ColorMix = GenericColorMix<Color, Percentage>;
+
+impl Animate for Color {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (left_weight, right_weight) = procedure.weights();
+ Ok(Self::from_color_mix(ColorMix {
+ interpolation: ColorInterpolationMethod::srgb(),
+ left: self.clone(),
+ left_percentage: Percentage(left_weight as f32),
+ right: other.clone(),
+ right_percentage: Percentage(right_weight as f32),
+ // See https://github.com/w3c/csswg-drafts/issues/7324
+ flags: ColorMixFlags::empty(),
+ }))
+ }
+}
+
+impl ComputeSquaredDistance for Color {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let current_color = AbsoluteColor::TRANSPARENT_BLACK;
+ self.resolve_to_absolute(&current_color)
+ .compute_squared_distance(&other.resolve_to_absolute(&current_color))
+ }
+}
+
+impl ToAnimatedZero for Color {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Color::Absolute(AbsoluteColor::TRANSPARENT_BLACK))
+ }
+}
diff --git a/servo/components/style/values/animated/effects.rs b/servo/components/style/values/animated/effects.rs
new file mode 100644
index 0000000000..67557e54b7
--- /dev/null
+++ b/servo/components/style/values/animated/effects.rs
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Animated types for CSS values related to effects.
+
+use crate::values::animated::color::Color;
+use crate::values::computed::length::Length;
+#[cfg(feature = "gecko")]
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Angle, Number};
+use crate::values::generics::effects::Filter as GenericFilter;
+use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow;
+#[cfg(not(feature = "gecko"))]
+use crate::values::Impossible;
+
+/// An animated value for the `drop-shadow()` filter.
+pub type AnimatedSimpleShadow = GenericSimpleShadow<Color, Length, Length>;
+
+/// An animated value for a single `filter`.
+#[cfg(feature = "gecko")]
+pub type AnimatedFilter =
+ GenericFilter<Angle, Number, Number, Length, AnimatedSimpleShadow, ComputedUrl>;
+
+/// An animated value for a single `filter`.
+#[cfg(not(feature = "gecko"))]
+pub type AnimatedFilter = GenericFilter<Angle, Number, Number, Length, Impossible, Impossible>;
diff --git a/servo/components/style/values/animated/font.rs b/servo/components/style/values/animated/font.rs
new file mode 100644
index 0000000000..63d4a14b2f
--- /dev/null
+++ b/servo/components/style/values/animated/font.rs
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Animation implementation for various font-related types.
+
+use super::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::font::FontVariationSettings;
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+
+/// <https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def>
+///
+/// Note that the ComputedValue implementation will already have sorted and de-dup'd
+/// the lists of settings, so we can just iterate over the two lists together and
+/// animate their individual values.
+impl Animate for FontVariationSettings {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let result: Vec<_> =
+ super::lists::by_computed_value::animate(&self.0, &other.0, procedure)?;
+ Ok(Self(result.into_boxed_slice()))
+ }
+}
+
+impl ComputeSquaredDistance for FontVariationSettings {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ super::lists::by_computed_value::squared_distance(&self.0, &other.0)
+ }
+}
+
+impl ToAnimatedZero for FontVariationSettings {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
diff --git a/servo/components/style/values/animated/grid.rs b/servo/components/style/values/animated/grid.rs
new file mode 100644
index 0000000000..04f1a2fcaa
--- /dev/null
+++ b/servo/components/style/values/animated/grid.rs
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Animation implementation for various grid-related types.
+
+// Note: we can implement Animate on their generic types directly, but in this case we need to
+// make sure two trait bounds, L: Clone and I: PartialEq, are satisfied on almost all the
+// grid-related types and their other trait implementations because Animate needs them. So in
+// order to avoid adding these two trait bounds (or maybe more..) everywhere, we implement
+// Animate for the computed types, instead of the generic types.
+
+use super::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::Integer;
+use crate::values::computed::LengthPercentage;
+use crate::values::computed::{GridTemplateComponent, TrackList, TrackSize};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::grid as generics;
+
+fn discrete<T: Clone>(from: &T, to: &T, procedure: Procedure) -> Result<T, ()> {
+ if let Procedure::Interpolate { progress } = procedure {
+ Ok(if progress < 0.5 {
+ from.clone()
+ } else {
+ to.clone()
+ })
+ } else {
+ // The discrete animation is not additive, so per spec [1] we should use the |from|, which
+ // is the underlying value. However this mismatches our animation mechanism (see
+ // composite_endpoint() in servo/ports/geckolib/glues.rs), which uses the effect value
+ // (i.e. |to| value here) [2]. So in order to match the behavior of other properties and
+ // other browsers, we use |to| value for addition and accumulation, i.e. Vresult = Vb.
+ //
+ // [1] https://drafts.csswg.org/css-values-4/#not-additive
+ // [2] https://github.com/w3c/csswg-drafts/issues/9070
+ Ok(to.clone())
+ }
+}
+
+fn animate_with_discrete_fallback<T: Animate + Clone>(
+ from: &T,
+ to: &T,
+ procedure: Procedure,
+) -> Result<T, ()> {
+ from.animate(to, procedure)
+ .or_else(|_| discrete(from, to, procedure))
+}
+
+impl Animate for TrackSize {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&generics::TrackSize::Breadth(ref from), &generics::TrackSize::Breadth(ref to)) => {
+ animate_with_discrete_fallback(from, to, procedure)
+ .map(generics::TrackSize::Breadth)
+ },
+ (
+ &generics::TrackSize::Minmax(ref from_min, ref from_max),
+ &generics::TrackSize::Minmax(ref to_min, ref to_max),
+ ) => Ok(generics::TrackSize::Minmax(
+ animate_with_discrete_fallback(from_min, to_min, procedure)?,
+ animate_with_discrete_fallback(from_max, to_max, procedure)?,
+ )),
+ (
+ &generics::TrackSize::FitContent(ref from),
+ &generics::TrackSize::FitContent(ref to),
+ ) => animate_with_discrete_fallback(from, to, procedure)
+ .map(generics::TrackSize::FitContent),
+ (_, _) => discrete(self, other, procedure),
+ }
+ }
+}
+
+impl Animate for generics::TrackRepeat<LengthPercentage, Integer> {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ // If the keyword, auto-fit/fill, is the same it can result in different
+ // number of tracks. For both auto-fit/fill, the number of columns isn't
+ // known until you do layout since it depends on the container size, item
+ // placement and other factors, so we cannot do the correct interpolation
+ // by computed values. Therefore, return Err(()) if it's keywords. If it
+ // is Number, we support animation only if the count is the same and the
+ // length of track_sizes is the same.
+ // https://github.com/w3c/csswg-drafts/issues/3503
+ match (&self.count, &other.count) {
+ (&generics::RepeatCount::Number(from), &generics::RepeatCount::Number(to))
+ if from == to =>
+ {
+ ()
+ },
+ (_, _) => return Err(()),
+ }
+
+ let count = self.count;
+ let track_sizes = super::lists::by_computed_value::animate(
+ &self.track_sizes,
+ &other.track_sizes,
+ procedure,
+ )?;
+
+ // The length of |line_names| is always 0 or N+1, where N is the length
+ // of |track_sizes|. Besides, <line-names> is always discrete.
+ let line_names = discrete(&self.line_names, &other.line_names, procedure)?;
+
+ Ok(generics::TrackRepeat {
+ count,
+ line_names,
+ track_sizes,
+ })
+ }
+}
+
+impl Animate for TrackList {
+ // Based on https://github.com/w3c/csswg-drafts/issues/3201:
+ // 1. Check interpolation type per track, so we need to handle discrete animations
+ // in TrackSize, so any Err(()) returned from TrackSize doesn't make all TrackSize
+ // fallback to discrete animation.
+ // 2. line-names is always discrete.
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.values.len() != other.values.len() {
+ return Err(());
+ }
+
+ if self.is_explicit() != other.is_explicit() {
+ return Err(());
+ }
+
+ // For now, repeat(auto-fill/auto-fit, ...) is not animatable.
+ // TrackRepeat will return Err(()) if we use keywords. Therefore, we can
+ // early return here to avoid traversing |values| in <auto-track-list>.
+ // This may be updated in the future.
+ // https://github.com/w3c/csswg-drafts/issues/3503
+ if self.has_auto_repeat() || other.has_auto_repeat() {
+ return Err(());
+ }
+
+ let values =
+ super::lists::by_computed_value::animate(&self.values, &other.values, procedure)?;
+
+ // The length of |line_names| is always 0 or N+1, where N is the length
+ // of |track_sizes|. Besides, <line-names> is always discrete.
+ let line_names = discrete(&self.line_names, &other.line_names, procedure)?;
+
+ Ok(TrackList {
+ values,
+ line_names,
+ auto_repeat_index: self.auto_repeat_index,
+ })
+ }
+}
+
+impl ComputeSquaredDistance for GridTemplateComponent {
+ #[inline]
+ fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> {
+ // TODO: Bug 1518585, we should implement ComputeSquaredDistance.
+ Err(())
+ }
+}
+
+impl ToAnimatedZero for GridTemplateComponent {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ // It's not clear to get a zero grid track list based on the current definition
+ // of spec, so we return Err(()) directly.
+ Err(())
+ }
+}
diff --git a/servo/components/style/values/animated/lists.rs b/servo/components/style/values/animated/lists.rs
new file mode 100644
index 0000000000..8b3898c497
--- /dev/null
+++ b/servo/components/style/values/animated/lists.rs
@@ -0,0 +1,141 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Lists have various ways of being animated, this module implements them.
+//!
+//! See https://drafts.csswg.org/web-animations-1/#animating-properties
+
+/// https://drafts.csswg.org/web-animations-1/#by-computed-value
+pub mod by_computed_value {
+ use crate::values::{
+ animated::{Animate, Procedure},
+ distance::{ComputeSquaredDistance, SquaredDistance},
+ };
+ use std::iter::FromIterator;
+
+ #[allow(missing_docs)]
+ pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()>
+ where
+ T: Animate,
+ C: FromIterator<T>,
+ {
+ if left.len() != right.len() {
+ return Err(());
+ }
+ left.iter()
+ .zip(right.iter())
+ .map(|(left, right)| left.animate(right, procedure))
+ .collect()
+ }
+
+ #[allow(missing_docs)]
+ pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()>
+ where
+ T: ComputeSquaredDistance,
+ {
+ if left.len() != right.len() {
+ return Err(());
+ }
+ left.iter()
+ .zip(right.iter())
+ .map(|(left, right)| left.compute_squared_distance(right))
+ .sum()
+ }
+}
+
+/// This is the animation used for some of the types like shadows and filters, where the
+/// interpolation happens with the zero value if one of the sides is not present.
+///
+/// https://drafts.csswg.org/web-animations-1/#animating-shadow-lists
+pub mod with_zero {
+ use crate::values::animated::ToAnimatedZero;
+ use crate::values::{
+ animated::{Animate, Procedure},
+ distance::{ComputeSquaredDistance, SquaredDistance},
+ };
+ use itertools::{EitherOrBoth, Itertools};
+ use std::iter::FromIterator;
+
+ #[allow(missing_docs)]
+ pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()>
+ where
+ T: Animate + Clone + ToAnimatedZero,
+ C: FromIterator<T>,
+ {
+ if procedure == Procedure::Add {
+ return Ok(left.iter().chain(right.iter()).cloned().collect());
+ }
+ left.iter()
+ .zip_longest(right.iter())
+ .map(|it| match it {
+ EitherOrBoth::Both(left, right) => left.animate(right, procedure),
+ EitherOrBoth::Left(left) => left.animate(&left.to_animated_zero()?, procedure),
+ EitherOrBoth::Right(right) => right.to_animated_zero()?.animate(right, procedure),
+ })
+ .collect()
+ }
+
+ #[allow(missing_docs)]
+ pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()>
+ where
+ T: ToAnimatedZero + ComputeSquaredDistance,
+ {
+ left.iter()
+ .zip_longest(right.iter())
+ .map(|it| match it {
+ EitherOrBoth::Both(left, right) => left.compute_squared_distance(right),
+ EitherOrBoth::Left(item) | EitherOrBoth::Right(item) => {
+ item.to_animated_zero()?.compute_squared_distance(item)
+ },
+ })
+ .sum()
+ }
+}
+
+/// https://drafts.csswg.org/web-animations-1/#repeatable-list
+pub mod repeatable_list {
+ use crate::values::{
+ animated::{Animate, Procedure},
+ distance::{ComputeSquaredDistance, SquaredDistance},
+ };
+ use std::iter::FromIterator;
+
+ #[allow(missing_docs)]
+ pub fn animate<T, C>(left: &[T], right: &[T], procedure: Procedure) -> Result<C, ()>
+ where
+ T: Animate,
+ C: FromIterator<T>,
+ {
+ use num_integer::lcm;
+ // If the length of either list is zero, the least common multiple is undefined.
+ if left.is_empty() || right.is_empty() {
+ return Err(());
+ }
+ let len = lcm(left.len(), right.len());
+ left.iter()
+ .cycle()
+ .zip(right.iter().cycle())
+ .take(len)
+ .map(|(left, right)| left.animate(right, procedure))
+ .collect()
+ }
+
+ #[allow(missing_docs)]
+ pub fn squared_distance<T>(left: &[T], right: &[T]) -> Result<SquaredDistance, ()>
+ where
+ T: ComputeSquaredDistance,
+ {
+ use num_integer::lcm;
+ if left.is_empty() || right.is_empty() {
+ return Err(());
+ }
+ let len = lcm(left.len(), right.len());
+ left.iter()
+ .cycle()
+ .zip(right.iter().cycle())
+ .take(len)
+ .map(|(left, right)| left.compute_squared_distance(right))
+ .sum()
+ }
+}
diff --git a/servo/components/style/values/animated/mod.rs b/servo/components/style/values/animated/mod.rs
new file mode 100644
index 0000000000..31ea206fc0
--- /dev/null
+++ b/servo/components/style/values/animated/mod.rs
@@ -0,0 +1,487 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Animated values.
+//!
+//! Some values, notably colors, cannot be interpolated directly with their
+//! computed values and need yet another intermediate representation. This
+//! module's raison d'être is to ultimately contain all these types.
+
+use crate::color::AbsoluteColor;
+use crate::properties::PropertyId;
+use crate::values::computed::length::LengthPercentage;
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::Angle as ComputedAngle;
+use crate::values::computed::Image;
+use crate::values::specified::SVGPathData;
+use crate::values::CSSFloat;
+use app_units::Au;
+use smallvec::SmallVec;
+use std::cmp;
+
+pub mod color;
+pub mod effects;
+mod font;
+mod grid;
+pub mod lists;
+mod svg;
+pub mod transform;
+
+/// The category a property falls into for ordering purposes.
+///
+/// https://drafts.csswg.org/web-animations/#calculating-computed-keyframes
+#[derive(Clone, Copy, Eq, Ord, PartialEq, PartialOrd)]
+enum PropertyCategory {
+ Custom,
+ PhysicalLonghand,
+ LogicalLonghand,
+ Shorthand,
+}
+
+impl PropertyCategory {
+ fn of(id: &PropertyId) -> Self {
+ match *id {
+ PropertyId::NonCustom(id) => match id.longhand_or_shorthand() {
+ Ok(id) => if id.is_logical() {
+ PropertyCategory::LogicalLonghand
+ } else {
+ PropertyCategory::PhysicalLonghand
+ },
+ Err(..) => PropertyCategory::Shorthand,
+ },
+ PropertyId::Custom(..) => PropertyCategory::Custom,
+ }
+ }
+}
+
+/// A comparator to sort PropertyIds such that physical longhands are sorted
+/// before logical longhands and shorthands, shorthands with fewer components
+/// are sorted before shorthands with more components, and otherwise shorthands
+/// are sorted by IDL name as defined by [Web Animations][property-order].
+///
+/// Using this allows us to prioritize values specified by longhands (or smaller
+/// shorthand subsets) when longhands and shorthands are both specified on the
+/// one keyframe.
+///
+/// [property-order] https://drafts.csswg.org/web-animations/#calculating-computed-keyframes
+pub fn compare_property_priority(a: &PropertyId, b: &PropertyId) -> cmp::Ordering {
+ let a_category = PropertyCategory::of(a);
+ let b_category = PropertyCategory::of(b);
+
+ if a_category != b_category {
+ return a_category.cmp(&b_category);
+ }
+
+ if a_category != PropertyCategory::Shorthand {
+ return cmp::Ordering::Equal;
+ }
+
+ let a = a.as_shorthand().unwrap();
+ let b = b.as_shorthand().unwrap();
+ // Within shorthands, sort by the number of subproperties, then by IDL
+ // name.
+ let subprop_count_a = a.longhands().count();
+ let subprop_count_b = b.longhands().count();
+ subprop_count_a
+ .cmp(&subprop_count_b)
+ .then_with(|| a.idl_name_sort_order().cmp(&b.idl_name_sort_order()))
+}
+
+/// A helper function to animate two multiplicative factor.
+pub fn animate_multiplicative_factor(
+ this: CSSFloat,
+ other: CSSFloat,
+ procedure: Procedure,
+) -> Result<CSSFloat, ()> {
+ Ok((this - 1.).animate(&(other - 1.), procedure)? + 1.)
+}
+
+/// Animate from one value to another.
+///
+/// This trait is derivable with `#[derive(Animate)]`. The derived
+/// implementation uses a `match` expression with identical patterns for both
+/// `self` and `other`, calling `Animate::animate` on each fields of the values.
+/// If a field is annotated with `#[animation(constant)]`, the two values should
+/// be equal or an error is returned.
+///
+/// If a variant is annotated with `#[animation(error)]`, the corresponding
+/// `match` arm returns an error.
+///
+/// Trait bounds for type parameter `Foo` can be opted out of with
+/// `#[animation(no_bound(Foo))]` on the type definition, trait bounds for
+/// fields can be opted into with `#[animation(field_bound)]` on the field.
+pub trait Animate: Sized {
+ /// Animate a value towards another one, given an animation procedure.
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()>;
+}
+
+/// An animation procedure.
+///
+/// <https://drafts.csswg.org/web-animations/#procedures-for-animating-properties>
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum Procedure {
+ /// <https://drafts.csswg.org/web-animations/#animation-interpolation>
+ Interpolate { progress: f64 },
+ /// <https://drafts.csswg.org/web-animations/#animation-addition>
+ Add,
+ /// <https://drafts.csswg.org/web-animations/#animation-accumulation>
+ Accumulate { count: u64 },
+}
+
+/// Conversion between computed values and intermediate values for animations.
+///
+/// Notably, colors are represented as four floats during animations.
+///
+/// This trait is derivable with `#[derive(ToAnimatedValue)]`.
+pub trait ToAnimatedValue {
+ /// The type of the animated value.
+ type AnimatedValue;
+
+ /// Converts this value to an animated value.
+ fn to_animated_value(self) -> Self::AnimatedValue;
+
+ /// Converts back an animated value into a computed value.
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self;
+}
+
+/// Returns a value similar to `self` that represents zero.
+///
+/// This trait is derivable with `#[derive(ToAnimatedValue)]`. If a field is
+/// annotated with `#[animation(constant)]`, a clone of its value will be used
+/// instead of calling `ToAnimatedZero::to_animated_zero` on it.
+///
+/// If a variant is annotated with `#[animation(error)]`, the corresponding
+/// `match` arm is not generated.
+///
+/// Trait bounds for type parameter `Foo` can be opted out of with
+/// `#[animation(no_bound(Foo))]` on the type definition.
+pub trait ToAnimatedZero: Sized {
+ /// Returns a value that, when added with an underlying value, will produce the underlying
+ /// value. This is used for SMIL animation's "by-animation" where SMIL first interpolates from
+ /// the zero value to the 'by' value, and then adds the result to the underlying value.
+ ///
+ /// This is not the necessarily the same as the initial value of a property. For example, the
+ /// initial value of 'stroke-width' is 1, but the zero value is 0, since adding 1 to the
+ /// underlying value will not produce the underlying value.
+ fn to_animated_zero(&self) -> Result<Self, ()>;
+}
+
+impl Procedure {
+ /// Returns this procedure as a pair of weights.
+ ///
+ /// This is useful for animations that don't animate differently
+ /// depending on the used procedure.
+ #[inline]
+ pub fn weights(self) -> (f64, f64) {
+ match self {
+ Procedure::Interpolate { progress } => (1. - progress, progress),
+ Procedure::Add => (1., 1.),
+ Procedure::Accumulate { count } => (count as f64, 1.),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-transitions/#animtype-number>
+impl Animate for i32 {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(((*self as f64).animate(&(*other as f64), procedure)? + 0.5).floor() as i32)
+ }
+}
+
+/// <https://drafts.csswg.org/css-transitions/#animtype-number>
+impl Animate for f32 {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let ret = (*self as f64).animate(&(*other as f64), procedure)?;
+ Ok(ret.min(f32::MAX as f64).max(f32::MIN as f64) as f32)
+ }
+}
+
+/// <https://drafts.csswg.org/css-transitions/#animtype-number>
+impl Animate for f64 {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (self_weight, other_weight) = procedure.weights();
+
+ let ret = *self * self_weight + *other * other_weight;
+ Ok(ret.min(f64::MAX).max(f64::MIN))
+ }
+}
+
+impl<T> Animate for Option<T>
+where
+ T: Animate,
+{
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self.as_ref(), other.as_ref()) {
+ (Some(ref this), Some(ref other)) => Ok(Some(this.animate(other, procedure)?)),
+ (None, None) => Ok(None),
+ _ => Err(()),
+ }
+ }
+}
+
+impl Animate for Au {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Au::new(self.0.animate(&other.0, procedure)?))
+ }
+}
+
+impl<T: Animate> Animate for Box<T> {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Box::new((**self).animate(&other, procedure)?))
+ }
+}
+
+impl<T> ToAnimatedValue for Option<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Option<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.map(T::to_animated_value)
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.map(T::from_animated_value)
+ }
+}
+
+impl<T> ToAnimatedValue for Vec<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Vec<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_iter().map(T::to_animated_value).collect()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.into_iter().map(T::from_animated_value).collect()
+ }
+}
+
+impl<T> ToAnimatedValue for Box<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Box<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ Box::new((*self).to_animated_value())
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Box::new(T::from_animated_value(*animated))
+ }
+}
+
+impl<T> ToAnimatedValue for Box<[T]>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = Box<[<T as ToAnimatedValue>::AnimatedValue]>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_vec()
+ .into_iter()
+ .map(T::to_animated_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated
+ .into_vec()
+ .into_iter()
+ .map(T::from_animated_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+}
+
+impl<T> ToAnimatedValue for crate::OwnedSlice<T>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = crate::OwnedSlice<<T as ToAnimatedValue>::AnimatedValue>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_box().to_animated_value().into()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Self::from(Box::from_animated_value(animated.into_box()))
+ }
+}
+
+impl<T> ToAnimatedValue for SmallVec<[T; 1]>
+where
+ T: ToAnimatedValue,
+{
+ type AnimatedValue = SmallVec<[T::AnimatedValue; 1]>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.into_iter().map(T::to_animated_value).collect()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.into_iter().map(T::from_animated_value).collect()
+ }
+}
+
+macro_rules! trivial_to_animated_value {
+ ($ty:ty) => {
+ impl $crate::values::animated::ToAnimatedValue for $ty {
+ type AnimatedValue = Self;
+
+ #[inline]
+ fn to_animated_value(self) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated
+ }
+ }
+ };
+}
+
+trivial_to_animated_value!(Au);
+trivial_to_animated_value!(LengthPercentage);
+trivial_to_animated_value!(ComputedAngle);
+trivial_to_animated_value!(ComputedUrl);
+trivial_to_animated_value!(bool);
+trivial_to_animated_value!(f32);
+trivial_to_animated_value!(i32);
+trivial_to_animated_value!(AbsoluteColor);
+trivial_to_animated_value!(crate::values::generics::color::ColorMixFlags);
+// Note: This implementation is for ToAnimatedValue of ShapeSource.
+//
+// SVGPathData uses Box<[T]>. If we want to derive ToAnimatedValue for all the
+// types, we have to do "impl ToAnimatedValue for Box<[T]>" first.
+// However, the general version of "impl ToAnimatedValue for Box<[T]>" needs to
+// clone |T| and convert it into |T::AnimatedValue|. However, for SVGPathData
+// that is unnecessary--moving |T| is sufficient. So here, we implement this
+// trait manually.
+trivial_to_animated_value!(SVGPathData);
+// FIXME: Bug 1514342, Image is not animatable, but we still need to implement
+// this to avoid adding this derive to generic::Image and all its arms. We can
+// drop this after landing Bug 1514342.
+trivial_to_animated_value!(Image);
+
+impl ToAnimatedZero for Au {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Au(0))
+ }
+}
+
+impl ToAnimatedZero for f32 {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(0.)
+ }
+}
+
+impl ToAnimatedZero for f64 {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(0.)
+ }
+}
+
+impl ToAnimatedZero for i32 {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(0)
+ }
+}
+
+impl<T> ToAnimatedZero for Box<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Box::new((**self).to_animated_zero()?))
+ }
+}
+
+impl<T> ToAnimatedZero for Option<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ match *self {
+ Some(ref value) => Ok(Some(value.to_animated_zero()?)),
+ None => Ok(None),
+ }
+ }
+}
+
+impl<T> ToAnimatedZero for Vec<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ self.iter().map(|v| v.to_animated_zero()).collect()
+ }
+}
+
+impl<T> ToAnimatedZero for Box<[T]>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ self.iter().map(|v| v.to_animated_zero()).collect()
+ }
+}
+
+impl<T> ToAnimatedZero for crate::OwnedSlice<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ self.iter().map(|v| v.to_animated_zero()).collect()
+ }
+}
+
+impl<T> ToAnimatedZero for crate::ArcSlice<T>
+where
+ T: ToAnimatedZero,
+{
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ let v = self
+ .iter()
+ .map(|v| v.to_animated_zero())
+ .collect::<Result<Vec<_>, _>>()?;
+ Ok(crate::ArcSlice::from_iter(v.into_iter()))
+ }
+}
diff --git a/servo/components/style/values/animated/svg.rs b/servo/components/style/values/animated/svg.rs
new file mode 100644
index 0000000000..04e35098ad
--- /dev/null
+++ b/servo/components/style/values/animated/svg.rs
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Animation implementations for various SVG-related types.
+
+use super::{Animate, Procedure};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::svg::SVGStrokeDashArray;
+
+/// <https://www.w3.org/TR/SVG11/painting.html#StrokeDasharrayProperty>
+impl<L> Animate for SVGStrokeDashArray<L>
+where
+ L: Clone + Animate,
+{
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) {
+ // Non-additive.
+ return Err(());
+ }
+ match (self, other) {
+ (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => {
+ Ok(SVGStrokeDashArray::Values(
+ super::lists::repeatable_list::animate(this, other, procedure)?,
+ ))
+ },
+ _ => Err(()),
+ }
+ }
+}
+
+impl<L> ComputeSquaredDistance for SVGStrokeDashArray<L>
+where
+ L: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ match (self, other) {
+ (&SVGStrokeDashArray::Values(ref this), &SVGStrokeDashArray::Values(ref other)) => {
+ super::lists::repeatable_list::squared_distance(this, other)
+ },
+ _ => Err(()),
+ }
+ }
+}
diff --git a/servo/components/style/values/animated/transform.rs b/servo/components/style/values/animated/transform.rs
new file mode 100644
index 0000000000..b91e3ed8bc
--- /dev/null
+++ b/servo/components/style/values/animated/transform.rs
@@ -0,0 +1,1667 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Animated types for transform.
+// There are still some implementation on Matrix3D in animated_properties.mako.rs
+// because they still need mako to generate the code.
+
+use super::animate_multiplicative_factor;
+use super::{Animate, Procedure, ToAnimatedZero};
+use crate::values::computed::transform::Rotate as ComputedRotate;
+use crate::values::computed::transform::Scale as ComputedScale;
+use crate::values::computed::transform::Transform as ComputedTransform;
+use crate::values::computed::transform::TransformOperation as ComputedTransformOperation;
+use crate::values::computed::transform::Translate as ComputedTranslate;
+use crate::values::computed::transform::{DirectionVector, Matrix, Matrix3D};
+use crate::values::computed::Angle;
+use crate::values::computed::{Length, LengthPercentage};
+use crate::values::computed::{Number, Percentage};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::transform::{self, Transform, TransformOperation};
+use crate::values::generics::transform::{Rotate, Scale, Translate};
+use crate::values::CSSFloat;
+use crate::Zero;
+use std::cmp;
+use std::ops::Add;
+
+// ------------------------------------
+// Animations for Matrix/Matrix3D.
+// ------------------------------------
+/// A 2d matrix for interpolation.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[allow(missing_docs)]
+// FIXME: We use custom derive for ComputeSquaredDistance. However, If possible, we should convert
+// the InnerMatrix2D into types with physical meaning. This custom derive computes the squared
+// distance from each matrix item, and this makes the result different from that in Gecko if we
+// have skew factor in the Matrix3D.
+pub struct InnerMatrix2D {
+ pub m11: CSSFloat,
+ pub m12: CSSFloat,
+ pub m21: CSSFloat,
+ pub m22: CSSFloat,
+}
+
+impl Animate for InnerMatrix2D {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(InnerMatrix2D {
+ m11: animate_multiplicative_factor(self.m11, other.m11, procedure)?,
+ m12: self.m12.animate(&other.m12, procedure)?,
+ m21: self.m21.animate(&other.m21, procedure)?,
+ m22: animate_multiplicative_factor(self.m22, other.m22, procedure)?,
+ })
+ }
+}
+
+/// A 2d translation function.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)]
+pub struct Translate2D(f32, f32);
+
+/// A 2d scale function.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Scale2D(f32, f32);
+
+impl Animate for Scale2D {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Scale2D(
+ animate_multiplicative_factor(self.0, other.0, procedure)?,
+ animate_multiplicative_factor(self.1, other.1, procedure)?,
+ ))
+ }
+}
+
+/// A decomposed 2d matrix.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct MatrixDecomposed2D {
+ /// The translation function.
+ pub translate: Translate2D,
+ /// The scale function.
+ pub scale: Scale2D,
+ /// The rotation angle.
+ pub angle: f32,
+ /// The inner matrix.
+ pub matrix: InnerMatrix2D,
+}
+
+impl Animate for MatrixDecomposed2D {
+ /// <https://drafts.csswg.org/css-transforms/#interpolation-of-decomposed-2d-matrix-values>
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ // If x-axis of one is flipped, and y-axis of the other,
+ // convert to an unflipped rotation.
+ let mut scale = self.scale;
+ let mut angle = self.angle;
+ let mut other_angle = other.angle;
+ if (scale.0 < 0.0 && other.scale.1 < 0.0) || (scale.1 < 0.0 && other.scale.0 < 0.0) {
+ scale.0 = -scale.0;
+ scale.1 = -scale.1;
+ angle += if angle < 0.0 { 180. } else { -180. };
+ }
+
+ // Don't rotate the long way around.
+ if angle == 0.0 {
+ angle = 360.
+ }
+ if other_angle == 0.0 {
+ other_angle = 360.
+ }
+
+ if (angle - other_angle).abs() > 180. {
+ if angle > other_angle {
+ angle -= 360.
+ } else {
+ other_angle -= 360.
+ }
+ }
+
+ // Interpolate all values.
+ let translate = self.translate.animate(&other.translate, procedure)?;
+ let scale = scale.animate(&other.scale, procedure)?;
+ let angle = angle.animate(&other_angle, procedure)?;
+ let matrix = self.matrix.animate(&other.matrix, procedure)?;
+
+ Ok(MatrixDecomposed2D {
+ translate,
+ scale,
+ angle,
+ matrix,
+ })
+ }
+}
+
+impl ComputeSquaredDistance for MatrixDecomposed2D {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // Use Radian to compute the distance.
+ const RAD_PER_DEG: f64 = std::f64::consts::PI / 180.0;
+ let angle1 = self.angle as f64 * RAD_PER_DEG;
+ let angle2 = other.angle as f64 * RAD_PER_DEG;
+ Ok(self.translate.compute_squared_distance(&other.translate)? +
+ self.scale.compute_squared_distance(&other.scale)? +
+ angle1.compute_squared_distance(&angle2)? +
+ self.matrix.compute_squared_distance(&other.matrix)?)
+ }
+}
+
+impl From<Matrix3D> for MatrixDecomposed2D {
+ /// Decompose a 2D matrix.
+ /// <https://drafts.csswg.org/css-transforms/#decomposing-a-2d-matrix>
+ fn from(matrix: Matrix3D) -> MatrixDecomposed2D {
+ let mut row0x = matrix.m11;
+ let mut row0y = matrix.m12;
+ let mut row1x = matrix.m21;
+ let mut row1y = matrix.m22;
+
+ let translate = Translate2D(matrix.m41, matrix.m42);
+ let mut scale = Scale2D(
+ (row0x * row0x + row0y * row0y).sqrt(),
+ (row1x * row1x + row1y * row1y).sqrt(),
+ );
+
+ // If determinant is negative, one axis was flipped.
+ let determinant = row0x * row1y - row0y * row1x;
+ if determinant < 0. {
+ if row0x < row1y {
+ scale.0 = -scale.0;
+ } else {
+ scale.1 = -scale.1;
+ }
+ }
+
+ // Renormalize matrix to remove scale.
+ if scale.0 != 0.0 {
+ row0x *= 1. / scale.0;
+ row0y *= 1. / scale.0;
+ }
+ if scale.1 != 0.0 {
+ row1x *= 1. / scale.1;
+ row1y *= 1. / scale.1;
+ }
+
+ // Compute rotation and renormalize matrix.
+ let mut angle = row0y.atan2(row0x);
+ if angle != 0.0 {
+ let sn = -row0y;
+ let cs = row0x;
+ let m11 = row0x;
+ let m12 = row0y;
+ let m21 = row1x;
+ let m22 = row1y;
+ row0x = cs * m11 + sn * m21;
+ row0y = cs * m12 + sn * m22;
+ row1x = -sn * m11 + cs * m21;
+ row1y = -sn * m12 + cs * m22;
+ }
+
+ let m = InnerMatrix2D {
+ m11: row0x,
+ m12: row0y,
+ m21: row1x,
+ m22: row1y,
+ };
+
+ // Convert into degrees because our rotation functions expect it.
+ angle = angle.to_degrees();
+ MatrixDecomposed2D {
+ translate: translate,
+ scale: scale,
+ angle: angle,
+ matrix: m,
+ }
+ }
+}
+
+impl From<MatrixDecomposed2D> for Matrix3D {
+ /// Recompose a 2D matrix.
+ /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-2d-matrix>
+ fn from(decomposed: MatrixDecomposed2D) -> Matrix3D {
+ let mut computed_matrix = Matrix3D::identity();
+ computed_matrix.m11 = decomposed.matrix.m11;
+ computed_matrix.m12 = decomposed.matrix.m12;
+ computed_matrix.m21 = decomposed.matrix.m21;
+ computed_matrix.m22 = decomposed.matrix.m22;
+
+ // Translate matrix.
+ computed_matrix.m41 = decomposed.translate.0;
+ computed_matrix.m42 = decomposed.translate.1;
+
+ // Rotate matrix.
+ let angle = decomposed.angle.to_radians();
+ let cos_angle = angle.cos();
+ let sin_angle = angle.sin();
+
+ let mut rotate_matrix = Matrix3D::identity();
+ rotate_matrix.m11 = cos_angle;
+ rotate_matrix.m12 = sin_angle;
+ rotate_matrix.m21 = -sin_angle;
+ rotate_matrix.m22 = cos_angle;
+
+ // Multiplication of computed_matrix and rotate_matrix
+ computed_matrix = rotate_matrix.multiply(&computed_matrix);
+
+ // Scale matrix.
+ computed_matrix.m11 *= decomposed.scale.0;
+ computed_matrix.m12 *= decomposed.scale.0;
+ computed_matrix.m21 *= decomposed.scale.1;
+ computed_matrix.m22 *= decomposed.scale.1;
+ computed_matrix
+ }
+}
+
+impl Animate for Matrix {
+ #[cfg(feature = "servo")]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let this = Matrix3D::from(*self);
+ let other = Matrix3D::from(*other);
+ let this = MatrixDecomposed2D::from(this);
+ let other = MatrixDecomposed2D::from(other);
+ Matrix3D::from(this.animate(&other, procedure)?).into_2d()
+ }
+
+ #[cfg(feature = "gecko")]
+ // Gecko doesn't exactly follow the spec here; we use a different procedure
+ // to match it
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let this = Matrix3D::from(*self);
+ let other = Matrix3D::from(*other);
+ let from = decompose_2d_matrix(&this)?;
+ let to = decompose_2d_matrix(&other)?;
+ Matrix3D::from(from.animate(&to, procedure)?).into_2d()
+ }
+}
+
+/// A 3d translation.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)]
+pub struct Translate3D(pub f32, pub f32, pub f32);
+
+/// A 3d scale function.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Scale3D(pub f32, pub f32, pub f32);
+
+impl Scale3D {
+ /// Negate self.
+ fn negate(&mut self) {
+ self.0 *= -1.0;
+ self.1 *= -1.0;
+ self.2 *= -1.0;
+ }
+}
+
+impl Animate for Scale3D {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Scale3D(
+ animate_multiplicative_factor(self.0, other.0, procedure)?,
+ animate_multiplicative_factor(self.1, other.1, procedure)?,
+ animate_multiplicative_factor(self.2, other.2, procedure)?,
+ ))
+ }
+}
+
+/// A 3d skew function.
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Animate, Clone, Copy, Debug)]
+pub struct Skew(f32, f32, f32);
+
+impl ComputeSquaredDistance for Skew {
+ // We have to use atan() to convert the skew factors into skew angles, so implement
+ // ComputeSquaredDistance manually.
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(self.0.atan().compute_squared_distance(&other.0.atan())? +
+ self.1.atan().compute_squared_distance(&other.1.atan())? +
+ self.2.atan().compute_squared_distance(&other.2.atan())?)
+ }
+}
+
+/// A 3d perspective transformation.
+#[derive(Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Perspective(pub f32, pub f32, pub f32, pub f32);
+
+impl Animate for Perspective {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(Perspective(
+ self.0.animate(&other.0, procedure)?,
+ self.1.animate(&other.1, procedure)?,
+ self.2.animate(&other.2, procedure)?,
+ animate_multiplicative_factor(self.3, other.3, procedure)?,
+ ))
+ }
+}
+
+/// A quaternion used to represent a rotation.
+#[derive(Clone, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct Quaternion(f64, f64, f64, f64);
+
+impl Quaternion {
+ /// Return a quaternion from a unit direction vector and angle (unit: radian).
+ #[inline]
+ fn from_direction_and_angle(vector: &DirectionVector, angle: f64) -> Self {
+ debug_assert!(
+ (vector.length() - 1.).abs() < 0.0001,
+ "Only accept an unit direction vector to create a quaternion"
+ );
+
+ // Quaternions between the range [360, 720] will treated as rotations at the other
+ // direction: [-360, 0]. And quaternions between the range [720*k, 720*(k+1)] will be
+ // treated as rotations [0, 720]. So it does not make sense to use quaternions to rotate
+ // the element more than ±360deg. Therefore, we have to make sure its range is (-360, 360).
+ let half_angle = angle
+ .abs()
+ .rem_euclid(std::f64::consts::TAU)
+ .copysign(angle) /
+ 2.;
+
+ // Reference:
+ // https://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation
+ //
+ // if the direction axis is (x, y, z) = xi + yj + zk,
+ // and the angle is |theta|, this formula can be done using
+ // an extension of Euler's formula:
+ // q = cos(theta/2) + (xi + yj + zk)(sin(theta/2))
+ // = cos(theta/2) +
+ // x*sin(theta/2)i + y*sin(theta/2)j + z*sin(theta/2)k
+ Quaternion(
+ vector.x as f64 * half_angle.sin(),
+ vector.y as f64 * half_angle.sin(),
+ vector.z as f64 * half_angle.sin(),
+ half_angle.cos(),
+ )
+ }
+
+ /// Calculate the dot product.
+ #[inline]
+ fn dot(&self, other: &Self) -> f64 {
+ self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3
+ }
+
+ /// Return the scaled quaternion by a factor.
+ #[inline]
+ fn scale(&self, factor: f64) -> Self {
+ Quaternion(
+ self.0 * factor,
+ self.1 * factor,
+ self.2 * factor,
+ self.3 * factor,
+ )
+ }
+}
+
+impl Add for Quaternion {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self(
+ self.0 + other.0,
+ self.1 + other.1,
+ self.2 + other.2,
+ self.3 + other.3,
+ )
+ }
+}
+
+impl Animate for Quaternion {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (this_weight, other_weight) = procedure.weights();
+ debug_assert!(
+ // Doule EPSILON since both this_weight and other_weght have calculation errors
+ // which are approximately equal to EPSILON.
+ (this_weight + other_weight - 1.0f64).abs() <= f64::EPSILON * 2.0 ||
+ other_weight == 1.0f64 ||
+ other_weight == 0.0f64,
+ "animate should only be used for interpolating or accumulating transforms"
+ );
+
+ // We take a specialized code path for accumulation (where other_weight
+ // is 1).
+ if let Procedure::Accumulate { .. } = procedure {
+ debug_assert_eq!(other_weight, 1.0);
+ if this_weight == 0.0 {
+ return Ok(*other);
+ }
+
+ let clamped_w = self.3.min(1.0).max(-1.0);
+
+ // Determine the scale factor.
+ let mut theta = clamped_w.acos();
+ let mut scale = if theta == 0.0 { 0.0 } else { 1.0 / theta.sin() };
+ theta *= this_weight;
+ scale *= theta.sin();
+
+ // Scale the self matrix by this_weight.
+ let mut scaled_self = *self;
+ scaled_self.0 *= scale;
+ scaled_self.1 *= scale;
+ scaled_self.2 *= scale;
+ scaled_self.3 = theta.cos();
+
+ // Multiply scaled-self by other.
+ let a = &scaled_self;
+ let b = other;
+ return Ok(Quaternion(
+ a.3 * b.0 + a.0 * b.3 + a.1 * b.2 - a.2 * b.1,
+ a.3 * b.1 - a.0 * b.2 + a.1 * b.3 + a.2 * b.0,
+ a.3 * b.2 + a.0 * b.1 - a.1 * b.0 + a.2 * b.3,
+ a.3 * b.3 - a.0 * b.0 - a.1 * b.1 - a.2 * b.2,
+ ));
+ }
+
+ // https://drafts.csswg.org/css-transforms-2/#interpolation-of-decomposed-3d-matrix-values
+ //
+ // Dot product, clamped between -1 and 1.
+ let cos_half_theta =
+ (self.0 * other.0 + self.1 * other.1 + self.2 * other.2 + self.3 * other.3)
+ .min(1.0)
+ .max(-1.0);
+
+ if cos_half_theta.abs() == 1.0 {
+ return Ok(*self);
+ }
+
+ let half_theta = cos_half_theta.acos();
+ let sin_half_theta = (1.0 - cos_half_theta * cos_half_theta).sqrt();
+
+ let right_weight = (other_weight * half_theta).sin() / sin_half_theta;
+ // The spec would like to use
+ // "(other_weight * half_theta).cos() - cos_half_theta * right_weight". However, this
+ // formula may produce some precision issues of floating-point number calculation, e.g.
+ // when the progress is 100% (i.e. |other_weight| is 1), the |left_weight| may not be
+ // perfectly equal to 0. It could be something like -2.22e-16, which is approximately equal
+ // to zero, in the test. And after we recompose the Matrix3D, these approximated zeros
+ // make us failed to treat this Matrix3D as a Matrix2D, when serializating it.
+ //
+ // Therefore, we use another formula to calculate |left_weight| here. Blink and WebKit also
+ // use this formula, which is defined in:
+ // https://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/slerp/index.htm
+ // https://github.com/w3c/csswg-drafts/issues/9338
+ let left_weight = (this_weight * half_theta).sin() / sin_half_theta;
+
+ Ok(self.scale(left_weight) + other.scale(right_weight))
+ }
+}
+
+impl ComputeSquaredDistance for Quaternion {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // Use quaternion vectors to get the angle difference. Both q1 and q2 are unit vectors,
+ // so we can get their angle difference by:
+ // cos(theta/2) = (q1 dot q2) / (|q1| * |q2|) = q1 dot q2.
+ let distance = self.dot(other).max(-1.0).min(1.0).acos() * 2.0;
+ Ok(SquaredDistance::from_sqrt(distance))
+ }
+}
+
+/// A decomposed 3d matrix.
+#[derive(Animate, Clone, ComputeSquaredDistance, Copy, Debug)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+pub struct MatrixDecomposed3D {
+ /// A translation function.
+ pub translate: Translate3D,
+ /// A scale function.
+ pub scale: Scale3D,
+ /// The skew component of the transformation.
+ pub skew: Skew,
+ /// The perspective component of the transformation.
+ pub perspective: Perspective,
+ /// The quaternion used to represent the rotation.
+ pub quaternion: Quaternion,
+}
+
+impl From<MatrixDecomposed3D> for Matrix3D {
+ /// Recompose a 3D matrix.
+ /// <https://drafts.csswg.org/css-transforms/#recomposing-to-a-3d-matrix>
+ fn from(decomposed: MatrixDecomposed3D) -> Matrix3D {
+ let mut matrix = Matrix3D::identity();
+
+ // Apply perspective
+ matrix.set_perspective(&decomposed.perspective);
+
+ // Apply translation
+ matrix.apply_translate(&decomposed.translate);
+
+ // Apply rotation
+ {
+ let x = decomposed.quaternion.0;
+ let y = decomposed.quaternion.1;
+ let z = decomposed.quaternion.2;
+ let w = decomposed.quaternion.3;
+
+ // Construct a composite rotation matrix from the quaternion values
+ // rotationMatrix is a identity 4x4 matrix initially
+ let mut rotation_matrix = Matrix3D::identity();
+ rotation_matrix.m11 = 1.0 - 2.0 * (y * y + z * z) as f32;
+ rotation_matrix.m12 = 2.0 * (x * y + z * w) as f32;
+ rotation_matrix.m13 = 2.0 * (x * z - y * w) as f32;
+ rotation_matrix.m21 = 2.0 * (x * y - z * w) as f32;
+ rotation_matrix.m22 = 1.0 - 2.0 * (x * x + z * z) as f32;
+ rotation_matrix.m23 = 2.0 * (y * z + x * w) as f32;
+ rotation_matrix.m31 = 2.0 * (x * z + y * w) as f32;
+ rotation_matrix.m32 = 2.0 * (y * z - x * w) as f32;
+ rotation_matrix.m33 = 1.0 - 2.0 * (x * x + y * y) as f32;
+
+ matrix = rotation_matrix.multiply(&matrix);
+ }
+
+ // Apply skew
+ {
+ let mut temp = Matrix3D::identity();
+ if decomposed.skew.2 != 0.0 {
+ temp.m32 = decomposed.skew.2;
+ matrix = temp.multiply(&matrix);
+ temp.m32 = 0.0;
+ }
+
+ if decomposed.skew.1 != 0.0 {
+ temp.m31 = decomposed.skew.1;
+ matrix = temp.multiply(&matrix);
+ temp.m31 = 0.0;
+ }
+
+ if decomposed.skew.0 != 0.0 {
+ temp.m21 = decomposed.skew.0;
+ matrix = temp.multiply(&matrix);
+ }
+ }
+
+ // Apply scale
+ matrix.apply_scale(&decomposed.scale);
+
+ matrix
+ }
+}
+
+/// Decompose a 3D matrix.
+/// https://drafts.csswg.org/css-transforms-2/#decomposing-a-3d-matrix
+/// http://www.realtimerendering.com/resources/GraphicsGems/gemsii/unmatrix.c
+fn decompose_3d_matrix(mut matrix: Matrix3D) -> Result<MatrixDecomposed3D, ()> {
+ // Combine 2 point.
+ let combine = |a: [f32; 3], b: [f32; 3], ascl: f32, bscl: f32| {
+ [
+ (ascl * a[0]) + (bscl * b[0]),
+ (ascl * a[1]) + (bscl * b[1]),
+ (ascl * a[2]) + (bscl * b[2]),
+ ]
+ };
+ // Dot product.
+ let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
+ // Cross product.
+ let cross = |row1: [f32; 3], row2: [f32; 3]| {
+ [
+ row1[1] * row2[2] - row1[2] * row2[1],
+ row1[2] * row2[0] - row1[0] * row2[2],
+ row1[0] * row2[1] - row1[1] * row2[0],
+ ]
+ };
+
+ if matrix.m44 == 0.0 {
+ return Err(());
+ }
+
+ let scaling_factor = matrix.m44;
+
+ // Normalize the matrix.
+ matrix.scale_by_factor(1.0 / scaling_factor);
+
+ // perspective_matrix is used to solve for perspective, but it also provides
+ // an easy way to test for singularity of the upper 3x3 component.
+ let mut perspective_matrix = matrix;
+
+ perspective_matrix.m14 = 0.0;
+ perspective_matrix.m24 = 0.0;
+ perspective_matrix.m34 = 0.0;
+ perspective_matrix.m44 = 1.0;
+
+ if perspective_matrix.determinant() == 0.0 {
+ return Err(());
+ }
+
+ // First, isolate perspective.
+ let perspective = if matrix.m14 != 0.0 || matrix.m24 != 0.0 || matrix.m34 != 0.0 {
+ let right_hand_side: [f32; 4] = [matrix.m14, matrix.m24, matrix.m34, matrix.m44];
+
+ perspective_matrix = perspective_matrix.inverse().unwrap().transpose();
+ let perspective = perspective_matrix.pre_mul_point4(&right_hand_side);
+ // NOTE(emilio): Even though the reference algorithm clears the
+ // fourth column here (matrix.m14..matrix.m44), they're not used below
+ // so it's not really needed.
+ Perspective(
+ perspective[0],
+ perspective[1],
+ perspective[2],
+ perspective[3],
+ )
+ } else {
+ Perspective(0.0, 0.0, 0.0, 1.0)
+ };
+
+ // Next take care of translation (easy).
+ let translate = Translate3D(matrix.m41, matrix.m42, matrix.m43);
+
+ // Now get scale and shear. 'row' is a 3 element array of 3 component vectors
+ let mut row = matrix.get_matrix_3x3_part();
+
+ // Compute X scale factor and normalize first row.
+ let row0len = (row[0][0] * row[0][0] + row[0][1] * row[0][1] + row[0][2] * row[0][2]).sqrt();
+ let mut scale = Scale3D(row0len, 0.0, 0.0);
+ row[0] = [
+ row[0][0] / row0len,
+ row[0][1] / row0len,
+ row[0][2] / row0len,
+ ];
+
+ // Compute XY shear factor and make 2nd row orthogonal to 1st.
+ let mut skew = Skew(dot(row[0], row[1]), 0.0, 0.0);
+ row[1] = combine(row[1], row[0], 1.0, -skew.0);
+
+ // Now, compute Y scale and normalize 2nd row.
+ let row1len = (row[1][0] * row[1][0] + row[1][1] * row[1][1] + row[1][2] * row[1][2]).sqrt();
+ scale.1 = row1len;
+ row[1] = [
+ row[1][0] / row1len,
+ row[1][1] / row1len,
+ row[1][2] / row1len,
+ ];
+ skew.0 /= scale.1;
+
+ // Compute XZ and YZ shears, orthogonalize 3rd row
+ skew.1 = dot(row[0], row[2]);
+ row[2] = combine(row[2], row[0], 1.0, -skew.1);
+ skew.2 = dot(row[1], row[2]);
+ row[2] = combine(row[2], row[1], 1.0, -skew.2);
+
+ // Next, get Z scale and normalize 3rd row.
+ let row2len = (row[2][0] * row[2][0] + row[2][1] * row[2][1] + row[2][2] * row[2][2]).sqrt();
+ scale.2 = row2len;
+ row[2] = [
+ row[2][0] / row2len,
+ row[2][1] / row2len,
+ row[2][2] / row2len,
+ ];
+ skew.1 /= scale.2;
+ skew.2 /= scale.2;
+
+ // At this point, the matrix (in rows) is orthonormal.
+ // Check for a coordinate system flip. If the determinant
+ // is -1, then negate the matrix and the scaling factors.
+ if dot(row[0], cross(row[1], row[2])) < 0.0 {
+ scale.negate();
+ for i in 0..3 {
+ row[i][0] *= -1.0;
+ row[i][1] *= -1.0;
+ row[i][2] *= -1.0;
+ }
+ }
+
+ // Now, get the rotations out.
+ let mut quaternion = Quaternion(
+ 0.5 * ((1.0 + row[0][0] - row[1][1] - row[2][2]).max(0.0) as f64).sqrt(),
+ 0.5 * ((1.0 - row[0][0] + row[1][1] - row[2][2]).max(0.0) as f64).sqrt(),
+ 0.5 * ((1.0 - row[0][0] - row[1][1] + row[2][2]).max(0.0) as f64).sqrt(),
+ 0.5 * ((1.0 + row[0][0] + row[1][1] + row[2][2]).max(0.0) as f64).sqrt(),
+ );
+
+ if row[2][1] > row[1][2] {
+ quaternion.0 = -quaternion.0
+ }
+ if row[0][2] > row[2][0] {
+ quaternion.1 = -quaternion.1
+ }
+ if row[1][0] > row[0][1] {
+ quaternion.2 = -quaternion.2
+ }
+
+ Ok(MatrixDecomposed3D {
+ translate,
+ scale,
+ skew,
+ perspective,
+ quaternion,
+ })
+}
+
+/**
+ * The relevant section of the transitions specification:
+ * https://drafts.csswg.org/web-animations-1/#animation-types
+ * http://dev.w3.org/csswg/css3-transitions/#animation-of-property-types-
+ * defers all of the details to the 2-D and 3-D transforms specifications.
+ * For the 2-D transforms specification (all that's relevant for us, right
+ * now), the relevant section is:
+ * https://drafts.csswg.org/css-transforms-1/#interpolation-of-transforms
+ * This, in turn, refers to the unmatrix program in Graphics Gems,
+ * available from http://graphicsgems.org/ , and in
+ * particular as the file GraphicsGems/gemsii/unmatrix.c
+ * in http://graphicsgems.org/AllGems.tar.gz
+ *
+ * The unmatrix reference is for general 3-D transform matrices (any of the
+ * 16 components can have any value).
+ *
+ * For CSS 2-D transforms, we have a 2-D matrix with the bottom row constant:
+ *
+ * [ A C E ]
+ * [ B D F ]
+ * [ 0 0 1 ]
+ *
+ * For that case, I believe the algorithm in unmatrix reduces to:
+ *
+ * (1) If A * D - B * C == 0, the matrix is singular. Fail.
+ *
+ * (2) Set translation components (Tx and Ty) to the translation parts of
+ * the matrix (E and F) and then ignore them for the rest of the time.
+ * (For us, E and F each actually consist of three constants: a
+ * length, a multiplier for the width, and a multiplier for the
+ * height. This actually requires its own decomposition, but I'll
+ * keep that separate.)
+ *
+ * (3) Let the X scale (Sx) be sqrt(A^2 + B^2). Then divide both A and B
+ * by it.
+ *
+ * (4) Let the XY shear (K) be A * C + B * D. From C, subtract A times
+ * the XY shear. From D, subtract B times the XY shear.
+ *
+ * (5) Let the Y scale (Sy) be sqrt(C^2 + D^2). Divide C, D, and the XY
+ * shear (K) by it.
+ *
+ * (6) At this point, A * D - B * C is either 1 or -1. If it is -1,
+ * negate the XY shear (K), the X scale (Sx), and A, B, C, and D.
+ * (Alternatively, we could negate the XY shear (K) and the Y scale
+ * (Sy).)
+ *
+ * (7) Let the rotation be R = atan2(B, A).
+ *
+ * Then the resulting decomposed transformation is:
+ *
+ * translate(Tx, Ty) rotate(R) skewX(atan(K)) scale(Sx, Sy)
+ *
+ * An interesting result of this is that all of the simple transform
+ * functions (i.e., all functions other than matrix()), in isolation,
+ * decompose back to themselves except for:
+ * 'skewY(φ)', which is 'matrix(1, tan(φ), 0, 1, 0, 0)', which decomposes
+ * to 'rotate(φ) skewX(φ) scale(sec(φ), cos(φ))' since (ignoring the
+ * alternate sign possibilities that would get fixed in step 6):
+ * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) =
+ * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) =
+ * sin(φ). In step 4, the XY shear is sin(φ). Thus, after step 4, C =
+ * -cos(φ)sin(φ) and D = 1 - sin²(φ) = cos²(φ). Thus, in step 5, the Y scale is
+ * sqrt(cos²(φ)(sin²(φ) + cos²(φ)) = cos(φ). Thus, after step 5, C = -sin(φ), D
+ * = cos(φ), and the XY shear is tan(φ). Thus, in step 6, A * D - B * C =
+ * cos²(φ) + sin²(φ) = 1. In step 7, the rotation is thus φ.
+ *
+ * skew(θ, φ), which is matrix(1, tan(φ), tan(θ), 1, 0, 0), which decomposes
+ * to 'rotate(φ) skewX(θ + φ) scale(sec(φ), cos(φ))' since (ignoring
+ * the alternate sign possibilities that would get fixed in step 6):
+ * In step 3, the X scale factor is sqrt(1+tan²(φ)) = sqrt(sec²(φ)) =
+ * sec(φ). Thus, after step 3, A = 1/sec(φ) = cos(φ) and B = tan(φ) / sec(φ) =
+ * sin(φ). In step 4, the XY shear is cos(φ)tan(θ) + sin(φ). Thus, after step 4,
+ * C = tan(θ) - cos(φ)(cos(φ)tan(θ) + sin(φ)) = tan(θ)sin²(φ) - cos(φ)sin(φ)
+ * D = 1 - sin(φ)(cos(φ)tan(θ) + sin(φ)) = cos²(φ) - sin(φ)cos(φ)tan(θ)
+ * Thus, in step 5, the Y scale is sqrt(C² + D²) =
+ * sqrt(tan²(θ)(sin⁴(φ) + sin²(φ)cos²(φ)) -
+ * 2 tan(θ)(sin³(φ)cos(φ) + sin(φ)cos³(φ)) +
+ * (sin²(φ)cos²(φ) + cos⁴(φ))) =
+ * sqrt(tan²(θ)sin²(φ) - 2 tan(θ)sin(φ)cos(φ) + cos²(φ)) =
+ * cos(φ) - tan(θ)sin(φ) (taking the negative of the obvious solution so
+ * we avoid flipping in step 6).
+ * After step 5, C = -sin(φ) and D = cos(φ), and the XY shear is
+ * (cos(φ)tan(θ) + sin(φ)) / (cos(φ) - tan(θ)sin(φ)) =
+ * (dividing both numerator and denominator by cos(φ))
+ * (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)) = tan(θ + φ).
+ * (See http://en.wikipedia.org/wiki/List_of_trigonometric_identities .)
+ * Thus, in step 6, A * D - B * C = cos²(φ) + sin²(φ) = 1.
+ * In step 7, the rotation is thus φ.
+ *
+ * To check this result, we can multiply things back together:
+ *
+ * [ cos(φ) -sin(φ) ] [ 1 tan(θ + φ) ] [ sec(φ) 0 ]
+ * [ sin(φ) cos(φ) ] [ 0 1 ] [ 0 cos(φ) ]
+ *
+ * [ cos(φ) cos(φ)tan(θ + φ) - sin(φ) ] [ sec(φ) 0 ]
+ * [ sin(φ) sin(φ)tan(θ + φ) + cos(φ) ] [ 0 cos(φ) ]
+ *
+ * but since tan(θ + φ) = (tan(θ) + tan(φ)) / (1 - tan(θ)tan(φ)),
+ * cos(φ)tan(θ + φ) - sin(φ)
+ * = cos(φ)(tan(θ) + tan(φ)) - sin(φ) + sin(φ)tan(θ)tan(φ)
+ * = cos(φ)tan(θ) + sin(φ) - sin(φ) + sin(φ)tan(θ)tan(φ)
+ * = cos(φ)tan(θ) + sin(φ)tan(θ)tan(φ)
+ * = tan(θ) (cos(φ) + sin(φ)tan(φ))
+ * = tan(θ) sec(φ) (cos²(φ) + sin²(φ))
+ * = tan(θ) sec(φ)
+ * and
+ * sin(φ)tan(θ + φ) + cos(φ)
+ * = sin(φ)(tan(θ) + tan(φ)) + cos(φ) - cos(φ)tan(θ)tan(φ)
+ * = tan(θ) (sin(φ) - sin(φ)) + sin(φ)tan(φ) + cos(φ)
+ * = sec(φ) (sin²(φ) + cos²(φ))
+ * = sec(φ)
+ * so the above is:
+ * [ cos(φ) tan(θ) sec(φ) ] [ sec(φ) 0 ]
+ * [ sin(φ) sec(φ) ] [ 0 cos(φ) ]
+ *
+ * [ 1 tan(θ) ]
+ * [ tan(φ) 1 ]
+ */
+
+/// Decompose a 2D matrix for Gecko. This implements the above decomposition algorithm.
+#[cfg(feature = "gecko")]
+fn decompose_2d_matrix(matrix: &Matrix3D) -> Result<MatrixDecomposed3D, ()> {
+ // The index is column-major, so the equivalent transform matrix is:
+ // | m11 m21 0 m41 | => | m11 m21 | and translate(m41, m42)
+ // | m12 m22 0 m42 | | m12 m22 |
+ // | 0 0 1 0 |
+ // | 0 0 0 1 |
+ let (mut m11, mut m12) = (matrix.m11, matrix.m12);
+ let (mut m21, mut m22) = (matrix.m21, matrix.m22);
+ // Check if this is a singular matrix.
+ if m11 * m22 == m12 * m21 {
+ return Err(());
+ }
+
+ let mut scale_x = (m11 * m11 + m12 * m12).sqrt();
+ m11 /= scale_x;
+ m12 /= scale_x;
+
+ let mut shear_xy = m11 * m21 + m12 * m22;
+ m21 -= m11 * shear_xy;
+ m22 -= m12 * shear_xy;
+
+ let scale_y = (m21 * m21 + m22 * m22).sqrt();
+ m21 /= scale_y;
+ m22 /= scale_y;
+ shear_xy /= scale_y;
+
+ let determinant = m11 * m22 - m12 * m21;
+ // Determinant should now be 1 or -1.
+ if 0.99 > determinant.abs() || determinant.abs() > 1.01 {
+ return Err(());
+ }
+
+ if determinant < 0. {
+ m11 = -m11;
+ m12 = -m12;
+ shear_xy = -shear_xy;
+ scale_x = -scale_x;
+ }
+
+ Ok(MatrixDecomposed3D {
+ translate: Translate3D(matrix.m41, matrix.m42, 0.),
+ scale: Scale3D(scale_x, scale_y, 1.),
+ skew: Skew(shear_xy, 0., 0.),
+ perspective: Perspective(0., 0., 0., 1.),
+ quaternion: Quaternion::from_direction_and_angle(
+ &DirectionVector::new(0., 0., 1.),
+ m12.atan2(m11) as f64,
+ ),
+ })
+}
+
+impl Animate for Matrix3D {
+ #[cfg(feature = "servo")]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.is_3d() || other.is_3d() {
+ let decomposed_from = decompose_3d_matrix(*self);
+ let decomposed_to = decompose_3d_matrix(*other);
+ match (decomposed_from, decomposed_to) {
+ (Ok(this), Ok(other)) => Ok(Matrix3D::from(this.animate(&other, procedure)?)),
+ // Matrices can be undecomposable due to couple reasons, e.g.,
+ // non-invertible matrices. In this case, we should report Err
+ // here, and let the caller do the fallback procedure.
+ _ => Err(()),
+ }
+ } else {
+ let this = MatrixDecomposed2D::from(*self);
+ let other = MatrixDecomposed2D::from(*other);
+ Ok(Matrix3D::from(this.animate(&other, procedure)?))
+ }
+ }
+
+ #[cfg(feature = "gecko")]
+ // Gecko doesn't exactly follow the spec here; we use a different procedure
+ // to match it
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ let (from, to) = if self.is_3d() || other.is_3d() {
+ (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?)
+ } else {
+ (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?)
+ };
+ // Matrices can be undecomposable due to couple reasons, e.g.,
+ // non-invertible matrices. In this case, we should report Err here,
+ // and let the caller do the fallback procedure.
+ Ok(Matrix3D::from(from.animate(&to, procedure)?))
+ }
+}
+
+impl ComputeSquaredDistance for Matrix3D {
+ #[inline]
+ #[cfg(feature = "servo")]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.is_3d() || other.is_3d() {
+ let from = decompose_3d_matrix(*self)?;
+ let to = decompose_3d_matrix(*other)?;
+ from.compute_squared_distance(&to)
+ } else {
+ let from = MatrixDecomposed2D::from(*self);
+ let to = MatrixDecomposed2D::from(*other);
+ from.compute_squared_distance(&to)
+ }
+ }
+
+ #[inline]
+ #[cfg(feature = "gecko")]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let (from, to) = if self.is_3d() || other.is_3d() {
+ (decompose_3d_matrix(*self)?, decompose_3d_matrix(*other)?)
+ } else {
+ (decompose_2d_matrix(self)?, decompose_2d_matrix(other)?)
+ };
+ from.compute_squared_distance(&to)
+ }
+}
+
+// ------------------------------------
+// Animation for Transform list.
+// ------------------------------------
+fn is_matched_operation(
+ first: &ComputedTransformOperation,
+ second: &ComputedTransformOperation,
+) -> bool {
+ match (first, second) {
+ (&TransformOperation::Matrix(..), &TransformOperation::Matrix(..)) |
+ (&TransformOperation::Matrix3D(..), &TransformOperation::Matrix3D(..)) |
+ (&TransformOperation::Skew(..), &TransformOperation::Skew(..)) |
+ (&TransformOperation::SkewX(..), &TransformOperation::SkewX(..)) |
+ (&TransformOperation::SkewY(..), &TransformOperation::SkewY(..)) |
+ (&TransformOperation::Rotate(..), &TransformOperation::Rotate(..)) |
+ (&TransformOperation::Rotate3D(..), &TransformOperation::Rotate3D(..)) |
+ (&TransformOperation::RotateX(..), &TransformOperation::RotateX(..)) |
+ (&TransformOperation::RotateY(..), &TransformOperation::RotateY(..)) |
+ (&TransformOperation::RotateZ(..), &TransformOperation::RotateZ(..)) |
+ (&TransformOperation::Perspective(..), &TransformOperation::Perspective(..)) => true,
+ // Match functions that have the same primitive transform function
+ (a, b) if a.is_translate() && b.is_translate() => true,
+ (a, b) if a.is_scale() && b.is_scale() => true,
+ (a, b) if a.is_rotate() && b.is_rotate() => true,
+ // InterpolateMatrix and AccumulateMatrix are for mismatched transforms
+ _ => false,
+ }
+}
+
+/// <https://drafts.csswg.org/css-transforms/#interpolation-of-transforms>
+impl Animate for ComputedTransform {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ use std::borrow::Cow;
+
+ // Addition for transforms simply means appending to the list of
+ // transform functions. This is different to how we handle the other
+ // animation procedures so we treat it separately here rather than
+ // handling it in TransformOperation.
+ if procedure == Procedure::Add {
+ let result = self.0.iter().chain(&*other.0).cloned().collect();
+ return Ok(Transform(result));
+ }
+
+ let this = Cow::Borrowed(&self.0);
+ let other = Cow::Borrowed(&other.0);
+
+ // Interpolate the common prefix
+ let mut result = this
+ .iter()
+ .zip(other.iter())
+ .take_while(|(this, other)| is_matched_operation(this, other))
+ .map(|(this, other)| this.animate(other, procedure))
+ .collect::<Result<Vec<_>, _>>()?;
+
+ // Deal with the remainders
+ let this_remainder = if this.len() > result.len() {
+ Some(&this[result.len()..])
+ } else {
+ None
+ };
+ let other_remainder = if other.len() > result.len() {
+ Some(&other[result.len()..])
+ } else {
+ None
+ };
+
+ match (this_remainder, other_remainder) {
+ // If there is a remainder from *both* lists we must have had mismatched functions.
+ // => Add the remainders to a suitable ___Matrix function.
+ (Some(this_remainder), Some(other_remainder)) => {
+ result.push(TransformOperation::animate_mismatched_transforms(
+ this_remainder,
+ other_remainder,
+ procedure,
+ )?);
+ },
+ // If there is a remainder from just one list, then one list must be shorter but
+ // completely match the type of the corresponding functions in the longer list.
+ // => Interpolate the remainder with identity transforms.
+ (Some(remainder), None) | (None, Some(remainder)) => {
+ let fill_right = this_remainder.is_some();
+ result.append(
+ &mut remainder
+ .iter()
+ .map(|transform| {
+ let identity = transform.to_animated_zero().unwrap();
+
+ match transform {
+ TransformOperation::AccumulateMatrix { .. } |
+ TransformOperation::InterpolateMatrix { .. } => {
+ let (from, to) = if fill_right {
+ (transform, &identity)
+ } else {
+ (&identity, transform)
+ };
+
+ TransformOperation::animate_mismatched_transforms(
+ &[from.clone()],
+ &[to.clone()],
+ procedure,
+ )
+ },
+ _ => {
+ let (lhs, rhs) = if fill_right {
+ (transform, &identity)
+ } else {
+ (&identity, transform)
+ };
+ lhs.animate(rhs, procedure)
+ },
+ }
+ })
+ .collect::<Result<Vec<_>, _>>()?,
+ );
+ },
+ (None, None) => {},
+ }
+
+ Ok(Transform(result.into()))
+ }
+}
+
+impl ComputeSquaredDistance for ComputedTransform {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let squared_dist = super::lists::with_zero::squared_distance(&self.0, &other.0);
+
+ // Roll back to matrix interpolation if there is any Err(()) in the
+ // transform lists, such as mismatched transform functions.
+ //
+ // FIXME: Using a zero size here seems a bit sketchy but matches the
+ // previous behavior.
+ if squared_dist.is_err() {
+ let rect = euclid::Rect::zero();
+ let matrix1: Matrix3D = self.to_transform_3d_matrix(Some(&rect))?.0.into();
+ let matrix2: Matrix3D = other.to_transform_3d_matrix(Some(&rect))?.0.into();
+ return matrix1.compute_squared_distance(&matrix2);
+ }
+
+ squared_dist
+ }
+}
+
+/// <http://dev.w3.org/csswg/css-transforms/#interpolation-of-transforms>
+impl Animate for ComputedTransformOperation {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => {
+ Ok(TransformOperation::Matrix3D(
+ this.animate(other, procedure)?,
+ ))
+ },
+ (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => {
+ Ok(TransformOperation::Matrix(this.animate(other, procedure)?))
+ },
+ (
+ &TransformOperation::Skew(ref fx, ref fy),
+ &TransformOperation::Skew(ref tx, ref ty),
+ ) => Ok(TransformOperation::Skew(
+ fx.animate(tx, procedure)?,
+ fy.animate(ty, procedure)?,
+ )),
+ (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) => {
+ Ok(TransformOperation::SkewX(f.animate(t, procedure)?))
+ },
+ (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => {
+ Ok(TransformOperation::SkewY(f.animate(t, procedure)?))
+ },
+ (
+ &TransformOperation::Translate3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Translate3D(ref tx, ref ty, ref tz),
+ ) => Ok(TransformOperation::Translate3D(
+ fx.animate(tx, procedure)?,
+ fy.animate(ty, procedure)?,
+ fz.animate(tz, procedure)?,
+ )),
+ (
+ &TransformOperation::Translate(ref fx, ref fy),
+ &TransformOperation::Translate(ref tx, ref ty),
+ ) => Ok(TransformOperation::Translate(
+ fx.animate(tx, procedure)?,
+ fy.animate(ty, procedure)?,
+ )),
+ (&TransformOperation::TranslateX(ref f), &TransformOperation::TranslateX(ref t)) => {
+ Ok(TransformOperation::TranslateX(f.animate(t, procedure)?))
+ },
+ (&TransformOperation::TranslateY(ref f), &TransformOperation::TranslateY(ref t)) => {
+ Ok(TransformOperation::TranslateY(f.animate(t, procedure)?))
+ },
+ (&TransformOperation::TranslateZ(ref f), &TransformOperation::TranslateZ(ref t)) => {
+ Ok(TransformOperation::TranslateZ(f.animate(t, procedure)?))
+ },
+ (
+ &TransformOperation::Scale3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Scale3D(ref tx, ref ty, ref tz),
+ ) => Ok(TransformOperation::Scale3D(
+ animate_multiplicative_factor(*fx, *tx, procedure)?,
+ animate_multiplicative_factor(*fy, *ty, procedure)?,
+ animate_multiplicative_factor(*fz, *tz, procedure)?,
+ )),
+ (&TransformOperation::ScaleX(ref f), &TransformOperation::ScaleX(ref t)) => Ok(
+ TransformOperation::ScaleX(animate_multiplicative_factor(*f, *t, procedure)?),
+ ),
+ (&TransformOperation::ScaleY(ref f), &TransformOperation::ScaleY(ref t)) => Ok(
+ TransformOperation::ScaleY(animate_multiplicative_factor(*f, *t, procedure)?),
+ ),
+ (&TransformOperation::ScaleZ(ref f), &TransformOperation::ScaleZ(ref t)) => Ok(
+ TransformOperation::ScaleZ(animate_multiplicative_factor(*f, *t, procedure)?),
+ ),
+ (
+ &TransformOperation::Scale(ref fx, ref fy),
+ &TransformOperation::Scale(ref tx, ref ty),
+ ) => Ok(TransformOperation::Scale(
+ animate_multiplicative_factor(*fx, *tx, procedure)?,
+ animate_multiplicative_factor(*fy, *ty, procedure)?,
+ )),
+ (
+ &TransformOperation::Rotate3D(fx, fy, fz, fa),
+ &TransformOperation::Rotate3D(tx, ty, tz, ta),
+ ) => {
+ let animated = Rotate::Rotate3D(fx, fy, fz, fa)
+ .animate(&Rotate::Rotate3D(tx, ty, tz, ta), procedure)?;
+ let (fx, fy, fz, fa) = ComputedRotate::resolve(&animated);
+ Ok(TransformOperation::Rotate3D(fx, fy, fz, fa))
+ },
+ (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) => {
+ Ok(TransformOperation::RotateX(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) => {
+ Ok(TransformOperation::RotateY(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) => {
+ Ok(TransformOperation::RotateZ(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => {
+ Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::Rotate(fa), &TransformOperation::RotateZ(ta)) => {
+ Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?))
+ },
+ (&TransformOperation::RotateZ(fa), &TransformOperation::Rotate(ta)) => {
+ Ok(TransformOperation::Rotate(fa.animate(&ta, procedure)?))
+ },
+ (
+ &TransformOperation::Perspective(ref fd),
+ &TransformOperation::Perspective(ref td),
+ ) => {
+ use crate::values::computed::CSSPixelLength;
+ use crate::values::generics::transform::create_perspective_matrix;
+
+ // From https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions:
+ //
+ // The transform functions matrix(), matrix3d() and
+ // perspective() get converted into 4x4 matrices first and
+ // interpolated as defined in section Interpolation of
+ // Matrices afterwards.
+ //
+ let from = create_perspective_matrix(fd.infinity_or(|l| l.px()));
+ let to = create_perspective_matrix(td.infinity_or(|l| l.px()));
+
+ let interpolated = Matrix3D::from(from).animate(&Matrix3D::from(to), procedure)?;
+
+ let decomposed = decompose_3d_matrix(interpolated)?;
+ let perspective_z = decomposed.perspective.2;
+ // Clamp results outside of the -1 to 0 range so that we get perspective
+ // function values between 1 and infinity.
+ let used_value = if perspective_z >= 0. {
+ transform::PerspectiveFunction::None
+ } else {
+ transform::PerspectiveFunction::Length(CSSPixelLength::new(
+ if perspective_z <= -1. {
+ 1.
+ } else {
+ -1. / perspective_z
+ },
+ ))
+ };
+ Ok(TransformOperation::Perspective(used_value))
+ },
+ _ if self.is_translate() && other.is_translate() => self
+ .to_translate_3d()
+ .animate(&other.to_translate_3d(), procedure),
+ _ if self.is_scale() && other.is_scale() => {
+ self.to_scale_3d().animate(&other.to_scale_3d(), procedure)
+ },
+ _ if self.is_rotate() && other.is_rotate() => self
+ .to_rotate_3d()
+ .animate(&other.to_rotate_3d(), procedure),
+ _ => Err(()),
+ }
+ }
+}
+
+impl ComputedTransformOperation {
+ /// If there are no size dependencies, we try to animate in-place, to avoid
+ /// creating deeply nested Interpolate* operations.
+ fn try_animate_mismatched_transforms_in_place(
+ left: &[Self],
+ right: &[Self],
+ procedure: Procedure,
+ ) -> Result<Self, ()> {
+ let (left, _left_3d) = Transform::components_to_transform_3d_matrix(left, None)?;
+ let (right, _right_3d) = Transform::components_to_transform_3d_matrix(right, None)?;
+ Ok(Self::Matrix3D(
+ Matrix3D::from(left).animate(&Matrix3D::from(right), procedure)?,
+ ))
+ }
+
+ fn animate_mismatched_transforms(
+ left: &[Self],
+ right: &[Self],
+ procedure: Procedure,
+ ) -> Result<Self, ()> {
+ if let Ok(op) = Self::try_animate_mismatched_transforms_in_place(left, right, procedure) {
+ return Ok(op);
+ }
+ let from_list = Transform(left.to_vec().into());
+ let to_list = Transform(right.to_vec().into());
+ Ok(match procedure {
+ Procedure::Add => {
+ debug_assert!(false, "Addition should've been handled earlier");
+ return Err(());
+ },
+ Procedure::Interpolate { progress } => Self::InterpolateMatrix {
+ from_list,
+ to_list,
+ progress: Percentage(progress as f32),
+ },
+ Procedure::Accumulate { count } => Self::AccumulateMatrix {
+ from_list,
+ to_list,
+ count: cmp::min(count, i32::max_value() as u64) as i32,
+ },
+ })
+ }
+}
+
+// This might not be the most useful definition of distance. It might be better, for example,
+// to trace the distance travelled by a point as its transform is interpolated between the two
+// lists. That, however, proves to be quite complicated so we take a simple approach for now.
+// See https://bugzilla.mozilla.org/show_bug.cgi?id=1318591#c0.
+impl ComputeSquaredDistance for ComputedTransformOperation {
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ match (self, other) {
+ (&TransformOperation::Matrix3D(ref this), &TransformOperation::Matrix3D(ref other)) => {
+ this.compute_squared_distance(other)
+ },
+ (&TransformOperation::Matrix(ref this), &TransformOperation::Matrix(ref other)) => {
+ let this: Matrix3D = (*this).into();
+ let other: Matrix3D = (*other).into();
+ this.compute_squared_distance(&other)
+ },
+ (
+ &TransformOperation::Skew(ref fx, ref fy),
+ &TransformOperation::Skew(ref tx, ref ty),
+ ) => Ok(fx.compute_squared_distance(&tx)? + fy.compute_squared_distance(&ty)?),
+ (&TransformOperation::SkewX(ref f), &TransformOperation::SkewX(ref t)) |
+ (&TransformOperation::SkewY(ref f), &TransformOperation::SkewY(ref t)) => {
+ f.compute_squared_distance(&t)
+ },
+ (
+ &TransformOperation::Translate3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Translate3D(ref tx, ref ty, ref tz),
+ ) => {
+ // For translate, We don't want to require doing layout in order
+ // to calculate the result, so drop the percentage part.
+ //
+ // However, dropping percentage makes us impossible to compute
+ // the distance for the percentage-percentage case, but Gecko
+ // uses the same formula, so it's fine for now.
+ let basis = Length::new(0.);
+ let fx = fx.resolve(basis).px();
+ let fy = fy.resolve(basis).px();
+ let tx = tx.resolve(basis).px();
+ let ty = ty.resolve(basis).px();
+
+ Ok(fx.compute_squared_distance(&tx)? +
+ fy.compute_squared_distance(&ty)? +
+ fz.compute_squared_distance(&tz)?)
+ },
+ (
+ &TransformOperation::Scale3D(ref fx, ref fy, ref fz),
+ &TransformOperation::Scale3D(ref tx, ref ty, ref tz),
+ ) => Ok(fx.compute_squared_distance(&tx)? +
+ fy.compute_squared_distance(&ty)? +
+ fz.compute_squared_distance(&tz)?),
+ (
+ &TransformOperation::Rotate3D(fx, fy, fz, fa),
+ &TransformOperation::Rotate3D(tx, ty, tz, ta),
+ ) => Rotate::Rotate3D(fx, fy, fz, fa)
+ .compute_squared_distance(&Rotate::Rotate3D(tx, ty, tz, ta)),
+ (&TransformOperation::RotateX(fa), &TransformOperation::RotateX(ta)) |
+ (&TransformOperation::RotateY(fa), &TransformOperation::RotateY(ta)) |
+ (&TransformOperation::RotateZ(fa), &TransformOperation::RotateZ(ta)) |
+ (&TransformOperation::Rotate(fa), &TransformOperation::Rotate(ta)) => {
+ fa.compute_squared_distance(&ta)
+ },
+ (
+ &TransformOperation::Perspective(ref fd),
+ &TransformOperation::Perspective(ref td),
+ ) => fd
+ .infinity_or(|l| l.px())
+ .compute_squared_distance(&td.infinity_or(|l| l.px())),
+ (&TransformOperation::Perspective(ref p), &TransformOperation::Matrix3D(ref m)) |
+ (&TransformOperation::Matrix3D(ref m), &TransformOperation::Perspective(ref p)) => {
+ // FIXME(emilio): Is this right? Why interpolating this with
+ // Perspective but not with anything else?
+ let mut p_matrix = Matrix3D::identity();
+ let p = p.infinity_or(|p| p.px());
+ if p >= 0. {
+ p_matrix.m34 = -1. / p.max(1.);
+ }
+ p_matrix.compute_squared_distance(&m)
+ },
+ // Gecko cross-interpolates amongst all translate and all scale
+ // functions (See ToPrimitive in layout/style/StyleAnimationValue.cpp)
+ // without falling back to InterpolateMatrix
+ _ if self.is_translate() && other.is_translate() => self
+ .to_translate_3d()
+ .compute_squared_distance(&other.to_translate_3d()),
+ _ if self.is_scale() && other.is_scale() => self
+ .to_scale_3d()
+ .compute_squared_distance(&other.to_scale_3d()),
+ _ if self.is_rotate() && other.is_rotate() => self
+ .to_rotate_3d()
+ .compute_squared_distance(&other.to_rotate_3d()),
+ _ => Err(()),
+ }
+ }
+}
+
+// ------------------------------------
+// Individual transforms.
+// ------------------------------------
+/// <https://drafts.csswg.org/css-transforms-2/#propdef-rotate>
+impl ComputedRotate {
+ fn resolve(&self) -> (Number, Number, Number, Angle) {
+ // According to the spec:
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ //
+ // If the axis is unspecified, it defaults to "0 0 1"
+ match *self {
+ Rotate::None => (0., 0., 1., Angle::zero()),
+ Rotate::Rotate3D(rx, ry, rz, angle) => (rx, ry, rz, angle),
+ Rotate::Rotate(angle) => (0., 0., 1., angle),
+ }
+ }
+}
+
+impl Animate for ComputedRotate {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ use euclid::approxeq::ApproxEq;
+ match (self, other) {
+ (&Rotate::None, &Rotate::None) => Ok(Rotate::None),
+ (&Rotate::Rotate3D(fx, fy, fz, fa), &Rotate::None) => {
+ // We always normalize direction vector for rotate3d() first, so we should also
+ // apply the same rule for rotate property. In other words, we promote none into
+ // a 3d rotate, and normalize both direction vector first, and then do
+ // interpolation.
+ let (fx, fy, fz, fa) = transform::get_normalized_vector_and_angle(fx, fy, fz, fa);
+ Ok(Rotate::Rotate3D(
+ fx,
+ fy,
+ fz,
+ fa.animate(&Angle::zero(), procedure)?,
+ ))
+ },
+ (&Rotate::None, &Rotate::Rotate3D(tx, ty, tz, ta)) => {
+ // Normalize direction vector first.
+ let (tx, ty, tz, ta) = transform::get_normalized_vector_and_angle(tx, ty, tz, ta);
+ Ok(Rotate::Rotate3D(
+ tx,
+ ty,
+ tz,
+ Angle::zero().animate(&ta, procedure)?,
+ ))
+ },
+ (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => {
+ // https://drafts.csswg.org/css-transforms-2/#interpolation-of-transform-functions
+
+ let (from, to) = (self.resolve(), other.resolve());
+ // For interpolations with the primitive rotate3d(), the direction vectors of the
+ // transform functions get normalized first.
+ let (fx, fy, fz, fa) =
+ transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3);
+ let (tx, ty, tz, ta) =
+ transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3);
+
+ // The rotation angle gets interpolated numerically and the rotation vector of the
+ // non-zero angle is used or (0, 0, 1) if both angles are zero.
+ //
+ // Note: the normalization may get two different vectors because of the
+ // floating-point precision, so we have to use approx_eq to compare two
+ // vectors.
+ let fv = DirectionVector::new(fx, fy, fz);
+ let tv = DirectionVector::new(tx, ty, tz);
+ if fa.is_zero() || ta.is_zero() || fv.approx_eq(&tv) {
+ let (x, y, z) = if fa.is_zero() && ta.is_zero() {
+ (0., 0., 1.)
+ } else if fa.is_zero() {
+ (tx, ty, tz)
+ } else {
+ // ta.is_zero() or both vectors are equal.
+ (fx, fy, fz)
+ };
+ return Ok(Rotate::Rotate3D(x, y, z, fa.animate(&ta, procedure)?));
+ }
+
+ // Slerp algorithm doesn't work well for Procedure::Add, which makes both
+ // |this_weight| and |other_weight| be 1.0, and this may make the cosine value of
+ // the angle be out of the range (i.e. the 4th component of the quaternion vector).
+ // (See Quaternion::animate() for more details about the Slerp formula.)
+ // Therefore, if the cosine value is out of range, we get an NaN after applying
+ // acos() on it, and so the result is invalid.
+ // Note: This is specialized for `rotate` property. The addition of `transform`
+ // property has been handled in `ComputedTransform::animate()` by merging two list
+ // directly.
+ let rq = if procedure == Procedure::Add {
+ // In Transform::animate(), it converts two rotations into transform matrices,
+ // and do matrix multiplication. This match the spec definition for the
+ // addition.
+ // https://drafts.csswg.org/css-transforms-2/#combining-transform-lists
+ let f = ComputedTransformOperation::Rotate3D(fx, fy, fz, fa);
+ let t = ComputedTransformOperation::Rotate3D(tx, ty, tz, ta);
+ let v =
+ Transform(vec![f].into()).animate(&Transform(vec![t].into()), procedure)?;
+ let (m, _) = v.to_transform_3d_matrix(None)?;
+ // Decompose the matrix and retrive the quaternion vector.
+ decompose_3d_matrix(Matrix3D::from(m))?.quaternion
+ } else {
+ // If the normalized vectors are not equal and both rotation angles are
+ // non-zero the transform functions get converted into 4x4 matrices first and
+ // interpolated as defined in section Interpolation of Matrices afterwards.
+ // However, per the spec issue [1], we prefer to converting the rotate3D into
+ // quaternion vectors directly, and then apply Slerp algorithm.
+ //
+ // Both ways should be identical, and converting rotate3D into quaternion
+ // vectors directly can avoid redundant math operations, e.g. the generation of
+ // the equivalent matrix3D and the unnecessary decomposition parts of
+ // translation, scale, skew, and persepctive in the matrix3D.
+ //
+ // [1] https://github.com/w3c/csswg-drafts/issues/9278
+ let fq = Quaternion::from_direction_and_angle(&fv, fa.radians64());
+ let tq = Quaternion::from_direction_and_angle(&tv, ta.radians64());
+ Quaternion::animate(&fq, &tq, procedure)?
+ };
+
+ debug_assert!(rq.3 <= 1.0 && rq.3 >= -1.0, "Invalid cosine value");
+ let (x, y, z, angle) = transform::get_normalized_vector_and_angle(
+ rq.0 as f32,
+ rq.1 as f32,
+ rq.2 as f32,
+ rq.3.acos() as f32 * 2.0,
+ );
+
+ Ok(Rotate::Rotate3D(x, y, z, Angle::from_radians(angle)))
+ },
+ (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => {
+ // If this is a 2D rotation, we just animate the <angle>
+ let (from, to) = (self.resolve().3, other.resolve().3);
+ Ok(Rotate::Rotate(from.animate(&to, procedure)?))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for ComputedRotate {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ use euclid::approxeq::ApproxEq;
+ match (self, other) {
+ (&Rotate::None, &Rotate::None) => Ok(SquaredDistance::from_sqrt(0.)),
+ (&Rotate::Rotate3D(_, _, _, a), &Rotate::None) |
+ (&Rotate::None, &Rotate::Rotate3D(_, _, _, a)) => {
+ a.compute_squared_distance(&Angle::zero())
+ },
+ (&Rotate::Rotate3D(_, ..), _) | (_, &Rotate::Rotate3D(_, ..)) => {
+ let (from, to) = (self.resolve(), other.resolve());
+ let (mut fx, mut fy, mut fz, angle1) =
+ transform::get_normalized_vector_and_angle(from.0, from.1, from.2, from.3);
+ let (mut tx, mut ty, mut tz, angle2) =
+ transform::get_normalized_vector_and_angle(to.0, to.1, to.2, to.3);
+
+ if angle1.is_zero() && angle2.is_zero() {
+ (fx, fy, fz) = (0., 0., 1.);
+ (tx, ty, tz) = (0., 0., 1.);
+ } else if angle1.is_zero() {
+ (fx, fy, fz) = (tx, ty, tz);
+ } else if angle2.is_zero() {
+ (tx, ty, tz) = (fx, fy, fz);
+ }
+
+ let v1 = DirectionVector::new(fx, fy, fz);
+ let v2 = DirectionVector::new(tx, ty, tz);
+ if v1.approx_eq(&v2) {
+ angle1.compute_squared_distance(&angle2)
+ } else {
+ let q1 = Quaternion::from_direction_and_angle(&v1, angle1.radians64());
+ let q2 = Quaternion::from_direction_and_angle(&v2, angle2.radians64());
+ q1.compute_squared_distance(&q2)
+ }
+ },
+ (&Rotate::Rotate(_), _) | (_, &Rotate::Rotate(_)) => self
+ .resolve()
+ .3
+ .compute_squared_distance(&other.resolve().3),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-transforms-2/#propdef-translate>
+impl ComputedTranslate {
+ fn resolve(&self) -> (LengthPercentage, LengthPercentage, Length) {
+ // According to the spec:
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ //
+ // Unspecified translations default to 0px
+ match *self {
+ Translate::None => (
+ LengthPercentage::zero(),
+ LengthPercentage::zero(),
+ Length::zero(),
+ ),
+ Translate::Translate(ref tx, ref ty, ref tz) => (tx.clone(), ty.clone(), tz.clone()),
+ }
+ }
+}
+
+impl Animate for ComputedTranslate {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&Translate::None, &Translate::None) => Ok(Translate::None),
+ (&Translate::Translate(_, ..), _) | (_, &Translate::Translate(_, ..)) => {
+ let (from, to) = (self.resolve(), other.resolve());
+ Ok(Translate::Translate(
+ from.0.animate(&to.0, procedure)?,
+ from.1.animate(&to.1, procedure)?,
+ from.2.animate(&to.2, procedure)?,
+ ))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for ComputedTranslate {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let (from, to) = (self.resolve(), other.resolve());
+ Ok(from.0.compute_squared_distance(&to.0)? +
+ from.1.compute_squared_distance(&to.1)? +
+ from.2.compute_squared_distance(&to.2)?)
+ }
+}
+
+/// <https://drafts.csswg.org/css-transforms-2/#propdef-scale>
+impl ComputedScale {
+ fn resolve(&self) -> (Number, Number, Number) {
+ // According to the spec:
+ // https://drafts.csswg.org/css-transforms-2/#individual-transforms
+ //
+ // Unspecified scales default to 1
+ match *self {
+ Scale::None => (1.0, 1.0, 1.0),
+ Scale::Scale(sx, sy, sz) => (sx, sy, sz),
+ }
+ }
+}
+
+impl Animate for ComputedScale {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ match (self, other) {
+ (&Scale::None, &Scale::None) => Ok(Scale::None),
+ (&Scale::Scale(_, ..), _) | (_, &Scale::Scale(_, ..)) => {
+ let (from, to) = (self.resolve(), other.resolve());
+ // For transform lists, we add by appending to the list of
+ // transform functions. However, ComputedScale cannot be
+ // simply concatenated, so we have to calculate the additive
+ // result here.
+ if procedure == Procedure::Add {
+ // scale(x1,y1,z1)*scale(x2,y2,z2) = scale(x1*x2, y1*y2, z1*z2)
+ return Ok(Scale::Scale(from.0 * to.0, from.1 * to.1, from.2 * to.2));
+ }
+ Ok(Scale::Scale(
+ animate_multiplicative_factor(from.0, to.0, procedure)?,
+ animate_multiplicative_factor(from.1, to.1, procedure)?,
+ animate_multiplicative_factor(from.2, to.2, procedure)?,
+ ))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for ComputedScale {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ let (from, to) = (self.resolve(), other.resolve());
+ Ok(from.0.compute_squared_distance(&to.0)? +
+ from.1.compute_squared_distance(&to.1)? +
+ from.2.compute_squared_distance(&to.2)?)
+ }
+}
diff --git a/servo/components/style/values/computed/align.rs b/servo/components/style/values/computed/align.rs
new file mode 100644
index 0000000000..94847fd80f
--- /dev/null
+++ b/servo/components/style/values/computed/align.rs
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Values for CSS Box Alignment properties
+//!
+//! https://drafts.csswg.org/css-align/
+
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified;
+
+pub use super::specified::{
+ AlignContent, AlignItems, AlignTracks, ContentDistribution, JustifyContent, JustifyTracks,
+ SelfAlignment,
+};
+pub use super::specified::{AlignSelf, JustifySelf};
+
+/// The computed value for the `justify-items` property.
+///
+/// Need to carry around both the specified and computed value to handle the
+/// special legacy keyword without destroying style sharing.
+///
+/// In particular, `justify-items` is a reset property, so we ought to be able
+/// to share its computed representation across elements as long as they match
+/// the same rules. Except that it's not true if the specified value for
+/// `justify-items` is `legacy` and the computed value of the parent has the
+/// `legacy` modifier.
+///
+/// So instead of computing `legacy` "normally" looking at get_parent_position(),
+/// marking it as uncacheable, we carry the specified value around and handle
+/// the special case in `StyleAdjuster` instead, only when the result of the
+/// computation would vary.
+///
+/// Note that we also need to special-case this property in matching.rs, in
+/// order to properly handle changes to the legacy keyword... This all kinda
+/// sucks :(.
+///
+/// See the discussion in https://bugzil.la/1384542.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)]
+#[repr(C)]
+pub struct ComputedJustifyItems {
+ /// The specified value for the property. Can contain the bare `legacy`
+ /// keyword.
+ #[css(skip)]
+ pub specified: specified::JustifyItems,
+ /// The computed value for the property. Cannot contain the bare `legacy`
+ /// keyword, but note that it could contain it in combination with other
+ /// keywords like `left`, `right` or `center`.
+ pub computed: specified::JustifyItems,
+}
+
+pub use self::ComputedJustifyItems as JustifyItems;
+
+impl JustifyItems {
+ /// Returns the `legacy` value.
+ pub fn legacy() -> Self {
+ Self {
+ specified: specified::JustifyItems::legacy(),
+ computed: specified::JustifyItems::normal(),
+ }
+ }
+}
+
+impl ToComputedValue for specified::JustifyItems {
+ type ComputedValue = JustifyItems;
+
+ /// <https://drafts.csswg.org/css-align/#valdef-justify-items-legacy>
+ fn to_computed_value(&self, _context: &Context) -> JustifyItems {
+ use crate::values::specified::align;
+ let specified = *self;
+ let computed = if self.0 != align::AlignFlags::LEGACY {
+ *self
+ } else {
+ // If the inherited value of `justify-items` includes the
+ // `legacy` keyword, `legacy` computes to the inherited value, but
+ // we assume it computes to `normal`, and handle that special-case
+ // in StyleAdjuster.
+ Self::normal()
+ };
+
+ JustifyItems {
+ specified,
+ computed,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &JustifyItems) -> Self {
+ computed.specified
+ }
+}
diff --git a/servo/components/style/values/computed/angle.rs b/servo/components/style/values/computed/angle.rs
new file mode 100644
index 0000000000..ea321d2233
--- /dev/null
+++ b/servo/components/style/values/computed/angle.rs
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed angles.
+
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::CSSFloat;
+use crate::Zero;
+use std::f64::consts::PI;
+use std::fmt::{self, Write};
+use std::{f32, f64};
+use style_traits::{CssWriter, ToCss};
+
+/// A computed angle in degrees.
+#[derive(
+ Add,
+ Animate,
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct Angle(CSSFloat);
+
+impl ToCss for Angle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.degrees().to_css(dest)?;
+ dest.write_str("deg")
+ }
+}
+
+const RAD_PER_DEG: f64 = PI / 180.0;
+
+impl Angle {
+ /// Creates a computed `Angle` value from a radian amount.
+ pub fn from_radians(radians: CSSFloat) -> Self {
+ Angle(radians / RAD_PER_DEG as f32)
+ }
+
+ /// Creates a computed `Angle` value from a degrees amount.
+ #[inline]
+ pub fn from_degrees(degrees: CSSFloat) -> Self {
+ Angle(degrees)
+ }
+
+ /// Returns the amount of radians this angle represents.
+ #[inline]
+ pub fn radians(&self) -> CSSFloat {
+ self.radians64().min(f32::MAX as f64).max(f32::MIN as f64) as f32
+ }
+
+ /// Returns the amount of radians this angle represents as a `f64`.
+ ///
+ /// Gecko stores angles as singles, but does this computation using doubles.
+ ///
+ /// This is significant enough to mess up rounding to the nearest
+ /// quarter-turn for 225 degrees, for example.
+ #[inline]
+ pub fn radians64(&self) -> f64 {
+ self.0 as f64 * RAD_PER_DEG
+ }
+
+ /// Return the value in degrees.
+ #[inline]
+ pub fn degrees(&self) -> CSSFloat {
+ self.0
+ }
+}
+
+impl Zero for Angle {
+ #[inline]
+ fn zero() -> Self {
+ Angle(0.0)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.0 == 0.
+ }
+}
+
+impl ComputeSquaredDistance for Angle {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // Use the formula for calculating the distance between angles defined in SVG:
+ // https://www.w3.org/TR/SVG/animate.html#complexDistances
+ self.radians64()
+ .compute_squared_distance(&other.radians64())
+ }
+}
diff --git a/servo/components/style/values/computed/animation.rs b/servo/components/style/values/computed/animation.rs
new file mode 100644
index 0000000000..626dbe5347
--- /dev/null
+++ b/servo/components/style/values/computed/animation.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed values for properties related to animations and transitions
+
+use crate::values::computed::{Context, LengthPercentage, ToComputedValue};
+use crate::values::generics::animation as generics;
+use crate::values::specified::animation as specified;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::animation::{
+ AnimationName, ScrollAxis, ScrollTimelineName, TransitionProperty, AnimationComposition,
+ AnimationDirection, AnimationFillMode, AnimationPlayState,
+};
+
+/// A computed value for the `animation-iteration-count` property.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct AnimationIterationCount(pub f32);
+
+impl ToComputedValue for specified::AnimationIterationCount {
+ type ComputedValue = AnimationIterationCount;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ AnimationIterationCount(match *self {
+ specified::AnimationIterationCount::Number(n) => n.to_computed_value(context).0,
+ specified::AnimationIterationCount::Infinite => f32::INFINITY,
+ })
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ use crate::values::specified::NonNegativeNumber;
+ if computed.0.is_infinite() {
+ specified::AnimationIterationCount::Infinite
+ } else {
+ specified::AnimationIterationCount::Number(NonNegativeNumber::new(computed.0))
+ }
+ }
+}
+
+impl AnimationIterationCount {
+ /// Returns the value `1.0`.
+ #[inline]
+ pub fn one() -> Self {
+ Self(1.0)
+ }
+}
+
+impl ToCss for AnimationIterationCount {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.0.is_infinite() {
+ dest.write_str("infinite")
+ } else {
+ self.0.to_css(dest)
+ }
+ }
+}
+
+/// A computed value for the `animation-timeline` property.
+pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>;
+
+/// A computed value for the `view-timeline-inset` property.
+pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>;
diff --git a/servo/components/style/values/computed/background.rs b/servo/components/style/values/computed/background.rs
new file mode 100644
index 0000000000..e2a58f8b74
--- /dev/null
+++ b/servo/components/style/values/computed/background.rs
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS values related to backgrounds.
+
+use crate::values::computed::length::NonNegativeLengthPercentage;
+use crate::values::generics::background::BackgroundSize as GenericBackgroundSize;
+
+pub use crate::values::specified::background::BackgroundRepeat;
+
+/// A computed value for the `background-size` property.
+pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>;
diff --git a/servo/components/style/values/computed/basic_shape.rs b/servo/components/style/values/computed/basic_shape.rs
new file mode 100644
index 0000000000..d39110ec1c
--- /dev/null
+++ b/servo/components/style/values/computed/basic_shape.rs
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the computed value of
+//! [`basic-shape`][basic-shape]s
+//!
+//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape
+
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Image, LengthPercentage, NonNegativeLengthPercentage, Position};
+use crate::values::generics::basic_shape as generic;
+
+/// A computed alias for FillRule.
+pub use crate::values::generics::basic_shape::FillRule;
+
+/// A computed `clip-path` value.
+pub type ClipPath = generic::GenericClipPath<BasicShape, ComputedUrl>;
+
+/// A computed `shape-outside` value.
+pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
+
+/// A computed basic shape.
+pub type BasicShape =
+ generic::GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, InsetRect>;
+
+/// The computed value of `inset()`.
+pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>;
+
+/// A computed circle.
+pub type Circle = generic::Circle<Position, NonNegativeLengthPercentage>;
+
+/// A computed ellipse.
+pub type Ellipse = generic::Ellipse<Position, NonNegativeLengthPercentage>;
+
+/// The computed value of `ShapeRadius`.
+pub type ShapeRadius = generic::GenericShapeRadius<NonNegativeLengthPercentage>;
diff --git a/servo/components/style/values/computed/border.rs b/servo/components/style/values/computed/border.rs
new file mode 100644
index 0000000000..e073f671b3
--- /dev/null
+++ b/servo/components/style/values/computed/border.rs
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS values related to borders.
+
+use crate::values::computed::length::{NonNegativeLength, NonNegativeLengthPercentage};
+use crate::values::computed::{NonNegativeNumber, NonNegativeNumberOrPercentage};
+use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius;
+use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice;
+use crate::values::generics::border::BorderRadius as GenericBorderRadius;
+use crate::values::generics::border::BorderSpacing as GenericBorderSpacing;
+use crate::values::generics::border::GenericBorderImageSideWidth;
+use crate::values::generics::rect::Rect;
+use crate::values::generics::size::Size2D;
+use crate::values::generics::NonNegative;
+use crate::Zero;
+use app_units::Au;
+
+pub use crate::values::specified::border::BorderImageRepeat;
+
+/// A computed value for -webkit-text-stroke-width.
+pub type LineWidth = Au;
+
+/// A computed value for border-width (and the like).
+pub type BorderSideWidth = Au;
+
+/// A computed value for the `border-image-width` property.
+pub type BorderImageWidth = Rect<BorderImageSideWidth>;
+
+/// A computed value for a single side of a `border-image-width` property.
+pub type BorderImageSideWidth =
+ GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>;
+
+/// A computed value for the `border-image-slice` property.
+pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>;
+
+/// A computed value for the `border-radius` property.
+pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>;
+
+/// A computed value for the `border-*-radius` longhand properties.
+pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>;
+
+/// A computed value for the `border-spacing` longhand property.
+pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>;
+
+impl BorderImageSideWidth {
+ /// Returns `1`.
+ #[inline]
+ pub fn one() -> Self {
+ GenericBorderImageSideWidth::Number(NonNegative(1.))
+ }
+}
+
+impl BorderImageSlice {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ GenericBorderImageSlice {
+ offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()),
+ fill: false,
+ }
+ }
+}
+
+impl BorderSpacing {
+ /// Returns `0 0`.
+ pub fn zero() -> Self {
+ GenericBorderSpacing(Size2D::new(
+ NonNegativeLength::zero(),
+ NonNegativeLength::zero(),
+ ))
+ }
+
+ /// Returns the horizontal spacing.
+ pub fn horizontal(&self) -> Au {
+ Au::from(*self.0.width())
+ }
+
+ /// Returns the vertical spacing.
+ pub fn vertical(&self) -> Au {
+ Au::from(*self.0.height())
+ }
+}
diff --git a/servo/components/style/values/computed/box.rs b/servo/components/style/values/computed/box.rs
new file mode 100644
index 0000000000..62811d9851
--- /dev/null
+++ b/servo/components/style/values/computed/box.rs
@@ -0,0 +1,388 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for box properties.
+
+use crate::values::animated::{Animate, Procedure, ToAnimatedValue};
+use crate::values::computed::font::FixedPoint;
+use crate::values::computed::length::{LengthPercentage, NonNegativeLength};
+use crate::values::computed::{Context, Integer, Number, ToComputedValue};
+use crate::values::generics::box_::{
+ GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign,
+};
+use crate::values::specified::box_ as specified;
+use std::fmt;
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::box_::{
+ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear as SpecifiedClear, Contain,
+ ContainerName, ContainerType, ContentVisibility, Display, Float as SpecifiedFloat, Overflow,
+ OverflowAnchor, OverflowClipBox, OverscrollBehavior, ScrollSnapAlign, ScrollSnapAxis,
+ ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, TouchAction, WillChange,
+};
+
+/// A computed value for the `vertical-align` property.
+pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>;
+
+/// A computed value for the `contain-intrinsic-size` property.
+pub type ContainIntrinsicSize = GenericContainIntrinsicSize<NonNegativeLength>;
+
+impl ContainIntrinsicSize {
+ /// Converts contain-intrinsic-size to auto style.
+ pub fn add_auto_if_needed(&self) -> Option<Self> {
+ Some(match *self {
+ Self::None => Self::AutoNone,
+ Self::Length(ref l) => Self::AutoLength(*l),
+ Self::AutoNone | Self::AutoLength(..) => return None,
+ })
+ }
+}
+
+/// A computed value for the `line-clamp` property.
+pub type LineClamp = GenericLineClamp<Integer>;
+
+impl Animate for LineClamp {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.is_none() != other.is_none() {
+ return Err(());
+ }
+ if self.is_none() {
+ return Ok(Self::none());
+ }
+ Ok(Self(self.0.animate(&other.0, procedure)?.max(1)))
+ }
+}
+
+/// A computed value for the `perspective` property.
+pub type Perspective = GenericPerspective<NonNegativeLength>;
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(u8)]
+/// A computed value for the `float` property.
+pub enum Float {
+ Left,
+ Right,
+ None,
+}
+
+impl Float {
+ /// Returns true if `self` is not `None`.
+ pub fn is_floating(self) -> bool {
+ self != Self::None
+ }
+}
+
+impl ToComputedValue for SpecifiedFloat {
+ type ComputedValue = Float;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let ltr = context.style().writing_mode.is_bidi_ltr();
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ match *self {
+ SpecifiedFloat::InlineStart => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Float::Left
+ } else {
+ Float::Right
+ }
+ },
+ SpecifiedFloat::InlineEnd => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Float::Right
+ } else {
+ Float::Left
+ }
+ },
+ SpecifiedFloat::Left => Float::Left,
+ SpecifiedFloat::Right => Float::Right,
+ SpecifiedFloat::None => Float::None,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedFloat {
+ match *computed {
+ Float::Left => SpecifiedFloat::Left,
+ Float::Right => SpecifiedFloat::Right,
+ Float::None => SpecifiedFloat::None,
+ }
+ }
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+)]
+/// A computed value for the `clear` property.
+#[repr(u8)]
+pub enum Clear {
+ None,
+ Left,
+ Right,
+ Both,
+}
+
+impl ToComputedValue for SpecifiedClear {
+ type ComputedValue = Clear;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let ltr = context.style().writing_mode.is_bidi_ltr();
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ match *self {
+ SpecifiedClear::InlineStart => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Clear::Left
+ } else {
+ Clear::Right
+ }
+ },
+ SpecifiedClear::InlineEnd => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if ltr {
+ Clear::Right
+ } else {
+ Clear::Left
+ }
+ },
+ SpecifiedClear::None => Clear::None,
+ SpecifiedClear::Left => Clear::Left,
+ SpecifiedClear::Right => Clear::Right,
+ SpecifiedClear::Both => Clear::Both,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> SpecifiedClear {
+ match *computed {
+ Clear::None => SpecifiedClear::None,
+ Clear::Left => SpecifiedClear::Left,
+ Clear::Right => SpecifiedClear::Right,
+ Clear::Both => SpecifiedClear::Both,
+ }
+ }
+}
+
+/// A computed value for the `resize` property.
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, ToCss, ToResolvedValue)]
+#[repr(u8)]
+pub enum Resize {
+ None,
+ Both,
+ Horizontal,
+ Vertical,
+}
+
+impl ToComputedValue for specified::Resize {
+ type ComputedValue = Resize;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Resize {
+ let is_vertical = context.style().writing_mode.is_vertical();
+ match self {
+ specified::Resize::Inline => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if is_vertical {
+ Resize::Vertical
+ } else {
+ Resize::Horizontal
+ }
+ },
+ specified::Resize::Block => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if is_vertical {
+ Resize::Horizontal
+ } else {
+ Resize::Vertical
+ }
+ },
+ specified::Resize::None => Resize::None,
+ specified::Resize::Both => Resize::Both,
+ specified::Resize::Horizontal => Resize::Horizontal,
+ specified::Resize::Vertical => Resize::Vertical,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Resize) -> specified::Resize {
+ match computed {
+ Resize::None => specified::Resize::None,
+ Resize::Both => specified::Resize::Both,
+ Resize::Horizontal => specified::Resize::Horizontal,
+ Resize::Vertical => specified::Resize::Vertical,
+ }
+ }
+}
+
+/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375).
+pub const ZOOM_FRACTION_BITS: u16 = 6;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference workaround.
+pub type ZoomFixedPoint = FixedPoint<u16, ZOOM_FRACTION_BITS>;
+
+/// The computed `zoom` property value. We store it as a 16-bit fixed point because we need to
+/// store it efficiently in the ComputedStyle representation. The assumption being that zooms over
+/// 1000 aren't quite useful.
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct Zoom(ZoomFixedPoint);
+
+impl ToComputedValue for specified::Zoom {
+ type ComputedValue = Zoom;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ let n = match *self {
+ Self::Normal => return Zoom::ONE,
+ Self::Document => return Zoom::DOCUMENT,
+ Self::Value(ref n) => n.0.to_number().get(),
+ };
+ if n == 0.0 {
+ // For legacy reasons, zoom: 0 (and 0%) computes to 1. ¯\_(ツ)_/¯
+ return Zoom::ONE;
+ }
+ Zoom(ZoomFixedPoint::from_float(n))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::new_number(computed.value())
+ }
+}
+
+impl ToCss for Zoom {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use std::fmt::Write;
+ if *self == Self::DOCUMENT {
+ return dest.write_str("document");
+ }
+ self.value().to_css(dest)
+ }
+}
+
+impl ToAnimatedValue for Zoom {
+ type AnimatedValue = Number;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.value()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Zoom(ZoomFixedPoint::from_float(animated.max(0.0)))
+ }
+}
+
+impl Zoom {
+ /// The value 1. This is by far the most common value.
+ pub const ONE: Zoom = Zoom(ZoomFixedPoint {
+ value: 1 << ZOOM_FRACTION_BITS,
+ });
+
+ /// The `document` value. This can appear in the computed zoom property value, but not in the
+ /// `effective_zoom` field.
+ pub const DOCUMENT: Zoom = Zoom(ZoomFixedPoint { value: 0 });
+
+ /// Returns whether we're the number 1.
+ #[inline]
+ pub fn is_one(self) -> bool {
+ self == Self::ONE
+ }
+
+ /// Returns the value as a float.
+ #[inline]
+ pub fn value(&self) -> f32 {
+ self.0.to_float()
+ }
+
+ /// Computes the effective zoom for a given new zoom value in rhs.
+ pub fn compute_effective(self, specified: Self) -> Self {
+ if specified == Self::DOCUMENT {
+ return Self::ONE;
+ }
+ if self == Self::ONE {
+ return specified;
+ }
+ if specified == Self::ONE {
+ return self;
+ }
+ Zoom(self.0 * specified.0)
+ }
+
+ /// Returns the zoomed value.
+ #[inline]
+ pub fn zoom(self, value: f32) -> f32 {
+ if self == Self::ONE {
+ return value;
+ }
+ self.value() * value
+ }
+}
diff --git a/servo/components/style/values/computed/color.rs b/servo/components/style/values/computed/color.rs
new file mode 100644
index 0000000000..9b5185d923
--- /dev/null
+++ b/servo/components/style/values/computed/color.rs
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed color values.
+
+use crate::color::parsing::Color as CSSParserColor;
+use crate::color::AbsoluteColor;
+use crate::values::animated::ToAnimatedZero;
+use crate::values::computed::percentage::Percentage;
+use crate::values::generics::color::{
+ GenericCaretColor, GenericColor, GenericColorMix, GenericColorOrAuto,
+};
+use std::fmt;
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::color::{ColorScheme, ForcedColorAdjust, PrintColorAdjust};
+
+/// The computed value of the `color` property.
+pub type ColorPropertyValue = AbsoluteColor;
+
+/// A computed value for `<color>`.
+pub type Color = GenericColor<Percentage>;
+
+/// A computed color-mix().
+pub type ColorMix = GenericColorMix<Color, Percentage>;
+
+impl ToCss for Color {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match *self {
+ Self::Absolute(ref c) => c.to_css(dest),
+ Self::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest),
+ Self::ColorMix(ref m) => m.to_css(dest),
+ }
+ }
+}
+
+impl Color {
+ /// A fully transparent color.
+ pub const TRANSPARENT_BLACK: Self = Self::Absolute(AbsoluteColor::TRANSPARENT_BLACK);
+
+ /// An opaque black color.
+ pub const BLACK: Self = Self::Absolute(AbsoluteColor::BLACK);
+
+ /// An opaque white color.
+ pub const WHITE: Self = Self::Absolute(AbsoluteColor::WHITE);
+
+ /// Create a new computed [`Color`] from a given color-mix, simplifying it to an absolute color
+ /// if possible.
+ pub fn from_color_mix(color_mix: ColorMix) -> Self {
+ if let Some(absolute) = color_mix.mix_to_absolute() {
+ Self::Absolute(absolute)
+ } else {
+ Self::ColorMix(Box::new(color_mix))
+ }
+ }
+
+ /// Combine this complex color with the given foreground color into an
+ /// absolute color.
+ pub fn resolve_to_absolute(&self, current_color: &AbsoluteColor) -> AbsoluteColor {
+ use crate::values::specified::percentage::ToPercentage;
+
+ match *self {
+ Self::Absolute(c) => c,
+ Self::CurrentColor => *current_color,
+ Self::ColorMix(ref mix) => {
+ let left = mix.left.resolve_to_absolute(current_color);
+ let right = mix.right.resolve_to_absolute(current_color);
+ crate::color::mix::mix(
+ mix.interpolation,
+ &left,
+ mix.left_percentage.to_percentage(),
+ &right,
+ mix.right_percentage.to_percentage(),
+ mix.flags,
+ )
+ },
+ }
+ }
+}
+
+impl ToAnimatedZero for AbsoluteColor {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(Self::TRANSPARENT_BLACK)
+ }
+}
+
+/// auto | <color>
+pub type ColorOrAuto = GenericColorOrAuto<Color>;
+
+/// caret-color
+pub type CaretColor = GenericCaretColor<Color>;
diff --git a/servo/components/style/values/computed/column.rs b/servo/components/style/values/computed/column.rs
new file mode 100644
index 0000000000..38437ea110
--- /dev/null
+++ b/servo/components/style/values/computed/column.rs
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for the column properties.
+
+use crate::values::computed::PositiveInteger;
+use crate::values::generics::column::ColumnCount as GenericColumnCount;
+
+/// A computed type for `column-count` values.
+pub type ColumnCount = GenericColumnCount<PositiveInteger>;
diff --git a/servo/components/style/values/computed/counters.rs b/servo/components/style/values/computed/counters.rs
new file mode 100644
index 0000000000..fd5e915c4a
--- /dev/null
+++ b/servo/components/style/values/computed/counters.rs
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed values for counter properties
+
+use crate::values::computed::image::Image;
+use crate::values::generics::counters as generics;
+use crate::values::generics::counters::CounterIncrement as GenericCounterIncrement;
+use crate::values::generics::counters::CounterReset as GenericCounterReset;
+use crate::values::generics::counters::CounterSet as GenericCounterSet;
+
+/// A computed value for the `counter-increment` property.
+pub type CounterIncrement = GenericCounterIncrement<i32>;
+
+/// A computed value for the `counter-reset` property.
+pub type CounterReset = GenericCounterReset<i32>;
+
+/// A computed value for the `counter-set` property.
+pub type CounterSet = GenericCounterSet<i32>;
+
+/// A computed value for the `content` property.
+pub type Content = generics::GenericContent<Image>;
+
+/// A computed content item.
+pub type ContentItem = generics::GenericContentItem<Image>;
diff --git a/servo/components/style/values/computed/easing.rs b/servo/components/style/values/computed/easing.rs
new file mode 100644
index 0000000000..d351b3c71d
--- /dev/null
+++ b/servo/components/style/values/computed/easing.rs
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS Easing functions.
+
+use euclid::approxeq::ApproxEq;
+
+use crate::bezier::Bezier;
+use crate::piecewise_linear::PiecewiseLinearFunction;
+use crate::values::computed::{Integer, Number};
+use crate::values::generics::easing::{self, BeforeFlag, StepPosition, TimingKeyword};
+
+/// A computed timing function.
+pub type ComputedTimingFunction = easing::TimingFunction<Integer, Number, PiecewiseLinearFunction>;
+
+/// An alias of the computed timing function.
+pub type TimingFunction = ComputedTimingFunction;
+
+impl ComputedTimingFunction {
+ fn calculate_step_output(
+ steps: i32,
+ pos: StepPosition,
+ progress: f64,
+ before_flag: BeforeFlag,
+ ) -> f64 {
+ // User specified values can cause overflow (bug 1706157). Increments/decrements
+ // should be gravefully handled.
+ let mut current_step = (progress * (steps as f64)).floor() as i32;
+
+ // Increment current step if it is jump-start or start.
+ if pos == StepPosition::Start ||
+ pos == StepPosition::JumpStart ||
+ pos == StepPosition::JumpBoth
+ {
+ current_step = current_step.checked_add(1).unwrap_or(current_step);
+ }
+
+ // If the "before flag" is set and we are at a transition point,
+ // drop back a step
+ if before_flag == BeforeFlag::Set &&
+ (progress * steps as f64).rem_euclid(1.0).approx_eq(&0.0)
+ {
+ current_step = current_step.checked_sub(1).unwrap_or(current_step);
+ }
+
+ // We should not produce a result outside [0, 1] unless we have an
+ // input outside that range. This takes care of steps that would otherwise
+ // occur at boundaries.
+ if progress >= 0.0 && current_step < 0 {
+ current_step = 0;
+ }
+
+ // |jumps| should always be in [1, i32::MAX].
+ let jumps = if pos == StepPosition::JumpBoth {
+ steps.checked_add(1).unwrap_or(steps)
+ } else if pos == StepPosition::JumpNone {
+ steps.checked_sub(1).unwrap_or(steps)
+ } else {
+ steps
+ };
+
+ if progress <= 1.0 && current_step > jumps {
+ current_step = jumps;
+ }
+
+ (current_step as f64) / (jumps as f64)
+ }
+
+ /// The output of the timing function given the progress ratio of this animation.
+ pub fn calculate_output(&self, progress: f64, before_flag: BeforeFlag, epsilon: f64) -> f64 {
+ let progress = match self {
+ TimingFunction::CubicBezier { x1, y1, x2, y2 } => {
+ Bezier::calculate_bezier_output(progress, epsilon, *x1, *y1, *x2, *y2)
+ },
+ TimingFunction::Steps(steps, pos) => {
+ Self::calculate_step_output(*steps, *pos, progress, before_flag)
+ },
+ TimingFunction::LinearFunction(function) => function.at(progress as f32).into(),
+ TimingFunction::Keyword(keyword) => match keyword {
+ TimingKeyword::Linear => progress,
+ TimingKeyword::Ease => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0.25, 0.1, 0.25, 1.)
+ },
+ TimingKeyword::EaseIn => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 1., 1.)
+ },
+ TimingKeyword::EaseOut => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0., 0., 0.58, 1.)
+ },
+ TimingKeyword::EaseInOut => {
+ Bezier::calculate_bezier_output(progress, epsilon, 0.42, 0., 0.58, 1.)
+ },
+ },
+ };
+
+ // The output progress value of an easing function is a real number in the range:
+ // [-inf, inf].
+ // https://drafts.csswg.org/css-easing-1/#output-progress-value
+ //
+ // However, we expect to use the finite progress for interpolation and web-animations
+ // https://drafts.csswg.org/css-values-4/#interpolation
+ // https://drafts.csswg.org/web-animations-1/#dom-computedeffecttiming-progress
+ //
+ // So we clamp the infinite progress, per the spec issue:
+ // https://github.com/w3c/csswg-drafts/issues/8344
+ progress.min(f64::MAX).max(f64::MIN)
+ }
+}
diff --git a/servo/components/style/values/computed/effects.rs b/servo/components/style/values/computed/effects.rs
new file mode 100644
index 0000000000..b0a92024ca
--- /dev/null
+++ b/servo/components/style/values/computed/effects.rs
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS values related to effects.
+
+use crate::values::computed::color::Color;
+use crate::values::computed::length::{Length, NonNegativeLength};
+#[cfg(feature = "gecko")]
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Angle, NonNegativeNumber, ZeroToOneNumber};
+use crate::values::generics::effects::BoxShadow as GenericBoxShadow;
+use crate::values::generics::effects::Filter as GenericFilter;
+use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow;
+#[cfg(not(feature = "gecko"))]
+use crate::values::Impossible;
+
+/// A computed value for a single shadow of the `box-shadow` property.
+pub type BoxShadow = GenericBoxShadow<Color, Length, NonNegativeLength, Length>;
+
+/// A computed value for a single `filter`.
+#[cfg(feature = "gecko")]
+pub type Filter = GenericFilter<
+ Angle,
+ NonNegativeNumber,
+ ZeroToOneNumber,
+ NonNegativeLength,
+ SimpleShadow,
+ ComputedUrl,
+>;
+
+/// A computed value for a single `filter`.
+#[cfg(feature = "servo")]
+pub type Filter = GenericFilter<
+ Angle,
+ NonNegativeNumber,
+ ZeroToOneNumber,
+ NonNegativeLength,
+ Impossible,
+ Impossible,
+>;
+
+/// A computed value for the `drop-shadow()` filter.
+pub type SimpleShadow = GenericSimpleShadow<Color, Length, NonNegativeLength>;
diff --git a/servo/components/style/values/computed/flex.rs b/servo/components/style/values/computed/flex.rs
new file mode 100644
index 0000000000..95c497ecf6
--- /dev/null
+++ b/servo/components/style/values/computed/flex.rs
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS values related to flexbox.
+
+use crate::values::computed::Size;
+use crate::values::generics::flex::FlexBasis as GenericFlexBasis;
+
+/// A computed value for the `flex-basis` property.
+pub type FlexBasis = GenericFlexBasis<Size>;
+
+impl FlexBasis {
+ /// `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ GenericFlexBasis::Size(Size::auto())
+ }
+}
diff --git a/servo/components/style/values/computed/font.rs b/servo/components/style/values/computed/font.rs
new file mode 100644
index 0000000000..de0a5e372b
--- /dev/null
+++ b/servo/components/style/values/computed/font.rs
@@ -0,0 +1,1369 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed values for font properties
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::animated::ToAnimatedValue;
+use crate::values::computed::{
+ Angle, Context, Integer, Length, NonNegativeLength, NonNegativeNumber, Number, Percentage,
+ ToComputedValue,
+};
+use crate::values::generics::font::{
+ FeatureTagValue, FontSettings, TaggedFontValue, VariationValue,
+};
+use crate::values::generics::{font as generics, NonNegative};
+use crate::values::resolved::{Context as ResolvedContext, ToResolvedValue};
+use crate::values::specified::font::{
+ self as specified, KeywordInfo, MAX_FONT_WEIGHT, MIN_FONT_WEIGHT,
+};
+use crate::values::specified::length::{FontBaseSize, LineHeightBase, NoCalcLength};
+use crate::Atom;
+use cssparser::{serialize_identifier, CssStringWriter, Parser};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use num_traits::abs;
+use num_traits::cast::AsPrimitive;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+pub use crate::values::computed::Length as MozScriptMinSize;
+pub use crate::values::specified::font::MozScriptSizeMultiplier;
+pub use crate::values::specified::font::{FontPalette, FontSynthesis};
+pub use crate::values::specified::font::{
+ FontVariantAlternates, FontVariantEastAsian, FontVariantLigatures, FontVariantNumeric, XLang,
+ XTextScale,
+};
+pub use crate::values::specified::Integer as SpecifiedInteger;
+pub use crate::values::specified::Number as SpecifiedNumber;
+
+/// Generic template for font property type classes that use a fixed-point
+/// internal representation with `FRACTION_BITS` for the fractional part.
+///
+/// Values are constructed from and exposed as floating-point, but stored
+/// internally as fixed point, so there will be a quantization effect on
+/// fractional values, depending on the number of fractional bits used.
+///
+/// Using (16-bit) fixed-point types rather than floats for these style
+/// attributes reduces the memory footprint of gfxFontEntry and gfxFontStyle; it
+/// will also tend to reduce the number of distinct font instances that get
+/// created, particularly when styles are animated or set to arbitrary values
+/// (e.g. by sliders in the UI), which should reduce pressure on graphics
+/// resources and improve cache hit rates.
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[repr(C)]
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+pub struct FixedPoint<T, const FRACTION_BITS: u16> {
+ /// The actual representation.
+ pub value: T,
+}
+
+impl<T, const FRACTION_BITS: u16> FixedPoint<T, FRACTION_BITS>
+where
+ T: AsPrimitive<f32>,
+ f32: AsPrimitive<T>,
+ u16: AsPrimitive<T>,
+{
+ const SCALE: u16 = 1 << FRACTION_BITS;
+ const INVERSE_SCALE: f32 = 1.0 / Self::SCALE as f32;
+
+ /// Returns a fixed-point bit from a floating-point context.
+ pub fn from_float(v: f32) -> Self {
+ Self {
+ value: (v * Self::SCALE as f32).round().as_(),
+ }
+ }
+
+ /// Returns the floating-point representation.
+ pub fn to_float(&self) -> f32 {
+ self.value.as_() * Self::INVERSE_SCALE
+ }
+}
+
+// We implement this and mul below only for u16 types, because u32 types might need more care about
+// overflow. But it's not hard to implement in either case.
+impl<const FRACTION_BITS: u16> std::ops::Div for FixedPoint<u16, FRACTION_BITS> {
+ type Output = Self;
+ fn div(self, rhs: Self) -> Self {
+ Self {
+ value: (((self.value as u32) << (FRACTION_BITS as u32)) / (rhs.value as u32)) as u16,
+ }
+ }
+}
+impl<const FRACTION_BITS: u16> std::ops::Mul for FixedPoint<u16, FRACTION_BITS> {
+ type Output = Self;
+ fn mul(self, rhs: Self) -> Self {
+ Self {
+ value: (((self.value as u32) * (rhs.value as u32)) >> (FRACTION_BITS as u32)) as u16,
+ }
+ }
+}
+
+/// font-weight: range 1..1000, fractional values permitted; keywords
+/// 'normal', 'bold' aliased to 400, 700 respectively.
+///
+/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375)
+pub const FONT_WEIGHT_FRACTION_BITS: u16 = 6;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference
+/// workaround.
+pub type FontWeightFixedPoint = FixedPoint<u16, FONT_WEIGHT_FRACTION_BITS>;
+
+/// A value for the font-weight property per:
+///
+/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct FontWeight(FontWeightFixedPoint);
+impl ToAnimatedValue for FontWeight {
+ type AnimatedValue = Number;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.value()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ FontWeight::from_float(animated)
+ }
+}
+
+impl ToCss for FontWeight {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.value().to_css(dest)
+ }
+}
+
+impl FontWeight {
+ /// The `normal` keyword.
+ pub const NORMAL: FontWeight = FontWeight(FontWeightFixedPoint {
+ value: 400 << FONT_WEIGHT_FRACTION_BITS,
+ });
+
+ /// The `bold` value.
+ pub const BOLD: FontWeight = FontWeight(FontWeightFixedPoint {
+ value: 700 << FONT_WEIGHT_FRACTION_BITS,
+ });
+
+ /// The threshold from which we consider a font bold.
+ pub const BOLD_THRESHOLD: FontWeight = FontWeight(FontWeightFixedPoint {
+ value: 600 << FONT_WEIGHT_FRACTION_BITS,
+ });
+
+ /// Returns the `normal` keyword value.
+ pub fn normal() -> Self {
+ Self::NORMAL
+ }
+
+ /// Weither this weight is bold
+ pub fn is_bold(&self) -> bool {
+ *self >= Self::BOLD_THRESHOLD
+ }
+
+ /// Returns the value as a float.
+ pub fn value(&self) -> f32 {
+ self.0.to_float()
+ }
+
+ /// Construct a valid weight from a float value.
+ pub fn from_float(v: f32) -> Self {
+ Self(FixedPoint::from_float(
+ v.max(MIN_FONT_WEIGHT).min(MAX_FONT_WEIGHT),
+ ))
+ }
+
+ /// Return the bolder weight.
+ ///
+ /// See the table in:
+ /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values
+ pub fn bolder(self) -> Self {
+ let value = self.value();
+ if value < 350. {
+ return Self::NORMAL;
+ }
+ if value < 550. {
+ return Self::BOLD;
+ }
+ Self::from_float(value.max(900.))
+ }
+
+ /// Return the lighter weight.
+ ///
+ /// See the table in:
+ /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values
+ pub fn lighter(self) -> Self {
+ let value = self.value();
+ if value < 550. {
+ return Self::from_float(value.min(100.));
+ }
+ if value < 750. {
+ return Self::NORMAL;
+ }
+ Self::BOLD
+ }
+}
+
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))]
+/// The computed value of font-size
+pub struct FontSize {
+ /// The computed size, that we use to compute ems etc. This accounts for
+ /// e.g., text-zoom.
+ pub computed_size: NonNegativeLength,
+ /// The actual used size. This is the computed font size, potentially
+ /// constrained by other factors like minimum font-size settings and so on.
+ #[css(skip)]
+ pub used_size: NonNegativeLength,
+ /// If derived from a keyword, the keyword and additional transformations applied to it
+ #[css(skip)]
+ pub keyword_info: KeywordInfo,
+}
+
+impl FontSize {
+ /// The actual computed font size.
+ #[inline]
+ pub fn computed_size(&self) -> Length {
+ self.computed_size.0
+ }
+
+ /// The actual used font size.
+ #[inline]
+ pub fn used_size(&self) -> Length {
+ self.used_size.0
+ }
+
+ #[inline]
+ /// Get default value of font size.
+ pub fn medium() -> Self {
+ Self {
+ computed_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)),
+ used_size: NonNegative(Length::new(specified::FONT_MEDIUM_PX)),
+ keyword_info: KeywordInfo::medium(),
+ }
+ }
+}
+
+impl ToAnimatedValue for FontSize {
+ type AnimatedValue = Length;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.computed_size.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ FontSize {
+ computed_size: NonNegative(animated.clamp_to_non_negative()),
+ used_size: NonNegative(animated.clamp_to_non_negative()),
+ keyword_info: KeywordInfo::none(),
+ }
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq, ToComputedValue, ToResolvedValue)]
+#[cfg_attr(feature = "servo", derive(Hash, MallocSizeOf, Serialize, Deserialize))]
+/// Specifies a prioritized list of font family names or generic family names.
+#[repr(C)]
+pub struct FontFamily {
+ /// The actual list of family names.
+ pub families: FontFamilyList,
+ /// Whether this font-family came from a specified system-font.
+ pub is_system_font: bool,
+ /// Whether this is the initial font-family that might react to language
+ /// changes.
+ pub is_initial: bool,
+}
+
+macro_rules! static_font_family {
+ ($ident:ident, $family:expr) => {
+ lazy_static! {
+ static ref $ident: FontFamily = FontFamily {
+ families: FontFamilyList {
+ list: crate::ArcSlice::from_iter_leaked(std::iter::once($family)),
+ },
+ is_system_font: false,
+ is_initial: false,
+ };
+ }
+ };
+}
+
+impl FontFamily {
+ #[inline]
+ /// Get default font family as `serif` which is a generic font-family
+ pub fn serif() -> Self {
+ Self::generic(GenericFontFamily::Serif).clone()
+ }
+
+ /// Returns the font family for `-moz-bullet-font`.
+ pub(crate) fn moz_bullet() -> &'static Self {
+ static_font_family!(
+ MOZ_BULLET,
+ SingleFontFamily::FamilyName(FamilyName {
+ name: atom!("-moz-bullet-font"),
+ syntax: FontFamilyNameSyntax::Identifiers,
+ })
+ );
+
+ &*MOZ_BULLET
+ }
+
+ /// Returns a font family for a single system font.
+ pub fn for_system_font(name: &str) -> Self {
+ Self {
+ families: FontFamilyList {
+ list: crate::ArcSlice::from_iter(std::iter::once(SingleFontFamily::FamilyName(
+ FamilyName {
+ name: Atom::from(name),
+ syntax: FontFamilyNameSyntax::Identifiers,
+ },
+ ))),
+ },
+ is_system_font: true,
+ is_initial: false,
+ }
+ }
+
+ /// Returns a generic font family.
+ pub fn generic(generic: GenericFontFamily) -> &'static Self {
+ macro_rules! generic_font_family {
+ ($ident:ident, $family:ident) => {
+ static_font_family!(
+ $ident,
+ SingleFontFamily::Generic(GenericFontFamily::$family)
+ )
+ };
+ }
+
+ generic_font_family!(SERIF, Serif);
+ generic_font_family!(SANS_SERIF, SansSerif);
+ generic_font_family!(MONOSPACE, Monospace);
+ generic_font_family!(CURSIVE, Cursive);
+ generic_font_family!(FANTASY, Fantasy);
+ generic_font_family!(MOZ_EMOJI, MozEmoji);
+ generic_font_family!(SYSTEM_UI, SystemUi);
+
+ let family = match generic {
+ GenericFontFamily::None => {
+ debug_assert!(false, "Bogus caller!");
+ &*SERIF
+ },
+ GenericFontFamily::Serif => &*SERIF,
+ GenericFontFamily::SansSerif => &*SANS_SERIF,
+ GenericFontFamily::Monospace => &*MONOSPACE,
+ GenericFontFamily::Cursive => &*CURSIVE,
+ GenericFontFamily::Fantasy => &*FANTASY,
+ GenericFontFamily::MozEmoji => &*MOZ_EMOJI,
+ GenericFontFamily::SystemUi => &*SYSTEM_UI,
+ };
+ debug_assert_eq!(
+ *family.families.iter().next().unwrap(),
+ SingleFontFamily::Generic(generic)
+ );
+ family
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl MallocSizeOf for FontFamily {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ use malloc_size_of::MallocUnconditionalSizeOf;
+ // SharedFontList objects are generally measured from the pointer stored
+ // in the specified value. So only count this if the SharedFontList is
+ // unshared.
+ let shared_font_list = &self.families.list;
+ if shared_font_list.is_unique() {
+ shared_font_list.unconditional_size_of(ops)
+ } else {
+ 0
+ }
+ }
+}
+
+impl ToCss for FontFamily {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let mut iter = self.families.iter();
+ match iter.next() {
+ Some(f) => f.to_css(dest)?,
+ None => return Ok(()),
+ }
+ for family in iter {
+ dest.write_str(", ")?;
+ family.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// The name of a font family of choice.
+#[derive(
+ Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct FamilyName {
+ /// Name of the font family.
+ pub name: Atom,
+ /// Syntax of the font family.
+ pub syntax: FontFamilyNameSyntax,
+}
+
+impl FamilyName {
+ fn is_known_icon_font_family(&self) -> bool {
+ use crate::gecko_bindings::bindings;
+ unsafe { bindings::Gecko_IsKnownIconFontFamily(self.name.as_ptr()) }
+ }
+}
+
+impl ToCss for FamilyName {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ match self.syntax {
+ FontFamilyNameSyntax::Quoted => {
+ dest.write_char('"')?;
+ write!(CssStringWriter::new(dest), "{}", self.name)?;
+ dest.write_char('"')
+ },
+ FontFamilyNameSyntax::Identifiers => {
+ let mut first = true;
+ for ident in self.name.to_string().split(' ') {
+ if first {
+ first = false;
+ } else {
+ dest.write_char(' ')?;
+ }
+ debug_assert!(
+ !ident.is_empty(),
+ "Family name with leading, \
+ trailing, or consecutive white spaces should \
+ have been marked quoted by the parser"
+ );
+ serialize_identifier(ident, dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+/// Font family names must either be given quoted as strings,
+/// or unquoted as a sequence of one or more identifiers.
+#[repr(u8)]
+pub enum FontFamilyNameSyntax {
+ /// The family name was specified in a quoted form, e.g. "Font Name"
+ /// or 'Font Name'.
+ Quoted,
+
+ /// The family name was specified in an unquoted form as a sequence of
+ /// identifiers.
+ Identifiers,
+}
+
+/// A set of faces that vary in weight, width or slope.
+/// cbindgen:derive-mut-casts=true
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize, Hash))]
+#[repr(u8)]
+pub enum SingleFontFamily {
+ /// The name of a font family of choice.
+ FamilyName(FamilyName),
+ /// Generic family name.
+ Generic(GenericFontFamily),
+}
+
+fn system_ui_enabled(_: &ParserContext) -> bool {
+ static_prefs::pref!("layout.css.system-ui.enabled")
+}
+
+/// A generic font-family name.
+///
+/// The order here is important, if you change it make sure that
+/// `gfxPlatformFontList.h`s ranged array and `gfxFontFamilyList`'s
+/// sSingleGenerics are updated as well.
+///
+/// NOTE(emilio): Should be u8, but it's a u32 because of ABI issues between GCC
+/// and LLVM see https://bugs.llvm.org/show_bug.cgi?id=44228 / bug 1600735 /
+/// bug 1726515.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(u32)]
+#[allow(missing_docs)]
+pub enum GenericFontFamily {
+ /// No generic family specified, only for internal usage.
+ ///
+ /// NOTE(emilio): Gecko code relies on this variant being zero.
+ #[css(skip)]
+ None = 0,
+ Serif,
+ SansSerif,
+ #[parse(aliases = "-moz-fixed")]
+ Monospace,
+ Cursive,
+ Fantasy,
+ #[parse(condition = "system_ui_enabled")]
+ SystemUi,
+ /// An internal value for emoji font selection.
+ #[css(skip)]
+ #[cfg(feature = "gecko")]
+ MozEmoji,
+}
+
+impl GenericFontFamily {
+ /// When we disallow websites to override fonts, we ignore some generic
+ /// families that the website might specify, since they're not configured by
+ /// the user. See bug 789788 and bug 1730098.
+ pub(crate) fn valid_for_user_font_prioritization(self) -> bool {
+ match self {
+ Self::None | Self::Fantasy | Self::Cursive | Self::SystemUi | Self::MozEmoji => false,
+
+ Self::Serif | Self::SansSerif | Self::Monospace => true,
+ }
+ }
+}
+
+impl Parse for SingleFontFamily {
+ /// Parse a font-family value.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(value) = input.try_parse(|i| i.expect_string_cloned()) {
+ return Ok(SingleFontFamily::FamilyName(FamilyName {
+ name: Atom::from(&*value),
+ syntax: FontFamilyNameSyntax::Quoted,
+ }));
+ }
+
+ if let Ok(generic) = input.try_parse(|i| GenericFontFamily::parse(context, i)) {
+ return Ok(SingleFontFamily::Generic(generic));
+ }
+
+ let first_ident = input.expect_ident_cloned()?;
+ let reserved = match_ignore_ascii_case! { &first_ident,
+ // https://drafts.csswg.org/css-fonts/#propdef-font-family
+ // "Font family names that happen to be the same as a keyword value
+ // (`inherit`, `serif`, `sans-serif`, `monospace`, `fantasy`, and `cursive`)
+ // must be quoted to prevent confusion with the keywords with the same names.
+ // The keywords ‘initial’ and ‘default’ are reserved for future use
+ // and must also be quoted when used as font names.
+ // UAs must not consider these keywords as matching the <family-name> type."
+ "inherit" | "initial" | "unset" | "revert" | "default" => true,
+ _ => false,
+ };
+
+ let mut value = first_ident.as_ref().to_owned();
+ let mut serialize_quoted = value.contains(' ');
+
+ // These keywords are not allowed by themselves.
+ // The only way this value can be valid with with another keyword.
+ if reserved {
+ let ident = input.expect_ident()?;
+ serialize_quoted = serialize_quoted || ident.contains(' ');
+ value.push(' ');
+ value.push_str(&ident);
+ }
+ while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+ serialize_quoted = serialize_quoted || ident.contains(' ');
+ value.push(' ');
+ value.push_str(&ident);
+ }
+ let syntax = if serialize_quoted {
+ // For font family names which contains special white spaces, e.g.
+ // `font-family: \ a\ \ b\ \ c\ ;`, it is tricky to serialize them
+ // as identifiers correctly. Just mark them quoted so we don't need
+ // to worry about them in serialization code.
+ FontFamilyNameSyntax::Quoted
+ } else {
+ FontFamilyNameSyntax::Identifiers
+ };
+ Ok(SingleFontFamily::FamilyName(FamilyName {
+ name: Atom::from(value),
+ syntax,
+ }))
+ }
+}
+
+#[cfg(feature = "servo")]
+impl SingleFontFamily {
+ /// Get the corresponding font-family with Atom
+ pub fn from_atom(input: Atom) -> SingleFontFamily {
+ match input {
+ atom!("serif") => return SingleFontFamily::Generic(GenericFontFamily::Serif),
+ atom!("sans-serif") => return SingleFontFamily::Generic(GenericFontFamily::SansSerif),
+ atom!("cursive") => return SingleFontFamily::Generic(GenericFontFamily::Cursive),
+ atom!("fantasy") => return SingleFontFamily::Generic(GenericFontFamily::Fantasy),
+ atom!("monospace") => return SingleFontFamily::Generic(GenericFontFamily::Monospace),
+ _ => {},
+ }
+
+ match_ignore_ascii_case! { &input,
+ "serif" => return SingleFontFamily::Generic(GenericFontFamily::Serif),
+ "sans-serif" => return SingleFontFamily::Generic(GenericFontFamily::SansSerif),
+ "cursive" => return SingleFontFamily::Generic(GenericFontFamily::Cursive),
+ "fantasy" => return SingleFontFamily::Generic(GenericFontFamily::Fantasy),
+ "monospace" => return SingleFontFamily::Generic(GenericFontFamily::Monospace),
+ _ => {}
+ }
+
+ // We don't know if it's quoted or not. So we set it to
+ // quoted by default.
+ SingleFontFamily::FamilyName(FamilyName {
+ name: input,
+ syntax: FontFamilyNameSyntax::Quoted,
+ })
+ }
+}
+
+/// A list of font families.
+#[derive(Clone, Debug, ToComputedValue, ToResolvedValue, ToShmem, PartialEq, Eq)]
+#[repr(C)]
+pub struct FontFamilyList {
+ /// The actual list of font families specified.
+ pub list: crate::ArcSlice<SingleFontFamily>,
+}
+
+impl FontFamilyList {
+ /// Return iterator of SingleFontFamily
+ pub fn iter(&self) -> impl Iterator<Item = &SingleFontFamily> {
+ self.list.iter()
+ }
+
+ /// If there's a generic font family on the list which is suitable for user
+ /// font prioritization, then move it ahead of the other families in the list,
+ /// except for any families known to be ligature-based icon fonts, where using a
+ /// generic instead of the site's specified font may cause substantial breakage.
+ /// If no suitable generic is found in the list, insert the default generic ahead
+ /// of all the listed families except for known ligature-based icon fonts.
+ pub(crate) fn prioritize_first_generic_or_prepend(&mut self, generic: GenericFontFamily) {
+ let mut index_of_first_generic = None;
+ let mut target_index = None;
+
+ for (i, f) in self.iter().enumerate() {
+ match &*f {
+ SingleFontFamily::Generic(f) => {
+ if index_of_first_generic.is_none() && f.valid_for_user_font_prioritization() {
+ // If we haven't found a target position, there's nothing to do;
+ // this entry is already ahead of everything except any whitelisted
+ // icon fonts.
+ if target_index.is_none() {
+ return;
+ }
+ index_of_first_generic = Some(i);
+ break;
+ }
+ // A non-prioritized generic (e.g. cursive, fantasy) becomes the target
+ // position for prioritization, just like arbitrary named families.
+ if target_index.is_none() {
+ target_index = Some(i);
+ }
+ },
+ SingleFontFamily::FamilyName(fam) => {
+ // Target position for the first generic is in front of the first
+ // non-whitelisted icon font family we find.
+ if target_index.is_none() && !fam.is_known_icon_font_family() {
+ target_index = Some(i);
+ }
+ },
+ }
+ }
+
+ let mut new_list = self.list.iter().cloned().collect::<Vec<_>>();
+ let first_generic = match index_of_first_generic {
+ Some(i) => new_list.remove(i),
+ None => SingleFontFamily::Generic(generic),
+ };
+
+ if let Some(i) = target_index {
+ new_list.insert(i, first_generic);
+ } else {
+ new_list.push(first_generic);
+ }
+ self.list = crate::ArcSlice::from_iter(new_list.into_iter());
+ }
+
+ /// Returns whether we need to prioritize user fonts.
+ pub(crate) fn needs_user_font_prioritization(&self) -> bool {
+ self.iter().next().map_or(true, |f| match f {
+ SingleFontFamily::Generic(f) => !f.valid_for_user_font_prioritization(),
+ _ => true,
+ })
+ }
+
+ /// Return the generic ID if it is a single generic font
+ pub fn single_generic(&self) -> Option<GenericFontFamily> {
+ let mut iter = self.iter();
+ if let Some(SingleFontFamily::Generic(f)) = iter.next() {
+ if iter.next().is_none() {
+ return Some(*f);
+ }
+ }
+ None
+ }
+}
+
+/// Preserve the readability of text when font fallback occurs.
+pub type FontSizeAdjust = generics::GenericFontSizeAdjust<NonNegativeNumber>;
+
+impl FontSizeAdjust {
+ #[inline]
+ /// Default value of font-size-adjust
+ pub fn none() -> Self {
+ FontSizeAdjust::None
+ }
+}
+
+impl ToComputedValue for specified::FontSizeAdjust {
+ type ComputedValue = FontSizeAdjust;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::font_metrics::FontMetricsOrientation;
+
+ let font_metrics = |vertical| {
+ let orient = if vertical {
+ FontMetricsOrientation::MatchContextPreferVertical
+ } else {
+ FontMetricsOrientation::Horizontal
+ };
+ let metrics = context.query_font_metrics(FontBaseSize::CurrentStyle, orient, false);
+ let font_size = context.style().get_font().clone_font_size().used_size.0;
+ (metrics, font_size)
+ };
+
+ // Macro to resolve a from-font value using the given metric field. If not present,
+ // returns the fallback value, or if that is negative, resolves using ascent instead
+ // of the missing field (this is the fallback for cap-height).
+ macro_rules! resolve {
+ ($basis:ident, $value:expr, $vertical:expr, $field:ident, $fallback:expr) => {{
+ match $value {
+ specified::FontSizeAdjustFactor::Number(f) => {
+ FontSizeAdjust::$basis(f.to_computed_value(context))
+ },
+ specified::FontSizeAdjustFactor::FromFont => {
+ let (metrics, font_size) = font_metrics($vertical);
+ let ratio = if let Some(metric) = metrics.$field {
+ metric / font_size
+ } else if $fallback >= 0.0 {
+ $fallback
+ } else {
+ metrics.ascent / font_size
+ };
+ if ratio.is_nan() {
+ FontSizeAdjust::$basis(NonNegative(abs($fallback)))
+ } else {
+ FontSizeAdjust::$basis(NonNegative(ratio))
+ }
+ },
+ }
+ }};
+ }
+
+ match *self {
+ Self::None => FontSizeAdjust::None,
+ Self::ExHeight(val) => resolve!(ExHeight, val, false, x_height, 0.5),
+ Self::CapHeight(val) => {
+ resolve!(CapHeight, val, false, cap_height, -1.0 /* fall back to ascent */)
+ },
+ Self::ChWidth(val) => resolve!(ChWidth, val, false, zero_advance_measure, 0.5),
+ Self::IcWidth(val) => resolve!(IcWidth, val, false, ic_width, 1.0),
+ Self::IcHeight(val) => resolve!(IcHeight, val, true, ic_width, 1.0),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ macro_rules! case {
+ ($basis:ident, $val:expr) => {
+ Self::$basis(specified::FontSizeAdjustFactor::Number(
+ ToComputedValue::from_computed_value($val),
+ ))
+ };
+ }
+ match *computed {
+ FontSizeAdjust::None => Self::None,
+ FontSizeAdjust::ExHeight(ref val) => case!(ExHeight, val),
+ FontSizeAdjust::CapHeight(ref val) => case!(CapHeight, val),
+ FontSizeAdjust::ChWidth(ref val) => case!(ChWidth, val),
+ FontSizeAdjust::IcWidth(ref val) => case!(IcWidth, val),
+ FontSizeAdjust::IcHeight(ref val) => case!(IcHeight, val),
+ }
+ }
+}
+
+/// Use FontSettings as computed type of FontFeatureSettings.
+pub type FontFeatureSettings = FontSettings<FeatureTagValue<Integer>>;
+
+/// The computed value for font-variation-settings.
+pub type FontVariationSettings = FontSettings<VariationValue<Number>>;
+
+// The computed value of font-{feature,variation}-settings discards values
+// with duplicate tags, keeping only the last occurrence of each tag.
+fn dedup_font_settings<T>(settings_list: &mut Vec<T>)
+where
+ T: TaggedFontValue,
+{
+ if settings_list.len() > 1 {
+ settings_list.sort_by_key(|k| k.tag().0);
+ // dedup() keeps the first of any duplicates, but we want the last,
+ // so we implement it manually here.
+ let mut prev_tag = settings_list.last().unwrap().tag();
+ for i in (0..settings_list.len() - 1).rev() {
+ let cur_tag = settings_list[i].tag();
+ if cur_tag == prev_tag {
+ settings_list.remove(i);
+ }
+ prev_tag = cur_tag;
+ }
+ }
+}
+
+impl<T> ToComputedValue for FontSettings<T>
+where
+ T: ToComputedValue,
+ <T as ToComputedValue>::ComputedValue: TaggedFontValue,
+{
+ type ComputedValue = FontSettings<T::ComputedValue>;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let mut v = self
+ .0
+ .iter()
+ .map(|item| item.to_computed_value(context))
+ .collect::<Vec<_>>();
+ dedup_font_settings(&mut v);
+ FontSettings(v.into_boxed_slice())
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self(
+ computed
+ .0
+ .iter()
+ .map(T::from_computed_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice(),
+ )
+ }
+}
+
+/// font-language-override can only have a single 1-4 ASCII character
+/// OpenType "language system" tag, so we should be able to compute
+/// it and store it as a 32-bit integer
+/// (see http://www.microsoft.com/typography/otspec/languagetags.htm).
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[value_info(other_values = "normal")]
+pub struct FontLanguageOverride(pub u32);
+
+impl FontLanguageOverride {
+ #[inline]
+ /// Get computed default value of `font-language-override` with 0
+ pub fn normal() -> FontLanguageOverride {
+ FontLanguageOverride(0)
+ }
+
+ /// Returns this value as a `&str`, backed by `storage`.
+ #[inline]
+ pub(crate) fn to_str(self, storage: &mut [u8; 4]) -> &str {
+ *storage = u32::to_be_bytes(self.0);
+ // Safe because we ensure it's ASCII during parsing
+ let slice = if cfg!(debug_assertions) {
+ std::str::from_utf8(&storage[..]).unwrap()
+ } else {
+ unsafe { std::str::from_utf8_unchecked(&storage[..]) }
+ };
+ slice.trim_end()
+ }
+
+ /// Unsafe because `Self::to_str` requires the value to represent a UTF-8
+ /// string.
+ #[inline]
+ pub unsafe fn from_u32(value: u32) -> Self {
+ Self(value)
+ }
+}
+
+impl ToCss for FontLanguageOverride {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ if self.0 == 0 {
+ return dest.write_str("normal");
+ }
+ self.to_str(&mut [0; 4]).to_css(dest)
+ }
+}
+
+// FIXME(emilio): Make Gecko use the cbindgen'd fontLanguageOverride, then
+// remove this.
+#[cfg(feature = "gecko")]
+impl From<u32> for FontLanguageOverride {
+ fn from(v: u32) -> Self {
+ unsafe { Self::from_u32(v) }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl From<FontLanguageOverride> for u32 {
+ fn from(v: FontLanguageOverride) -> u32 {
+ v.0
+ }
+}
+
+impl ToComputedValue for specified::MozScriptMinSize {
+ type ComputedValue = MozScriptMinSize;
+
+ fn to_computed_value(&self, cx: &Context) -> MozScriptMinSize {
+ // this value is used in the computation of font-size, so
+ // we use the parent size
+ let base_size = FontBaseSize::InheritedStyle;
+ let line_height_base = LineHeightBase::InheritedStyle;
+ match self.0 {
+ NoCalcLength::FontRelative(value) => {
+ value.to_computed_value(cx, base_size, line_height_base)
+ },
+ NoCalcLength::ServoCharacterWidth(value) => {
+ value.to_computed_value(base_size.resolve(cx).computed_size())
+ },
+ ref l => l.to_computed_value(cx),
+ }
+ }
+
+ fn from_computed_value(other: &MozScriptMinSize) -> Self {
+ specified::MozScriptMinSize(ToComputedValue::from_computed_value(other))
+ }
+}
+
+/// The computed value of the math-depth property.
+pub type MathDepth = i8;
+
+#[cfg(feature = "gecko")]
+impl ToComputedValue for specified::MathDepth {
+ type ComputedValue = MathDepth;
+
+ fn to_computed_value(&self, cx: &Context) -> i8 {
+ use crate::properties::longhands::math_style::SpecifiedValue as MathStyleValue;
+ use std::{cmp, i8};
+
+ let int = match *self {
+ specified::MathDepth::AutoAdd => {
+ let parent = cx.builder.get_parent_font().clone_math_depth() as i32;
+ let style = cx.builder.get_parent_font().clone_math_style();
+ if style == MathStyleValue::Compact {
+ parent.saturating_add(1)
+ } else {
+ parent
+ }
+ },
+ specified::MathDepth::Add(rel) => {
+ let parent = cx.builder.get_parent_font().clone_math_depth();
+ (parent as i32).saturating_add(rel.to_computed_value(cx))
+ },
+ specified::MathDepth::Absolute(abs) => abs.to_computed_value(cx),
+ };
+ cmp::min(int, i8::MAX as i32) as i8
+ }
+
+ fn from_computed_value(other: &i8) -> Self {
+ let computed_value = *other as i32;
+ specified::MathDepth::Absolute(SpecifiedInteger::from_computed_value(&computed_value))
+ }
+}
+
+/// - Use a signed 8.8 fixed-point value (representable range -128.0..128)
+///
+/// Values of <angle> below -90 or above 90 not permitted, so we use out of
+/// range values to represent normal | oblique
+pub const FONT_STYLE_FRACTION_BITS: u16 = 8;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference
+/// workaround.
+pub type FontStyleFixedPoint = FixedPoint<i16, FONT_STYLE_FRACTION_BITS>;
+
+/// The computed value of `font-style`.
+///
+/// - Define out of range values min value (-128.0) as meaning 'normal'
+/// - Define max value (127.99609375) as 'italic'
+/// - Other values represent 'oblique <angle>'
+/// - Note that 'oblique 0deg' is distinct from 'normal' (should it be?)
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct FontStyle(FontStyleFixedPoint);
+
+impl FontStyle {
+ /// The normal keyword.
+ pub const NORMAL: FontStyle = FontStyle(FontStyleFixedPoint {
+ value: 100 << FONT_STYLE_FRACTION_BITS,
+ });
+ /// The italic keyword.
+ pub const ITALIC: FontStyle = FontStyle(FontStyleFixedPoint {
+ value: 101 << FONT_STYLE_FRACTION_BITS,
+ });
+
+ /// The default angle for `font-style: oblique`.
+ /// See also https://github.com/w3c/csswg-drafts/issues/2295
+ pub const DEFAULT_OBLIQUE_DEGREES: i16 = 14;
+
+ /// The `oblique` keyword with the default degrees.
+ pub const OBLIQUE: FontStyle = FontStyle(FontStyleFixedPoint {
+ value: Self::DEFAULT_OBLIQUE_DEGREES << FONT_STYLE_FRACTION_BITS,
+ });
+
+ /// The `normal` value.
+ #[inline]
+ pub fn normal() -> Self {
+ Self::NORMAL
+ }
+
+ /// Returns the oblique angle for this style.
+ pub fn oblique(degrees: f32) -> Self {
+ Self(FixedPoint::from_float(
+ degrees
+ .max(specified::FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES)
+ .min(specified::FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES),
+ ))
+ }
+
+ /// Returns the oblique angle for this style.
+ pub fn oblique_degrees(&self) -> f32 {
+ debug_assert_ne!(*self, Self::NORMAL);
+ debug_assert_ne!(*self, Self::ITALIC);
+ self.0.to_float()
+ }
+}
+
+impl ToCss for FontStyle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ if *self == Self::NORMAL {
+ return dest.write_str("normal");
+ }
+ if *self == Self::ITALIC {
+ return dest.write_str("italic");
+ }
+ if *self == Self::OBLIQUE {
+ return dest.write_str("oblique");
+ }
+ dest.write_str("oblique ")?;
+ let angle = Angle::from_degrees(self.oblique_degrees());
+ angle.to_css(dest)?;
+ Ok(())
+ }
+}
+
+impl ToAnimatedValue for FontStyle {
+ type AnimatedValue = generics::FontStyle<Angle>;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ if self == Self::NORMAL {
+ // This allows us to animate between normal and oblique values. Per spec,
+ // https://drafts.csswg.org/css-fonts-4/#font-style-prop:
+ // Animation type: by computed value type; 'normal' animates as 'oblique 0deg'
+ return generics::FontStyle::Oblique(Angle::from_degrees(0.0));
+ }
+ if self == Self::ITALIC {
+ return generics::FontStyle::Italic;
+ }
+ generics::FontStyle::Oblique(Angle::from_degrees(self.oblique_degrees()))
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ match animated {
+ generics::FontStyle::Normal => Self::NORMAL,
+ generics::FontStyle::Italic => Self::ITALIC,
+ generics::FontStyle::Oblique(ref angle) => {
+ if angle.degrees() == 0.0 {
+ // Reverse the conversion done in to_animated_value()
+ Self::NORMAL
+ } else {
+ Self::oblique(angle.degrees())
+ }
+ },
+ }
+ }
+}
+
+/// font-stretch is a percentage relative to normal.
+///
+/// We use an unsigned 10.6 fixed-point value (range 0.0 - 1023.984375)
+///
+/// We arbitrarily limit here to 1000%. (If that becomes a problem, we could
+/// reduce the number of fractional bits and increase the limit.)
+pub const FONT_STRETCH_FRACTION_BITS: u16 = 6;
+
+/// This is an alias which is useful mostly as a cbindgen / C++ inference
+/// workaround.
+pub type FontStretchFixedPoint = FixedPoint<u16, FONT_STRETCH_FRACTION_BITS>;
+
+/// A value for the font-stretch property per:
+///
+/// https://drafts.csswg.org/css-fonts-4/#propdef-font-stretch
+///
+/// cbindgen:derive-lt
+/// cbindgen:derive-lte
+/// cbindgen:derive-gt
+/// cbindgen:derive-gte
+#[derive(
+ Clone, ComputeSquaredDistance, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue,
+)]
+#[repr(C)]
+pub struct FontStretch(pub FontStretchFixedPoint);
+
+impl FontStretch {
+ /// The fraction bits, as an easy-to-access-constant.
+ pub const FRACTION_BITS: u16 = FONT_STRETCH_FRACTION_BITS;
+ /// 0.5 in our floating point representation.
+ pub const HALF: u16 = 1 << (Self::FRACTION_BITS - 1);
+
+ /// The `ultra-condensed` keyword.
+ pub const ULTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 50 << Self::FRACTION_BITS,
+ });
+ /// The `extra-condensed` keyword.
+ pub const EXTRA_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: (62 << Self::FRACTION_BITS) + Self::HALF,
+ });
+ /// The `condensed` keyword.
+ pub const CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 75 << Self::FRACTION_BITS,
+ });
+ /// The `semi-condensed` keyword.
+ pub const SEMI_CONDENSED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: (87 << Self::FRACTION_BITS) + Self::HALF,
+ });
+ /// The `normal` keyword.
+ pub const NORMAL: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 100 << Self::FRACTION_BITS,
+ });
+ /// The `semi-expanded` keyword.
+ pub const SEMI_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: (112 << Self::FRACTION_BITS) + Self::HALF,
+ });
+ /// The `expanded` keyword.
+ pub const EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 125 << Self::FRACTION_BITS,
+ });
+ /// The `extra-expanded` keyword.
+ pub const EXTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 150 << Self::FRACTION_BITS,
+ });
+ /// The `ultra-expanded` keyword.
+ pub const ULTRA_EXPANDED: FontStretch = FontStretch(FontStretchFixedPoint {
+ value: 200 << Self::FRACTION_BITS,
+ });
+
+ /// 100%
+ pub fn hundred() -> Self {
+ Self::NORMAL
+ }
+
+ /// Converts to a computed percentage.
+ #[inline]
+ pub fn to_percentage(&self) -> Percentage {
+ Percentage(self.0.to_float() / 100.0)
+ }
+
+ /// Converts from a computed percentage value.
+ pub fn from_percentage(p: f32) -> Self {
+ Self(FixedPoint::from_float((p * 100.).max(0.0).min(1000.0)))
+ }
+
+ /// Returns a relevant stretch value from a keyword.
+ /// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop
+ pub fn from_keyword(kw: specified::FontStretchKeyword) -> Self {
+ use specified::FontStretchKeyword::*;
+ match kw {
+ UltraCondensed => Self::ULTRA_CONDENSED,
+ ExtraCondensed => Self::EXTRA_CONDENSED,
+ Condensed => Self::CONDENSED,
+ SemiCondensed => Self::SEMI_CONDENSED,
+ Normal => Self::NORMAL,
+ SemiExpanded => Self::SEMI_EXPANDED,
+ Expanded => Self::EXPANDED,
+ ExtraExpanded => Self::EXTRA_EXPANDED,
+ UltraExpanded => Self::ULTRA_EXPANDED,
+ }
+ }
+
+ /// Returns the stretch keyword if we map to one of the relevant values.
+ pub fn as_keyword(&self) -> Option<specified::FontStretchKeyword> {
+ use specified::FontStretchKeyword::*;
+ // TODO: Can we use match here?
+ if *self == Self::ULTRA_CONDENSED {
+ return Some(UltraCondensed);
+ }
+ if *self == Self::EXTRA_CONDENSED {
+ return Some(ExtraCondensed);
+ }
+ if *self == Self::CONDENSED {
+ return Some(Condensed);
+ }
+ if *self == Self::SEMI_CONDENSED {
+ return Some(SemiCondensed);
+ }
+ if *self == Self::NORMAL {
+ return Some(Normal);
+ }
+ if *self == Self::SEMI_EXPANDED {
+ return Some(SemiExpanded);
+ }
+ if *self == Self::EXPANDED {
+ return Some(Expanded);
+ }
+ if *self == Self::EXTRA_EXPANDED {
+ return Some(ExtraExpanded);
+ }
+ if *self == Self::ULTRA_EXPANDED {
+ return Some(UltraExpanded);
+ }
+ None
+ }
+}
+
+impl ToCss for FontStretch {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.to_percentage().to_css(dest)
+ }
+}
+
+impl ToAnimatedValue for FontStretch {
+ type AnimatedValue = Percentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.to_percentage()
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Self::from_percentage(animated.0)
+ }
+}
+
+/// A computed value for the `line-height` property.
+pub type LineHeight = generics::GenericLineHeight<NonNegativeNumber, NonNegativeLength>;
+
+impl ToResolvedValue for LineHeight {
+ type ResolvedValue = Self;
+
+ fn to_resolved_value(self, context: &ResolvedContext) -> Self::ResolvedValue {
+ // Resolve <number> to an absolute <length> based on font size.
+ if matches!(self, Self::Normal | Self::MozBlockHeight) {
+ return self;
+ }
+ let wm = context.style.writing_mode;
+ Self::Length(context.device.calc_line_height(
+ context.style.get_font(),
+ wm,
+ Some(context.element_info.element),
+ ))
+ }
+
+ #[inline]
+ fn from_resolved_value(value: Self::ResolvedValue) -> Self {
+ value
+ }
+}
diff --git a/servo/components/style/values/computed/image.rs b/servo/components/style/values/computed/image.rs
new file mode 100644
index 0000000000..8a91d95313
--- /dev/null
+++ b/servo/components/style/values/computed/image.rs
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the computed value of
+//! [`image`][image]s
+//!
+//! [image]: https://drafts.csswg.org/css-images/#image-values
+
+use crate::values::computed::percentage::Percentage;
+use crate::values::computed::position::Position;
+use crate::values::computed::url::ComputedImageUrl;
+use crate::values::computed::{Angle, Color, Context};
+use crate::values::computed::{
+ AngleOrPercentage, LengthPercentage, NonNegativeLength, NonNegativeLengthPercentage,
+ Resolution, ToComputedValue,
+};
+use crate::values::generics::image::{self as generic, GradientCompatMode};
+use crate::values::specified::image as specified;
+use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
+use std::f32::consts::PI;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+pub use specified::ImageRendering;
+
+/// Computed values for an image according to CSS-IMAGES.
+/// <https://drafts.csswg.org/css-images/#image-values>
+pub type Image = generic::GenericImage<Gradient, ComputedImageUrl, Color, Percentage, Resolution>;
+
+// Images should remain small, see https://github.com/servo/servo/pull/18430
+size_of_test!(Image, 16);
+
+/// Computed values for a CSS gradient.
+/// <https://drafts.csswg.org/css-images/#gradients>
+pub type Gradient = generic::GenericGradient<
+ LineDirection,
+ LengthPercentage,
+ NonNegativeLength,
+ NonNegativeLengthPercentage,
+ Position,
+ Angle,
+ AngleOrPercentage,
+ Color,
+>;
+
+/// Computed values for CSS cross-fade
+/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
+pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
+
+/// A computed radial gradient ending shape.
+pub type EndingShape = generic::GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>;
+
+/// A computed gradient line direction.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToResolvedValue)]
+#[repr(C, u8)]
+pub enum LineDirection {
+ /// An angle.
+ Angle(Angle),
+ /// A horizontal direction.
+ Horizontal(HorizontalPositionKeyword),
+ /// A vertical direction.
+ Vertical(VerticalPositionKeyword),
+ /// A corner.
+ Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
+}
+
+/// The computed value for an `image-set()` image.
+pub type ImageSet = generic::GenericImageSet<Image, Resolution>;
+
+impl ToComputedValue for specified::ImageSet {
+ type ComputedValue = ImageSet;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let items = self.items.to_computed_value(context);
+ let dpr = context.device().device_pixel_ratio().get();
+
+ let mut supported_image = false;
+ let mut selected_index = std::usize::MAX;
+ let mut selected_resolution = 0.0;
+
+ for (i, item) in items.iter().enumerate() {
+ if item.has_mime_type && !context.device().is_supported_mime_type(&item.mime_type) {
+ // If the MIME type is not supported, we discard the ImageSetItem.
+ continue;
+ }
+
+ let candidate_resolution = item.resolution.dppx();
+ debug_assert!(
+ candidate_resolution >= 0.0,
+ "Resolutions should be non-negative"
+ );
+ if candidate_resolution == 0.0 {
+ // If the resolution is 0, we also treat it as an invalid image.
+ continue;
+ }
+
+ // https://drafts.csswg.org/css-images-4/#image-set-notation:
+ //
+ // Make a UA-specific choice of which to load, based on whatever criteria deemed
+ // relevant (such as the resolution of the display, connection speed, etc).
+ //
+ // For now, select the lowest resolution greater than display density, otherwise the
+ // greatest resolution available.
+ let better_candidate = || {
+ if selected_resolution < dpr && candidate_resolution > selected_resolution {
+ return true;
+ }
+ if candidate_resolution < selected_resolution && candidate_resolution >= dpr {
+ return true;
+ }
+ false
+ };
+
+ // The first item with a supported MIME type is obviously the current best candidate
+ if !supported_image || better_candidate() {
+ supported_image = true;
+ selected_index = i;
+ selected_resolution = candidate_resolution;
+ }
+ }
+
+ ImageSet {
+ selected_index,
+ items,
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self {
+ selected_index: std::usize::MAX,
+ items: ToComputedValue::from_computed_value(&computed.items),
+ }
+ }
+}
+
+impl generic::LineDirection for LineDirection {
+ fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool {
+ match *self {
+ LineDirection::Angle(angle) => angle.radians() == PI,
+ LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
+ compat_mode == GradientCompatMode::Modern
+ },
+ LineDirection::Vertical(VerticalPositionKeyword::Top) => {
+ compat_mode != GradientCompatMode::Modern
+ },
+ _ => false,
+ }
+ }
+
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ LineDirection::Angle(ref angle) => angle.to_css(dest),
+ LineDirection::Horizontal(x) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)
+ },
+ LineDirection::Vertical(y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ y.to_css(dest)
+ },
+ LineDirection::Corner(x, y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)?;
+ dest.write_char(' ')?;
+ y.to_css(dest)
+ },
+ }
+ }
+}
+
+impl ToComputedValue for specified::LineDirection {
+ type ComputedValue = LineDirection;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ specified::LineDirection::Angle(ref angle) => {
+ LineDirection::Angle(angle.to_computed_value(context))
+ },
+ specified::LineDirection::Horizontal(x) => LineDirection::Horizontal(x),
+ specified::LineDirection::Vertical(y) => LineDirection::Vertical(y),
+ specified::LineDirection::Corner(x, y) => LineDirection::Corner(x, y),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match *computed {
+ LineDirection::Angle(ref angle) => {
+ specified::LineDirection::Angle(ToComputedValue::from_computed_value(angle))
+ },
+ LineDirection::Horizontal(x) => specified::LineDirection::Horizontal(x),
+ LineDirection::Vertical(y) => specified::LineDirection::Vertical(y),
+ LineDirection::Corner(x, y) => specified::LineDirection::Corner(x, y),
+ }
+ }
+}
diff --git a/servo/components/style/values/computed/length.rs b/servo/components/style/values/computed/length.rs
new file mode 100644
index 0000000000..e75676a76d
--- /dev/null
+++ b/servo/components/style/values/computed/length.rs
@@ -0,0 +1,531 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! `<length>` computed values, and related ones.
+
+use super::{Context, Number, ToComputedValue};
+use crate::values::animated::ToAnimatedValue;
+use crate::values::computed::NonNegativeNumber;
+use crate::values::generics::length as generics;
+use crate::values::generics::length::{
+ GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize,
+};
+use crate::values::generics::NonNegative;
+use crate::values::specified::length::{AbsoluteLength, FontBaseSize, LineHeightBase};
+use crate::values::{specified, CSSFloat};
+use crate::Zero;
+use app_units::Au;
+use std::fmt::{self, Write};
+use std::ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub};
+use style_traits::{CSSPixel, CssWriter, ToCss};
+
+pub use super::image::Image;
+pub use super::length_percentage::{LengthPercentage, NonNegativeLengthPercentage};
+pub use crate::values::specified::url::UrlOrNone;
+pub use crate::values::specified::{Angle, BorderStyle, Time};
+
+impl ToComputedValue for specified::NoCalcLength {
+ type ComputedValue = Length;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.to_computed_value_with_base_size(
+ context,
+ FontBaseSize::CurrentStyle,
+ LineHeightBase::CurrentStyle,
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Absolute(AbsoluteLength::Px(computed.px()))
+ }
+}
+
+impl specified::NoCalcLength {
+ /// Computes a length with a given font-relative base size.
+ pub fn to_computed_value_with_base_size(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> Length {
+ match *self {
+ Self::Absolute(length) => length.to_computed_value(context),
+ Self::FontRelative(length) => {
+ length.to_computed_value(context, base_size, line_height_base)
+ },
+ Self::ViewportPercentage(length) => length.to_computed_value(context),
+ Self::ContainerRelative(length) => length.to_computed_value(context),
+ Self::ServoCharacterWidth(length) => length
+ .to_computed_value(context.style().get_font().clone_font_size().computed_size()),
+ }
+ }
+}
+
+impl ToComputedValue for specified::Length {
+ type ComputedValue = Length;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ Self::NoCalc(l) => l.to_computed_value(context),
+ Self::Calc(ref calc) => {
+ let result = calc.to_computed_value(context);
+ debug_assert!(
+ result.to_length().is_some(),
+ "{:?} didn't resolve to a length: {:?}",
+ calc,
+ result,
+ );
+ result.to_length().unwrap_or_else(Length::zero)
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::NoCalc(specified::NoCalcLength::from_computed_value(computed))
+ }
+}
+
+/// Some boilerplate to share between negative and non-negative
+/// length-percentage or auto.
+macro_rules! computed_length_percentage_or_auto {
+ ($inner:ty) => {
+ /// Returns the used value.
+ #[inline]
+ pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> {
+ match *self {
+ Self::Auto => None,
+ Self::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)),
+ }
+ }
+
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match *self {
+ LengthPercentage(ref l) => l.is_definitely_zero(),
+ Auto => false,
+ }
+ }
+ };
+}
+
+/// A computed type for `<length-percentage> | auto`.
+pub type LengthPercentageOrAuto = generics::GenericLengthPercentageOrAuto<LengthPercentage>;
+
+impl LengthPercentageOrAuto {
+ /// Clamps the value to a non-negative value.
+ pub fn clamp_to_non_negative(self) -> Self {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match self {
+ LengthPercentage(l) => LengthPercentage(l.clamp_to_non_negative()),
+ Auto => Auto,
+ }
+ }
+
+ /// Convert to have a borrow inside the enum
+ pub fn as_ref(&self) -> generics::GenericLengthPercentageOrAuto<&LengthPercentage> {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match *self {
+ LengthPercentage(ref lp) => LengthPercentage(lp),
+ Auto => Auto,
+ }
+ }
+
+ computed_length_percentage_or_auto!(LengthPercentage);
+}
+
+impl generics::GenericLengthPercentageOrAuto<&LengthPercentage> {
+ /// Resolves the percentage.
+ #[inline]
+ pub fn percentage_relative_to(&self, basis: Length) -> LengthOrAuto {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match self {
+ LengthPercentage(length_percentage) => {
+ LengthPercentage(length_percentage.percentage_relative_to(basis))
+ },
+ Auto => Auto,
+ }
+ }
+
+ /// Maybe resolves the percentage.
+ #[inline]
+ pub fn maybe_percentage_relative_to(&self, basis: Option<Length>) -> LengthOrAuto {
+ use crate::values::generics::length::LengthPercentageOrAuto::*;
+ match self {
+ LengthPercentage(length_percentage) => length_percentage
+ .maybe_percentage_relative_to(basis)
+ .map_or(Auto, LengthPercentage),
+ Auto => Auto,
+ }
+ }
+}
+
+/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0.
+pub type NonNegativeLengthPercentageOrAuto =
+ generics::GenericLengthPercentageOrAuto<NonNegativeLengthPercentage>;
+
+impl NonNegativeLengthPercentageOrAuto {
+ computed_length_percentage_or_auto!(NonNegativeLengthPercentage);
+}
+
+#[cfg(feature = "servo")]
+impl MaxSize {
+ /// Convert the computed value into used value.
+ #[inline]
+ pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> {
+ match *self {
+ GenericMaxSize::None => None,
+ GenericMaxSize::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)),
+ }
+ }
+}
+
+impl Size {
+ /// Convert the computed value into used value.
+ #[inline]
+ #[cfg(feature = "servo")]
+ pub fn to_used_value(&self, percentage_basis: Au) -> Option<Au> {
+ match *self {
+ GenericSize::Auto => None,
+ GenericSize::LengthPercentage(ref lp) => Some(lp.to_used_value(percentage_basis)),
+ }
+ }
+
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ match *self {
+ Self::Auto => false,
+ Self::LengthPercentage(ref lp) => lp.is_definitely_zero(),
+ #[cfg(feature = "gecko")]
+ Self::MinContent |
+ Self::MaxContent |
+ Self::FitContent |
+ Self::MozAvailable |
+ Self::FitContentFunction(_) => false,
+ }
+ }
+}
+
+/// The computed `<length>` value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ Serialize,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct CSSPixelLength(CSSFloat);
+
+impl fmt::Debug for CSSPixelLength {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.0.fmt(f)?;
+ f.write_str(" px")
+ }
+}
+
+impl CSSPixelLength {
+ /// Return a new CSSPixelLength.
+ #[inline]
+ pub fn new(px: CSSFloat) -> Self {
+ CSSPixelLength(px)
+ }
+
+ /// Returns a normalized (NaN turned to zero) version of this length.
+ #[inline]
+ pub fn normalized(self) -> Self {
+ Self::new(crate::values::normalize(self.0))
+ }
+
+ /// Returns a finite (normalized and clamped to float min and max) version of this length.
+ #[inline]
+ pub fn finite(self) -> Self {
+ Self::new(crate::values::normalize(self.0).min(f32::MAX).max(f32::MIN))
+ }
+
+ /// Scale the length by a given amount.
+ #[inline]
+ pub fn scale_by(self, scale: CSSFloat) -> Self {
+ CSSPixelLength(self.0 * scale)
+ }
+
+ /// Return the containing pixel value.
+ #[inline]
+ pub fn px(self) -> CSSFloat {
+ self.0
+ }
+
+ /// Return the length with app_unit i32 type.
+ #[inline]
+ pub fn to_i32_au(self) -> i32 {
+ Au::from(self).0
+ }
+
+ /// Return the absolute value of this length.
+ #[inline]
+ pub fn abs(self) -> Self {
+ CSSPixelLength::new(self.0.abs())
+ }
+
+ /// Return the clamped value of this length.
+ #[inline]
+ pub fn clamp_to_non_negative(self) -> Self {
+ CSSPixelLength::new(self.0.max(0.))
+ }
+
+ /// Returns the minimum between `self` and `other`.
+ #[inline]
+ pub fn min(self, other: Self) -> Self {
+ CSSPixelLength::new(self.0.min(other.0))
+ }
+
+ /// Returns the maximum between `self` and `other`.
+ #[inline]
+ pub fn max(self, other: Self) -> Self {
+ CSSPixelLength::new(self.0.max(other.0))
+ }
+
+ /// Sets `self` to the maximum between `self` and `other`.
+ #[inline]
+ pub fn max_assign(&mut self, other: Self) {
+ *self = self.max(other);
+ }
+
+ /// Clamp the value to a lower bound and an optional upper bound.
+ ///
+ /// Can be used for example with `min-width` and `max-width`.
+ #[inline]
+ pub fn clamp_between_extremums(self, min_size: Self, max_size: Option<Self>) -> Self {
+ self.clamp_below_max(max_size).max(min_size)
+ }
+
+ /// Clamp the value to an optional upper bound.
+ ///
+ /// Can be used for example with `max-width`.
+ #[inline]
+ pub fn clamp_below_max(self, max_size: Option<Self>) -> Self {
+ match max_size {
+ None => self,
+ Some(max_size) => self.min(max_size),
+ }
+ }
+}
+
+impl num_traits::Zero for CSSPixelLength {
+ fn zero() -> Self {
+ CSSPixelLength::new(0.)
+ }
+
+ fn is_zero(&self) -> bool {
+ self.px() == 0.
+ }
+}
+
+impl ToCss for CSSPixelLength {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ dest.write_str("px")
+ }
+}
+
+impl std::iter::Sum for CSSPixelLength {
+ fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
+ iter.fold(Length::zero(), Add::add)
+ }
+}
+
+impl Add for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn add(self, other: Self) -> Self {
+ Self::new(self.px() + other.px())
+ }
+}
+
+impl AddAssign for CSSPixelLength {
+ #[inline]
+ fn add_assign(&mut self, other: Self) {
+ self.0 += other.0;
+ }
+}
+
+impl Div for CSSPixelLength {
+ type Output = CSSFloat;
+
+ #[inline]
+ fn div(self, other: Self) -> CSSFloat {
+ self.px() / other.px()
+ }
+}
+
+impl Div<CSSFloat> for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn div(self, other: CSSFloat) -> Self {
+ Self::new(self.px() / other)
+ }
+}
+
+impl MulAssign<CSSFloat> for CSSPixelLength {
+ #[inline]
+ fn mul_assign(&mut self, other: CSSFloat) {
+ self.0 *= other;
+ }
+}
+
+impl Mul<CSSFloat> for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn mul(self, other: CSSFloat) -> Self {
+ Self::new(self.px() * other)
+ }
+}
+
+impl Neg for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn neg(self) -> Self {
+ CSSPixelLength::new(-self.0)
+ }
+}
+
+impl Sub for CSSPixelLength {
+ type Output = Self;
+
+ #[inline]
+ fn sub(self, other: Self) -> Self {
+ Self::new(self.px() - other.px())
+ }
+}
+
+impl From<CSSPixelLength> for Au {
+ #[inline]
+ fn from(len: CSSPixelLength) -> Self {
+ Au::from_f32_px(len.0)
+ }
+}
+
+impl From<Au> for CSSPixelLength {
+ #[inline]
+ fn from(len: Au) -> Self {
+ CSSPixelLength::new(len.to_f32_px())
+ }
+}
+
+impl From<CSSPixelLength> for euclid::Length<CSSFloat, CSSPixel> {
+ #[inline]
+ fn from(length: CSSPixelLength) -> Self {
+ Self::new(length.0)
+ }
+}
+
+/// An alias of computed `<length>` value.
+pub type Length = CSSPixelLength;
+
+/// Either a computed `<length>` or the `auto` keyword.
+pub type LengthOrAuto = generics::GenericLengthPercentageOrAuto<Length>;
+
+/// Either a non-negative `<length>` or the `auto` keyword.
+pub type NonNegativeLengthOrAuto = generics::GenericLengthPercentageOrAuto<NonNegativeLength>;
+
+/// Either a computed `<length>` or a `<number>` value.
+pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>;
+
+/// A wrapper of Length, whose value must be >= 0.
+pub type NonNegativeLength = NonNegative<Length>;
+
+impl ToAnimatedValue for NonNegativeLength {
+ type AnimatedValue = Length;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegativeLength::new(animated.px().max(0.))
+ }
+}
+
+impl NonNegativeLength {
+ /// Create a NonNegativeLength.
+ #[inline]
+ pub fn new(px: CSSFloat) -> Self {
+ NonNegative(Length::new(px.max(0.)))
+ }
+
+ /// Return the pixel value of |NonNegativeLength|.
+ #[inline]
+ pub fn px(&self) -> CSSFloat {
+ self.0.px()
+ }
+
+ #[inline]
+ /// Ensures it is non negative
+ pub fn clamp(self) -> Self {
+ if (self.0).0 < 0. {
+ Self::zero()
+ } else {
+ self
+ }
+ }
+}
+
+impl From<Length> for NonNegativeLength {
+ #[inline]
+ fn from(len: Length) -> Self {
+ NonNegative(len)
+ }
+}
+
+impl From<Au> for NonNegativeLength {
+ #[inline]
+ fn from(au: Au) -> Self {
+ NonNegative(au.into())
+ }
+}
+
+impl From<NonNegativeLength> for Au {
+ #[inline]
+ fn from(non_negative_len: NonNegativeLength) -> Self {
+ Au::from(non_negative_len.0)
+ }
+}
+
+/// Either a computed NonNegativeLengthPercentage or the `normal` keyword.
+pub type NonNegativeLengthPercentageOrNormal =
+ GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>;
+
+/// Either a non-negative `<length>` or a `<number>`.
+pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>;
+
+/// A computed value for `min-width`, `min-height`, `width` or `height` property.
+pub type Size = GenericSize<NonNegativeLengthPercentage>;
+
+/// A computed value for `max-width` or `min-height` property.
+pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>;
diff --git a/servo/components/style/values/computed/length_percentage.rs b/servo/components/style/values/computed/length_percentage.rs
new file mode 100644
index 0000000000..898281a7ef
--- /dev/null
+++ b/servo/components/style/values/computed/length_percentage.rs
@@ -0,0 +1,1055 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! `<length-percentage>` computed values, and related ones.
+//!
+//! The over-all design is a tagged pointer, with the lower bits of the pointer
+//! being non-zero if it is a non-calc value.
+//!
+//! It is expected to take 64 bits both in x86 and x86-64. This is implemented
+//! as a `union`, with 4 different variants:
+//!
+//! * The length and percentage variants have a { tag, f32 } (effectively)
+//! layout. The tag has to overlap with the lower 2 bits of the calc variant.
+//!
+//! * The `calc()` variant is a { tag, pointer } in x86 (so same as the
+//! others), or just a { pointer } in x86-64 (so that the two bits of the tag
+//! can be obtained from the lower bits of the pointer).
+//!
+//! * There's a `tag` variant just to make clear when only the tag is intended
+//! to be read. Note that the tag needs to be masked always by `TAG_MASK`, to
+//! deal with the pointer variant in x86-64.
+//!
+//! The assertions in the constructor methods ensure that the tag getter matches
+//! our expectations.
+
+use super::{Context, Length, Percentage, ToComputedValue};
+use crate::gecko_bindings::structs::GeckoFontMetrics;
+use crate::values::animated::{Animate, Procedure, ToAnimatedValue, ToAnimatedZero};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::calc::{CalcUnits, PositivePercentageBasis};
+use crate::values::generics::{calc, NonNegative};
+use crate::values::specified::length::{FontBaseSize, LineHeightBase};
+use crate::values::{specified, CSSFloat};
+use crate::{Zero, ZeroNoPercent};
+use app_units::Au;
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
+use serde::{Deserialize, Serialize};
+use std::borrow::Cow;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ToCss};
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct LengthVariant {
+ tag: u8,
+ length: Length,
+}
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct PercentageVariant {
+ tag: u8,
+ percentage: Percentage,
+}
+
+// NOTE(emilio): cbindgen only understands the #[cfg] on the top level
+// definition.
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+#[cfg(target_pointer_width = "32")]
+pub struct CalcVariant {
+ tag: u8,
+ ptr: *mut CalcLengthPercentage,
+}
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+#[cfg(target_pointer_width = "64")]
+pub struct CalcVariant {
+ ptr: usize, // In little-endian byte order
+}
+
+// `CalcLengthPercentage` is `Send + Sync` as asserted below.
+unsafe impl Send for CalcVariant {}
+unsafe impl Sync for CalcVariant {}
+
+#[doc(hidden)]
+#[derive(Clone, Copy)]
+#[repr(C)]
+pub struct TagVariant {
+ tag: u8,
+}
+
+/// A `<length-percentage>` value. This can be either a `<length>`, a
+/// `<percentage>`, or a combination of both via `calc()`.
+///
+/// cbindgen:private-default-tagged-enum-constructor=false
+/// cbindgen:derive-mut-casts=true
+///
+/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage
+///
+/// The tag is stored in the lower two bits.
+///
+/// We need to use a struct instead of the union directly because unions with
+/// Drop implementations are unstable, looks like.
+///
+/// Also we need the union and the variants to be `pub` (even though the member
+/// is private) so that cbindgen generates it. They're not part of the public
+/// API otherwise.
+#[repr(transparent)]
+pub struct LengthPercentage(LengthPercentageUnion);
+
+#[doc(hidden)]
+#[repr(C)]
+pub union LengthPercentageUnion {
+ length: LengthVariant,
+ percentage: PercentageVariant,
+ calc: CalcVariant,
+ tag: TagVariant,
+}
+
+impl LengthPercentageUnion {
+ #[doc(hidden)] // Need to be public so that cbindgen generates it.
+ pub const TAG_CALC: u8 = 0;
+ #[doc(hidden)]
+ pub const TAG_LENGTH: u8 = 1;
+ #[doc(hidden)]
+ pub const TAG_PERCENTAGE: u8 = 2;
+ #[doc(hidden)]
+ pub const TAG_MASK: u8 = 0b11;
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+#[repr(u8)]
+enum Tag {
+ Calc = LengthPercentageUnion::TAG_CALC,
+ Length = LengthPercentageUnion::TAG_LENGTH,
+ Percentage = LengthPercentageUnion::TAG_PERCENTAGE,
+}
+
+// All the members should be 64 bits, even in 32-bit builds.
+#[allow(unused)]
+unsafe fn static_assert() {
+ fn assert_send_and_sync<T: Send + Sync>() {}
+ std::mem::transmute::<u64, LengthVariant>(0u64);
+ std::mem::transmute::<u64, PercentageVariant>(0u64);
+ std::mem::transmute::<u64, CalcVariant>(0u64);
+ std::mem::transmute::<u64, LengthPercentage>(0u64);
+ assert_send_and_sync::<LengthVariant>();
+ assert_send_and_sync::<PercentageVariant>();
+ assert_send_and_sync::<CalcLengthPercentage>();
+}
+
+impl Drop for LengthPercentage {
+ fn drop(&mut self) {
+ if self.tag() == Tag::Calc {
+ let _ = unsafe { Box::from_raw(self.calc_ptr()) };
+ }
+ }
+}
+
+impl MallocSizeOf for LengthPercentage {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match self.unpack() {
+ Unpacked::Length(..) | Unpacked::Percentage(..) => 0,
+ Unpacked::Calc(c) => unsafe { ops.malloc_size_of(c) },
+ }
+ }
+}
+
+/// An unpacked `<length-percentage>` that borrows the `calc()` variant.
+#[derive(Clone, Debug, PartialEq, ToCss)]
+enum Unpacked<'a> {
+ Calc(&'a CalcLengthPercentage),
+ Length(Length),
+ Percentage(Percentage),
+}
+
+/// An unpacked `<length-percentage>` that mutably borrows the `calc()` variant.
+enum UnpackedMut<'a> {
+ Calc(&'a mut CalcLengthPercentage),
+ Length(Length),
+ Percentage(Percentage),
+}
+
+/// An unpacked `<length-percentage>` that owns the `calc()` variant, for
+/// serialization purposes.
+#[derive(Deserialize, PartialEq, Serialize)]
+enum Serializable {
+ Calc(CalcLengthPercentage),
+ Length(Length),
+ Percentage(Percentage),
+}
+
+impl LengthPercentage {
+ /// 1px length value for SVG defaults
+ #[inline]
+ pub fn one() -> Self {
+ Self::new_length(Length::new(1.))
+ }
+
+ /// 0%
+ #[inline]
+ pub fn zero_percent() -> Self {
+ Self::new_percent(Percentage::zero())
+ }
+
+ fn to_calc_node(&self) -> Cow<CalcNode> {
+ match self.unpack() {
+ Unpacked::Length(l) => Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Length(l))),
+ Unpacked::Percentage(p) => {
+ Cow::Owned(CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(p)))
+ },
+ Unpacked::Calc(p) => Cow::Borrowed(&p.node),
+ }
+ }
+
+ /// Constructs a length value.
+ #[inline]
+ pub fn new_length(length: Length) -> Self {
+ let length = Self(LengthPercentageUnion {
+ length: LengthVariant {
+ tag: LengthPercentageUnion::TAG_LENGTH,
+ length,
+ },
+ });
+ debug_assert_eq!(length.tag(), Tag::Length);
+ length
+ }
+
+ /// Constructs a percentage value.
+ #[inline]
+ pub fn new_percent(percentage: Percentage) -> Self {
+ let percent = Self(LengthPercentageUnion {
+ percentage: PercentageVariant {
+ tag: LengthPercentageUnion::TAG_PERCENTAGE,
+ percentage,
+ },
+ });
+ debug_assert_eq!(percent.tag(), Tag::Percentage);
+ percent
+ }
+
+ /// Given a `LengthPercentage` value `v`, construct the value representing
+ /// `calc(100% - v)`.
+ pub fn hundred_percent_minus(v: Self, clamping_mode: AllowedNumericType) -> Self {
+ // TODO: This could in theory take ownership of the calc node in `v` if
+ // possible instead of cloning.
+ let mut node = v.to_calc_node().into_owned();
+ node.negate();
+
+ let new_node = CalcNode::Sum(
+ vec![
+ CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(Percentage::hundred())),
+ node,
+ ]
+ .into(),
+ );
+
+ Self::new_calc(new_node, clamping_mode)
+ }
+
+ /// Given a list of `LengthPercentage` values, construct the value representing
+ /// `calc(100% - the sum of the list)`.
+ pub fn hundred_percent_minus_list(list: &[&Self], clamping_mode: AllowedNumericType) -> Self {
+ let mut new_list = vec![CalcNode::Leaf(CalcLengthPercentageLeaf::Percentage(
+ Percentage::hundred(),
+ ))];
+
+ for lp in list.iter() {
+ let mut node = lp.to_calc_node().into_owned();
+ node.negate();
+ new_list.push(node)
+ }
+
+ Self::new_calc(CalcNode::Sum(new_list.into()), clamping_mode)
+ }
+
+ /// Constructs a `calc()` value.
+ #[inline]
+ pub fn new_calc(mut node: CalcNode, clamping_mode: AllowedNumericType) -> Self {
+ node.simplify_and_sort();
+
+ match node {
+ CalcNode::Leaf(l) => {
+ return match l {
+ CalcLengthPercentageLeaf::Length(l) => {
+ Self::new_length(Length::new(clamping_mode.clamp(l.px())).normalized())
+ },
+ CalcLengthPercentageLeaf::Percentage(p) => Self::new_percent(Percentage(
+ clamping_mode.clamp(crate::values::normalize(p.0)),
+ )),
+ CalcLengthPercentageLeaf::Number(number) => {
+ debug_assert!(
+ false,
+ "The final result of a <length-percentage> should never be a number"
+ );
+ Self::new_length(Length::new(number))
+ },
+ };
+ },
+ _ => Self::new_calc_unchecked(Box::new(CalcLengthPercentage {
+ clamping_mode,
+ node,
+ })),
+ }
+ }
+
+ /// Private version of new_calc() that constructs a calc() variant without
+ /// checking.
+ fn new_calc_unchecked(calc: Box<CalcLengthPercentage>) -> Self {
+ let ptr = Box::into_raw(calc);
+
+ #[cfg(target_pointer_width = "32")]
+ let calc = CalcVariant {
+ tag: LengthPercentageUnion::TAG_CALC,
+ ptr,
+ };
+
+ #[cfg(target_pointer_width = "64")]
+ let calc = CalcVariant {
+ #[cfg(target_endian = "little")]
+ ptr: ptr as usize,
+ #[cfg(target_endian = "big")]
+ ptr: (ptr as usize).swap_bytes(),
+ };
+
+ let calc = Self(LengthPercentageUnion { calc });
+ debug_assert_eq!(calc.tag(), Tag::Calc);
+ calc
+ }
+
+ #[inline]
+ fn tag(&self) -> Tag {
+ match unsafe { self.0.tag.tag & LengthPercentageUnion::TAG_MASK } {
+ LengthPercentageUnion::TAG_CALC => Tag::Calc,
+ LengthPercentageUnion::TAG_LENGTH => Tag::Length,
+ LengthPercentageUnion::TAG_PERCENTAGE => Tag::Percentage,
+ _ => unsafe { debug_unreachable!("Bogus tag?") },
+ }
+ }
+
+ #[inline]
+ fn unpack_mut<'a>(&'a mut self) -> UnpackedMut<'a> {
+ unsafe {
+ match self.tag() {
+ Tag::Calc => UnpackedMut::Calc(&mut *self.calc_ptr()),
+ Tag::Length => UnpackedMut::Length(self.0.length.length),
+ Tag::Percentage => UnpackedMut::Percentage(self.0.percentage.percentage),
+ }
+ }
+ }
+
+ #[inline]
+ fn unpack<'a>(&'a self) -> Unpacked<'a> {
+ unsafe {
+ match self.tag() {
+ Tag::Calc => Unpacked::Calc(&*self.calc_ptr()),
+ Tag::Length => Unpacked::Length(self.0.length.length),
+ Tag::Percentage => Unpacked::Percentage(self.0.percentage.percentage),
+ }
+ }
+ }
+
+ #[inline]
+ unsafe fn calc_ptr(&self) -> *mut CalcLengthPercentage {
+ #[cfg(not(all(target_endian = "big", target_pointer_width = "64")))]
+ {
+ self.0.calc.ptr as *mut _
+ }
+ #[cfg(all(target_endian = "big", target_pointer_width = "64"))]
+ {
+ self.0.calc.ptr.swap_bytes() as *mut _
+ }
+ }
+
+ #[inline]
+ fn to_serializable(&self) -> Serializable {
+ match self.unpack() {
+ Unpacked::Calc(c) => Serializable::Calc(c.clone()),
+ Unpacked::Length(l) => Serializable::Length(l),
+ Unpacked::Percentage(p) => Serializable::Percentage(p),
+ }
+ }
+
+ #[inline]
+ fn from_serializable(s: Serializable) -> Self {
+ match s {
+ Serializable::Calc(c) => Self::new_calc_unchecked(Box::new(c)),
+ Serializable::Length(l) => Self::new_length(l),
+ Serializable::Percentage(p) => Self::new_percent(p),
+ }
+ }
+
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ match self.unpack() {
+ Unpacked::Length(l) => l.px() == 0.0,
+ Unpacked::Percentage(p) => p.0 == 0.0,
+ Unpacked::Calc(..) => false,
+ }
+ }
+
+ /// Resolves the percentage.
+ #[inline]
+ pub fn resolve(&self, basis: Length) -> Length {
+ match self.unpack() {
+ Unpacked::Length(l) => l,
+ Unpacked::Percentage(p) => (basis * p.0).normalized(),
+ Unpacked::Calc(ref c) => c.resolve(basis),
+ }
+ }
+
+ /// Resolves the percentage. Just an alias of resolve().
+ #[inline]
+ pub fn percentage_relative_to(&self, basis: Length) -> Length {
+ self.resolve(basis)
+ }
+
+ /// Return whether there's any percentage in this value.
+ #[inline]
+ pub fn has_percentage(&self) -> bool {
+ match self.unpack() {
+ Unpacked::Length(..) => false,
+ Unpacked::Percentage(..) | Unpacked::Calc(..) => true,
+ }
+ }
+
+ /// Converts to a `<length>` if possible.
+ pub fn to_length(&self) -> Option<Length> {
+ match self.unpack() {
+ Unpacked::Length(l) => Some(l),
+ Unpacked::Percentage(..) | Unpacked::Calc(..) => {
+ debug_assert!(self.has_percentage());
+ return None;
+ },
+ }
+ }
+
+ /// Converts to a `<percentage>` if possible.
+ #[inline]
+ pub fn to_percentage(&self) -> Option<Percentage> {
+ match self.unpack() {
+ Unpacked::Percentage(p) => Some(p),
+ Unpacked::Length(..) | Unpacked::Calc(..) => None,
+ }
+ }
+
+ /// Returns the used value.
+ #[inline]
+ pub fn to_used_value(&self, containing_length: Au) -> Au {
+ Au::from(self.to_pixel_length(containing_length))
+ }
+
+ /// Returns the used value as CSSPixelLength.
+ #[inline]
+ pub fn to_pixel_length(&self, containing_length: Au) -> Length {
+ self.resolve(containing_length.into())
+ }
+
+ /// Convert the computed value into used value.
+ #[inline]
+ pub fn maybe_to_used_value(&self, container_len: Option<Length>) -> Option<Au> {
+ self.maybe_percentage_relative_to(container_len)
+ .map(Au::from)
+ }
+
+ /// If there are special rules for computing percentages in a value (e.g.
+ /// the height property), they apply whenever a calc() expression contains
+ /// percentages.
+ pub fn maybe_percentage_relative_to(&self, container_len: Option<Length>) -> Option<Length> {
+ if let Unpacked::Length(l) = self.unpack() {
+ return Some(l);
+ }
+ Some(self.resolve(container_len?))
+ }
+
+ /// Returns the clamped non-negative values.
+ #[inline]
+ pub fn clamp_to_non_negative(mut self) -> Self {
+ match self.unpack_mut() {
+ UnpackedMut::Length(l) => Self::new_length(l.clamp_to_non_negative()),
+ UnpackedMut::Percentage(p) => Self::new_percent(p.clamp_to_non_negative()),
+ UnpackedMut::Calc(ref mut c) => {
+ c.clamping_mode = AllowedNumericType::NonNegative;
+ self
+ },
+ }
+ }
+}
+
+impl PartialEq for LengthPercentage {
+ fn eq(&self, other: &Self) -> bool {
+ self.unpack() == other.unpack()
+ }
+}
+
+impl fmt::Debug for LengthPercentage {
+ fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+ self.unpack().fmt(formatter)
+ }
+}
+
+impl ToAnimatedZero for LengthPercentage {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(match self.unpack() {
+ Unpacked::Length(l) => Self::new_length(l.to_animated_zero()?),
+ Unpacked::Percentage(p) => Self::new_percent(p.to_animated_zero()?),
+ Unpacked::Calc(c) => Self::new_calc_unchecked(Box::new(c.to_animated_zero()?)),
+ })
+ }
+}
+
+impl Clone for LengthPercentage {
+ fn clone(&self) -> Self {
+ match self.unpack() {
+ Unpacked::Length(l) => Self::new_length(l),
+ Unpacked::Percentage(p) => Self::new_percent(p),
+ Unpacked::Calc(c) => Self::new_calc_unchecked(Box::new(c.clone())),
+ }
+ }
+}
+
+impl ToComputedValue for specified::LengthPercentage {
+ type ComputedValue = LengthPercentage;
+
+ fn to_computed_value(&self, context: &Context) -> LengthPercentage {
+ match *self {
+ specified::LengthPercentage::Length(ref value) => {
+ LengthPercentage::new_length(value.to_computed_value(context))
+ },
+ specified::LengthPercentage::Percentage(value) => LengthPercentage::new_percent(value),
+ specified::LengthPercentage::Calc(ref calc) => (**calc).to_computed_value(context),
+ }
+ }
+
+ fn from_computed_value(computed: &LengthPercentage) -> Self {
+ match computed.unpack() {
+ Unpacked::Length(ref l) => {
+ specified::LengthPercentage::Length(ToComputedValue::from_computed_value(l))
+ },
+ Unpacked::Percentage(p) => specified::LengthPercentage::Percentage(p),
+ Unpacked::Calc(c) => {
+ // We simplify before constructing the LengthPercentage if
+ // needed, so this is always fine.
+ specified::LengthPercentage::Calc(Box::new(
+ specified::CalcLengthPercentage::from_computed_value(c),
+ ))
+ },
+ }
+ }
+}
+
+impl ComputeSquaredDistance for LengthPercentage {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ // A somewhat arbitrary base, it doesn't really make sense to mix
+ // lengths with percentages, but we can't do much better here, and this
+ // ensures that the distance between length-only and percentage-only
+ // lengths makes sense.
+ let basis = Length::new(100.);
+ self.resolve(basis)
+ .compute_squared_distance(&other.resolve(basis))
+ }
+}
+
+impl ToCss for LengthPercentage {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.unpack().to_css(dest)
+ }
+}
+
+impl Zero for LengthPercentage {
+ fn zero() -> Self {
+ LengthPercentage::new_length(Length::zero())
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.is_definitely_zero()
+ }
+}
+
+impl ZeroNoPercent for LengthPercentage {
+ #[inline]
+ fn is_zero_no_percent(&self) -> bool {
+ self.is_definitely_zero() && !self.has_percentage()
+ }
+}
+
+impl Serialize for LengthPercentage {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ self.to_serializable().serialize(serializer)
+ }
+}
+
+impl<'de> Deserialize<'de> for LengthPercentage {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: serde::Deserializer<'de>,
+ {
+ Ok(Self::from_serializable(Serializable::deserialize(
+ deserializer,
+ )?))
+ }
+}
+
+/// The leaves of a `<length-percentage>` calc expression.
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum CalcLengthPercentageLeaf {
+ Length(Length),
+ Percentage(Percentage),
+ Number(f32),
+}
+
+impl CalcLengthPercentageLeaf {
+ fn is_zero_length(&self) -> bool {
+ match *self {
+ Self::Length(ref l) => l.is_zero(),
+ Self::Percentage(..) => false,
+ Self::Number(..) => false,
+ }
+ }
+}
+
+impl calc::CalcNodeLeaf for CalcLengthPercentageLeaf {
+ fn unit(&self) -> CalcUnits {
+ match self {
+ Self::Length(_) => CalcUnits::LENGTH,
+ Self::Percentage(_) => CalcUnits::PERCENTAGE,
+ Self::Number(_) => CalcUnits::empty(),
+ }
+ }
+
+ fn unitless_value(&self) -> f32 {
+ match *self {
+ Self::Length(ref l) => l.px(),
+ Self::Percentage(ref p) => p.0,
+ Self::Number(n) => n,
+ }
+ }
+
+ fn new_number(value: f32) -> Self {
+ Self::Number(value)
+ }
+
+ fn as_number(&self) -> Option<f32> {
+ match *self {
+ Self::Length(_) | Self::Percentage(_) => None,
+ Self::Number(value) => Some(value),
+ }
+ }
+
+ fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<std::cmp::Ordering> {
+ use self::CalcLengthPercentageLeaf::*;
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) {
+ return None;
+ }
+
+ let self_negative = self.is_negative();
+ if self_negative != other.is_negative() {
+ return Some(if self_negative { std::cmp::Ordering::Less } else { std::cmp::Ordering::Greater });
+ }
+
+ match (self, other) {
+ (&Length(ref one), &Length(ref other)) => one.partial_cmp(other),
+ (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other),
+ (&Number(ref one), &Number(ref other)) => one.partial_cmp(other),
+ _ => unsafe {
+ match *self {
+ Length(..) | Percentage(..) | Number(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in compare()")
+ },
+ }
+ }
+
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
+ use self::CalcLengthPercentageLeaf::*;
+
+ // 0px plus anything else is equal to the right hand side.
+ if self.is_zero_length() {
+ *self = other.clone();
+ return Ok(());
+ }
+
+ if other.is_zero_length() {
+ return Ok(());
+ }
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&mut Length(ref mut one), &Length(ref other)) => {
+ *one += *other;
+ },
+ (&mut Percentage(ref mut one), &Percentage(ref other)) => {
+ one.0 += other.0;
+ },
+ (&mut Number(ref mut one), &Number(ref other)) => {
+ *one += *other;
+ },
+ _ => unsafe {
+ match *other {
+ Length(..) | Percentage(..) | Number(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_sum_in_place()")
+ },
+ }
+
+ Ok(())
+ }
+
+ fn try_product_in_place(&mut self, other: &mut Self) -> bool {
+ if let Self::Number(ref mut left) = *self {
+ if let Self::Number(ref right) = *other {
+ // Both sides are numbers, so we can just modify the left side.
+ *left *= *right;
+ true
+ } else {
+ // The right side is not a number, so the result should be in the units of the right
+ // side.
+ other.map(|v| v * *left);
+ std::mem::swap(self, other);
+ true
+ }
+ } else if let Self::Number(ref right) = *other {
+ // The left side is not a number, but the right side is, so the result is the left
+ // side unit.
+ self.map(|v| v * *right);
+ true
+ } else {
+ // Neither side is a number, so a product is not possible.
+ false
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::CalcLengthPercentageLeaf::*;
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+ Ok(match (self, other) {
+ (&Length(ref one), &Length(ref other)) => {
+ Length(super::Length::new(op(one.px(), other.px())))
+ },
+ (&Percentage(one), &Percentage(other)) => {
+ Self::Percentage(super::Percentage(op(one.0, other.0)))
+ },
+ (&Number(one), &Number(other)) => Self::Number(op(one, other)),
+ _ => unsafe {
+ match *self {
+ Length(..) | Percentage(..) | Number(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ fn map(&mut self, mut op: impl FnMut(f32) -> f32) {
+ match self {
+ Self::Length(value) => {
+ *value = Length::new(op(value.px()));
+ },
+ Self::Percentage(value) => {
+ *value = Percentage(op(value.0));
+ },
+ Self::Number(value) => {
+ *value = op(*value);
+ },
+ }
+ }
+
+ fn simplify(&mut self) {}
+
+ fn sort_key(&self) -> calc::SortKey {
+ match *self {
+ Self::Length(..) => calc::SortKey::Px,
+ Self::Percentage(..) => calc::SortKey::Percentage,
+ Self::Number(..) => calc::SortKey::Number,
+ }
+ }
+}
+
+/// The computed version of a calc() node for `<length-percentage>` values.
+pub type CalcNode = calc::GenericCalcNode<CalcLengthPercentageLeaf>;
+
+/// The representation of a calc() function with mixed lengths and percentages.
+#[derive(
+ Clone, Debug, Deserialize, MallocSizeOf, Serialize, ToAnimatedZero, ToResolvedValue, ToCss,
+)]
+#[repr(C)]
+pub struct CalcLengthPercentage {
+ #[animation(constant)]
+ #[css(skip)]
+ clamping_mode: AllowedNumericType,
+ node: CalcNode,
+}
+
+impl CalcLengthPercentage {
+ /// Resolves the percentage.
+ #[inline]
+ pub fn resolve(&self, basis: Length) -> Length {
+ // unwrap() is fine because the conversion below is infallible.
+ if let CalcLengthPercentageLeaf::Length(px) = self
+ .node
+ .resolve_map(|leaf| {
+ Ok(if let CalcLengthPercentageLeaf::Percentage(p) = leaf {
+ CalcLengthPercentageLeaf::Length(Length::new(basis.px() * p.0))
+ } else {
+ leaf.clone()
+ })
+ })
+ .unwrap()
+ {
+ Length::new(self.clamping_mode.clamp(px.px())).normalized()
+ } else {
+ unreachable!("resolve_map should turn percentages to lengths, and parsing should ensure that we don't end up with a number");
+ }
+ }
+}
+
+// NOTE(emilio): We don't compare `clamping_mode` since we want to preserve the
+// invariant that `from_computed_value(length).to_computed_value(..) == length`.
+//
+// Right now for e.g. a non-negative length, we set clamping_mode to `All`
+// unconditionally for non-calc values, and to `NonNegative` for calc.
+//
+// If we determine that it's sound, from_computed_value() can generate an
+// absolute length, which then would get `All` as the clamping mode.
+//
+// We may want to just eagerly-detect whether we can clamp in
+// `LengthPercentage::new` and switch to `AllowedNumericType::NonNegative` then,
+// maybe.
+impl PartialEq for CalcLengthPercentage {
+ fn eq(&self, other: &Self) -> bool {
+ self.node == other.node
+ }
+}
+
+impl specified::CalcLengthPercentage {
+ /// Compute the value, zooming any absolute units by the zoom function.
+ fn to_computed_value_with_zoom<F>(
+ &self,
+ context: &Context,
+ zoom_fn: F,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> LengthPercentage
+ where
+ F: Fn(Length) -> Length,
+ {
+ use crate::values::specified::calc::Leaf;
+
+ let node = self.node.map_leaves(|leaf| match *leaf {
+ Leaf::Percentage(p) => CalcLengthPercentageLeaf::Percentage(Percentage(p)),
+ Leaf::Length(l) => CalcLengthPercentageLeaf::Length({
+ let result =
+ l.to_computed_value_with_base_size(context, base_size, line_height_base);
+ if l.should_zoom_text() {
+ zoom_fn(result)
+ } else {
+ result
+ }
+ }),
+ Leaf::Number(n) => CalcLengthPercentageLeaf::Number(n),
+ Leaf::Angle(..) | Leaf::Time(..) | Leaf::Resolution(..) => {
+ unreachable!("Shouldn't have parsed")
+ },
+ });
+
+ LengthPercentage::new_calc(node, self.clamping_mode)
+ }
+
+ /// Compute font-size or line-height taking into account text-zoom if necessary.
+ pub fn to_computed_value_zoomed(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> LengthPercentage {
+ self.to_computed_value_with_zoom(
+ context,
+ |abs| context.maybe_zoom_text(abs),
+ base_size,
+ line_height_base,
+ )
+ }
+
+ /// Compute the value into pixel length as CSSFloat without context,
+ /// so it returns Err(()) if there is any non-absolute unit.
+ pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> {
+ use crate::values::specified::calc::Leaf;
+ use crate::values::specified::length::NoCalcLength;
+
+ // Simplification should've turned this into an absolute length,
+ // otherwise it wouldn't have been able to.
+ match self.node {
+ calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()),
+ _ => Err(()),
+ }
+ }
+
+ /// Compute the value into pixel length as CSSFloat, using the get_font_metrics function
+ /// if provided to resolve font-relative dimensions.
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>,
+ ) -> Result<CSSFloat, ()> {
+ use crate::values::specified::calc::Leaf;
+ use crate::values::specified::length::NoCalcLength;
+
+ match self.node {
+ calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::Absolute(ref l))) => Ok(l.to_px()),
+ calc::CalcNode::Leaf(Leaf::Length(NoCalcLength::FontRelative(ref l))) => {
+ if let Some(getter) = get_font_metrics {
+ l.to_computed_pixel_length_with_font_metrics(getter)
+ } else {
+ Err(())
+ }
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Compute the calc using the current font-size and line-height. (and without text-zoom).
+ pub fn to_computed_value(&self, context: &Context) -> LengthPercentage {
+ self.to_computed_value_with_zoom(
+ context,
+ |abs| abs,
+ FontBaseSize::CurrentStyle,
+ LineHeightBase::CurrentStyle,
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &CalcLengthPercentage) -> Self {
+ use crate::values::specified::calc::Leaf;
+ use crate::values::specified::length::NoCalcLength;
+
+ specified::CalcLengthPercentage {
+ clamping_mode: computed.clamping_mode,
+ node: computed.node.map_leaves(|l| match l {
+ CalcLengthPercentageLeaf::Length(ref l) => {
+ Leaf::Length(NoCalcLength::from_px(l.px()))
+ },
+ CalcLengthPercentageLeaf::Percentage(ref p) => Leaf::Percentage(p.0),
+ CalcLengthPercentageLeaf::Number(n) => Leaf::Number(*n),
+ }),
+ }
+ }
+}
+
+/// https://drafts.csswg.org/css-transitions/#animtype-lpcalc
+/// https://drafts.csswg.org/css-values-4/#combine-math
+/// https://drafts.csswg.org/css-values-4/#combine-mixed
+impl Animate for LengthPercentage {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ Ok(match (self.unpack(), other.unpack()) {
+ (Unpacked::Length(one), Unpacked::Length(other)) => {
+ Self::new_length(one.animate(&other, procedure)?)
+ },
+ (Unpacked::Percentage(one), Unpacked::Percentage(other)) => {
+ Self::new_percent(one.animate(&other, procedure)?)
+ },
+ _ => {
+ use calc::CalcNodeLeaf;
+
+ fn product_with(mut node: CalcNode, product: f32) -> CalcNode {
+ let mut number = CalcNode::Leaf(CalcLengthPercentageLeaf::new_number(product));
+ if !node.try_product_in_place(&mut number) {
+ CalcNode::Product(vec![node, number].into())
+ } else {
+ node
+ }
+ }
+
+ let (l, r) = procedure.weights();
+ let one = product_with(self.to_calc_node().into_owned(), l as f32);
+ let other = product_with(other.to_calc_node().into_owned(), r as f32);
+
+ Self::new_calc(
+ CalcNode::Sum(vec![one, other].into()),
+ AllowedNumericType::All,
+ )
+ },
+ })
+ }
+}
+
+/// A wrapper of LengthPercentage, whose value must be >= 0.
+pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>;
+
+impl ToAnimatedValue for NonNegativeLengthPercentage {
+ type AnimatedValue = LengthPercentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegative(animated.clamp_to_non_negative())
+ }
+}
+
+impl NonNegativeLengthPercentage {
+ /// Returns true if the computed value is absolute 0 or 0%.
+ #[inline]
+ pub fn is_definitely_zero(&self) -> bool {
+ self.0.is_definitely_zero()
+ }
+
+ /// Returns the used value.
+ #[inline]
+ pub fn to_used_value(&self, containing_length: Au) -> Au {
+ let resolved = self.0.to_used_value(containing_length);
+ std::cmp::max(resolved, Au(0))
+ }
+
+ /// Convert the computed value into used value.
+ #[inline]
+ pub fn maybe_to_used_value(&self, containing_length: Option<Au>) -> Option<Au> {
+ let resolved = self
+ .0
+ .maybe_to_used_value(containing_length.map(|v| v.into()))?;
+ Some(std::cmp::max(resolved, Au(0)))
+ }
+}
diff --git a/servo/components/style/values/computed/list.rs b/servo/components/style/values/computed/list.rs
new file mode 100644
index 0000000000..3e5d1eb220
--- /dev/null
+++ b/servo/components/style/values/computed/list.rs
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! `list` computed values.
+
+#[cfg(feature = "gecko")]
+pub use crate::values::specified::list::ListStyleType;
+pub use crate::values::specified::list::Quotes;
+
+impl Quotes {
+ /// Initial value for `quotes`.
+ #[inline]
+ pub fn get_initial_value() -> Quotes {
+ Quotes::Auto
+ }
+}
diff --git a/servo/components/style/values/computed/mod.rs b/servo/components/style/values/computed/mod.rs
new file mode 100644
index 0000000000..de5db2cdab
--- /dev/null
+++ b/servo/components/style/values/computed/mod.rs
@@ -0,0 +1,1035 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed values.
+
+use self::transform::DirectionVector;
+use super::animated::ToAnimatedValue;
+use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent;
+use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks;
+use super::generics::grid::{GenericGridLine, GenericTrackBreadth};
+use super::generics::grid::{GenericTrackSize, TrackList as GenericTrackList};
+use super::generics::transform::IsParallelTo;
+use super::generics::{self, GreaterThanOrEqualToOne, NonNegative, ZeroToOne};
+use super::specified;
+use super::{CSSFloat, CSSInteger};
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::context::QuirksMode;
+use crate::custom_properties::ComputedCustomProperties;
+use crate::font_metrics::{FontMetrics, FontMetricsOrientation};
+use crate::media_queries::Device;
+#[cfg(feature = "gecko")]
+use crate::properties;
+use crate::properties::{ComputedValues, StyleBuilder};
+use crate::rule_cache::RuleCacheConditions;
+use crate::stylesheets::container_rule::{
+ ContainerInfo, ContainerSizeQuery, ContainerSizeQueryResult,
+};
+use crate::stylist::Stylist;
+use crate::values::specified::length::FontBaseSize;
+use crate::{ArcSlice, Atom, One};
+use euclid::{default, Point2D, Rect, Size2D};
+use servo_arc::Arc;
+use std::cell::RefCell;
+use std::cmp;
+use std::f32;
+use std::ops::{Add, Sub};
+
+#[cfg(feature = "gecko")]
+pub use self::align::{
+ AlignContent, AlignItems, AlignTracks, JustifyContent, JustifyItems, JustifyTracks,
+ SelfAlignment,
+};
+#[cfg(feature = "gecko")]
+pub use self::align::{AlignSelf, JustifySelf};
+pub use self::angle::Angle;
+pub use self::animation::{
+ AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState,
+ AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis,
+ ScrollTimelineName, TransitionProperty, ViewTimelineInset
+};
+pub use self::background::{BackgroundRepeat, BackgroundSize};
+pub use self::basic_shape::FillRule;
+pub use self::border::{
+ BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice,
+ BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, LineWidth,
+};
+pub use self::box_::{
+ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize,
+ ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow,
+ OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign,
+ ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter,
+ TouchAction, VerticalAlign, WillChange, Zoom,
+};
+pub use self::color::{
+ Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust,
+};
+pub use self::column::ColumnCount;
+pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet};
+pub use self::easing::TimingFunction;
+pub use self::effects::{BoxShadow, Filter, SimpleShadow};
+pub use self::flex::FlexBasis;
+pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle};
+pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric};
+pub use self::font::{FontSize, FontSizeAdjust, FontStretch, FontSynthesis, LineHeight};
+pub use self::font::{FontVariantAlternates, FontWeight};
+pub use self::font::{FontVariantEastAsian, FontVariationSettings};
+pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale};
+pub use self::image::{Gradient, Image, ImageRendering, LineDirection};
+pub use self::length::{CSSPixelLength, NonNegativeLength};
+pub use self::length::{Length, LengthOrNumber, LengthPercentage, NonNegativeLengthOrNumber};
+pub use self::length::{LengthOrAuto, LengthPercentageOrAuto, MaxSize, Size};
+pub use self::length::{NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto};
+#[cfg(feature = "gecko")]
+pub use self::list::ListStyleType;
+pub use self::list::Quotes;
+pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate};
+pub use self::outline::OutlineStyle;
+pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize};
+pub use self::percentage::{NonNegativePercentage, Percentage};
+pub use self::position::AspectRatio;
+pub use self::position::{
+ GridAutoFlow, GridTemplateAreas, MasonryAutoFlow, Position, PositionOrAuto, ZIndex,
+};
+pub use self::ratio::Ratio;
+pub use self::rect::NonNegativeLengthOrNumberRect;
+pub use self::resolution::Resolution;
+pub use self::svg::{DProperty, MozContextProperties};
+pub use self::svg::{SVGLength, SVGOpacity, SVGPaint, SVGPaintKind};
+pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth};
+pub use self::text::HyphenateCharacter;
+pub use self::text::TextUnderlinePosition;
+pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextIndent};
+pub use self::text::{OverflowWrap, RubyPosition, TextOverflow, WordBreak, WordSpacing};
+pub use self::text::{TextAlign, TextAlignLast, TextEmphasisPosition, TextEmphasisStyle};
+pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify};
+pub use self::time::Time;
+pub use self::transform::{Rotate, Scale, Transform, TransformBox, TransformOperation};
+pub use self::transform::{TransformOrigin, TransformStyle, Translate};
+#[cfg(feature = "gecko")]
+pub use self::ui::CursorImage;
+pub use self::ui::{BoolInteger, Cursor, UserSelect};
+pub use super::specified::TextTransform;
+pub use super::specified::ViewportVariant;
+pub use super::specified::{BorderStyle, TextDecorationLine};
+pub use app_units::Au;
+
+#[cfg(feature = "gecko")]
+pub mod align;
+pub mod angle;
+pub mod animation;
+pub mod background;
+pub mod basic_shape;
+pub mod border;
+#[path = "box.rs"]
+pub mod box_;
+pub mod color;
+pub mod column;
+pub mod counters;
+pub mod easing;
+pub mod effects;
+pub mod flex;
+pub mod font;
+pub mod image;
+pub mod length;
+pub mod length_percentage;
+pub mod list;
+pub mod motion;
+pub mod outline;
+pub mod page;
+pub mod percentage;
+pub mod position;
+pub mod ratio;
+pub mod rect;
+pub mod resolution;
+pub mod svg;
+pub mod table;
+pub mod text;
+pub mod time;
+pub mod transform;
+pub mod ui;
+pub mod url;
+
+/// A `Context` is all the data a specified value could ever need to compute
+/// itself and be transformed to a computed value.
+pub struct Context<'a> {
+ /// Values accessed through this need to be in the properties "computed
+ /// early": color, text-decoration, font-size, display, position, float,
+ /// border-*-style, outline-style, font-family, writing-mode...
+ pub builder: StyleBuilder<'a>,
+
+ /// A cached computed system font value, for use by gecko.
+ ///
+ /// See properties/longhands/font.mako.rs
+ #[cfg(feature = "gecko")]
+ pub cached_system_font: Option<properties::longhands::system_font::ComputedSystemFont>,
+
+ /// A dummy option for servo so initializing a computed::Context isn't
+ /// painful.
+ ///
+ /// TODO(emilio): Make constructors for Context, and drop this.
+ #[cfg(feature = "servo")]
+ pub cached_system_font: Option<()>,
+
+ /// Whether or not we are computing the media list in a media query.
+ pub in_media_query: bool,
+
+ /// Whether or not we are computing the container query condition.
+ pub in_container_query: bool,
+
+ /// The quirks mode of this context.
+ pub quirks_mode: QuirksMode,
+
+ /// Whether this computation is being done for a SMIL animation.
+ ///
+ /// This is used to allow certain properties to generate out-of-range
+ /// values, which SMIL allows.
+ pub for_smil_animation: bool,
+
+ /// Returns the container information to evaluate a given container query.
+ pub container_info: Option<ContainerInfo>,
+
+ /// Whether we're computing a value for a non-inherited property.
+ /// False if we are computed a value for an inherited property or not computing for a property
+ /// at all (e.g. in a media query evaluation).
+ pub for_non_inherited_property: bool,
+
+ /// The conditions to cache a rule node on the rule cache.
+ ///
+ /// FIXME(emilio): Drop the refcell.
+ pub rule_cache_conditions: RefCell<&'a mut RuleCacheConditions>,
+
+ /// Container size query for this context.
+ container_size_query: RefCell<ContainerSizeQuery<'a>>,
+}
+
+impl<'a> Context<'a> {
+ /// Lazily evaluate the container size query, returning the result.
+ pub fn get_container_size_query(&self) -> ContainerSizeQueryResult {
+ let mut resolved = self.container_size_query.borrow_mut();
+ resolved.get().clone()
+ }
+
+ /// Creates a suitable context for media query evaluation, in which
+ /// font-relative units compute against the system_font, and executes `f`
+ /// with it.
+ pub fn for_media_query_evaluation<F, R>(device: &Device, quirks_mode: QuirksMode, f: F) -> R
+ where
+ F: FnOnce(&Context) -> R,
+ {
+ let mut conditions = RuleCacheConditions::default();
+ let context = Context {
+ builder: StyleBuilder::for_inheritance(device, None, None, None),
+ cached_system_font: None,
+ in_media_query: true,
+ in_container_query: false,
+ quirks_mode,
+ for_smil_animation: false,
+ container_info: None,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(&mut conditions),
+ container_size_query: RefCell::new(ContainerSizeQuery::none()),
+ };
+ f(&context)
+ }
+
+ /// Creates a suitable context for container query evaluation for the style
+ /// specified.
+ pub fn for_container_query_evaluation<F, R>(
+ device: &Device,
+ stylist: Option<&Stylist>,
+ container_info_and_style: Option<(ContainerInfo, Arc<ComputedValues>)>,
+ container_size_query: ContainerSizeQuery,
+ f: F,
+ ) -> R
+ where
+ F: FnOnce(&Context) -> R,
+ {
+ let mut conditions = RuleCacheConditions::default();
+
+ let (container_info, style) = match container_info_and_style {
+ Some((ci, s)) => (Some(ci), Some(s)),
+ None => (None, None),
+ };
+
+ let style = style.as_ref().map(|s| &**s);
+ let quirks_mode = device.quirks_mode();
+ let context = Context {
+ builder: StyleBuilder::for_inheritance(device, stylist, style, None),
+ cached_system_font: None,
+ in_media_query: false,
+ in_container_query: true,
+ quirks_mode,
+ for_smil_animation: false,
+ container_info,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(&mut conditions),
+ container_size_query: RefCell::new(container_size_query),
+ };
+
+ f(&context)
+ }
+
+ /// Creates a context suitable for more general cases.
+ pub fn new(
+ builder: StyleBuilder<'a>,
+ quirks_mode: QuirksMode,
+ rule_cache_conditions: &'a mut RuleCacheConditions,
+ container_size_query: ContainerSizeQuery<'a>,
+ ) -> Self {
+ Self {
+ builder,
+ cached_system_font: None,
+ in_media_query: false,
+ in_container_query: false,
+ quirks_mode,
+ container_info: None,
+ for_smil_animation: false,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(rule_cache_conditions),
+ container_size_query: RefCell::new(container_size_query),
+ }
+ }
+
+ /// Creates a context suitable for computing animations.
+ pub fn new_for_animation(
+ builder: StyleBuilder<'a>,
+ for_smil_animation: bool,
+ quirks_mode: QuirksMode,
+ rule_cache_conditions: &'a mut RuleCacheConditions,
+ container_size_query: ContainerSizeQuery<'a>,
+ ) -> Self {
+ Self {
+ builder,
+ cached_system_font: None,
+ in_media_query: false,
+ in_container_query: false,
+ quirks_mode,
+ container_info: None,
+ for_smil_animation,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(rule_cache_conditions),
+ container_size_query: RefCell::new(container_size_query),
+ }
+ }
+
+ /// Creates a context suitable for computing the initial value of @property.
+ pub fn new_for_initial_at_property_value(
+ stylist: &'a Stylist,
+ rule_cache_conditions: &'a mut RuleCacheConditions,
+ ) -> Self {
+ Self {
+ builder: StyleBuilder::new(stylist.device(), Some(stylist), None, None, None, false),
+ cached_system_font: None,
+ // Because font-relative values are disallowed in @property initial values, we do not
+ // need to keep track of whether we're in a media query, whether we're in a container
+ // query, and so on.
+ in_media_query: false,
+ in_container_query: false,
+ quirks_mode: stylist.quirks_mode(),
+ container_info: None,
+ for_smil_animation: false,
+ for_non_inherited_property: false,
+ rule_cache_conditions: RefCell::new(rule_cache_conditions),
+ container_size_query: RefCell::new(ContainerSizeQuery::none()),
+ }
+ }
+
+ /// The current device.
+ pub fn device(&self) -> &Device {
+ self.builder.device
+ }
+
+ /// Get the inherited custom properties map.
+ pub fn inherited_custom_properties(&self) -> &ComputedCustomProperties {
+ &self.builder.inherited_custom_properties()
+ }
+
+ /// Whether the style is for the root element.
+ pub fn is_root_element(&self) -> bool {
+ self.builder.is_root_element
+ }
+
+ /// Queries font metrics.
+ pub fn query_font_metrics(
+ &self,
+ base_size: FontBaseSize,
+ orientation: FontMetricsOrientation,
+ retrieve_math_scales: bool,
+ ) -> FontMetrics {
+ if self.for_non_inherited_property {
+ self.rule_cache_conditions.borrow_mut().set_uncacheable();
+ }
+ self.builder.add_flags(match base_size {
+ FontBaseSize::CurrentStyle => ComputedValueFlags::DEPENDS_ON_SELF_FONT_METRICS,
+ FontBaseSize::InheritedStyle => ComputedValueFlags::DEPENDS_ON_INHERITED_FONT_METRICS,
+ });
+ let size = base_size.resolve(self).used_size();
+ let style = self.style();
+
+ let (wm, font) = match base_size {
+ FontBaseSize::CurrentStyle => (style.writing_mode, style.get_font()),
+ // This is only used for font-size computation.
+ FontBaseSize::InheritedStyle => {
+ (*style.inherited_writing_mode(), style.get_parent_font())
+ },
+ };
+
+ let vertical = match orientation {
+ FontMetricsOrientation::MatchContextPreferHorizontal => {
+ wm.is_vertical() && wm.is_upright()
+ },
+ FontMetricsOrientation::MatchContextPreferVertical => wm.is_text_vertical(),
+ FontMetricsOrientation::Horizontal => false,
+ };
+ self.device().query_font_metrics(
+ vertical,
+ font,
+ size,
+ self.in_media_or_container_query(),
+ retrieve_math_scales,
+ )
+ }
+
+ /// The current viewport size, used to resolve viewport units.
+ pub fn viewport_size_for_viewport_unit_resolution(
+ &self,
+ variant: ViewportVariant,
+ ) -> default::Size2D<Au> {
+ self.builder
+ .add_flags(ComputedValueFlags::USES_VIEWPORT_UNITS);
+ self.builder
+ .device
+ .au_viewport_size_for_viewport_unit_resolution(variant)
+ }
+
+ /// Whether we're in a media or container query.
+ pub fn in_media_or_container_query(&self) -> bool {
+ self.in_media_query || self.in_container_query
+ }
+
+ /// The default computed style we're getting our reset style from.
+ pub fn default_style(&self) -> &ComputedValues {
+ self.builder.default_style()
+ }
+
+ /// The current style.
+ pub fn style(&self) -> &StyleBuilder {
+ &self.builder
+ }
+
+ /// Apply text-zoom if enabled.
+ #[cfg(feature = "gecko")]
+ pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength {
+ if self
+ .style()
+ .get_font()
+ .clone__x_text_scale()
+ .text_zoom_enabled()
+ {
+ self.device().zoom_text(size)
+ } else {
+ size
+ }
+ }
+
+ /// (Servo doesn't do text-zoom)
+ #[cfg(feature = "servo")]
+ pub fn maybe_zoom_text(&self, size: CSSPixelLength) -> CSSPixelLength {
+ size
+ }
+}
+
+/// An iterator over a slice of computed values
+#[derive(Clone)]
+pub struct ComputedVecIter<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> {
+ cx: &'cx Context<'cx_a>,
+ values: &'a [S],
+}
+
+impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ComputedVecIter<'a, 'cx, 'cx_a, S> {
+ /// Construct an iterator from a slice of specified values and a context
+ pub fn new(cx: &'cx Context<'cx_a>, values: &'a [S]) -> Self {
+ ComputedVecIter { cx, values }
+ }
+}
+
+impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> ExactSizeIterator
+ for ComputedVecIter<'a, 'cx, 'cx_a, S>
+{
+ fn len(&self) -> usize {
+ self.values.len()
+ }
+}
+
+impl<'a, 'cx, 'cx_a: 'cx, S: ToComputedValue + 'a> Iterator for ComputedVecIter<'a, 'cx, 'cx_a, S> {
+ type Item = S::ComputedValue;
+ fn next(&mut self) -> Option<Self::Item> {
+ if let Some((next, rest)) = self.values.split_first() {
+ let ret = next.to_computed_value(self.cx);
+ self.values = rest;
+ Some(ret)
+ } else {
+ None
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ (self.values.len(), Some(self.values.len()))
+ }
+}
+
+/// A trait to represent the conversion between computed and specified values.
+///
+/// This trait is derivable with `#[derive(ToComputedValue)]`. The derived
+/// implementation just calls `ToComputedValue::to_computed_value` on each field
+/// of the passed value. The deriving code assumes that if the type isn't
+/// generic, then the trait can be implemented as simple `Clone::clone` calls,
+/// this means that a manual implementation with `ComputedValue = Self` is bogus
+/// if it returns anything else than a clone.
+pub trait ToComputedValue {
+ /// The computed value type we're going to be converted to.
+ type ComputedValue;
+
+ /// Convert a specified value to a computed value, using itself and the data
+ /// inside the `Context`.
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue;
+
+ /// Convert a computed value to specified value form.
+ ///
+ /// This will be used for recascading during animation.
+ /// Such from_computed_valued values should recompute to the same value.
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self;
+}
+
+impl<A, B> ToComputedValue for (A, B)
+where
+ A: ToComputedValue,
+ B: ToComputedValue,
+{
+ type ComputedValue = (
+ <A as ToComputedValue>::ComputedValue,
+ <B as ToComputedValue>::ComputedValue,
+ );
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ (
+ self.0.to_computed_value(context),
+ self.1.to_computed_value(context),
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ (
+ A::from_computed_value(&computed.0),
+ B::from_computed_value(&computed.1),
+ )
+ }
+}
+
+impl<T> ToComputedValue for Option<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Option<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.as_ref().map(|item| item.to_computed_value(context))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed.as_ref().map(T::from_computed_value)
+ }
+}
+
+impl<T> ToComputedValue for default::Size2D<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = default::Size2D<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ Size2D::new(
+ self.width.to_computed_value(context),
+ self.height.to_computed_value(context),
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Size2D::new(
+ T::from_computed_value(&computed.width),
+ T::from_computed_value(&computed.height),
+ )
+ }
+}
+
+impl<T> ToComputedValue for Vec<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Vec<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.iter()
+ .map(|item| item.to_computed_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed.iter().map(T::from_computed_value).collect()
+ }
+}
+
+impl<T> ToComputedValue for Box<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Box<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ Box::new(T::to_computed_value(self, context))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Box::new(T::from_computed_value(computed))
+ }
+}
+
+impl<T> ToComputedValue for Box<[T]>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = Box<[<T as ToComputedValue>::ComputedValue]>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.iter()
+ .map(|item| item.to_computed_value(context))
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed
+ .iter()
+ .map(T::from_computed_value)
+ .collect::<Vec<_>>()
+ .into_boxed_slice()
+ }
+}
+
+impl<T> ToComputedValue for crate::OwnedSlice<T>
+where
+ T: ToComputedValue,
+{
+ type ComputedValue = crate::OwnedSlice<<T as ToComputedValue>::ComputedValue>;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ self.iter()
+ .map(|item| item.to_computed_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ computed.iter().map(T::from_computed_value).collect()
+ }
+}
+
+// NOTE(emilio): This is implementable more generically, but it's unlikely
+// what you want there, as it forces you to have an extra allocation.
+//
+// We could do that if needed, ideally with specialization for the case where
+// ComputedValue = T. But we don't need it for now.
+impl<T> ToComputedValue for Arc<T>
+where
+ T: ToComputedValue<ComputedValue = T>,
+{
+ type ComputedValue = Self;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self {
+ self.clone()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self) -> Self {
+ computed.clone()
+ }
+}
+
+// Same caveat as above applies.
+impl<T> ToComputedValue for ArcSlice<T>
+where
+ T: ToComputedValue<ComputedValue = T>,
+{
+ type ComputedValue = Self;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self {
+ self.clone()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self) -> Self {
+ computed.clone()
+ }
+}
+
+trivial_to_computed_value!(());
+trivial_to_computed_value!(bool);
+trivial_to_computed_value!(f32);
+trivial_to_computed_value!(i32);
+trivial_to_computed_value!(u8);
+trivial_to_computed_value!(u16);
+trivial_to_computed_value!(u32);
+trivial_to_computed_value!(usize);
+trivial_to_computed_value!(Atom);
+trivial_to_computed_value!(crate::values::AtomIdent);
+#[cfg(feature = "servo")]
+trivial_to_computed_value!(crate::Namespace);
+#[cfg(feature = "servo")]
+trivial_to_computed_value!(crate::Prefix);
+trivial_to_computed_value!(String);
+trivial_to_computed_value!(Box<str>);
+trivial_to_computed_value!(crate::OwnedStr);
+trivial_to_computed_value!(style_traits::values::specified::AllowedNumericType);
+trivial_to_computed_value!(crate::values::generics::color::ColorMixFlags);
+
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(C, u8)]
+pub enum AngleOrPercentage {
+ Percentage(Percentage),
+ Angle(Angle),
+}
+
+impl ToComputedValue for specified::AngleOrPercentage {
+ type ComputedValue = AngleOrPercentage;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> AngleOrPercentage {
+ match *self {
+ specified::AngleOrPercentage::Percentage(percentage) => {
+ AngleOrPercentage::Percentage(percentage.to_computed_value(context))
+ },
+ specified::AngleOrPercentage::Angle(angle) => {
+ AngleOrPercentage::Angle(angle.to_computed_value(context))
+ },
+ }
+ }
+ #[inline]
+ fn from_computed_value(computed: &AngleOrPercentage) -> Self {
+ match *computed {
+ AngleOrPercentage::Percentage(percentage) => specified::AngleOrPercentage::Percentage(
+ ToComputedValue::from_computed_value(&percentage),
+ ),
+ AngleOrPercentage::Angle(angle) => {
+ specified::AngleOrPercentage::Angle(ToComputedValue::from_computed_value(&angle))
+ },
+ }
+ }
+}
+
+/// A `<number>` value.
+pub type Number = CSSFloat;
+
+impl IsParallelTo for (Number, Number, Number) {
+ fn is_parallel_to(&self, vector: &DirectionVector) -> bool {
+ use euclid::approxeq::ApproxEq;
+ // If a and b is parallel, the angle between them is 0deg, so
+ // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0.
+ let self_vector = DirectionVector::new(self.0, self.1, self.2);
+ self_vector
+ .cross(*vector)
+ .square_length()
+ .approx_eq(&0.0f32)
+ }
+}
+
+/// A wrapper of Number, but the value >= 0.
+pub type NonNegativeNumber = NonNegative<CSSFloat>;
+
+impl ToAnimatedValue for NonNegativeNumber {
+ type AnimatedValue = CSSFloat;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.max(0.).into()
+ }
+}
+
+impl From<CSSFloat> for NonNegativeNumber {
+ #[inline]
+ fn from(number: CSSFloat) -> NonNegativeNumber {
+ NonNegative::<CSSFloat>(number)
+ }
+}
+
+impl From<NonNegativeNumber> for CSSFloat {
+ #[inline]
+ fn from(number: NonNegativeNumber) -> CSSFloat {
+ number.0
+ }
+}
+
+impl One for NonNegativeNumber {
+ #[inline]
+ fn one() -> Self {
+ NonNegative(1.0)
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.0 == 1.0
+ }
+}
+
+/// A wrapper of Number, but the value between 0 and 1
+pub type ZeroToOneNumber = ZeroToOne<CSSFloat>;
+
+impl ToAnimatedValue for ZeroToOneNumber {
+ type AnimatedValue = CSSFloat;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ Self(animated.max(0.).min(1.))
+ }
+}
+
+impl From<CSSFloat> for ZeroToOneNumber {
+ #[inline]
+ fn from(number: CSSFloat) -> Self {
+ Self(number)
+ }
+}
+
+/// A wrapper of Number, but the value >= 1.
+pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<CSSFloat>;
+
+impl ToAnimatedValue for GreaterThanOrEqualToOneNumber {
+ type AnimatedValue = CSSFloat;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ animated.max(1.).into()
+ }
+}
+
+impl From<CSSFloat> for GreaterThanOrEqualToOneNumber {
+ #[inline]
+ fn from(number: CSSFloat) -> GreaterThanOrEqualToOneNumber {
+ GreaterThanOrEqualToOne::<CSSFloat>(number)
+ }
+}
+
+impl From<GreaterThanOrEqualToOneNumber> for CSSFloat {
+ #[inline]
+ fn from(number: GreaterThanOrEqualToOneNumber) -> CSSFloat {
+ number.0
+ }
+}
+
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(C, u8)]
+pub enum NumberOrPercentage {
+ Percentage(Percentage),
+ Number(Number),
+}
+
+impl NumberOrPercentage {
+ fn clamp_to_non_negative(self) -> Self {
+ match self {
+ NumberOrPercentage::Percentage(p) => {
+ NumberOrPercentage::Percentage(p.clamp_to_non_negative())
+ },
+ NumberOrPercentage::Number(n) => NumberOrPercentage::Number(n.max(0.)),
+ }
+ }
+}
+
+impl ToComputedValue for specified::NumberOrPercentage {
+ type ComputedValue = NumberOrPercentage;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> NumberOrPercentage {
+ match *self {
+ specified::NumberOrPercentage::Percentage(percentage) => {
+ NumberOrPercentage::Percentage(percentage.to_computed_value(context))
+ },
+ specified::NumberOrPercentage::Number(number) => {
+ NumberOrPercentage::Number(number.to_computed_value(context))
+ },
+ }
+ }
+ #[inline]
+ fn from_computed_value(computed: &NumberOrPercentage) -> Self {
+ match *computed {
+ NumberOrPercentage::Percentage(percentage) => {
+ specified::NumberOrPercentage::Percentage(ToComputedValue::from_computed_value(
+ &percentage,
+ ))
+ },
+ NumberOrPercentage::Number(number) => {
+ specified::NumberOrPercentage::Number(ToComputedValue::from_computed_value(&number))
+ },
+ }
+ }
+}
+
+/// A non-negative <number-percentage>.
+pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>;
+
+impl NonNegativeNumberOrPercentage {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ NonNegative(NumberOrPercentage::Percentage(Percentage::hundred()))
+ }
+}
+
+impl ToAnimatedValue for NonNegativeNumberOrPercentage {
+ type AnimatedValue = NumberOrPercentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegative(animated.clamp_to_non_negative())
+ }
+}
+
+/// A type used for opacity.
+pub type Opacity = CSSFloat;
+
+/// A `<integer>` value.
+pub type Integer = CSSInteger;
+
+/// A wrapper of Integer, but only accept a value >= 1.
+pub type PositiveInteger = GreaterThanOrEqualToOne<CSSInteger>;
+
+impl ToAnimatedValue for PositiveInteger {
+ type AnimatedValue = CSSInteger;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ cmp::max(animated, 1).into()
+ }
+}
+
+impl From<CSSInteger> for PositiveInteger {
+ #[inline]
+ fn from(int: CSSInteger) -> PositiveInteger {
+ GreaterThanOrEqualToOne::<CSSInteger>(int)
+ }
+}
+
+/// rect(...) | auto
+pub type ClipRect = generics::GenericClipRect<LengthOrAuto>;
+
+/// rect(...) | auto
+pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>;
+
+/// The computed value of a grid `<track-breadth>`
+pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>;
+
+/// The computed value of a grid `<track-size>`
+pub type TrackSize = GenericTrackSize<LengthPercentage>;
+
+/// The computed value of a grid `<track-size>+`
+pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>;
+
+/// The computed value of a grid `<track-list>`
+/// (could also be `<auto-track-list>` or `<explicit-track-list>`)
+pub type TrackList = GenericTrackList<LengthPercentage, Integer>;
+
+/// The computed value of a `<grid-line>`.
+pub type GridLine = GenericGridLine<Integer>;
+
+/// `<grid-template-rows> | <grid-template-columns>`
+pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>;
+
+impl ClipRect {
+ /// Given a border box, resolves the clip rect against the border box
+ /// in the same space the border box is in
+ pub fn for_border_rect<T: Copy + From<Length> + Add<Output = T> + Sub<Output = T>, U>(
+ &self,
+ border_box: Rect<T, U>,
+ ) -> Rect<T, U> {
+ fn extract_clip_component<T: From<Length>>(p: &LengthOrAuto, or: T) -> T {
+ match *p {
+ LengthOrAuto::Auto => or,
+ LengthOrAuto::LengthPercentage(ref length) => T::from(*length),
+ }
+ }
+
+ let clip_origin = Point2D::new(
+ From::from(self.left.auto_is(|| Length::new(0.))),
+ From::from(self.top.auto_is(|| Length::new(0.))),
+ );
+ let right = extract_clip_component(&self.right, border_box.size.width);
+ let bottom = extract_clip_component(&self.bottom, border_box.size.height);
+ let clip_size = Size2D::new(right - clip_origin.x, bottom - clip_origin.y);
+
+ Rect::new(clip_origin, clip_size).translate(border_box.origin.to_vector())
+ }
+}
diff --git a/servo/components/style/values/computed/motion.rs b/servo/components/style/values/computed/motion.rs
new file mode 100644
index 0000000000..37b9f4909e
--- /dev/null
+++ b/servo/components/style/values/computed/motion.rs
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS values that are related to motion path.
+
+use crate::values::computed::basic_shape::BasicShape;
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{Angle, LengthPercentage, Position};
+use crate::values::generics::motion::{
+ GenericOffsetPath, GenericOffsetPathFunction, GenericOffsetPosition, GenericRayFunction,
+};
+use crate::Zero;
+
+/// The computed value of ray() function.
+pub type RayFunction = GenericRayFunction<Angle, Position>;
+
+/// The computed value of <offset-path>.
+pub type OffsetPathFunction = GenericOffsetPathFunction<BasicShape, RayFunction, ComputedUrl>;
+
+/// The computed value of `offset-path`.
+pub type OffsetPath = GenericOffsetPath<OffsetPathFunction>;
+
+/// The computed value of `offset-position`.
+pub type OffsetPosition = GenericOffsetPosition<LengthPercentage, LengthPercentage>;
+
+#[inline]
+fn is_auto_zero_angle(auto: &bool, angle: &Angle) -> bool {
+ *auto && angle.is_zero()
+}
+
+/// A computed offset-rotate.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+)]
+#[repr(C)]
+pub struct OffsetRotate {
+ /// If auto is false, this is a fixed angle which indicates a
+ /// constant clockwise rotation transformation applied to it by this
+ /// specified rotation angle. Otherwise, the angle will be added to
+ /// the angle of the direction in layout.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub auto: bool,
+ /// The angle value.
+ #[css(contextual_skip_if = "is_auto_zero_angle")]
+ pub angle: Angle,
+}
+
+impl OffsetRotate {
+ /// Returns "auto 0deg".
+ #[inline]
+ pub fn auto() -> Self {
+ OffsetRotate {
+ auto: true,
+ angle: Zero::zero(),
+ }
+ }
+}
diff --git a/servo/components/style/values/computed/outline.rs b/servo/components/style/values/computed/outline.rs
new file mode 100644
index 0000000000..f872176529
--- /dev/null
+++ b/servo/components/style/values/computed/outline.rs
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed values for outline properties
+
+pub use crate::values::specified::OutlineStyle;
diff --git a/servo/components/style/values/computed/page.rs b/servo/components/style/values/computed/page.rs
new file mode 100644
index 0000000000..6f71c912cf
--- /dev/null
+++ b/servo/components/style/values/computed/page.rs
@@ -0,0 +1,75 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed @page at-rule properties and named-page style properties
+
+use crate::values::computed::length::NonNegativeLength;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics;
+use crate::values::generics::size::Size2D;
+
+use crate::values::specified::page as specified;
+pub use generics::page::GenericPageSize;
+pub use generics::page::PageOrientation;
+pub use generics::page::PageSizeOrientation;
+pub use generics::page::PaperSize;
+pub use specified::PageName;
+
+/// Computed value of the @page size descriptor
+///
+/// The spec says that the computed value should be the same as the specified
+/// value but with all absolute units, but it's not currently possibly observe
+/// the computed value of page-size.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)]
+#[repr(C, u8)]
+pub enum PageSize {
+ /// Specified size, paper size, or paper size and orientation.
+ Size(Size2D<NonNegativeLength>),
+ /// `landscape` or `portrait` value, no specified size.
+ Orientation(PageSizeOrientation),
+ /// `auto` value
+ Auto,
+}
+
+impl ToComputedValue for specified::PageSize {
+ type ComputedValue = PageSize;
+
+ fn to_computed_value(&self, ctx: &Context) -> Self::ComputedValue {
+ match &*self {
+ Self::Size(s) => PageSize::Size(s.to_computed_value(ctx)),
+ Self::PaperSize(p, PageSizeOrientation::Landscape) => PageSize::Size(Size2D {
+ width: p.long_edge().to_computed_value(ctx),
+ height: p.short_edge().to_computed_value(ctx),
+ }),
+ Self::PaperSize(p, PageSizeOrientation::Portrait) => PageSize::Size(Size2D {
+ width: p.short_edge().to_computed_value(ctx),
+ height: p.long_edge().to_computed_value(ctx),
+ }),
+ Self::Orientation(o) => PageSize::Orientation(*o),
+ Self::Auto => PageSize::Auto,
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match *computed {
+ PageSize::Size(s) => Self::Size(ToComputedValue::from_computed_value(&s)),
+ PageSize::Orientation(o) => Self::Orientation(o),
+ PageSize::Auto => Self::Auto,
+ }
+ }
+}
+
+impl PageSize {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ PageSize::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, PageSize::Auto)
+ }
+}
diff --git a/servo/components/style/values/computed/percentage.rs b/servo/components/style/values/computed/percentage.rs
new file mode 100644
index 0000000000..994c01594a
--- /dev/null
+++ b/servo/components/style/values/computed/percentage.rs
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed percentages.
+
+use crate::values::animated::ToAnimatedValue;
+use crate::values::generics::NonNegative;
+use crate::values::specified::percentage::ToPercentage;
+use crate::values::{serialize_normalized_percentage, CSSFloat};
+use crate::Zero;
+use std::fmt;
+use style_traits::{CssWriter, ToCss};
+
+/// A computed percentage.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Default,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Percentage(pub CSSFloat);
+
+impl Percentage {
+ /// 100%
+ #[inline]
+ pub fn hundred() -> Self {
+ Percentage(1.)
+ }
+
+ /// Returns the absolute value for this percentage.
+ #[inline]
+ pub fn abs(&self) -> Self {
+ Percentage(self.0.abs())
+ }
+
+ /// Clamps this percentage to a non-negative percentage.
+ #[inline]
+ pub fn clamp_to_non_negative(self) -> Self {
+ Percentage(self.0.max(0.))
+ }
+}
+
+impl Zero for Percentage {
+ fn zero() -> Self {
+ Percentage(0.)
+ }
+
+ fn is_zero(&self) -> bool {
+ self.0 == 0.
+ }
+}
+
+impl ToPercentage for Percentage {
+ fn to_percentage(&self) -> CSSFloat {
+ self.0
+ }
+}
+
+impl std::ops::AddAssign for Percentage {
+ fn add_assign(&mut self, other: Self) {
+ self.0 += other.0
+ }
+}
+
+impl std::ops::Add for Percentage {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Percentage(self.0 + other.0)
+ }
+}
+
+impl std::ops::Sub for Percentage {
+ type Output = Self;
+
+ fn sub(self, other: Self) -> Self {
+ Percentage(self.0 - other.0)
+ }
+}
+
+impl std::ops::Rem for Percentage {
+ type Output = Self;
+
+ fn rem(self, other: Self) -> Self {
+ Percentage(self.0 % other.0)
+ }
+}
+
+impl ToCss for Percentage {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ serialize_normalized_percentage(self.0, dest)
+ }
+}
+
+/// A wrapper over a `Percentage`, whose value should be clamped to 0.
+pub type NonNegativePercentage = NonNegative<Percentage>;
+
+impl NonNegativePercentage {
+ /// 100%
+ #[inline]
+ pub fn hundred() -> Self {
+ NonNegative(Percentage::hundred())
+ }
+}
+
+impl ToAnimatedValue for NonNegativePercentage {
+ type AnimatedValue = Percentage;
+
+ #[inline]
+ fn to_animated_value(self) -> Self::AnimatedValue {
+ self.0
+ }
+
+ #[inline]
+ fn from_animated_value(animated: Self::AnimatedValue) -> Self {
+ NonNegative(animated.clamp_to_non_negative())
+ }
+}
diff --git a/servo/components/style/values/computed/position.rs b/servo/components/style/values/computed/position.rs
new file mode 100644
index 0000000000..5a10c0f23d
--- /dev/null
+++ b/servo/components/style/values/computed/position.rs
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the computed value of
+//! [`position`][position] values.
+//!
+//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position
+
+use crate::values::computed::{Integer, LengthPercentage, NonNegativeNumber, Percentage};
+use crate::values::generics::position::AspectRatio as GenericAspectRatio;
+use crate::values::generics::position::Position as GenericPosition;
+use crate::values::generics::position::PositionComponent as GenericPositionComponent;
+use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto;
+use crate::values::generics::position::ZIndex as GenericZIndex;
+pub use crate::values::specified::position::{GridAutoFlow, GridTemplateAreas, MasonryAutoFlow};
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// The computed value of a CSS `<position>`
+pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>;
+
+/// The computed value of an `auto | <position>`
+pub type PositionOrAuto = GenericPositionOrAuto<Position>;
+
+/// The computed value of a CSS horizontal position.
+pub type HorizontalPosition = LengthPercentage;
+
+/// The computed value of a CSS vertical position.
+pub type VerticalPosition = LengthPercentage;
+
+impl Position {
+ /// `50% 50%`
+ #[inline]
+ pub fn center() -> Self {
+ Self::new(
+ LengthPercentage::new_percent(Percentage(0.5)),
+ LengthPercentage::new_percent(Percentage(0.5)),
+ )
+ }
+
+ /// `0% 0%`
+ #[inline]
+ pub fn zero() -> Self {
+ Self::new(LengthPercentage::zero(), LengthPercentage::zero())
+ }
+}
+
+impl ToCss for Position {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.horizontal.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.vertical.to_css(dest)
+ }
+}
+
+impl GenericPositionComponent for LengthPercentage {
+ fn is_center(&self) -> bool {
+ match self.to_percentage() {
+ Some(Percentage(per)) => per == 0.5,
+ _ => false,
+ }
+ }
+}
+
+/// A computed value for the `z-index` property.
+pub type ZIndex = GenericZIndex<Integer>;
+
+/// A computed value for the `aspect-ratio` property.
+pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>;
diff --git a/servo/components/style/values/computed/ratio.rs b/servo/components/style/values/computed/ratio.rs
new file mode 100644
index 0000000000..ae8997cfc0
--- /dev/null
+++ b/servo/components/style/values/computed/ratio.rs
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! `<ratio>` computed values.
+
+use crate::values::animated::{Animate, Procedure};
+use crate::values::computed::NonNegativeNumber;
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::ratio::Ratio as GenericRatio;
+use crate::{One, Zero};
+use std::cmp::{Ordering, PartialOrd};
+
+/// A computed <ratio> value.
+pub type Ratio = GenericRatio<NonNegativeNumber>;
+
+impl PartialOrd for Ratio {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ f64::partial_cmp(
+ &((self.0).0 as f64 * (other.1).0 as f64),
+ &((self.1).0 as f64 * (other.0).0 as f64),
+ )
+ }
+}
+
+/// https://drafts.csswg.org/css-values/#combine-ratio
+impl Animate for Ratio {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ // If either <ratio> is degenerate, the values cannot be interpolated.
+ if self.is_degenerate() || other.is_degenerate() {
+ return Err(());
+ }
+
+ // Addition of <ratio>s is not possible, and based on
+ // https://drafts.csswg.org/css-values-4/#not-additive,
+ // we simply use the first value as the result value.
+ // Besides, the procedure for accumulation should be identical to addition here.
+ if matches!(procedure, Procedure::Add | Procedure::Accumulate { .. }) {
+ return Ok(self.clone());
+ }
+
+ // The interpolation of a <ratio> is defined by converting each <ratio> to a number by
+ // dividing the first value by the second (so a ratio of 3 / 2 would become 1.5), taking
+ // the logarithm of that result (so the 1.5 would become approximately 0.176), then
+ // interpolating those values.
+ //
+ // The result during the interpolation is converted back to a <ratio> by inverting the
+ // logarithm, then interpreting the result as a <ratio> with the result as the first value
+ // and 1 as the second value.
+ let start = self.to_f32().ln();
+ let end = other.to_f32().ln();
+ let e = std::f32::consts::E;
+ let result = e.powf(start.animate(&end, procedure)?);
+ // The range of the result is [0, inf), based on the easing function.
+ if result.is_zero() || result.is_infinite() {
+ return Err(());
+ }
+ Ok(Ratio::new(result, 1.0f32))
+ }
+}
+
+impl ComputeSquaredDistance for Ratio {
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.is_degenerate() || other.is_degenerate() {
+ return Err(());
+ }
+ // Use the distance of their logarithm values. (This is used by testing, so don't need to
+ // care about the base. Here we use the same base as that in animate().)
+ self.to_f32()
+ .ln()
+ .compute_squared_distance(&other.to_f32().ln())
+ }
+}
+
+impl Zero for Ratio {
+ fn zero() -> Self {
+ Self::new(Zero::zero(), One::one())
+ }
+
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+impl Ratio {
+ /// Returns a new Ratio.
+ #[inline]
+ pub fn new(a: f32, b: f32) -> Self {
+ GenericRatio(a.into(), b.into())
+ }
+
+ /// Returns the used value. A ratio of 0/0 behaves as the ratio 1/0.
+ /// https://drafts.csswg.org/css-values-4/#ratios
+ pub fn used_value(self) -> Self {
+ if self.0.is_zero() && self.1.is_zero() {
+ Ratio::new(One::one(), Zero::zero())
+ } else {
+ self
+ }
+ }
+
+ /// Returns true if this is a degenerate ratio.
+ /// https://drafts.csswg.org/css-values/#degenerate-ratio
+ #[inline]
+ pub fn is_degenerate(&self) -> bool {
+ self.0.is_zero() || self.1.is_zero()
+ }
+
+ /// Returns the f32 value by dividing the first value by the second one.
+ #[inline]
+ fn to_f32(&self) -> f32 {
+ debug_assert!(!self.is_degenerate());
+ (self.0).0 / (self.1).0
+ }
+}
diff --git a/servo/components/style/values/computed/rect.rs b/servo/components/style/values/computed/rect.rs
new file mode 100644
index 0000000000..ec44360fc8
--- /dev/null
+++ b/servo/components/style/values/computed/rect.rs
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS borders.
+
+use crate::values::computed::length::NonNegativeLengthOrNumber;
+use crate::values::generics::rect::Rect;
+
+/// A specified rectangle made of four `<length-or-number>` values.
+pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>;
diff --git a/servo/components/style/values/computed/resolution.rs b/servo/components/style/values/computed/resolution.rs
new file mode 100644
index 0000000000..430c80d211
--- /dev/null
+++ b/servo/components/style/values/computed/resolution.rs
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Resolution values:
+//!
+//! https://drafts.csswg.org/css-values/#resolution
+
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified;
+use crate::values::CSSFloat;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A computed `<resolution>`.
+#[repr(C)]
+#[derive(Animate, Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem)]
+pub struct Resolution(CSSFloat);
+
+impl Resolution {
+ /// Returns this resolution value as dppx.
+ #[inline]
+ pub fn dppx(&self) -> CSSFloat {
+ self.0
+ }
+
+ /// Return a computed `resolution` value from a dppx float value.
+ #[inline]
+ pub fn from_dppx(dppx: CSSFloat) -> Self {
+ Resolution(dppx)
+ }
+}
+
+impl ToComputedValue for specified::Resolution {
+ type ComputedValue = Resolution;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ Resolution(crate::values::normalize(self.dppx().max(0.0)))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ specified::Resolution::from_dppx(computed.dppx())
+ }
+}
+
+impl ToCss for Resolution {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.dppx().to_css(dest)?;
+ dest.write_str("dppx")
+ }
+}
diff --git a/servo/components/style/values/computed/svg.rs b/servo/components/style/values/computed/svg.rs
new file mode 100644
index 0000000000..6efdfca36b
--- /dev/null
+++ b/servo/components/style/values/computed/svg.rs
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for SVG properties.
+
+use crate::values::computed::color::Color;
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::{LengthPercentage, NonNegativeLengthPercentage, Opacity};
+use crate::values::generics::svg as generic;
+use crate::Zero;
+
+pub use crate::values::specified::{DProperty, MozContextProperties, SVGPaintOrder};
+
+/// Computed SVG Paint value
+pub type SVGPaint = generic::GenericSVGPaint<Color, ComputedUrl>;
+
+/// Computed SVG Paint Kind value
+pub type SVGPaintKind = generic::GenericSVGPaintKind<Color, ComputedUrl>;
+
+impl SVGPaint {
+ /// Opaque black color
+ pub const BLACK: Self = Self {
+ kind: generic::SVGPaintKind::Color(Color::BLACK),
+ fallback: generic::SVGPaintFallback::Unset,
+ };
+}
+
+/// <length> | <percentage> | <number> | context-value
+pub type SVGLength = generic::GenericSVGLength<LengthPercentage>;
+
+impl SVGLength {
+ /// `0px`
+ pub fn zero() -> Self {
+ generic::SVGLength::LengthPercentage(LengthPercentage::zero())
+ }
+}
+
+/// An non-negative wrapper of SVGLength.
+pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>;
+
+impl SVGWidth {
+ /// `1px`.
+ pub fn one() -> Self {
+ use crate::values::generics::NonNegative;
+ generic::SVGLength::LengthPercentage(NonNegative(LengthPercentage::one()))
+ }
+}
+
+/// [ <length> | <percentage> | <number> ]# | context-value
+pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>;
+
+impl Default for SVGStrokeDashArray {
+ fn default() -> Self {
+ generic::SVGStrokeDashArray::Values(Default::default())
+ }
+}
+
+/// <opacity-value> | context-fill-opacity | context-stroke-opacity
+pub type SVGOpacity = generic::GenericSVGOpacity<Opacity>;
+
+impl Default for SVGOpacity {
+ fn default() -> Self {
+ generic::SVGOpacity::Opacity(1.)
+ }
+}
diff --git a/servo/components/style/values/computed/table.rs b/servo/components/style/values/computed/table.rs
new file mode 100644
index 0000000000..47109e20ec
--- /dev/null
+++ b/servo/components/style/values/computed/table.rs
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS values related to tables.
+
+pub use super::specified::table::CaptionSide;
diff --git a/servo/components/style/values/computed/text.rs b/servo/components/style/values/computed/text.rs
new file mode 100644
index 0000000000..a4fec654a5
--- /dev/null
+++ b/servo/components/style/values/computed/text.rs
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for text properties.
+
+#[cfg(feature = "servo")]
+use crate::properties::StyleBuilder;
+use crate::values::computed::length::{Length, LengthPercentage};
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::text::InitialLetter as GenericInitialLetter;
+use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing};
+use crate::values::specified::text::{self as specified, TextOverflowSide};
+use crate::values::specified::text::{TextEmphasisFillMode, TextEmphasisShapeKeyword};
+use crate::values::{CSSFloat, CSSInteger};
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+pub use crate::values::specified::text::{
+ MozControlCharacterVisibility, TextAlignLast, TextUnderlinePosition,
+};
+pub use crate::values::specified::HyphenateCharacter;
+pub use crate::values::specified::{LineBreak, OverflowWrap, RubyPosition, WordBreak};
+pub use crate::values::specified::{TextDecorationLine, TextEmphasisPosition};
+pub use crate::values::specified::{TextDecorationSkipInk, TextJustify, TextTransform};
+
+/// A computed value for the `initial-letter` property.
+pub type InitialLetter = GenericInitialLetter<CSSFloat, CSSInteger>;
+
+/// Implements type for `text-decoration-thickness` property.
+pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
+
+/// The computed value of `text-align`.
+pub type TextAlign = specified::TextAlignKeyword;
+
+/// The computed value of `text-indent`.
+pub type TextIndent = GenericTextIndent<LengthPercentage>;
+
+/// A computed value for the `letter-spacing` property.
+#[repr(transparent)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+)]
+pub struct LetterSpacing(pub Length);
+
+impl LetterSpacing {
+ /// Return the `normal` computed value, which is just zero.
+ #[inline]
+ pub fn normal() -> Self {
+ LetterSpacing(Length::zero())
+ }
+}
+
+impl ToCss for LetterSpacing {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ // https://drafts.csswg.org/css-text/#propdef-letter-spacing
+ //
+ // For legacy reasons, a computed letter-spacing of zero yields a
+ // resolved value (getComputedStyle() return value) of normal.
+ if self.0.is_zero() {
+ return dest.write_str("normal");
+ }
+ self.0.to_css(dest)
+ }
+}
+
+impl ToComputedValue for specified::LetterSpacing {
+ type ComputedValue = LetterSpacing;
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ Spacing::Normal => LetterSpacing(Length::zero()),
+ Spacing::Value(ref v) => LetterSpacing(v.to_computed_value(context)),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ if computed.0.is_zero() {
+ return Spacing::Normal;
+ }
+ Spacing::Value(ToComputedValue::from_computed_value(&computed.0))
+ }
+}
+
+/// A computed value for the `word-spacing` property.
+pub type WordSpacing = LengthPercentage;
+
+impl ToComputedValue for specified::WordSpacing {
+ type ComputedValue = WordSpacing;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ Spacing::Normal => LengthPercentage::zero(),
+ Spacing::Value(ref v) => v.to_computed_value(context),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Spacing::Value(ToComputedValue::from_computed_value(computed))
+ }
+}
+
+impl WordSpacing {
+ /// Return the `normal` computed value, which is just zero.
+ #[inline]
+ pub fn normal() -> Self {
+ LengthPercentage::zero()
+ }
+}
+
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue)]
+#[repr(C)]
+/// text-overflow.
+/// When the specified value only has one side, that's the "second"
+/// side, and the sides are logical, so "second" means "end". The
+/// start side is Clip in that case.
+///
+/// When the specified value has two sides, those are our "first"
+/// and "second" sides, and they are physical sides ("left" and
+/// "right").
+pub struct TextOverflow {
+ /// First side
+ pub first: TextOverflowSide,
+ /// Second side
+ pub second: TextOverflowSide,
+ /// True if the specified value only has one side.
+ pub sides_are_logical: bool,
+}
+
+impl TextOverflow {
+ /// Returns the initial `text-overflow` value
+ pub fn get_initial_value() -> TextOverflow {
+ TextOverflow {
+ first: TextOverflowSide::Clip,
+ second: TextOverflowSide::Clip,
+ sides_are_logical: true,
+ }
+ }
+}
+
+impl ToCss for TextOverflow {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.sides_are_logical {
+ debug_assert_eq!(self.first, TextOverflowSide::Clip);
+ self.second.to_css(dest)?;
+ } else {
+ self.first.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.second.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// A struct that represents the _used_ value of the text-decoration property.
+///
+/// FIXME(emilio): This is done at style resolution time, though probably should
+/// be done at layout time, otherwise we need to account for display: contents
+/// and similar stuff when we implement it.
+///
+/// FIXME(emilio): Also, should be just a bitfield instead of three bytes.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToResolvedValue)]
+pub struct TextDecorationsInEffect {
+ /// Whether an underline is in effect.
+ pub underline: bool,
+ /// Whether an overline decoration is in effect.
+ pub overline: bool,
+ /// Whether a line-through style is in effect.
+ pub line_through: bool,
+}
+
+impl TextDecorationsInEffect {
+ /// Computes the text-decorations in effect for a given style.
+ #[cfg(feature = "servo")]
+ pub fn from_style(style: &StyleBuilder) -> Self {
+ // Start with no declarations if this is an atomic inline-level box;
+ // otherwise, start with the declarations in effect and add in the text
+ // decorations that this block specifies.
+ let mut result = if style.get_box().clone_display().is_atomic_inline_level() {
+ Self::default()
+ } else {
+ style
+ .get_parent_inherited_text()
+ .text_decorations_in_effect
+ .clone()
+ };
+
+ let line = style.get_text().clone_text_decoration_line();
+
+ result.underline |= line.contains(TextDecorationLine::UNDERLINE);
+ result.overline |= line.contains(TextDecorationLine::OVERLINE);
+ result.line_through |= line.contains(TextDecorationLine::LINE_THROUGH);
+
+ result
+ }
+}
+
+/// Computed value for the text-emphasis-style property
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue)]
+#[allow(missing_docs)]
+#[repr(C, u8)]
+pub enum TextEmphasisStyle {
+ /// [ <fill> || <shape> ]
+ Keyword {
+ #[css(skip_if = "TextEmphasisFillMode::is_filled")]
+ fill: TextEmphasisFillMode,
+ shape: TextEmphasisShapeKeyword,
+ },
+ /// `none`
+ None,
+ /// `<string>` (of which only the first grapheme cluster will be used).
+ String(crate::OwnedStr),
+}
diff --git a/servo/components/style/values/computed/time.rs b/servo/components/style/values/computed/time.rs
new file mode 100644
index 0000000000..b81c6e879a
--- /dev/null
+++ b/servo/components/style/values/computed/time.rs
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed time values.
+
+use crate::values::CSSFloat;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A computed `<time>` value.
+#[derive(Animate, Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToResolvedValue)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[repr(C)]
+pub struct Time {
+ seconds: CSSFloat,
+}
+
+impl Time {
+ /// Creates a time value from a seconds amount.
+ pub fn from_seconds(seconds: CSSFloat) -> Self {
+ Time { seconds }
+ }
+
+ /// Returns `0s`.
+ pub fn zero() -> Self {
+ Self::from_seconds(0.0)
+ }
+
+ /// Returns the amount of seconds this time represents.
+ #[inline]
+ pub fn seconds(&self) -> CSSFloat {
+ self.seconds
+ }
+}
+
+impl ToCss for Time {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.seconds().to_css(dest)?;
+ dest.write_char('s')
+ }
+}
diff --git a/servo/components/style/values/computed/transform.rs b/servo/components/style/values/computed/transform.rs
new file mode 100644
index 0000000000..b1a617fd4b
--- /dev/null
+++ b/servo/components/style/values/computed/transform.rs
@@ -0,0 +1,559 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed types for CSS values that are related to transformations.
+
+use super::CSSFloat;
+use crate::values::animated::transform::{Perspective, Scale3D, Translate3D};
+use crate::values::animated::ToAnimatedZero;
+use crate::values::computed::{Angle, Integer, Length, LengthPercentage, Number, Percentage};
+use crate::values::generics::transform as generic;
+use crate::Zero;
+use euclid::default::{Transform3D, Vector3D};
+
+pub use crate::values::generics::transform::TransformStyle;
+pub use crate::values::specified::transform::TransformBox;
+
+/// A single operation in a computed CSS `transform`
+pub type TransformOperation =
+ generic::GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>;
+/// A computed CSS `transform`
+pub type Transform = generic::GenericTransform<TransformOperation>;
+
+/// The computed value of a CSS `<transform-origin>`
+pub type TransformOrigin =
+ generic::GenericTransformOrigin<LengthPercentage, LengthPercentage, Length>;
+
+/// The computed value of the `perspective()` transform function.
+pub type PerspectiveFunction = generic::PerspectiveFunction<Length>;
+
+/// A vector to represent the direction vector (rotate axis) for Rotate3D.
+pub type DirectionVector = Vector3D<CSSFloat>;
+
+impl TransformOrigin {
+ /// Returns the initial computed value for `transform-origin`.
+ #[inline]
+ pub fn initial_value() -> Self {
+ Self::new(
+ LengthPercentage::new_percent(Percentage(0.5)),
+ LengthPercentage::new_percent(Percentage(0.5)),
+ Length::new(0.),
+ )
+ }
+}
+
+/// computed value of matrix3d()
+pub type Matrix3D = generic::Matrix3D<Number>;
+
+/// computed value of matrix()
+pub type Matrix = generic::Matrix<Number>;
+
+// we rustfmt_skip here because we want the matrices to look like
+// matrices instead of being split across lines
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl Matrix3D {
+ /// Get an identity matrix
+ #[inline]
+ pub fn identity() -> Self {
+ Self {
+ m11: 1.0, m12: 0.0, m13: 0.0, m14: 0.0,
+ m21: 0.0, m22: 1.0, m23: 0.0, m24: 0.0,
+ m31: 0.0, m32: 0.0, m33: 1.0, m34: 0.0,
+ m41: 0., m42: 0., m43: 0., m44: 1.0
+ }
+ }
+
+ /// Convert to a 2D Matrix
+ #[inline]
+ pub fn into_2d(self) -> Result<Matrix, ()> {
+ if self.m13 == 0. && self.m23 == 0. &&
+ self.m31 == 0. && self.m32 == 0. &&
+ self.m33 == 1. && self.m34 == 0. &&
+ self.m14 == 0. && self.m24 == 0. &&
+ self.m43 == 0. && self.m44 == 1. {
+ Ok(Matrix {
+ a: self.m11, c: self.m21, e: self.m41,
+ b: self.m12, d: self.m22, f: self.m42,
+ })
+ } else {
+ Err(())
+ }
+ }
+
+ /// Return true if this has 3D components.
+ #[inline]
+ pub fn is_3d(&self) -> bool {
+ self.m13 != 0.0 || self.m14 != 0.0 ||
+ self.m23 != 0.0 || self.m24 != 0.0 ||
+ self.m31 != 0.0 || self.m32 != 0.0 ||
+ self.m33 != 1.0 || self.m34 != 0.0 ||
+ self.m43 != 0.0 || self.m44 != 1.0
+ }
+
+ /// Return determinant value.
+ #[inline]
+ pub fn determinant(&self) -> CSSFloat {
+ self.m14 * self.m23 * self.m32 * self.m41 -
+ self.m13 * self.m24 * self.m32 * self.m41 -
+ self.m14 * self.m22 * self.m33 * self.m41 +
+ self.m12 * self.m24 * self.m33 * self.m41 +
+ self.m13 * self.m22 * self.m34 * self.m41 -
+ self.m12 * self.m23 * self.m34 * self.m41 -
+ self.m14 * self.m23 * self.m31 * self.m42 +
+ self.m13 * self.m24 * self.m31 * self.m42 +
+ self.m14 * self.m21 * self.m33 * self.m42 -
+ self.m11 * self.m24 * self.m33 * self.m42 -
+ self.m13 * self.m21 * self.m34 * self.m42 +
+ self.m11 * self.m23 * self.m34 * self.m42 +
+ self.m14 * self.m22 * self.m31 * self.m43 -
+ self.m12 * self.m24 * self.m31 * self.m43 -
+ self.m14 * self.m21 * self.m32 * self.m43 +
+ self.m11 * self.m24 * self.m32 * self.m43 +
+ self.m12 * self.m21 * self.m34 * self.m43 -
+ self.m11 * self.m22 * self.m34 * self.m43 -
+ self.m13 * self.m22 * self.m31 * self.m44 +
+ self.m12 * self.m23 * self.m31 * self.m44 +
+ self.m13 * self.m21 * self.m32 * self.m44 -
+ self.m11 * self.m23 * self.m32 * self.m44 -
+ self.m12 * self.m21 * self.m33 * self.m44 +
+ self.m11 * self.m22 * self.m33 * self.m44
+ }
+
+ /// Transpose a matrix.
+ #[inline]
+ pub fn transpose(&self) -> Self {
+ Self {
+ m11: self.m11, m12: self.m21, m13: self.m31, m14: self.m41,
+ m21: self.m12, m22: self.m22, m23: self.m32, m24: self.m42,
+ m31: self.m13, m32: self.m23, m33: self.m33, m34: self.m43,
+ m41: self.m14, m42: self.m24, m43: self.m34, m44: self.m44,
+ }
+ }
+
+ /// Return inverse matrix.
+ pub fn inverse(&self) -> Result<Matrix3D, ()> {
+ let mut det = self.determinant();
+
+ if det == 0.0 {
+ return Err(());
+ }
+
+ det = 1.0 / det;
+ let x = Matrix3D {
+ m11: det *
+ (self.m23 * self.m34 * self.m42 - self.m24 * self.m33 * self.m42 +
+ self.m24 * self.m32 * self.m43 - self.m22 * self.m34 * self.m43 -
+ self.m23 * self.m32 * self.m44 + self.m22 * self.m33 * self.m44),
+ m12: det *
+ (self.m14 * self.m33 * self.m42 - self.m13 * self.m34 * self.m42 -
+ self.m14 * self.m32 * self.m43 + self.m12 * self.m34 * self.m43 +
+ self.m13 * self.m32 * self.m44 - self.m12 * self.m33 * self.m44),
+ m13: det *
+ (self.m13 * self.m24 * self.m42 - self.m14 * self.m23 * self.m42 +
+ self.m14 * self.m22 * self.m43 - self.m12 * self.m24 * self.m43 -
+ self.m13 * self.m22 * self.m44 + self.m12 * self.m23 * self.m44),
+ m14: det *
+ (self.m14 * self.m23 * self.m32 - self.m13 * self.m24 * self.m32 -
+ self.m14 * self.m22 * self.m33 + self.m12 * self.m24 * self.m33 +
+ self.m13 * self.m22 * self.m34 - self.m12 * self.m23 * self.m34),
+ m21: det *
+ (self.m24 * self.m33 * self.m41 - self.m23 * self.m34 * self.m41 -
+ self.m24 * self.m31 * self.m43 + self.m21 * self.m34 * self.m43 +
+ self.m23 * self.m31 * self.m44 - self.m21 * self.m33 * self.m44),
+ m22: det *
+ (self.m13 * self.m34 * self.m41 - self.m14 * self.m33 * self.m41 +
+ self.m14 * self.m31 * self.m43 - self.m11 * self.m34 * self.m43 -
+ self.m13 * self.m31 * self.m44 + self.m11 * self.m33 * self.m44),
+ m23: det *
+ (self.m14 * self.m23 * self.m41 - self.m13 * self.m24 * self.m41 -
+ self.m14 * self.m21 * self.m43 + self.m11 * self.m24 * self.m43 +
+ self.m13 * self.m21 * self.m44 - self.m11 * self.m23 * self.m44),
+ m24: det *
+ (self.m13 * self.m24 * self.m31 - self.m14 * self.m23 * self.m31 +
+ self.m14 * self.m21 * self.m33 - self.m11 * self.m24 * self.m33 -
+ self.m13 * self.m21 * self.m34 + self.m11 * self.m23 * self.m34),
+ m31: det *
+ (self.m22 * self.m34 * self.m41 - self.m24 * self.m32 * self.m41 +
+ self.m24 * self.m31 * self.m42 - self.m21 * self.m34 * self.m42 -
+ self.m22 * self.m31 * self.m44 + self.m21 * self.m32 * self.m44),
+ m32: det *
+ (self.m14 * self.m32 * self.m41 - self.m12 * self.m34 * self.m41 -
+ self.m14 * self.m31 * self.m42 + self.m11 * self.m34 * self.m42 +
+ self.m12 * self.m31 * self.m44 - self.m11 * self.m32 * self.m44),
+ m33: det *
+ (self.m12 * self.m24 * self.m41 - self.m14 * self.m22 * self.m41 +
+ self.m14 * self.m21 * self.m42 - self.m11 * self.m24 * self.m42 -
+ self.m12 * self.m21 * self.m44 + self.m11 * self.m22 * self.m44),
+ m34: det *
+ (self.m14 * self.m22 * self.m31 - self.m12 * self.m24 * self.m31 -
+ self.m14 * self.m21 * self.m32 + self.m11 * self.m24 * self.m32 +
+ self.m12 * self.m21 * self.m34 - self.m11 * self.m22 * self.m34),
+ m41: det *
+ (self.m23 * self.m32 * self.m41 - self.m22 * self.m33 * self.m41 -
+ self.m23 * self.m31 * self.m42 + self.m21 * self.m33 * self.m42 +
+ self.m22 * self.m31 * self.m43 - self.m21 * self.m32 * self.m43),
+ m42: det *
+ (self.m12 * self.m33 * self.m41 - self.m13 * self.m32 * self.m41 +
+ self.m13 * self.m31 * self.m42 - self.m11 * self.m33 * self.m42 -
+ self.m12 * self.m31 * self.m43 + self.m11 * self.m32 * self.m43),
+ m43: det *
+ (self.m13 * self.m22 * self.m41 - self.m12 * self.m23 * self.m41 -
+ self.m13 * self.m21 * self.m42 + self.m11 * self.m23 * self.m42 +
+ self.m12 * self.m21 * self.m43 - self.m11 * self.m22 * self.m43),
+ m44: det *
+ (self.m12 * self.m23 * self.m31 - self.m13 * self.m22 * self.m31 +
+ self.m13 * self.m21 * self.m32 - self.m11 * self.m23 * self.m32 -
+ self.m12 * self.m21 * self.m33 + self.m11 * self.m22 * self.m33),
+ };
+
+ Ok(x)
+ }
+
+ /// Multiply `pin * self`.
+ #[inline]
+ pub fn pre_mul_point4(&self, pin: &[f32; 4]) -> [f32; 4] {
+ [
+ pin[0] * self.m11 + pin[1] * self.m21 + pin[2] * self.m31 + pin[3] * self.m41,
+ pin[0] * self.m12 + pin[1] * self.m22 + pin[2] * self.m32 + pin[3] * self.m42,
+ pin[0] * self.m13 + pin[1] * self.m23 + pin[2] * self.m33 + pin[3] * self.m43,
+ pin[0] * self.m14 + pin[1] * self.m24 + pin[2] * self.m34 + pin[3] * self.m44,
+ ]
+ }
+
+ /// Return the multiplication of two 4x4 matrices.
+ #[inline]
+ pub fn multiply(&self, other: &Self) -> Self {
+ Matrix3D {
+ m11: self.m11 * other.m11 + self.m12 * other.m21 +
+ self.m13 * other.m31 + self.m14 * other.m41,
+ m12: self.m11 * other.m12 + self.m12 * other.m22 +
+ self.m13 * other.m32 + self.m14 * other.m42,
+ m13: self.m11 * other.m13 + self.m12 * other.m23 +
+ self.m13 * other.m33 + self.m14 * other.m43,
+ m14: self.m11 * other.m14 + self.m12 * other.m24 +
+ self.m13 * other.m34 + self.m14 * other.m44,
+ m21: self.m21 * other.m11 + self.m22 * other.m21 +
+ self.m23 * other.m31 + self.m24 * other.m41,
+ m22: self.m21 * other.m12 + self.m22 * other.m22 +
+ self.m23 * other.m32 + self.m24 * other.m42,
+ m23: self.m21 * other.m13 + self.m22 * other.m23 +
+ self.m23 * other.m33 + self.m24 * other.m43,
+ m24: self.m21 * other.m14 + self.m22 * other.m24 +
+ self.m23 * other.m34 + self.m24 * other.m44,
+ m31: self.m31 * other.m11 + self.m32 * other.m21 +
+ self.m33 * other.m31 + self.m34 * other.m41,
+ m32: self.m31 * other.m12 + self.m32 * other.m22 +
+ self.m33 * other.m32 + self.m34 * other.m42,
+ m33: self.m31 * other.m13 + self.m32 * other.m23 +
+ self.m33 * other.m33 + self.m34 * other.m43,
+ m34: self.m31 * other.m14 + self.m32 * other.m24 +
+ self.m33 * other.m34 + self.m34 * other.m44,
+ m41: self.m41 * other.m11 + self.m42 * other.m21 +
+ self.m43 * other.m31 + self.m44 * other.m41,
+ m42: self.m41 * other.m12 + self.m42 * other.m22 +
+ self.m43 * other.m32 + self.m44 * other.m42,
+ m43: self.m41 * other.m13 + self.m42 * other.m23 +
+ self.m43 * other.m33 + self.m44 * other.m43,
+ m44: self.m41 * other.m14 + self.m42 * other.m24 +
+ self.m43 * other.m34 + self.m44 * other.m44,
+ }
+ }
+
+ /// Scale the matrix by a factor.
+ #[inline]
+ pub fn scale_by_factor(&mut self, scaling_factor: CSSFloat) {
+ self.m11 *= scaling_factor;
+ self.m12 *= scaling_factor;
+ self.m13 *= scaling_factor;
+ self.m14 *= scaling_factor;
+ self.m21 *= scaling_factor;
+ self.m22 *= scaling_factor;
+ self.m23 *= scaling_factor;
+ self.m24 *= scaling_factor;
+ self.m31 *= scaling_factor;
+ self.m32 *= scaling_factor;
+ self.m33 *= scaling_factor;
+ self.m34 *= scaling_factor;
+ self.m41 *= scaling_factor;
+ self.m42 *= scaling_factor;
+ self.m43 *= scaling_factor;
+ self.m44 *= scaling_factor;
+ }
+
+ /// Return the matrix 3x3 part (top-left corner).
+ /// This is used by retrieving the scale and shear factors
+ /// during decomposing a 3d matrix.
+ #[inline]
+ pub fn get_matrix_3x3_part(&self) -> [[f32; 3]; 3] {
+ [
+ [ self.m11, self.m12, self.m13 ],
+ [ self.m21, self.m22, self.m23 ],
+ [ self.m31, self.m32, self.m33 ],
+ ]
+ }
+
+ /// Set perspective on the matrix.
+ #[inline]
+ pub fn set_perspective(&mut self, perspective: &Perspective) {
+ self.m14 = perspective.0;
+ self.m24 = perspective.1;
+ self.m34 = perspective.2;
+ self.m44 = perspective.3;
+ }
+
+ /// Apply translate on the matrix.
+ #[inline]
+ pub fn apply_translate(&mut self, translate: &Translate3D) {
+ self.m41 += translate.0 * self.m11 + translate.1 * self.m21 + translate.2 * self.m31;
+ self.m42 += translate.0 * self.m12 + translate.1 * self.m22 + translate.2 * self.m32;
+ self.m43 += translate.0 * self.m13 + translate.1 * self.m23 + translate.2 * self.m33;
+ self.m44 += translate.0 * self.m14 + translate.1 * self.m24 + translate.2 * self.m34;
+ }
+
+ /// Apply scale on the matrix.
+ #[inline]
+ pub fn apply_scale(&mut self, scale: &Scale3D) {
+ self.m11 *= scale.0;
+ self.m12 *= scale.0;
+ self.m13 *= scale.0;
+ self.m14 *= scale.0;
+ self.m21 *= scale.1;
+ self.m22 *= scale.1;
+ self.m23 *= scale.1;
+ self.m24 *= scale.1;
+ self.m31 *= scale.2;
+ self.m32 *= scale.2;
+ self.m33 *= scale.2;
+ self.m34 *= scale.2;
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl Matrix {
+ #[inline]
+ /// Get an identity matrix
+ pub fn identity() -> Self {
+ Self {
+ a: 1., c: 0., /* 0 0*/
+ b: 0., d: 1., /* 0 0*/
+ /* 0 0 1 0 */
+ e: 0., f: 0., /* 0 1 */
+ }
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl From<Matrix> for Matrix3D {
+ fn from(m: Matrix) -> Self {
+ Self {
+ m11: m.a, m12: m.b, m13: 0.0, m14: 0.0,
+ m21: m.c, m22: m.d, m23: 0.0, m24: 0.0,
+ m31: 0.0, m32: 0.0, m33: 1.0, m34: 0.0,
+ m41: m.e, m42: m.f, m43: 0.0, m44: 1.0
+ }
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl From<Transform3D<CSSFloat>> for Matrix3D {
+ #[inline]
+ fn from(m: Transform3D<CSSFloat>) -> Self {
+ Matrix3D {
+ m11: m.m11, m12: m.m12, m13: m.m13, m14: m.m14,
+ m21: m.m21, m22: m.m22, m23: m.m23, m24: m.m24,
+ m31: m.m31, m32: m.m32, m33: m.m33, m34: m.m34,
+ m41: m.m41, m42: m.m42, m43: m.m43, m44: m.m44
+ }
+ }
+}
+
+impl TransformOperation {
+ /// Convert to a Translate3D.
+ ///
+ /// Must be called on a Translate function
+ pub fn to_translate_3d(&self) -> Self {
+ match *self {
+ generic::TransformOperation::Translate3D(..) => self.clone(),
+ generic::TransformOperation::TranslateX(ref x) => {
+ generic::TransformOperation::Translate3D(
+ x.clone(),
+ LengthPercentage::zero(),
+ Length::zero(),
+ )
+ },
+ generic::TransformOperation::Translate(ref x, ref y) => {
+ generic::TransformOperation::Translate3D(x.clone(), y.clone(), Length::zero())
+ },
+ generic::TransformOperation::TranslateY(ref y) => {
+ generic::TransformOperation::Translate3D(
+ LengthPercentage::zero(),
+ y.clone(),
+ Length::zero(),
+ )
+ },
+ generic::TransformOperation::TranslateZ(ref z) => {
+ generic::TransformOperation::Translate3D(
+ LengthPercentage::zero(),
+ LengthPercentage::zero(),
+ z.clone(),
+ )
+ },
+ _ => unreachable!(),
+ }
+ }
+
+ /// Convert to a Rotate3D.
+ ///
+ /// Must be called on a Rotate function.
+ pub fn to_rotate_3d(&self) -> Self {
+ match *self {
+ generic::TransformOperation::Rotate3D(..) => self.clone(),
+ generic::TransformOperation::RotateZ(ref angle) |
+ generic::TransformOperation::Rotate(ref angle) => {
+ generic::TransformOperation::Rotate3D(0., 0., 1., angle.clone())
+ },
+ generic::TransformOperation::RotateX(ref angle) => {
+ generic::TransformOperation::Rotate3D(1., 0., 0., angle.clone())
+ },
+ generic::TransformOperation::RotateY(ref angle) => {
+ generic::TransformOperation::Rotate3D(0., 1., 0., angle.clone())
+ },
+ _ => unreachable!(),
+ }
+ }
+
+ /// Convert to a Scale3D.
+ ///
+ /// Must be called on a Scale function
+ pub fn to_scale_3d(&self) -> Self {
+ match *self {
+ generic::TransformOperation::Scale3D(..) => self.clone(),
+ generic::TransformOperation::Scale(x, y) => {
+ generic::TransformOperation::Scale3D(x, y, 1.)
+ },
+ generic::TransformOperation::ScaleX(x) => {
+ generic::TransformOperation::Scale3D(x, 1., 1.)
+ },
+ generic::TransformOperation::ScaleY(y) => {
+ generic::TransformOperation::Scale3D(1., y, 1.)
+ },
+ generic::TransformOperation::ScaleZ(z) => {
+ generic::TransformOperation::Scale3D(1., 1., z)
+ },
+ _ => unreachable!(),
+ }
+ }
+}
+
+/// Build an equivalent 'identity transform function list' based
+/// on an existing transform list.
+/// http://dev.w3.org/csswg/css-transforms/#none-transform-animation
+impl ToAnimatedZero for TransformOperation {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ match *self {
+ generic::TransformOperation::Matrix3D(..) => {
+ Ok(generic::TransformOperation::Matrix3D(Matrix3D::identity()))
+ },
+ generic::TransformOperation::Matrix(..) => {
+ Ok(generic::TransformOperation::Matrix(Matrix::identity()))
+ },
+ generic::TransformOperation::Skew(sx, sy) => Ok(generic::TransformOperation::Skew(
+ sx.to_animated_zero()?,
+ sy.to_animated_zero()?,
+ )),
+ generic::TransformOperation::SkewX(s) => {
+ Ok(generic::TransformOperation::SkewX(s.to_animated_zero()?))
+ },
+ generic::TransformOperation::SkewY(s) => {
+ Ok(generic::TransformOperation::SkewY(s.to_animated_zero()?))
+ },
+ generic::TransformOperation::Translate3D(ref tx, ref ty, ref tz) => {
+ Ok(generic::TransformOperation::Translate3D(
+ tx.to_animated_zero()?,
+ ty.to_animated_zero()?,
+ tz.to_animated_zero()?,
+ ))
+ },
+ generic::TransformOperation::Translate(ref tx, ref ty) => {
+ Ok(generic::TransformOperation::Translate(
+ tx.to_animated_zero()?,
+ ty.to_animated_zero()?,
+ ))
+ },
+ generic::TransformOperation::TranslateX(ref t) => Ok(
+ generic::TransformOperation::TranslateX(t.to_animated_zero()?),
+ ),
+ generic::TransformOperation::TranslateY(ref t) => Ok(
+ generic::TransformOperation::TranslateY(t.to_animated_zero()?),
+ ),
+ generic::TransformOperation::TranslateZ(ref t) => Ok(
+ generic::TransformOperation::TranslateZ(t.to_animated_zero()?),
+ ),
+ generic::TransformOperation::Scale3D(..) => {
+ Ok(generic::TransformOperation::Scale3D(1.0, 1.0, 1.0))
+ },
+ generic::TransformOperation::Scale(_, _) => {
+ Ok(generic::TransformOperation::Scale(1.0, 1.0))
+ },
+ generic::TransformOperation::ScaleX(..) => Ok(generic::TransformOperation::ScaleX(1.0)),
+ generic::TransformOperation::ScaleY(..) => Ok(generic::TransformOperation::ScaleY(1.0)),
+ generic::TransformOperation::ScaleZ(..) => Ok(generic::TransformOperation::ScaleZ(1.0)),
+ generic::TransformOperation::Rotate3D(x, y, z, a) => {
+ let (x, y, z, _) = generic::get_normalized_vector_and_angle(x, y, z, a);
+ Ok(generic::TransformOperation::Rotate3D(
+ x,
+ y,
+ z,
+ Angle::zero(),
+ ))
+ },
+ generic::TransformOperation::RotateX(_) => {
+ Ok(generic::TransformOperation::RotateX(Angle::zero()))
+ },
+ generic::TransformOperation::RotateY(_) => {
+ Ok(generic::TransformOperation::RotateY(Angle::zero()))
+ },
+ generic::TransformOperation::RotateZ(_) => {
+ Ok(generic::TransformOperation::RotateZ(Angle::zero()))
+ },
+ generic::TransformOperation::Rotate(_) => {
+ Ok(generic::TransformOperation::Rotate(Angle::zero()))
+ },
+ generic::TransformOperation::Perspective(_) => Ok(
+ generic::TransformOperation::Perspective(generic::PerspectiveFunction::None),
+ ),
+ generic::TransformOperation::AccumulateMatrix { .. } |
+ generic::TransformOperation::InterpolateMatrix { .. } => {
+ // AccumulateMatrix/InterpolateMatrix: We do interpolation on
+ // AccumulateMatrix/InterpolateMatrix by reading it as a ComputedMatrix
+ // (with layout information), and then do matrix interpolation.
+ //
+ // Therefore, we use an identity matrix to represent the identity transform list.
+ // http://dev.w3.org/csswg/css-transforms/#identity-transform-function
+ Ok(generic::TransformOperation::Matrix3D(Matrix3D::identity()))
+ },
+ }
+ }
+}
+
+impl ToAnimatedZero for Transform {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Ok(generic::Transform(
+ self.0
+ .iter()
+ .map(|op| op.to_animated_zero())
+ .collect::<Result<crate::OwnedSlice<_>, _>>()?,
+ ))
+ }
+}
+
+/// A computed CSS `rotate`
+pub type Rotate = generic::GenericRotate<Number, Angle>;
+
+/// A computed CSS `translate`
+pub type Translate = generic::GenericTranslate<LengthPercentage, Length>;
+
+/// A computed CSS `scale`
+pub type Scale = generic::GenericScale<Number>;
diff --git a/servo/components/style/values/computed/ui.rs b/servo/components/style/values/computed/ui.rs
new file mode 100644
index 0000000000..f285c0626b
--- /dev/null
+++ b/servo/components/style/values/computed/ui.rs
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Computed values for UI properties
+
+use crate::values::computed::color::Color;
+use crate::values::computed::image::Image;
+use crate::values::computed::Number;
+use crate::values::generics::ui as generics;
+
+pub use crate::values::specified::ui::{BoolInteger, CursorKind, MozTheme, UserSelect};
+
+/// A computed value for the `cursor` property.
+pub type Cursor = generics::GenericCursor<CursorImage>;
+
+/// A computed value for item of `image cursors`.
+pub type CursorImage = generics::GenericCursorImage<Image, Number>;
+
+/// A computed value for `scrollbar-color` property.
+pub type ScrollbarColor = generics::GenericScrollbarColor<Color>;
diff --git a/servo/components/style/values/computed/url.rs b/servo/components/style/values/computed/url.rs
new file mode 100644
index 0000000000..9f0d8f5bb3
--- /dev/null
+++ b/servo/components/style/values/computed/url.rs
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Common handling for the computed value CSS url() values.
+
+use crate::values::generics::url::UrlOrNone as GenericUrlOrNone;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::url::{ComputedImageUrl, ComputedUrl};
+#[cfg(feature = "servo")]
+pub use crate::servo::url::{ComputedImageUrl, ComputedUrl};
+
+/// Computed <url> | <none>
+pub type UrlOrNone = GenericUrlOrNone<ComputedUrl>;
diff --git a/servo/components/style/values/distance.rs b/servo/components/style/values/distance.rs
new file mode 100644
index 0000000000..fef376cf5f
--- /dev/null
+++ b/servo/components/style/values/distance.rs
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Machinery to compute distances between animatable values.
+
+use app_units::Au;
+use euclid::default::Size2D;
+use std::iter::Sum;
+use std::ops::Add;
+
+/// A trait to compute squared distances between two animatable values.
+///
+/// This trait is derivable with `#[derive(ComputeSquaredDistance)]`. The derived
+/// implementation uses a `match` expression with identical patterns for both
+/// `self` and `other`, calling `ComputeSquaredDistance::compute_squared_distance`
+/// on each fields of the values.
+///
+/// If a variant is annotated with `#[animation(error)]`, the corresponding
+/// `match` arm returns an error.
+///
+/// Trait bounds for type parameter `Foo` can be opted out of with
+/// `#[animation(no_bound(Foo))]` on the type definition, trait bounds for
+/// fields can be opted into with `#[distance(field_bound)]` on the field.
+pub trait ComputeSquaredDistance {
+ /// Computes the squared distance between two animatable values.
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()>;
+}
+
+/// A distance between two animatable values.
+#[derive(Add, Clone, Copy, Debug, From)]
+pub struct SquaredDistance {
+ value: f64,
+}
+
+impl SquaredDistance {
+ /// Returns a squared distance from its square root.
+ #[inline]
+ pub fn from_sqrt(sqrt: f64) -> Self {
+ Self { value: sqrt * sqrt }
+ }
+}
+
+impl ComputeSquaredDistance for u16 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt(
+ ((*self as f64) - (*other as f64)).abs(),
+ ))
+ }
+}
+
+impl ComputeSquaredDistance for i16 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64))
+ }
+}
+
+impl ComputeSquaredDistance for i32 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64))
+ }
+}
+
+impl ComputeSquaredDistance for f32 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs() as f64))
+ }
+}
+
+impl ComputeSquaredDistance for f64 {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(SquaredDistance::from_sqrt((*self - *other).abs()))
+ }
+}
+
+impl ComputeSquaredDistance for Au {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ self.0.compute_squared_distance(&other.0)
+ }
+}
+
+impl<T> ComputeSquaredDistance for Box<T>
+where
+ T: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ (**self).compute_squared_distance(&**other)
+ }
+}
+
+impl<T> ComputeSquaredDistance for Option<T>
+where
+ T: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ match (self.as_ref(), other.as_ref()) {
+ (Some(this), Some(other)) => this.compute_squared_distance(other),
+ (None, None) => Ok(SquaredDistance::from_sqrt(0.)),
+ _ => Err(()),
+ }
+ }
+}
+
+impl<T> ComputeSquaredDistance for Size2D<T>
+where
+ T: ComputeSquaredDistance,
+{
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ Ok(self.width.compute_squared_distance(&other.width)? +
+ self.height.compute_squared_distance(&other.height)?)
+ }
+}
+
+impl SquaredDistance {
+ /// Returns the square root of this squared distance.
+ #[inline]
+ pub fn sqrt(self) -> f64 {
+ self.value.sqrt()
+ }
+}
+
+impl Sum for SquaredDistance {
+ fn sum<I>(iter: I) -> Self
+ where
+ I: Iterator<Item = Self>,
+ {
+ iter.fold(SquaredDistance::from_sqrt(0.), Add::add)
+ }
+}
diff --git a/servo/components/style/values/generics/animation.rs b/servo/components/style/values/generics/animation.rs
new file mode 100644
index 0000000000..edee9e9f25
--- /dev/null
+++ b/servo/components/style/values/generics/animation.rs
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic values for properties related to animations and transitions.
+
+use crate::values::generics::length::GenericLengthPercentageOrAuto;
+use crate::values::specified::animation::{ScrollAxis, ScrollFunction};
+use crate::values::TimelineName;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// The view() notation.
+/// https://drafts.csswg.org/scroll-animations-1/#view-notation
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "view")]
+#[repr(C)]
+pub struct GenericViewFunction<LengthPercent> {
+ /// The axis of scrolling that drives the progress of the timeline.
+ #[css(skip_if = "ScrollAxis::is_default")]
+ pub axis: ScrollAxis,
+ /// An adjustment of the view progress visibility range.
+ #[css(skip_if = "GenericViewTimelineInset::is_auto")]
+ #[css(field_bound)]
+ pub inset: GenericViewTimelineInset<LengthPercent>,
+}
+
+pub use self::GenericViewFunction as ViewFunction;
+
+/// A value for the <single-animation-timeline>.
+///
+/// https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericAnimationTimeline<LengthPercent> {
+ /// Use default timeline. The animation’s timeline is a DocumentTimeline.
+ Auto,
+ /// The scroll-timeline name or view-timeline-name.
+ /// https://drafts.csswg.org/scroll-animations-1/#scroll-timelines-named
+ /// https://drafts.csswg.org/scroll-animations-1/#view-timeline-name
+ Timeline(TimelineName),
+ /// The scroll() notation.
+ /// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
+ Scroll(ScrollFunction),
+ /// The view() notation.
+ /// https://drafts.csswg.org/scroll-animations-1/#view-notation
+ View(#[css(field_bound)] GenericViewFunction<LengthPercent>),
+}
+
+pub use self::GenericAnimationTimeline as AnimationTimeline;
+
+impl<LengthPercent> AnimationTimeline<LengthPercent> {
+ /// Returns the `auto` value.
+ pub fn auto() -> Self {
+ Self::Auto
+ }
+
+ /// Returns true if it is auto (i.e. the default value).
+ pub fn is_auto(&self) -> bool {
+ matches!(self, Self::Auto)
+ }
+}
+
+/// A generic value for the `[ [ auto | <length-percentage> ]{1,2} ]`.
+///
+/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-inset
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericViewTimelineInset<LengthPercent> {
+ /// The start inset in the relevant axis.
+ pub start: GenericLengthPercentageOrAuto<LengthPercent>,
+ /// The end inset.
+ pub end: GenericLengthPercentageOrAuto<LengthPercent>,
+}
+
+pub use self::GenericViewTimelineInset as ViewTimelineInset;
+
+impl<LengthPercent> ViewTimelineInset<LengthPercent> {
+ /// Returns true if it is auto.
+ #[inline]
+ fn is_auto(&self) -> bool {
+ self.start.is_auto() && self.end.is_auto()
+ }
+}
+
+impl<LengthPercent> ToCss for ViewTimelineInset<LengthPercent>
+where
+ LengthPercent: PartialEq + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.start.to_css(dest)?;
+ if self.end != self.start {
+ dest.write_char(' ')?;
+ self.end.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl<LengthPercent> Default for ViewTimelineInset<LengthPercent> {
+ fn default() -> Self {
+ Self {
+ start: GenericLengthPercentageOrAuto::auto(),
+ end: GenericLengthPercentageOrAuto::auto(),
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/background.rs b/servo/components/style/values/generics/background.rs
new file mode 100644
index 0000000000..d9b6624595
--- /dev/null
+++ b/servo/components/style/values/generics/background.rs
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values related to backgrounds.
+
+use crate::values::generics::length::{GenericLengthPercentageOrAuto, LengthPercentageOrAuto};
+
+/// A generic value for the `background-size` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericBackgroundSize<LengthPercent> {
+ /// `<width> <height>`
+ ExplicitSize {
+ /// Explicit width.
+ width: GenericLengthPercentageOrAuto<LengthPercent>,
+ /// Explicit height.
+ #[css(skip_if = "GenericLengthPercentageOrAuto::is_auto")]
+ height: GenericLengthPercentageOrAuto<LengthPercent>,
+ },
+ /// `cover`
+ #[animation(error)]
+ Cover,
+ /// `contain`
+ #[animation(error)]
+ Contain,
+}
+
+pub use self::GenericBackgroundSize as BackgroundSize;
+
+impl<LengthPercentage> BackgroundSize<LengthPercentage> {
+ /// Returns `auto auto`.
+ pub fn auto() -> Self {
+ GenericBackgroundSize::ExplicitSize {
+ width: LengthPercentageOrAuto::Auto,
+ height: LengthPercentageOrAuto::Auto,
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/basic_shape.rs b/servo/components/style/values/generics/basic_shape.rs
new file mode 100644
index 0000000000..13d27995c1
--- /dev/null
+++ b/servo/components/style/values/generics/basic_shape.rs
@@ -0,0 +1,567 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the [`basic-shape`](https://drafts.csswg.org/css-shapes/#typedef-basic-shape)
+//! types that are generic over their `ToCss` implementations.
+
+use crate::values::animated::{lists, Animate, Procedure, ToAnimatedZero};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::generics::border::GenericBorderRadius;
+use crate::values::generics::position::GenericPositionOrAuto;
+use crate::values::generics::rect::Rect;
+use crate::values::specified::SVGPathData;
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// <https://drafts.fxtf.org/css-masking-1/#typedef-geometry-box>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ShapeGeometryBox {
+ /// Depending on which kind of element this style value applied on, the
+ /// default value of the reference-box can be different. For an HTML
+ /// element, the default value of reference-box is border-box; for an SVG
+ /// element, the default value is fill-box. Since we can not determine the
+ /// default value at parsing time, we keep this value to make a decision on
+ /// it.
+ #[css(skip)]
+ ElementDependent,
+ FillBox,
+ StrokeBox,
+ ViewBox,
+ ShapeBox(ShapeBox),
+}
+
+impl Default for ShapeGeometryBox {
+ fn default() -> Self {
+ Self::ElementDependent
+ }
+}
+
+/// Skip the serialization if the author omits the box or specifies border-box.
+#[inline]
+fn is_default_box_for_clip_path(b: &ShapeGeometryBox) -> bool {
+ // Note: for clip-path, ElementDependent is always border-box, so we have to check both of them
+ // for serialization.
+ matches!(b, ShapeGeometryBox::ElementDependent) ||
+ matches!(b, ShapeGeometryBox::ShapeBox(ShapeBox::BorderBox))
+}
+
+/// https://drafts.csswg.org/css-shapes-1/#typedef-shape-box
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ Copy,
+ ComputeSquaredDistance,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ShapeBox {
+ MarginBox,
+ BorderBox,
+ PaddingBox,
+ ContentBox,
+}
+
+impl Default for ShapeBox {
+ fn default() -> Self {
+ ShapeBox::MarginBox
+ }
+}
+
+/// A value for the `clip-path` property.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(u8)]
+pub enum GenericClipPath<BasicShape, U> {
+ #[animation(error)]
+ None,
+ #[animation(error)]
+ Url(U),
+ Shape(
+ Box<BasicShape>,
+ #[css(skip_if = "is_default_box_for_clip_path")] ShapeGeometryBox,
+ ),
+ #[animation(error)]
+ Box(ShapeGeometryBox),
+}
+
+pub use self::GenericClipPath as ClipPath;
+
+/// A value for the `shape-outside` property.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(I))]
+#[repr(u8)]
+pub enum GenericShapeOutside<BasicShape, I> {
+ #[animation(error)]
+ None,
+ #[animation(error)]
+ Image(I),
+ Shape(Box<BasicShape>, #[css(skip_if = "is_default")] ShapeBox),
+ #[animation(error)]
+ Box(ShapeBox),
+}
+
+pub use self::GenericShapeOutside as ShapeOutside;
+
+/// The <basic-shape>.
+///
+/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericBasicShape<Position, LengthPercentage, NonNegativeLengthPercentage, BasicShapeRect>
+{
+ /// The <basic-shape-rect>.
+ Rect(BasicShapeRect),
+ /// Defines a circle with a center and a radius.
+ Circle(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ Circle<Position, NonNegativeLengthPercentage>,
+ ),
+ /// Defines an ellipse with a center and x-axis/y-axis radii.
+ Ellipse(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ Ellipse<Position, NonNegativeLengthPercentage>,
+ ),
+ /// Defines a polygon with pair arguments.
+ Polygon(GenericPolygon<LengthPercentage>),
+ /// Defines a path with SVG path syntax.
+ Path(Path),
+ // TODO: Bug 1823463. Add shape().
+ // https://drafts.csswg.org/css-shapes-2/#shape-function
+}
+
+pub use self::GenericBasicShape as BasicShape;
+
+/// <https://drafts.csswg.org/css-shapes/#funcdef-inset>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "inset")]
+#[repr(C)]
+pub struct GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage> {
+ pub rect: Rect<LengthPercentage>,
+ #[shmem(field_bound)]
+ pub round: GenericBorderRadius<NonNegativeLengthPercentage>,
+}
+
+pub use self::GenericInsetRect as InsetRect;
+
+/// <https://drafts.csswg.org/css-shapes/#funcdef-circle>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function)]
+#[repr(C)]
+pub struct Circle<Position, NonNegativeLengthPercentage> {
+ pub position: GenericPositionOrAuto<Position>,
+ pub radius: GenericShapeRadius<NonNegativeLengthPercentage>,
+}
+
+/// <https://drafts.csswg.org/css-shapes/#funcdef-ellipse>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function)]
+#[repr(C)]
+pub struct Ellipse<Position, NonNegativeLengthPercentage> {
+ pub position: GenericPositionOrAuto<Position>,
+ pub semiaxis_x: GenericShapeRadius<NonNegativeLengthPercentage>,
+ pub semiaxis_y: GenericShapeRadius<NonNegativeLengthPercentage>,
+}
+
+/// <https://drafts.csswg.org/css-shapes/#typedef-shape-radius>
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericShapeRadius<NonNegativeLengthPercentage> {
+ Length(NonNegativeLengthPercentage),
+ #[animation(error)]
+ ClosestSide,
+ #[animation(error)]
+ FarthestSide,
+}
+
+pub use self::GenericShapeRadius as ShapeRadius;
+
+/// A generic type for representing the `polygon()` function
+///
+/// <https://drafts.csswg.org/css-shapes/#funcdef-polygon>
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "polygon")]
+#[repr(C)]
+pub struct GenericPolygon<LengthPercentage> {
+ /// The filling rule for a polygon.
+ #[css(skip_if = "is_default")]
+ pub fill: FillRule,
+ /// A collection of (x, y) coordinates to draw the polygon.
+ #[css(iterable)]
+ pub coordinates: crate::OwnedSlice<PolygonCoord<LengthPercentage>>,
+}
+
+pub use self::GenericPolygon as Polygon;
+
+/// Coordinates for Polygon.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct PolygonCoord<LengthPercentage>(pub LengthPercentage, pub LengthPercentage);
+
+// https://drafts.csswg.org/css-shapes/#typedef-fill-rule
+// NOTE: Basic shapes spec says that these are the only two values, however
+// https://www.w3.org/TR/SVG/painting.html#FillRuleProperty
+// says that it can also be `inherit`
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum FillRule {
+ Nonzero,
+ Evenodd,
+}
+
+/// The path function defined in css-shape-2.
+///
+/// https://drafts.csswg.org/css-shapes-2/#funcdef-path
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "path")]
+#[repr(C)]
+pub struct Path {
+ /// The filling rule for the svg path.
+ #[css(skip_if = "is_default")]
+ pub fill: FillRule,
+ /// The svg path data.
+ pub path: SVGPathData,
+}
+
+impl<B, U> ToAnimatedZero for ClipPath<B, U> {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+impl<B, U> ToAnimatedZero for ShapeOutside<B, U> {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+impl<Length, NonNegativeLength> ToCss for InsetRect<Length, NonNegativeLength>
+where
+ Length: ToCss + PartialEq,
+ NonNegativeLength: ToCss + PartialEq + Zero,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("inset(")?;
+ self.rect.to_css(dest)?;
+ if !self.round.is_zero() {
+ dest.write_str(" round ")?;
+ self.round.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<Position, NonNegativeLengthPercentage> ToCss for Circle<Position, NonNegativeLengthPercentage>
+where
+ Position: ToCss,
+ NonNegativeLengthPercentage: ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let has_radius = self.radius != Default::default();
+
+ dest.write_str("circle(")?;
+ if has_radius {
+ self.radius.to_css(dest)?;
+ }
+
+ // Preserve the `at <position>` even if it specified the default value.
+ // https://github.com/w3c/csswg-drafts/issues/8695
+ if !matches!(self.position, GenericPositionOrAuto::Auto) {
+ if has_radius {
+ dest.write_char(' ')?;
+ }
+ dest.write_str("at ")?;
+ self.position.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<Position, NonNegativeLengthPercentage> ToCss for Ellipse<Position, NonNegativeLengthPercentage>
+where
+ Position: ToCss,
+ NonNegativeLengthPercentage: ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let has_radii =
+ self.semiaxis_x != Default::default() || self.semiaxis_y != Default::default();
+
+ dest.write_str("ellipse(")?;
+ if has_radii {
+ self.semiaxis_x.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.semiaxis_y.to_css(dest)?;
+ }
+
+ // Preserve the `at <position>` even if it specified the default value.
+ // https://github.com/w3c/csswg-drafts/issues/8695
+ if !matches!(self.position, GenericPositionOrAuto::Auto) {
+ if has_radii {
+ dest.write_char(' ')?;
+ }
+ dest.write_str("at ")?;
+ self.position.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<L> Default for ShapeRadius<L> {
+ #[inline]
+ fn default() -> Self {
+ ShapeRadius::ClosestSide
+ }
+}
+
+impl<L> Animate for Polygon<L>
+where
+ L: Animate,
+{
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.fill != other.fill {
+ return Err(());
+ }
+ let coordinates =
+ lists::by_computed_value::animate(&self.coordinates, &other.coordinates, procedure)?;
+ Ok(Polygon {
+ fill: self.fill,
+ coordinates,
+ })
+ }
+}
+
+impl<L> ComputeSquaredDistance for Polygon<L>
+where
+ L: ComputeSquaredDistance,
+{
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.fill != other.fill {
+ return Err(());
+ }
+ lists::by_computed_value::squared_distance(&self.coordinates, &other.coordinates)
+ }
+}
+
+impl Default for FillRule {
+ #[inline]
+ fn default() -> Self {
+ FillRule::Nonzero
+ }
+}
+
+#[inline]
+fn is_default<T: Default + PartialEq>(fill: &T) -> bool {
+ *fill == Default::default()
+}
diff --git a/servo/components/style/values/generics/border.rs b/servo/components/style/values/generics/border.rs
new file mode 100644
index 0000000000..feb80998d1
--- /dev/null
+++ b/servo/components/style/values/generics/border.rs
@@ -0,0 +1,261 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values related to borders.
+
+use crate::values::generics::rect::Rect;
+use crate::values::generics::size::Size2D;
+use crate::Zero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic value for a single side of a `border-image-width` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericBorderImageSideWidth<LP, N> {
+ /// `<number>`
+ ///
+ /// NOTE: Numbers need to be before length-percentagess, in order to parse
+ /// them first, since `0` should be a number, not the `0px` length.
+ Number(N),
+ /// `<length-or-percentage>`
+ LengthPercentage(LP),
+ /// `auto`
+ Auto,
+}
+
+pub use self::GenericBorderImageSideWidth as BorderImageSideWidth;
+
+/// A generic value for the `border-image-slice` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBorderImageSlice<NumberOrPercentage> {
+ /// The offsets.
+ #[css(field_bound)]
+ pub offsets: Rect<NumberOrPercentage>,
+ /// Whether to fill the middle part.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub fill: bool,
+}
+
+pub use self::GenericBorderImageSlice as BorderImageSlice;
+
+/// A generic value for the `border-*-radius` longhand properties.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBorderCornerRadius<L>(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ pub Size2D<L>,
+);
+
+pub use self::GenericBorderCornerRadius as BorderCornerRadius;
+
+impl<L> BorderCornerRadius<L> {
+ /// Trivially create a `BorderCornerRadius`.
+ pub fn new(w: L, h: L) -> Self {
+ BorderCornerRadius(Size2D::new(w, h))
+ }
+}
+
+impl<L: Zero> Zero for BorderCornerRadius<L> {
+ fn zero() -> Self {
+ BorderCornerRadius(Size2D::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+/// A generic value for the `border-spacing` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct BorderSpacing<L>(
+ #[css(field_bound)]
+ #[shmem(field_bound)]
+ pub Size2D<L>,
+);
+
+impl<L> BorderSpacing<L> {
+ /// Trivially create a `BorderCornerRadius`.
+ pub fn new(w: L, h: L) -> Self {
+ BorderSpacing(Size2D::new(w, h))
+ }
+}
+
+/// A generic value for `border-radius` and `inset()`.
+///
+/// <https://drafts.csswg.org/css-backgrounds-3/#border-radius>
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBorderRadius<LengthPercentage> {
+ /// The top left radius.
+ #[shmem(field_bound)]
+ pub top_left: GenericBorderCornerRadius<LengthPercentage>,
+ /// The top right radius.
+ pub top_right: GenericBorderCornerRadius<LengthPercentage>,
+ /// The bottom right radius.
+ pub bottom_right: GenericBorderCornerRadius<LengthPercentage>,
+ /// The bottom left radius.
+ pub bottom_left: GenericBorderCornerRadius<LengthPercentage>,
+}
+
+pub use self::GenericBorderRadius as BorderRadius;
+
+impl<L> BorderRadius<L> {
+ /// Returns a new `BorderRadius<L>`.
+ #[inline]
+ pub fn new(
+ tl: BorderCornerRadius<L>,
+ tr: BorderCornerRadius<L>,
+ br: BorderCornerRadius<L>,
+ bl: BorderCornerRadius<L>,
+ ) -> Self {
+ BorderRadius {
+ top_left: tl,
+ top_right: tr,
+ bottom_right: br,
+ bottom_left: bl,
+ }
+ }
+
+ /// Serialises two given rects following the syntax of the `border-radius``
+ /// property.
+ pub fn serialize_rects<W>(
+ widths: Rect<&L>,
+ heights: Rect<&L>,
+ dest: &mut CssWriter<W>,
+ ) -> fmt::Result
+ where
+ L: PartialEq + ToCss,
+ W: Write,
+ {
+ widths.to_css(dest)?;
+ if widths != heights {
+ dest.write_str(" / ")?;
+ heights.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl<L: Zero> Zero for BorderRadius<L> {
+ fn zero() -> Self {
+ Self::new(
+ BorderCornerRadius::<L>::zero(),
+ BorderCornerRadius::<L>::zero(),
+ BorderCornerRadius::<L>::zero(),
+ BorderCornerRadius::<L>::zero(),
+ )
+ }
+
+ fn is_zero(&self) -> bool {
+ self.top_left.is_zero() &&
+ self.top_right.is_zero() &&
+ self.bottom_right.is_zero() &&
+ self.bottom_left.is_zero()
+ }
+}
+
+impl<L> ToCss for BorderRadius<L>
+where
+ L: PartialEq + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let BorderRadius {
+ top_left: BorderCornerRadius(ref tl),
+ top_right: BorderCornerRadius(ref tr),
+ bottom_right: BorderCornerRadius(ref br),
+ bottom_left: BorderCornerRadius(ref bl),
+ } = *self;
+
+ let widths = Rect::new(&tl.width, &tr.width, &br.width, &bl.width);
+ let heights = Rect::new(&tl.height, &tr.height, &br.height, &bl.height);
+
+ Self::serialize_rects(widths, heights, dest)
+ }
+}
diff --git a/servo/components/style/values/generics/box.rs b/servo/components/style/values/generics/box.rs
new file mode 100644
index 0000000000..12c5f28bfb
--- /dev/null
+++ b/servo/components/style/values/generics/box.rs
@@ -0,0 +1,211 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for box properties.
+
+use crate::values::animated::ToAnimatedZero;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ FromPrimitive,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum VerticalAlignKeyword {
+ Baseline,
+ Sub,
+ Super,
+ Top,
+ TextTop,
+ Middle,
+ Bottom,
+ TextBottom,
+ #[cfg(feature = "gecko")]
+ MozMiddleWithBaseline,
+}
+
+/// A generic value for the `vertical-align` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericVerticalAlign<LengthPercentage> {
+ /// One of the vertical-align keywords.
+ Keyword(VerticalAlignKeyword),
+ /// `<length-percentage>`
+ Length(LengthPercentage),
+}
+
+pub use self::GenericVerticalAlign as VerticalAlign;
+
+impl<L> VerticalAlign<L> {
+ /// Returns `baseline`.
+ #[inline]
+ pub fn baseline() -> Self {
+ VerticalAlign::Keyword(VerticalAlignKeyword::Baseline)
+ }
+}
+
+impl<L> ToAnimatedZero for VerticalAlign<L> {
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+/// https://drafts.csswg.org/css-sizing-4/#intrinsic-size-override
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "auto")]
+#[repr(C, u8)]
+pub enum GenericContainIntrinsicSize<L> {
+ /// The keyword `none`.
+ None,
+ /// The keywords 'auto none',
+ AutoNone,
+ /// A non-negative length.
+ Length(L),
+ /// "auto <Length>"
+ AutoLength(L),
+}
+
+pub use self::GenericContainIntrinsicSize as ContainIntrinsicSize;
+
+impl<L: ToCss> ToCss for ContainIntrinsicSize<L> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Self::None => dest.write_str("none"),
+ Self::AutoNone => dest.write_str("auto none"),
+ Self::Length(ref l) => l.to_css(dest),
+ Self::AutoLength(ref l) => {
+ dest.write_str("auto ")?;
+ l.to_css(dest)
+ },
+ }
+ }
+}
+
+/// Note that we only implement -webkit-line-clamp as a single, longhand
+/// property for now, but the spec defines line-clamp as a shorthand for
+/// separate max-lines, block-ellipsis, and continue properties.
+///
+/// https://drafts.csswg.org/css-overflow-3/#line-clamp
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+#[value_info(other_values = "none")]
+pub struct GenericLineClamp<I>(pub I);
+
+pub use self::GenericLineClamp as LineClamp;
+
+impl<I: crate::Zero> LineClamp<I> {
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(crate::Zero::zero())
+ }
+
+ /// Returns whether we're the `none` value.
+ pub fn is_none(&self) -> bool {
+ self.0.is_zero()
+ }
+}
+
+impl<I: crate::Zero + ToCss> ToCss for LineClamp<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_none() {
+ return dest.write_str("none");
+ }
+ self.0.to_css(dest)
+ }
+}
+
+/// A generic value for the `perspective` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericPerspective<NonNegativeLength> {
+ /// A non-negative length.
+ Length(NonNegativeLength),
+ /// The keyword `none`.
+ None,
+}
+
+pub use self::GenericPerspective as Perspective;
+
+impl<L> Perspective<L> {
+ /// Returns `none`.
+ #[inline]
+ pub fn none() -> Self {
+ Perspective::None
+ }
+}
diff --git a/servo/components/style/values/generics/calc.rs b/servo/components/style/values/generics/calc.rs
new file mode 100644
index 0000000000..abcb5fe6eb
--- /dev/null
+++ b/servo/components/style/values/generics/calc.rs
@@ -0,0 +1,1820 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! [Calc expressions][calc].
+//!
+//! [calc]: https://drafts.csswg.org/css-values/#calc-notation
+
+use num_traits::Zero;
+use smallvec::SmallVec;
+use std::fmt::{self, Write};
+use std::ops::{Add, Mul, Neg, Rem, Sub};
+use std::{cmp, mem};
+use style_traits::{CssWriter, ToCss};
+
+/// Whether we're a `min` or `max` function.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum MinMaxOp {
+ /// `min()`
+ Min,
+ /// `max()`
+ Max,
+}
+
+/// Whether we're a `mod` or `rem` function.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ModRemOp {
+ /// `mod()`
+ Mod,
+ /// `rem()`
+ Rem,
+}
+
+impl ModRemOp {
+ fn apply(self, dividend: f32, divisor: f32) -> f32 {
+ // In mod(A, B) only, if B is infinite and A has opposite sign to B
+ // (including an oppositely-signed zero), the result is NaN.
+ // https://drafts.csswg.org/css-values/#round-infinities
+ if matches!(self, Self::Mod) &&
+ divisor.is_infinite() &&
+ dividend.is_sign_negative() != divisor.is_sign_negative()
+ {
+ return f32::NAN;
+ }
+
+ let (r, same_sign_as) = match self {
+ Self::Mod => (dividend - divisor * (dividend / divisor).floor(), divisor),
+ Self::Rem => (dividend - divisor * (dividend / divisor).trunc(), dividend),
+ };
+ if r == 0.0 && same_sign_as.is_sign_negative() {
+ -0.0
+ } else {
+ r
+ }
+ }
+}
+
+/// The strategy used in `round()`
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum RoundingStrategy {
+ /// `round(nearest, a, b)`
+ /// round a to the nearest multiple of b
+ Nearest,
+ /// `round(up, a, b)`
+ /// round a up to the nearest multiple of b
+ Up,
+ /// `round(down, a, b)`
+ /// round a down to the nearest multiple of b
+ Down,
+ /// `round(to-zero, a, b)`
+ /// round a to the nearest multiple of b that is towards zero
+ ToZero,
+}
+
+/// This determines the order in which we serialize members of a calc() sum.
+///
+/// See https://drafts.csswg.org/css-values-4/#sort-a-calculations-children
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+#[allow(missing_docs)]
+pub enum SortKey {
+ Number,
+ Percentage,
+ Cap,
+ Ch,
+ Cqb,
+ Cqh,
+ Cqi,
+ Cqmax,
+ Cqmin,
+ Cqw,
+ Deg,
+ Dppx,
+ Dvb,
+ Dvh,
+ Dvi,
+ Dvmax,
+ Dvmin,
+ Dvw,
+ Em,
+ Ex,
+ Ic,
+ Lh,
+ Lvb,
+ Lvh,
+ Lvi,
+ Lvmax,
+ Lvmin,
+ Lvw,
+ Px,
+ Rem,
+ Rlh,
+ Sec,
+ Svb,
+ Svh,
+ Svi,
+ Svmax,
+ Svmin,
+ Svw,
+ Vb,
+ Vh,
+ Vi,
+ Vmax,
+ Vmin,
+ Vw,
+ Other,
+}
+
+/// A generic node in a calc expression.
+///
+/// FIXME: This would be much more elegant if we used `Self` in the types below,
+/// but we can't because of https://github.com/serde-rs/serde/issues/1565.
+///
+/// FIXME: The following annotations are to workaround an LLVM inlining bug, see
+/// bug 1631929.
+///
+/// cbindgen:destructor-attributes=MOZ_NEVER_INLINE
+/// cbindgen:copy-constructor-attributes=MOZ_NEVER_INLINE
+/// cbindgen:eq-attributes=MOZ_NEVER_INLINE
+#[repr(u8)]
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum GenericCalcNode<L> {
+ /// A leaf node.
+ Leaf(L),
+ /// A node that negates its child, e.g. Negate(1) == -1.
+ Negate(Box<GenericCalcNode<L>>),
+ /// A node that inverts its child, e.g. Invert(10) == 1 / 10 == 0.1. The child must always
+ /// resolve to a number unit.
+ Invert(Box<GenericCalcNode<L>>),
+ /// A sum node, representing `a + b + c` where a, b, and c are the
+ /// arguments.
+ Sum(crate::OwnedSlice<GenericCalcNode<L>>),
+ /// A product node, representing `a * b * c` where a, b, and c are the
+ /// arguments.
+ Product(crate::OwnedSlice<GenericCalcNode<L>>),
+ /// A `min` or `max` function.
+ MinMax(crate::OwnedSlice<GenericCalcNode<L>>, MinMaxOp),
+ /// A `clamp()` function.
+ Clamp {
+ /// The minimum value.
+ min: Box<GenericCalcNode<L>>,
+ /// The central value.
+ center: Box<GenericCalcNode<L>>,
+ /// The maximum value.
+ max: Box<GenericCalcNode<L>>,
+ },
+ /// A `round()` function.
+ Round {
+ /// The rounding strategy.
+ strategy: RoundingStrategy,
+ /// The value to round.
+ value: Box<GenericCalcNode<L>>,
+ /// The step value.
+ step: Box<GenericCalcNode<L>>,
+ },
+ /// A `mod()` or `rem()` function.
+ ModRem {
+ /// The dividend calculation.
+ dividend: Box<GenericCalcNode<L>>,
+ /// The divisor calculation.
+ divisor: Box<GenericCalcNode<L>>,
+ /// Is the function mod or rem?
+ op: ModRemOp,
+ },
+ /// A `hypot()` function
+ Hypot(crate::OwnedSlice<GenericCalcNode<L>>),
+ /// An `abs()` function.
+ Abs(Box<GenericCalcNode<L>>),
+ /// A `sign()` function.
+ Sign(Box<GenericCalcNode<L>>),
+}
+
+pub use self::GenericCalcNode as CalcNode;
+
+bitflags! {
+ /// Expected units we allow parsing within a `calc()` expression.
+ ///
+ /// This is used as a hint for the parser to fast-reject invalid
+ /// expressions. Numbers are always allowed because they multiply other
+ /// units.
+ #[derive(Clone, Copy, PartialEq, Eq)]
+ pub struct CalcUnits: u8 {
+ /// <length>
+ const LENGTH = 1 << 0;
+ /// <percentage>
+ const PERCENTAGE = 1 << 1;
+ /// <angle>
+ const ANGLE = 1 << 2;
+ /// <time>
+ const TIME = 1 << 3;
+ /// <resolution>
+ const RESOLUTION = 1 << 4;
+
+ /// <length-percentage>
+ const LENGTH_PERCENTAGE = Self::LENGTH.bits() | Self::PERCENTAGE.bits();
+ // NOTE: When you add to this, make sure to make Atan2 deal with these.
+ /// Allow all units.
+ const ALL = Self::LENGTH.bits() | Self::PERCENTAGE.bits() | Self::ANGLE.bits() | Self::TIME.bits() | Self::RESOLUTION.bits();
+ }
+}
+
+impl CalcUnits {
+ /// Returns whether the flags only represent a single unit. This will return true for 0, which
+ /// is a "number" this is also fine.
+ #[inline]
+ fn is_single_unit(&self) -> bool {
+ self.bits() == 0 || self.bits() & (self.bits() - 1) == 0
+ }
+
+ /// Returns true if this unit is allowed to be summed with the given unit, otherwise false.
+ #[inline]
+ fn can_sum_with(&self, other: Self) -> bool {
+ match *self {
+ Self::LENGTH => other.intersects(Self::LENGTH | Self::PERCENTAGE),
+ Self::PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE),
+ Self::LENGTH_PERCENTAGE => other.intersects(Self::LENGTH | Self::PERCENTAGE),
+ u => u.is_single_unit() && other == u,
+ }
+ }
+}
+
+/// For percentage resolution, sometimes we can't assume that the percentage basis is positive (so
+/// we don't know whether a percentage is larger than another).
+pub enum PositivePercentageBasis {
+ /// The percent basis is not known-positive, we can't compare percentages.
+ Unknown,
+ /// The percent basis is known-positive, we assume larger percentages are larger.
+ Yes,
+}
+
+macro_rules! compare_helpers {
+ () => {
+ /// Return whether a leaf is greater than another.
+ #[allow(unused)]
+ fn gt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool {
+ self.compare(other, basis_positive) == Some(cmp::Ordering::Greater)
+ }
+
+ /// Return whether a leaf is less than another.
+ fn lt(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool {
+ self.compare(other, basis_positive) == Some(cmp::Ordering::Less)
+ }
+
+ /// Return whether a leaf is smaller or equal than another.
+ fn lte(&self, other: &Self, basis_positive: PositivePercentageBasis) -> bool {
+ match self.compare(other, basis_positive) {
+ Some(cmp::Ordering::Less) => true,
+ Some(cmp::Ordering::Equal) => true,
+ Some(cmp::Ordering::Greater) => false,
+ None => false,
+ }
+ }
+ };
+}
+
+/// A trait that represents all the stuff a valid leaf of a calc expression.
+pub trait CalcNodeLeaf: Clone + Sized + PartialEq + ToCss {
+ /// Returns the unit of the leaf.
+ fn unit(&self) -> CalcUnits;
+
+ /// Returns the unitless value of this leaf.
+ fn unitless_value(&self) -> f32;
+
+ /// Return true if the units of both leaves are equal. (NOTE: Does not take
+ /// the values into account)
+ fn is_same_unit_as(&self, other: &Self) -> bool {
+ std::mem::discriminant(self) == std::mem::discriminant(other)
+ }
+
+ /// Do a partial comparison of these values.
+ fn compare(
+ &self,
+ other: &Self,
+ base_is_positive: PositivePercentageBasis,
+ ) -> Option<cmp::Ordering>;
+ compare_helpers!();
+
+ /// Create a new leaf with a number value.
+ fn new_number(value: f32) -> Self;
+
+ /// Returns a float value if the leaf is a number.
+ fn as_number(&self) -> Option<f32>;
+
+ /// Whether this value is known-negative.
+ fn is_negative(&self) -> bool {
+ self.unitless_value().is_sign_negative()
+ }
+
+ /// Whether this value is infinite.
+ fn is_infinite(&self) -> bool {
+ self.unitless_value().is_infinite()
+ }
+
+ /// Whether this value is zero.
+ fn is_zero(&self) -> bool {
+ self.unitless_value().is_zero()
+ }
+
+ /// Whether this value is NaN.
+ fn is_nan(&self) -> bool {
+ self.unitless_value().is_nan()
+ }
+
+ /// Tries to merge one leaf into another using the sum, that is, perform `x` + `y`.
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()>;
+
+ /// Try to merge the right leaf into the left by using a multiplication. Return true if the
+ /// merge was successful, otherwise false.
+ fn try_product_in_place(&mut self, other: &mut Self) -> bool;
+
+ /// Tries a generic arithmetic operation.
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32;
+
+ /// Map the value of this node with the given operation.
+ fn map(&mut self, op: impl FnMut(f32) -> f32);
+
+ /// Negates the leaf.
+ fn negate(&mut self) {
+ self.map(std::ops::Neg::neg);
+ }
+
+ /// Canonicalizes the expression if necessary.
+ fn simplify(&mut self);
+
+ /// Returns the sort key for simplification.
+ fn sort_key(&self) -> SortKey;
+
+ /// Create a new leaf containing the sign() result of the given leaf.
+ fn sign_from(leaf: &impl CalcNodeLeaf) -> Self {
+ Self::new_number(if leaf.is_nan() {
+ f32::NAN
+ } else if leaf.is_zero() {
+ leaf.unitless_value()
+ } else if leaf.is_negative() {
+ -1.0
+ } else {
+ 1.0
+ })
+ }
+}
+
+/// The level of any argument being serialized in `to_css_impl`.
+enum ArgumentLevel {
+ /// The root of a calculation tree.
+ CalculationRoot,
+ /// The root of an operand node's argument, e.g. `min(10, 20)`, `10` and `20` will have this
+ /// level, but min in this case will have `TopMost`.
+ ArgumentRoot,
+ /// Any other values serialized in the tree.
+ Nested,
+}
+
+impl<L: CalcNodeLeaf> CalcNode<L> {
+ /// Create a dummy CalcNode that can be used to do replacements of other nodes.
+ fn dummy() -> Self {
+ Self::MinMax(Default::default(), MinMaxOp::Max)
+ }
+
+ /// Change all the leaf nodes to have the given value. This is useful when
+ /// you have `calc(1px * nan)` and you want to replace the product node with
+ /// `calc(nan)`, in which case the unit will be retained.
+ fn coerce_to_value(&mut self, value: f32) {
+ self.map(|_| value);
+ }
+
+ /// Return true if a product is distributive over this node.
+ /// Is distributive: (2 + 3) * 4 = 8 + 12
+ /// Not distributive: sign(2 + 3) * 4 != sign(8 + 12)
+ #[inline]
+ pub fn is_product_distributive(&self) -> bool {
+ match self {
+ Self::Leaf(_) => true,
+ Self::Sum(children) => children.iter().all(|c| c.is_product_distributive()),
+ _ => false,
+ }
+ }
+
+ /// If the node has a valid unit outcome, then return it, otherwise fail.
+ pub fn unit(&self) -> Result<CalcUnits, ()> {
+ Ok(match self {
+ CalcNode::Leaf(l) => l.unit(),
+ CalcNode::Negate(child) | CalcNode::Invert(child) | CalcNode::Abs(child) => {
+ child.unit()?
+ },
+ CalcNode::Sum(children) => {
+ let mut unit = children.first().unwrap().unit()?;
+ for child in children.iter().skip(1) {
+ let child_unit = child.unit()?;
+ if !child_unit.can_sum_with(unit) {
+ return Err(());
+ }
+ unit |= child_unit;
+ }
+ unit
+ },
+ CalcNode::Product(children) => {
+ // Only one node is allowed to have a unit, the rest must be numbers.
+ let mut unit = None;
+ for child in children.iter() {
+ let child_unit = child.unit()?;
+ if child_unit.is_empty() {
+ // Numbers are always allowed in a product, so continue with the next.
+ continue;
+ }
+
+ if unit.is_some() {
+ // We already have a unit for the node, so another unit node is invalid.
+ return Err(());
+ }
+
+ // We have the unit for the node.
+ unit = Some(child_unit);
+ }
+ // We only keep track of specified units, so if we end up with a None and no failure
+ // so far, then we have a number.
+ unit.unwrap_or(CalcUnits::empty())
+ },
+ CalcNode::MinMax(children, _) | CalcNode::Hypot(children) => {
+ let mut unit = children.first().unwrap().unit()?;
+ for child in children.iter().skip(1) {
+ let child_unit = child.unit()?;
+ if !child_unit.can_sum_with(unit) {
+ return Err(());
+ }
+ unit |= child_unit;
+ }
+ unit
+ },
+ CalcNode::Clamp { min, center, max } => {
+ let min_unit = min.unit()?;
+ let center_unit = center.unit()?;
+
+ if !min_unit.can_sum_with(center_unit) {
+ return Err(());
+ }
+
+ let max_unit = max.unit()?;
+
+ if !center_unit.can_sum_with(max_unit) {
+ return Err(());
+ }
+
+ min_unit | center_unit | max_unit
+ },
+ CalcNode::Round { value, step, .. } => {
+ let value_unit = value.unit()?;
+ let step_unit = step.unit()?;
+ if !step_unit.can_sum_with(value_unit) {
+ return Err(());
+ }
+ value_unit | step_unit
+ },
+ CalcNode::ModRem {
+ dividend, divisor, ..
+ } => {
+ let dividend_unit = dividend.unit()?;
+ let divisor_unit = divisor.unit()?;
+ if !divisor_unit.can_sum_with(dividend_unit) {
+ return Err(());
+ }
+ dividend_unit | divisor_unit
+ },
+ CalcNode::Sign(ref child) => {
+ // sign() always resolves to a number, but we still need to make sure that the
+ // child units make sense.
+ let _ = child.unit()?;
+ CalcUnits::empty()
+ },
+ })
+ }
+
+ /// Negate the node inline. If the node is distributive, it is replaced by the result,
+ /// otherwise the node is wrapped in a [`Negate`] node.
+ pub fn negate(&mut self) {
+ /// Node(params) -> Negate(Node(params))
+ fn wrap_self_in_negate<L: CalcNodeLeaf>(s: &mut CalcNode<L>) {
+ let result = mem::replace(s, CalcNode::dummy());
+ *s = CalcNode::Negate(Box::new(result));
+ }
+
+ match *self {
+ CalcNode::Leaf(ref mut leaf) => leaf.negate(),
+ CalcNode::Negate(ref mut value) => {
+ // Don't negate the value here. Replace `self` with it's child.
+ let result = mem::replace(value.as_mut(), Self::dummy());
+ *self = result;
+ },
+ CalcNode::Invert(_) => {
+ // -(1 / -10) == -(-0.1) == 0.1
+ wrap_self_in_negate(self)
+ },
+ CalcNode::Sum(ref mut children) => {
+ for child in children.iter_mut() {
+ child.negate();
+ }
+ },
+ CalcNode::Product(_) => {
+ // -(2 * 3 / 4) == -(1.5)
+ wrap_self_in_negate(self);
+ },
+ CalcNode::MinMax(ref mut children, ref mut op) => {
+ for child in children.iter_mut() {
+ child.negate();
+ }
+
+ // Negating min-max means the operation is swapped.
+ *op = match *op {
+ MinMaxOp::Min => MinMaxOp::Max,
+ MinMaxOp::Max => MinMaxOp::Min,
+ };
+ },
+ CalcNode::Clamp {
+ ref mut min,
+ ref mut center,
+ ref mut max,
+ } => {
+ if min.lte(max, PositivePercentageBasis::Unknown) {
+ min.negate();
+ center.negate();
+ max.negate();
+
+ mem::swap(min, max);
+ } else {
+ wrap_self_in_negate(self);
+ }
+ },
+ CalcNode::Round {
+ ref mut strategy,
+ ref mut value,
+ ref mut step,
+ } => {
+ match *strategy {
+ RoundingStrategy::Nearest => {
+ // Nearest is tricky because we'd have to swap the
+ // behavior at the half-way point from using the upper
+ // to lower bound.
+ // Simpler to just wrap self in a negate node.
+ wrap_self_in_negate(self);
+ return;
+ },
+ RoundingStrategy::Up => *strategy = RoundingStrategy::Down,
+ RoundingStrategy::Down => *strategy = RoundingStrategy::Up,
+ RoundingStrategy::ToZero => (),
+ }
+ value.negate();
+ step.negate();
+ },
+ CalcNode::ModRem {
+ ref mut dividend,
+ ref mut divisor,
+ ..
+ } => {
+ dividend.negate();
+ divisor.negate();
+ },
+ CalcNode::Hypot(ref mut children) => {
+ for child in children.iter_mut() {
+ child.negate();
+ }
+ },
+ CalcNode::Abs(_) => {
+ wrap_self_in_negate(self);
+ },
+ CalcNode::Sign(ref mut child) => {
+ child.negate();
+ },
+ }
+ }
+
+ fn sort_key(&self) -> SortKey {
+ match *self {
+ Self::Leaf(ref l) => l.sort_key(),
+ _ => SortKey::Other,
+ }
+ }
+
+ /// Returns the leaf if we can (if simplification has allowed it).
+ pub fn as_leaf(&self) -> Option<&L> {
+ match *self {
+ Self::Leaf(ref l) => Some(l),
+ _ => None,
+ }
+ }
+
+ /// Tries to merge one node into another using the sum, that is, perform `x` + `y`.
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
+ match (self, other) {
+ (&mut CalcNode::Leaf(ref mut one), &CalcNode::Leaf(ref other)) => {
+ one.try_sum_in_place(other)
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Tries to merge one node into another using the product, that is, perform `x` * `y`.
+ pub fn try_product_in_place(&mut self, other: &mut Self) -> bool {
+ if let Ok(resolved) = other.resolve() {
+ if let Some(number) = resolved.as_number() {
+ if number == 1.0 {
+ return true;
+ }
+
+ if self.is_product_distributive() {
+ self.map(|v| v * number);
+ return true;
+ }
+ }
+ }
+
+ if let Ok(resolved) = self.resolve() {
+ if let Some(number) = resolved.as_number() {
+ if number == 1.0 {
+ std::mem::swap(self, other);
+ return true;
+ }
+
+ if other.is_product_distributive() {
+ other.map(|v| v * number);
+ std::mem::swap(self, other);
+ return true;
+ }
+ }
+ }
+
+ false
+ }
+
+ /// Tries to apply a generic arithmetic operator
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ match (self, other) {
+ (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => {
+ Ok(CalcNode::Leaf(one.try_op(other, op)?))
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Map the value of this node with the given operation.
+ pub fn map(&mut self, mut op: impl FnMut(f32) -> f32) {
+ fn map_internal<L: CalcNodeLeaf>(node: &mut CalcNode<L>, op: &mut impl FnMut(f32) -> f32) {
+ match node {
+ CalcNode::Leaf(l) => l.map(op),
+ CalcNode::Negate(v) | CalcNode::Invert(v) => map_internal(v, op),
+ CalcNode::Sum(children) | CalcNode::Product(children) => {
+ for node in &mut **children {
+ map_internal(node, op);
+ }
+ },
+ CalcNode::MinMax(children, _) => {
+ for node in &mut **children {
+ map_internal(node, op);
+ }
+ },
+ CalcNode::Clamp { min, center, max } => {
+ map_internal(min, op);
+ map_internal(center, op);
+ map_internal(max, op);
+ },
+ CalcNode::Round { value, step, .. } => {
+ map_internal(value, op);
+ map_internal(step, op);
+ },
+ CalcNode::ModRem {
+ dividend, divisor, ..
+ } => {
+ map_internal(dividend, op);
+ map_internal(divisor, op);
+ },
+ CalcNode::Hypot(children) => {
+ for node in &mut **children {
+ map_internal(node, op);
+ }
+ },
+ CalcNode::Abs(child) | CalcNode::Sign(child) => {
+ map_internal(child, op);
+ },
+ }
+ }
+
+ map_internal(self, &mut op);
+ }
+
+ /// Convert this `CalcNode` into a `CalcNode` with a different leaf kind.
+ pub fn map_leaves<O, F>(&self, mut map: F) -> CalcNode<O>
+ where
+ O: CalcNodeLeaf,
+ F: FnMut(&L) -> O,
+ {
+ self.map_leaves_internal(&mut map)
+ }
+
+ fn map_leaves_internal<O, F>(&self, map: &mut F) -> CalcNode<O>
+ where
+ O: CalcNodeLeaf,
+ F: FnMut(&L) -> O,
+ {
+ fn map_children<L, O, F>(
+ children: &[CalcNode<L>],
+ map: &mut F,
+ ) -> crate::OwnedSlice<CalcNode<O>>
+ where
+ L: CalcNodeLeaf,
+ O: CalcNodeLeaf,
+ F: FnMut(&L) -> O,
+ {
+ children
+ .iter()
+ .map(|c| c.map_leaves_internal(map))
+ .collect()
+ }
+
+ match *self {
+ Self::Leaf(ref l) => CalcNode::Leaf(map(l)),
+ Self::Negate(ref c) => CalcNode::Negate(Box::new(c.map_leaves_internal(map))),
+ Self::Invert(ref c) => CalcNode::Invert(Box::new(c.map_leaves_internal(map))),
+ Self::Sum(ref c) => CalcNode::Sum(map_children(c, map)),
+ Self::Product(ref c) => CalcNode::Product(map_children(c, map)),
+ Self::MinMax(ref c, op) => CalcNode::MinMax(map_children(c, map), op),
+ Self::Clamp {
+ ref min,
+ ref center,
+ ref max,
+ } => {
+ let min = Box::new(min.map_leaves_internal(map));
+ let center = Box::new(center.map_leaves_internal(map));
+ let max = Box::new(max.map_leaves_internal(map));
+ CalcNode::Clamp { min, center, max }
+ },
+ Self::Round {
+ strategy,
+ ref value,
+ ref step,
+ } => {
+ let value = Box::new(value.map_leaves_internal(map));
+ let step = Box::new(step.map_leaves_internal(map));
+ CalcNode::Round {
+ strategy,
+ value,
+ step,
+ }
+ },
+ Self::ModRem {
+ ref dividend,
+ ref divisor,
+ op,
+ } => {
+ let dividend = Box::new(dividend.map_leaves_internal(map));
+ let divisor = Box::new(divisor.map_leaves_internal(map));
+ CalcNode::ModRem {
+ dividend,
+ divisor,
+ op,
+ }
+ },
+ Self::Hypot(ref c) => CalcNode::Hypot(map_children(c, map)),
+ Self::Abs(ref c) => CalcNode::Abs(Box::new(c.map_leaves_internal(map))),
+ Self::Sign(ref c) => CalcNode::Sign(Box::new(c.map_leaves_internal(map))),
+ }
+ }
+
+ /// Resolve this node into a value.
+ pub fn resolve(&self) -> Result<L, ()> {
+ self.resolve_map(|l| Ok(l.clone()))
+ }
+
+ /// Resolve this node into a value, given a function that maps the leaf values.
+ pub fn resolve_map<F>(&self, mut leaf_to_output_fn: F) -> Result<L, ()>
+ where
+ F: FnMut(&L) -> Result<L, ()>,
+ {
+ self.resolve_internal(&mut leaf_to_output_fn)
+ }
+
+ fn resolve_internal<F>(&self, leaf_to_output_fn: &mut F) -> Result<L, ()>
+ where
+ F: FnMut(&L) -> Result<L, ()>,
+ {
+ match self {
+ Self::Leaf(l) => leaf_to_output_fn(l),
+ Self::Negate(child) => {
+ let mut result = child.resolve_internal(leaf_to_output_fn)?;
+ result.map(|v| v.neg());
+ Ok(result)
+ },
+ Self::Invert(child) => {
+ let mut result = child.resolve_internal(leaf_to_output_fn)?;
+ result.map(|v| 1.0 / v);
+ Ok(result)
+ },
+ Self::Sum(children) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+
+ for child in children.iter().skip(1) {
+ let right = child.resolve_internal(leaf_to_output_fn)?;
+ // try_op will make sure we only sum leaves with the same type.
+ result = result.try_op(&right, |left, right| left + right)?;
+ }
+
+ Ok(result)
+ },
+ Self::Product(children) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+
+ for child in children.iter().skip(1) {
+ let right = child.resolve_internal(leaf_to_output_fn)?;
+ // Mutliply only allowed when either side is a number.
+ match result.as_number() {
+ Some(left) => {
+ // Left side is a number, so we use the right node as the result.
+ result = right;
+ result.map(|v| v * left);
+ },
+ None => {
+ // Left side is not a number, so check if the right side is.
+ match right.as_number() {
+ Some(right) => {
+ result.map(|v| v * right);
+ },
+ None => {
+ // Multiplying with both sides having units.
+ return Err(());
+ },
+ }
+ },
+ }
+ }
+
+ Ok(result)
+ },
+ Self::MinMax(children, op) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+
+ if result.is_nan() {
+ return Ok(result);
+ }
+
+ for child in children.iter().skip(1) {
+ let candidate = child.resolve_internal(leaf_to_output_fn)?;
+
+ // Leave types must match for each child.
+ if !result.is_same_unit_as(&candidate) {
+ return Err(());
+ }
+
+ if candidate.is_nan() {
+ result = candidate;
+ break;
+ }
+
+ let candidate_wins = match op {
+ MinMaxOp::Min => candidate.lt(&result, PositivePercentageBasis::Yes),
+ MinMaxOp::Max => candidate.gt(&result, PositivePercentageBasis::Yes),
+ };
+
+ if candidate_wins {
+ result = candidate;
+ }
+ }
+
+ Ok(result)
+ },
+ Self::Clamp { min, center, max } => {
+ let min = min.resolve_internal(leaf_to_output_fn)?;
+ let center = center.resolve_internal(leaf_to_output_fn)?;
+ let max = max.resolve_internal(leaf_to_output_fn)?;
+
+ if !min.is_same_unit_as(&center) || !max.is_same_unit_as(&center) {
+ return Err(());
+ }
+
+ if min.is_nan() {
+ return Ok(min);
+ }
+
+ if center.is_nan() {
+ return Ok(center);
+ }
+
+ if max.is_nan() {
+ return Ok(max);
+ }
+
+ let mut result = center;
+ if result.gt(&max, PositivePercentageBasis::Yes) {
+ result = max;
+ }
+ if result.lt(&min, PositivePercentageBasis::Yes) {
+ result = min
+ }
+
+ Ok(result)
+ },
+ Self::Round {
+ strategy,
+ value,
+ step,
+ } => {
+ let mut value = value.resolve_internal(leaf_to_output_fn)?;
+ let step = step.resolve_internal(leaf_to_output_fn)?;
+
+ if !value.is_same_unit_as(&step) {
+ return Err(());
+ }
+
+ let step = step.unitless_value().abs();
+
+ value.map(|value| {
+ // TODO(emilio): Seems like at least a few of these
+ // special-cases could be removed if we do the math in a
+ // particular order.
+ if step.is_zero() {
+ return f32::NAN;
+ }
+
+ if value.is_infinite() {
+ if step.is_infinite() {
+ return f32::NAN;
+ }
+ return value;
+ }
+
+ if step.is_infinite() {
+ match strategy {
+ RoundingStrategy::Nearest | RoundingStrategy::ToZero => {
+ return if value.is_sign_negative() { -0.0 } else { 0.0 }
+ },
+ RoundingStrategy::Up => {
+ return if !value.is_sign_negative() && !value.is_zero() {
+ f32::INFINITY
+ } else if !value.is_sign_negative() && value.is_zero() {
+ value
+ } else {
+ -0.0
+ }
+ },
+ RoundingStrategy::Down => {
+ return if value.is_sign_negative() && !value.is_zero() {
+ -f32::INFINITY
+ } else if value.is_sign_negative() && value.is_zero() {
+ value
+ } else {
+ 0.0
+ }
+ },
+ }
+ }
+
+ let div = value / step;
+ let lower_bound = div.floor() * step;
+ let upper_bound = div.ceil() * step;
+
+ match strategy {
+ RoundingStrategy::Nearest => {
+ // In case of a tie, use the upper bound
+ if value - lower_bound < upper_bound - value {
+ lower_bound
+ } else {
+ upper_bound
+ }
+ },
+ RoundingStrategy::Up => upper_bound,
+ RoundingStrategy::Down => lower_bound,
+ RoundingStrategy::ToZero => {
+ // In case of a tie, use the upper bound
+ if lower_bound.abs() < upper_bound.abs() {
+ lower_bound
+ } else {
+ upper_bound
+ }
+ },
+ }
+ });
+
+ Ok(value)
+ },
+ Self::ModRem {
+ dividend,
+ divisor,
+ op,
+ } => {
+ let mut dividend = dividend.resolve_internal(leaf_to_output_fn)?;
+ let divisor = divisor.resolve_internal(leaf_to_output_fn)?;
+
+ if !dividend.is_same_unit_as(&divisor) {
+ return Err(());
+ }
+
+ let divisor = divisor.unitless_value();
+ dividend.map(|dividend| op.apply(dividend, divisor));
+ Ok(dividend)
+ },
+ Self::Hypot(children) => {
+ let mut result = children[0].resolve_internal(leaf_to_output_fn)?;
+ result.map(|v| v.powi(2));
+
+ for child in children.iter().skip(1) {
+ let child_value = child.resolve_internal(leaf_to_output_fn)?;
+
+ if !result.is_same_unit_as(&child_value) {
+ return Err(());
+ }
+
+ result.map(|v| v + child_value.unitless_value().powi(2));
+ }
+
+ result.map(|v| v.sqrt());
+ Ok(result)
+ },
+ Self::Abs(ref c) => {
+ let mut result = c.resolve_internal(leaf_to_output_fn)?;
+
+ result.map(|v| v.abs());
+
+ Ok(result)
+ },
+ Self::Sign(ref c) => {
+ let result = c.resolve_internal(leaf_to_output_fn)?;
+ Ok(L::sign_from(&result))
+ },
+ }
+ }
+
+ fn is_negative_leaf(&self) -> bool {
+ match *self {
+ Self::Leaf(ref l) => l.is_negative(),
+ _ => false,
+ }
+ }
+
+ fn is_zero_leaf(&self) -> bool {
+ match *self {
+ Self::Leaf(ref l) => l.is_zero(),
+ _ => false,
+ }
+ }
+
+ fn is_infinite_leaf(&self) -> bool {
+ match *self {
+ Self::Leaf(ref l) => l.is_infinite(),
+ _ => false,
+ }
+ }
+
+ /// Visits all the nodes in this calculation tree recursively, starting by
+ /// the leaves and bubbling all the way up.
+ ///
+ /// This is useful for simplification, but can also be used for validation
+ /// and such.
+ pub fn visit_depth_first(&mut self, mut f: impl FnMut(&mut Self)) {
+ self.visit_depth_first_internal(&mut f)
+ }
+
+ fn visit_depth_first_internal(&mut self, f: &mut impl FnMut(&mut Self)) {
+ match *self {
+ Self::Clamp {
+ ref mut min,
+ ref mut center,
+ ref mut max,
+ } => {
+ min.visit_depth_first_internal(f);
+ center.visit_depth_first_internal(f);
+ max.visit_depth_first_internal(f);
+ },
+ Self::Round {
+ ref mut value,
+ ref mut step,
+ ..
+ } => {
+ value.visit_depth_first_internal(f);
+ step.visit_depth_first_internal(f);
+ },
+ Self::ModRem {
+ ref mut dividend,
+ ref mut divisor,
+ ..
+ } => {
+ dividend.visit_depth_first_internal(f);
+ divisor.visit_depth_first_internal(f);
+ },
+ Self::Sum(ref mut children) |
+ Self::Product(ref mut children) |
+ Self::MinMax(ref mut children, _) |
+ Self::Hypot(ref mut children) => {
+ for child in &mut **children {
+ child.visit_depth_first_internal(f);
+ }
+ },
+ Self::Negate(ref mut value) | Self::Invert(ref mut value) => {
+ value.visit_depth_first_internal(f);
+ },
+ Self::Abs(ref mut value) | Self::Sign(ref mut value) => {
+ value.visit_depth_first_internal(f);
+ },
+ Self::Leaf(..) => {},
+ }
+ f(self);
+ }
+
+ /// This function simplifies and sorts the calculation of the specified node. It simplifies
+ /// directly nested nodes while assuming that all nodes below it have already been simplified.
+ /// It is recommended to use this function in combination with `visit_depth_first()`.
+ ///
+ /// This function is necessary only if the node needs to be preserved after parsing,
+ /// specifically for `<length-percentage>` cases where the calculation contains percentages or
+ /// relative units. Otherwise, the node can be evaluated using `resolve()`, which will
+ /// automatically provide a simplified value.
+ ///
+ /// <https://drafts.csswg.org/css-values-4/#calc-simplification>
+ pub fn simplify_and_sort_direct_children(&mut self) {
+ macro_rules! replace_self_with {
+ ($slot:expr) => {{
+ let result = mem::replace($slot, Self::dummy());
+ *self = result;
+ }};
+ }
+
+ macro_rules! value_or_stop {
+ ($op:expr) => {{
+ match $op {
+ Ok(value) => value,
+ Err(_) => return,
+ }
+ }};
+ }
+
+ match *self {
+ Self::Clamp {
+ ref mut min,
+ ref mut center,
+ ref mut max,
+ } => {
+ // NOTE: clamp() is max(min, min(center, max))
+ let min_cmp_center = match min.compare(&center, PositivePercentageBasis::Unknown) {
+ Some(o) => o,
+ None => return,
+ };
+
+ // So if we can prove that min is more than center, then we won,
+ // as that's what we should always return.
+ if matches!(min_cmp_center, cmp::Ordering::Greater) {
+ replace_self_with!(&mut **min);
+ return;
+ }
+
+ // Otherwise try with max.
+ let max_cmp_center = match max.compare(&center, PositivePercentageBasis::Unknown) {
+ Some(o) => o,
+ None => return,
+ };
+
+ if matches!(max_cmp_center, cmp::Ordering::Less) {
+ // max is less than center, so we need to return effectively
+ // `max(min, max)`.
+ let max_cmp_min = match max.compare(&min, PositivePercentageBasis::Unknown) {
+ Some(o) => o,
+ None => {
+ debug_assert!(
+ false,
+ "We compared center with min and max, how are \
+ min / max not comparable with each other?"
+ );
+ return;
+ },
+ };
+
+ if matches!(max_cmp_min, cmp::Ordering::Less) {
+ replace_self_with!(&mut **min);
+ return;
+ }
+
+ replace_self_with!(&mut **max);
+ return;
+ }
+
+ // Otherwise we're the center node.
+ replace_self_with!(&mut **center);
+ },
+ Self::Round {
+ strategy,
+ ref mut value,
+ ref mut step,
+ } => {
+ if step.is_zero_leaf() {
+ value.coerce_to_value(f32::NAN);
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ if value.is_infinite_leaf() && step.is_infinite_leaf() {
+ value.coerce_to_value(f32::NAN);
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ if value.is_infinite_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ if step.is_infinite_leaf() {
+ match strategy {
+ RoundingStrategy::Nearest | RoundingStrategy::ToZero => {
+ value.coerce_to_value(0.0);
+ replace_self_with!(&mut **value);
+ return;
+ },
+ RoundingStrategy::Up => {
+ if !value.is_negative_leaf() && !value.is_zero_leaf() {
+ value.coerce_to_value(f32::INFINITY);
+ replace_self_with!(&mut **value);
+ return;
+ } else if !value.is_negative_leaf() && value.is_zero_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ } else {
+ value.coerce_to_value(0.0);
+ replace_self_with!(&mut **value);
+ return;
+ }
+ },
+ RoundingStrategy::Down => {
+ if value.is_negative_leaf() && !value.is_zero_leaf() {
+ value.coerce_to_value(f32::INFINITY);
+ replace_self_with!(&mut **value);
+ return;
+ } else if value.is_negative_leaf() && value.is_zero_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ } else {
+ value.coerce_to_value(0.0);
+ replace_self_with!(&mut **value);
+ return;
+ }
+ },
+ }
+ }
+
+ if step.is_negative_leaf() {
+ step.negate();
+ }
+
+ let remainder = value_or_stop!(value.try_op(step, Rem::rem));
+ if remainder.is_zero_leaf() {
+ replace_self_with!(&mut **value);
+ return;
+ }
+
+ let (mut lower_bound, mut upper_bound) = if value.is_negative_leaf() {
+ let upper_bound = value_or_stop!(value.try_op(&remainder, Sub::sub));
+ let lower_bound = value_or_stop!(upper_bound.try_op(&step, Sub::sub));
+
+ (lower_bound, upper_bound)
+ } else {
+ let lower_bound = value_or_stop!(value.try_op(&remainder, Sub::sub));
+ let upper_bound = value_or_stop!(lower_bound.try_op(&step, Add::add));
+
+ (lower_bound, upper_bound)
+ };
+
+ match strategy {
+ RoundingStrategy::Nearest => {
+ let lower_diff = value_or_stop!(value.try_op(&lower_bound, Sub::sub));
+ let upper_diff = value_or_stop!(upper_bound.try_op(value, Sub::sub));
+ // In case of a tie, use the upper bound
+ if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) {
+ replace_self_with!(&mut lower_bound);
+ } else {
+ replace_self_with!(&mut upper_bound);
+ }
+ },
+ RoundingStrategy::Up => {
+ replace_self_with!(&mut upper_bound);
+ },
+ RoundingStrategy::Down => {
+ replace_self_with!(&mut lower_bound);
+ },
+ RoundingStrategy::ToZero => {
+ let mut lower_diff = lower_bound.clone();
+ let mut upper_diff = upper_bound.clone();
+
+ if lower_diff.is_negative_leaf() {
+ lower_diff.negate();
+ }
+
+ if upper_diff.is_negative_leaf() {
+ upper_diff.negate();
+ }
+
+ // In case of a tie, use the upper bound
+ if lower_diff.lt(&upper_diff, PositivePercentageBasis::Unknown) {
+ replace_self_with!(&mut lower_bound);
+ } else {
+ replace_self_with!(&mut upper_bound);
+ }
+ },
+ };
+ },
+ Self::ModRem {
+ ref dividend,
+ ref divisor,
+ op,
+ } => {
+ let mut result = value_or_stop!(dividend.try_op(divisor, |a, b| op.apply(a, b)));
+ replace_self_with!(&mut result);
+ },
+ Self::MinMax(ref mut children, op) => {
+ let winning_order = match op {
+ MinMaxOp::Min => cmp::Ordering::Less,
+ MinMaxOp::Max => cmp::Ordering::Greater,
+ };
+
+ let mut result = 0;
+ for i in 1..children.len() {
+ let o = match children[i]
+ .compare(&children[result], PositivePercentageBasis::Unknown)
+ {
+ // We can't compare all the children, so we can't
+ // know which one will actually win. Bail out and
+ // keep ourselves as a min / max function.
+ //
+ // TODO: Maybe we could simplify compatible children,
+ // see https://github.com/w3c/csswg-drafts/issues/4756
+ None => return,
+ Some(o) => o,
+ };
+
+ if o == winning_order {
+ result = i;
+ }
+ }
+
+ replace_self_with!(&mut children[result]);
+ },
+ Self::Sum(ref mut children_slot) => {
+ let mut sums_to_merge = SmallVec::<[_; 3]>::new();
+ let mut extra_kids = 0;
+ for (i, child) in children_slot.iter().enumerate() {
+ if let Self::Sum(ref children) = *child {
+ extra_kids += children.len();
+ sums_to_merge.push(i);
+ }
+ }
+
+ // If we only have one kid, we've already simplified it, and it
+ // doesn't really matter whether it's a sum already or not, so
+ // lift it up and continue.
+ if children_slot.len() == 1 {
+ replace_self_with!(&mut children_slot[0]);
+ return;
+ }
+
+ let mut children = mem::take(children_slot).into_vec();
+
+ if !sums_to_merge.is_empty() {
+ children.reserve(extra_kids - sums_to_merge.len());
+ // Merge all our nested sums, in reverse order so that the
+ // list indices are not invalidated.
+ for i in sums_to_merge.drain(..).rev() {
+ let kid_children = match children.swap_remove(i) {
+ Self::Sum(c) => c,
+ _ => unreachable!(),
+ };
+
+ // This would be nicer with
+ // https://github.com/rust-lang/rust/issues/59878 fixed.
+ children.extend(kid_children.into_vec());
+ }
+ }
+
+ debug_assert!(children.len() >= 2, "Should still have multiple kids!");
+
+ // Sort by spec order.
+ children.sort_unstable_by_key(|c| c.sort_key());
+
+ // NOTE: if the function returns true, by the docs of dedup_by,
+ // a is removed.
+ children.dedup_by(|a, b| b.try_sum_in_place(a).is_ok());
+
+ if children.len() == 1 {
+ // If only one children remains, lift it up, and carry on.
+ replace_self_with!(&mut children[0]);
+ } else {
+ // Else put our simplified children back.
+ *children_slot = children.into_boxed_slice().into();
+ }
+ },
+ Self::Product(ref mut children_slot) => {
+ let mut products_to_merge = SmallVec::<[_; 3]>::new();
+ let mut extra_kids = 0;
+ for (i, child) in children_slot.iter().enumerate() {
+ if let Self::Product(ref children) = *child {
+ extra_kids += children.len();
+ products_to_merge.push(i);
+ }
+ }
+
+ // If we only have one kid, we've already simplified it, and it
+ // doesn't really matter whether it's a product already or not,
+ // so lift it up and continue.
+ if children_slot.len() == 1 {
+ replace_self_with!(&mut children_slot[0]);
+ return;
+ }
+
+ let mut children = mem::take(children_slot).into_vec();
+
+ if !products_to_merge.is_empty() {
+ children.reserve(extra_kids - products_to_merge.len());
+ // Merge all our nested sums, in reverse order so that the
+ // list indices are not invalidated.
+ for i in products_to_merge.drain(..).rev() {
+ let kid_children = match children.swap_remove(i) {
+ Self::Product(c) => c,
+ _ => unreachable!(),
+ };
+
+ // This would be nicer with
+ // https://github.com/rust-lang/rust/issues/59878 fixed.
+ children.extend(kid_children.into_vec());
+ }
+ }
+
+ debug_assert!(children.len() >= 2, "Should still have multiple kids!");
+
+ // NOTE: if the function returns true, by the docs of dedup_by,
+ // a is removed.
+ children.dedup_by(|right, left| left.try_product_in_place(right));
+
+ if children.len() == 1 {
+ // If only one children remains, lift it up, and carry on.
+ replace_self_with!(&mut children[0]);
+ } else {
+ // Else put our simplified children back.
+ *children_slot = children.into_boxed_slice().into();
+ }
+ },
+ Self::Hypot(ref children) => {
+ let mut result = value_or_stop!(children[0].try_op(&children[0], Mul::mul));
+
+ for child in children.iter().skip(1) {
+ let square = value_or_stop!(child.try_op(&child, Mul::mul));
+ result = value_or_stop!(result.try_op(&square, Add::add));
+ }
+
+ result = value_or_stop!(result.try_op(&result, |a, _| a.sqrt()));
+
+ replace_self_with!(&mut result);
+ },
+ Self::Abs(ref mut child) => {
+ if let CalcNode::Leaf(leaf) = child.as_mut() {
+ leaf.map(|v| v.abs());
+ replace_self_with!(&mut **child);
+ }
+ },
+ Self::Sign(ref mut child) => {
+ if let CalcNode::Leaf(leaf) = child.as_mut() {
+ let mut result = Self::Leaf(L::sign_from(leaf));
+ replace_self_with!(&mut result);
+ }
+ },
+ Self::Negate(ref mut child) => {
+ // Step 6.
+ match &mut **child {
+ CalcNode::Leaf(_) => {
+ // 1. If root’s child is a numeric value, return an equivalent numeric value, but
+ // with the value negated (0 - value).
+ child.negate();
+ replace_self_with!(&mut **child);
+ },
+ CalcNode::Negate(value) => {
+ // 2. If root’s child is a Negate node, return the child’s child.
+ replace_self_with!(&mut **value);
+ },
+ _ => {
+ // 3. Return root.
+ },
+ }
+ },
+ Self::Invert(ref mut child) => {
+ // Step 7.
+ match &mut **child {
+ CalcNode::Leaf(leaf) => {
+ // 1. If root’s child is a number (not a percentage or dimension) return the
+ // reciprocal of the child’s value.
+ if leaf.unit().is_empty() {
+ child.map(|v| 1.0 / v);
+ replace_self_with!(&mut **child);
+ }
+ },
+ CalcNode::Invert(value) => {
+ // 2. If root’s child is an Invert node, return the child’s child.
+ replace_self_with!(&mut **value);
+ },
+ _ => {
+ // 3. Return root.
+ },
+ }
+ },
+ Self::Leaf(ref mut l) => {
+ l.simplify();
+ },
+ }
+ }
+
+ /// Simplifies and sorts the kids in the whole calculation subtree.
+ pub fn simplify_and_sort(&mut self) {
+ self.visit_depth_first(|node| node.simplify_and_sort_direct_children())
+ }
+
+ fn to_css_impl<W>(&self, dest: &mut CssWriter<W>, level: ArgumentLevel) -> fmt::Result
+ where
+ W: Write,
+ {
+ let write_closing_paren = match *self {
+ Self::MinMax(_, op) => {
+ dest.write_str(match op {
+ MinMaxOp::Max => "max(",
+ MinMaxOp::Min => "min(",
+ })?;
+ true
+ },
+ Self::Clamp { .. } => {
+ dest.write_str("clamp(")?;
+ true
+ },
+ Self::Round { strategy, .. } => {
+ match strategy {
+ RoundingStrategy::Nearest => dest.write_str("round("),
+ RoundingStrategy::Up => dest.write_str("round(up, "),
+ RoundingStrategy::Down => dest.write_str("round(down, "),
+ RoundingStrategy::ToZero => dest.write_str("round(to-zero, "),
+ }?;
+
+ true
+ },
+ Self::ModRem { op, .. } => {
+ dest.write_str(match op {
+ ModRemOp::Mod => "mod(",
+ ModRemOp::Rem => "rem(",
+ })?;
+
+ true
+ },
+ Self::Hypot(_) => {
+ dest.write_str("hypot(")?;
+ true
+ },
+ Self::Abs(_) => {
+ dest.write_str("abs(")?;
+ true
+ },
+ Self::Sign(_) => {
+ dest.write_str("sign(")?;
+ true
+ },
+ Self::Negate(_) => {
+ // We never generate a [`Negate`] node as the root of a calculation, only inside
+ // [`Sum`] nodes as a child. Because negate nodes are handled by the [`Sum`] node
+ // directly (see below), this node will never be serialized.
+ debug_assert!(
+ false,
+ "We never serialize Negate nodes as they are handled inside Sum nodes."
+ );
+ dest.write_str("(-1 * ")?;
+ true
+ },
+ Self::Invert(_) => {
+ dest.write_str("(1 / ")?;
+ true
+ },
+ Self::Sum(_) | Self::Product(_) => match level {
+ ArgumentLevel::CalculationRoot => {
+ dest.write_str("calc(")?;
+ true
+ },
+ ArgumentLevel::ArgumentRoot => false,
+ ArgumentLevel::Nested => {
+ dest.write_str("(")?;
+ true
+ },
+ },
+ Self::Leaf(_) => match level {
+ ArgumentLevel::CalculationRoot => {
+ dest.write_str("calc(")?;
+ true
+ },
+ ArgumentLevel::ArgumentRoot | ArgumentLevel::Nested => false,
+ },
+ };
+
+ match *self {
+ Self::MinMax(ref children, _) | Self::Hypot(ref children) => {
+ let mut first = true;
+ for child in &**children {
+ if !first {
+ dest.write_str(", ")?;
+ }
+ first = false;
+ child.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ }
+ },
+ Self::Negate(ref value) | Self::Invert(ref value) => {
+ value.to_css_impl(dest, ArgumentLevel::Nested)?
+ },
+ Self::Sum(ref children) => {
+ let mut first = true;
+ for child in &**children {
+ if !first {
+ match child {
+ Self::Leaf(l) => {
+ if l.is_negative() {
+ dest.write_str(" - ")?;
+ let mut negated = l.clone();
+ negated.negate();
+ negated.to_css(dest)?;
+ } else {
+ dest.write_str(" + ")?;
+ l.to_css(dest)?;
+ }
+ },
+ Self::Negate(n) => {
+ dest.write_str(" - ")?;
+ n.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ _ => {
+ dest.write_str(" + ")?;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ }
+ } else {
+ first = false;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ }
+ }
+ },
+ Self::Product(ref children) => {
+ let mut first = true;
+ for child in &**children {
+ if !first {
+ match child {
+ Self::Invert(n) => {
+ dest.write_str(" / ")?;
+ n.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ _ => {
+ dest.write_str(" * ")?;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ },
+ }
+ } else {
+ first = false;
+ child.to_css_impl(dest, ArgumentLevel::Nested)?;
+ }
+ }
+ },
+ Self::Clamp {
+ ref min,
+ ref center,
+ ref max,
+ } => {
+ min.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ center.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ max.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ },
+ Self::Round {
+ ref value,
+ ref step,
+ ..
+ } => {
+ value.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ step.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ },
+ Self::ModRem {
+ ref dividend,
+ ref divisor,
+ ..
+ } => {
+ dividend.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ dest.write_str(", ")?;
+ divisor.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?;
+ },
+ Self::Abs(ref v) | Self::Sign(ref v) => {
+ v.to_css_impl(dest, ArgumentLevel::ArgumentRoot)?
+ },
+ Self::Leaf(ref l) => l.to_css(dest)?,
+ }
+
+ if write_closing_paren {
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+
+ fn compare(
+ &self,
+ other: &Self,
+ basis_positive: PositivePercentageBasis,
+ ) -> Option<cmp::Ordering> {
+ match (self, other) {
+ (&CalcNode::Leaf(ref one), &CalcNode::Leaf(ref other)) => {
+ one.compare(other, basis_positive)
+ },
+ _ => None,
+ }
+ }
+
+ compare_helpers!();
+}
+
+impl<L: CalcNodeLeaf> ToCss for CalcNode<L> {
+ /// <https://drafts.csswg.org/css-values/#calc-serialize>
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.to_css_impl(dest, ArgumentLevel::CalculationRoot)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn can_sum_with_checks() {
+ assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH));
+ assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::PERCENTAGE));
+ assert!(CalcUnits::LENGTH.can_sum_with(CalcUnits::LENGTH_PERCENTAGE));
+
+ assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH));
+ assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE));
+ assert!(CalcUnits::PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE));
+
+ assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH));
+ assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::PERCENTAGE));
+ assert!(CalcUnits::LENGTH_PERCENTAGE.can_sum_with(CalcUnits::LENGTH_PERCENTAGE));
+
+ assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::TIME));
+ assert!(CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE));
+
+ assert!(!(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE));
+ assert!(!CalcUnits::ANGLE.can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME));
+ assert!(
+ !(CalcUnits::ANGLE | CalcUnits::TIME).can_sum_with(CalcUnits::ANGLE | CalcUnits::TIME)
+ );
+ }
+}
diff --git a/servo/components/style/values/generics/color.rs b/servo/components/style/values/generics/color.rs
new file mode 100644
index 0000000000..e37cabdc59
--- /dev/null
+++ b/servo/components/style/values/generics/color.rs
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for color properties.
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::color::AbsoluteColor;
+use crate::values::specified::percentage::ToPercentage;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// This struct represents a combined color from a numeric color and
+/// the current foreground color (currentcolor keyword).
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
+#[repr(C)]
+pub enum GenericColor<Percentage> {
+ /// The actual numeric color.
+ Absolute(AbsoluteColor),
+ /// The `CurrentColor` keyword.
+ CurrentColor,
+ /// The color-mix() function.
+ ColorMix(Box<GenericColorMix<Self, Percentage>>),
+}
+
+/// Flags used to modify the calculation of a color mix result.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
+#[repr(C)]
+pub struct ColorMixFlags(u8);
+bitflags! {
+ impl ColorMixFlags : u8 {
+ /// Normalize the weights of the mix.
+ const NORMALIZE_WEIGHTS = 1 << 0;
+ /// The result should always be converted to the modern color syntax.
+ const RESULT_IN_MODERN_SYNTAX = 1 << 1;
+ }
+}
+
+/// A restricted version of the css `color-mix()` function, which only supports
+/// percentages.
+///
+/// https://drafts.csswg.org/css-color-5/#color-mix
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(C)]
+pub struct GenericColorMix<Color, Percentage> {
+ pub interpolation: ColorInterpolationMethod,
+ pub left: Color,
+ pub left_percentage: Percentage,
+ pub right: Color,
+ pub right_percentage: Percentage,
+ pub flags: ColorMixFlags,
+}
+
+pub use self::GenericColorMix as ColorMix;
+
+impl<Color: ToCss, Percentage: ToCss + ToPercentage> ToCss for ColorMix<Color, Percentage> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ fn can_omit<Percentage: ToPercentage>(
+ percent: &Percentage,
+ other: &Percentage,
+ is_left: bool,
+ ) -> bool {
+ if percent.is_calc() {
+ return false;
+ }
+ if percent.to_percentage() == 0.5 {
+ return other.to_percentage() == 0.5;
+ }
+ if is_left {
+ return false;
+ }
+ (1.0 - percent.to_percentage() - other.to_percentage()).abs() <= f32::EPSILON
+ }
+
+ dest.write_str("color-mix(")?;
+ self.interpolation.to_css(dest)?;
+ dest.write_str(", ")?;
+ self.left.to_css(dest)?;
+ if !can_omit(&self.left_percentage, &self.right_percentage, true) {
+ dest.write_char(' ')?;
+ self.left_percentage.to_css(dest)?;
+ }
+ dest.write_str(", ")?;
+ self.right.to_css(dest)?;
+ if !can_omit(&self.right_percentage, &self.left_percentage, false) {
+ dest.write_char(' ')?;
+ self.right_percentage.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<Percentage> ColorMix<GenericColor<Percentage>, Percentage> {
+ /// Mix the colors so that we get a single color. If any of the 2 colors are
+ /// not mixable (perhaps not absolute?), then return None.
+ pub fn mix_to_absolute(&self) -> Option<AbsoluteColor>
+ where
+ Percentage: ToPercentage,
+ {
+ let left = self.left.as_absolute()?;
+ let right = self.right.as_absolute()?;
+
+ Some(crate::color::mix::mix(
+ self.interpolation,
+ &left,
+ self.left_percentage.to_percentage(),
+ &right,
+ self.right_percentage.to_percentage(),
+ self.flags,
+ ))
+ }
+}
+
+pub use self::GenericColor as Color;
+
+impl<Percentage> Color<Percentage> {
+ /// If this color is absolute return it's value, otherwise return None.
+ pub fn as_absolute(&self) -> Option<&AbsoluteColor> {
+ match *self {
+ Self::Absolute(ref absolute) => Some(absolute),
+ _ => None,
+ }
+ }
+
+ /// Returns a color value representing currentcolor.
+ pub fn currentcolor() -> Self {
+ Self::CurrentColor
+ }
+
+ /// Whether it is a currentcolor value (no numeric color component).
+ pub fn is_currentcolor(&self) -> bool {
+ matches!(*self, Self::CurrentColor)
+ }
+
+ /// Whether this color is an absolute color.
+ pub fn is_absolute(&self) -> bool {
+ matches!(*self, Self::Absolute(..))
+ }
+}
+
+/// Either `<color>` or `auto`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToCss,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericColorOrAuto<C> {
+ /// A `<color>`.
+ Color(C),
+ /// `auto`
+ Auto,
+}
+
+pub use self::GenericColorOrAuto as ColorOrAuto;
+
+/// Caret color is effectively a ColorOrAuto, but resolves `auto` to
+/// currentColor.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCaretColor<C>(pub GenericColorOrAuto<C>);
+
+impl<C> GenericCaretColor<C> {
+ /// Returns the `auto` value.
+ pub fn auto() -> Self {
+ GenericCaretColor(GenericColorOrAuto::Auto)
+ }
+}
+
+pub use self::GenericCaretColor as CaretColor;
diff --git a/servo/components/style/values/generics/column.rs b/servo/components/style/values/generics/column.rs
new file mode 100644
index 0000000000..4b5f0e0399
--- /dev/null
+++ b/servo/components/style/values/generics/column.rs
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for the column properties.
+
+/// A generic type for `column-count` values.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum ColumnCount<PositiveInteger> {
+ /// A positive integer.
+ Integer(PositiveInteger),
+ /// The keyword `auto`.
+ #[animation(error)]
+ Auto,
+}
+
+impl<I> ColumnCount<I> {
+ /// Returns `auto`.
+ #[inline]
+ pub fn auto() -> Self {
+ ColumnCount::Auto
+ }
+
+ /// Returns whether this value is `auto`.
+ #[inline]
+ pub fn is_auto(self) -> bool {
+ matches!(self, ColumnCount::Auto)
+ }
+}
diff --git a/servo/components/style/values/generics/counters.rs b/servo/components/style/values/generics/counters.rs
new file mode 100644
index 0000000000..1d4518c57b
--- /dev/null
+++ b/servo/components/style/values/generics/counters.rs
@@ -0,0 +1,295 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for counters-related CSS values.
+
+#[cfg(feature = "servo-layout-2013")]
+use crate::computed_values::list_style_type::T as ListStyleType;
+#[cfg(feature = "gecko")]
+use crate::values::generics::CounterStyle;
+#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+use crate::values::specified::Attr;
+use crate::values::CustomIdent;
+use std::fmt::{self, Write};
+use std::ops::Deref;
+use style_traits::{CssWriter, ToCss};
+
+/// A name / value pair for counters.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericCounterPair<Integer> {
+ /// The name of the counter.
+ pub name: CustomIdent,
+ /// The value of the counter / increment / etc.
+ pub value: Integer,
+ /// If true, then this represents `reversed(name)`.
+ /// NOTE: It can only be true on `counter-reset` values.
+ pub is_reversed: bool,
+}
+pub use self::GenericCounterPair as CounterPair;
+
+impl<Integer> ToCss for CounterPair<Integer>
+where
+ Integer: ToCss + PartialEq<i32>,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_reversed {
+ dest.write_str("reversed(")?;
+ }
+ self.name.to_css(dest)?;
+ if self.is_reversed {
+ dest.write_char(')')?;
+ if self.value == i32::min_value() {
+ return Ok(());
+ }
+ }
+ dest.write_char(' ')?;
+ self.value.to_css(dest)
+ }
+}
+
+/// A generic value for the `counter-increment` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounterIncrement<I>(#[css(field_bound)] pub GenericCounters<I>);
+pub use self::GenericCounterIncrement as CounterIncrement;
+
+impl<I> CounterIncrement<I> {
+ /// Returns a new value for `counter-increment`.
+ #[inline]
+ pub fn new(counters: Vec<CounterPair<I>>) -> Self {
+ CounterIncrement(Counters(counters.into()))
+ }
+}
+
+impl<I> Deref for CounterIncrement<I> {
+ type Target = [CounterPair<I>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &(self.0).0
+ }
+}
+
+/// A generic value for the `counter-set` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounterSet<I>(#[css(field_bound)] pub GenericCounters<I>);
+pub use self::GenericCounterSet as CounterSet;
+
+impl<I> CounterSet<I> {
+ /// Returns a new value for `counter-set`.
+ #[inline]
+ pub fn new(counters: Vec<CounterPair<I>>) -> Self {
+ CounterSet(Counters(counters.into()))
+ }
+}
+
+impl<I> Deref for CounterSet<I> {
+ type Target = [CounterPair<I>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &(self.0).0
+ }
+}
+
+/// A generic value for the `counter-reset` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounterReset<I>(#[css(field_bound)] pub GenericCounters<I>);
+pub use self::GenericCounterReset as CounterReset;
+
+impl<I> CounterReset<I> {
+ /// Returns a new value for `counter-reset`.
+ #[inline]
+ pub fn new(counters: Vec<CounterPair<I>>) -> Self {
+ CounterReset(Counters(counters.into()))
+ }
+}
+
+impl<I> Deref for CounterReset<I> {
+ type Target = [CounterPair<I>];
+
+ #[inline]
+ fn deref(&self) -> &Self::Target {
+ &(self.0).0
+ }
+}
+
+/// A generic value for lists of counters.
+///
+/// Keyword `none` is represented by an empty vector.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericCounters<I>(
+ #[css(field_bound)]
+ #[css(iterable, if_empty = "none")]
+ crate::OwnedSlice<GenericCounterPair<I>>,
+);
+pub use self::GenericCounters as Counters;
+
+#[cfg(feature = "servo-layout-2013")]
+type CounterStyleType = ListStyleType;
+
+#[cfg(feature = "gecko")]
+type CounterStyleType = CounterStyle;
+
+#[cfg(feature = "servo-layout-2013")]
+#[inline]
+fn is_decimal(counter_type: &CounterStyleType) -> bool {
+ *counter_type == ListStyleType::Decimal
+}
+
+#[cfg(feature = "gecko")]
+#[inline]
+fn is_decimal(counter_type: &CounterStyleType) -> bool {
+ *counter_type == CounterStyle::decimal()
+}
+
+/// The specified value for the `content` property.
+///
+/// https://drafts.csswg.org/css-content/#propdef-content
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum GenericContent<Image> {
+ /// `normal` reserved keyword.
+ Normal,
+ /// `none` reserved keyword.
+ None,
+ /// Content items.
+ Items(#[css(iterable)] crate::OwnedSlice<GenericContentItem<Image>>),
+}
+
+pub use self::GenericContent as Content;
+
+impl<Image> Content<Image> {
+ /// Whether `self` represents list of items.
+ #[inline]
+ pub fn is_items(&self) -> bool {
+ matches!(*self, Self::Items(..))
+ }
+
+ /// Set `content` property to `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ Content::Normal
+ }
+}
+
+/// Items for the `content` property.
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum GenericContentItem<I> {
+ /// Literal string content.
+ String(crate::OwnedStr),
+ /// `counter(name, style)`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ #[css(comma, function)]
+ Counter(CustomIdent, #[css(skip_if = "is_decimal")] CounterStyleType),
+ /// `counters(name, separator, style)`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ #[css(comma, function)]
+ Counters(
+ CustomIdent,
+ crate::OwnedStr,
+ #[css(skip_if = "is_decimal")] CounterStyleType,
+ ),
+ /// `open-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ OpenQuote,
+ /// `close-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ CloseQuote,
+ /// `no-open-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ NoOpenQuote,
+ /// `no-close-quote`.
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ NoCloseQuote,
+ /// `-moz-alt-content`.
+ #[cfg(feature = "gecko")]
+ MozAltContent,
+ /// `-moz-label-content`.
+ /// This is needed to make `accesskey` work for XUL labels. It's basically
+ /// attr(value) otherwise.
+ #[cfg(feature = "gecko")]
+ MozLabelContent,
+ /// `attr([namespace? `|`]? ident)`
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+ Attr(Attr),
+ /// image-set(url) | url(url)
+ Image(I),
+}
+
+pub use self::GenericContentItem as ContentItem;
diff --git a/servo/components/style/values/generics/easing.rs b/servo/components/style/values/generics/easing.rs
new file mode 100644
index 0000000000..e04b49a4be
--- /dev/null
+++ b/servo/components/style/values/generics/easing.rs
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS Easing Functions.
+//! https://drafts.csswg.org/css-easing/#timing-functions
+
+use crate::parser::ParserContext;
+
+/// A generic easing function.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[value_info(ty = "TIMING_FUNCTION")]
+#[repr(u8, C)]
+pub enum TimingFunction<Integer, Number, LinearStops> {
+ /// `linear | ease | ease-in | ease-out | ease-in-out`
+ Keyword(TimingKeyword),
+ /// `cubic-bezier(<number>, <number>, <number>, <number>)`
+ #[allow(missing_docs)]
+ #[css(comma, function)]
+ CubicBezier {
+ x1: Number,
+ y1: Number,
+ x2: Number,
+ y2: Number,
+ },
+ /// `step-start | step-end | steps(<integer>, [ <step-position> ]?)`
+ /// `<step-position> = jump-start | jump-end | jump-none | jump-both | start | end`
+ #[css(comma, function)]
+ #[value_info(other_values = "step-start,step-end")]
+ Steps(Integer, #[css(skip_if = "is_end")] StepPosition),
+ /// linear([<linear-stop>]#)
+ /// <linear-stop> = <output> && <linear-stop-length>?
+ /// <linear-stop-length> = <percentage>{1, 2}
+ #[css(function = "linear")]
+ LinearFunction(LinearStops),
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(u8)]
+pub enum TimingKeyword {
+ Linear,
+ Ease,
+ EaseIn,
+ EaseOut,
+ EaseInOut,
+}
+
+/// Before flag, defined as per https://drafts.csswg.org/css-easing/#before-flag
+/// This flag is never user-specified.
+#[allow(missing_docs)]
+#[derive(PartialEq)]
+#[repr(u8)]
+pub enum BeforeFlag {
+ Unset,
+ Set,
+}
+
+#[cfg(feature = "gecko")]
+fn step_position_jump_enabled(_context: &ParserContext) -> bool {
+ true
+}
+
+#[cfg(feature = "servo")]
+fn step_position_jump_enabled(_context: &ParserContext) -> bool {
+ false
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(u8)]
+pub enum StepPosition {
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpStart,
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpEnd,
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpNone,
+ #[parse(condition = "step_position_jump_enabled")]
+ JumpBoth,
+ Start,
+ End,
+}
+
+#[inline]
+fn is_end(position: &StepPosition) -> bool {
+ *position == StepPosition::JumpEnd || *position == StepPosition::End
+}
+
+impl<Integer, Number, LinearStops> TimingFunction<Integer, Number, LinearStops> {
+ /// `ease`
+ #[inline]
+ pub fn ease() -> Self {
+ TimingFunction::Keyword(TimingKeyword::Ease)
+ }
+
+ /// Returns true if it is `ease`.
+ #[inline]
+ pub fn is_ease(&self) -> bool {
+ matches!(*self, TimingFunction::Keyword(TimingKeyword::Ease))
+ }
+}
diff --git a/servo/components/style/values/generics/effects.rs b/servo/components/style/values/generics/effects.rs
new file mode 100644
index 0000000000..f5666f3055
--- /dev/null
+++ b/servo/components/style/values/generics/effects.rs
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values related to effects.
+
+/// A generic value for a single `box-shadow`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericBoxShadow<Color, SizeLength, BlurShapeLength, ShapeLength> {
+ /// The base shadow.
+ pub base: GenericSimpleShadow<Color, SizeLength, BlurShapeLength>,
+ /// The spread radius.
+ pub spread: ShapeLength,
+ /// Whether this is an inset box shadow.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub inset: bool,
+}
+
+pub use self::GenericBoxShadow as BoxShadow;
+
+/// A generic value for a single `filter`.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(C, u8)]
+pub enum GenericFilter<Angle, NonNegativeFactor, ZeroToOneFactor, Length, Shadow, U> {
+ /// `blur(<length>)`
+ #[css(function)]
+ Blur(Length),
+ /// `brightness(<factor>)`
+ #[css(function)]
+ Brightness(NonNegativeFactor),
+ /// `contrast(<factor>)`
+ #[css(function)]
+ Contrast(NonNegativeFactor),
+ /// `grayscale(<factor>)`
+ #[css(function)]
+ Grayscale(ZeroToOneFactor),
+ /// `hue-rotate(<angle>)`
+ #[css(function)]
+ HueRotate(Angle),
+ /// `invert(<factor>)`
+ #[css(function)]
+ Invert(ZeroToOneFactor),
+ /// `opacity(<factor>)`
+ #[css(function)]
+ Opacity(ZeroToOneFactor),
+ /// `saturate(<factor>)`
+ #[css(function)]
+ Saturate(NonNegativeFactor),
+ /// `sepia(<factor>)`
+ #[css(function)]
+ Sepia(ZeroToOneFactor),
+ /// `drop-shadow(...)`
+ #[css(function)]
+ DropShadow(Shadow),
+ /// `<url>`
+ #[animation(error)]
+ Url(U),
+}
+
+pub use self::GenericFilter as Filter;
+
+/// A generic value for the `drop-shadow()` filter and the `text-shadow` property.
+///
+/// Contrary to the canonical order from the spec, the color is serialised
+/// first, like in Gecko and Webkit.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericSimpleShadow<Color, SizeLength, ShapeLength> {
+ /// Color.
+ pub color: Color,
+ /// Horizontal radius.
+ pub horizontal: SizeLength,
+ /// Vertical radius.
+ pub vertical: SizeLength,
+ /// Blur radius.
+ pub blur: ShapeLength,
+}
+
+pub use self::GenericSimpleShadow as SimpleShadow;
diff --git a/servo/components/style/values/generics/flex.rs b/servo/components/style/values/generics/flex.rs
new file mode 100644
index 0000000000..85b64000f2
--- /dev/null
+++ b/servo/components/style/values/generics/flex.rs
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values related to flexbox.
+
+/// A generic value for the `flex-basis` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub enum GenericFlexBasis<S> {
+ /// `content`
+ Content,
+ /// `<width>`
+ Size(S),
+}
+
+pub use self::GenericFlexBasis as FlexBasis;
diff --git a/servo/components/style/values/generics/font.rs b/servo/components/style/values/generics/font.rs
new file mode 100644
index 0000000000..91dd2d8515
--- /dev/null
+++ b/servo/components/style/values/generics/font.rs
@@ -0,0 +1,316 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for font stuff.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::animated::ToAnimatedZero;
+use crate::One;
+use byteorder::{BigEndian, ReadBytesExt};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use std::io::Cursor;
+use style_traits::{CssWriter, ParseError};
+use style_traits::{StyleParseErrorKind, ToCss};
+
+/// A trait for values that are labelled with a FontTag (for feature and
+/// variation settings).
+pub trait TaggedFontValue {
+ /// The value's tag.
+ fn tag(&self) -> FontTag;
+}
+
+/// https://drafts.csswg.org/css-fonts-4/#feature-tag-value
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct FeatureTagValue<Integer> {
+ /// A four-character tag, packed into a u32 (one byte per character).
+ pub tag: FontTag,
+ /// The actual value.
+ pub value: Integer,
+}
+
+impl<T> TaggedFontValue for FeatureTagValue<T> {
+ fn tag(&self) -> FontTag {
+ self.tag
+ }
+}
+
+impl<Integer> ToCss for FeatureTagValue<Integer>
+where
+ Integer: One + ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.tag.to_css(dest)?;
+ // Don't serialize the default value.
+ if !self.value.is_one() {
+ dest.write_char(' ')?;
+ self.value.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// Variation setting for a single feature, see:
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct VariationValue<Number> {
+ /// A four-character tag, packed into a u32 (one byte per character).
+ #[animation(constant)]
+ pub tag: FontTag,
+ /// The actual value.
+ pub value: Number,
+}
+
+impl<T> TaggedFontValue for VariationValue<T> {
+ fn tag(&self) -> FontTag {
+ self.tag
+ }
+}
+
+/// A value both for font-variation-settings and font-feature-settings.
+#[derive(
+ Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToResolvedValue, ToShmem,
+)]
+#[css(comma)]
+pub struct FontSettings<T>(#[css(if_empty = "normal", iterable)] pub Box<[T]>);
+
+impl<T> FontSettings<T> {
+ /// Default value of font settings as `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ FontSettings(vec![].into_boxed_slice())
+ }
+}
+
+impl<T: Parse> Parse for FontSettings<T> {
+ /// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-feature-settings
+ /// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(Self::normal());
+ }
+
+ Ok(FontSettings(
+ input
+ .parse_comma_separated(|i| T::parse(context, i))?
+ .into_boxed_slice(),
+ ))
+ }
+}
+
+/// A font four-character tag, represented as a u32 for convenience.
+///
+/// See:
+/// https://drafts.csswg.org/css-fonts-4/#font-variation-settings-def
+/// https://drafts.csswg.org/css-fonts-4/#descdef-font-face-font-feature-settings
+///
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct FontTag(pub u32);
+
+impl ToCss for FontTag {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ use byteorder::ByteOrder;
+ use std::str;
+
+ let mut raw = [0u8; 4];
+ BigEndian::write_u32(&mut raw, self.0);
+ str::from_utf8(&raw).unwrap_or_default().to_css(dest)
+ }
+}
+
+impl Parse for FontTag {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let tag = input.expect_string()?;
+
+ // allowed strings of length 4 containing chars: <U+20, U+7E>
+ if tag.len() != 4 || tag.as_bytes().iter().any(|c| *c < b' ' || *c > b'~') {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let mut raw = Cursor::new(tag.as_bytes());
+ Ok(FontTag(raw.read_u32::<BigEndian>().unwrap()))
+ }
+}
+
+/// A generic value for the `font-style` property.
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-style-prop
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum FontStyle<Angle> {
+ #[animation(error)]
+ Normal,
+ #[animation(error)]
+ Italic,
+ #[value_info(starts_with_keyword)]
+ Oblique(Angle),
+}
+
+/// A generic value for the `font-size-adjust` property.
+///
+/// https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop
+#[allow(missing_docs)]
+#[repr(u8)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum GenericFontSizeAdjust<Factor> {
+ #[animation(error)]
+ None,
+ #[value_info(starts_with_keyword)]
+ ExHeight(Factor),
+ #[value_info(starts_with_keyword)]
+ CapHeight(Factor),
+ #[value_info(starts_with_keyword)]
+ ChWidth(Factor),
+ #[value_info(starts_with_keyword)]
+ IcWidth(Factor),
+ #[value_info(starts_with_keyword)]
+ IcHeight(Factor),
+}
+
+impl<Factor: ToCss> ToCss for GenericFontSizeAdjust<Factor> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let (prefix, value) = match self {
+ Self::None => return dest.write_str("none"),
+ Self::ExHeight(v) => ("", v),
+ Self::CapHeight(v) => ("cap-height ", v),
+ Self::ChWidth(v) => ("ch-width ", v),
+ Self::IcWidth(v) => ("ic-width ", v),
+ Self::IcHeight(v) => ("ic-height ", v),
+ };
+
+ dest.write_str(prefix)?;
+ value.to_css(dest)
+ }
+}
+
+/// A generic value for the `line-height` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToCss,
+ ToShmem,
+ Parse,
+)]
+#[repr(C, u8)]
+pub enum GenericLineHeight<N, L> {
+ /// `normal`
+ Normal,
+ /// `-moz-block-height`
+ #[cfg(feature = "gecko")]
+ #[parse(condition = "ParserContext::in_ua_sheet")]
+ MozBlockHeight,
+ /// `<number>`
+ Number(N),
+ /// `<length-percentage>`
+ Length(L),
+}
+
+pub use self::GenericLineHeight as LineHeight;
+
+impl<N, L> ToAnimatedZero for LineHeight<N, L> {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+impl<N, L> LineHeight<N, L> {
+ /// Returns `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ LineHeight::Normal
+ }
+}
diff --git a/servo/components/style/values/generics/grid.rs b/servo/components/style/values/generics/grid.rs
new file mode 100644
index 0000000000..22fe249c83
--- /dev/null
+++ b/servo/components/style/values/generics/grid.rs
@@ -0,0 +1,867 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for the handling of
+//! [grids](https://drafts.csswg.org/css-grid/).
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified;
+use crate::values::{CSSFloat, CustomIdent};
+use crate::{One, Zero};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use std::{cmp, usize};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// These are the limits that we choose to clamp grid line numbers to.
+/// http://drafts.csswg.org/css-grid/#overlarge-grids
+/// line_num is clamped to this range at parse time.
+pub const MIN_GRID_LINE: i32 = -10000;
+/// See above.
+pub const MAX_GRID_LINE: i32 = 10000;
+
+/// A `<grid-line>` type.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-grid-row-start-grid-line>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericGridLine<Integer> {
+ /// A custom identifier for named lines, or the empty atom otherwise.
+ ///
+ /// <https://drafts.csswg.org/css-grid/#grid-placement-slot>
+ pub ident: CustomIdent,
+ /// Denotes the nth grid line from grid item's placement.
+ ///
+ /// This is clamped by MIN_GRID_LINE and MAX_GRID_LINE.
+ ///
+ /// NOTE(emilio): If we ever allow animating these we need to either do
+ /// something more complicated for the clamping, or do this clamping at
+ /// used-value time.
+ pub line_num: Integer,
+ /// Flag to check whether it's a `span` keyword.
+ pub is_span: bool,
+}
+
+pub use self::GenericGridLine as GridLine;
+
+impl<Integer> GridLine<Integer>
+where
+ Integer: PartialEq + Zero,
+{
+ /// The `auto` value.
+ pub fn auto() -> Self {
+ Self {
+ is_span: false,
+ line_num: Zero::zero(),
+ ident: CustomIdent(atom!("")),
+ }
+ }
+
+ /// Check whether this `<grid-line>` represents an `auto` value.
+ pub fn is_auto(&self) -> bool {
+ self.ident.0 == atom!("") && self.line_num.is_zero() && !self.is_span
+ }
+
+ /// Check whether this `<grid-line>` represents a `<custom-ident>` value.
+ pub fn is_ident_only(&self) -> bool {
+ self.ident.0 != atom!("") && self.line_num.is_zero() && !self.is_span
+ }
+
+ /// Check if `self` makes `other` omittable according to the rules at:
+ /// https://drafts.csswg.org/css-grid/#propdef-grid-column
+ /// https://drafts.csswg.org/css-grid/#propdef-grid-area
+ pub fn can_omit(&self, other: &Self) -> bool {
+ if self.is_ident_only() {
+ self == other
+ } else {
+ other.is_auto()
+ }
+ }
+}
+
+impl<Integer> ToCss for GridLine<Integer>
+where
+ Integer: ToCss + PartialEq + Zero + One,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ // 1. `auto`
+ if self.is_auto() {
+ return dest.write_str("auto");
+ }
+
+ // 2. `<custom-ident>`
+ if self.is_ident_only() {
+ return self.ident.to_css(dest);
+ }
+
+ // 3. `[ span && [ <integer [1,∞]> || <custom-ident> ] ]`
+ let has_ident = self.ident.0 != atom!("");
+ if self.is_span {
+ dest.write_str("span")?;
+ debug_assert!(!self.line_num.is_zero() || has_ident);
+
+ // We omit `line_num` if
+ // 1. we don't specify it, or
+ // 2. it is the default value, i.e. 1.0, and the ident is specified.
+ // https://drafts.csswg.org/css-grid/#grid-placement-span-int
+ if !self.line_num.is_zero() && !(self.line_num.is_one() && has_ident) {
+ dest.write_char(' ')?;
+ self.line_num.to_css(dest)?;
+ }
+
+ if has_ident {
+ dest.write_char(' ')?;
+ self.ident.to_css(dest)?;
+ }
+ return Ok(());
+ }
+
+ // 4. `[ <integer [-∞,-1]> | <integer [1,∞]> ] && <custom-ident>? ]`
+ debug_assert!(!self.line_num.is_zero());
+ self.line_num.to_css(dest)?;
+ if has_ident {
+ dest.write_char(' ')?;
+ self.ident.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl Parse for GridLine<specified::Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut grid_line = Self::auto();
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(grid_line);
+ }
+
+ // <custom-ident> | [ <integer> && <custom-ident>? ] | [ span && [ <integer> || <custom-ident> ] ]
+ // This <grid-line> horror is simply,
+ // [ span? && [ <custom-ident> || <integer> ] ]
+ // And, for some magical reason, "span" should be the first or last value and not in-between.
+ let mut val_before_span = false;
+
+ for _ in 0..3 {
+ // Maximum possible entities for <grid-line>
+ let location = input.current_source_location();
+ if input.try_parse(|i| i.expect_ident_matching("span")).is_ok() {
+ if grid_line.is_span {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if !grid_line.line_num.is_zero() || grid_line.ident.0 != atom!("") {
+ val_before_span = true;
+ }
+
+ grid_line.is_span = true;
+ } else if let Ok(i) = input.try_parse(|i| specified::Integer::parse(context, i)) {
+ // FIXME(emilio): Probably shouldn't reject if it's calc()...
+ let value = i.value();
+ if value == 0 || val_before_span || !grid_line.line_num.is_zero() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ grid_line.line_num = specified::Integer::new(cmp::max(
+ MIN_GRID_LINE,
+ cmp::min(value, MAX_GRID_LINE),
+ ));
+ } else if let Ok(name) = input.try_parse(|i| CustomIdent::parse(i, &["auto"])) {
+ if val_before_span || grid_line.ident.0 != atom!("") {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ // NOTE(emilio): `span` is consumed above, so we only need to
+ // reject `auto`.
+ grid_line.ident = name;
+ } else {
+ break;
+ }
+ }
+
+ if grid_line.is_auto() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if grid_line.is_span {
+ if !grid_line.line_num.is_zero() {
+ if grid_line.line_num.value() <= 0 {
+ // disallow negative integers for grid spans
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ } else if grid_line.ident.0 == atom!("") {
+ // integer could be omitted
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ Ok(grid_line)
+ }
+}
+
+/// A track breadth for explicit grid track sizing. It's generic solely to
+/// avoid re-implementing it for the computed type.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-breadth>
+#[derive(
+ Animate,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericTrackBreadth<L> {
+ /// The generic type is almost always a non-negative `<length-percentage>`
+ Breadth(L),
+ /// A flex fraction specified in `fr` units.
+ #[css(dimension)]
+ Fr(CSSFloat),
+ /// `auto`
+ Auto,
+ /// `min-content`
+ MinContent,
+ /// `max-content`
+ MaxContent,
+}
+
+pub use self::GenericTrackBreadth as TrackBreadth;
+
+impl<L> TrackBreadth<L> {
+ /// Check whether this is a `<fixed-breadth>` (i.e., it only has `<length-percentage>`)
+ ///
+ /// <https://drafts.csswg.org/css-grid/#typedef-fixed-breadth>
+ #[inline]
+ pub fn is_fixed(&self) -> bool {
+ matches!(*self, TrackBreadth::Breadth(..))
+ }
+}
+
+/// A `<track-size>` type for explicit grid track sizing. Like `<track-breadth>`, this is
+/// generic only to avoid code bloat. It only takes `<length-percentage>`
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-size>
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericTrackSize<L> {
+ /// A flexible `<track-breadth>`
+ Breadth(GenericTrackBreadth<L>),
+ /// A `minmax` function for a range over an inflexible `<track-breadth>`
+ /// and a flexible `<track-breadth>`
+ ///
+ /// <https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-minmax>
+ #[css(function)]
+ Minmax(GenericTrackBreadth<L>, GenericTrackBreadth<L>),
+ /// A `fit-content` function.
+ ///
+ /// This stores a TrackBreadth<L> for convenience, but it can only be a
+ /// LengthPercentage.
+ ///
+ /// <https://drafts.csswg.org/css-grid/#valdef-grid-template-columns-fit-content>
+ #[css(function)]
+ FitContent(GenericTrackBreadth<L>),
+}
+
+pub use self::GenericTrackSize as TrackSize;
+
+impl<L> TrackSize<L> {
+ /// The initial value.
+ const INITIAL_VALUE: Self = TrackSize::Breadth(TrackBreadth::Auto);
+
+ /// Returns the initial value.
+ pub const fn initial_value() -> Self {
+ Self::INITIAL_VALUE
+ }
+
+ /// Returns true if `self` is the initial value.
+ pub fn is_initial(&self) -> bool {
+ matches!(*self, TrackSize::Breadth(TrackBreadth::Auto)) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585
+ }
+
+ /// Check whether this is a `<fixed-size>`
+ ///
+ /// <https://drafts.csswg.org/css-grid/#typedef-fixed-size>
+ pub fn is_fixed(&self) -> bool {
+ match *self {
+ TrackSize::Breadth(ref breadth) => breadth.is_fixed(),
+ // For minmax function, it could be either
+ // minmax(<fixed-breadth>, <track-breadth>) or minmax(<inflexible-breadth>, <fixed-breadth>),
+ // and since both variants are a subset of minmax(<inflexible-breadth>, <track-breadth>), we only
+ // need to make sure that they're fixed. So, we don't have to modify the parsing function.
+ TrackSize::Minmax(ref breadth_1, ref breadth_2) => {
+ if breadth_1.is_fixed() {
+ return true; // the second value is always a <track-breadth>
+ }
+
+ match *breadth_1 {
+ TrackBreadth::Fr(_) => false, // should be <inflexible-breadth> at this point
+ _ => breadth_2.is_fixed(),
+ }
+ },
+ TrackSize::FitContent(_) => false,
+ }
+ }
+}
+
+impl<L> Default for TrackSize<L> {
+ fn default() -> Self {
+ Self::initial_value()
+ }
+}
+
+impl<L: ToCss> ToCss for TrackSize<L> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ TrackSize::Breadth(ref breadth) => breadth.to_css(dest),
+ TrackSize::Minmax(ref min, ref max) => {
+ // According to gecko minmax(auto, <flex>) is equivalent to <flex>,
+ // and both are serialized as <flex>.
+ if let TrackBreadth::Auto = *min {
+ if let TrackBreadth::Fr(_) = *max {
+ return max.to_css(dest);
+ }
+ }
+
+ dest.write_str("minmax(")?;
+ min.to_css(dest)?;
+ dest.write_str(", ")?;
+ max.to_css(dest)?;
+ dest.write_char(')')
+ },
+ TrackSize::FitContent(ref lp) => {
+ dest.write_str("fit-content(")?;
+ lp.to_css(dest)?;
+ dest.write_char(')')
+ },
+ }
+ }
+}
+
+/// A `<track-size>+`.
+/// We use the empty slice as `auto`, and always parse `auto` as an empty slice.
+/// This means it's impossible to have a slice containing only one auto item.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct GenericImplicitGridTracks<T>(
+ #[css(if_empty = "auto", iterable)] pub crate::OwnedSlice<T>,
+);
+
+pub use self::GenericImplicitGridTracks as ImplicitGridTracks;
+
+impl<T: fmt::Debug + Default + PartialEq> ImplicitGridTracks<T> {
+ /// Returns true if current value is same as its initial value (i.e. auto).
+ pub fn is_initial(&self) -> bool {
+ debug_assert_ne!(
+ *self,
+ ImplicitGridTracks(crate::OwnedSlice::from(vec![Default::default()]))
+ );
+ self.0.is_empty()
+ }
+}
+
+/// Helper function for serializing identifiers with a prefix and suffix, used
+/// for serializing <line-names> (in grid).
+pub fn concat_serialize_idents<W>(
+ prefix: &str,
+ suffix: &str,
+ slice: &[CustomIdent],
+ sep: &str,
+ dest: &mut CssWriter<W>,
+) -> fmt::Result
+where
+ W: Write,
+{
+ if let Some((ref first, rest)) = slice.split_first() {
+ dest.write_str(prefix)?;
+ first.to_css(dest)?;
+ for thing in rest {
+ dest.write_str(sep)?;
+ thing.to_css(dest)?;
+ }
+
+ dest.write_str(suffix)?;
+ }
+
+ Ok(())
+}
+
+/// The initial argument of the `repeat` function.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum RepeatCount<Integer> {
+ /// A positive integer. This is allowed only for `<track-repeat>` and `<fixed-repeat>`
+ Number(Integer),
+ /// An `<auto-fill>` keyword allowed only for `<auto-repeat>`
+ AutoFill,
+ /// An `<auto-fit>` keyword allowed only for `<auto-repeat>`
+ AutoFit,
+}
+
+impl Parse for RepeatCount<specified::Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(mut i) = input.try_parse(|i| specified::Integer::parse_positive(context, i)) {
+ if i.value() > MAX_GRID_LINE {
+ i = specified::Integer::new(MAX_GRID_LINE);
+ }
+ return Ok(RepeatCount::Number(i));
+ }
+ try_match_ident_ignore_ascii_case! { input,
+ "auto-fill" => Ok(RepeatCount::AutoFill),
+ "auto-fit" => Ok(RepeatCount::AutoFit),
+ }
+ }
+}
+
+/// The structure containing `<line-names>` and `<track-size>` values.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "repeat")]
+#[repr(C)]
+pub struct GenericTrackRepeat<L, I> {
+ /// The number of times for the value to be repeated (could also be `auto-fit` or `auto-fill`)
+ pub count: RepeatCount<I>,
+ /// `<line-names>` accompanying `<track_size>` values.
+ ///
+ /// If there's no `<line-names>`, then it's represented by an empty vector.
+ /// For N `<track-size>` values, there will be N+1 `<line-names>`, and so this vector's
+ /// length is always one value more than that of the `<track-size>`.
+ pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>,
+ /// `<track-size>` values.
+ pub track_sizes: crate::OwnedSlice<GenericTrackSize<L>>,
+}
+
+pub use self::GenericTrackRepeat as TrackRepeat;
+
+impl<L: ToCss, I: ToCss> ToCss for TrackRepeat<L, I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("repeat(")?;
+ self.count.to_css(dest)?;
+ dest.write_str(", ")?;
+
+ let mut line_names_iter = self.line_names.iter();
+ for (i, (ref size, ref names)) in self
+ .track_sizes
+ .iter()
+ .zip(&mut line_names_iter)
+ .enumerate()
+ {
+ if i > 0 {
+ dest.write_char(' ')?;
+ }
+
+ concat_serialize_idents("[", "] ", names, " ", dest)?;
+ size.to_css(dest)?;
+ }
+
+ if let Some(line_names_last) = line_names_iter.next() {
+ concat_serialize_idents(" [", "]", line_names_last, " ", dest)?;
+ }
+
+ dest.write_char(')')?;
+
+ Ok(())
+ }
+}
+
+/// Track list values. Can be <track-size> or <track-repeat>
+#[derive(
+ Animate,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericTrackListValue<LengthPercentage, Integer> {
+ /// A <track-size> value.
+ TrackSize(#[animation(field_bound)] GenericTrackSize<LengthPercentage>),
+ /// A <track-repeat> value.
+ TrackRepeat(#[animation(field_bound)] GenericTrackRepeat<LengthPercentage, Integer>),
+}
+
+pub use self::GenericTrackListValue as TrackListValue;
+
+impl<L, I> TrackListValue<L, I> {
+ // FIXME: can't use TrackSize::initial_value() here b/c rustc error "is not yet stable as a const fn"
+ const INITIAL_VALUE: Self = TrackListValue::TrackSize(TrackSize::Breadth(TrackBreadth::Auto));
+
+ fn is_repeat(&self) -> bool {
+ matches!(*self, TrackListValue::TrackRepeat(..))
+ }
+
+ /// Returns true if `self` is the initial value.
+ pub fn is_initial(&self) -> bool {
+ matches!(
+ *self,
+ TrackListValue::TrackSize(TrackSize::Breadth(TrackBreadth::Auto))
+ ) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585
+ }
+}
+
+impl<L, I> Default for TrackListValue<L, I> {
+ #[inline]
+ fn default() -> Self {
+ Self::INITIAL_VALUE
+ }
+}
+
+/// A grid `<track-list>` type.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-list>
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericTrackList<LengthPercentage, Integer> {
+ /// The index in `values` where our `<auto-repeat>` value is, if in bounds.
+ #[css(skip)]
+ pub auto_repeat_index: usize,
+ /// A vector of `<track-size> | <track-repeat>` values.
+ pub values: crate::OwnedSlice<GenericTrackListValue<LengthPercentage, Integer>>,
+ /// `<line-names>` accompanying `<track-size> | <track-repeat>` values.
+ ///
+ /// If there's no `<line-names>`, then it's represented by an empty vector.
+ /// For N values, there will be N+1 `<line-names>`, and so this vector's
+ /// length is always one value more than that of the `<track-size>`.
+ pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>,
+}
+
+pub use self::GenericTrackList as TrackList;
+
+impl<L, I> TrackList<L, I> {
+ /// Whether this track list is an explicit track list (that is, doesn't have
+ /// any repeat values).
+ pub fn is_explicit(&self) -> bool {
+ !self.values.iter().any(|v| v.is_repeat())
+ }
+
+ /// Whether this track list has an `<auto-repeat>` value.
+ pub fn has_auto_repeat(&self) -> bool {
+ self.auto_repeat_index < self.values.len()
+ }
+}
+
+impl<L: ToCss, I: ToCss> ToCss for TrackList<L, I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let mut values_iter = self.values.iter().peekable();
+ let mut line_names_iter = self.line_names.iter().peekable();
+
+ for idx in 0.. {
+ let names = line_names_iter.next().unwrap(); // This should exist!
+ concat_serialize_idents("[", "]", names, " ", dest)?;
+
+ match values_iter.next() {
+ Some(value) => {
+ if !names.is_empty() {
+ dest.write_char(' ')?;
+ }
+
+ value.to_css(dest)?;
+ },
+ None => break,
+ }
+
+ if values_iter.peek().is_some() ||
+ line_names_iter.peek().map_or(false, |v| !v.is_empty()) ||
+ (idx + 1 == self.auto_repeat_index)
+ {
+ dest.write_char(' ')?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+/// The `<name-repeat>` for subgrids.
+///
+/// <name-repeat> = repeat( [ <integer [1,∞]> | auto-fill ], <line-names>+)
+///
+/// https://drafts.csswg.org/css-grid/#typedef-name-repeat
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericNameRepeat<I> {
+ /// The number of times for the value to be repeated (could also be `auto-fill`).
+ /// Note: `RepeatCount` accepts `auto-fit`, so we should reject it after parsing it.
+ pub count: RepeatCount<I>,
+ /// This represents `<line-names>+`. The length of the outer vector is at least one.
+ pub line_names: crate::OwnedSlice<crate::OwnedSlice<CustomIdent>>,
+}
+
+pub use self::GenericNameRepeat as NameRepeat;
+
+impl<I: ToCss> ToCss for NameRepeat<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("repeat(")?;
+ self.count.to_css(dest)?;
+ dest.write_char(',')?;
+
+ for ref names in self.line_names.iter() {
+ if names.is_empty() {
+ // Note: concat_serialize_idents() skip the empty list so we have to handle it
+ // manually for NameRepeat.
+ dest.write_str(" []")?;
+ } else {
+ concat_serialize_idents(" [", "]", names, " ", dest)?;
+ }
+ }
+
+ dest.write_char(')')
+ }
+}
+
+impl<I> NameRepeat<I> {
+ /// Returns true if it is auto-fill.
+ #[inline]
+ pub fn is_auto_fill(&self) -> bool {
+ matches!(self.count, RepeatCount::AutoFill)
+ }
+}
+
+/// A single value for `<line-names>` or `<name-repeat>`.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericLineNameListValue<I> {
+ /// `<line-names>`.
+ LineNames(crate::OwnedSlice<CustomIdent>),
+ /// `<name-repeat>`.
+ Repeat(GenericNameRepeat<I>),
+}
+
+pub use self::GenericLineNameListValue as LineNameListValue;
+
+impl<I: ToCss> ToCss for LineNameListValue<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Self::Repeat(ref r) => r.to_css(dest),
+ Self::LineNames(ref names) => {
+ dest.write_char('[')?;
+
+ if let Some((ref first, rest)) = names.split_first() {
+ first.to_css(dest)?;
+ for name in rest {
+ dest.write_char(' ')?;
+ name.to_css(dest)?;
+ }
+ }
+
+ dest.write_char(']')
+ },
+ }
+ }
+}
+
+/// The `<line-name-list>` for subgrids.
+///
+/// <line-name-list> = [ <line-names> | <name-repeat> ]+
+/// <name-repeat> = repeat( [ <integer [1,∞]> | auto-fill ], <line-names>+)
+///
+/// https://drafts.csswg.org/css-grid/#typedef-line-name-list
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericLineNameList<I> {
+ /// The pre-computed length of line_names, without the length of repeat(auto-fill, ...).
+ // We precomputed this at parsing time, so we can avoid an extra loop when expanding
+ // repeat(auto-fill).
+ pub expanded_line_names_length: usize,
+ /// The line name list.
+ pub line_names: crate::OwnedSlice<GenericLineNameListValue<I>>,
+}
+
+pub use self::GenericLineNameList as LineNameList;
+
+impl<I: ToCss> ToCss for LineNameList<I> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("subgrid")?;
+
+ for value in self.line_names.iter() {
+ dest.write_char(' ')?;
+ value.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// Variants for `<grid-template-rows> | <grid-template-columns>`
+#[derive(
+ Animate,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "subgrid")]
+#[repr(C, u8)]
+pub enum GenericGridTemplateComponent<L, I> {
+ /// `none` value.
+ None,
+ /// The grid `<track-list>`
+ TrackList(
+ #[animation(field_bound)]
+ #[compute(field_bound)]
+ #[resolve(field_bound)]
+ #[shmem(field_bound)]
+ Box<GenericTrackList<L, I>>,
+ ),
+ /// A `subgrid <line-name-list>?`
+ /// TODO: Support animations for this after subgrid is addressed in [grid-2] spec.
+ #[animation(error)]
+ Subgrid(Box<GenericLineNameList<I>>),
+ /// `masonry` value.
+ /// https://github.com/w3c/csswg-drafts/issues/4650
+ Masonry,
+}
+
+pub use self::GenericGridTemplateComponent as GridTemplateComponent;
+
+impl<L, I> GridTemplateComponent<L, I> {
+ /// The initial value.
+ const INITIAL_VALUE: Self = Self::None;
+
+ /// Returns length of the <track-list>s <track-size>
+ pub fn track_list_len(&self) -> usize {
+ match *self {
+ GridTemplateComponent::TrackList(ref tracklist) => tracklist.values.len(),
+ _ => 0,
+ }
+ }
+
+ /// Returns true if `self` is the initial value.
+ pub fn is_initial(&self) -> bool {
+ matches!(*self, Self::None) // FIXME: can't use Self::INITIAL_VALUE here yet: https://github.com/rust-lang/rust/issues/66585
+ }
+}
+
+impl<L, I> Default for GridTemplateComponent<L, I> {
+ #[inline]
+ fn default() -> Self {
+ Self::INITIAL_VALUE
+ }
+}
diff --git a/servo/components/style/values/generics/image.rs b/servo/components/style/values/generics/image.rs
new file mode 100644
index 0000000000..6fc0870e15
--- /dev/null
+++ b/servo/components/style/values/generics/image.rs
@@ -0,0 +1,631 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for the handling of [images].
+//!
+//! [images]: https://drafts.csswg.org/css-images/#image-values
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::custom_properties;
+use crate::values::generics::position::PositionComponent;
+use crate::values::generics::Optional;
+use crate::values::serialize_atom_identifier;
+use crate::Atom;
+use crate::Zero;
+use servo_arc::Arc;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// An `<image> | none` value.
+///
+/// https://drafts.csswg.org/css-images/#image-values
+#[derive(
+ Clone, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericImage<G, ImageUrl, Color, Percentage, Resolution> {
+ /// `none` variant.
+ None,
+ /// A `<url()>` image.
+ Url(ImageUrl),
+
+ /// A `<gradient>` image. Gradients are rather large, and not nearly as
+ /// common as urls, so we box them here to keep the size of this enum sane.
+ Gradient(Box<G>),
+
+ /// A `-moz-element(# <element-id>)`
+ #[cfg(feature = "gecko")]
+ #[css(function = "-moz-element")]
+ Element(Atom),
+
+ /// A paint worklet image.
+ /// <https://drafts.css-houdini.org/css-paint-api/>
+ #[cfg(feature = "servo-layout-2013")]
+ PaintWorklet(PaintWorklet),
+
+ /// A `<cross-fade()>` image. Storing this directly inside of
+ /// GenericImage increases the size by 8 bytes so we box it here
+ /// and store images directly inside of cross-fade instead of
+ /// boxing them there.
+ CrossFade(Box<GenericCrossFade<Self, Color, Percentage>>),
+
+ /// An `image-set()` function.
+ ImageSet(#[compute(field_bound)] Box<GenericImageSet<Self, Resolution>>),
+}
+
+pub use self::GenericImage as Image;
+
+/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
+#[derive(
+ Clone, Debug, MallocSizeOf, PartialEq, ToResolvedValue, ToShmem, ToCss, ToComputedValue,
+)]
+#[css(comma, function = "cross-fade")]
+#[repr(C)]
+pub struct GenericCrossFade<Image, Color, Percentage> {
+ /// All of the image percent pairings passed as arguments to
+ /// cross-fade.
+ #[css(iterable)]
+ pub elements: crate::OwnedSlice<GenericCrossFadeElement<Image, Color, Percentage>>,
+}
+
+/// An optional percent and a cross fade image.
+#[derive(
+ Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, ToCss,
+)]
+#[repr(C)]
+pub struct GenericCrossFadeElement<Image, Color, Percentage> {
+ /// The percent of the final image that `image` will be.
+ pub percent: Optional<Percentage>,
+ /// A color or image that will be blended when cross-fade is
+ /// evaluated.
+ pub image: GenericCrossFadeImage<Image, Color>,
+}
+
+/// An image or a color. `cross-fade` takes either when blending
+/// images together.
+#[derive(
+ Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, ToCss,
+)]
+#[repr(C, u8)]
+pub enum GenericCrossFadeImage<I, C> {
+ /// A boxed image value. Boxing provides indirection so images can
+ /// be cross-fades and cross-fades can be images.
+ Image(I),
+ /// A color value.
+ Color(C),
+}
+
+pub use self::GenericCrossFade as CrossFade;
+pub use self::GenericCrossFadeElement as CrossFadeElement;
+pub use self::GenericCrossFadeImage as CrossFadeImage;
+
+/// https://drafts.csswg.org/css-images-4/#image-set-notation
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)]
+#[css(comma, function = "image-set")]
+#[repr(C)]
+pub struct GenericImageSet<Image, Resolution> {
+ /// The index of the selected candidate. usize::MAX for specified values or invalid images.
+ #[css(skip)]
+ pub selected_index: usize,
+
+ /// All of the image and resolution pairs.
+ #[css(iterable)]
+ pub items: crate::OwnedSlice<GenericImageSetItem<Image, Resolution>>,
+}
+
+/// An optional percent and a cross fade image.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct GenericImageSetItem<Image, Resolution> {
+ /// `<image>`. `<string>` is converted to `Image::Url` at parse time.
+ pub image: Image,
+ /// The `<resolution>`.
+ ///
+ /// TODO: Skip serialization if it is 1x.
+ pub resolution: Resolution,
+
+ /// The `type(<string>)`
+ /// (Optional) Specify the image's MIME type
+ pub mime_type: crate::OwnedStr,
+
+ /// True if mime_type has been specified
+ pub has_mime_type: bool,
+}
+
+impl<I: style_traits::ToCss, R: style_traits::ToCss> ToCss for GenericImageSetItem<I, R> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ self.image.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.resolution.to_css(dest)?;
+
+ if self.has_mime_type {
+ dest.write_char(' ')?;
+ dest.write_str("type(")?;
+ self.mime_type.to_css(dest)?;
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+}
+
+pub use self::GenericImageSet as ImageSet;
+pub use self::GenericImageSetItem as ImageSetItem;
+
+/// State flags stored on each variant of a Gradient.
+#[derive(
+ Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C)]
+pub struct GradientFlags(u8);
+bitflags! {
+ impl GradientFlags: u8 {
+ /// Set if this is a repeating gradient.
+ const REPEATING = 1 << 0;
+ /// Set if the color interpolation method matches the default for the items.
+ const HAS_DEFAULT_COLOR_INTERPOLATION_METHOD = 1 << 1;
+ }
+}
+
+/// A CSS gradient.
+/// <https://drafts.csswg.org/css-images/#gradients>
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub enum GenericGradient<
+ LineDirection,
+ LengthPercentage,
+ NonNegativeLength,
+ NonNegativeLengthPercentage,
+ Position,
+ Angle,
+ AngleOrPercentage,
+ Color,
+> {
+ /// A linear gradient.
+ Linear {
+ /// Line direction
+ direction: LineDirection,
+ /// Method to use for color interpolation.
+ color_interpolation_method: ColorInterpolationMethod,
+ /// The color stops and interpolation hints.
+ items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>,
+ /// State flags for the gradient.
+ flags: GradientFlags,
+ /// Compatibility mode.
+ compat_mode: GradientCompatMode,
+ },
+ /// A radial gradient.
+ Radial {
+ /// Shape of gradient
+ shape: GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage>,
+ /// Center of gradient
+ position: Position,
+ /// Method to use for color interpolation.
+ color_interpolation_method: ColorInterpolationMethod,
+ /// The color stops and interpolation hints.
+ items: crate::OwnedSlice<GenericGradientItem<Color, LengthPercentage>>,
+ /// State flags for the gradient.
+ flags: GradientFlags,
+ /// Compatibility mode.
+ compat_mode: GradientCompatMode,
+ },
+ /// A conic gradient.
+ Conic {
+ /// Start angle of gradient
+ angle: Angle,
+ /// Center of gradient
+ position: Position,
+ /// Method to use for color interpolation.
+ color_interpolation_method: ColorInterpolationMethod,
+ /// The color stops and interpolation hints.
+ items: crate::OwnedSlice<GenericGradientItem<Color, AngleOrPercentage>>,
+ /// State flags for the gradient.
+ flags: GradientFlags,
+ },
+}
+
+pub use self::GenericGradient as Gradient;
+
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(u8)]
+/// Whether we used the modern notation or the compatibility `-webkit`, `-moz` prefixes.
+pub enum GradientCompatMode {
+ /// Modern syntax.
+ Modern,
+ /// `-webkit` prefix.
+ WebKit,
+ /// `-moz` prefix
+ Moz,
+}
+
+/// A radial gradient's ending shape.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericEndingShape<NonNegativeLength, NonNegativeLengthPercentage> {
+ /// A circular gradient.
+ Circle(GenericCircle<NonNegativeLength>),
+ /// An elliptic gradient.
+ Ellipse(GenericEllipse<NonNegativeLengthPercentage>),
+}
+
+pub use self::GenericEndingShape as EndingShape;
+
+/// A circle shape.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericCircle<NonNegativeLength> {
+ /// A circle radius.
+ Radius(NonNegativeLength),
+ /// A circle extent.
+ Extent(ShapeExtent),
+}
+
+pub use self::GenericCircle as Circle;
+
+/// An ellipse shape.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericEllipse<NonNegativeLengthPercentage> {
+ /// An ellipse pair of radii.
+ Radii(NonNegativeLengthPercentage, NonNegativeLengthPercentage),
+ /// An ellipse extent.
+ Extent(ShapeExtent),
+}
+
+pub use self::GenericEllipse as Ellipse;
+
+/// <https://drafts.csswg.org/css-images/#typedef-extent-keyword>
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ShapeExtent {
+ ClosestSide,
+ FarthestSide,
+ ClosestCorner,
+ FarthestCorner,
+ Contain,
+ Cover,
+}
+
+/// A gradient item.
+/// <https://drafts.csswg.org/css-images-4/#color-stop-syntax>
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericGradientItem<Color, T> {
+ /// A simple color stop, without position.
+ SimpleColorStop(Color),
+ /// A complex color stop, with a position.
+ ComplexColorStop {
+ /// The color for the stop.
+ color: Color,
+ /// The position for the stop.
+ position: T,
+ },
+ /// An interpolation hint.
+ InterpolationHint(T),
+}
+
+pub use self::GenericGradientItem as GradientItem;
+
+/// A color stop.
+/// <https://drafts.csswg.org/css-images/#typedef-color-stop-list>
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem,
+)]
+pub struct ColorStop<Color, T> {
+ /// The color of this stop.
+ pub color: Color,
+ /// The position of this stop.
+ pub position: Option<T>,
+}
+
+impl<Color, T> ColorStop<Color, T> {
+ /// Convert the color stop into an appropriate `GradientItem`.
+ #[inline]
+ pub fn into_item(self) -> GradientItem<Color, T> {
+ match self.position {
+ Some(position) => GradientItem::ComplexColorStop {
+ color: self.color,
+ position,
+ },
+ None => GradientItem::SimpleColorStop(self.color),
+ }
+ }
+}
+
+/// Specified values for a paint worklet.
+/// <https://drafts.css-houdini.org/css-paint-api/>
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+#[derive(Clone, Debug, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+pub struct PaintWorklet {
+ /// The name the worklet was registered with.
+ pub name: Atom,
+ /// The arguments for the worklet.
+ /// TODO: store a parsed representation of the arguments.
+ #[cfg_attr(feature = "servo", ignore_malloc_size_of = "Arc")]
+ #[compute(no_field_bound)]
+ #[resolve(no_field_bound)]
+ pub arguments: Vec<Arc<custom_properties::SpecifiedValue>>,
+}
+
+impl ::style_traits::SpecifiedValueInfo for PaintWorklet {}
+
+impl ToCss for PaintWorklet {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("paint(")?;
+ serialize_atom_identifier(&self.name, dest)?;
+ for argument in &self.arguments {
+ dest.write_str(", ")?;
+ argument.to_css(dest)?;
+ }
+ dest.write_char(')')
+ }
+}
+
+impl<G, U, C, P, Resolution> fmt::Debug for Image<G, U, C, P, Resolution>
+where
+ Image<G, U, C, P, Resolution>: ToCss,
+{
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ self.to_css(&mut CssWriter::new(f))
+ }
+}
+
+impl<G, U, C, P, Resolution> ToCss for Image<G, U, C, P, Resolution>
+where
+ G: ToCss,
+ U: ToCss,
+ C: ToCss,
+ P: ToCss,
+ Resolution: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Image::None => dest.write_str("none"),
+ Image::Url(ref url) => url.to_css(dest),
+ Image::Gradient(ref gradient) => gradient.to_css(dest),
+ #[cfg(feature = "servo-layout-2013")]
+ Image::PaintWorklet(ref paint_worklet) => paint_worklet.to_css(dest),
+ #[cfg(feature = "gecko")]
+ Image::Element(ref selector) => {
+ dest.write_str("-moz-element(#")?;
+ serialize_atom_identifier(selector, dest)?;
+ dest.write_char(')')
+ },
+ Image::ImageSet(ref is) => is.to_css(dest),
+ Image::CrossFade(ref cf) => cf.to_css(dest),
+ }
+ }
+}
+
+impl<D, LP, NL, NLP, P, A: Zero, AoP, C> ToCss for Gradient<D, LP, NL, NLP, P, A, AoP, C>
+where
+ D: LineDirection,
+ LP: ToCss,
+ NL: ToCss,
+ NLP: ToCss,
+ P: PositionComponent + ToCss,
+ A: ToCss,
+ AoP: ToCss,
+ C: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let (compat_mode, repeating, has_default_color_interpolation_method) = match *self {
+ Gradient::Linear {
+ compat_mode, flags, ..
+ } |
+ Gradient::Radial {
+ compat_mode, flags, ..
+ } => (
+ compat_mode,
+ flags.contains(GradientFlags::REPEATING),
+ flags.contains(GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD),
+ ),
+ Gradient::Conic { flags, .. } => (
+ GradientCompatMode::Modern,
+ flags.contains(GradientFlags::REPEATING),
+ flags.contains(GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD),
+ ),
+ };
+
+ match compat_mode {
+ GradientCompatMode::WebKit => dest.write_str("-webkit-")?,
+ GradientCompatMode::Moz => dest.write_str("-moz-")?,
+ _ => {},
+ }
+
+ if repeating {
+ dest.write_str("repeating-")?;
+ }
+
+ match *self {
+ Gradient::Linear {
+ ref direction,
+ ref color_interpolation_method,
+ ref items,
+ compat_mode,
+ ..
+ } => {
+ dest.write_str("linear-gradient(")?;
+ let mut skip_comma = true;
+ if !direction.points_downwards(compat_mode) {
+ direction.to_css(dest, compat_mode)?;
+ skip_comma = false;
+ }
+ if !has_default_color_interpolation_method {
+ if !skip_comma {
+ dest.write_char(' ')?;
+ }
+ color_interpolation_method.to_css(dest)?;
+ skip_comma = false;
+ }
+ for item in &**items {
+ if !skip_comma {
+ dest.write_str(", ")?;
+ }
+ skip_comma = false;
+ item.to_css(dest)?;
+ }
+ },
+ Gradient::Radial {
+ ref shape,
+ ref position,
+ ref color_interpolation_method,
+ ref items,
+ compat_mode,
+ ..
+ } => {
+ dest.write_str("radial-gradient(")?;
+ let omit_shape = match *shape {
+ EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::Cover)) |
+ EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) => true,
+ _ => false,
+ };
+ let omit_position = position.is_center();
+ if compat_mode == GradientCompatMode::Modern {
+ if !omit_shape {
+ shape.to_css(dest)?;
+ if !omit_position {
+ dest.write_char(' ')?;
+ }
+ }
+ if !omit_position {
+ dest.write_str("at ")?;
+ position.to_css(dest)?;
+ }
+ } else {
+ if !omit_position {
+ position.to_css(dest)?;
+ if !omit_shape {
+ dest.write_str(", ")?;
+ }
+ }
+ if !omit_shape {
+ shape.to_css(dest)?;
+ }
+ }
+ if !has_default_color_interpolation_method {
+ if !omit_shape || !omit_position {
+ dest.write_char(' ')?;
+ }
+ color_interpolation_method.to_css(dest)?;
+ }
+
+ let mut skip_comma =
+ omit_shape && omit_position && has_default_color_interpolation_method;
+ for item in &**items {
+ if !skip_comma {
+ dest.write_str(", ")?;
+ }
+ skip_comma = false;
+ item.to_css(dest)?;
+ }
+ },
+ Gradient::Conic {
+ ref angle,
+ ref position,
+ ref color_interpolation_method,
+ ref items,
+ ..
+ } => {
+ dest.write_str("conic-gradient(")?;
+ let omit_angle = angle.is_zero();
+ let omit_position = position.is_center();
+ if !omit_angle {
+ dest.write_str("from ")?;
+ angle.to_css(dest)?;
+ if !omit_position {
+ dest.write_char(' ')?;
+ }
+ }
+ if !omit_position {
+ dest.write_str("at ")?;
+ position.to_css(dest)?;
+ }
+ if !has_default_color_interpolation_method {
+ if !omit_angle || !omit_position {
+ dest.write_char(' ')?;
+ }
+ color_interpolation_method.to_css(dest)?;
+ }
+ let mut skip_comma =
+ omit_angle && omit_position && has_default_color_interpolation_method;
+ for item in &**items {
+ if !skip_comma {
+ dest.write_str(", ")?;
+ }
+ skip_comma = false;
+ item.to_css(dest)?;
+ }
+ },
+ }
+ dest.write_char(')')
+ }
+}
+
+/// The direction of a linear gradient.
+pub trait LineDirection {
+ /// Whether this direction points towards, and thus can be omitted.
+ fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool;
+
+ /// Serialises this direction according to the compatibility mode.
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
+ where
+ W: Write;
+}
+
+impl<L> ToCss for Circle<L>
+where
+ L: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Circle::Extent(ShapeExtent::FarthestCorner) | Circle::Extent(ShapeExtent::Cover) => {
+ dest.write_str("circle")
+ },
+ Circle::Extent(keyword) => {
+ dest.write_str("circle ")?;
+ keyword.to_css(dest)
+ },
+ Circle::Radius(ref length) => length.to_css(dest),
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/length.rs b/servo/components/style/values/generics/length.rs
new file mode 100644
index 0000000000..de0dd7fbc1
--- /dev/null
+++ b/servo/components/style/values/generics/length.rs
@@ -0,0 +1,304 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values related to length.
+
+use crate::parser::{Parse, ParserContext};
+#[cfg(feature = "gecko")]
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// A `<length-percentage> | auto` value.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericLengthPercentageOrAuto<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ Auto,
+}
+
+pub use self::GenericLengthPercentageOrAuto as LengthPercentageOrAuto;
+
+impl<LengthPercentage> LengthPercentageOrAuto<LengthPercentage> {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ LengthPercentageOrAuto::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, LengthPercentageOrAuto::Auto)
+ }
+
+ /// A helper function to parse this with quirks or not and so forth.
+ pub fn parse_with<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parser: impl FnOnce(
+ &ParserContext,
+ &mut Parser<'i, 't>,
+ ) -> Result<LengthPercentage, ParseError<'i>>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(LengthPercentageOrAuto::Auto);
+ }
+
+ Ok(LengthPercentageOrAuto::LengthPercentage(parser(
+ context, input,
+ )?))
+ }
+}
+
+impl<LengthPercentage> LengthPercentageOrAuto<LengthPercentage>
+where
+ LengthPercentage: Clone,
+{
+ /// Resolves `auto` values by calling `f`.
+ #[inline]
+ pub fn auto_is(&self, f: impl FnOnce() -> LengthPercentage) -> LengthPercentage {
+ match self {
+ LengthPercentageOrAuto::LengthPercentage(length) => length.clone(),
+ LengthPercentageOrAuto::Auto => f(),
+ }
+ }
+
+ /// Returns the non-`auto` value, if any.
+ #[inline]
+ pub fn non_auto(&self) -> Option<LengthPercentage> {
+ match self {
+ LengthPercentageOrAuto::LengthPercentage(length) => Some(length.clone()),
+ LengthPercentageOrAuto::Auto => None,
+ }
+ }
+
+ /// Maps the length of this value.
+ pub fn map<T>(&self, f: impl FnOnce(LengthPercentage) -> T) -> LengthPercentageOrAuto<T> {
+ match self {
+ LengthPercentageOrAuto::LengthPercentage(l) => {
+ LengthPercentageOrAuto::LengthPercentage(f(l.clone()))
+ },
+ LengthPercentageOrAuto::Auto => LengthPercentageOrAuto::Auto,
+ }
+ }
+}
+
+impl<LengthPercentage: Zero> Zero for LengthPercentageOrAuto<LengthPercentage> {
+ fn zero() -> Self {
+ LengthPercentageOrAuto::LengthPercentage(Zero::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ match *self {
+ LengthPercentageOrAuto::LengthPercentage(ref l) => l.is_zero(),
+ LengthPercentageOrAuto::Auto => false,
+ }
+ }
+}
+
+impl<LengthPercentage: Parse> Parse for LengthPercentageOrAuto<LengthPercentage> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, LengthPercentage::parse)
+ }
+}
+
+/// A generic value for the `width`, `height`, `min-width`, or `min-height` property.
+///
+/// Unlike `max-width` or `max-height` properties, a Size can be `auto`,
+/// and cannot be `none`.
+///
+/// Note that it only accepts non-negative values.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSize<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ Auto,
+ #[animation(error)]
+ MaxContent,
+ #[animation(error)]
+ MinContent,
+ #[animation(error)]
+ FitContent,
+ #[animation(error)]
+ MozAvailable,
+ #[animation(error)]
+ #[css(function = "fit-content")]
+ FitContentFunction(LengthPercent),
+}
+
+pub use self::GenericSize as Size;
+
+impl<LengthPercentage> Size<LengthPercentage> {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ Size::Auto
+ }
+
+ /// Returns whether we're the auto value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, Size::Auto)
+ }
+}
+
+/// A generic value for the `max-width` or `max-height` property.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericMaxSize<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ None,
+ #[animation(error)]
+ MaxContent,
+ #[animation(error)]
+ MinContent,
+ #[animation(error)]
+ FitContent,
+ #[animation(error)]
+ MozAvailable,
+ #[animation(error)]
+ #[css(function = "fit-content")]
+ FitContentFunction(LengthPercent),
+}
+
+pub use self::GenericMaxSize as MaxSize;
+
+impl<LengthPercentage> MaxSize<LengthPercentage> {
+ /// `none` value.
+ #[inline]
+ pub fn none() -> Self {
+ MaxSize::None
+ }
+}
+
+/// A generic `<length>` | `<number>` value for the `tab-size` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericLengthOrNumber<L, N> {
+ /// A number.
+ ///
+ /// NOTE: Numbers need to be before lengths, in order to parse them
+ /// first, since `0` should be a number, not the `0px` length.
+ Number(N),
+ /// A length.
+ Length(L),
+}
+
+pub use self::GenericLengthOrNumber as LengthOrNumber;
+
+impl<L, N: Zero> Zero for LengthOrNumber<L, N> {
+ fn zero() -> Self {
+ LengthOrNumber::Number(Zero::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ match *self {
+ LengthOrNumber::Number(ref n) => n.is_zero(),
+ LengthOrNumber::Length(..) => false,
+ }
+ }
+}
+
+/// A generic `<length-percentage>` | normal` value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+#[allow(missing_docs)]
+pub enum GenericLengthPercentageOrNormal<LengthPercent> {
+ LengthPercentage(LengthPercent),
+ Normal,
+}
+
+pub use self::GenericLengthPercentageOrNormal as LengthPercentageOrNormal;
+
+impl<LengthPercent> LengthPercentageOrNormal<LengthPercent> {
+ /// Returns the normal value.
+ #[inline]
+ pub fn normal() -> Self {
+ LengthPercentageOrNormal::Normal
+ }
+}
diff --git a/servo/components/style/values/generics/mod.rs b/servo/components/style/values/generics/mod.rs
new file mode 100644
index 0000000000..800d058170
--- /dev/null
+++ b/servo/components/style/values/generics/mod.rs
@@ -0,0 +1,388 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types that share their serialization implementations
+//! for both specified and computed values.
+
+use super::CustomIdent;
+use crate::counter_style::{parse_counter_style_name, Symbols};
+use crate::parser::{Parse, ParserContext};
+use crate::Zero;
+use cssparser::Parser;
+use std::ops::Add;
+use style_traits::{KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind};
+
+pub mod animation;
+pub mod background;
+pub mod basic_shape;
+pub mod border;
+#[path = "box.rs"]
+pub mod box_;
+pub mod calc;
+pub mod color;
+pub mod column;
+pub mod counters;
+pub mod easing;
+pub mod effects;
+pub mod flex;
+pub mod font;
+pub mod grid;
+pub mod image;
+pub mod length;
+pub mod motion;
+pub mod page;
+pub mod position;
+pub mod ratio;
+pub mod rect;
+pub mod size;
+pub mod svg;
+pub mod text;
+pub mod transform;
+pub mod ui;
+pub mod url;
+
+/// https://drafts.csswg.org/css-counter-styles/#typedef-symbols-type
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum SymbolsType {
+ Cyclic,
+ Numeric,
+ Alphabetic,
+ Symbolic,
+ Fixed,
+}
+
+/// <https://drafts.csswg.org/css-counter-styles/#typedef-counter-style>
+///
+/// Note that 'none' is not a valid name.
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, Debug, Eq, PartialEq, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+pub enum CounterStyle {
+ /// `<counter-style-name>`
+ Name(CustomIdent),
+ /// `symbols()`
+ #[css(function)]
+ Symbols(#[css(skip_if = "is_symbolic")] SymbolsType, Symbols),
+}
+
+#[inline]
+fn is_symbolic(symbols_type: &SymbolsType) -> bool {
+ *symbols_type == SymbolsType::Symbolic
+}
+
+impl CounterStyle {
+ /// disc value
+ pub fn disc() -> Self {
+ CounterStyle::Name(CustomIdent(atom!("disc")))
+ }
+
+ /// decimal value
+ pub fn decimal() -> Self {
+ CounterStyle::Name(CustomIdent(atom!("decimal")))
+ }
+
+ /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`)
+ #[inline]
+ pub fn is_bullet(&self) -> bool {
+ match self {
+ CounterStyle::Name(CustomIdent(ref name)) => {
+ name == &atom!("disc") ||
+ name == &atom!("circle") ||
+ name == &atom!("square") ||
+ name == &atom!("disclosure-closed") ||
+ name == &atom!("disclosure-open")
+ },
+ _ => false,
+ }
+ }
+}
+
+impl Parse for CounterStyle {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(name) = input.try_parse(|i| parse_counter_style_name(i)) {
+ return Ok(CounterStyle::Name(name));
+ }
+ input.expect_function_matching("symbols")?;
+ input.parse_nested_block(|input| {
+ let symbols_type = input
+ .try_parse(SymbolsType::parse)
+ .unwrap_or(SymbolsType::Symbolic);
+ let symbols = Symbols::parse(context, input)?;
+ // There must be at least two symbols for alphabetic or
+ // numeric system.
+ if (symbols_type == SymbolsType::Alphabetic || symbols_type == SymbolsType::Numeric) &&
+ symbols.0.len() < 2
+ {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ // Identifier is not allowed in symbols() function.
+ if symbols.0.iter().any(|sym| !sym.is_allowed_in_symbols()) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(CounterStyle::Symbols(symbols_type, symbols))
+ })
+ }
+}
+
+impl SpecifiedValueInfo for CounterStyle {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // XXX The best approach for implementing this is probably
+ // having a CounterStyleName type wrapping CustomIdent, and
+ // put the predefined list for that type in counter_style mod.
+ // But that's a non-trivial change itself, so we use a simpler
+ // approach here.
+ macro_rules! predefined {
+ ($($name:expr,)+) => {
+ f(&["symbols", $($name,)+])
+ }
+ }
+ include!("../../counter_style/predefined.rs");
+ }
+}
+
+/// A wrapper of Non-negative values.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct NonNegative<T>(pub T);
+
+impl<T: Add<Output = T>> Add<NonNegative<T>> for NonNegative<T> {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ NonNegative(self.0 + other.0)
+ }
+}
+
+impl<T: Zero> Zero for NonNegative<T> {
+ fn is_zero(&self) -> bool {
+ self.0.is_zero()
+ }
+
+ fn zero() -> Self {
+ NonNegative(T::zero())
+ }
+}
+
+/// A wrapper of greater-than-or-equal-to-one values.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct GreaterThanOrEqualToOne<T>(pub T);
+
+/// A wrapper of values between zero and one.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct ZeroToOne<T>(pub T);
+
+/// A clip rect for clip and image-region
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "rect", comma)]
+#[repr(C)]
+pub struct GenericClipRect<LengthOrAuto> {
+ pub top: LengthOrAuto,
+ pub right: LengthOrAuto,
+ pub bottom: LengthOrAuto,
+ pub left: LengthOrAuto,
+}
+
+pub use self::GenericClipRect as ClipRect;
+
+/// Either a clip-rect or `auto`.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericClipRectOrAuto<R> {
+ Auto,
+ Rect(R),
+}
+
+pub use self::GenericClipRectOrAuto as ClipRectOrAuto;
+
+impl<L> ClipRectOrAuto<L> {
+ /// Returns the `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ ClipRectOrAuto::Auto
+ }
+
+ /// Returns whether this value is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, ClipRectOrAuto::Auto)
+ }
+}
+
+pub use page::PageSize;
+
+/// An optional value, much like `Option<T>`, but with a defined struct layout
+/// to be able to use it from C++ as well.
+///
+/// Note that this is relatively inefficient, struct-layout-wise, as you have
+/// one byte for the tag, but padding to the alignment of T. If you have
+/// multiple optional values and care about struct compactness, you might be
+/// better off "coalescing" the combinations into a parent enum. But that
+/// shouldn't matter for most use cases.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[repr(C, u8)]
+pub enum Optional<T> {
+ #[css(skip)]
+ None,
+ Some(T),
+}
+
+impl<T> Optional<T> {
+ /// Returns whether this value is present.
+ pub fn is_some(&self) -> bool {
+ matches!(*self, Self::Some(..))
+ }
+
+ /// Returns whether this value is not present.
+ pub fn is_none(&self) -> bool {
+ matches!(*self, Self::None)
+ }
+
+ /// Turns this Optional<> into a regular rust Option<>.
+ pub fn into_rust(self) -> Option<T> {
+ match self {
+ Self::Some(v) => Some(v),
+ Self::None => None,
+ }
+ }
+
+ /// Return a reference to the containing value, if any, as a plain rust
+ /// Option<>.
+ pub fn as_ref(&self) -> Option<&T> {
+ match *self {
+ Self::Some(ref v) => Some(v),
+ Self::None => None,
+ }
+ }
+}
+
+impl<T> From<Option<T>> for Optional<T> {
+ fn from(rust: Option<T>) -> Self {
+ match rust {
+ Some(t) => Self::Some(t),
+ None => Self::None,
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/motion.rs b/servo/components/style/values/generics/motion.rs
new file mode 100644
index 0000000000..ee6f5702da
--- /dev/null
+++ b/servo/components/style/values/generics/motion.rs
@@ -0,0 +1,270 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS Motion Path.
+
+use crate::values::animated::ToAnimatedZero;
+use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto};
+use crate::values::specified::motion::CoordBox;
+use serde::Deserializer;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// The <size> in ray() function.
+///
+/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-size
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum RaySize {
+ ClosestSide,
+ ClosestCorner,
+ FarthestSide,
+ FarthestCorner,
+ Sides,
+}
+
+/// The `ray()` function, `ray( [ <angle> && <size> && contain? && [at <position>]? ] )`
+///
+/// https://drafts.fxtf.org/motion-1/#valdef-offsetpath-ray
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericRayFunction<Angle, Position> {
+ /// The bearing angle with `0deg` pointing up and positive angles
+ /// representing clockwise rotation.
+ pub angle: Angle,
+ /// Decide the path length used when `offset-distance` is expressed
+ /// as a percentage.
+ pub size: RaySize,
+ /// Clamp `offset-distance` so that the box is entirely contained
+ /// within the path.
+ #[animation(constant)]
+ pub contain: bool,
+ /// The "at <position>" part. If omitted, we use auto to represent it.
+ pub position: GenericPositionOrAuto<Position>,
+}
+
+pub use self::GenericRayFunction as RayFunction;
+
+impl<Angle, Position> ToCss for RayFunction<Angle, Position>
+where
+ Angle: ToCss,
+ Position: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.angle.to_css(dest)?;
+
+ if !matches!(self.size, RaySize::ClosestSide) {
+ dest.write_char(' ')?;
+ self.size.to_css(dest)?;
+ }
+
+ if self.contain {
+ dest.write_str(" contain")?;
+ }
+
+ if !matches!(self.position, GenericPositionOrAuto::Auto) {
+ dest.write_str(" at ")?;
+ self.position.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+/// Return error if we try to deserialize the url, for Gecko IPC purposes.
+// Note: we cannot use #[serde(skip_deserializing)] variant attribute, which may cause the fatal
+// error when trying to read the parameters because it cannot deserialize the input byte buffer,
+// even if the type of OffsetPathFunction is not an url(), in our tests. This may be an issue of
+// #[serde(skip_deserializing)] on enum, at least in the version (1.0) we are using. So we have to
+// manually implement this deseriailzing function, but return error.
+// FIXME: Bug 1847620, fiure out this is a serde issue or a gecko bug.
+fn deserialize_url<'de, D, T>(_deserializer: D) -> Result<T, D::Error>
+where
+ D: Deserializer<'de>,
+{
+ use crate::serde::de::Error;
+ // Return Err() so the IPC will catch it and assert this as a fetal error.
+ Err(<D as Deserializer>::Error::custom(
+ "we don't support the deserializing for url",
+ ))
+}
+
+/// The <offset-path> value.
+/// <offset-path> = <ray()> | <url> | <basic-shape>
+///
+/// https://drafts.fxtf.org/motion-1/#typedef-offset-path
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(C, u8)]
+pub enum GenericOffsetPathFunction<Shapes, RayFunction, U> {
+ /// ray() function, which defines a path in the polar coordinate system.
+ /// Use Box<> to make sure the size of offset-path is not too large.
+ #[css(function)]
+ Ray(RayFunction),
+ /// A URL reference to an SVG shape element. If the URL does not reference a shape element,
+ /// this behaves as path("m 0 0") instead.
+ #[animation(error)]
+ #[serde(deserialize_with = "deserialize_url")]
+ #[serde(skip_serializing)]
+ Url(U),
+ /// The <basic-shape> value.
+ Shape(Shapes),
+}
+
+pub use self::GenericOffsetPathFunction as OffsetPathFunction;
+
+/// The offset-path property.
+/// offset-path: none | <offset-path> || <coord-box>
+///
+/// https://drafts.fxtf.org/motion-1/#offset-path-property
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericOffsetPath<Function> {
+ /// <offset-path> || <coord-box>.
+ OffsetPath {
+ /// <offset-path> part.
+ // Note: Use Box<> to make sure the size of this property doesn't go over the threshold.
+ path: Box<Function>,
+ /// <coord-box> part.
+ #[css(skip_if = "CoordBox::is_default")]
+ coord_box: CoordBox,
+ },
+ /// Only <coord-box>. This represents that <offset-path> is omitted, so we use the default
+ /// value, inset(0 round X), where X is the value of border-radius on the element that
+ /// establishes the containing block for this element.
+ CoordBox(CoordBox),
+ /// None value.
+ #[animation(error)]
+ None,
+}
+
+pub use self::GenericOffsetPath as OffsetPath;
+
+impl<Function> OffsetPath<Function> {
+ /// Return None.
+ #[inline]
+ pub fn none() -> Self {
+ OffsetPath::None
+ }
+}
+
+impl<Function> ToAnimatedZero for OffsetPath<Function> {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
+
+/// The offset-position property, which specifies the offset starting position that is used by the
+/// <offset-path> functions if they don’t specify their own starting position.
+///
+/// https://drafts.fxtf.org/motion-1/#offset-position-property
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericOffsetPosition<H, V> {
+ /// The element does not have an offset starting position.
+ Normal,
+ /// The offset starting position is the top-left corner of the box.
+ Auto,
+ /// The offset starting position is the result of using the <position> to position a 0x0 object
+ /// area within the box’s containing block.
+ Position(
+ #[css(field_bound)]
+ #[parse(field_bound)]
+ GenericPosition<H, V>,
+ ),
+}
+
+pub use self::GenericOffsetPosition as OffsetPosition;
+
+impl<H, V> OffsetPosition<H, V> {
+ /// Returns the initial value, normal.
+ #[inline]
+ pub fn normal() -> Self {
+ Self::Normal
+ }
+}
diff --git a/servo/components/style/values/generics/page.rs b/servo/components/style/values/generics/page.rs
new file mode 100644
index 0000000000..91f02bc4b3
--- /dev/null
+++ b/servo/components/style/values/generics/page.rs
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! @page at-rule properties
+
+use crate::values::generics::NonNegative;
+use crate::values::specified::length::AbsoluteLength;
+
+/// Page size names.
+///
+/// https://drafts.csswg.org/css-page-3/#typedef-page-size-page-size
+#[derive(
+ Clone, Copy, Debug, Eq, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum PaperSize {
+ /// ISO A5 media
+ A5,
+ /// ISO A4 media
+ A4,
+ /// ISO A3 media
+ A3,
+ /// ISO B5 media
+ B5,
+ /// ISO B4 media
+ B4,
+ /// JIS B5 media
+ JisB5,
+ /// JIS B4 media
+ JisB4,
+ /// North American Letter size
+ Letter,
+ /// North American Legal size
+ Legal,
+ /// North American Ledger size
+ Ledger,
+}
+
+impl PaperSize {
+ /// Gets the long edge length of the paper size
+ pub fn long_edge(&self) -> NonNegative<AbsoluteLength> {
+ NonNegative(match *self {
+ PaperSize::A5 => AbsoluteLength::Mm(210.0),
+ PaperSize::A4 => AbsoluteLength::Mm(297.0),
+ PaperSize::A3 => AbsoluteLength::Mm(420.0),
+ PaperSize::B5 => AbsoluteLength::Mm(250.0),
+ PaperSize::B4 => AbsoluteLength::Mm(353.0),
+ PaperSize::JisB5 => AbsoluteLength::Mm(257.0),
+ PaperSize::JisB4 => AbsoluteLength::Mm(364.0),
+ PaperSize::Letter => AbsoluteLength::In(11.0),
+ PaperSize::Legal => AbsoluteLength::In(14.0),
+ PaperSize::Ledger => AbsoluteLength::In(17.0),
+ })
+ }
+ /// Gets the short edge length of the paper size
+ pub fn short_edge(&self) -> NonNegative<AbsoluteLength> {
+ NonNegative(match *self {
+ PaperSize::A5 => AbsoluteLength::Mm(148.0),
+ PaperSize::A4 => AbsoluteLength::Mm(210.0),
+ PaperSize::A3 => AbsoluteLength::Mm(297.0),
+ PaperSize::B5 => AbsoluteLength::Mm(176.0),
+ PaperSize::B4 => AbsoluteLength::Mm(250.0),
+ PaperSize::JisB5 => AbsoluteLength::Mm(182.0),
+ PaperSize::JisB4 => AbsoluteLength::Mm(257.0),
+ PaperSize::Letter => AbsoluteLength::In(8.5),
+ PaperSize::Legal => AbsoluteLength::In(8.5),
+ PaperSize::Ledger => AbsoluteLength::In(11.0),
+ })
+ }
+}
+
+/// Page orientation names.
+///
+/// https://drafts.csswg.org/css-page-3/#page-orientation-prop
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum PageOrientation {
+ /// upright
+ Upright,
+ /// rotate-left (counter-clockwise)
+ RotateLeft,
+ /// rotate-right (clockwise)
+ RotateRight,
+}
+
+/// Paper orientation
+///
+/// https://drafts.csswg.org/css-page-3/#page-size-prop
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum PageSizeOrientation {
+ /// Portrait orientation
+ Portrait,
+ /// Landscape orientation
+ Landscape,
+}
+
+#[inline]
+fn is_portrait(orientation: &PageSizeOrientation) -> bool {
+ *orientation == PageSizeOrientation::Portrait
+}
+
+/// Page size property
+///
+/// https://drafts.csswg.org/css-page-3/#page-size-prop
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+#[repr(C, u8)]
+pub enum GenericPageSize<S> {
+ /// `auto` value.
+ Auto,
+ /// Page dimensions.
+ Size(S),
+ /// An orientation with no size.
+ Orientation(PageSizeOrientation),
+ /// Paper size by name
+ PaperSize(
+ PaperSize,
+ #[css(skip_if = "is_portrait")] PageSizeOrientation,
+ ),
+}
+
+pub use self::GenericPageSize as PageSize;
+
+impl<S> PageSize<S> {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ PageSize::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, PageSize::Auto)
+ }
+}
diff --git a/servo/components/style/values/generics/position.rs b/servo/components/style/values/generics/position.rs
new file mode 100644
index 0000000000..8a4a8c9e24
--- /dev/null
+++ b/servo/components/style/values/generics/position.rs
@@ -0,0 +1,238 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS handling of specified and computed values of
+//! [`position`](https://drafts.csswg.org/css-backgrounds-3/#position)
+
+use crate::values::animated::ToAnimatedZero;
+use crate::values::generics::ratio::Ratio;
+
+/// A generic type for representing a CSS [position](https://drafts.csswg.org/css-values/#position).
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericPosition<H, V> {
+ /// The horizontal component of position.
+ pub horizontal: H,
+ /// The vertical component of position.
+ pub vertical: V,
+}
+
+impl<H, V> PositionComponent for Position<H, V>
+where
+ H: PositionComponent,
+ V: PositionComponent,
+{
+ #[inline]
+ fn is_center(&self) -> bool {
+ self.horizontal.is_center() && self.vertical.is_center()
+ }
+}
+
+pub use self::GenericPosition as Position;
+
+impl<H, V> Position<H, V> {
+ /// Returns a new position.
+ pub fn new(horizontal: H, vertical: V) -> Self {
+ Self {
+ horizontal,
+ vertical,
+ }
+ }
+}
+
+/// Implements a method that checks if the position is centered.
+pub trait PositionComponent {
+ /// Returns if the position component is 50% or center.
+ /// For pixel lengths, it always returns false.
+ fn is_center(&self) -> bool;
+}
+
+/// A generic type for representing an `Auto | <position>`.
+/// This is used by <offset-anchor> for now.
+/// https://drafts.fxtf.org/motion-1/#offset-anchor-property
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericPositionOrAuto<Pos> {
+ /// The <position> value.
+ Position(Pos),
+ /// The keyword `auto`.
+ Auto,
+}
+
+pub use self::GenericPositionOrAuto as PositionOrAuto;
+
+impl<Pos> PositionOrAuto<Pos> {
+ /// Return `auto`.
+ #[inline]
+ pub fn auto() -> Self {
+ PositionOrAuto::Auto
+ }
+
+ /// Return true if it is 'auto'.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(self, PositionOrAuto::Auto)
+ }
+}
+
+/// A generic value for the `z-index` property.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericZIndex<I> {
+ /// An integer value.
+ Integer(I),
+ /// The keyword `auto`.
+ Auto,
+}
+
+pub use self::GenericZIndex as ZIndex;
+
+impl<Integer> ZIndex<Integer> {
+ /// Returns `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ ZIndex::Auto
+ }
+
+ /// Returns whether `self` is `auto`.
+ #[inline]
+ pub fn is_auto(self) -> bool {
+ matches!(self, ZIndex::Auto)
+ }
+
+ /// Returns the integer value if it is an integer, or `auto`.
+ #[inline]
+ pub fn integer_or(self, auto: Integer) -> Integer {
+ match self {
+ ZIndex::Integer(n) => n,
+ ZIndex::Auto => auto,
+ }
+ }
+}
+
+/// Ratio or None.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum PreferredRatio<N> {
+ /// Without specified ratio
+ #[css(skip)]
+ None,
+ /// With specified ratio
+ Ratio(
+ #[animation(field_bound)]
+ #[css(field_bound)]
+ #[distance(field_bound)]
+ Ratio<N>,
+ ),
+}
+
+/// A generic value for the `aspect-ratio` property, the value is `auto || <ratio>`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericAspectRatio<N> {
+ /// Specifiy auto or not.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub auto: bool,
+ /// The preferred aspect-ratio value.
+ #[animation(field_bound)]
+ #[css(field_bound)]
+ #[distance(field_bound)]
+ pub ratio: PreferredRatio<N>,
+}
+
+pub use self::GenericAspectRatio as AspectRatio;
+
+impl<N> AspectRatio<N> {
+ /// Returns `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ AspectRatio {
+ auto: true,
+ ratio: PreferredRatio::None,
+ }
+ }
+}
+
+impl<N> ToAnimatedZero for AspectRatio<N> {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ Err(())
+ }
+}
diff --git a/servo/components/style/values/generics/ratio.rs b/servo/components/style/values/generics/ratio.rs
new file mode 100644
index 0000000000..8c66fed602
--- /dev/null
+++ b/servo/components/style/values/generics/ratio.rs
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values related to <ratio>.
+//! https://drafts.csswg.org/css-values/#ratios
+
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic value for the `<ratio>` value.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Ratio<N>(pub N, pub N);
+
+impl<N> ToCss for Ratio<N>
+where
+ N: ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ // Even though 1 could be omitted, we don't per
+ // https://drafts.csswg.org/css-values-4/#ratio-value:
+ //
+ // The second <number> is optional, defaulting to 1. However,
+ // <ratio> is always serialized with both components.
+ //
+ // And for compat reasons, see bug 1669742.
+ //
+ // We serialize with spaces for consistency with all other
+ // slash-delimited things, see
+ // https://github.com/w3c/csswg-drafts/issues/4282
+ dest.write_str(" / ")?;
+ self.1.to_css(dest)?;
+ Ok(())
+ }
+}
diff --git a/servo/components/style/values/generics/rect.rs b/servo/components/style/values/generics/rect.rs
new file mode 100644
index 0000000000..ea9de67732
--- /dev/null
+++ b/servo/components/style/values/generics/rect.rs
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values that are composed of four sides.
+
+use crate::parser::{Parse, ParserContext};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A CSS value made of four components, where its `ToCss` impl will try to
+/// serialize as few components as possible, like for example in `border-width`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Rect<T>(pub T, pub T, pub T, pub T);
+
+impl<T> Rect<T> {
+ /// Returns a new `Rect<T>` value.
+ pub fn new(first: T, second: T, third: T, fourth: T) -> Self {
+ Rect(first, second, third, fourth)
+ }
+}
+
+impl<T> Rect<T>
+where
+ T: Clone,
+{
+ /// Returns a rect with all the values equal to `v`.
+ pub fn all(v: T) -> Self {
+ Rect::new(v.clone(), v.clone(), v.clone(), v)
+ }
+
+ /// Parses a new `Rect<T>` value with the given parse function.
+ pub fn parse_with<'i, 't, Parse>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse: Parse,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ Parse: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>,
+ {
+ let first = parse(context, input)?;
+ let second = if let Ok(second) = input.try_parse(|i| parse(context, i)) {
+ second
+ } else {
+ // <first>
+ return Ok(Self::new(
+ first.clone(),
+ first.clone(),
+ first.clone(),
+ first,
+ ));
+ };
+ let third = if let Ok(third) = input.try_parse(|i| parse(context, i)) {
+ third
+ } else {
+ // <first> <second>
+ return Ok(Self::new(first.clone(), second.clone(), first, second));
+ };
+ let fourth = if let Ok(fourth) = input.try_parse(|i| parse(context, i)) {
+ fourth
+ } else {
+ // <first> <second> <third>
+ return Ok(Self::new(first, second.clone(), third, second));
+ };
+ // <first> <second> <third> <fourth>
+ Ok(Self::new(first, second, third, fourth))
+ }
+
+ /// Parses a new `Rect<T>` value which all components must be specified, with the given parse
+ /// function.
+ pub fn parse_all_components_with<'i, 't, Parse>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse: Parse,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ Parse: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<T, ParseError<'i>>,
+ {
+ let first = parse(context, input)?;
+ let second = parse(context, input)?;
+ let third = parse(context, input)?;
+ let fourth = parse(context, input)?;
+ // <first> <second> <third> <fourth>
+ Ok(Self::new(first, second, third, fourth))
+ }
+}
+
+impl<T> Parse for Rect<T>
+where
+ T: Clone + Parse,
+{
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, T::parse)
+ }
+}
+
+impl<T> ToCss for Rect<T>
+where
+ T: PartialEq + ToCss,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ let same_vertical = self.0 == self.2;
+ let same_horizontal = self.1 == self.3;
+ if same_vertical && same_horizontal && self.0 == self.1 {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ self.1.to_css(dest)?;
+ if same_vertical && same_horizontal {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ self.2.to_css(dest)?;
+ if same_horizontal {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ self.3.to_css(dest)
+ }
+}
diff --git a/servo/components/style/values/generics/size.rs b/servo/components/style/values/generics/size.rs
new file mode 100644
index 0000000000..979b8f9322
--- /dev/null
+++ b/servo/components/style/values/generics/size.rs
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic type for CSS properties that are composed by two dimensions.
+
+use crate::parser::ParserContext;
+use crate::Zero;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A generic size, for `border-*-radius` longhand properties, or
+/// `border-spacing`.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ Serialize,
+ ToAnimatedZero,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(C)]
+pub struct Size2D<L> {
+ pub width: L,
+ pub height: L,
+}
+
+impl<L> Size2D<L> {
+ #[inline]
+ /// Create a new `Size2D` for an area of given width and height.
+ pub fn new(width: L, height: L) -> Self {
+ Self { width, height }
+ }
+
+ /// Returns the width component.
+ pub fn width(&self) -> &L {
+ &self.width
+ }
+
+ /// Returns the height component.
+ pub fn height(&self) -> &L {
+ &self.height
+ }
+
+ /// Parse a `Size2D` with a given parsing function.
+ pub fn parse_with<'i, 't, F>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse_one: F,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ L: Clone,
+ F: Fn(&ParserContext, &mut Parser<'i, 't>) -> Result<L, ParseError<'i>>,
+ {
+ let first = parse_one(context, input)?;
+ let second = input
+ .try_parse(|i| parse_one(context, i))
+ .unwrap_or_else(|_| first.clone());
+ Ok(Self::new(first, second))
+ }
+}
+
+impl<L> ToCss for Size2D<L>
+where
+ L: ToCss + PartialEq,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.width.to_css(dest)?;
+
+ if self.height != self.width {
+ dest.write_char(' ')?;
+ self.height.to_css(dest)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl<L: Zero> Zero for Size2D<L> {
+ fn zero() -> Self {
+ Self::new(L::zero(), L::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ self.width.is_zero() && self.height.is_zero()
+ }
+}
diff --git a/servo/components/style/values/generics/svg.rs b/servo/components/style/values/generics/svg.rs
new file mode 100644
index 0000000000..43ba77f1ff
--- /dev/null
+++ b/servo/components/style/values/generics/svg.rs
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values in SVG
+
+use crate::parser::{Parse, ParserContext};
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// The fallback of an SVG paint server value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGPaintFallback<C> {
+ /// The `none` keyword.
+ None,
+ /// A magic value that represents no fallback specified and serializes to
+ /// the empty string.
+ #[css(skip)]
+ Unset,
+ /// A color.
+ Color(C),
+}
+
+pub use self::GenericSVGPaintFallback as SVGPaintFallback;
+
+/// An SVG paint value
+///
+/// <https://www.w3.org/TR/SVG2/painting.html#SpecifyingPaint>
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(Url))]
+#[repr(C)]
+pub struct GenericSVGPaint<Color, Url> {
+ /// The paint source.
+ pub kind: GenericSVGPaintKind<Color, Url>,
+ /// The fallback color.
+ pub fallback: GenericSVGPaintFallback<Color>,
+}
+
+pub use self::GenericSVGPaint as SVGPaint;
+
+impl<C, U> Default for SVGPaint<C, U> {
+ fn default() -> Self {
+ Self {
+ kind: SVGPaintKind::None,
+ fallback: SVGPaintFallback::Unset,
+ }
+ }
+}
+
+/// An SVG paint value without the fallback.
+///
+/// Whereas the spec only allows PaintServer to have a fallback, Gecko lets the
+/// context properties have a fallback as well.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[animation(no_bound(U))]
+#[repr(C, u8)]
+pub enum GenericSVGPaintKind<C, U> {
+ /// `none`
+ #[animation(error)]
+ None,
+ /// `<color>`
+ Color(C),
+ /// `url(...)`
+ #[animation(error)]
+ PaintServer(U),
+ /// `context-fill`
+ ContextFill,
+ /// `context-stroke`
+ ContextStroke,
+}
+
+pub use self::GenericSVGPaintKind as SVGPaintKind;
+
+impl<C: Parse, U: Parse> Parse for SVGPaint<C, U> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let kind = SVGPaintKind::parse(context, input)?;
+ if matches!(kind, SVGPaintKind::None | SVGPaintKind::Color(..)) {
+ return Ok(SVGPaint {
+ kind,
+ fallback: SVGPaintFallback::Unset,
+ });
+ }
+ let fallback = input
+ .try_parse(|i| SVGPaintFallback::parse(context, i))
+ .unwrap_or(SVGPaintFallback::Unset);
+ Ok(SVGPaint { kind, fallback })
+ }
+}
+
+/// An SVG length value supports `context-value` in addition to length.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGLength<L> {
+ /// `<length> | <percentage> | <number>`
+ LengthPercentage(L),
+ /// `context-value`
+ #[animation(error)]
+ ContextValue,
+}
+
+pub use self::GenericSVGLength as SVGLength;
+
+/// Generic value for stroke-dasharray.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGStrokeDashArray<L> {
+ /// `[ <length> | <percentage> | <number> ]#`
+ #[css(comma)]
+ Values(#[css(if_empty = "none", iterable)] crate::OwnedSlice<L>),
+ /// `context-value`
+ ContextValue,
+}
+
+pub use self::GenericSVGStrokeDashArray as SVGStrokeDashArray;
+
+/// An SVG opacity value accepts `context-{fill,stroke}-opacity` in
+/// addition to opacity value.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericSVGOpacity<OpacityType> {
+ /// `<opacity-value>`
+ Opacity(OpacityType),
+ /// `context-fill-opacity`
+ #[animation(error)]
+ ContextFillOpacity,
+ /// `context-stroke-opacity`
+ #[animation(error)]
+ ContextStrokeOpacity,
+}
+
+pub use self::GenericSVGOpacity as SVGOpacity;
diff --git a/servo/components/style/values/generics/text.rs b/servo/components/style/values/generics/text.rs
new file mode 100644
index 0000000000..ef2647b014
--- /dev/null
+++ b/servo/components/style/values/generics/text.rs
@@ -0,0 +1,148 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for text properties.
+
+use crate::parser::ParserContext;
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// A generic value for the `initial-letter` property.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum InitialLetter<Number, Integer> {
+ /// `normal`
+ Normal,
+ /// `<number> <integer>?`
+ Specified(Number, Option<Integer>),
+}
+
+impl<N, I> InitialLetter<N, I> {
+ /// Returns `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ InitialLetter::Normal
+ }
+}
+
+/// A generic spacing value for the `letter-spacing` and `word-spacing` properties.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum Spacing<Value> {
+ /// `normal`
+ Normal,
+ /// `<value>`
+ Value(Value),
+}
+
+impl<Value> Spacing<Value> {
+ /// Returns `normal`.
+ #[inline]
+ pub fn normal() -> Self {
+ Spacing::Normal
+ }
+
+ /// Parses.
+ #[inline]
+ pub fn parse_with<'i, 't, F>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse: F,
+ ) -> Result<Self, ParseError<'i>>
+ where
+ F: FnOnce(&ParserContext, &mut Parser<'i, 't>) -> Result<Value, ParseError<'i>>,
+ {
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(Spacing::Normal);
+ }
+ parse(context, input).map(Spacing::Value)
+ }
+}
+
+/// Implements type for text-decoration-thickness
+/// which takes the grammar of auto | from-font | <length> | <percentage>
+///
+/// https://drafts.csswg.org/css-text-decor-4/
+#[repr(C, u8)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Animate,
+ Clone,
+ Copy,
+ ComputeSquaredDistance,
+ ToAnimatedZero,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum GenericTextDecorationLength<L> {
+ LengthPercentage(L),
+ Auto,
+ FromFont,
+}
+
+/// Implements type for text-indent
+/// which takes the grammar of [<length-percentage>] && hanging? && each-line?
+///
+/// https://drafts.csswg.org/css-text/#propdef-text-indent
+#[repr(C)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct GenericTextIndent<LengthPercentage> {
+ /// The amount of indent to be applied to the inline-start of the first line.
+ pub length: LengthPercentage,
+ /// Apply indent to non-first lines instead of first.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub hanging: bool,
+ /// Apply to each line after a hard break, not only first in block.
+ #[animation(constant)]
+ #[css(represents_keyword)]
+ pub each_line: bool,
+}
+
+impl<LengthPercentage: Zero> GenericTextIndent<LengthPercentage> {
+ /// Return the initial zero value.
+ pub fn zero() -> Self {
+ Self {
+ length: LengthPercentage::zero(),
+ hanging: false,
+ each_line: false,
+ }
+ }
+}
diff --git a/servo/components/style/values/generics/transform.rs b/servo/components/style/values/generics/transform.rs
new file mode 100644
index 0000000000..3a65c460a7
--- /dev/null
+++ b/servo/components/style/values/generics/transform.rs
@@ -0,0 +1,879 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for CSS values that are related to transformations.
+
+use crate::values::computed::length::Length as ComputedLength;
+use crate::values::computed::length::LengthPercentage as ComputedLengthPercentage;
+use crate::values::specified::angle::Angle as SpecifiedAngle;
+use crate::values::specified::length::Length as SpecifiedLength;
+use crate::values::specified::length::LengthPercentage as SpecifiedLengthPercentage;
+use crate::values::{computed, CSSFloat};
+use crate::{Zero, ZeroNoPercent};
+use euclid;
+use euclid::default::{Rect, Transform3D};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic 2D transformation matrix.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "matrix")]
+#[repr(C)]
+pub struct GenericMatrix<T> {
+ pub a: T,
+ pub b: T,
+ pub c: T,
+ pub d: T,
+ pub e: T,
+ pub f: T,
+}
+
+pub use self::GenericMatrix as Matrix;
+
+#[allow(missing_docs)]
+#[cfg_attr(rustfmt, rustfmt_skip)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma, function = "matrix3d")]
+#[repr(C)]
+pub struct GenericMatrix3D<T> {
+ pub m11: T, pub m12: T, pub m13: T, pub m14: T,
+ pub m21: T, pub m22: T, pub m23: T, pub m24: T,
+ pub m31: T, pub m32: T, pub m33: T, pub m34: T,
+ pub m41: T, pub m42: T, pub m43: T, pub m44: T,
+}
+
+pub use self::GenericMatrix3D as Matrix3D;
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl<T: Into<f64>> From<Matrix<T>> for Transform3D<f64> {
+ #[inline]
+ fn from(m: Matrix<T>) -> Self {
+ Transform3D::new(
+ m.a.into(), m.b.into(), 0.0, 0.0,
+ m.c.into(), m.d.into(), 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0,
+ m.e.into(), m.f.into(), 0.0, 1.0,
+ )
+ }
+}
+
+#[cfg_attr(rustfmt, rustfmt_skip)]
+impl<T: Into<f64>> From<Matrix3D<T>> for Transform3D<f64> {
+ #[inline]
+ fn from(m: Matrix3D<T>) -> Self {
+ Transform3D::new(
+ m.m11.into(), m.m12.into(), m.m13.into(), m.m14.into(),
+ m.m21.into(), m.m22.into(), m.m23.into(), m.m24.into(),
+ m.m31.into(), m.m32.into(), m.m33.into(), m.m34.into(),
+ m.m41.into(), m.m42.into(), m.m43.into(), m.m44.into(),
+ )
+ }
+}
+
+/// A generic transform origin.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericTransformOrigin<H, V, Depth> {
+ /// The horizontal origin.
+ pub horizontal: H,
+ /// The vertical origin.
+ pub vertical: V,
+ /// The depth.
+ pub depth: Depth,
+}
+
+pub use self::GenericTransformOrigin as TransformOrigin;
+
+impl<H, V, D> TransformOrigin<H, V, D> {
+ /// Returns a new transform origin.
+ pub fn new(horizontal: H, vertical: V, depth: D) -> Self {
+ Self {
+ horizontal,
+ vertical,
+ depth,
+ }
+ }
+}
+
+fn is_same<N: PartialEq>(x: &N, y: &N) -> bool {
+ x == y
+}
+
+/// A value for the `perspective()` transform function, which is either a
+/// non-negative `<length>` or `none`.
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericPerspectiveFunction<L> {
+ /// `none`
+ None,
+ /// A `<length>`.
+ Length(L),
+}
+
+impl<L> GenericPerspectiveFunction<L> {
+ /// Returns `f32::INFINITY` or the result of a function on the length value.
+ pub fn infinity_or(&self, f: impl FnOnce(&L) -> f32) -> f32 {
+ match *self {
+ Self::None => f32::INFINITY,
+ Self::Length(ref l) => f(l),
+ }
+ }
+}
+
+pub use self::GenericPerspectiveFunction as PerspectiveFunction;
+
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A single operation in the list of a `transform` value
+pub enum GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>
+where
+ Angle: Zero,
+ LengthPercentage: Zero + ZeroNoPercent,
+ Number: PartialEq,
+{
+ /// Represents a 2D 2x3 matrix.
+ Matrix(GenericMatrix<Number>),
+ /// Represents a 3D 4x4 matrix.
+ Matrix3D(GenericMatrix3D<Number>),
+ /// A 2D skew.
+ ///
+ /// If the second angle is not provided it is assumed zero.
+ ///
+ /// Syntax can be skew(angle) or skew(angle, angle)
+ #[css(comma, function)]
+ Skew(Angle, #[css(skip_if = "Zero::is_zero")] Angle),
+ /// skewX(angle)
+ #[css(function = "skewX")]
+ SkewX(Angle),
+ /// skewY(angle)
+ #[css(function = "skewY")]
+ SkewY(Angle),
+ /// translate(x, y) or translate(x)
+ #[css(comma, function)]
+ Translate(
+ LengthPercentage,
+ #[css(skip_if = "ZeroNoPercent::is_zero_no_percent")] LengthPercentage,
+ ),
+ /// translateX(x)
+ #[css(function = "translateX")]
+ TranslateX(LengthPercentage),
+ /// translateY(y)
+ #[css(function = "translateY")]
+ TranslateY(LengthPercentage),
+ /// translateZ(z)
+ #[css(function = "translateZ")]
+ TranslateZ(Length),
+ /// translate3d(x, y, z)
+ #[css(comma, function = "translate3d")]
+ Translate3D(LengthPercentage, LengthPercentage, Length),
+ /// A 2D scaling factor.
+ ///
+ /// Syntax can be scale(factor) or scale(factor, factor)
+ #[css(comma, function)]
+ Scale(Number, #[css(contextual_skip_if = "is_same")] Number),
+ /// scaleX(factor)
+ #[css(function = "scaleX")]
+ ScaleX(Number),
+ /// scaleY(factor)
+ #[css(function = "scaleY")]
+ ScaleY(Number),
+ /// scaleZ(factor)
+ #[css(function = "scaleZ")]
+ ScaleZ(Number),
+ /// scale3D(factorX, factorY, factorZ)
+ #[css(comma, function = "scale3d")]
+ Scale3D(Number, Number, Number),
+ /// Describes a 2D Rotation.
+ ///
+ /// In a 3D scene `rotate(angle)` is equivalent to `rotateZ(angle)`.
+ #[css(function)]
+ Rotate(Angle),
+ /// Rotation in 3D space around the x-axis.
+ #[css(function = "rotateX")]
+ RotateX(Angle),
+ /// Rotation in 3D space around the y-axis.
+ #[css(function = "rotateY")]
+ RotateY(Angle),
+ /// Rotation in 3D space around the z-axis.
+ #[css(function = "rotateZ")]
+ RotateZ(Angle),
+ /// Rotation in 3D space.
+ ///
+ /// Generalization of rotateX, rotateY and rotateZ.
+ #[css(comma, function = "rotate3d")]
+ Rotate3D(Number, Number, Number, Angle),
+ /// Specifies a perspective projection matrix.
+ ///
+ /// Part of CSS Transform Module Level 2 and defined at
+ /// [§ 13.1. 3D Transform Function](https://drafts.csswg.org/css-transforms-2/#funcdef-perspective).
+ ///
+ /// The value must be greater than or equal to zero.
+ #[css(function)]
+ Perspective(GenericPerspectiveFunction<Length>),
+ /// A intermediate type for interpolation of mismatched transform lists.
+ #[allow(missing_docs)]
+ #[css(comma, function = "interpolatematrix")]
+ InterpolateMatrix {
+ from_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ to_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ progress: computed::Percentage,
+ },
+ /// A intermediate type for accumulation of mismatched transform lists.
+ #[allow(missing_docs)]
+ #[css(comma, function = "accumulatematrix")]
+ AccumulateMatrix {
+ from_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ to_list: GenericTransform<
+ GenericTransformOperation<Angle, Number, Length, Integer, LengthPercentage>,
+ >,
+ count: Integer,
+ },
+}
+
+pub use self::GenericTransformOperation as TransformOperation;
+
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// A value of the `transform` property
+pub struct GenericTransform<T>(#[css(if_empty = "none", iterable)] pub crate::OwnedSlice<T>);
+
+pub use self::GenericTransform as Transform;
+
+impl<Angle, Number, Length, Integer, LengthPercentage>
+ TransformOperation<Angle, Number, Length, Integer, LengthPercentage>
+where
+ Angle: Zero,
+ LengthPercentage: Zero + ZeroNoPercent,
+ Number: PartialEq,
+{
+ /// Check if it is any rotate function.
+ pub fn is_rotate(&self) -> bool {
+ use self::TransformOperation::*;
+ matches!(
+ *self,
+ Rotate(..) | Rotate3D(..) | RotateX(..) | RotateY(..) | RotateZ(..)
+ )
+ }
+
+ /// Check if it is any translate function
+ pub fn is_translate(&self) -> bool {
+ use self::TransformOperation::*;
+ match *self {
+ Translate(..) | Translate3D(..) | TranslateX(..) | TranslateY(..) | TranslateZ(..) => {
+ true
+ },
+ _ => false,
+ }
+ }
+
+ /// Check if it is any scale function
+ pub fn is_scale(&self) -> bool {
+ use self::TransformOperation::*;
+ match *self {
+ Scale(..) | Scale3D(..) | ScaleX(..) | ScaleY(..) | ScaleZ(..) => true,
+ _ => false,
+ }
+ }
+}
+
+/// Convert a length type into the absolute lengths.
+pub trait ToAbsoluteLength {
+ /// Returns the absolute length as pixel value.
+ fn to_pixel_length(&self, containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()>;
+}
+
+impl ToAbsoluteLength for SpecifiedLength {
+ // This returns Err(()) if there is any relative length or percentage. We use this when
+ // parsing a transform list of DOMMatrix because we want to return a DOM Exception
+ // if there is relative length.
+ #[inline]
+ fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ match *self {
+ SpecifiedLength::NoCalc(len) => len.to_computed_pixel_length_without_context(),
+ SpecifiedLength::Calc(ref calc) => calc.to_computed_pixel_length_without_context(),
+ }
+ }
+}
+
+impl ToAbsoluteLength for SpecifiedLengthPercentage {
+ // This returns Err(()) if there is any relative length or percentage. We use this when
+ // parsing a transform list of DOMMatrix because we want to return a DOM Exception
+ // if there is relative length.
+ #[inline]
+ fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ use self::SpecifiedLengthPercentage::*;
+ match *self {
+ Length(len) => len.to_computed_pixel_length_without_context(),
+ Calc(ref calc) => calc.to_computed_pixel_length_without_context(),
+ Percentage(..) => Err(()),
+ }
+ }
+}
+
+impl ToAbsoluteLength for ComputedLength {
+ #[inline]
+ fn to_pixel_length(&self, _containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ Ok(self.px())
+ }
+}
+
+impl ToAbsoluteLength for ComputedLengthPercentage {
+ #[inline]
+ fn to_pixel_length(&self, containing_len: Option<ComputedLength>) -> Result<CSSFloat, ()> {
+ Ok(self
+ .maybe_percentage_relative_to(containing_len)
+ .ok_or(())?
+ .px())
+ }
+}
+
+/// Support the conversion to a 3d matrix.
+pub trait ToMatrix {
+ /// Check if it is a 3d transform function.
+ fn is_3d(&self) -> bool;
+
+ /// Return the equivalent 3d matrix.
+ fn to_3d_matrix(
+ &self,
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<Transform3D<f64>, ()>;
+}
+
+/// A little helper to deal with both specified and computed angles.
+pub trait ToRadians {
+ /// Return the radians value as a 64-bit floating point value.
+ fn radians64(&self) -> f64;
+}
+
+impl ToRadians for computed::angle::Angle {
+ #[inline]
+ fn radians64(&self) -> f64 {
+ computed::angle::Angle::radians64(self)
+ }
+}
+
+impl ToRadians for SpecifiedAngle {
+ #[inline]
+ fn radians64(&self) -> f64 {
+ computed::angle::Angle::from_degrees(self.degrees()).radians64()
+ }
+}
+
+impl<Angle, Number, Length, Integer, LoP> ToMatrix
+ for TransformOperation<Angle, Number, Length, Integer, LoP>
+where
+ Angle: Zero + ToRadians + Copy,
+ Number: PartialEq + Copy + Into<f32> + Into<f64>,
+ Length: ToAbsoluteLength,
+ LoP: Zero + ToAbsoluteLength + ZeroNoPercent,
+{
+ #[inline]
+ fn is_3d(&self) -> bool {
+ use self::TransformOperation::*;
+ match *self {
+ Translate3D(..) | TranslateZ(..) | Rotate3D(..) | RotateX(..) | RotateY(..) |
+ RotateZ(..) | Scale3D(..) | ScaleZ(..) | Perspective(..) | Matrix3D(..) => true,
+ _ => false,
+ }
+ }
+
+ /// If |reference_box| is None, we will drop the percent part from translate because
+ /// we cannot resolve it without the layout info, for computed TransformOperation.
+ /// However, for specified TransformOperation, we will return Err(()) if there is any relative
+ /// lengths because the only caller, DOMMatrix, doesn't accept relative lengths.
+ #[inline]
+ fn to_3d_matrix(
+ &self,
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<Transform3D<f64>, ()> {
+ use self::TransformOperation::*;
+
+ let reference_width = reference_box.map(|v| v.size.width);
+ let reference_height = reference_box.map(|v| v.size.height);
+ let matrix = match *self {
+ Rotate3D(ax, ay, az, theta) => {
+ let theta = theta.radians64();
+ let (ax, ay, az, theta) =
+ get_normalized_vector_and_angle(ax.into(), ay.into(), az.into(), theta);
+ Transform3D::rotation(
+ ax as f64,
+ ay as f64,
+ az as f64,
+ euclid::Angle::radians(theta),
+ )
+ },
+ RotateX(theta) => {
+ let theta = euclid::Angle::radians(theta.radians64());
+ Transform3D::rotation(1., 0., 0., theta)
+ },
+ RotateY(theta) => {
+ let theta = euclid::Angle::radians(theta.radians64());
+ Transform3D::rotation(0., 1., 0., theta)
+ },
+ RotateZ(theta) | Rotate(theta) => {
+ let theta = euclid::Angle::radians(theta.radians64());
+ Transform3D::rotation(0., 0., 1., theta)
+ },
+ Perspective(ref p) => {
+ let px = match p {
+ PerspectiveFunction::None => f32::INFINITY,
+ PerspectiveFunction::Length(ref p) => p.to_pixel_length(None)?,
+ };
+ create_perspective_matrix(px).cast()
+ },
+ Scale3D(sx, sy, sz) => Transform3D::scale(sx.into(), sy.into(), sz.into()),
+ Scale(sx, sy) => Transform3D::scale(sx.into(), sy.into(), 1.),
+ ScaleX(s) => Transform3D::scale(s.into(), 1., 1.),
+ ScaleY(s) => Transform3D::scale(1., s.into(), 1.),
+ ScaleZ(s) => Transform3D::scale(1., 1., s.into()),
+ Translate3D(ref tx, ref ty, ref tz) => {
+ let tx = tx.to_pixel_length(reference_width)? as f64;
+ let ty = ty.to_pixel_length(reference_height)? as f64;
+ Transform3D::translation(tx, ty, tz.to_pixel_length(None)? as f64)
+ },
+ Translate(ref tx, ref ty) => {
+ let tx = tx.to_pixel_length(reference_width)? as f64;
+ let ty = ty.to_pixel_length(reference_height)? as f64;
+ Transform3D::translation(tx, ty, 0.)
+ },
+ TranslateX(ref t) => {
+ let t = t.to_pixel_length(reference_width)? as f64;
+ Transform3D::translation(t, 0., 0.)
+ },
+ TranslateY(ref t) => {
+ let t = t.to_pixel_length(reference_height)? as f64;
+ Transform3D::translation(0., t, 0.)
+ },
+ TranslateZ(ref z) => Transform3D::translation(0., 0., z.to_pixel_length(None)? as f64),
+ Skew(theta_x, theta_y) => Transform3D::skew(
+ euclid::Angle::radians(theta_x.radians64()),
+ euclid::Angle::radians(theta_y.radians64()),
+ ),
+ SkewX(theta) => Transform3D::skew(
+ euclid::Angle::radians(theta.radians64()),
+ euclid::Angle::radians(0.),
+ ),
+ SkewY(theta) => Transform3D::skew(
+ euclid::Angle::radians(0.),
+ euclid::Angle::radians(theta.radians64()),
+ ),
+ Matrix3D(m) => m.into(),
+ Matrix(m) => m.into(),
+ InterpolateMatrix { .. } | AccumulateMatrix { .. } => {
+ // TODO: Convert InterpolateMatrix/AccumulateMatrix into a valid Transform3D by
+ // the reference box and do interpolation on these two Transform3D matrices.
+ // Both Gecko and Servo don't support this for computing distance, and Servo
+ // doesn't support animations on InterpolateMatrix/AccumulateMatrix, so
+ // return an identity matrix.
+ // Note: DOMMatrix doesn't go into this arm.
+ Transform3D::identity()
+ },
+ };
+ Ok(matrix)
+ }
+}
+
+impl<T> Transform<T> {
+ /// `none`
+ pub fn none() -> Self {
+ Transform(Default::default())
+ }
+}
+
+impl<T: ToMatrix> Transform<T> {
+ /// Return the equivalent 3d matrix of this transform list.
+ ///
+ /// We return a pair: the first one is the transform matrix, and the second one
+ /// indicates if there is any 3d transform function in this transform list.
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ pub fn to_transform_3d_matrix(
+ &self,
+ reference_box: Option<&Rect<ComputedLength>>
+ ) -> Result<(Transform3D<CSSFloat>, bool), ()> {
+ Self::components_to_transform_3d_matrix(&self.0, reference_box)
+ }
+
+ /// Converts a series of components to a 3d matrix.
+ #[cfg_attr(rustfmt, rustfmt_skip)]
+ pub fn components_to_transform_3d_matrix(
+ ops: &[T],
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<(Transform3D<CSSFloat>, bool), ()> {
+ let cast_3d_transform = |m: Transform3D<f64>| -> Transform3D<CSSFloat> {
+ use std::{f32, f64};
+ let cast = |v: f64| v.min(f32::MAX as f64).max(f32::MIN as f64) as f32;
+ Transform3D::new(
+ cast(m.m11), cast(m.m12), cast(m.m13), cast(m.m14),
+ cast(m.m21), cast(m.m22), cast(m.m23), cast(m.m24),
+ cast(m.m31), cast(m.m32), cast(m.m33), cast(m.m34),
+ cast(m.m41), cast(m.m42), cast(m.m43), cast(m.m44),
+ )
+ };
+
+ let (m, is_3d) = Self::components_to_transform_3d_matrix_f64(ops, reference_box)?;
+ Ok((cast_3d_transform(m), is_3d))
+ }
+
+ /// Same as Transform::to_transform_3d_matrix but a f64 version.
+ fn components_to_transform_3d_matrix_f64(
+ ops: &[T],
+ reference_box: Option<&Rect<ComputedLength>>,
+ ) -> Result<(Transform3D<f64>, bool), ()> {
+ // We intentionally use Transform3D<f64> during computation to avoid
+ // error propagation because using f32 to compute triangle functions
+ // (e.g. in rotation()) is not accurate enough. In Gecko, we also use
+ // "double" to compute the triangle functions. Therefore, let's use
+ // Transform3D<f64> during matrix computation and cast it into f32 in
+ // the end.
+ let mut transform = Transform3D::<f64>::identity();
+ let mut contain_3d = false;
+
+ for operation in ops {
+ let matrix = operation.to_3d_matrix(reference_box)?;
+ contain_3d = contain_3d || operation.is_3d();
+ transform = matrix.then(&transform);
+ }
+
+ Ok((transform, contain_3d))
+ }
+}
+
+/// Return the transform matrix from a perspective length.
+#[inline]
+pub fn create_perspective_matrix(d: CSSFloat) -> Transform3D<CSSFloat> {
+ if d.is_finite() {
+ Transform3D::perspective(d.max(1.))
+ } else {
+ Transform3D::identity()
+ }
+}
+
+/// Return the normalized direction vector and its angle for Rotate3D.
+pub fn get_normalized_vector_and_angle<T: Zero>(
+ x: CSSFloat,
+ y: CSSFloat,
+ z: CSSFloat,
+ angle: T,
+) -> (CSSFloat, CSSFloat, CSSFloat, T) {
+ use crate::values::computed::transform::DirectionVector;
+ use euclid::approxeq::ApproxEq;
+ let vector = DirectionVector::new(x, y, z);
+ if vector.square_length().approx_eq(&f32::zero()) {
+ // https://www.w3.org/TR/css-transforms-1/#funcdef-rotate3d
+ // A direction vector that cannot be normalized, such as [0, 0, 0], will cause the
+ // rotation to not be applied, so we use identity matrix (i.e. rotate3d(0, 0, 1, 0)).
+ (0., 0., 1., T::zero())
+ } else {
+ let vector = vector.robust_normalize();
+ (vector.x, vector.y, vector.z, angle)
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A value of the `Rotate` property
+///
+/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms>
+pub enum GenericRotate<Number, Angle> {
+ /// 'none'
+ None,
+ /// '<angle>'
+ Rotate(Angle),
+ /// '<number>{3} <angle>'
+ Rotate3D(Number, Number, Number, Angle),
+}
+
+pub use self::GenericRotate as Rotate;
+
+/// A trait to check if the current 3D vector is parallel to the DirectionVector.
+/// This is especially for serialization on Rotate.
+pub trait IsParallelTo {
+ /// Returns true if this is parallel to the vector.
+ fn is_parallel_to(&self, vector: &computed::transform::DirectionVector) -> bool;
+}
+
+impl<Number, Angle> ToCss for Rotate<Number, Angle>
+where
+ Number: Copy + ToCss + Zero,
+ Angle: ToCss,
+ (Number, Number, Number): IsParallelTo,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use crate::values::computed::transform::DirectionVector;
+ match *self {
+ Rotate::None => dest.write_str("none"),
+ Rotate::Rotate(ref angle) => angle.to_css(dest),
+ Rotate::Rotate3D(x, y, z, ref angle) => {
+ // If the axis is parallel with the x or y axes, it must serialize as the
+ // appropriate keyword. If a rotation about the z axis (that is, in 2D) is
+ // specified, the property must serialize as just an <angle>
+ //
+ // https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization
+ let v = (x, y, z);
+ let axis = if x.is_zero() && y.is_zero() && z.is_zero() {
+ // The zero length vector is parallel to every other vector, so
+ // is_parallel_to() returns true for it. However, it is definitely different
+ // from x axis, y axis, or z axis, and it's meaningless to perform a rotation
+ // using that direction vector. So we *have* to serialize it using that same
+ // vector - we can't simplify to some theoretically parallel axis-aligned
+ // vector.
+ None
+ } else if v.is_parallel_to(&DirectionVector::new(1., 0., 0.)) {
+ Some("x ")
+ } else if v.is_parallel_to(&DirectionVector::new(0., 1., 0.)) {
+ Some("y ")
+ } else if v.is_parallel_to(&DirectionVector::new(0., 0., 1.)) {
+ // When we're parallel to the z-axis, we can just serialize the angle.
+ return angle.to_css(dest);
+ } else {
+ None
+ };
+ match axis {
+ Some(a) => dest.write_str(a)?,
+ None => {
+ x.to_css(dest)?;
+ dest.write_char(' ')?;
+ y.to_css(dest)?;
+ dest.write_char(' ')?;
+ z.to_css(dest)?;
+ dest.write_char(' ')?;
+ },
+ }
+ angle.to_css(dest)
+ },
+ }
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A value of the `Scale` property
+///
+/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms>
+pub enum GenericScale<Number> {
+ /// 'none'
+ None,
+ /// '<number>{1,3}'
+ Scale(Number, Number, Number),
+}
+
+pub use self::GenericScale as Scale;
+
+impl<Number> ToCss for Scale<Number>
+where
+ Number: ToCss + PartialEq + Copy,
+ f32: From<Number>,
+{
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ f32: From<Number>,
+ {
+ match *self {
+ Scale::None => dest.write_str("none"),
+ Scale::Scale(ref x, ref y, ref z) => {
+ x.to_css(dest)?;
+
+ let is_3d = f32::from(*z) != 1.0;
+ if is_3d || x != y {
+ dest.write_char(' ')?;
+ y.to_css(dest)?;
+ }
+
+ if is_3d {
+ dest.write_char(' ')?;
+ z.to_css(dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+#[inline]
+fn y_axis_and_z_axis_are_zero<LengthPercentage: Zero + ZeroNoPercent, Length: Zero>(
+ _: &LengthPercentage,
+ y: &LengthPercentage,
+ z: &Length,
+) -> bool {
+ y.is_zero_no_percent() && z.is_zero()
+}
+
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// A value of the `translate` property
+///
+/// https://drafts.csswg.org/css-transforms-2/#individual-transform-serialization:
+///
+/// If a 2d translation is specified, the property must serialize with only one
+/// or two values (per usual, if the second value is 0px, the default, it must
+/// be omitted when serializing; however if 0% is the second value, it is included).
+///
+/// If a 3d translation is specified and the value can be expressed as 2d, we treat as 2d and
+/// serialize accoringly. Otherwise, we serialize all three values.
+/// https://github.com/w3c/csswg-drafts/issues/3305
+///
+/// <https://drafts.csswg.org/css-transforms-2/#individual-transforms>
+pub enum GenericTranslate<LengthPercentage, Length>
+where
+ LengthPercentage: Zero + ZeroNoPercent,
+ Length: Zero,
+{
+ /// 'none'
+ None,
+ /// <length-percentage> [ <length-percentage> <length>? ]?
+ Translate(
+ LengthPercentage,
+ #[css(contextual_skip_if = "y_axis_and_z_axis_are_zero")] LengthPercentage,
+ #[css(skip_if = "Zero::is_zero")] Length,
+ ),
+}
+
+pub use self::GenericTranslate as Translate;
+
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TransformStyle {
+ Flat,
+ #[css(keyword = "preserve-3d")]
+ Preserve3d,
+}
diff --git a/servo/components/style/values/generics/ui.rs b/servo/components/style/values/generics/ui.rs
new file mode 100644
index 0000000000..87c8674182
--- /dev/null
+++ b/servo/components/style/values/generics/ui.rs
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic values for UI properties.
+
+use crate::values::specified::ui::CursorKind;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ToCss};
+
+/// A generic value for the `cursor` property.
+///
+/// https://drafts.csswg.org/css-ui/#cursor
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct GenericCursor<Image> {
+ /// The parsed images for the cursor.
+ pub images: crate::OwnedSlice<Image>,
+ /// The kind of the cursor [default | help | ...].
+ pub keyword: CursorKind,
+}
+
+pub use self::GenericCursor as Cursor;
+
+impl<Image> Cursor<Image> {
+ /// Set `cursor` to `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ Self {
+ images: Default::default(),
+ keyword: CursorKind::Auto,
+ }
+ }
+}
+
+impl<Image: ToCss> ToCss for Cursor<Image> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ for image in &*self.images {
+ image.to_css(dest)?;
+ dest.write_str(", ")?;
+ }
+ self.keyword.to_css(dest)
+ }
+}
+
+/// A generic value for item of `image cursors`.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct GenericCursorImage<Image, Number> {
+ /// The url to parse images from.
+ pub image: Image,
+ /// Whether the image has a hotspot or not.
+ pub has_hotspot: bool,
+ /// The x coordinate.
+ pub hotspot_x: Number,
+ /// The y coordinate.
+ pub hotspot_y: Number,
+}
+
+pub use self::GenericCursorImage as CursorImage;
+
+impl<Image: ToCss, Number: ToCss> ToCss for CursorImage<Image, Number> {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.image.to_css(dest)?;
+ if self.has_hotspot {
+ dest.write_char(' ')?;
+ self.hotspot_x.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.hotspot_y.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// A generic value for `scrollbar-color` property.
+///
+/// https://drafts.csswg.org/css-scrollbars-1/#scrollbar-color
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericScrollbarColor<Color> {
+ /// `auto`
+ Auto,
+ /// `<color>{2}`
+ Colors {
+ /// First `<color>`, for color of the scrollbar thumb.
+ thumb: Color,
+ /// Second `<color>`, for color of the scrollbar track.
+ track: Color,
+ },
+}
+
+pub use self::GenericScrollbarColor as ScrollbarColor;
+
+impl<Color> Default for ScrollbarColor<Color> {
+ #[inline]
+ fn default() -> Self {
+ ScrollbarColor::Auto
+ }
+}
diff --git a/servo/components/style/values/generics/url.rs b/servo/components/style/values/generics/url.rs
new file mode 100644
index 0000000000..46ed453e82
--- /dev/null
+++ b/servo/components/style/values/generics/url.rs
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Generic types for url properties.
+
+/// An image url or none, used for example in list-style-image
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum GenericUrlOrNone<U> {
+ /// `none`
+ None,
+ /// A URL.
+ Url(U),
+}
+
+pub use self::GenericUrlOrNone as UrlOrNone;
+
+impl<Url> UrlOrNone<Url> {
+ /// Initial "none" value for properties such as `list-style-image`
+ pub fn none() -> Self {
+ UrlOrNone::None
+ }
+
+ /// Returns whether the value is `none`.
+ pub fn is_none(&self) -> bool {
+ match *self {
+ UrlOrNone::None => true,
+ UrlOrNone::Url(..) => false,
+ }
+ }
+}
diff --git a/servo/components/style/values/mod.rs b/servo/components/style/values/mod.rs
new file mode 100644
index 0000000000..6138d5a2ab
--- /dev/null
+++ b/servo/components/style/values/mod.rs
@@ -0,0 +1,796 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Common [values][values] used in CSS.
+//!
+//! [values]: https://drafts.csswg.org/css-values/
+
+#![deny(missing_docs)]
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::Atom;
+pub use cssparser::{serialize_identifier, serialize_name, CowRcStr, Parser};
+pub use cssparser::{SourceLocation, Token};
+use precomputed_hash::PrecomputedHash;
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Debug, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+use to_shmem::impl_trivial_to_shmem;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::url::CssUrl;
+#[cfg(feature = "servo")]
+pub use crate::servo::url::CssUrl;
+
+pub mod animated;
+pub mod computed;
+pub mod distance;
+pub mod generics;
+pub mod resolved;
+pub mod specified;
+
+/// A CSS float value.
+pub type CSSFloat = f32;
+
+/// Normalizes a float value to zero after a set of operations that might turn
+/// it into NaN.
+#[inline]
+pub fn normalize(v: CSSFloat) -> CSSFloat {
+ if v.is_nan() {
+ 0.0
+ } else {
+ v
+ }
+}
+
+/// A CSS integer value.
+pub type CSSInteger = i32;
+
+/// Serialize an identifier which is represented as an atom.
+#[cfg(feature = "gecko")]
+pub fn serialize_atom_identifier<W>(ident: &Atom, dest: &mut W) -> fmt::Result
+where
+ W: Write,
+{
+ ident.with_str(|s| serialize_identifier(s, dest))
+}
+
+/// Serialize an identifier which is represented as an atom.
+#[cfg(feature = "servo")]
+pub fn serialize_atom_identifier<Static, W>(
+ ident: &::string_cache::Atom<Static>,
+ dest: &mut W,
+) -> fmt::Result
+where
+ Static: string_cache::StaticAtomSet,
+ W: Write,
+{
+ serialize_identifier(&ident, dest)
+}
+
+/// Serialize a name which is represented as an Atom.
+#[cfg(feature = "gecko")]
+pub fn serialize_atom_name<W>(ident: &Atom, dest: &mut W) -> fmt::Result
+where
+ W: Write,
+{
+ ident.with_str(|s| serialize_name(s, dest))
+}
+
+/// Serialize a name which is represented as an Atom.
+#[cfg(feature = "servo")]
+pub fn serialize_atom_name<Static, W>(
+ ident: &::string_cache::Atom<Static>,
+ dest: &mut W,
+) -> fmt::Result
+where
+ Static: string_cache::StaticAtomSet,
+ W: Write,
+{
+ serialize_name(&ident, dest)
+}
+
+/// Serialize a number with calc, and NaN/infinity handling (if enabled)
+pub fn serialize_number<W>(v: f32, was_calc: bool, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ serialize_specified_dimension(v, "", was_calc, dest)
+}
+
+/// Serialize a specified dimension with unit, calc, and NaN/infinity handling (if enabled)
+pub fn serialize_specified_dimension<W>(
+ v: f32,
+ unit: &str,
+ was_calc: bool,
+ dest: &mut CssWriter<W>,
+) -> fmt::Result
+where
+ W: Write,
+{
+ if was_calc {
+ dest.write_str("calc(")?;
+ }
+
+ if !v.is_finite() {
+ // https://drafts.csswg.org/css-values/#calc-error-constants:
+ // "While not technically numbers, these keywords act as numeric values,
+ // similar to e and pi. Thus to get an infinite length, for example,
+ // requires an expression like calc(infinity * 1px)."
+
+ if v.is_nan() {
+ dest.write_str("NaN")?;
+ } else if v == f32::INFINITY {
+ dest.write_str("infinity")?;
+ } else if v == f32::NEG_INFINITY {
+ dest.write_str("-infinity")?;
+ }
+
+ if !unit.is_empty() {
+ dest.write_str(" * 1")?;
+ }
+ } else {
+ v.to_css(dest)?;
+ }
+
+ dest.write_str(unit)?;
+
+ if was_calc {
+ dest.write_char(')')?;
+ }
+ Ok(())
+}
+
+/// A CSS string stored as an `Atom`.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Deref,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct AtomString(pub Atom);
+
+#[cfg(feature = "servo")]
+impl AsRef<str> for AtomString {
+ fn as_ref(&self) -> &str {
+ &*self.0
+ }
+}
+
+impl cssparser::ToCss for AtomString {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: Write,
+ {
+ // Wrap in quotes to form a string literal
+ dest.write_char('"')?;
+ #[cfg(feature = "servo")]
+ {
+ cssparser::CssStringWriter::new(dest).write_str(self.as_ref())?;
+ }
+ #[cfg(feature = "gecko")]
+ {
+ self.0
+ .with_str(|s| cssparser::CssStringWriter::new(dest).write_str(s))?;
+ }
+ dest.write_char('"')
+ }
+}
+
+impl style_traits::ToCss for AtomString {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ cssparser::ToCss::to_css(self, dest)
+ }
+}
+
+impl PrecomputedHash for AtomString {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.0.precomputed_hash()
+ }
+}
+
+impl<'a> From<&'a str> for AtomString {
+ #[inline]
+ fn from(string: &str) -> Self {
+ Self(Atom::from(string))
+ }
+}
+
+/// A generic CSS `<ident>` stored as an `Atom`.
+#[cfg(feature = "servo")]
+#[repr(transparent)]
+#[derive(Deref)]
+pub struct GenericAtomIdent<Set>(pub string_cache::Atom<Set>)
+where
+ Set: string_cache::StaticAtomSet;
+
+/// A generic CSS `<ident>` stored as an `Atom`, for the default atom set.
+#[cfg(feature = "servo")]
+pub type AtomIdent = GenericAtomIdent<servo_atoms::AtomStaticSet>;
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> style_traits::SpecifiedValueInfo for GenericAtomIdent<Set> {}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> Default for GenericAtomIdent<Set> {
+ fn default() -> Self {
+ Self(string_cache::Atom::default())
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> std::fmt::Debug for GenericAtomIdent<Set> {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> std::hash::Hash for GenericAtomIdent<Set> {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.0.hash(state)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> Eq for GenericAtomIdent<Set> {}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> PartialEq for GenericAtomIdent<Set> {
+ fn eq(&self, other: &Self) -> bool {
+ self.0 == other.0
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> Clone for GenericAtomIdent<Set> {
+ fn clone(&self) -> Self {
+ Self(self.0.clone())
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> to_shmem::ToShmem for GenericAtomIdent<Set> {
+ fn to_shmem(&self, builder: &mut to_shmem::SharedMemoryBuilder) -> to_shmem::Result<Self> {
+ use std::mem::ManuallyDrop;
+
+ let atom = self.0.to_shmem(builder)?;
+ Ok(ManuallyDrop::new(Self(ManuallyDrop::into_inner(atom))))
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> malloc_size_of::MallocSizeOf for GenericAtomIdent<Set> {
+ fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
+ self.0.size_of(ops)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> cssparser::ToCss for GenericAtomIdent<Set> {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> PrecomputedHash for GenericAtomIdent<Set> {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.0.precomputed_hash()
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<'a, Set: string_cache::StaticAtomSet> From<&'a str> for GenericAtomIdent<Set> {
+ #[inline]
+ fn from(string: &str) -> Self {
+ Self(string_cache::Atom::from(string))
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> std::borrow::Borrow<string_cache::Atom<Set>>
+ for GenericAtomIdent<Set>
+{
+ #[inline]
+ fn borrow(&self) -> &string_cache::Atom<Set> {
+ &self.0
+ }
+}
+
+#[cfg(feature = "servo")]
+impl<Set: string_cache::StaticAtomSet> GenericAtomIdent<Set> {
+ /// Constructs a new GenericAtomIdent.
+ #[inline]
+ pub fn new(atom: string_cache::Atom<Set>) -> Self {
+ Self(atom)
+ }
+
+ /// Cast an atom ref to an AtomIdent ref.
+ #[inline]
+ pub fn cast<'a>(atom: &'a string_cache::Atom<Set>) -> &'a Self {
+ let ptr = atom as *const _ as *const Self;
+ // safety: repr(transparent)
+ unsafe { &*ptr }
+ }
+}
+
+/// A CSS `<ident>` stored as an `Atom`.
+#[cfg(feature = "gecko")]
+#[repr(transparent)]
+#[derive(
+ Clone, Debug, Default, Deref, Eq, Hash, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem,
+)]
+pub struct AtomIdent(pub Atom);
+
+#[cfg(feature = "gecko")]
+impl cssparser::ToCss for AtomIdent {
+ fn to_css<W>(&self, dest: &mut W) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl style_traits::ToCss for AtomIdent {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ cssparser::ToCss::to_css(self, dest)
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl PrecomputedHash for AtomIdent {
+ #[inline]
+ fn precomputed_hash(&self) -> u32 {
+ self.0.precomputed_hash()
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl<'a> From<&'a str> for AtomIdent {
+ #[inline]
+ fn from(string: &str) -> Self {
+ Self(Atom::from(string))
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl AtomIdent {
+ /// Constructs a new AtomIdent.
+ #[inline]
+ pub fn new(atom: Atom) -> Self {
+ Self(atom)
+ }
+
+ /// Like `Atom::with` but for `AtomIdent`.
+ pub unsafe fn with<F, R>(ptr: *const crate::gecko_bindings::structs::nsAtom, callback: F) -> R
+ where
+ F: FnOnce(&Self) -> R,
+ {
+ Atom::with(ptr, |atom: &Atom| {
+ // safety: repr(transparent)
+ let atom = atom as *const Atom as *const AtomIdent;
+ callback(&*atom)
+ })
+ }
+
+ /// Cast an atom ref to an AtomIdent ref.
+ #[inline]
+ pub fn cast<'a>(atom: &'a Atom) -> &'a Self {
+ let ptr = atom as *const _ as *const Self;
+ // safety: repr(transparent)
+ unsafe { &*ptr }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl std::borrow::Borrow<crate::gecko_string_cache::WeakAtom> for AtomIdent {
+ #[inline]
+ fn borrow(&self) -> &crate::gecko_string_cache::WeakAtom {
+ self.0.borrow()
+ }
+}
+
+/// Serialize a value into percentage.
+pub fn serialize_percentage<W>(value: CSSFloat, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ serialize_specified_dimension(value * 100., "%", /* was_calc = */ false, dest)
+}
+
+/// Serialize a value into normalized (no NaN/inf serialization) percentage.
+pub fn serialize_normalized_percentage<W>(value: CSSFloat, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ (value * 100.).to_css(dest)?;
+ dest.write_char('%')
+}
+
+/// Convenience void type to disable some properties and values through types.
+#[cfg_attr(feature = "servo", derive(Deserialize, MallocSizeOf, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+)]
+pub enum Impossible {}
+
+// FIXME(nox): This should be derived but the derive code cannot cope
+// with uninhabited enums.
+impl ComputeSquaredDistance for Impossible {
+ #[inline]
+ fn compute_squared_distance(&self, _other: &Self) -> Result<SquaredDistance, ()> {
+ match *self {}
+ }
+}
+
+impl_trivial_to_shmem!(Impossible);
+
+impl Parse for Impossible {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+/// A struct representing one of two kinds of values.
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum Either<A, B> {
+ /// The first value.
+ First(A),
+ /// The second kind of value.
+ Second(B),
+}
+
+impl<A: Debug, B: Debug> Debug for Either<A, B> {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ Either::First(ref v) => v.fmt(f),
+ Either::Second(ref v) => v.fmt(f),
+ }
+ }
+}
+
+/// <https://drafts.csswg.org/css-values-4/#custom-idents>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct CustomIdent(pub Atom);
+
+impl CustomIdent {
+ /// Parse a <custom-ident>
+ ///
+ /// TODO(zrhoffman, bug 1844501): Use CustomIdent::parse in more places instead of
+ /// CustomIdent::from_ident.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ invalid: &[&str],
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ CustomIdent::from_ident(location, ident, invalid)
+ }
+
+ /// Parse an already-tokenizer identifier
+ pub fn from_ident<'i>(
+ location: SourceLocation,
+ ident: &CowRcStr<'i>,
+ excluding: &[&str],
+ ) -> Result<Self, ParseError<'i>> {
+ if !Self::is_valid(ident, excluding) {
+ return Err(
+ location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))
+ );
+ }
+ if excluding.iter().any(|s| ident.eq_ignore_ascii_case(s)) {
+ Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(CustomIdent(Atom::from(ident.as_ref())))
+ }
+ }
+
+ fn is_valid(ident: &str, excluding: &[&str]) -> bool {
+ use crate::properties::CSSWideKeyword;
+ // https://drafts.csswg.org/css-values-4/#custom-idents:
+ //
+ // The CSS-wide keywords are not valid <custom-ident>s. The default
+ // keyword is reserved and is also not a valid <custom-ident>.
+ if CSSWideKeyword::from_ident(ident).is_ok() || ident.eq_ignore_ascii_case("default") {
+ return false;
+ }
+
+ // https://drafts.csswg.org/css-values-4/#custom-idents:
+ //
+ // Excluded keywords are excluded in all ASCII case permutations.
+ !excluding.iter().any(|s| ident.eq_ignore_ascii_case(s))
+ }
+}
+
+impl ToCss for CustomIdent {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+/// <https://www.w3.org/TR/css-values-4/#dashed-idents>
+/// This is simply an Atom, but will only parse if the identifier starts with "--".
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct DashedIdent(pub Atom);
+
+impl Parse for DashedIdent {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ if ident.starts_with("--") {
+ Ok(Self(Atom::from(ident.as_ref())))
+ } else {
+ Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())))
+ }
+ }
+}
+
+impl ToCss for DashedIdent {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+/// The <timeline-name> or <keyframes-name>.
+/// The definition of these two names are the same, so we use the same type for them.
+///
+/// <https://drafts.csswg.org/css-animations-2/#typedef-timeline-name>
+/// <https://drafts.csswg.org/css-animations/#typedef-keyframes-name>
+///
+/// We use a single atom for these. Empty atom represents `none` animation.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Hash,
+ PartialEq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct TimelineOrKeyframesName(Atom);
+
+impl TimelineOrKeyframesName {
+ /// <https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name>
+ pub fn from_ident(value: &str) -> Self {
+ Self(Atom::from(value))
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(atom!(""))
+ }
+
+ /// Returns whether this is the special `none` value.
+ pub fn is_none(&self) -> bool {
+ self.0 == atom!("")
+ }
+
+ /// Create a new TimelineOrKeyframesName from Atom.
+ #[cfg(feature = "gecko")]
+ pub fn from_atom(atom: Atom) -> Self {
+ Self(atom)
+ }
+
+ /// The name as an Atom
+ pub fn as_atom(&self) -> &Atom {
+ &self.0
+ }
+
+ fn parse<'i, 't>(input: &mut Parser<'i, 't>, invalid: &[&str]) -> Result<Self, ParseError<'i>> {
+ debug_assert!(invalid.contains(&"none"));
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::Ident(ref s) => Self(CustomIdent::from_ident(location, s, invalid)?.0),
+ Token::QuotedString(ref s) => Self(Atom::from(s.as_ref())),
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, invalid: &[&str]) -> fmt::Result
+ where
+ W: Write,
+ {
+ debug_assert!(invalid.contains(&"none"));
+
+ if self.0 == atom!("") {
+ return dest.write_str("none");
+ }
+
+ self.0.with_str(|s| {
+ if CustomIdent::is_valid(s, invalid) {
+ serialize_identifier(s, dest)
+ } else {
+ s.to_css(dest)
+ }
+ })
+ }
+}
+
+impl Eq for TimelineOrKeyframesName {}
+
+/// The typedef of <timeline-name>.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Deref,
+ Hash,
+ Eq,
+ PartialEq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct TimelineName(TimelineOrKeyframesName);
+
+impl TimelineName {
+ /// Create a new TimelineName from Atom.
+ #[cfg(feature = "gecko")]
+ pub fn from_atom(atom: Atom) -> Self {
+ Self(TimelineOrKeyframesName::from_atom(atom))
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(TimelineOrKeyframesName::none())
+ }
+}
+
+impl Parse for TimelineName {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self(TimelineOrKeyframesName::parse(
+ input,
+ &["none", "auto"],
+ )?))
+ }
+}
+
+impl ToCss for TimelineName {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest, &["none", "auto"])
+ }
+}
+
+/// The typedef of <keyframes-name>.
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ Deref,
+ Hash,
+ Eq,
+ PartialEq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct KeyframesName(TimelineOrKeyframesName);
+
+impl KeyframesName {
+ /// Create a new KeyframesName from Atom.
+ #[cfg(feature = "gecko")]
+ pub fn from_atom(atom: Atom) -> Self {
+ Self(TimelineOrKeyframesName::from_atom(atom))
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ Self(TimelineOrKeyframesName::none())
+ }
+}
+
+impl Parse for KeyframesName {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self(TimelineOrKeyframesName::parse(input, &["none"])?))
+ }
+}
+
+impl ToCss for KeyframesName {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest, &["none"])
+ }
+}
diff --git a/servo/components/style/values/resolved/color.rs b/servo/components/style/values/resolved/color.rs
new file mode 100644
index 0000000000..79dfd8685f
--- /dev/null
+++ b/servo/components/style/values/resolved/color.rs
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Resolved color values.
+
+use super::{Context, ToResolvedValue};
+
+use crate::color::AbsoluteColor;
+use crate::values::computed::color as computed;
+use crate::values::generics::color as generics;
+
+impl ToResolvedValue for computed::Color {
+ // A resolved color value is an rgba color, with currentcolor resolved.
+ type ResolvedValue = AbsoluteColor;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ context.style.resolve_color(self)
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ generics::Color::Absolute(resolved)
+ }
+}
+
+impl ToResolvedValue for computed::CaretColor {
+ // A resolved caret-color value is an rgba color, with auto resolving to
+ // currentcolor.
+ type ResolvedValue = AbsoluteColor;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ let color = match self.0 {
+ generics::ColorOrAuto::Color(color) => color,
+ generics::ColorOrAuto::Auto => generics::Color::currentcolor(),
+ };
+ color.to_resolved_value(context)
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ generics::CaretColor(generics::ColorOrAuto::Color(
+ computed::Color::from_resolved_value(resolved),
+ ))
+ }
+}
diff --git a/servo/components/style/values/resolved/counters.rs b/servo/components/style/values/resolved/counters.rs
new file mode 100644
index 0000000000..c1332449ad
--- /dev/null
+++ b/servo/components/style/values/resolved/counters.rs
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Resolved values for counter properties
+
+use super::{Context, ToResolvedValue};
+use crate::values::computed;
+
+/// https://drafts.csswg.org/css-content/#content-property
+///
+/// We implement this at resolved value time because otherwise it causes us to
+/// allocate a bunch of useless initial structs for ::before / ::after, which is
+/// a bit unfortunate.
+///
+/// Though these should be temporary, mostly, so if this causes complexity in
+/// other places, it should be fine to move to `StyleAdjuster`.
+///
+/// See https://github.com/w3c/csswg-drafts/issues/4632 for where some related
+/// issues are being discussed.
+impl ToResolvedValue for computed::Content {
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self {
+ let (is_pseudo, is_before_or_after, is_marker) = match context.style.pseudo() {
+ Some(ref pseudo) => (true, pseudo.is_before_or_after(), pseudo.is_marker()),
+ None => (false, false, false),
+ };
+ match self {
+ Self::Normal if is_before_or_after => Self::None,
+ // For now, make `content: none` compute to `normal` for pseudos
+ // other than ::before, ::after and ::marker, as we don't respect it.
+ // https://github.com/w3c/csswg-drafts/issues/6124
+ // Ditto for non-pseudo elements if the pref is disabled.
+ Self::None
+ if (is_pseudo && !is_before_or_after && !is_marker) ||
+ (!is_pseudo &&
+ !static_prefs::pref!("layout.css.element-content-none.enabled")) =>
+ {
+ Self::Normal
+ },
+ other => other,
+ }
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self) -> Self {
+ resolved
+ }
+}
diff --git a/servo/components/style/values/resolved/mod.rs b/servo/components/style/values/resolved/mod.rs
new file mode 100644
index 0000000000..675f3cca68
--- /dev/null
+++ b/servo/components/style/values/resolved/mod.rs
@@ -0,0 +1,275 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Resolved values. These are almost always computed values, but in some cases
+//! there are used values.
+
+use crate::media_queries::Device;
+use crate::properties::ComputedValues;
+use crate::ArcSlice;
+use servo_arc::Arc;
+use smallvec::SmallVec;
+
+mod color;
+mod counters;
+
+use crate::values::computed;
+
+/// Element-specific information needed to resolve property values.
+pub struct ResolvedElementInfo<'a> {
+ /// Element we're resolving line-height against.
+ #[cfg(feature = "gecko")]
+ pub element: crate::gecko::wrapper::GeckoElement<'a>,
+}
+
+/// Information needed to resolve a given value.
+pub struct Context<'a> {
+ /// The style we're resolving for. This is useful to resolve currentColor.
+ pub style: &'a ComputedValues,
+ /// The device / document we're resolving style for. Useful to do font metrics stuff needed for
+ /// line-height.
+ pub device: &'a Device,
+ /// The element-specific information to resolve the value.
+ pub element_info: ResolvedElementInfo<'a>,
+}
+
+/// A trait to represent the conversion between resolved and resolved values.
+///
+/// This trait is derivable with `#[derive(ToResolvedValue)]`.
+///
+/// The deriving code assumes that if the type isn't generic, then the trait can
+/// be implemented as simple move. This means that a manual implementation with
+/// `ResolvedValue = Self` is bogus if it returns anything else than a clone.
+pub trait ToResolvedValue {
+ /// The resolved value type we're going to be converted to.
+ type ResolvedValue;
+
+ /// Convert a resolved value to a resolved value.
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue;
+
+ /// Convert a resolved value to resolved value form.
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self;
+}
+
+macro_rules! trivial_to_resolved_value {
+ ($ty:ty) => {
+ impl $crate::values::resolved::ToResolvedValue for $ty {
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, _: &Context) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved
+ }
+ }
+ };
+}
+
+trivial_to_resolved_value!(());
+trivial_to_resolved_value!(bool);
+trivial_to_resolved_value!(f32);
+trivial_to_resolved_value!(u8);
+trivial_to_resolved_value!(i8);
+trivial_to_resolved_value!(u16);
+trivial_to_resolved_value!(i16);
+trivial_to_resolved_value!(u32);
+trivial_to_resolved_value!(i32);
+trivial_to_resolved_value!(usize);
+trivial_to_resolved_value!(String);
+trivial_to_resolved_value!(Box<str>);
+trivial_to_resolved_value!(crate::OwnedStr);
+trivial_to_resolved_value!(crate::color::AbsoluteColor);
+trivial_to_resolved_value!(crate::values::generics::color::ColorMixFlags);
+trivial_to_resolved_value!(crate::Atom);
+trivial_to_resolved_value!(crate::values::AtomIdent);
+trivial_to_resolved_value!(app_units::Au);
+trivial_to_resolved_value!(computed::url::ComputedUrl);
+#[cfg(feature = "gecko")]
+trivial_to_resolved_value!(computed::url::ComputedImageUrl);
+#[cfg(feature = "servo")]
+trivial_to_resolved_value!(crate::Namespace);
+#[cfg(feature = "servo")]
+trivial_to_resolved_value!(crate::Prefix);
+trivial_to_resolved_value!(computed::LengthPercentage);
+trivial_to_resolved_value!(style_traits::values::specified::AllowedNumericType);
+trivial_to_resolved_value!(computed::TimingFunction);
+
+impl<A, B> ToResolvedValue for (A, B)
+where
+ A: ToResolvedValue,
+ B: ToResolvedValue,
+{
+ type ResolvedValue = (
+ <A as ToResolvedValue>::ResolvedValue,
+ <B as ToResolvedValue>::ResolvedValue,
+ );
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ (
+ self.0.to_resolved_value(context),
+ self.1.to_resolved_value(context),
+ )
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ (
+ A::from_resolved_value(resolved.0),
+ B::from_resolved_value(resolved.1),
+ )
+ }
+}
+
+impl<T> ToResolvedValue for Option<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Option<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.map(|item| item.to_resolved_value(context))
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved.map(T::from_resolved_value)
+ }
+}
+
+impl<T> ToResolvedValue for SmallVec<[T; 1]>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = SmallVec<[<T as ToResolvedValue>::ResolvedValue; 1]>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.into_iter()
+ .map(|item| item.to_resolved_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved.into_iter().map(T::from_resolved_value).collect()
+ }
+}
+
+impl<T> ToResolvedValue for Vec<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Vec<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.into_iter()
+ .map(|item| item.to_resolved_value(context))
+ .collect()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ resolved.into_iter().map(T::from_resolved_value).collect()
+ }
+}
+
+impl<T> ToResolvedValue for Box<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Box<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ Box::new(T::to_resolved_value(*self, context))
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ Box::new(T::from_resolved_value(*resolved))
+ }
+}
+
+impl<T> ToResolvedValue for Box<[T]>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = Box<[<T as ToResolvedValue>::ResolvedValue]>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ Vec::from(self)
+ .to_resolved_value(context)
+ .into_boxed_slice()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ Vec::from_resolved_value(Vec::from(resolved)).into_boxed_slice()
+ }
+}
+
+impl<T> ToResolvedValue for crate::OwnedSlice<T>
+where
+ T: ToResolvedValue,
+{
+ type ResolvedValue = crate::OwnedSlice<<T as ToResolvedValue>::ResolvedValue>;
+
+ #[inline]
+ fn to_resolved_value(self, context: &Context) -> Self::ResolvedValue {
+ self.into_box().to_resolved_value(context).into()
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self::ResolvedValue) -> Self {
+ Self::from(Box::from_resolved_value(resolved.into_box()))
+ }
+}
+
+// NOTE(emilio): This is implementable more generically, but it's unlikely what
+// you want there, as it forces you to have an extra allocation.
+//
+// We could do that if needed, ideally with specialization for the case where
+// ResolvedValue = T. But we don't need it for now.
+impl<T> ToResolvedValue for Arc<T>
+where
+ T: ToResolvedValue<ResolvedValue = T>,
+{
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, _: &Context) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self) -> Self {
+ resolved
+ }
+}
+
+// Same caveat as above applies.
+impl<T> ToResolvedValue for ArcSlice<T>
+where
+ T: ToResolvedValue<ResolvedValue = T>,
+{
+ type ResolvedValue = Self;
+
+ #[inline]
+ fn to_resolved_value(self, _: &Context) -> Self {
+ self
+ }
+
+ #[inline]
+ fn from_resolved_value(resolved: Self) -> Self {
+ resolved
+ }
+}
diff --git a/servo/components/style/values/specified/align.rs b/servo/components/style/values/specified/align.rs
new file mode 100644
index 0000000000..60eca4556b
--- /dev/null
+++ b/servo/components/style/values/specified/align.rs
@@ -0,0 +1,820 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Values for CSS Box Alignment properties
+//!
+//! https://drafts.csswg.org/css-align/
+
+use crate::parser::{Parse, ParserContext};
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss};
+
+/// Constants shared by multiple CSS Box Alignment properties
+#[derive(
+ Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(C)]
+pub struct AlignFlags(u8);
+bitflags! {
+ impl AlignFlags: u8 {
+ // Enumeration stored in the lower 5 bits:
+ /// {align,justify}-{content,items,self}: 'auto'
+ const AUTO = 0;
+ /// 'normal'
+ const NORMAL = 1;
+ /// 'start'
+ const START = 2;
+ /// 'end'
+ const END = 3;
+ /// 'flex-start'
+ const FLEX_START = 4;
+ /// 'flex-end'
+ const FLEX_END = 5;
+ /// 'center'
+ const CENTER = 6;
+ /// 'left'
+ const LEFT = 7;
+ /// 'right'
+ const RIGHT = 8;
+ /// 'baseline'
+ const BASELINE = 9;
+ /// 'last-baseline'
+ const LAST_BASELINE = 10;
+ /// 'stretch'
+ const STRETCH = 11;
+ /// 'self-start'
+ const SELF_START = 12;
+ /// 'self-end'
+ const SELF_END = 13;
+ /// 'space-between'
+ const SPACE_BETWEEN = 14;
+ /// 'space-around'
+ const SPACE_AROUND = 15;
+ /// 'space-evenly'
+ const SPACE_EVENLY = 16;
+
+ // Additional flags stored in the upper bits:
+ /// 'legacy' (mutually exclusive w. SAFE & UNSAFE)
+ const LEGACY = 1 << 5;
+ /// 'safe'
+ const SAFE = 1 << 6;
+ /// 'unsafe' (mutually exclusive w. SAFE)
+ const UNSAFE = 1 << 7;
+
+ /// Mask for the additional flags above.
+ const FLAG_BITS = 0b11100000;
+ }
+}
+
+impl AlignFlags {
+ /// Returns the enumeration value stored in the lower 5 bits.
+ #[inline]
+ fn value(&self) -> Self {
+ *self & !AlignFlags::FLAG_BITS
+ }
+}
+
+impl ToCss for AlignFlags {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ let extra_flags = *self & AlignFlags::FLAG_BITS;
+ let value = self.value();
+
+ match extra_flags {
+ AlignFlags::LEGACY => {
+ dest.write_str("legacy")?;
+ if value.is_empty() {
+ return Ok(());
+ }
+ dest.write_char(' ')?;
+ },
+ AlignFlags::SAFE => dest.write_str("safe ")?,
+ AlignFlags::UNSAFE => dest.write_str("unsafe ")?,
+ _ => {
+ debug_assert_eq!(extra_flags, AlignFlags::empty());
+ },
+ }
+
+ dest.write_str(match value {
+ AlignFlags::AUTO => "auto",
+ AlignFlags::NORMAL => "normal",
+ AlignFlags::START => "start",
+ AlignFlags::END => "end",
+ AlignFlags::FLEX_START => "flex-start",
+ AlignFlags::FLEX_END => "flex-end",
+ AlignFlags::CENTER => "center",
+ AlignFlags::LEFT => "left",
+ AlignFlags::RIGHT => "right",
+ AlignFlags::BASELINE => "baseline",
+ AlignFlags::LAST_BASELINE => "last baseline",
+ AlignFlags::STRETCH => "stretch",
+ AlignFlags::SELF_START => "self-start",
+ AlignFlags::SELF_END => "self-end",
+ AlignFlags::SPACE_BETWEEN => "space-between",
+ AlignFlags::SPACE_AROUND => "space-around",
+ AlignFlags::SPACE_EVENLY => "space-evenly",
+ _ => unreachable!(),
+ })
+ }
+}
+
+/// An axis direction, either inline (for the `justify` properties) or block,
+/// (for the `align` properties).
+#[derive(Clone, Copy, PartialEq)]
+pub enum AxisDirection {
+ /// Block direction.
+ Block,
+ /// Inline direction.
+ Inline,
+}
+
+/// Shared value for the `align-content` and `justify-content` properties.
+///
+/// <https://drafts.csswg.org/css-align/#content-distribution>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+pub struct ContentDistribution {
+ primary: AlignFlags,
+ // FIXME(https://github.com/w3c/csswg-drafts/issues/1002): This will need to
+ // accept fallback alignment, eventually.
+}
+
+impl ContentDistribution {
+ /// The initial value 'normal'
+ #[inline]
+ pub fn normal() -> Self {
+ Self::new(AlignFlags::NORMAL)
+ }
+
+ /// `start`
+ #[inline]
+ pub fn start() -> Self {
+ Self::new(AlignFlags::START)
+ }
+
+ /// The initial value 'normal'
+ #[inline]
+ pub fn new(primary: AlignFlags) -> Self {
+ Self { primary }
+ }
+
+ /// Returns whether this value is a <baseline-position>.
+ pub fn is_baseline_position(&self) -> bool {
+ matches!(
+ self.primary.value(),
+ AlignFlags::BASELINE | AlignFlags::LAST_BASELINE
+ )
+ }
+
+ /// The primary alignment
+ #[inline]
+ pub fn primary(self) -> AlignFlags {
+ self.primary
+ }
+
+ /// Parse a value for align-content / justify-content.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ axis: AxisDirection,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update the `list_keywords` function below
+ // when this function is updated.
+
+ // Try to parse normal first
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(ContentDistribution::normal());
+ }
+
+ // Parse <baseline-position>, but only on the block axis.
+ if axis == AxisDirection::Block {
+ if let Ok(value) = input.try_parse(parse_baseline) {
+ return Ok(ContentDistribution::new(value));
+ }
+ }
+
+ // <content-distribution>
+ if let Ok(value) = input.try_parse(parse_content_distribution) {
+ return Ok(ContentDistribution::new(value));
+ }
+
+ // <overflow-position>? <content-position>
+ let overflow_position = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+
+ let content_position = try_match_ident_ignore_ascii_case! { input,
+ "start" => AlignFlags::START,
+ "end" => AlignFlags::END,
+ "flex-start" => AlignFlags::FLEX_START,
+ "flex-end" => AlignFlags::FLEX_END,
+ "center" => AlignFlags::CENTER,
+ "left" if axis == AxisDirection::Inline => AlignFlags::LEFT,
+ "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT,
+ };
+
+ Ok(ContentDistribution::new(
+ content_position | overflow_position,
+ ))
+ }
+
+ fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
+ f(&["normal"]);
+ if axis == AxisDirection::Block {
+ list_baseline_keywords(f);
+ }
+ list_content_distribution_keywords(f);
+ list_overflow_position_keywords(f);
+ f(&["start", "end", "flex-start", "flex-end", "center"]);
+ if axis == AxisDirection::Inline {
+ f(&["left", "right"]);
+ }
+ }
+}
+
+/// Value for the `align-content` property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-align-content>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct AlignContent(pub ContentDistribution);
+
+impl Parse for AlignContent {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(AlignContent(ContentDistribution::parse(
+ input,
+ AxisDirection::Block,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for AlignContent {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ ContentDistribution::list_keywords(f, AxisDirection::Block);
+ }
+}
+
+/// Value for the `align-tracks` property.
+///
+/// <https://github.com/w3c/csswg-drafts/issues/4650>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+#[css(comma)]
+pub struct AlignTracks(#[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<AlignContent>);
+
+impl Parse for AlignTracks {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let values = input.parse_comma_separated(|input| AlignContent::parse(context, input))?;
+ Ok(AlignTracks(values.into()))
+ }
+}
+
+/// Value for the `justify-content` property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-justify-content>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct JustifyContent(pub ContentDistribution);
+
+impl Parse for JustifyContent {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(JustifyContent(ContentDistribution::parse(
+ input,
+ AxisDirection::Inline,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for JustifyContent {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ ContentDistribution::list_keywords(f, AxisDirection::Inline);
+ }
+}
+/// Value for the `justify-tracks` property.
+///
+/// <https://github.com/w3c/csswg-drafts/issues/4650>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+#[css(comma)]
+pub struct JustifyTracks(
+ #[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<JustifyContent>,
+);
+
+impl Parse for JustifyTracks {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let values = input.parse_comma_separated(|input| JustifyContent::parse(context, input))?;
+ Ok(JustifyTracks(values.into()))
+ }
+}
+
+/// <https://drafts.csswg.org/css-align/#self-alignment>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct SelfAlignment(pub AlignFlags);
+
+impl SelfAlignment {
+ /// The initial value 'auto'
+ #[inline]
+ pub fn auto() -> Self {
+ SelfAlignment(AlignFlags::AUTO)
+ }
+
+ /// Returns whether this value is valid for both axis directions.
+ pub fn is_valid_on_both_axes(&self) -> bool {
+ match self.0.value() {
+ // left | right are only allowed on the inline axis.
+ AlignFlags::LEFT | AlignFlags::RIGHT => false,
+
+ _ => true,
+ }
+ }
+
+ /// Parse a self-alignment value on one of the axis.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ axis: AxisDirection,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update the `list_keywords` function below
+ // when this function is updated.
+
+ // <baseline-position>
+ //
+ // It's weird that this accepts <baseline-position>, but not
+ // justify-content...
+ if let Ok(value) = input.try_parse(parse_baseline) {
+ return Ok(SelfAlignment(value));
+ }
+
+ // auto | normal | stretch
+ if let Ok(value) = input.try_parse(parse_auto_normal_stretch) {
+ return Ok(SelfAlignment(value));
+ }
+
+ // <overflow-position>? <self-position>
+ let overflow_position = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+ let self_position = parse_self_position(input, axis)?;
+ Ok(SelfAlignment(overflow_position | self_position))
+ }
+
+ fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
+ list_baseline_keywords(f);
+ list_auto_normal_stretch(f);
+ list_overflow_position_keywords(f);
+ list_self_position_keywords(f, axis);
+ }
+}
+
+/// The specified value of the align-self property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-align-self>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct AlignSelf(pub SelfAlignment);
+
+impl Parse for AlignSelf {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(AlignSelf(SelfAlignment::parse(
+ input,
+ AxisDirection::Block,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for AlignSelf {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ SelfAlignment::list_keywords(f, AxisDirection::Block);
+ }
+}
+
+/// The specified value of the justify-self property.
+///
+/// <https://drafts.csswg.org/css-align/#propdef-justify-self>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct JustifySelf(pub SelfAlignment);
+
+impl Parse for JustifySelf {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+ Ok(JustifySelf(SelfAlignment::parse(
+ input,
+ AxisDirection::Inline,
+ )?))
+ }
+}
+
+impl SpecifiedValueInfo for JustifySelf {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ SelfAlignment::list_keywords(f, AxisDirection::Inline);
+ }
+}
+
+/// Value of the `align-items` property
+///
+/// <https://drafts.csswg.org/css-align/#propdef-align-items>
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct AlignItems(pub AlignFlags);
+
+impl AlignItems {
+ /// The initial value 'normal'
+ #[inline]
+ pub fn normal() -> Self {
+ AlignItems(AlignFlags::NORMAL)
+ }
+}
+
+impl Parse for AlignItems {
+ // normal | stretch | <baseline-position> |
+ // <overflow-position>? <self-position>
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+
+ // <baseline-position>
+ if let Ok(baseline) = input.try_parse(parse_baseline) {
+ return Ok(AlignItems(baseline));
+ }
+
+ // normal | stretch
+ if let Ok(value) = input.try_parse(parse_normal_stretch) {
+ return Ok(AlignItems(value));
+ }
+ // <overflow-position>? <self-position>
+ let overflow = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+ let self_position = parse_self_position(input, AxisDirection::Block)?;
+ Ok(AlignItems(self_position | overflow))
+ }
+}
+
+impl SpecifiedValueInfo for AlignItems {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ list_baseline_keywords(f);
+ list_normal_stretch(f);
+ list_overflow_position_keywords(f);
+ list_self_position_keywords(f, AxisDirection::Block);
+ }
+}
+
+/// Value of the `justify-items` property
+///
+/// <https://drafts.csswg.org/css-align/#justify-items-property>
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)]
+#[repr(C)]
+pub struct JustifyItems(pub AlignFlags);
+
+impl JustifyItems {
+ /// The initial value 'legacy'
+ #[inline]
+ pub fn legacy() -> Self {
+ JustifyItems(AlignFlags::LEGACY)
+ }
+
+ /// The value 'normal'
+ #[inline]
+ pub fn normal() -> Self {
+ JustifyItems(AlignFlags::NORMAL)
+ }
+}
+
+impl Parse for JustifyItems {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // NOTE Please also update `impl SpecifiedValueInfo` below when
+ // this function is updated.
+
+ // <baseline-position>
+ //
+ // It's weird that this accepts <baseline-position>, but not
+ // justify-content...
+ if let Ok(baseline) = input.try_parse(parse_baseline) {
+ return Ok(JustifyItems(baseline));
+ }
+
+ // normal | stretch
+ if let Ok(value) = input.try_parse(parse_normal_stretch) {
+ return Ok(JustifyItems(value));
+ }
+
+ // legacy | [ legacy && [ left | right | center ] ]
+ if let Ok(value) = input.try_parse(parse_legacy) {
+ return Ok(JustifyItems(value));
+ }
+
+ // <overflow-position>? <self-position>
+ let overflow = input
+ .try_parse(parse_overflow_position)
+ .unwrap_or(AlignFlags::empty());
+ let self_position = parse_self_position(input, AxisDirection::Inline)?;
+ Ok(JustifyItems(overflow | self_position))
+ }
+}
+
+impl SpecifiedValueInfo for JustifyItems {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ list_baseline_keywords(f);
+ list_normal_stretch(f);
+ list_legacy_keywords(f);
+ list_overflow_position_keywords(f);
+ list_self_position_keywords(f, AxisDirection::Inline);
+ }
+}
+
+// auto | normal | stretch
+fn parse_auto_normal_stretch<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_auto_normal_stretch` function
+ // below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "auto" => Ok(AlignFlags::AUTO),
+ "normal" => Ok(AlignFlags::NORMAL),
+ "stretch" => Ok(AlignFlags::STRETCH),
+ }
+}
+
+fn list_auto_normal_stretch(f: KeywordsCollectFn) {
+ f(&["auto", "normal", "stretch"]);
+}
+
+// normal | stretch
+fn parse_normal_stretch<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_normal_stretch` function below
+ // when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "normal" => Ok(AlignFlags::NORMAL),
+ "stretch" => Ok(AlignFlags::STRETCH),
+ }
+}
+
+fn list_normal_stretch(f: KeywordsCollectFn) {
+ f(&["normal", "stretch"]);
+}
+
+// <baseline-position>
+fn parse_baseline<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_baseline_keywords` function
+ // below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "baseline" => Ok(AlignFlags::BASELINE),
+ "first" => {
+ input.expect_ident_matching("baseline")?;
+ Ok(AlignFlags::BASELINE)
+ },
+ "last" => {
+ input.expect_ident_matching("baseline")?;
+ Ok(AlignFlags::LAST_BASELINE)
+ },
+ }
+}
+
+fn list_baseline_keywords(f: KeywordsCollectFn) {
+ f(&["baseline", "first baseline", "last baseline"]);
+}
+
+// <content-distribution>
+fn parse_content_distribution<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_content_distribution_keywords`
+ // function below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "stretch" => Ok(AlignFlags::STRETCH),
+ "space-between" => Ok(AlignFlags::SPACE_BETWEEN),
+ "space-around" => Ok(AlignFlags::SPACE_AROUND),
+ "space-evenly" => Ok(AlignFlags::SPACE_EVENLY),
+ }
+}
+
+fn list_content_distribution_keywords(f: KeywordsCollectFn) {
+ f(&["stretch", "space-between", "space-around", "space-evenly"]);
+}
+
+// <overflow-position>
+fn parse_overflow_position<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_overflow_position_keywords`
+ // function below when this function is updated.
+ try_match_ident_ignore_ascii_case! { input,
+ "safe" => Ok(AlignFlags::SAFE),
+ "unsafe" => Ok(AlignFlags::UNSAFE),
+ }
+}
+
+fn list_overflow_position_keywords(f: KeywordsCollectFn) {
+ f(&["safe", "unsafe"]);
+}
+
+// <self-position> | left | right in the inline axis.
+fn parse_self_position<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ axis: AxisDirection,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_self_position_keywords`
+ // function below when this function is updated.
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "start" => AlignFlags::START,
+ "end" => AlignFlags::END,
+ "flex-start" => AlignFlags::FLEX_START,
+ "flex-end" => AlignFlags::FLEX_END,
+ "center" => AlignFlags::CENTER,
+ "self-start" => AlignFlags::SELF_START,
+ "self-end" => AlignFlags::SELF_END,
+ "left" if axis == AxisDirection::Inline => AlignFlags::LEFT,
+ "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT,
+ })
+}
+
+fn list_self_position_keywords(f: KeywordsCollectFn, axis: AxisDirection) {
+ f(&[
+ "start",
+ "end",
+ "flex-start",
+ "flex-end",
+ "center",
+ "self-start",
+ "self-end",
+ ]);
+ if axis == AxisDirection::Inline {
+ f(&["left", "right"]);
+ }
+}
+
+fn parse_left_right_center<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_legacy_keywords` function below
+ // when this function is updated.
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "left" => AlignFlags::LEFT,
+ "right" => AlignFlags::RIGHT,
+ "center" => AlignFlags::CENTER,
+ })
+}
+
+// legacy | [ legacy && [ left | right | center ] ]
+fn parse_legacy<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> {
+ // NOTE Please also update the `list_legacy_keywords` function below
+ // when this function is updated.
+ let flags = try_match_ident_ignore_ascii_case! { input,
+ "legacy" => {
+ let flags = input.try_parse(parse_left_right_center)
+ .unwrap_or(AlignFlags::empty());
+
+ return Ok(AlignFlags::LEGACY | flags)
+ },
+ "left" => AlignFlags::LEFT,
+ "right" => AlignFlags::RIGHT,
+ "center" => AlignFlags::CENTER,
+ };
+
+ input.expect_ident_matching("legacy")?;
+ Ok(AlignFlags::LEGACY | flags)
+}
+
+fn list_legacy_keywords(f: KeywordsCollectFn) {
+ f(&["legacy", "left", "right", "center"]);
+}
diff --git a/servo/components/style/values/specified/angle.rs b/servo/components/style/values/specified/angle.rs
new file mode 100644
index 0000000000..fb4554eb85
--- /dev/null
+++ b/servo/components/style/values/specified/angle.rs
@@ -0,0 +1,276 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified angles.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::angle::Angle as ComputedAngle;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified::calc::CalcNode;
+use crate::values::CSSFloat;
+use crate::Zero;
+use cssparser::{Parser, Token};
+use std::f32::consts::PI;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss};
+
+/// A specified angle dimension.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToCss, ToShmem)]
+pub enum AngleDimension {
+ /// An angle with degree unit.
+ #[css(dimension)]
+ Deg(CSSFloat),
+ /// An angle with gradian unit.
+ #[css(dimension)]
+ Grad(CSSFloat),
+ /// An angle with radian unit.
+ #[css(dimension)]
+ Rad(CSSFloat),
+ /// An angle with turn unit.
+ #[css(dimension)]
+ Turn(CSSFloat),
+}
+
+impl Zero for AngleDimension {
+ fn zero() -> Self {
+ AngleDimension::Deg(0.)
+ }
+
+ fn is_zero(&self) -> bool {
+ self.unitless_value() == 0.0
+ }
+}
+
+impl AngleDimension {
+ /// Returns the amount of degrees this angle represents.
+ #[inline]
+ fn degrees(&self) -> CSSFloat {
+ const DEG_PER_RAD: f32 = 180.0 / PI;
+ const DEG_PER_TURN: f32 = 360.0;
+ const DEG_PER_GRAD: f32 = 180.0 / 200.0;
+
+ match *self {
+ AngleDimension::Deg(d) => d,
+ AngleDimension::Rad(rad) => rad * DEG_PER_RAD,
+ AngleDimension::Turn(turns) => turns * DEG_PER_TURN,
+ AngleDimension::Grad(gradians) => gradians * DEG_PER_GRAD,
+ }
+ }
+
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ AngleDimension::Deg(v) |
+ AngleDimension::Rad(v) |
+ AngleDimension::Turn(v) |
+ AngleDimension::Grad(v) => v,
+ }
+ }
+
+ fn unit(&self) -> &'static str {
+ match *self {
+ AngleDimension::Deg(_) => "deg",
+ AngleDimension::Rad(_) => "rad",
+ AngleDimension::Turn(_) => "turn",
+ AngleDimension::Grad(_) => "grad",
+ }
+ }
+}
+
+/// A specified Angle value, which is just the angle dimension, plus whether it
+/// was specified as `calc()` or not.
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Angle {
+ value: AngleDimension,
+ was_calc: bool,
+}
+
+impl Zero for Angle {
+ fn zero() -> Self {
+ Self {
+ value: Zero::zero(),
+ was_calc: false,
+ }
+ }
+
+ fn is_zero(&self) -> bool {
+ self.value.is_zero()
+ }
+}
+
+impl ToCss for Angle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.value.unitless_value(),
+ self.value.unit(),
+ self.was_calc,
+ dest,
+ )
+ }
+}
+
+impl ToComputedValue for Angle {
+ type ComputedValue = ComputedAngle;
+
+ #[inline]
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ let degrees = self.degrees();
+
+ // NaN and +-infinity should degenerate to 0: https://github.com/w3c/csswg-drafts/issues/6105
+ ComputedAngle::from_degrees(if degrees.is_finite() { degrees } else { 0.0 })
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Angle {
+ value: AngleDimension::Deg(computed.degrees()),
+ was_calc: false,
+ }
+ }
+}
+
+impl Angle {
+ /// Creates an angle with the given value in degrees.
+ #[inline]
+ pub fn from_degrees(value: CSSFloat, was_calc: bool) -> Self {
+ Angle {
+ value: AngleDimension::Deg(value),
+ was_calc,
+ }
+ }
+
+ /// Creates an angle with the given value in radians.
+ #[inline]
+ pub fn from_radians(value: CSSFloat) -> Self {
+ Angle {
+ value: AngleDimension::Rad(value),
+ was_calc: false,
+ }
+ }
+
+ /// Return `0deg`.
+ pub fn zero() -> Self {
+ Self::from_degrees(0.0, false)
+ }
+
+ /// Returns the value of the angle in degrees, mostly for `calc()`.
+ #[inline]
+ pub fn degrees(&self) -> CSSFloat {
+ self.value.degrees()
+ }
+
+ /// Returns the value of the angle in radians.
+ #[inline]
+ pub fn radians(&self) -> CSSFloat {
+ const RAD_PER_DEG: f32 = PI / 180.0;
+ self.value.degrees() * RAD_PER_DEG
+ }
+
+ /// Whether this specified angle came from a `calc()` expression.
+ #[inline]
+ pub fn was_calc(&self) -> bool {
+ self.was_calc
+ }
+
+ /// Returns an `Angle` parsed from a `calc()` expression.
+ pub fn from_calc(degrees: CSSFloat) -> Self {
+ Angle {
+ value: AngleDimension::Deg(degrees),
+ was_calc: true,
+ }
+ }
+
+ /// Returns the unit of the angle.
+ #[inline]
+ pub fn unit(&self) -> &'static str {
+ self.value.unit()
+ }
+}
+
+/// Whether to allow parsing an unitless zero as a valid angle.
+///
+/// This should always be `No`, except for exceptions like:
+///
+/// https://github.com/w3c/fxtf-drafts/issues/228
+///
+/// See also: https://github.com/w3c/csswg-drafts/issues/1162.
+#[allow(missing_docs)]
+pub enum AllowUnitlessZeroAngle {
+ Yes,
+ No,
+}
+
+impl Parse for Angle {
+ /// Parses an angle according to CSS-VALUES § 6.1.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowUnitlessZeroAngle::No)
+ }
+}
+
+impl Angle {
+ /// Parse an `<angle>` value given a value and an unit.
+ pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result<Angle, ()> {
+ let value = match_ignore_ascii_case! { unit,
+ "deg" => AngleDimension::Deg(value),
+ "grad" => AngleDimension::Grad(value),
+ "turn" => AngleDimension::Turn(value),
+ "rad" => AngleDimension::Rad(value),
+ _ => return Err(())
+ };
+
+ Ok(Self { value, was_calc })
+ }
+
+ /// Parse an `<angle>` allowing unitless zero to represent a zero angle.
+ ///
+ /// See the comment in `AllowUnitlessZeroAngle` for why.
+ #[inline]
+ pub fn parse_with_unitless<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowUnitlessZeroAngle::Yes)
+ }
+
+ pub(super) fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_unitless_zero: AllowUnitlessZeroAngle,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let t = input.next()?;
+ let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes);
+ match *t {
+ Token::Dimension {
+ value, ref unit, ..
+ } => {
+ match Angle::parse_dimension(value, unit, /* from_calc = */ false) {
+ Ok(angle) => Ok(angle),
+ Err(()) => {
+ let t = t.clone();
+ Err(input.new_unexpected_token_error(t))
+ },
+ }
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse_angle(context, input, function)
+ },
+ Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()),
+ ref t => {
+ let t = t.clone();
+ Err(input.new_unexpected_token_error(t))
+ },
+ }
+ }
+}
+
+impl SpecifiedValueInfo for Angle {}
diff --git a/servo/components/style/values/specified/animation.rs b/servo/components/style/values/specified/animation.rs
new file mode 100644
index 0000000000..e7bbf26fb3
--- /dev/null
+++ b/servo/components/style/values/specified/animation.rs
@@ -0,0 +1,463 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for properties related to animations and transitions.
+
+use crate::parser::{Parse, ParserContext};
+use crate::properties::{NonCustomPropertyId, PropertyId, ShorthandId};
+use crate::values::generics::animation as generics;
+use crate::values::specified::{LengthPercentage, NonNegativeNumber};
+use crate::values::{CustomIdent, KeyframesName, TimelineName};
+use crate::Atom;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{
+ CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss,
+};
+
+/// A given transition property, that is either `All`, a longhand or shorthand
+/// property, or an unsupported or custom property.
+#[derive(
+ Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[repr(u8)]
+pub enum TransitionProperty {
+ /// A non-custom property.
+ NonCustom(NonCustomPropertyId),
+ /// A custom property.
+ Custom(Atom),
+ /// Unrecognized property which could be any non-transitionable, custom property, or
+ /// unknown property.
+ Unsupported(CustomIdent),
+}
+
+impl ToCss for TransitionProperty {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ TransitionProperty::NonCustom(ref id) => id.to_css(dest),
+ TransitionProperty::Custom(ref name) => {
+ dest.write_str("--")?;
+ crate::values::serialize_atom_name(name, dest)
+ },
+ TransitionProperty::Unsupported(ref i) => i.to_css(dest),
+ }
+ }
+}
+
+impl Parse for TransitionProperty {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+
+ let id = match PropertyId::parse_ignoring_rule_type(&ident, context) {
+ Ok(id) => id,
+ Err(..) => {
+ // None is not acceptable as a single transition-property.
+ return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident(
+ location,
+ ident,
+ &["none"],
+ )?));
+ },
+ };
+
+ Ok(match id {
+ PropertyId::NonCustom(id) => TransitionProperty::NonCustom(id.unaliased()),
+ PropertyId::Custom(name) => TransitionProperty::Custom(name),
+ })
+ }
+}
+
+impl SpecifiedValueInfo for TransitionProperty {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // `transition-property` can actually accept all properties and
+ // arbitrary identifiers, but `all` is a special one we'd like
+ // to list.
+ f(&["all"]);
+ }
+}
+
+impl TransitionProperty {
+ /// Returns the `none` value.
+ #[inline]
+ pub fn none() -> Self {
+ TransitionProperty::Unsupported(CustomIdent(atom!("none")))
+ }
+
+ /// Returns whether we're the `none` value.
+ #[inline]
+ pub fn is_none(&self) -> bool {
+ matches!(*self, TransitionProperty::Unsupported(ref ident) if ident.0 == atom!("none"))
+ }
+
+ /// Returns `all`.
+ #[inline]
+ pub fn all() -> Self {
+ TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(ShorthandId::All))
+ }
+
+ /// Returns true if it is `all`.
+ #[inline]
+ pub fn is_all(&self) -> bool {
+ self == &TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(
+ ShorthandId::All,
+ ))
+ }
+}
+
+/// https://drafts.csswg.org/css-animations/#animation-iteration-count
+#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum AnimationIterationCount {
+ /// A `<number>` value.
+ Number(NonNegativeNumber),
+ /// The `infinite` keyword.
+ Infinite,
+}
+
+impl AnimationIterationCount {
+ /// Returns the value `1.0`.
+ #[inline]
+ pub fn one() -> Self {
+ Self::Number(NonNegativeNumber::new(1.0))
+ }
+}
+
+/// A value for the `animation-name` property.
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "none")]
+#[repr(C)]
+pub struct AnimationName(pub KeyframesName);
+
+impl AnimationName {
+ /// Get the name of the animation as an `Atom`.
+ pub fn as_atom(&self) -> Option<&Atom> {
+ if self.is_none() {
+ return None;
+ }
+ Some(self.0.as_atom())
+ }
+
+ /// Returns the `none` value.
+ pub fn none() -> Self {
+ AnimationName(KeyframesName::none())
+ }
+
+ /// Returns whether this is the none value.
+ pub fn is_none(&self) -> bool {
+ self.0.is_none()
+ }
+}
+
+impl Parse for AnimationName {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) {
+ return Ok(AnimationName(name));
+ }
+
+ input.expect_ident_matching("none")?;
+ Ok(AnimationName(KeyframesName::none()))
+ }
+}
+
+/// https://drafts.csswg.org/css-animations/#propdef-animation-direction
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationDirection {
+ Normal,
+ Reverse,
+ Alternate,
+ AlternateReverse,
+}
+
+/// https://drafts.csswg.org/css-animations/#animation-play-state
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationPlayState {
+ Running,
+ Paused,
+}
+
+/// https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationFillMode {
+ None,
+ Forwards,
+ Backwards,
+ Both,
+}
+
+/// https://drafts.csswg.org/css-animations-2/#animation-composition
+#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)]
+#[repr(u8)]
+#[allow(missing_docs)]
+pub enum AnimationComposition {
+ Replace,
+ Add,
+ Accumulate,
+}
+
+/// A value for the <Scroller> used in scroll().
+///
+/// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller
+#[derive(
+ Copy,
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum Scroller {
+ /// The nearest ancestor scroll container. (Default.)
+ Nearest,
+ /// The document viewport as the scroll container.
+ Root,
+ /// Specifies to use the element’s own principal box as the scroll container.
+ #[css(keyword = "self")]
+ SelfElement,
+}
+
+impl Scroller {
+ /// Returns true if it is default.
+ #[inline]
+ fn is_default(&self) -> bool {
+ matches!(*self, Self::Nearest)
+ }
+}
+
+impl Default for Scroller {
+ fn default() -> Self {
+ Self::Nearest
+ }
+}
+
+/// A value for the <Axis> used in scroll(), or a value for {scroll|view}-timeline-axis.
+///
+/// https://drafts.csswg.org/scroll-animations-1/#typedef-axis
+/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis
+/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis
+#[derive(
+ Copy,
+ Clone,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollAxis {
+ /// The block axis of the scroll container. (Default.)
+ Block = 0,
+ /// The inline axis of the scroll container.
+ Inline = 1,
+ /// The vertical block axis of the scroll container.
+ Vertical = 2,
+ /// The horizontal axis of the scroll container.
+ Horizontal = 3,
+}
+
+impl ScrollAxis {
+ /// Returns true if it is default.
+ #[inline]
+ pub fn is_default(&self) -> bool {
+ matches!(*self, Self::Block)
+ }
+}
+
+impl Default for ScrollAxis {
+ fn default() -> Self {
+ Self::Block
+ }
+}
+
+/// The scroll() notation.
+/// https://drafts.csswg.org/scroll-animations-1/#scroll-notation
+#[derive(
+ Copy,
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function = "scroll")]
+#[repr(C)]
+pub struct ScrollFunction {
+ /// The scroll container element whose scroll position drives the progress of the timeline.
+ #[css(skip_if = "Scroller::is_default")]
+ pub scroller: Scroller,
+ /// The axis of scrolling that drives the progress of the timeline.
+ #[css(skip_if = "ScrollAxis::is_default")]
+ pub axis: ScrollAxis,
+}
+
+impl ScrollFunction {
+ /// Parse the inner function arguments of `scroll()`.
+ fn parse_arguments<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ // <scroll()> = scroll( [ <scroller> || <axis> ]? )
+ // https://drafts.csswg.org/scroll-animations-1/#funcdef-scroll
+ let mut scroller = None;
+ let mut axis = None;
+ loop {
+ if scroller.is_none() {
+ scroller = input.try_parse(Scroller::parse).ok();
+ }
+
+ if axis.is_none() {
+ axis = input.try_parse(ScrollAxis::parse).ok();
+ if axis.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ Ok(Self {
+ scroller: scroller.unwrap_or_default(),
+ axis: axis.unwrap_or_default(),
+ })
+ }
+}
+
+impl generics::ViewFunction<LengthPercentage> {
+ /// Parse the inner function arguments of `view()`.
+ fn parse_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // <view()> = view( [ <axis> || <'view-timeline-inset'> ]? )
+ // https://drafts.csswg.org/scroll-animations-1/#funcdef-view
+ let mut axis = None;
+ let mut inset = None;
+ loop {
+ if axis.is_none() {
+ axis = input.try_parse(ScrollAxis::parse).ok();
+ }
+
+ if inset.is_none() {
+ inset = input
+ .try_parse(|i| ViewTimelineInset::parse(context, i))
+ .ok();
+ if inset.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ Ok(Self {
+ inset: inset.unwrap_or_default(),
+ axis: axis.unwrap_or_default(),
+ })
+ }
+}
+
+/// A specified value for the `animation-timeline` property.
+pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>;
+
+impl Parse for AnimationTimeline {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::generics::animation::ViewFunction;
+
+ // <single-animation-timeline> = auto | none | <custom-ident> | <scroll()> | <view()>
+ // https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline
+
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(Self::Auto);
+ }
+
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(AnimationTimeline::Timeline(TimelineName::none()));
+ }
+
+ if let Ok(name) = input.try_parse(|i| TimelineName::parse(context, i)) {
+ return Ok(AnimationTimeline::Timeline(name));
+ }
+
+ // Parse possible functions
+ let location = input.current_source_location();
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(move |i| {
+ match_ignore_ascii_case! { &function,
+ "scroll" => ScrollFunction::parse_arguments(i).map(Self::Scroll),
+ "view" => ViewFunction::parse_arguments(context, i).map(Self::View),
+ _ => {
+ Err(location.new_custom_error(
+ StyleParseErrorKind::UnexpectedFunction(function.clone())
+ ))
+ },
+ }
+ })
+ }
+}
+
+/// A value for the scroll-timeline-name or view-timeline-name.
+pub type ScrollTimelineName = AnimationName;
+
+/// A specified value for the `view-timeline-inset` property.
+pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>;
+
+impl Parse for ViewTimelineInset {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::LengthPercentageOrAuto;
+
+ let start = LengthPercentageOrAuto::parse(context, input)?;
+ let end = match input.try_parse(|input| LengthPercentageOrAuto::parse(context, input)) {
+ Ok(end) => end,
+ Err(_) => start.clone(),
+ };
+
+ Ok(Self { start, end })
+ }
+}
diff --git a/servo/components/style/values/specified/background.rs b/servo/components/style/values/specified/background.rs
new file mode 100644
index 0000000000..39a5a85193
--- /dev/null
+++ b/servo/components/style/values/specified/background.rs
@@ -0,0 +1,143 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values related to backgrounds.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::background::BackgroundSize as GenericBackgroundSize;
+use crate::values::specified::length::{
+ NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto,
+};
+use cssparser::Parser;
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, ToCss};
+
+/// A specified value for the `background-size` property.
+pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>;
+
+impl Parse for BackgroundSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(width) = input.try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i))
+ {
+ let height = input
+ .try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i))
+ .unwrap_or(NonNegativeLengthPercentageOrAuto::auto());
+ return Ok(GenericBackgroundSize::ExplicitSize { width, height });
+ }
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "cover" => GenericBackgroundSize::Cover,
+ "contain" => GenericBackgroundSize::Contain,
+ })
+ }
+}
+
+/// One of the keywords for `background-repeat`.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[value_info(other_values = "repeat-x,repeat-y")]
+pub enum BackgroundRepeatKeyword {
+ Repeat,
+ Space,
+ Round,
+ NoRepeat,
+}
+
+/// The value of the `background-repeat` property, with `repeat-x` / `repeat-y`
+/// represented as the combination of `no-repeat` and `repeat` in the opposite
+/// axes.
+///
+/// https://drafts.csswg.org/css-backgrounds/#the-background-repeat
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct BackgroundRepeat(pub BackgroundRepeatKeyword, pub BackgroundRepeatKeyword);
+
+impl BackgroundRepeat {
+ /// Returns the `repeat repeat` value.
+ pub fn repeat() -> Self {
+ BackgroundRepeat(
+ BackgroundRepeatKeyword::Repeat,
+ BackgroundRepeatKeyword::Repeat,
+ )
+ }
+}
+
+impl ToCss for BackgroundRepeat {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match (self.0, self.1) {
+ (BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat) => {
+ dest.write_str("repeat-x")
+ },
+ (BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat) => {
+ dest.write_str("repeat-y")
+ },
+ (horizontal, vertical) => {
+ horizontal.to_css(dest)?;
+ if horizontal != vertical {
+ dest.write_char(' ')?;
+ vertical.to_css(dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+impl Parse for BackgroundRepeat {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let ident = input.expect_ident_cloned()?;
+
+ match_ignore_ascii_case! { &ident,
+ "repeat-x" => {
+ return Ok(BackgroundRepeat(BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat));
+ },
+ "repeat-y" => {
+ return Ok(BackgroundRepeat(BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat));
+ },
+ _ => {},
+ }
+
+ let horizontal = match BackgroundRepeatKeyword::from_ident(&ident) {
+ Ok(h) => h,
+ Err(()) => {
+ return Err(
+ input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))
+ );
+ },
+ };
+
+ let vertical = input.try_parse(BackgroundRepeatKeyword::parse).ok();
+ Ok(BackgroundRepeat(horizontal, vertical.unwrap_or(horizontal)))
+ }
+}
diff --git a/servo/components/style/values/specified/basic_shape.rs b/servo/components/style/values/specified/basic_shape.rs
new file mode 100644
index 0000000000..526296b735
--- /dev/null
+++ b/servo/components/style/values/specified/basic_shape.rs
@@ -0,0 +1,719 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the specified value of
+//! [`basic-shape`][basic-shape]s
+//!
+//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::basic_shape as generic;
+use crate::values::generics::basic_shape::{Path, PolygonCoord};
+use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto};
+use crate::values::generics::rect::Rect;
+use crate::values::specified::border::BorderRadius;
+use crate::values::specified::image::Image;
+use crate::values::specified::length::LengthPercentageOrAuto;
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData};
+use crate::Zero;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// A specified alias for FillRule.
+pub use crate::values::generics::basic_shape::FillRule;
+
+/// A specified `clip-path` value.
+pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>;
+
+/// A specified `shape-outside` value.
+pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>;
+
+/// A specified value for `at <position>` in circle() and ellipse().
+// Note: its computed value is the same as computed::position::Position. We just want to always use
+// LengthPercentage as the type of its components, for basic shapes.
+pub type ShapePosition = GenericPosition<LengthPercentage, LengthPercentage>;
+
+/// A specified basic shape.
+pub type BasicShape = generic::GenericBasicShape<
+ ShapePosition,
+ LengthPercentage,
+ NonNegativeLengthPercentage,
+ BasicShapeRect,
+>;
+
+/// The specified value of `inset()`.
+pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>;
+
+/// A specified circle.
+pub type Circle = generic::Circle<ShapePosition, NonNegativeLengthPercentage>;
+
+/// A specified ellipse.
+pub type Ellipse = generic::Ellipse<ShapePosition, NonNegativeLengthPercentage>;
+
+/// The specified value of `ShapeRadius`.
+pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>;
+
+/// The specified value of `Polygon`.
+pub type Polygon = generic::GenericPolygon<LengthPercentage>;
+
+/// The specified value of `xywh()`.
+/// Defines a rectangle via offsets from the top and left edge of the reference box, and a
+/// specified width and height.
+///
+/// The four <length-percentage>s define, respectively, the inset from the left edge of the
+/// reference box, the inset from the top edge of the reference box, the width of the rectangle,
+/// and the height of the rectangle.
+///
+/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
+pub struct Xywh {
+ /// The left edge of the reference box.
+ pub x: LengthPercentage,
+ /// The top edge of the reference box.
+ pub y: LengthPercentage,
+ /// The specified width.
+ pub width: NonNegativeLengthPercentage,
+ /// The specified height.
+ pub height: NonNegativeLengthPercentage,
+ /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
+ /// using the border-radius shorthand syntax.
+ pub round: BorderRadius,
+}
+
+/// Defines a rectangle via insets from the top and left edges of the reference box.
+///
+/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
+#[repr(C)]
+pub struct ShapeRectFunction {
+ /// The four <length-percentage>s define the position of the top, right, bottom, and left edges
+ /// of a rectangle, respectively, as insets from the top edge of the reference box (for the
+ /// first and third values) or the left edge of the reference box (for the second and fourth
+ /// values).
+ ///
+ /// An auto value makes the edge of the box coincide with the corresponding edge of the
+ /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and
+ /// equivalent to 100% as the second (right) or third (bottom) value.
+ pub rect: Rect<LengthPercentageOrAuto>,
+ /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle
+ /// using the border-radius shorthand syntax.
+ pub round: BorderRadius,
+}
+
+/// The specified value of <basic-shape-rect>.
+/// <basic-shape-rect> = <inset()> | <rect()> | <xywh()>
+///
+/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum BasicShapeRect {
+ /// Defines an inset rectangle via insets from each edge of the reference box.
+ Inset(InsetRect),
+ /// Defines a xywh function.
+ #[css(function)]
+ Xywh(Xywh),
+ /// Defines a rect function.
+ #[css(function)]
+ Rect(ShapeRectFunction),
+}
+
+/// For filled shapes, we use fill-rule, and store it for path() and polygon().
+/// For outline shapes, we should ignore fill-rule.
+///
+/// https://github.com/w3c/fxtf-drafts/issues/512
+/// https://github.com/w3c/csswg-drafts/issues/7390
+/// https://github.com/w3c/csswg-drafts/issues/3468
+pub enum ShapeType {
+ /// The CSS property uses filled shapes. The default behavior.
+ Filled,
+ /// The CSS property uses outline shapes. This is especially useful for offset-path.
+ Outline,
+}
+
+bitflags! {
+ /// The flags to represent which basic shapes we would like to support.
+ ///
+ /// Different properties may use different subsets of <basic-shape>:
+ /// e.g.
+ /// clip-path: all basic shapes.
+ /// motion-path: all basic shapes (but ignore fill-rule).
+ /// shape-outside: inset(), circle(), ellipse(), polygon().
+ ///
+ /// Also there are some properties we don't support for now:
+ /// shape-inside: inset(), circle(), ellipse(), polygon().
+ /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon().
+ ///
+ /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now
+ /// we use the bitflags to choose the supported basic shapes for each property at the parse
+ /// time.
+ /// https://github.com/w3c/csswg-drafts/issues/7390
+ #[derive(Clone, Copy)]
+ #[repr(C)]
+ pub struct AllowedBasicShapes: u8 {
+ /// inset().
+ const INSET = 1 << 0;
+ /// xywh().
+ const XYWH = 1 << 1;
+ /// rect().
+ const RECT = 1 << 2;
+ /// circle().
+ const CIRCLE = 1 << 3;
+ /// ellipse().
+ const ELLIPSE = 1 << 4;
+ /// polygon().
+ const POLYGON = 1 << 5;
+ /// path().
+ const PATH = 1 << 6;
+ // TODO: Bug 1823463. Add shape().
+ // const SHAPE = 1 << 7;
+
+ /// All flags.
+ const ALL =
+ Self::INSET.bits() |
+ Self::XYWH.bits() |
+ Self::RECT.bits() |
+ Self::CIRCLE.bits() |
+ Self::ELLIPSE.bits() |
+ Self::POLYGON.bits() |
+ Self::PATH.bits();
+
+ /// For shape-outside.
+ const SHAPE_OUTSIDE =
+ Self::INSET.bits() |
+ Self::CIRCLE.bits() |
+ Self::ELLIPSE.bits() |
+ Self::POLYGON.bits();
+ }
+}
+
+/// A helper for both clip-path and shape-outside parsing of shapes.
+fn parse_shape_or_box<'i, 't, R, ReferenceBox>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R,
+ to_reference_box: impl FnOnce(ReferenceBox) -> R,
+ flags: AllowedBasicShapes,
+) -> Result<R, ParseError<'i>>
+where
+ ReferenceBox: Default + Parse,
+{
+ let mut shape = None;
+ let mut ref_box = None;
+ loop {
+ if shape.is_none() {
+ shape = input
+ .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled))
+ .ok();
+ }
+
+ if ref_box.is_none() {
+ ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok();
+ if ref_box.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ if let Some(shp) = shape {
+ return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default()));
+ }
+
+ match ref_box {
+ Some(r) => Ok(to_reference_box(r)),
+ None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+}
+
+impl Parse for ClipPath {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(ClipPath::None);
+ }
+
+ if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
+ return Ok(ClipPath::Url(url));
+ }
+
+ parse_shape_or_box(
+ context,
+ input,
+ ClipPath::Shape,
+ ClipPath::Box,
+ AllowedBasicShapes::ALL,
+ )
+ }
+}
+
+impl Parse for ShapeOutside {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Need to parse this here so that `Image::parse_with_cors_anonymous`
+ // doesn't parse it.
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(ShapeOutside::None);
+ }
+
+ if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) {
+ debug_assert_ne!(image, Image::None);
+ return Ok(ShapeOutside::Image(image));
+ }
+
+ parse_shape_or_box(
+ context,
+ input,
+ ShapeOutside::Shape,
+ ShapeOutside::Box,
+ AllowedBasicShapes::SHAPE_OUTSIDE,
+ )
+ }
+}
+
+impl BasicShape {
+ /// Parse with some parameters.
+ /// 1. The supported <basic-shape>.
+ /// 2. The type of shapes. Should we ignore fill-rule?
+ /// 3. The default value of `at <position>`.
+ pub fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ flags: AllowedBasicShapes,
+ shape_type: ShapeType,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(move |i| {
+ match_ignore_ascii_case! { &function,
+ "inset" if flags.contains(AllowedBasicShapes::INSET) => {
+ InsetRect::parse_function_arguments(context, i)
+ .map(BasicShapeRect::Inset)
+ .map(BasicShape::Rect)
+ },
+ "xywh"
+ if flags.contains(AllowedBasicShapes::XYWH)
+ && static_prefs::pref!("layout.css.basic-shape-xywh.enabled") =>
+ {
+ Xywh::parse_function_arguments(context, i)
+ .map(BasicShapeRect::Xywh)
+ .map(BasicShape::Rect)
+ },
+ "rect"
+ if flags.contains(AllowedBasicShapes::RECT)
+ && static_prefs::pref!("layout.css.basic-shape-rect.enabled") =>
+ {
+ ShapeRectFunction::parse_function_arguments(context, i)
+ .map(BasicShapeRect::Rect)
+ .map(BasicShape::Rect)
+ },
+ "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => {
+ Circle::parse_function_arguments(context, i)
+ .map(BasicShape::Circle)
+ },
+ "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => {
+ Ellipse::parse_function_arguments(context, i)
+ .map(BasicShape::Ellipse)
+ },
+ "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => {
+ Polygon::parse_function_arguments(context, i, shape_type)
+ .map(BasicShape::Polygon)
+ },
+ "path" if flags.contains(AllowedBasicShapes::PATH) => {
+ Path::parse_function_arguments(i, shape_type).map(BasicShape::Path)
+ },
+ _ => Err(location
+ .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
+ }
+ })
+ }
+}
+
+impl Parse for InsetRect {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("inset")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+fn parse_round<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<BorderRadius, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("round"))
+ .is_ok()
+ {
+ return BorderRadius::parse(context, input);
+ }
+
+ Ok(BorderRadius::zero())
+}
+
+impl InsetRect {
+ /// Parse the inner function arguments of `inset()`
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let rect = Rect::parse_with(context, input, LengthPercentage::parse)?;
+ let round = parse_round(context, input)?;
+ Ok(generic::InsetRect { rect, round })
+ }
+}
+
+impl ToCss for ShapePosition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.horizontal.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.vertical.to_css(dest)
+ }
+}
+
+fn parse_at_position<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<GenericPositionOrAuto<ShapePosition>, ParseError<'i>> {
+ use crate::values::specified::position::{Position, Side};
+ use crate::values::specified::{AllowedNumericType, Percentage, PositionComponent};
+
+ fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage {
+ // Convert the value when parsing, to make sure we serialize it properly for both
+ // specified and computed values.
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization
+ match c {
+ // Since <position> keywords stand in for percentages, keywords without an offset
+ // turn into percentages.
+ PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)),
+ PositionComponent::Side(keyword, None) => {
+ Percentage::new(if keyword.is_start() { 0. } else { 1. }).into()
+ },
+ // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of
+ // "avoiding calc() expressions where possible" and "avoiding calc()
+ // transformations" will be removed from the spec, and we should follow the
+ // css-values-4 for position, i.e. we make it as length-percentage always.
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization.
+ // https://drafts.csswg.org/css-values-4/#typedef-position
+ PositionComponent::Side(keyword, Some(length)) => {
+ if keyword.is_start() {
+ length
+ } else {
+ length.hundred_percent_minus(AllowedNumericType::All)
+ }
+ },
+ PositionComponent::Length(length) => length,
+ }
+ }
+
+ if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
+ Position::parse(context, input).map(|pos| {
+ GenericPositionOrAuto::Position(ShapePosition::new(
+ convert_to_length_percentage(pos.horizontal),
+ convert_to_length_percentage(pos.vertical),
+ ))
+ })
+ } else {
+ // `at <position>` is omitted.
+ Ok(GenericPositionOrAuto::Auto)
+ }
+}
+
+impl Parse for Circle {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("circle")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+impl Circle {
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let radius = input
+ .try_parse(|i| ShapeRadius::parse(context, i))
+ .unwrap_or_default();
+ let position = parse_at_position(context, input)?;
+
+ Ok(generic::Circle { radius, position })
+ }
+}
+
+impl Parse for Ellipse {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("ellipse")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+impl Ellipse {
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let (semiaxis_x, semiaxis_y) = input
+ .try_parse(|i| -> Result<_, ParseError> {
+ Ok((
+ ShapeRadius::parse(context, i)?,
+ ShapeRadius::parse(context, i)?,
+ ))
+ })
+ .unwrap_or_default();
+ let position = parse_at_position(context, input)?;
+
+ Ok(generic::Ellipse {
+ semiaxis_x,
+ semiaxis_y,
+ position,
+ })
+ }
+}
+
+fn parse_fill_rule<'i, 't>(input: &mut Parser<'i, 't>, shape_type: ShapeType) -> FillRule {
+ match shape_type {
+ // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default
+ // value.
+ // [1] https://github.com/w3c/csswg-drafts/issues/3468
+ // [2] https://github.com/w3c/csswg-drafts/issues/7390
+ //
+ // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g.
+ // offset-path, which means we don't parse it when setting `ShapeType::Outline`.
+ // This should be web compatible because the shipped "offset-path:path()" doesn't have
+ // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the
+ // preference.
+ // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321
+ // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929
+ ShapeType::Outline => Default::default(),
+ ShapeType::Filled => input
+ .try_parse(|i| -> Result<_, ParseError> {
+ let fill = FillRule::parse(i)?;
+ i.expect_comma()?;
+ Ok(fill)
+ })
+ .unwrap_or_default(),
+ }
+}
+
+impl Parse for Polygon {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("polygon")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled))
+ }
+}
+
+impl Polygon {
+ /// Parse the inner arguments of a `polygon` function.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ shape_type: ShapeType,
+ ) -> Result<Self, ParseError<'i>> {
+ let fill = parse_fill_rule(input, shape_type);
+ let coordinates = input
+ .parse_comma_separated(|i| {
+ Ok(PolygonCoord(
+ LengthPercentage::parse(context, i)?,
+ LengthPercentage::parse(context, i)?,
+ ))
+ })?
+ .into();
+
+ Ok(Polygon { fill, coordinates })
+ }
+}
+
+impl Path {
+ /// Parse the inner arguments of a `path` function.
+ fn parse_function_arguments<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ shape_type: ShapeType,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::svg_path::AllowEmpty;
+
+ let fill = parse_fill_rule(input, shape_type);
+ let path = SVGPathData::parse(input, AllowEmpty::No)?;
+ Ok(Path { fill, path })
+ }
+}
+
+fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result
+where
+ W: Write,
+{
+ if !round.is_zero() {
+ dest.write_str(" round ")?;
+ round.to_css(dest)?;
+ }
+ Ok(())
+}
+
+impl ToCss for Xywh {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.x.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.y.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.width.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.height.to_css(dest)?;
+ round_to_css(&self.round, dest)
+ }
+}
+
+impl Xywh {
+ /// Parse the inner function arguments of `xywh()`.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let x = LengthPercentage::parse(context, input)?;
+ let y = LengthPercentage::parse(context, input)?;
+ let width = NonNegativeLengthPercentage::parse(context, input)?;
+ let height = NonNegativeLengthPercentage::parse(context, input)?;
+ let round = parse_round(context, input)?;
+ Ok(Xywh {
+ x,
+ y,
+ width,
+ height,
+ round,
+ })
+ }
+}
+
+impl ToCss for ShapeRectFunction {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.rect.0.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.rect.1.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.rect.2.to_css(dest)?;
+ dest.write_char(' ')?;
+ self.rect.3.to_css(dest)?;
+ round_to_css(&self.round, dest)
+ }
+}
+
+impl ShapeRectFunction {
+ /// Parse the inner function arguments of `rect()`.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?;
+ let round = parse_round(context, input)?;
+ Ok(ShapeRectFunction { rect, round })
+ }
+}
+
+impl ToComputedValue for BasicShapeRect {
+ type ComputedValue = ComputedInsetRect;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::values::computed::LengthPercentage;
+ use crate::values::computed::LengthPercentageOrAuto;
+ use style_traits::values::specified::AllowedNumericType;
+
+ match self {
+ Self::Inset(ref inset) => inset.to_computed_value(context),
+ Self::Xywh(ref xywh) => {
+ // Given `xywh(x y w h)`, construct the equivalent inset() function,
+ // `inset(y calc(100% - x - w) calc(100% - y - h) x)`.
+ //
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
+ // https://github.com/w3c/csswg-drafts/issues/9053
+ let x = xywh.x.to_computed_value(context);
+ let y = xywh.y.to_computed_value(context);
+ let w = xywh.width.to_computed_value(context);
+ let h = xywh.height.to_computed_value(context);
+ // calc(100% - x - w).
+ let right = LengthPercentage::hundred_percent_minus_list(
+ &[&x, &w.0],
+ AllowedNumericType::All,
+ );
+ // calc(100% - y - h).
+ let bottom = LengthPercentage::hundred_percent_minus_list(
+ &[&y, &h.0],
+ AllowedNumericType::All,
+ );
+
+ ComputedInsetRect {
+ rect: Rect::new(y, right, bottom, x),
+ round: xywh.round.to_computed_value(context),
+ }
+ },
+ Self::Rect(ref rect) => {
+ // Given `rect(t r b l)`, the equivalent function is
+ // `inset(t calc(100% - r) calc(100% - b) l)`.
+ //
+ // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values
+ fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage {
+ match v {
+ // it’s equivalent to 0% as the first (top) or fourth (left) value.
+ // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
+ LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
+ LengthPercentageOrAuto::LengthPercentage(lp) => lp,
+ }
+ }
+ fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage {
+ match v {
+ // It's equivalent to 100% as the second (right) or third (bottom) value.
+ // So calc(100% - 100%) = 0%.
+ // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect
+ LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(),
+ LengthPercentageOrAuto::LengthPercentage(lp) => {
+ LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All)
+ },
+ }
+ }
+
+ let round = rect.round.to_computed_value(context);
+ let rect = rect.rect.to_computed_value(context);
+ let rect = Rect::new(
+ compute_top_or_left(rect.0),
+ compute_bottom_or_right(rect.1),
+ compute_bottom_or_right(rect.2),
+ compute_top_or_left(rect.3),
+ );
+
+ ComputedInsetRect { rect, round }
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Inset(ToComputedValue::from_computed_value(computed))
+ }
+}
diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs
new file mode 100644
index 0000000000..a4660c7f60
--- /dev/null
+++ b/servo/components/style/values/specified/border.rs
@@ -0,0 +1,398 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values related to borders.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius;
+use crate::values::generics::border::BorderImageSideWidth as GenericBorderImageSideWidth;
+use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice;
+use crate::values::generics::border::BorderRadius as GenericBorderRadius;
+use crate::values::generics::border::BorderSpacing as GenericBorderSpacing;
+use crate::values::generics::rect::Rect;
+use crate::values::generics::size::Size2D;
+use crate::values::specified::length::{Length, NonNegativeLength, NonNegativeLengthPercentage};
+use crate::values::specified::Color;
+use crate::values::specified::{AllowQuirks, NonNegativeNumber, NonNegativeNumberOrPercentage};
+use crate::Zero;
+use app_units::Au;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{values::SequenceWriter, CssWriter, ParseError, ToCss};
+
+/// A specified value for a single side of a `border-style` property.
+///
+/// The order here corresponds to the integer values from the border conflict
+/// resolution rules in CSS 2.1 § 17.6.2.1. Higher values override lower values.
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Ord,
+ Parse,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum BorderStyle {
+ Hidden,
+ None,
+ Inset,
+ Groove,
+ Outset,
+ Ridge,
+ Dotted,
+ Dashed,
+ Solid,
+ Double,
+}
+
+impl BorderStyle {
+ /// Whether this border style is either none or hidden.
+ #[inline]
+ pub fn none_or_hidden(&self) -> bool {
+ matches!(*self, BorderStyle::None | BorderStyle::Hidden)
+ }
+}
+
+/// A specified value for the `border-image-width` property.
+pub type BorderImageWidth = Rect<BorderImageSideWidth>;
+
+/// A specified value for a single side of a `border-image-width` property.
+pub type BorderImageSideWidth =
+ GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>;
+
+/// A specified value for the `border-image-slice` property.
+pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>;
+
+/// A specified value for the `border-radius` property.
+pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>;
+
+/// A specified value for the `border-*-radius` longhand properties.
+pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>;
+
+/// A specified value for the `border-spacing` longhand properties.
+pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>;
+
+impl BorderImageSlice {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ GenericBorderImageSlice {
+ offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()),
+ fill: false,
+ }
+ }
+}
+
+/// https://drafts.csswg.org/css-backgrounds-3/#typedef-line-width
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum LineWidth {
+ /// `thin`
+ Thin,
+ /// `medium`
+ Medium,
+ /// `thick`
+ Thick,
+ /// `<length>`
+ Length(NonNegativeLength),
+}
+
+impl LineWidth {
+ /// Returns the `0px` value.
+ #[inline]
+ pub fn zero() -> Self {
+ Self::Length(NonNegativeLength::zero())
+ }
+
+ fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(length) =
+ input.try_parse(|i| NonNegativeLength::parse_quirky(context, i, allow_quirks))
+ {
+ return Ok(Self::Length(length));
+ }
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "thin" => Self::Thin,
+ "medium" => Self::Medium,
+ "thick" => Self::Thick,
+ })
+ }
+}
+
+impl Parse for LineWidth {
+ fn parse<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl ToComputedValue for LineWidth {
+ type ComputedValue = app_units::Au;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ // https://drafts.csswg.org/css-backgrounds-3/#line-width
+ Self::Thin => Au::from_px(1),
+ Self::Medium => Au::from_px(3),
+ Self::Thick => Au::from_px(5),
+ Self::Length(ref length) => Au::from_f32_px(length.to_computed_value(context).px()),
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Length(NonNegativeLength::from_px(computed.to_f32_px()))
+ }
+}
+
+/// A specified value for a single side of the `border-width` property. The difference between this
+/// and LineWidth is whether we snap to device pixels or not.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct BorderSideWidth(LineWidth);
+
+impl BorderSideWidth {
+ /// Returns the `medium` value.
+ pub fn medium() -> Self {
+ Self(LineWidth::Medium)
+ }
+
+ /// Returns a bare px value from the argument.
+ pub fn from_px(px: f32) -> Self {
+ Self(LineWidth::Length(Length::from_px(px).into()))
+ }
+
+ /// Parses, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self(LineWidth::parse_quirky(context, input, allow_quirks)?))
+ }
+}
+
+impl Parse for BorderSideWidth {
+ fn parse<'i>(
+ context: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl ToComputedValue for BorderSideWidth {
+ type ComputedValue = app_units::Au;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let width = self.0.to_computed_value(context);
+ // Round `width` down to the nearest device pixel, but any non-zero value that would round
+ // down to zero is clamped to 1 device pixel.
+ if width == Au(0) {
+ return width;
+ }
+
+ let au_per_dev_px = context.device().app_units_per_device_pixel();
+ std::cmp::max(
+ Au(au_per_dev_px),
+ Au(width.0 / au_per_dev_px * au_per_dev_px),
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self(LineWidth::from_computed_value(computed))
+ }
+}
+
+impl BorderImageSideWidth {
+ /// Returns `1`.
+ #[inline]
+ pub fn one() -> Self {
+ GenericBorderImageSideWidth::Number(NonNegativeNumber::new(1.))
+ }
+}
+
+impl Parse for BorderImageSlice {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
+ let offsets = Rect::parse_with(context, input, NonNegativeNumberOrPercentage::parse)?;
+ if !fill {
+ fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok();
+ }
+ Ok(GenericBorderImageSlice { offsets, fill })
+ }
+}
+
+impl Parse for BorderRadius {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let widths = Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)?;
+ let heights = if input.try_parse(|i| i.expect_delim('/')).is_ok() {
+ Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)?
+ } else {
+ widths.clone()
+ };
+
+ Ok(GenericBorderRadius {
+ top_left: BorderCornerRadius::new(widths.0, heights.0),
+ top_right: BorderCornerRadius::new(widths.1, heights.1),
+ bottom_right: BorderCornerRadius::new(widths.2, heights.2),
+ bottom_left: BorderCornerRadius::new(widths.3, heights.3),
+ })
+ }
+}
+
+impl Parse for BorderCornerRadius {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Size2D::parse_with(context, input, NonNegativeLengthPercentage::parse)
+ .map(GenericBorderCornerRadius)
+ }
+}
+
+impl Parse for BorderSpacing {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Size2D::parse_with(context, input, |context, input| {
+ NonNegativeLength::parse_quirky(context, input, AllowQuirks::Yes)
+ })
+ .map(GenericBorderSpacing)
+ }
+}
+
+/// A single border-image-repeat keyword.
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum BorderImageRepeatKeyword {
+ Stretch,
+ Repeat,
+ Round,
+ Space,
+}
+
+/// The specified value for the `border-image-repeat` property.
+///
+/// https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct BorderImageRepeat(pub BorderImageRepeatKeyword, pub BorderImageRepeatKeyword);
+
+impl ToCss for BorderImageRepeat {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.0.to_css(dest)?;
+ if self.0 != self.1 {
+ dest.write_char(' ')?;
+ self.1.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+impl BorderImageRepeat {
+ /// Returns the `stretch` value.
+ #[inline]
+ pub fn stretch() -> Self {
+ BorderImageRepeat(
+ BorderImageRepeatKeyword::Stretch,
+ BorderImageRepeatKeyword::Stretch,
+ )
+ }
+}
+
+impl Parse for BorderImageRepeat {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let horizontal = BorderImageRepeatKeyword::parse(input)?;
+ let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok();
+ Ok(BorderImageRepeat(
+ horizontal,
+ vertical.unwrap_or(horizontal),
+ ))
+ }
+}
+
+/// Serializes a border shorthand value composed of width/style/color.
+pub fn serialize_directional_border<W>(
+ dest: &mut CssWriter<W>,
+ width: &BorderSideWidth,
+ style: &BorderStyle,
+ color: &Color,
+) -> fmt::Result
+where
+ W: Write,
+{
+ let has_style = *style != BorderStyle::None;
+ let has_color = *color != Color::CurrentColor;
+ let has_width = *width != BorderSideWidth::medium();
+ if !has_style && !has_color && !has_width {
+ return width.to_css(dest);
+ }
+ let mut writer = SequenceWriter::new(dest, " ");
+ if has_width {
+ writer.item(width)?;
+ }
+ if has_style {
+ writer.item(style)?;
+ }
+ if has_color {
+ writer.item(color)?;
+ }
+ Ok(())
+}
diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs
new file mode 100644
index 0000000000..8414591c2b
--- /dev/null
+++ b/servo/components/style/values/specified/box.rs
@@ -0,0 +1,1945 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for box properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::properties::{LonghandId, PropertyDeclarationId, PropertyId};
+use crate::values::generics::box_::{
+ GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign,
+ VerticalAlignKeyword,
+};
+use crate::values::specified::length::{LengthPercentage, NonNegativeLength};
+use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumberOrPercentage};
+use crate::values::CustomIdent;
+use cssparser::Parser;
+use num_traits::FromPrimitive;
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, KeywordsCollectFn, ParseError};
+use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+#[cfg(not(feature = "servo-layout-2020"))]
+fn flexbox_enabled() -> bool {
+ true
+}
+
+#[cfg(feature = "servo-layout-2020")]
+fn flexbox_enabled() -> bool {
+ servo_config::prefs::pref_map()
+ .get("layout.flexbox.enabled")
+ .as_bool()
+ .unwrap_or(false)
+}
+
+/// Defines an element’s display type, which consists of
+/// the two basic qualities of how an element generates boxes
+/// <https://drafts.csswg.org/css-display/#propdef-display>
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+pub enum DisplayOutside {
+ None = 0,
+ Inline,
+ Block,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableCaption,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ InternalTable,
+ #[cfg(feature = "gecko")]
+ InternalRuby,
+}
+
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+pub enum DisplayInside {
+ None = 0,
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ Contents,
+ Flow,
+ FlowRoot,
+ Flex,
+ #[cfg(feature = "gecko")]
+ Grid,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ Table,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableRowGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableColumn,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableColumnGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableHeaderGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableFooterGroup,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableRow,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ TableCell,
+ #[cfg(feature = "gecko")]
+ Ruby,
+ #[cfg(feature = "gecko")]
+ RubyBase,
+ #[cfg(feature = "gecko")]
+ RubyBaseContainer,
+ #[cfg(feature = "gecko")]
+ RubyText,
+ #[cfg(feature = "gecko")]
+ RubyTextContainer,
+ #[cfg(feature = "gecko")]
+ WebkitBox,
+}
+
+impl DisplayInside {
+ fn is_valid_for_list_item(self) -> bool {
+ match self {
+ DisplayInside::Flow => true,
+ #[cfg(feature = "gecko")]
+ DisplayInside::FlowRoot => true,
+ _ => false,
+ }
+ }
+
+ /// https://drafts.csswg.org/css-display/#inside-model:
+ /// If <display-outside> is omitted, the element’s outside display type defaults to block
+ /// — except for ruby, which defaults to inline.
+ fn default_display_outside(self) -> DisplayOutside {
+ match self {
+ #[cfg(feature = "gecko")]
+ DisplayInside::Ruby => DisplayOutside::Inline,
+ _ => DisplayOutside::Block,
+ }
+ }
+}
+
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct Display(u16);
+
+/// Gecko-only impl block for Display (shared stuff later in this file):
+#[allow(missing_docs)]
+#[allow(non_upper_case_globals)]
+impl Display {
+ // Our u16 bits are used as follows: LOOOOOOOIIIIIIII
+ pub const LIST_ITEM_MASK: u16 = 0b1000000000000000;
+ pub const OUTSIDE_MASK: u16 = 0b0111111100000000;
+ pub const INSIDE_MASK: u16 = 0b0000000011111111;
+ pub const OUTSIDE_SHIFT: u16 = 8;
+
+ /// https://drafts.csswg.org/css-display/#the-display-properties
+ /// ::new() inlined so cbindgen can use it
+ pub const None: Self =
+ Self(((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::None as u16);
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ pub const Contents: Self = Self(
+ ((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Contents as u16,
+ );
+ pub const Inline: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16);
+ pub const InlineBlock: Self = Self(
+ ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16,
+ );
+ pub const Block: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16);
+ #[cfg(feature = "gecko")]
+ pub const FlowRoot: Self = Self(
+ ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16,
+ );
+ pub const Flex: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16);
+ pub const InlineFlex: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16);
+ #[cfg(feature = "gecko")]
+ pub const Grid: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16);
+ #[cfg(feature = "gecko")]
+ pub const InlineGrid: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16);
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const Table: Self =
+ Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16);
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const InlineTable: Self = Self(
+ ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableCaption: Self = Self(
+ ((DisplayOutside::TableCaption as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const Ruby: Self =
+ Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Ruby as u16);
+ #[cfg(feature = "gecko")]
+ pub const WebkitBox: Self = Self(
+ ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const WebkitInlineBox: Self = Self(
+ ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16,
+ );
+
+ // Internal table boxes.
+
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableRowGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableRowGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableHeaderGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableHeaderGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableFooterGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableFooterGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableColumn: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableColumn as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableColumnGroup: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableColumnGroup as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableRow: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableRow as u16,
+ );
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ pub const TableCell: Self = Self(
+ ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::TableCell as u16,
+ );
+
+ /// Internal ruby boxes.
+ #[cfg(feature = "gecko")]
+ pub const RubyBase: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyBase as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const RubyBaseContainer: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyBaseContainer as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const RubyText: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyText as u16,
+ );
+ #[cfg(feature = "gecko")]
+ pub const RubyTextContainer: Self = Self(
+ ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) |
+ DisplayInside::RubyTextContainer as u16,
+ );
+
+ /// Make a raw display value from <display-outside> and <display-inside> values.
+ #[inline]
+ const fn new(outside: DisplayOutside, inside: DisplayInside) -> Self {
+ Self((outside as u16) << Self::OUTSIDE_SHIFT | inside as u16)
+ }
+
+ /// Make a display enum value from <display-outside> and <display-inside> values.
+ #[inline]
+ fn from3(outside: DisplayOutside, inside: DisplayInside, list_item: bool) -> Self {
+ let v = Self::new(outside, inside);
+ if !list_item {
+ return v;
+ }
+ Self(v.0 | Self::LIST_ITEM_MASK)
+ }
+
+ /// Accessor for the <display-inside> value.
+ #[inline]
+ pub fn inside(&self) -> DisplayInside {
+ DisplayInside::from_u16(self.0 & Self::INSIDE_MASK).unwrap()
+ }
+
+ /// Accessor for the <display-outside> value.
+ #[inline]
+ pub fn outside(&self) -> DisplayOutside {
+ DisplayOutside::from_u16((self.0 & Self::OUTSIDE_MASK) >> Self::OUTSIDE_SHIFT).unwrap()
+ }
+
+ /// Returns the raw underlying u16 value.
+ #[inline]
+ pub const fn to_u16(&self) -> u16 {
+ self.0
+ }
+
+ /// Whether this is `display: inline` (or `inline list-item`).
+ #[inline]
+ pub fn is_inline_flow(&self) -> bool {
+ self.outside() == DisplayOutside::Inline && self.inside() == DisplayInside::Flow
+ }
+
+ /// Returns whether this `display` value is some kind of list-item.
+ #[inline]
+ pub const fn is_list_item(&self) -> bool {
+ (self.0 & Self::LIST_ITEM_MASK) != 0
+ }
+
+ /// Returns whether this `display` value is a ruby level container.
+ pub fn is_ruby_level_container(&self) -> bool {
+ match *self {
+ #[cfg(feature = "gecko")]
+ Display::RubyBaseContainer | Display::RubyTextContainer => true,
+ _ => false,
+ }
+ }
+
+ /// Returns whether this `display` value is one of the types for ruby.
+ pub fn is_ruby_type(&self) -> bool {
+ match self.inside() {
+ #[cfg(feature = "gecko")]
+ DisplayInside::Ruby |
+ DisplayInside::RubyBase |
+ DisplayInside::RubyText |
+ DisplayInside::RubyBaseContainer |
+ DisplayInside::RubyTextContainer => true,
+ _ => false,
+ }
+ }
+}
+
+/// Shared Display impl for both Gecko and Servo.
+impl Display {
+ /// The initial display value.
+ #[inline]
+ pub fn inline() -> Self {
+ Display::Inline
+ }
+
+ /// <https://drafts.csswg.org/css2/visuren.html#x13>
+ #[cfg(feature = "servo")]
+ #[inline]
+ pub fn is_atomic_inline_level(&self) -> bool {
+ match *self {
+ Display::InlineBlock | Display::InlineFlex => true,
+ #[cfg(any(feature = "servo-layout-2013"))]
+ Display::InlineTable => true,
+ _ => false,
+ }
+ }
+
+ /// Returns whether this `display` value is the display of a flex or
+ /// grid container.
+ ///
+ /// This is used to implement various style fixups.
+ pub fn is_item_container(&self) -> bool {
+ match self.inside() {
+ DisplayInside::Flex => true,
+ #[cfg(feature = "gecko")]
+ DisplayInside::Grid => true,
+ _ => false,
+ }
+ }
+
+ /// Returns whether an element with this display type is a line
+ /// participant, which means it may lay its children on the same
+ /// line as itself.
+ pub fn is_line_participant(&self) -> bool {
+ if self.is_inline_flow() {
+ return true;
+ }
+ match *self {
+ #[cfg(feature = "gecko")]
+ Display::Contents | Display::Ruby | Display::RubyBaseContainer => true,
+ _ => false,
+ }
+ }
+
+ /// Convert this display into an equivalent block display.
+ ///
+ /// Also used for :root style adjustments.
+ pub fn equivalent_block_display(&self, _is_root_element: bool) -> Self {
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ {
+ // Special handling for `contents` and `list-item`s on the root element.
+ if _is_root_element && (self.is_contents() || self.is_list_item()) {
+ return Display::Block;
+ }
+ }
+
+ match self.outside() {
+ DisplayOutside::Inline => {
+ let inside = match self.inside() {
+ // `inline-block` blockifies to `block` rather than
+ // `flow-root`, for legacy reasons.
+ DisplayInside::FlowRoot => DisplayInside::Flow,
+ inside => inside,
+ };
+ Display::from3(DisplayOutside::Block, inside, self.is_list_item())
+ },
+ DisplayOutside::Block | DisplayOutside::None => *self,
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ _ => Display::Block,
+ }
+ }
+
+ /// Convert this display into an equivalent inline-outside display.
+ /// https://drafts.csswg.org/css-display/#inlinify
+ #[cfg(feature = "gecko")]
+ pub fn inlinify(&self) -> Self {
+ match self.outside() {
+ DisplayOutside::Block => {
+ let inside = match self.inside() {
+ // `display: block` inlinifies to `display: inline-block`,
+ // rather than `inline`, for legacy reasons.
+ DisplayInside::Flow => DisplayInside::FlowRoot,
+ inside => inside,
+ };
+ Display::from3(DisplayOutside::Inline, inside, self.is_list_item())
+ },
+ _ => *self,
+ }
+ }
+
+ /// Returns true if the value is `Contents`
+ #[inline]
+ pub fn is_contents(&self) -> bool {
+ match *self {
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ Display::Contents => true,
+ _ => false,
+ }
+ }
+
+ /// Returns true if the value is `None`
+ #[inline]
+ pub fn is_none(&self) -> bool {
+ *self == Display::None
+ }
+}
+
+enum DisplayKeyword {
+ Full(Display),
+ Inside(DisplayInside),
+ Outside(DisplayOutside),
+ ListItem,
+}
+
+impl DisplayKeyword {
+ fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ use self::DisplayKeyword::*;
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "none" => Full(Display::None),
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ "contents" => Full(Display::Contents),
+ "inline-block" => Full(Display::InlineBlock),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "inline-table" => Full(Display::InlineTable),
+ "-webkit-flex" if flexbox_enabled() => Full(Display::Flex),
+ "inline-flex" | "-webkit-inline-flex" if flexbox_enabled() => Full(Display::InlineFlex),
+ #[cfg(feature = "gecko")]
+ "inline-grid" => Full(Display::InlineGrid),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-caption" => Full(Display::TableCaption),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-row-group" => Full(Display::TableRowGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-header-group" => Full(Display::TableHeaderGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-footer-group" => Full(Display::TableFooterGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-column" => Full(Display::TableColumn),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-column-group" => Full(Display::TableColumnGroup),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-row" => Full(Display::TableRow),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table-cell" => Full(Display::TableCell),
+ #[cfg(feature = "gecko")]
+ "ruby-base" => Full(Display::RubyBase),
+ #[cfg(feature = "gecko")]
+ "ruby-base-container" => Full(Display::RubyBaseContainer),
+ #[cfg(feature = "gecko")]
+ "ruby-text" => Full(Display::RubyText),
+ #[cfg(feature = "gecko")]
+ "ruby-text-container" => Full(Display::RubyTextContainer),
+ #[cfg(feature = "gecko")]
+ "-webkit-box" => Full(Display::WebkitBox),
+ #[cfg(feature = "gecko")]
+ "-webkit-inline-box" => Full(Display::WebkitInlineBox),
+
+ /// <display-outside> = block | inline | run-in
+ /// https://drafts.csswg.org/css-display/#typedef-display-outside
+ "block" => Outside(DisplayOutside::Block),
+ "inline" => Outside(DisplayOutside::Inline),
+
+ "list-item" => ListItem,
+
+ /// <display-inside> = flow | flow-root | table | flex | grid | ruby
+ /// https://drafts.csswg.org/css-display/#typedef-display-inside
+ "flow" => Inside(DisplayInside::Flow),
+ "flex" if flexbox_enabled() => Inside(DisplayInside::Flex),
+ #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))]
+ "flow-root" => Inside(DisplayInside::FlowRoot),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ "table" => Inside(DisplayInside::Table),
+ #[cfg(feature = "gecko")]
+ "grid" => Inside(DisplayInside::Grid),
+ #[cfg(feature = "gecko")]
+ "ruby" => Inside(DisplayInside::Ruby),
+ })
+ }
+}
+
+impl ToCss for Display {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ let outside = self.outside();
+ let inside = self.inside();
+ match *self {
+ Display::Block | Display::Inline => outside.to_css(dest),
+ Display::InlineBlock => dest.write_str("inline-block"),
+ #[cfg(feature = "gecko")]
+ Display::WebkitInlineBox => dest.write_str("-webkit-inline-box"),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ Display::TableCaption => dest.write_str("table-caption"),
+ _ => match (outside, inside) {
+ #[cfg(feature = "gecko")]
+ (DisplayOutside::Inline, DisplayInside::Grid) => dest.write_str("inline-grid"),
+ (DisplayOutside::Inline, DisplayInside::Flex) => dest.write_str("inline-flex"),
+ #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))]
+ (DisplayOutside::Inline, DisplayInside::Table) => dest.write_str("inline-table"),
+ #[cfg(feature = "gecko")]
+ (DisplayOutside::Block, DisplayInside::Ruby) => dest.write_str("block ruby"),
+ (_, inside) => {
+ if self.is_list_item() {
+ if outside != DisplayOutside::Block {
+ outside.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ if inside != DisplayInside::Flow {
+ inside.to_css(dest)?;
+ dest.write_char(' ')?;
+ }
+ dest.write_str("list-item")
+ } else {
+ inside.to_css(dest)
+ }
+ },
+ },
+ }
+ }
+}
+
+impl Parse for Display {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Display, ParseError<'i>> {
+ let mut got_list_item = false;
+ let mut inside = None;
+ let mut outside = None;
+ match DisplayKeyword::parse(input)? {
+ DisplayKeyword::Full(d) => return Ok(d),
+ DisplayKeyword::Outside(o) => {
+ outside = Some(o);
+ },
+ DisplayKeyword::Inside(i) => {
+ inside = Some(i);
+ },
+ DisplayKeyword::ListItem => {
+ got_list_item = true;
+ },
+ };
+
+ while let Ok(kw) = input.try_parse(DisplayKeyword::parse) {
+ match kw {
+ DisplayKeyword::ListItem if !got_list_item => {
+ got_list_item = true;
+ },
+ DisplayKeyword::Outside(o) if outside.is_none() => {
+ outside = Some(o);
+ },
+ DisplayKeyword::Inside(i) if inside.is_none() => {
+ inside = Some(i);
+ },
+ _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ let inside = inside.unwrap_or(DisplayInside::Flow);
+ let outside = outside.unwrap_or_else(|| inside.default_display_outside());
+ if got_list_item && !inside.is_valid_for_list_item() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ return Ok(Display::from3(outside, inside, got_list_item));
+ }
+}
+
+impl SpecifiedValueInfo for Display {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&[
+ "block",
+ "contents",
+ "flex",
+ "flow-root",
+ "flow-root list-item",
+ "grid",
+ "inline",
+ "inline-block",
+ "inline-flex",
+ "inline-grid",
+ "inline-table",
+ "inline list-item",
+ "inline flow-root list-item",
+ "list-item",
+ "none",
+ "block ruby",
+ "ruby",
+ "ruby-base",
+ "ruby-base-container",
+ "ruby-text",
+ "ruby-text-container",
+ "table",
+ "table-caption",
+ "table-cell",
+ "table-column",
+ "table-column-group",
+ "table-footer-group",
+ "table-header-group",
+ "table-row",
+ "table-row-group",
+ "-webkit-box",
+ "-webkit-inline-box",
+ ]);
+ }
+}
+
+/// A specified value for the `contain-intrinsic-size` property.
+pub type ContainIntrinsicSize = GenericContainIntrinsicSize<NonNegativeLength>;
+
+/// A specified value for the `line-clamp` property.
+pub type LineClamp = GenericLineClamp<Integer>;
+
+/// A specified value for the `vertical-align` property.
+pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>;
+
+impl Parse for VerticalAlign {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(lp) =
+ input.try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes))
+ {
+ return Ok(GenericVerticalAlign::Length(lp));
+ }
+
+ Ok(GenericVerticalAlign::Keyword(VerticalAlignKeyword::parse(
+ input,
+ )?))
+ }
+}
+
+/// A specified value for the `baseline-source` property.
+/// https://drafts.csswg.org/css-inline-3/#baseline-source
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToShmem,
+ ToComputedValue,
+ ToResolvedValue,
+)]
+#[repr(u8)]
+pub enum BaselineSource {
+ /// `Last` for `inline-block`, `First` otherwise.
+ Auto,
+ /// Use first baseline for alignment.
+ First,
+ /// Use last baseline for alignment.
+ Last,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#snap-axis
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapAxis {
+ X,
+ Y,
+ Block,
+ Inline,
+ Both,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapStrictness {
+ #[css(skip)]
+ None, // Used to represent scroll-snap-type: none. It's not parsed.
+ Mandatory,
+ Proximity,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ScrollSnapType {
+ axis: ScrollSnapAxis,
+ strictness: ScrollSnapStrictness,
+}
+
+impl ScrollSnapType {
+ /// Returns `none`.
+ #[inline]
+ pub fn none() -> Self {
+ Self {
+ axis: ScrollSnapAxis::Both,
+ strictness: ScrollSnapStrictness::None,
+ }
+ }
+}
+
+impl Parse for ScrollSnapType {
+ /// none | [ x | y | block | inline | both ] [ mandatory | proximity ]?
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(ScrollSnapType::none());
+ }
+
+ let axis = ScrollSnapAxis::parse(input)?;
+ let strictness = input
+ .try_parse(ScrollSnapStrictness::parse)
+ .unwrap_or(ScrollSnapStrictness::Proximity);
+ Ok(Self { axis, strictness })
+ }
+}
+
+impl ToCss for ScrollSnapType {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.strictness == ScrollSnapStrictness::None {
+ return dest.write_str("none");
+ }
+ self.axis.to_css(dest)?;
+ if self.strictness != ScrollSnapStrictness::Proximity {
+ dest.write_char(' ')?;
+ self.strictness.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// Specified value of scroll-snap-align keyword value.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapAlignKeyword {
+ None,
+ Start,
+ End,
+ Center,
+}
+
+/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ScrollSnapAlign {
+ block: ScrollSnapAlignKeyword,
+ inline: ScrollSnapAlignKeyword,
+}
+
+impl ScrollSnapAlign {
+ /// Returns `none`.
+ #[inline]
+ pub fn none() -> Self {
+ ScrollSnapAlign {
+ block: ScrollSnapAlignKeyword::None,
+ inline: ScrollSnapAlignKeyword::None,
+ }
+ }
+}
+
+impl Parse for ScrollSnapAlign {
+ /// [ none | start | end | center ]{1,2}
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<ScrollSnapAlign, ParseError<'i>> {
+ let block = ScrollSnapAlignKeyword::parse(input)?;
+ let inline = input
+ .try_parse(ScrollSnapAlignKeyword::parse)
+ .unwrap_or(block);
+ Ok(ScrollSnapAlign { block, inline })
+ }
+}
+
+impl ToCss for ScrollSnapAlign {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ self.block.to_css(dest)?;
+ if self.block != self.inline {
+ dest.write_char(' ')?;
+ self.inline.to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ScrollSnapStop {
+ Normal,
+ Always,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum OverscrollBehavior {
+ Auto,
+ Contain,
+ None,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum OverflowAnchor {
+ Auto,
+ None,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum OverflowClipBox {
+ PaddingBox,
+ ContentBox,
+}
+
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(comma)]
+#[repr(C)]
+/// Provides a rendering hint to the user agent, stating what kinds of changes
+/// the author expects to perform on the element.
+///
+/// `auto` is represented by an empty `features` list.
+///
+/// <https://drafts.csswg.org/css-will-change/#will-change>
+pub struct WillChange {
+ /// The features that are supposed to change.
+ ///
+ /// TODO(emilio): Consider using ArcSlice since we just clone them from the
+ /// specified value? That'd save an allocation, which could be worth it.
+ #[css(iterable, if_empty = "auto")]
+ features: crate::OwnedSlice<CustomIdent>,
+ /// A bitfield with the kind of change that the value will create, based
+ /// on the above field.
+ #[css(skip)]
+ bits: WillChangeBits,
+}
+
+impl WillChange {
+ #[inline]
+ /// Get default value of `will-change` as `auto`
+ pub fn auto() -> Self {
+ Self::default()
+ }
+}
+
+/// The change bits that we care about.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct WillChangeBits(u16);
+bitflags! {
+ impl WillChangeBits: u16 {
+ /// Whether a property which can create a stacking context **on any
+ /// box** will change.
+ const STACKING_CONTEXT_UNCONDITIONAL = 1 << 0;
+ /// Whether `transform` or related properties will change.
+ const TRANSFORM = 1 << 1;
+ /// Whether `scroll-position` will change.
+ const SCROLL = 1 << 2;
+ /// Whether `contain` will change.
+ const CONTAIN = 1 << 3;
+ /// Whether `opacity` will change.
+ const OPACITY = 1 << 4;
+ /// Whether `perspective` will change.
+ const PERSPECTIVE = 1 << 5;
+ /// Whether `z-index` will change.
+ const Z_INDEX = 1 << 6;
+ /// Whether any property which creates a containing block for non-svg
+ /// text frames will change.
+ const FIXPOS_CB_NON_SVG = 1 << 7;
+ /// Whether the position property will change.
+ const POSITION = 1 << 8;
+ }
+}
+
+fn change_bits_for_longhand(longhand: LonghandId) -> WillChangeBits {
+ match longhand {
+ LonghandId::Opacity => WillChangeBits::OPACITY,
+ LonghandId::Contain => WillChangeBits::CONTAIN,
+ LonghandId::Perspective => WillChangeBits::PERSPECTIVE,
+ LonghandId::Position => {
+ WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::POSITION
+ },
+ LonghandId::ZIndex => WillChangeBits::Z_INDEX,
+ LonghandId::Transform |
+ LonghandId::TransformStyle |
+ LonghandId::Translate |
+ LonghandId::Rotate |
+ LonghandId::Scale |
+ LonghandId::OffsetPath => WillChangeBits::TRANSFORM,
+ LonghandId::BackdropFilter | LonghandId::Filter => {
+ WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::FIXPOS_CB_NON_SVG
+ },
+ LonghandId::MixBlendMode |
+ LonghandId::Isolation |
+ LonghandId::MaskImage |
+ LonghandId::ClipPath => WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL,
+ _ => WillChangeBits::empty(),
+ }
+}
+
+fn change_bits_for_maybe_property(ident: &str, context: &ParserContext) -> WillChangeBits {
+ let id = match PropertyId::parse_ignoring_rule_type(ident, context) {
+ Ok(id) => id,
+ Err(..) => return WillChangeBits::empty(),
+ };
+
+ match id.as_shorthand() {
+ Ok(shorthand) => shorthand
+ .longhands()
+ .fold(WillChangeBits::empty(), |flags, p| {
+ flags | change_bits_for_longhand(p)
+ }),
+ Err(PropertyDeclarationId::Longhand(longhand)) => change_bits_for_longhand(longhand),
+ Err(PropertyDeclarationId::Custom(..)) => WillChangeBits::empty(),
+ }
+}
+
+impl Parse for WillChange {
+ /// auto | <animateable-feature>#
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("auto"))
+ .is_ok()
+ {
+ return Ok(Self::default());
+ }
+
+ let mut bits = WillChangeBits::empty();
+ let custom_idents = input.parse_comma_separated(|i| {
+ let location = i.current_source_location();
+ let parser_ident = i.expect_ident()?;
+ let ident = CustomIdent::from_ident(
+ location,
+ parser_ident,
+ &["will-change", "none", "all", "auto"],
+ )?;
+
+ if context.in_ua_sheet() && ident.0 == atom!("-moz-fixed-pos-containing-block") {
+ bits |= WillChangeBits::FIXPOS_CB_NON_SVG;
+ } else if ident.0 == atom!("scroll-position") {
+ bits |= WillChangeBits::SCROLL;
+ } else {
+ bits |= change_bits_for_maybe_property(&parser_ident, context);
+ }
+ Ok(ident)
+ })?;
+
+ Ok(Self {
+ features: custom_idents.into(),
+ bits,
+ })
+ }
+}
+
+/// Values for the `touch-action` property.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(single = "none,auto,manipulation", mixed = "pan-x,pan-y,pinch-zoom"))]
+#[repr(C)]
+pub struct TouchAction(u8);
+bitflags! {
+ impl TouchAction: u8 {
+ /// `none` variant
+ const NONE = 1 << 0;
+ /// `auto` variant
+ const AUTO = 1 << 1;
+ /// `pan-x` variant
+ const PAN_X = 1 << 2;
+ /// `pan-y` variant
+ const PAN_Y = 1 << 3;
+ /// `manipulation` variant
+ const MANIPULATION = 1 << 4;
+ /// `pinch-zoom` variant
+ const PINCH_ZOOM = 1 << 5;
+ }
+}
+
+impl TouchAction {
+ #[inline]
+ /// Get default `touch-action` as `auto`
+ pub fn auto() -> TouchAction {
+ TouchAction::AUTO
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(
+ single = "none,strict,content",
+ mixed = "size,layout,style,paint,inline-size",
+ overlapping_bits
+))]
+#[repr(C)]
+/// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property
+pub struct Contain(u8);
+bitflags! {
+ impl Contain: u8 {
+ /// `none` variant, just for convenience.
+ const NONE = 0;
+ /// `inline-size` variant, turns on single-axis inline size containment
+ const INLINE_SIZE = 1 << 0;
+ /// `block-size` variant, turns on single-axis block size containment, internal only
+ const BLOCK_SIZE = 1 << 1;
+ /// `layout` variant, turns on layout containment
+ const LAYOUT = 1 << 2;
+ /// `style` variant, turns on style containment
+ const STYLE = 1 << 3;
+ /// `paint` variant, turns on paint containment
+ const PAINT = 1 << 4;
+ /// 'size' variant, turns on size containment
+ const SIZE = 1 << 5 | Contain::INLINE_SIZE.bits() | Contain::BLOCK_SIZE.bits();
+ /// `content` variant, turns on layout and paint containment
+ const CONTENT = 1 << 6 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits();
+ /// `strict` variant, turns on all types of containment
+ const STRICT = 1 << 7 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits() | Contain::SIZE.bits();
+ }
+}
+
+impl Parse for ContainIntrinsicSize {
+ /// none | <length> | auto <length>
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(l) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
+ return Ok(Self::Length(l));
+ }
+
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(Self::AutoNone);
+ }
+
+ let l = NonNegativeLength::parse(context, input)?;
+ return Ok(Self::AutoLength(l));
+ }
+
+ input.expect_ident_matching("none")?;
+ Ok(Self::None)
+ }
+}
+
+impl Parse for LineClamp {
+ /// none | <positive-integer>
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(i) =
+ input.try_parse(|i| crate::values::specified::PositiveInteger::parse(context, i))
+ {
+ return Ok(Self(i.0));
+ }
+ input.expect_ident_matching("none")?;
+ Ok(Self::none())
+ }
+}
+
+/// https://drafts.csswg.org/css-contain-2/#content-visibility
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ContentVisibility {
+ /// `auto` variant, the element turns on layout containment, style containment, and paint
+ /// containment. In addition, if the element is not relevant to the user (such as by being
+ /// offscreen) it also skips its content
+ Auto,
+ /// `hidden` variant, the element skips its content
+ Hidden,
+ /// 'visible' variant, no effect
+ Visible,
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ PartialEq,
+ Eq,
+ MallocSizeOf,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ Parse,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+#[allow(missing_docs)]
+/// https://drafts.csswg.org/css-contain-3/#container-type
+pub enum ContainerType {
+ /// The `normal` variant.
+ Normal,
+ /// The `inline-size` variant.
+ InlineSize,
+ /// The `size` variant.
+ Size,
+}
+
+impl ContainerType {
+ /// Is this container-type: normal?
+ pub fn is_normal(self) -> bool {
+ self == Self::Normal
+ }
+
+ /// Is this type containing size in any way?
+ pub fn is_size_container_type(self) -> bool {
+ !self.is_normal()
+ }
+}
+
+/// https://drafts.csswg.org/css-contain-3/#container-name
+#[repr(transparent)]
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct ContainerName(#[css(iterable, if_empty = "none")] pub crate::OwnedSlice<CustomIdent>);
+
+impl ContainerName {
+ /// Return the `none` value.
+ pub fn none() -> Self {
+ Self(Default::default())
+ }
+
+ /// Returns whether this is the `none` value.
+ pub fn is_none(&self) -> bool {
+ self.0.is_empty()
+ }
+
+ fn parse_internal<'i>(
+ input: &mut Parser<'i, '_>,
+ for_query: bool,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut idents = vec![];
+ let location = input.current_source_location();
+ let first = input.expect_ident()?;
+ if !for_query && first.eq_ignore_ascii_case("none") {
+ return Ok(Self::none());
+ }
+ const DISALLOWED_CONTAINER_NAMES: &'static [&'static str] = &["none", "not", "or", "and"];
+ idents.push(CustomIdent::from_ident(
+ location,
+ first,
+ DISALLOWED_CONTAINER_NAMES,
+ )?);
+ if !for_query {
+ while let Ok(name) =
+ input.try_parse(|input| CustomIdent::parse(input, DISALLOWED_CONTAINER_NAMES))
+ {
+ idents.push(name);
+ }
+ }
+ Ok(ContainerName(idents.into()))
+ }
+
+ /// https://github.com/w3c/csswg-drafts/issues/7203
+ /// Only a single name allowed in @container rule.
+ /// Disallow none for container-name in @container rule.
+ pub fn parse_for_query<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(input, /* for_query = */ true)
+ }
+}
+
+impl Parse for ContainerName {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(input, /* for_query = */ false)
+ }
+}
+
+/// A specified value for the `perspective` property.
+pub type Perspective = GenericPerspective<NonNegativeLength>;
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+/// https://drafts.csswg.org/css-box/#propdef-float
+pub enum Float {
+ Left,
+ Right,
+ None,
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ InlineStart,
+ InlineEnd,
+}
+
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+/// https://drafts.csswg.org/css2/#propdef-clear
+pub enum Clear {
+ None,
+ Left,
+ Right,
+ Both,
+ // https://drafts.csswg.org/css-logical-props/#float-clear
+ InlineStart,
+ InlineEnd,
+}
+
+/// https://drafts.csswg.org/css-ui/#propdef-resize
+#[allow(missing_docs)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum Resize {
+ None,
+ Both,
+ Horizontal,
+ Vertical,
+ // https://drafts.csswg.org/css-logical-1/#resize
+ Inline,
+ Block,
+}
+
+/// The value for the `appearance` property.
+///
+/// https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum Appearance {
+ /// No appearance at all.
+ None,
+ /// Default appearance for the element.
+ ///
+ /// This value doesn't make sense for -moz-default-appearance, but we don't bother to guard
+ /// against parsing it.
+ Auto,
+ /// A searchfield.
+ Searchfield,
+ /// A multi-line text field, e.g. HTML <textarea>.
+ Textarea,
+ /// A checkbox element.
+ Checkbox,
+ /// A radio element within a radio group.
+ Radio,
+ /// A dropdown list.
+ Menulist,
+ /// List boxes.
+ Listbox,
+ /// A horizontal meter bar.
+ Meter,
+ /// A horizontal progress bar.
+ ProgressBar,
+ /// A typical dialog button.
+ Button,
+ /// A single-line text field, e.g. HTML <input type=text>.
+ Textfield,
+ /// The dropdown button(s) that open up a dropdown list.
+ MenulistButton,
+ /// Various arrows that go in buttons
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowDown,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowNext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowPrevious,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ButtonArrowUp,
+ /// A dual toolbar button (e.g., a Back button with a dropdown)
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Dualbutton,
+ /// Menu Popup background.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Menupopup,
+ /// The meter bar's meter indicator.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Meterchunk,
+ /// The "arrowed" part of the dropdown button that open up a dropdown list.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMenulistArrowButton,
+ /// For HTML's <input type=number>
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ NumberInput,
+ /// The progress bar's progress indicator
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Progresschunk,
+ /// nsRangeFrame and its subparts
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Range,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ RangeThumb,
+ /// The scrollbar slider
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarHorizontal,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarVertical,
+ /// A scrollbar button (up/down/left/right).
+ /// Keep these in order (some code casts these values to `int` in order to
+ /// compare them against each other).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonUp,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonDown,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonLeft,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarbuttonRight,
+ /// The scrollbar thumb.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarthumbHorizontal,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbarthumbVertical,
+ /// The scrollbar track.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbartrackHorizontal,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ScrollbartrackVertical,
+ /// The scroll corner
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Scrollcorner,
+ /// A separator. Can be horizontal or vertical.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Separator,
+ /// A spin control (up/down control for time/date pickers).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Spinner,
+ /// The up button of a spin control.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ SpinnerUpbutton,
+ /// The down button of a spin control.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ SpinnerDownbutton,
+ /// The textfield of a spin control
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ SpinnerTextfield,
+ /// A splitter. Can be horizontal or vertical.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Splitter,
+ /// A status bar in a main application window.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Statusbar,
+ /// A single tab in a tab widget.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tab,
+ /// A single pane (inside the tabpanels container).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tabpanel,
+ /// The tab panels container.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tabpanels,
+ /// The tabs scroll arrows (left/right).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ TabScrollArrowBack,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ TabScrollArrowForward,
+ /// A toolbar in an application window.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Toolbar,
+ /// A single toolbar button (with no associated dropdown).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Toolbarbutton,
+ /// The dropdown portion of a toolbar button
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ ToolbarbuttonDropdown,
+ /// The toolbox that contains the toolbars.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Toolbox,
+ /// A tooltip.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Tooltip,
+ /// A listbox or tree widget header
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeheader,
+ /// An individual header cell
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeheadercell,
+ /// A tree item.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeitem,
+ /// A tree widget branch line
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeline,
+ /// A tree widget twisty.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treetwisty,
+ /// Open tree widget twisty.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treetwistyopen,
+ /// A tree widget.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ Treeview,
+
+ /// Mac help button.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacHelpButton,
+
+ /// An appearance value for the root, so that we can get unified toolbar looks (which require a
+ /// transparent gecko background) without really using the whole transparency set-up which
+ /// otherwise loses window borders, see bug 1870481.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacUnifiedToolbarWindow,
+
+ /// Windows themed window frame elements.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonBox,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonClose,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonMaximize,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonMinimize,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowButtonRestore,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowTitlebar,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowTitlebarMaximized,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozWindowDecorations,
+
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDisclosureButtonClosed,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDisclosureButtonOpen,
+
+ /// A themed focus outline (for outline:auto).
+ ///
+ /// This isn't exposed to CSS at all, just here for convenience.
+ #[css(skip)]
+ FocusOutline,
+
+ /// A dummy variant that should be last to let the GTK widget do hackery.
+ #[css(skip)]
+ Count,
+}
+
+/// A kind of break between two boxes.
+///
+/// https://drafts.csswg.org/css-break/#break-between
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum BreakBetween {
+ Always,
+ Auto,
+ Page,
+ Avoid,
+ Left,
+ Right,
+}
+
+impl BreakBetween {
+ /// Parse a legacy break-between value for `page-break-{before,after}`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ #[inline]
+ pub(crate) fn parse_legacy<'i>(
+ _: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ let break_value = BreakBetween::parse(input)?;
+ match break_value {
+ BreakBetween::Always => Ok(BreakBetween::Page),
+ BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => {
+ Ok(break_value)
+ },
+ BreakBetween::Page => {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ }
+ }
+
+ /// Serialize a legacy break-between value for `page-break-*`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => {
+ self.to_css(dest)
+ },
+ BreakBetween::Page => dest.write_str("always"),
+ BreakBetween::Always => Ok(()),
+ }
+ }
+}
+
+/// A kind of break within a box.
+///
+/// https://drafts.csswg.org/css-break/#break-within
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum BreakWithin {
+ Auto,
+ Avoid,
+ AvoidPage,
+ AvoidColumn,
+}
+
+impl BreakWithin {
+ /// Parse a legacy break-between value for `page-break-inside`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ #[inline]
+ pub(crate) fn parse_legacy<'i>(
+ _: &ParserContext,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<Self, ParseError<'i>> {
+ let break_value = BreakWithin::parse(input)?;
+ match break_value {
+ BreakWithin::Auto | BreakWithin::Avoid => Ok(break_value),
+ BreakWithin::AvoidPage | BreakWithin::AvoidColumn => {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ }
+ }
+
+ /// Serialize a legacy break-between value for `page-break-inside`.
+ ///
+ /// See https://drafts.csswg.org/css-break/#page-break-properties.
+ pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ BreakWithin::Auto | BreakWithin::Avoid => self.to_css(dest),
+ BreakWithin::AvoidPage | BreakWithin::AvoidColumn => Ok(()),
+ }
+ }
+}
+
+/// The value for the `overflow-x` / `overflow-y` properties.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum Overflow {
+ Visible,
+ Hidden,
+ Scroll,
+ Auto,
+ #[cfg(feature = "gecko")]
+ Clip,
+}
+
+// This can be derived once we remove or keep `-moz-hidden-unscrollable`
+// indefinitely.
+impl Parse for Overflow {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "visible" => Self::Visible,
+ "hidden" => Self::Hidden,
+ "scroll" => Self::Scroll,
+ "auto" | "overlay" => Self::Auto,
+ #[cfg(feature = "gecko")]
+ "clip" => Self::Clip,
+ #[cfg(feature = "gecko")]
+ "-moz-hidden-unscrollable" if static_prefs::pref!("layout.css.overflow-moz-hidden-unscrollable.enabled") => {
+ Overflow::Clip
+ },
+ })
+ }
+}
+
+impl Overflow {
+ /// Return true if the value will create a scrollable box.
+ #[inline]
+ pub fn is_scrollable(&self) -> bool {
+ matches!(*self, Self::Hidden | Self::Scroll | Self::Auto)
+ }
+ /// Convert the value to a scrollable value if it's not already scrollable.
+ /// This maps `visible` to `auto` and `clip` to `hidden`.
+ #[inline]
+ pub fn to_scrollable(&self) -> Self {
+ match *self {
+ Self::Hidden | Self::Scroll | Self::Auto => *self,
+ Self::Visible => Self::Auto,
+ #[cfg(feature = "gecko")]
+ Self::Clip => Self::Hidden,
+ }
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[css(bitflags(
+ single = "auto",
+ mixed = "stable,both-edges",
+ validate_mixed = "Self::has_stable"
+))]
+/// Values for scrollbar-gutter:
+/// <https://drafts.csswg.org/css-overflow-3/#scrollbar-gutter-property>
+pub struct ScrollbarGutter(u8);
+bitflags! {
+ impl ScrollbarGutter: u8 {
+ /// `auto` variant. Just for convenience if there is no flag set.
+ const AUTO = 0;
+ /// `stable` variant.
+ const STABLE = 1 << 0;
+ /// `both-edges` variant.
+ const BOTH_EDGES = 1 << 1;
+ }
+}
+
+impl ScrollbarGutter {
+ #[inline]
+ fn has_stable(&self) -> bool {
+ self.intersects(Self::STABLE)
+ }
+}
+
+/// A specified value for the zoom property.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum Zoom {
+ Normal,
+ /// An internal value that resets the effective zoom to 1. Used for scrollbar parts, which
+ /// disregard zoom. We use this name because WebKit has this value exposed to the web.
+ #[parse(condition = "ParserContext::in_ua_sheet")]
+ Document,
+ Value(NonNegativeNumberOrPercentage),
+}
+
+impl Zoom {
+ /// Return a particular number value of the zoom property.
+ #[inline]
+ pub fn new_number(n: f32) -> Self {
+ Self::Value(NonNegativeNumberOrPercentage::new_number(n))
+ }
+}
diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs
new file mode 100644
index 0000000000..2660864319
--- /dev/null
+++ b/servo/components/style/values/specified/calc.rs
@@ -0,0 +1,1086 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! [Calc expressions][calc].
+//!
+//! [calc]: https://drafts.csswg.org/css-values/#calc-notation
+
+use crate::color::parsing::{AngleOrNumber, NumberOrPercentage};
+use crate::parser::ParserContext;
+use crate::values::generics::calc::{
+ self as generic, CalcNodeLeaf, CalcUnits, MinMaxOp, ModRemOp, PositivePercentageBasis,
+ RoundingStrategy, SortKey,
+};
+use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength};
+use crate::values::specified::length::{ContainerRelativeLength, ViewportPercentageLength};
+use crate::values::specified::{self, Angle, Resolution, Time};
+use crate::values::{serialize_number, serialize_percentage, CSSFloat, CSSInteger};
+use cssparser::{CowRcStr, Parser, Token};
+use smallvec::SmallVec;
+use std::cmp;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+/// The name of the mathematical function that we're parsing.
+#[derive(Clone, Copy, Debug, Parse)]
+pub enum MathFunction {
+ /// `calc()`: https://drafts.csswg.org/css-values-4/#funcdef-calc
+ Calc,
+ /// `min()`: https://drafts.csswg.org/css-values-4/#funcdef-min
+ Min,
+ /// `max()`: https://drafts.csswg.org/css-values-4/#funcdef-max
+ Max,
+ /// `clamp()`: https://drafts.csswg.org/css-values-4/#funcdef-clamp
+ Clamp,
+ /// `round()`: https://drafts.csswg.org/css-values-4/#funcdef-round
+ Round,
+ /// `mod()`: https://drafts.csswg.org/css-values-4/#funcdef-mod
+ Mod,
+ /// `rem()`: https://drafts.csswg.org/css-values-4/#funcdef-rem
+ Rem,
+ /// `sin()`: https://drafts.csswg.org/css-values-4/#funcdef-sin
+ Sin,
+ /// `cos()`: https://drafts.csswg.org/css-values-4/#funcdef-cos
+ Cos,
+ /// `tan()`: https://drafts.csswg.org/css-values-4/#funcdef-tan
+ Tan,
+ /// `asin()`: https://drafts.csswg.org/css-values-4/#funcdef-asin
+ Asin,
+ /// `acos()`: https://drafts.csswg.org/css-values-4/#funcdef-acos
+ Acos,
+ /// `atan()`: https://drafts.csswg.org/css-values-4/#funcdef-atan
+ Atan,
+ /// `atan2()`: https://drafts.csswg.org/css-values-4/#funcdef-atan2
+ Atan2,
+ /// `pow()`: https://drafts.csswg.org/css-values-4/#funcdef-pow
+ Pow,
+ /// `sqrt()`: https://drafts.csswg.org/css-values-4/#funcdef-sqrt
+ Sqrt,
+ /// `hypot()`: https://drafts.csswg.org/css-values-4/#funcdef-hypot
+ Hypot,
+ /// `log()`: https://drafts.csswg.org/css-values-4/#funcdef-log
+ Log,
+ /// `exp()`: https://drafts.csswg.org/css-values-4/#funcdef-exp
+ Exp,
+ /// `abs()`: https://drafts.csswg.org/css-values-4/#funcdef-abs
+ Abs,
+ /// `sign()`: https://drafts.csswg.org/css-values-4/#funcdef-sign
+ Sign,
+}
+
+/// A leaf node inside a `Calc` expression's AST.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum Leaf {
+ /// `<length>`
+ Length(NoCalcLength),
+ /// `<angle>`
+ Angle(Angle),
+ /// `<time>`
+ Time(Time),
+ /// `<resolution>`
+ Resolution(Resolution),
+ /// `<percentage>`
+ Percentage(CSSFloat),
+ /// `<number>`
+ Number(CSSFloat),
+}
+
+impl Leaf {
+ fn as_length(&self) -> Option<&NoCalcLength> {
+ match *self {
+ Self::Length(ref l) => Some(l),
+ _ => None,
+ }
+ }
+}
+
+impl ToCss for Leaf {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Self::Length(ref l) => l.to_css(dest),
+ Self::Number(n) => serialize_number(n, /* was_calc = */ false, dest),
+ Self::Resolution(ref r) => r.to_css(dest),
+ Self::Percentage(p) => serialize_percentage(p, dest),
+ Self::Angle(ref a) => a.to_css(dest),
+ Self::Time(ref t) => t.to_css(dest),
+ }
+ }
+}
+
+/// A struct to hold a simplified `<length>` or `<percentage>` expression.
+///
+/// In some cases, e.g. DOMMatrix, we support calc(), but reject all the
+/// relative lengths, and to_computed_pixel_length_without_context() handles
+/// this case. Therefore, if you want to add a new field, please make sure this
+/// function work properly.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub struct CalcLengthPercentage {
+ #[css(skip)]
+ pub clamping_mode: AllowedNumericType,
+ pub node: CalcNode,
+}
+
+impl CalcLengthPercentage {
+ fn same_unit_length_as(a: &Self, b: &Self) -> Option<(CSSFloat, CSSFloat)> {
+ debug_assert_eq!(a.clamping_mode, b.clamping_mode);
+ debug_assert_eq!(a.clamping_mode, AllowedNumericType::All);
+
+ let a = a.node.as_leaf()?;
+ let b = b.node.as_leaf()?;
+
+ if a.sort_key() != b.sort_key() {
+ return None;
+ }
+
+ let a = a.as_length()?.unitless_value();
+ let b = b.as_length()?.unitless_value();
+ return Some((a, b));
+ }
+}
+
+impl SpecifiedValueInfo for CalcLengthPercentage {}
+
+impl generic::CalcNodeLeaf for Leaf {
+ fn unit(&self) -> CalcUnits {
+ match self {
+ Leaf::Length(_) => CalcUnits::LENGTH,
+ Leaf::Angle(_) => CalcUnits::ANGLE,
+ Leaf::Time(_) => CalcUnits::TIME,
+ Leaf::Resolution(_) => CalcUnits::RESOLUTION,
+ Leaf::Percentage(_) => CalcUnits::PERCENTAGE,
+ Leaf::Number(_) => CalcUnits::empty(),
+ }
+ }
+
+ fn unitless_value(&self) -> f32 {
+ match *self {
+ Self::Length(ref l) => l.unitless_value(),
+ Self::Percentage(n) | Self::Number(n) => n,
+ Self::Resolution(ref r) => r.dppx(),
+ Self::Angle(ref a) => a.degrees(),
+ Self::Time(ref t) => t.seconds(),
+ }
+ }
+
+ fn new_number(value: f32) -> Self {
+ Self::Number(value)
+ }
+
+ fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<cmp::Ordering> {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) {
+ return None;
+ }
+
+ let self_negative = self.is_negative();
+ if self_negative != other.is_negative() {
+ return Some(if self_negative { cmp::Ordering::Less } else { cmp::Ordering::Greater });
+ }
+
+ match (self, other) {
+ (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other),
+ (&Length(ref one), &Length(ref other)) => one.partial_cmp(other),
+ (&Angle(ref one), &Angle(ref other)) => one.degrees().partial_cmp(&other.degrees()),
+ (&Time(ref one), &Time(ref other)) => one.seconds().partial_cmp(&other.seconds()),
+ (&Resolution(ref one), &Resolution(ref other)) => one.dppx().partial_cmp(&other.dppx()),
+ (&Number(ref one), &Number(ref other)) => one.partial_cmp(other),
+ _ => {
+ match *self {
+ Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) |
+ Resolution(..) => {},
+ }
+ unsafe {
+ debug_unreachable!("Forgot a branch?");
+ }
+ },
+ }
+ }
+
+ fn as_number(&self) -> Option<f32> {
+ match *self {
+ Leaf::Length(_) |
+ Leaf::Angle(_) |
+ Leaf::Time(_) |
+ Leaf::Resolution(_) |
+ Leaf::Percentage(_) => None,
+ Leaf::Number(value) => Some(value),
+ }
+ }
+
+ fn sort_key(&self) -> SortKey {
+ match *self {
+ Self::Number(..) => SortKey::Number,
+ Self::Percentage(..) => SortKey::Percentage,
+ Self::Time(..) => SortKey::Sec,
+ Self::Resolution(..) => SortKey::Dppx,
+ Self::Angle(..) => SortKey::Deg,
+ Self::Length(ref l) => match *l {
+ NoCalcLength::Absolute(..) => SortKey::Px,
+ NoCalcLength::FontRelative(ref relative) => match *relative {
+ FontRelativeLength::Ch(..) => SortKey::Ch,
+ FontRelativeLength::Em(..) => SortKey::Em,
+ FontRelativeLength::Ex(..) => SortKey::Ex,
+ FontRelativeLength::Cap(..) => SortKey::Cap,
+ FontRelativeLength::Ic(..) => SortKey::Ic,
+ FontRelativeLength::Rem(..) => SortKey::Rem,
+ FontRelativeLength::Lh(..) => SortKey::Lh,
+ FontRelativeLength::Rlh(..) => SortKey::Rlh,
+ },
+ NoCalcLength::ViewportPercentage(ref vp) => match *vp {
+ ViewportPercentageLength::Vh(..) => SortKey::Vh,
+ ViewportPercentageLength::Svh(..) => SortKey::Svh,
+ ViewportPercentageLength::Lvh(..) => SortKey::Lvh,
+ ViewportPercentageLength::Dvh(..) => SortKey::Dvh,
+ ViewportPercentageLength::Vw(..) => SortKey::Vw,
+ ViewportPercentageLength::Svw(..) => SortKey::Svw,
+ ViewportPercentageLength::Lvw(..) => SortKey::Lvw,
+ ViewportPercentageLength::Dvw(..) => SortKey::Dvw,
+ ViewportPercentageLength::Vmax(..) => SortKey::Vmax,
+ ViewportPercentageLength::Svmax(..) => SortKey::Svmax,
+ ViewportPercentageLength::Lvmax(..) => SortKey::Lvmax,
+ ViewportPercentageLength::Dvmax(..) => SortKey::Dvmax,
+ ViewportPercentageLength::Vmin(..) => SortKey::Vmin,
+ ViewportPercentageLength::Svmin(..) => SortKey::Svmin,
+ ViewportPercentageLength::Lvmin(..) => SortKey::Lvmin,
+ ViewportPercentageLength::Dvmin(..) => SortKey::Dvmin,
+ ViewportPercentageLength::Vb(..) => SortKey::Vb,
+ ViewportPercentageLength::Svb(..) => SortKey::Svb,
+ ViewportPercentageLength::Lvb(..) => SortKey::Lvb,
+ ViewportPercentageLength::Dvb(..) => SortKey::Dvb,
+ ViewportPercentageLength::Vi(..) => SortKey::Vi,
+ ViewportPercentageLength::Svi(..) => SortKey::Svi,
+ ViewportPercentageLength::Lvi(..) => SortKey::Lvi,
+ ViewportPercentageLength::Dvi(..) => SortKey::Dvi,
+ },
+ NoCalcLength::ContainerRelative(ref cq) => match *cq {
+ ContainerRelativeLength::Cqw(..) => SortKey::Cqw,
+ ContainerRelativeLength::Cqh(..) => SortKey::Cqh,
+ ContainerRelativeLength::Cqi(..) => SortKey::Cqi,
+ ContainerRelativeLength::Cqb(..) => SortKey::Cqb,
+ ContainerRelativeLength::Cqmin(..) => SortKey::Cqmin,
+ ContainerRelativeLength::Cqmax(..) => SortKey::Cqmax,
+ },
+ NoCalcLength::ServoCharacterWidth(..) => unreachable!(),
+ },
+ }
+ }
+
+ fn simplify(&mut self) {
+ if let Self::Length(NoCalcLength::Absolute(ref mut abs)) = *self {
+ *abs = AbsoluteLength::Px(abs.to_px());
+ }
+ }
+
+ /// Tries to merge one sum to another, that is, perform `x` + `y`.
+ ///
+ /// Only handles leaf nodes, it's the caller's responsibility to simplify
+ /// them before calling this if needed.
+ fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&mut Number(ref mut one), &Number(ref other)) |
+ (&mut Percentage(ref mut one), &Percentage(ref other)) => {
+ *one += *other;
+ },
+ (&mut Angle(ref mut one), &Angle(ref other)) => {
+ *one = specified::Angle::from_calc(one.degrees() + other.degrees());
+ },
+ (&mut Time(ref mut one), &Time(ref other)) => {
+ *one = specified::Time::from_seconds(one.seconds() + other.seconds());
+ },
+ (&mut Resolution(ref mut one), &Resolution(ref other)) => {
+ *one = specified::Resolution::from_dppx(one.dppx() + other.dppx());
+ },
+ (&mut Length(ref mut one), &Length(ref other)) => {
+ *one = one.try_op(other, std::ops::Add::add)?;
+ },
+ _ => {
+ match *other {
+ Number(..) | Percentage(..) | Angle(..) | Time(..) | Resolution(..) |
+ Length(..) => {},
+ }
+ unsafe {
+ debug_unreachable!();
+ }
+ },
+ }
+
+ Ok(())
+ }
+
+ fn try_product_in_place(&mut self, other: &mut Self) -> bool {
+ if let Self::Number(ref mut left) = *self {
+ if let Self::Number(ref right) = *other {
+ // Both sides are numbers, so we can just modify the left side.
+ *left *= *right;
+ true
+ } else {
+ // The right side is not a number, so the result should be in the units of the right
+ // side.
+ other.map(|v| v * *left);
+ std::mem::swap(self, other);
+ true
+ }
+ } else if let Self::Number(ref right) = *other {
+ // The left side is not a number, but the right side is, so the result is the left
+ // side unit.
+ self.map(|v| v * *right);
+ true
+ } else {
+ // Neither side is a number, so a product is not possible.
+ false
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::Leaf::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ match (self, other) {
+ (&Number(one), &Number(other)) => {
+ return Ok(Leaf::Number(op(one, other)));
+ },
+ (&Percentage(one), &Percentage(other)) => {
+ return Ok(Leaf::Percentage(op(one, other)));
+ },
+ (&Angle(ref one), &Angle(ref other)) => {
+ return Ok(Leaf::Angle(specified::Angle::from_calc(op(
+ one.degrees(),
+ other.degrees(),
+ ))));
+ },
+ (&Resolution(ref one), &Resolution(ref other)) => {
+ return Ok(Leaf::Resolution(specified::Resolution::from_dppx(op(
+ one.dppx(),
+ other.dppx(),
+ ))));
+ },
+ (&Time(ref one), &Time(ref other)) => {
+ return Ok(Leaf::Time(specified::Time::from_seconds(op(
+ one.seconds(),
+ other.seconds(),
+ ))));
+ },
+ (&Length(ref one), &Length(ref other)) => {
+ return Ok(Leaf::Length(one.try_op(other, op)?));
+ },
+ _ => {
+ match *other {
+ Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) |
+ Resolution(..) => {},
+ }
+ unsafe {
+ debug_unreachable!();
+ }
+ },
+ }
+ }
+
+ fn map(&mut self, mut op: impl FnMut(f32) -> f32) {
+ match self {
+ Leaf::Length(one) => *one = one.map(op),
+ Leaf::Angle(one) => *one = specified::Angle::from_calc(op(one.degrees())),
+ Leaf::Time(one) => *one = specified::Time::from_seconds(op(one.seconds())),
+ Leaf::Resolution(one) => *one = specified::Resolution::from_dppx(op(one.dppx())),
+ Leaf::Percentage(one) => *one = op(*one),
+ Leaf::Number(one) => *one = op(*one),
+ }
+ }
+}
+
+/// A calc node representation for specified values.
+pub type CalcNode = generic::GenericCalcNode<Leaf>;
+
+impl CalcNode {
+ /// Tries to parse a single element in the expression, that is, a
+ /// `<length>`, `<angle>`, `<time>`, `<percentage>`, `<resolution>`, etc.
+ ///
+ /// May return a "complex" `CalcNode`, in the presence of a parenthesized
+ /// expression, for example.
+ fn parse_one<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match input.next()? {
+ &Token::Number { value, .. } => Ok(CalcNode::Leaf(Leaf::Number(value))),
+ &Token::Dimension {
+ value, ref unit, ..
+ } => {
+ if allowed_units.intersects(CalcUnits::LENGTH) {
+ if let Ok(l) = NoCalcLength::parse_dimension(context, value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Length(l)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::ANGLE) {
+ if let Ok(a) = Angle::parse_dimension(value, unit, /* from_calc = */ true) {
+ return Ok(CalcNode::Leaf(Leaf::Angle(a)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::TIME) {
+ if let Ok(t) = Time::parse_dimension(value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Time(t)));
+ }
+ }
+ if allowed_units.intersects(CalcUnits::RESOLUTION) {
+ if let Ok(t) = Resolution::parse_dimension(value, unit) {
+ return Ok(CalcNode::Leaf(Leaf::Resolution(t)));
+ }
+ }
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ },
+ &Token::Percentage { unit_value, .. }
+ if allowed_units.intersects(CalcUnits::PERCENTAGE) =>
+ {
+ Ok(CalcNode::Leaf(Leaf::Percentage(unit_value)))
+ },
+ &Token::ParenthesisBlock => input.parse_nested_block(|input| {
+ CalcNode::parse_argument(context, input, allowed_units)
+ }),
+ &Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse(context, input, function, allowed_units)
+ },
+ &Token::Ident(ref ident) => {
+ let number = match_ignore_ascii_case! { &**ident,
+ "e" => std::f32::consts::E,
+ "pi" => std::f32::consts::PI,
+ "infinity" => f32::INFINITY,
+ "-infinity" => f32::NEG_INFINITY,
+ "nan" => f32::NAN,
+ _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))),
+ };
+ Ok(CalcNode::Leaf(Leaf::Number(number)))
+ },
+ t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+
+ /// Parse a top-level `calc` expression, with all nested sub-expressions.
+ ///
+ /// This is in charge of parsing, for example, `2 + 3 * 100%`.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ input.parse_nested_block(|input| {
+ match function {
+ MathFunction::Calc => Self::parse_argument(context, input, allowed_units),
+ MathFunction::Clamp => {
+ let min = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let center = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let max = Self::parse_argument(context, input, allowed_units)?;
+ Ok(Self::Clamp {
+ min: Box::new(min),
+ center: Box::new(center),
+ max: Box::new(max),
+ })
+ },
+ MathFunction::Round => {
+ let strategy = input.try_parse(parse_rounding_strategy);
+
+ // <rounding-strategy> = nearest | up | down | to-zero
+ // https://drafts.csswg.org/css-values-4/#calc-syntax
+ fn parse_rounding_strategy<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ ) -> Result<RoundingStrategy, ParseError<'i>> {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "nearest" => RoundingStrategy::Nearest,
+ "up" => RoundingStrategy::Up,
+ "down" => RoundingStrategy::Down,
+ "to-zero" => RoundingStrategy::ToZero,
+ })
+ }
+
+ if strategy.is_ok() {
+ input.expect_comma()?;
+ }
+
+ let value = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let step = Self::parse_argument(context, input, allowed_units)?;
+
+ Ok(Self::Round {
+ strategy: strategy.unwrap_or(RoundingStrategy::Nearest),
+ value: Box::new(value),
+ step: Box::new(step),
+ })
+ },
+ MathFunction::Mod | MathFunction::Rem => {
+ let dividend = Self::parse_argument(context, input, allowed_units)?;
+ input.expect_comma()?;
+ let divisor = Self::parse_argument(context, input, allowed_units)?;
+
+ let op = match function {
+ MathFunction::Mod => ModRemOp::Mod,
+ MathFunction::Rem => ModRemOp::Rem,
+ _ => unreachable!(),
+ };
+ Ok(Self::ModRem {
+ dividend: Box::new(dividend),
+ divisor: Box::new(divisor),
+ op,
+ })
+ },
+ MathFunction::Min | MathFunction::Max => {
+ // TODO(emilio): The common case for parse_comma_separated
+ // is just one element, but for min / max is two, really...
+ //
+ // Consider adding an API to cssparser to specify the
+ // initial vector capacity?
+ let arguments = input.parse_comma_separated(|input| {
+ Self::parse_argument(context, input, allowed_units)
+ })?;
+
+ let op = match function {
+ MathFunction::Min => MinMaxOp::Min,
+ MathFunction::Max => MinMaxOp::Max,
+ _ => unreachable!(),
+ };
+
+ Ok(Self::MinMax(arguments.into(), op))
+ },
+ MathFunction::Sin | MathFunction::Cos | MathFunction::Tan => {
+ let a = Self::parse_angle_argument(context, input)?;
+
+ let number = match function {
+ MathFunction::Sin => a.sin(),
+ MathFunction::Cos => a.cos(),
+ MathFunction::Tan => a.tan(),
+ _ => unsafe {
+ debug_unreachable!("We just checked!");
+ },
+ };
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Asin | MathFunction::Acos | MathFunction::Atan => {
+ let a = Self::parse_number_argument(context, input)?;
+
+ let radians = match function {
+ MathFunction::Asin => a.asin(),
+ MathFunction::Acos => a.acos(),
+ MathFunction::Atan => a.atan(),
+ _ => unsafe {
+ debug_unreachable!("We just checked!");
+ },
+ };
+
+ Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
+ },
+ MathFunction::Atan2 => {
+ let a = Self::parse_argument(context, input, CalcUnits::ALL)?;
+ input.expect_comma()?;
+ let b = Self::parse_argument(context, input, CalcUnits::ALL)?;
+
+ let radians = Self::try_resolve(input, || {
+ if let Ok(a) = a.to_number() {
+ let b = b.to_number()?;
+ return Ok(a.atan2(b));
+ }
+
+ if let Ok(a) = a.to_percentage() {
+ let b = b.to_percentage()?;
+ return Ok(a.atan2(b));
+ }
+
+ if let Ok(a) = a.to_time(None) {
+ let b = b.to_time(None)?;
+ return Ok(a.seconds().atan2(b.seconds()));
+ }
+
+ if let Ok(a) = a.to_angle() {
+ let b = b.to_angle()?;
+ return Ok(a.radians().atan2(b.radians()));
+ }
+
+ if let Ok(a) = a.to_resolution() {
+ let b = b.to_resolution()?;
+ return Ok(a.dppx().atan2(b.dppx()));
+ }
+
+ let a = a.into_length_or_percentage(AllowedNumericType::All)?;
+ let b = b.into_length_or_percentage(AllowedNumericType::All)?;
+ let (a, b) = CalcLengthPercentage::same_unit_length_as(&a, &b).ok_or(())?;
+
+ Ok(a.atan2(b))
+ })?;
+
+ Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians))))
+ },
+ MathFunction::Pow => {
+ let a = Self::parse_number_argument(context, input)?;
+ input.expect_comma()?;
+ let b = Self::parse_number_argument(context, input)?;
+
+ let number = a.powf(b);
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Sqrt => {
+ let a = Self::parse_number_argument(context, input)?;
+
+ let number = a.sqrt();
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Hypot => {
+ let arguments = input.parse_comma_separated(|input| {
+ Self::parse_argument(context, input, allowed_units)
+ })?;
+
+ Ok(Self::Hypot(arguments.into()))
+ },
+ MathFunction::Log => {
+ let a = Self::parse_number_argument(context, input)?;
+ let b = input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ Self::parse_number_argument(context, input)
+ })
+ .ok();
+
+ let number = match b {
+ Some(b) => a.log(b),
+ None => a.ln(),
+ };
+
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Exp => {
+ let a = Self::parse_number_argument(context, input)?;
+ let number = a.exp();
+ Ok(Self::Leaf(Leaf::Number(number)))
+ },
+ MathFunction::Abs => {
+ let node = Self::parse_argument(context, input, allowed_units)?;
+ Ok(Self::Abs(Box::new(node)))
+ },
+ MathFunction::Sign => {
+ // The sign of a percentage is dependent on the percentage basis, so if
+ // percentages aren't allowed (so there's no basis) we shouldn't allow them in
+ // sign(). The rest of the units are safe tho.
+ let sign_units = allowed_units | (CalcUnits::ALL - CalcUnits::PERCENTAGE);
+ let node = Self::parse_argument(context, input, sign_units)?;
+ Ok(Self::Sign(Box::new(node)))
+ },
+ }
+ })
+ }
+
+ fn parse_angle_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ let argument = Self::parse_argument(context, input, CalcUnits::ANGLE)?;
+ argument
+ .to_number()
+ .or_else(|()| Ok(argument.to_angle()?.radians()))
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ fn parse_number_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse_argument(context, input, CalcUnits::empty())?
+ .to_number()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ fn parse_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut sum = SmallVec::<[CalcNode; 1]>::new();
+ sum.push(Self::parse_product(context, input, allowed_units)?);
+
+ loop {
+ let start = input.state();
+ match input.next_including_whitespace() {
+ Ok(&Token::WhiteSpace(_)) => {
+ if input.is_exhausted() {
+ break; // allow trailing whitespace
+ }
+ match *input.next()? {
+ Token::Delim('+') => {
+ sum.push(Self::parse_product(context, input, allowed_units)?);
+ },
+ Token::Delim('-') => {
+ let mut rhs = Self::parse_product(context, input, allowed_units)?;
+ rhs.negate();
+ sum.push(rhs);
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ }
+
+ Ok(if sum.len() == 1 {
+ sum.drain(..).next().unwrap()
+ } else {
+ Self::Sum(sum.into_boxed_slice().into())
+ })
+ }
+
+ /// Parse a top-level `calc` expression, and all the products that may
+ /// follow, and stop as soon as a non-product expression is found.
+ ///
+ /// This should parse correctly:
+ ///
+ /// * `2`
+ /// * `2 * 2`
+ /// * `2 * 2 + 2` (but will leave the `+ 2` unparsed).
+ ///
+ fn parse_product<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allowed_units: CalcUnits,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut product = SmallVec::<[CalcNode; 1]>::new();
+ product.push(Self::parse_one(context, input, allowed_units)?);
+
+ loop {
+ let start = input.state();
+ match input.next() {
+ Ok(&Token::Delim('*')) => {
+ let mut rhs = Self::parse_one(context, input, allowed_units)?;
+
+ // We can unwrap here, becuase we start the function by adding a node to
+ // the list.
+ if !product.last_mut().unwrap().try_product_in_place(&mut rhs) {
+ product.push(rhs);
+ }
+ },
+ Ok(&Token::Delim('/')) => {
+ let rhs = Self::parse_one(context, input, allowed_units)?;
+
+ enum InPlaceDivisionResult {
+ /// The right was merged into the left.
+ Merged,
+ /// The right is not a number or could not be resolved, so the left is
+ /// unchanged.
+ Unchanged,
+ /// The right was resolved, but was not a number, so the calculation is
+ /// invalid.
+ Invalid,
+ }
+
+ fn try_division_in_place(
+ left: &mut CalcNode,
+ right: &CalcNode,
+ ) -> InPlaceDivisionResult {
+ if let Ok(resolved) = right.resolve() {
+ if let Some(number) = resolved.as_number() {
+ if number != 1.0 && left.is_product_distributive() {
+ left.map(|l| l / number);
+ return InPlaceDivisionResult::Merged;
+ }
+ } else {
+ return InPlaceDivisionResult::Invalid;
+ }
+ }
+ InPlaceDivisionResult::Unchanged
+ }
+
+ // The right hand side of a division *must* be a number, so if we can
+ // already resolve it, then merge it with the last node on the product list.
+ // We can unwrap here, becuase we start the function by adding a node to
+ // the list.
+ match try_division_in_place(product.last_mut().unwrap(), &rhs) {
+ InPlaceDivisionResult::Merged => {},
+ InPlaceDivisionResult::Unchanged => {
+ product.push(Self::Invert(Box::new(rhs)))
+ },
+ InPlaceDivisionResult::Invalid => {
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ )
+ },
+ }
+ },
+ _ => {
+ input.reset(&start);
+ break;
+ },
+ }
+ }
+
+ Ok(if product.len() == 1 {
+ product.drain(..).next().unwrap()
+ } else {
+ Self::Product(product.into_boxed_slice().into())
+ })
+ }
+
+ fn try_resolve<'i, 't, F>(
+ input: &Parser<'i, 't>,
+ closure: F,
+ ) -> Result<CSSFloat, ParseError<'i>>
+ where
+ F: FnOnce() -> Result<CSSFloat, ()>,
+ {
+ closure().map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Tries to simplify this expression into a `<length>` or `<percentage>`
+ /// value.
+ pub fn into_length_or_percentage(
+ mut self,
+ clamping_mode: AllowedNumericType,
+ ) -> Result<CalcLengthPercentage, ()> {
+ self.simplify_and_sort();
+
+ // Although we allow numbers inside CalcLengthPercentage, calculations that resolve to a
+ // number result is still not allowed.
+ let unit = self.unit()?;
+ if !CalcUnits::LENGTH_PERCENTAGE.intersects(unit) {
+ Err(())
+ } else {
+ Ok(CalcLengthPercentage {
+ clamping_mode,
+ node: self,
+ })
+ }
+ }
+
+ /// Tries to simplify this expression into a `<time>` value.
+ fn to_time(&self, clamping_mode: Option<AllowedNumericType>) -> Result<Time, ()> {
+ let seconds = if let Leaf::Time(time) = self.resolve()? {
+ time.seconds()
+ } else {
+ return Err(());
+ };
+
+ Ok(Time::from_seconds_with_calc_clamping_mode(
+ seconds,
+ clamping_mode,
+ ))
+ }
+
+ /// Tries to simplify the expression into a `<resolution>` value.
+ fn to_resolution(&self) -> Result<Resolution, ()> {
+ let dppx = if let Leaf::Resolution(resolution) = self.resolve()? {
+ resolution.dppx()
+ } else {
+ return Err(());
+ };
+
+ Ok(Resolution::from_dppx_calc(dppx))
+ }
+
+ /// Tries to simplify this expression into an `Angle` value.
+ fn to_angle(&self) -> Result<Angle, ()> {
+ let degrees = if let Leaf::Angle(angle) = self.resolve()? {
+ angle.degrees()
+ } else {
+ return Err(());
+ };
+
+ let result = Angle::from_calc(degrees);
+ Ok(result)
+ }
+
+ /// Tries to simplify this expression into a `<number>` value.
+ fn to_number(&self) -> Result<CSSFloat, ()> {
+ let number = if let Leaf::Number(number) = self.resolve()? {
+ number
+ } else {
+ return Err(());
+ };
+
+ let result = number;
+
+ Ok(result)
+ }
+
+ /// Tries to simplify this expression into a `<percentage>` value.
+ fn to_percentage(&self) -> Result<CSSFloat, ()> {
+ if let Leaf::Percentage(percentage) = self.resolve()? {
+ Ok(percentage)
+ } else {
+ Err(())
+ }
+ }
+
+ /// Given a function name, and the location from where the token came from,
+ /// return a mathematical function corresponding to that name or an error.
+ #[inline]
+ pub fn math_function<'i>(
+ _: &ParserContext,
+ name: &CowRcStr<'i>,
+ location: cssparser::SourceLocation,
+ ) -> Result<MathFunction, ParseError<'i>> {
+ let function = match MathFunction::from_ident(&*name) {
+ Ok(f) => f,
+ Err(()) => {
+ return Err(location.new_unexpected_token_error(Token::Function(name.clone())))
+ },
+ };
+
+ Ok(function)
+ }
+
+ /// Convenience parsing function for integers.
+ pub fn parse_integer<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSInteger, ParseError<'i>> {
+ Self::parse_number(context, input, function).map(|n| (n + 0.5).floor() as CSSInteger)
+ }
+
+ /// Convenience parsing function for `<length> | <percentage>`.
+ pub fn parse_length_or_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<CalcLengthPercentage, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::LENGTH_PERCENTAGE)?
+ .into_length_or_percentage(clamping_mode)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for percentages.
+ pub fn parse_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::PERCENTAGE)?
+ .to_percentage()
+ .map(crate::values::normalize)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<length>`.
+ pub fn parse_length<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<CalcLengthPercentage, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::LENGTH)?
+ .into_length_or_percentage(clamping_mode)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<number>`.
+ pub fn parse_number<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<CSSFloat, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::empty())?
+ .to_number()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<angle>`.
+ pub fn parse_angle<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<Angle, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::ANGLE)?
+ .to_angle()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<time>`.
+ pub fn parse_time<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ function: MathFunction,
+ ) -> Result<Time, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::TIME)?
+ .to_time(Some(clamping_mode))
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<resolution>`.
+ pub fn parse_resolution<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<Resolution, ParseError<'i>> {
+ Self::parse(context, input, function, CalcUnits::RESOLUTION)?
+ .to_resolution()
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+
+ /// Convenience parsing function for `<number>` or `<percentage>`.
+ pub fn parse_number_or_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<NumberOrPercentage, ParseError<'i>> {
+ let node = Self::parse(context, input, function, CalcUnits::PERCENTAGE)?;
+
+ if let Ok(value) = node.to_number() {
+ return Ok(NumberOrPercentage::Number { value });
+ }
+
+ match node.to_percentage() {
+ Ok(unit_value) => Ok(NumberOrPercentage::Percentage { unit_value }),
+ Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ /// Convenience parsing function for `<number>` or `<angle>`.
+ pub fn parse_angle_or_number<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ function: MathFunction,
+ ) -> Result<AngleOrNumber, ParseError<'i>> {
+ let node = Self::parse(context, input, function, CalcUnits::ANGLE)?;
+
+ if let Ok(angle) = node.to_angle() {
+ let degrees = angle.degrees();
+ return Ok(AngleOrNumber::Angle { degrees });
+ }
+
+ match node.to_number() {
+ Ok(value) => Ok(AngleOrNumber::Number { value }),
+ Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs
new file mode 100644
index 0000000000..3a19a2f4a3
--- /dev/null
+++ b/servo/components/style/values/specified/color.rs
@@ -0,0 +1,1175 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified color values.
+
+use super::AllowQuirks;
+use crate::color::parsing::{
+ self, AngleOrNumber, Color as CSSParserColor, FromParsedColor, NumberOrPercentage,
+};
+use crate::color::{mix::ColorInterpolationMethod, AbsoluteColor, ColorSpace};
+use crate::media_queries::Device;
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue};
+use crate::values::generics::color::{
+ ColorMixFlags, GenericCaretColor, GenericColorMix, GenericColorOrAuto,
+};
+use crate::values::specified::calc::CalcNode;
+use crate::values::specified::Percentage;
+use crate::values::{normalize, CustomIdent};
+use cssparser::{color::PredefinedColorSpace, BasicParseErrorKind, ParseErrorKind, Parser, Token};
+use itoa;
+use std::fmt::{self, Write};
+use std::io::Write as IoWrite;
+use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind};
+use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind};
+
+/// A specified color-mix().
+pub type ColorMix = GenericColorMix<Color, Percentage>;
+
+impl ColorMix {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ preserve_authored: PreserveAuthored,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("color-mix")?;
+
+ input.parse_nested_block(|input| {
+ let interpolation = ColorInterpolationMethod::parse(context, input)?;
+ input.expect_comma()?;
+
+ let try_parse_percentage = |input: &mut Parser| -> Option<Percentage> {
+ input
+ .try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input))
+ .ok()
+ };
+
+ let mut left_percentage = try_parse_percentage(input);
+
+ let left = Color::parse_internal(context, input, preserve_authored)?;
+ if left_percentage.is_none() {
+ left_percentage = try_parse_percentage(input);
+ }
+
+ input.expect_comma()?;
+
+ let mut right_percentage = try_parse_percentage(input);
+
+ let right = Color::parse_internal(context, input, preserve_authored)?;
+
+ if right_percentage.is_none() {
+ right_percentage = try_parse_percentage(input);
+ }
+
+ let right_percentage = right_percentage
+ .unwrap_or_else(|| Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get())));
+
+ let left_percentage =
+ left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get()));
+
+ if left_percentage.get() + right_percentage.get() <= 0.0 {
+ // If the percentages sum to zero, the function is invalid.
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ // Pass RESULT_IN_MODERN_SYNTAX here, because the result of the color-mix() function
+ // should always be in the modern color syntax to allow for out of gamut results and
+ // to preserve floating point precision.
+ Ok(ColorMix {
+ interpolation,
+ left,
+ left_percentage,
+ right,
+ right_percentage,
+ flags: ColorMixFlags::NORMALIZE_WEIGHTS | ColorMixFlags::RESULT_IN_MODERN_SYNTAX,
+ })
+ })
+ }
+}
+
+/// Container holding an absolute color and the text specified by an author.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Absolute {
+ /// The specified color.
+ pub color: AbsoluteColor,
+ /// Authored representation.
+ pub authored: Option<Box<str>>,
+}
+
+impl ToCss for Absolute {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if let Some(ref authored) = self.authored {
+ dest.write_str(authored)
+ } else {
+ self.color.to_css(dest)
+ }
+ }
+}
+
+/// Specified color value
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum Color {
+ /// The 'currentColor' keyword
+ CurrentColor,
+ /// An absolute color.
+ /// https://w3c.github.io/csswg-drafts/css-color-4/#typedef-absolute-color-function
+ Absolute(Box<Absolute>),
+ /// A system color.
+ #[cfg(feature = "gecko")]
+ System(SystemColor),
+ /// A color mix.
+ ColorMix(Box<ColorMix>),
+ /// A light-dark() color.
+ LightDark(Box<LightDark>),
+ /// Quirksmode-only rule for inheriting color from the body
+ #[cfg(feature = "gecko")]
+ InheritFromBodyQuirk,
+}
+
+/// A light-dark(<light-color>, <dark-color>) function.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, ToCss)]
+#[css(function, comma)]
+pub struct LightDark {
+ /// The <color> that is returned when using a light theme.
+ pub light: Color,
+ /// The <color> that is returned when using a dark theme.
+ pub dark: Color,
+}
+
+impl LightDark {
+ fn compute(&self, cx: &Context) -> ComputedColor {
+ let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme();
+ let dark = cx.device().is_dark_color_scheme(&style_color_scheme);
+ let used = if dark { &self.dark } else { &self.light };
+ used.to_computed_value(cx)
+ }
+
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ preserve_authored: PreserveAuthored,
+ ) -> Result<Self, ParseError<'i>> {
+ let enabled =
+ context.chrome_rules_enabled() || static_prefs::pref!("layout.css.light-dark.enabled");
+ if !enabled {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ input.expect_function_matching("light-dark")?;
+ input.parse_nested_block(|input| {
+ let light = Color::parse_internal(context, input, preserve_authored)?;
+ input.expect_comma()?;
+ let dark = Color::parse_internal(context, input, preserve_authored)?;
+ Ok(LightDark { light, dark })
+ })
+ }
+}
+
+impl From<AbsoluteColor> for Color {
+ #[inline]
+ fn from(value: AbsoluteColor) -> Self {
+ Self::from_absolute_color(value)
+ }
+}
+
+/// System colors. A bunch of these are ad-hoc, others come from Windows:
+///
+/// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor
+///
+/// Others are HTML/CSS specific. Spec is:
+///
+/// https://drafts.csswg.org/css-color/#css-system-colors
+/// https://drafts.csswg.org/css-color/#deprecated-system-colors
+#[allow(missing_docs)]
+#[cfg(feature = "gecko")]
+#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)]
+#[repr(u8)]
+pub enum SystemColor {
+ Activeborder,
+ /// Background in the (active) titlebar.
+ Activecaption,
+ Appworkspace,
+ Background,
+ Buttonface,
+ Buttonhighlight,
+ Buttonshadow,
+ Buttontext,
+ Buttonborder,
+ /// Text color in the (active) titlebar.
+ Captiontext,
+ #[parse(aliases = "-moz-field")]
+ Field,
+ /// Used for disabled field backgrounds.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozDisabledfield,
+ #[parse(aliases = "-moz-fieldtext")]
+ Fieldtext,
+
+ Mark,
+ Marktext,
+
+ /// Combobox widgets
+ MozComboboxtext,
+ MozCombobox,
+
+ Graytext,
+ Highlight,
+ Highlighttext,
+ Inactiveborder,
+ /// Background in the (inactive) titlebar.
+ Inactivecaption,
+ /// Text color in the (inactive) titlebar.
+ Inactivecaptiontext,
+ Infobackground,
+ Infotext,
+ Menu,
+ Menutext,
+ Scrollbar,
+ Threeddarkshadow,
+ Threedface,
+ Threedhighlight,
+ Threedlightshadow,
+ Threedshadow,
+ Window,
+ Windowframe,
+ Windowtext,
+ #[parse(aliases = "-moz-default-color")]
+ Canvastext,
+ #[parse(aliases = "-moz-default-background-color")]
+ Canvas,
+ MozDialog,
+ MozDialogtext,
+ /// Used for selected but not focused cell backgrounds.
+ #[parse(aliases = "-moz-html-cellhighlight")]
+ MozCellhighlight,
+ /// Used for selected but not focused cell text.
+ #[parse(aliases = "-moz-html-cellhighlighttext")]
+ MozCellhighlighttext,
+ /// Used for selected and focused html cell backgrounds.
+ Selecteditem,
+ /// Used for selected and focused html cell text.
+ Selecteditemtext,
+ /// Used to button text background when hovered.
+ MozButtonhoverface,
+ /// Used to button text color when hovered.
+ MozButtonhovertext,
+ /// Used for menu item backgrounds when hovered.
+ MozMenuhover,
+ /// Used for menu item backgrounds when hovered and disabled.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMenuhoverdisabled,
+ /// Used for menu item text when hovered.
+ MozMenuhovertext,
+ /// Used for menubar item text when hovered.
+ MozMenubarhovertext,
+
+ /// On platforms where these colors are the same as -moz-field, use
+ /// -moz-fieldtext as foreground color
+ MozEventreerow,
+ MozOddtreerow,
+
+ /// Used for button text when pressed.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozButtonactivetext,
+
+ /// Used for button background when pressed.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozButtonactiveface,
+
+ /// Used for button background when disabled.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozButtondisabledface,
+
+ /// Colors used for the header bar (sorta like the tab bar / menubar).
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbar,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbartext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbarinactive,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozHeaderbarinactivetext,
+
+ /// Foreground color of default buttons.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDefaultbuttontext,
+ /// Ring color around text fields and lists.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacFocusring,
+ /// Text color of disabled text on toolbars.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozMacDisabledtoolbartext,
+ /// The background of a sidebar.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozSidebar,
+ /// The foreground color of a sidebar.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozSidebartext,
+ /// The border color of a sidebar.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozSidebarborder,
+
+ /// Theme accent color.
+ /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolor
+ Accentcolor,
+
+ /// Foreground for the accent color.
+ /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolortext
+ Accentcolortext,
+
+ /// The background-color for :autofill-ed inputs.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozAutofillBackground,
+
+ /// Hyperlink color extracted from the system, not affected by the browser.anchor_color user
+ /// pref.
+ ///
+ /// There is no OS-specified safe background color for this text, but it is used regularly
+ /// within Windows and the Gnome DE on Dialog and Window colors.
+ #[css(skip)]
+ MozNativehyperlinktext,
+
+ /// As above, but visited link color.
+ #[css(skip)]
+ MozNativevisitedhyperlinktext,
+
+ #[parse(aliases = "-moz-hyperlinktext")]
+ Linktext,
+ #[parse(aliases = "-moz-activehyperlinktext")]
+ Activetext,
+ #[parse(aliases = "-moz-visitedhyperlinktext")]
+ Visitedtext,
+
+ /// Color of tree column headers
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheader,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheadertext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderhover,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderhovertext,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderactive,
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozColheaderactivetext,
+
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ TextSelectDisabledBackground,
+ #[css(skip)]
+ TextSelectAttentionBackground,
+ #[css(skip)]
+ TextSelectAttentionForeground,
+ #[css(skip)]
+ TextHighlightBackground,
+ #[css(skip)]
+ TextHighlightForeground,
+ #[css(skip)]
+ IMERawInputBackground,
+ #[css(skip)]
+ IMERawInputForeground,
+ #[css(skip)]
+ IMERawInputUnderline,
+ #[css(skip)]
+ IMESelectedRawTextBackground,
+ #[css(skip)]
+ IMESelectedRawTextForeground,
+ #[css(skip)]
+ IMESelectedRawTextUnderline,
+ #[css(skip)]
+ IMEConvertedTextBackground,
+ #[css(skip)]
+ IMEConvertedTextForeground,
+ #[css(skip)]
+ IMEConvertedTextUnderline,
+ #[css(skip)]
+ IMESelectedConvertedTextBackground,
+ #[css(skip)]
+ IMESelectedConvertedTextForeground,
+ #[css(skip)]
+ IMESelectedConvertedTextUnderline,
+ #[css(skip)]
+ SpellCheckerUnderline,
+ #[css(skip)]
+ ThemedScrollbar,
+ #[css(skip)]
+ ThemedScrollbarInactive,
+ #[css(skip)]
+ ThemedScrollbarThumb,
+ #[css(skip)]
+ ThemedScrollbarThumbHover,
+ #[css(skip)]
+ ThemedScrollbarThumbActive,
+ #[css(skip)]
+ ThemedScrollbarThumbInactive,
+
+ #[css(skip)]
+ End, // Just for array-indexing purposes.
+}
+
+#[cfg(feature = "gecko")]
+impl SystemColor {
+ #[inline]
+ fn compute(&self, cx: &Context) -> ComputedColor {
+ use crate::gecko::values::convert_nscolor_to_absolute_color;
+ use crate::gecko_bindings::bindings;
+
+ // TODO: We should avoid cloning here most likely, though it's cheap-ish.
+ let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme();
+ let color = cx.device().system_nscolor(*self, &style_color_scheme);
+ if color == bindings::NS_SAME_AS_FOREGROUND_COLOR {
+ return ComputedColor::currentcolor();
+ }
+ ComputedColor::Absolute(convert_nscolor_to_absolute_color(color))
+ }
+}
+
+impl FromParsedColor for Color {
+ fn from_current_color() -> Self {
+ Color::CurrentColor
+ }
+
+ fn from_rgba(r: u8, g: u8, b: u8, a: f32) -> Self {
+ AbsoluteColor::srgb_legacy(r, g, b, a).into()
+ }
+
+ fn from_hsl(
+ hue: Option<f32>,
+ saturation: Option<f32>,
+ lightness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Hsl, hue, saturation, lightness, alpha).into()
+ }
+
+ fn from_hwb(
+ hue: Option<f32>,
+ whiteness: Option<f32>,
+ blackness: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Hwb, hue, whiteness, blackness, alpha).into()
+ }
+
+ fn from_lab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Lab, lightness, a, b, alpha).into()
+ }
+
+ fn from_lch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Lch, lightness, chroma, hue, alpha).into()
+ }
+
+ fn from_oklab(
+ lightness: Option<f32>,
+ a: Option<f32>,
+ b: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Oklab, lightness, a, b, alpha).into()
+ }
+
+ fn from_oklch(
+ lightness: Option<f32>,
+ chroma: Option<f32>,
+ hue: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(ColorSpace::Oklch, lightness, chroma, hue, alpha).into()
+ }
+
+ fn from_color_function(
+ color_space: PredefinedColorSpace,
+ c1: Option<f32>,
+ c2: Option<f32>,
+ c3: Option<f32>,
+ alpha: Option<f32>,
+ ) -> Self {
+ AbsoluteColor::new(color_space.into(), c1, c2, c3, alpha).into()
+ }
+}
+
+struct ColorParser<'a, 'b: 'a>(&'a ParserContext<'b>);
+impl<'a, 'b: 'a, 'i: 'a> parsing::ColorParser<'i> for ColorParser<'a, 'b> {
+ type Output = Color;
+ type Error = StyleParseErrorKind<'i>;
+
+ fn parse_angle_or_number<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<AngleOrNumber, ParseError<'i>> {
+ use crate::values::specified::Angle;
+
+ let location = input.current_source_location();
+ let token = input.next()?.clone();
+ match token {
+ Token::Dimension {
+ value, ref unit, ..
+ } => {
+ let angle = Angle::parse_dimension(value, unit, /* from_calc = */ false);
+
+ let degrees = match angle {
+ Ok(angle) => angle.degrees(),
+ Err(()) => return Err(location.new_unexpected_token_error(token.clone())),
+ };
+
+ Ok(AngleOrNumber::Angle { degrees })
+ },
+ Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }),
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(self.0, name, location)?;
+ CalcNode::parse_angle_or_number(self.0, input, function)
+ },
+ t => return Err(location.new_unexpected_token_error(t)),
+ }
+ }
+
+ fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> {
+ Ok(Percentage::parse(self.0, input)?.get())
+ }
+
+ fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> {
+ use crate::values::specified::Number;
+
+ Ok(Number::parse(self.0, input)?.get())
+ }
+
+ fn parse_number_or_percentage<'t>(
+ &self,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<NumberOrPercentage, ParseError<'i>> {
+ let location = input.current_source_location();
+
+ match *input.next()? {
+ Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }),
+ Token::Percentage { unit_value, .. } => {
+ Ok(NumberOrPercentage::Percentage { unit_value })
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(self.0, name, location)?;
+ CalcNode::parse_number_or_percentage(self.0, input, function)
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
+
+/// Whether to preserve authored colors during parsing. That's useful only if we
+/// plan to serialize the color back.
+#[derive(Copy, Clone)]
+enum PreserveAuthored {
+ No,
+ Yes,
+}
+
+impl Parse for Color {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, PreserveAuthored::Yes)
+ }
+}
+
+impl Color {
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ preserve_authored: PreserveAuthored,
+ ) -> Result<Self, ParseError<'i>> {
+ let authored = match preserve_authored {
+ PreserveAuthored::No => None,
+ PreserveAuthored::Yes => {
+ // Currently we only store authored value for color keywords,
+ // because all browsers serialize those values as keywords for
+ // specified value.
+ let start = input.state();
+ let authored = input.expect_ident_cloned().ok();
+ input.reset(&start);
+ authored
+ },
+ };
+
+ let color_parser = ColorParser(&*context);
+ match input.try_parse(|i| parsing::parse_color_with(&color_parser, i)) {
+ Ok(mut color) => {
+ if let Color::Absolute(ref mut absolute) = color {
+ // Because we can't set the `authored` value at construction time, we have to set it
+ // here.
+ absolute.authored = authored.map(|s| s.to_ascii_lowercase().into_boxed_str());
+ }
+ Ok(color)
+ },
+ Err(e) => {
+ #[cfg(feature = "gecko")]
+ {
+ if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) {
+ return Ok(Color::System(system));
+ }
+ }
+
+ if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i, preserve_authored))
+ {
+ return Ok(Color::ColorMix(Box::new(mix)));
+ }
+
+ if let Ok(ld) = input.try_parse(|i| LightDark::parse(context, i, preserve_authored))
+ {
+ return Ok(Color::LightDark(Box::new(ld)));
+ }
+
+ match e.kind {
+ ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => {
+ Err(e.location.new_custom_error(StyleParseErrorKind::ValueError(
+ ValueParseErrorKind::InvalidColor(t),
+ )))
+ },
+ _ => Err(e),
+ }
+ },
+ }
+ }
+
+ /// Returns whether a given color is valid for authors.
+ pub fn is_valid(context: &ParserContext, input: &mut Parser) -> bool {
+ input
+ .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No))
+ .is_ok()
+ }
+
+ /// Tries to parse a color and compute it with a given device.
+ pub fn parse_and_compute(
+ context: &ParserContext,
+ input: &mut Parser,
+ device: Option<&Device>,
+ ) -> Option<ComputedColor> {
+ use crate::error_reporting::ContextualParseError;
+ let start = input.position();
+ let result = input
+ .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No));
+
+ let specified = match result {
+ Ok(s) => s,
+ Err(e) => {
+ if !context.error_reporting_enabled() {
+ return None;
+ }
+ // Ignore other kinds of errors that might be reported, such as
+ // ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken),
+ // since Gecko didn't use to report those to the error console.
+ //
+ // TODO(emilio): Revise whether we want to keep this at all, we
+ // use this only for canvas, this warnings are disabled by
+ // default and not available on OffscreenCanvas anyways...
+ if let ParseErrorKind::Custom(StyleParseErrorKind::ValueError(..)) = e.kind {
+ let location = e.location.clone();
+ let error = ContextualParseError::UnsupportedValue(input.slice_from(start), e);
+ context.log_css_error(location, error);
+ }
+ return None;
+ },
+ };
+
+ match device {
+ Some(device) => {
+ Context::for_media_query_evaluation(device, device.quirks_mode(), |context| {
+ specified.to_computed_color(Some(&context))
+ })
+ },
+ None => specified.to_computed_color(None),
+ }
+ }
+}
+
+impl ToCss for Color {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ Color::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest),
+ Color::Absolute(ref absolute) => absolute.to_css(dest),
+ Color::ColorMix(ref mix) => mix.to_css(dest),
+ Color::LightDark(ref ld) => ld.to_css(dest),
+ #[cfg(feature = "gecko")]
+ Color::System(system) => system.to_css(dest),
+ #[cfg(feature = "gecko")]
+ Color::InheritFromBodyQuirk => Ok(()),
+ }
+ }
+}
+
+impl Color {
+ /// Returns whether this color is allowed in forced-colors mode.
+ pub fn honored_in_forced_colors_mode(&self, allow_transparent: bool) -> bool {
+ match *self {
+ Self::InheritFromBodyQuirk => false,
+ Self::CurrentColor | Color::System(..) => true,
+ Self::Absolute(ref absolute) => allow_transparent && absolute.color.is_transparent(),
+ Self::LightDark(ref ld) => {
+ ld.light.honored_in_forced_colors_mode(allow_transparent) &&
+ ld.dark.honored_in_forced_colors_mode(allow_transparent)
+ },
+ Self::ColorMix(ref mix) => {
+ mix.left.honored_in_forced_colors_mode(allow_transparent) &&
+ mix.right.honored_in_forced_colors_mode(allow_transparent)
+ },
+ }
+ }
+
+ /// Returns currentcolor value.
+ #[inline]
+ pub fn currentcolor() -> Self {
+ Self::CurrentColor
+ }
+
+ /// Returns transparent value.
+ #[inline]
+ pub fn transparent() -> Self {
+ // We should probably set authored to "transparent", but maybe it doesn't matter.
+ Self::from_absolute_color(AbsoluteColor::TRANSPARENT_BLACK)
+ }
+
+ /// Create a color from an [`AbsoluteColor`].
+ pub fn from_absolute_color(color: AbsoluteColor) -> Self {
+ Color::Absolute(Box::new(Absolute {
+ color,
+ authored: None,
+ }))
+ }
+
+ /// Parse a color, with quirks.
+ ///
+ /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk>
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ input.try_parse(|i| Self::parse(context, i)).or_else(|e| {
+ if !allow_quirks.allowed(context.quirks_mode) {
+ return Err(e);
+ }
+ Color::parse_quirky_color(input).map_err(|_| e)
+ })
+ }
+
+ fn parse_hash<'i>(
+ bytes: &[u8],
+ loc: &cssparser::SourceLocation,
+ ) -> Result<Self, ParseError<'i>> {
+ match cssparser::color::parse_hash_color(bytes) {
+ Ok((r, g, b, a)) => Ok(Self::from_rgba(r, g, b, a)),
+ Err(()) => Err(loc.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+
+ /// Parse a <quirky-color> value.
+ ///
+ /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk>
+ fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let (value, unit) = match *input.next()? {
+ Token::Number {
+ int_value: Some(integer),
+ ..
+ } => (integer, None),
+ Token::Dimension {
+ int_value: Some(integer),
+ ref unit,
+ ..
+ } => (integer, Some(unit)),
+ Token::Ident(ref ident) => {
+ if ident.len() != 3 && ident.len() != 6 {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ return Self::parse_hash(ident.as_bytes(), &location);
+ },
+ ref t => {
+ return Err(location.new_unexpected_token_error(t.clone()));
+ },
+ };
+ if value < 0 {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ let length = if value <= 9 {
+ 1
+ } else if value <= 99 {
+ 2
+ } else if value <= 999 {
+ 3
+ } else if value <= 9999 {
+ 4
+ } else if value <= 99999 {
+ 5
+ } else if value <= 999999 {
+ 6
+ } else {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ };
+ let total = length + unit.as_ref().map_or(0, |d| d.len());
+ if total > 6 {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ let mut serialization = [b'0'; 6];
+ let space_padding = 6 - total;
+ let mut written = space_padding;
+ let mut buf = itoa::Buffer::new();
+ let s = buf.format(value);
+ (&mut serialization[written..])
+ .write_all(s.as_bytes())
+ .unwrap();
+ written += s.len();
+ if let Some(unit) = unit {
+ written += (&mut serialization[written..])
+ .write(unit.as_bytes())
+ .unwrap();
+ }
+ debug_assert_eq!(written, 6);
+ Self::parse_hash(&serialization, &location)
+ }
+}
+
+impl Color {
+ /// Converts this Color into a ComputedColor.
+ ///
+ /// If `context` is `None`, and the specified color requires data from
+ /// the context to resolve, then `None` is returned.
+ pub fn to_computed_color(&self, context: Option<&Context>) -> Option<ComputedColor> {
+ Some(match *self {
+ Color::CurrentColor => ComputedColor::CurrentColor,
+ Color::Absolute(ref absolute) => {
+ let mut color = absolute.color;
+
+ // Computed lightness values can not be NaN.
+ if matches!(
+ color.color_space,
+ ColorSpace::Lab | ColorSpace::Oklab | ColorSpace::Lch | ColorSpace::Oklch
+ ) {
+ color.components.0 = normalize(color.components.0);
+ }
+
+ // Computed RGB and XYZ components can not be NaN.
+ if !color.is_legacy_syntax() && color.color_space.is_rgb_or_xyz_like() {
+ color.components = color.components.map(normalize);
+ }
+
+ color.alpha = normalize(color.alpha);
+
+ ComputedColor::Absolute(color)
+ },
+ Color::LightDark(ref ld) => ld.compute(context?),
+ Color::ColorMix(ref mix) => {
+ use crate::values::computed::percentage::Percentage;
+
+ let left = mix.left.to_computed_color(context)?;
+ let right = mix.right.to_computed_color(context)?;
+
+ ComputedColor::from_color_mix(GenericColorMix {
+ interpolation: mix.interpolation,
+ left,
+ left_percentage: Percentage(mix.left_percentage.get()),
+ right,
+ right_percentage: Percentage(mix.right_percentage.get()),
+ flags: mix.flags,
+ })
+ },
+ #[cfg(feature = "gecko")]
+ Color::System(system) => system.compute(context?),
+ #[cfg(feature = "gecko")]
+ Color::InheritFromBodyQuirk => {
+ ComputedColor::Absolute(context?.device().body_text_color())
+ },
+ })
+ }
+}
+
+impl ToComputedValue for Color {
+ type ComputedValue = ComputedColor;
+
+ fn to_computed_value(&self, context: &Context) -> ComputedColor {
+ self.to_computed_color(Some(context)).unwrap()
+ }
+
+ fn from_computed_value(computed: &ComputedColor) -> Self {
+ match *computed {
+ ComputedColor::Absolute(ref color) => Self::from_absolute_color(color.clone()),
+ ComputedColor::CurrentColor => Color::CurrentColor,
+ ComputedColor::ColorMix(ref mix) => {
+ Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix)))
+ },
+ }
+ }
+}
+
+impl SpecifiedValueInfo for Color {
+ const SUPPORTED_TYPES: u8 = CssType::COLOR;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // We are not going to insert all the color names here. Caller and
+ // devtools should take care of them. XXX Actually, transparent
+ // should probably be handled that way as well.
+ // XXX `currentColor` should really be `currentcolor`. But let's
+ // keep it consistent with the old system for now.
+ f(&[
+ "rgb",
+ "rgba",
+ "hsl",
+ "hsla",
+ "hwb",
+ "currentColor",
+ "transparent",
+ "color-mix",
+ "color",
+ "lab",
+ "lch",
+ "oklab",
+ "oklch",
+ ]);
+ }
+}
+
+/// Specified value for the "color" property, which resolves the `currentcolor`
+/// keyword to the parent color instead of self's color.
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct ColorPropertyValue(pub Color);
+
+impl ToComputedValue for ColorPropertyValue {
+ type ComputedValue = AbsoluteColor;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ let current_color = context.builder.get_parent_inherited_text().clone_color();
+ self.0
+ .to_computed_value(context)
+ .resolve_to_absolute(&current_color)
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ ColorPropertyValue(Color::from_absolute_color(*computed).into())
+ }
+}
+
+impl Parse for ColorPropertyValue {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Color::parse_quirky(context, input, AllowQuirks::Yes).map(ColorPropertyValue)
+ }
+}
+
+/// auto | <color>
+pub type ColorOrAuto = GenericColorOrAuto<Color>;
+
+/// caret-color
+pub type CaretColor = GenericCaretColor<Color>;
+
+impl Parse for CaretColor {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ ColorOrAuto::parse(context, input).map(GenericCaretColor)
+ }
+}
+
+/// Various flags to represent the color-scheme property in an efficient
+/// way.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Default,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[value_info(other_values = "light,dark,only")]
+pub struct ColorSchemeFlags(u8);
+bitflags! {
+ impl ColorSchemeFlags: u8 {
+ /// Whether the author specified `light`.
+ const LIGHT = 1 << 0;
+ /// Whether the author specified `dark`.
+ const DARK = 1 << 1;
+ /// Whether the author specified `only`.
+ const ONLY = 1 << 2;
+ }
+}
+
+/// <https://drafts.csswg.org/css-color-adjust/#color-scheme-prop>
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[value_info(other_values = "normal")]
+pub struct ColorScheme {
+ #[ignore_malloc_size_of = "Arc"]
+ idents: crate::ArcSlice<CustomIdent>,
+ bits: ColorSchemeFlags,
+}
+
+impl ColorScheme {
+ /// Returns the `normal` value.
+ pub fn normal() -> Self {
+ Self {
+ idents: Default::default(),
+ bits: ColorSchemeFlags::empty(),
+ }
+ }
+
+ /// Returns the raw bitfield.
+ pub fn raw_bits(&self) -> u8 {
+ self.bits.bits()
+ }
+}
+
+impl Parse for ColorScheme {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut idents = vec![];
+ let mut bits = ColorSchemeFlags::empty();
+
+ let mut location = input.current_source_location();
+ while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+ let mut is_only = false;
+ match_ignore_ascii_case! { &ident,
+ "normal" => {
+ if idents.is_empty() && bits.is_empty() {
+ return Ok(Self::normal());
+ }
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ },
+ "light" => bits.insert(ColorSchemeFlags::LIGHT),
+ "dark" => bits.insert(ColorSchemeFlags::DARK),
+ "only" => {
+ if bits.intersects(ColorSchemeFlags::ONLY) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ bits.insert(ColorSchemeFlags::ONLY);
+ is_only = true;
+ },
+ _ => {},
+ };
+
+ if is_only {
+ if !idents.is_empty() {
+ // Only is allowed either at the beginning or at the end,
+ // but not in the middle.
+ break;
+ }
+ } else {
+ idents.push(CustomIdent::from_ident(location, &ident, &[])?);
+ }
+ location = input.current_source_location();
+ }
+
+ if idents.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(Self {
+ idents: crate::ArcSlice::from_iter(idents.into_iter()),
+ bits,
+ })
+ }
+}
+
+impl ToCss for ColorScheme {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.idents.is_empty() {
+ debug_assert!(self.bits.is_empty());
+ return dest.write_str("normal");
+ }
+ let mut first = true;
+ for ident in self.idents.iter() {
+ if !first {
+ dest.write_char(' ')?;
+ }
+ first = false;
+ ident.to_css(dest)?;
+ }
+ if self.bits.intersects(ColorSchemeFlags::ONLY) {
+ dest.write_str(" only")?;
+ }
+ Ok(())
+ }
+}
+
+/// https://drafts.csswg.org/css-color-adjust/#print-color-adjust
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum PrintColorAdjust {
+ /// Ignore backgrounds and darken text.
+ Economy,
+ /// Respect specified colors.
+ Exact,
+}
+
+/// https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ForcedColorAdjust {
+ /// Adjust colors if needed.
+ Auto,
+ /// Respect specified colors.
+ None,
+}
diff --git a/servo/components/style/values/specified/column.rs b/servo/components/style/values/specified/column.rs
new file mode 100644
index 0000000000..2dd7bb0144
--- /dev/null
+++ b/servo/components/style/values/specified/column.rs
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for the column properties.
+
+use crate::values::generics::column::ColumnCount as GenericColumnCount;
+use crate::values::specified::PositiveInteger;
+
+/// A specified type for `column-count` values.
+pub type ColumnCount = GenericColumnCount<PositiveInteger>;
diff --git a/servo/components/style/values/specified/counters.rs b/servo/components/style/values/specified/counters.rs
new file mode 100644
index 0000000000..9d8261ce6c
--- /dev/null
+++ b/servo/components/style/values/specified/counters.rs
@@ -0,0 +1,279 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for counter properties.
+
+#[cfg(feature = "servo-layout-2013")]
+use crate::computed_values::list_style_type::T as ListStyleType;
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::counters as generics;
+use crate::values::generics::counters::CounterPair;
+#[cfg(feature = "gecko")]
+use crate::values::generics::CounterStyle;
+use crate::values::specified::image::Image;
+#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+use crate::values::specified::Attr;
+use crate::values::specified::Integer;
+use crate::values::CustomIdent;
+use cssparser::{Parser, Token};
+#[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+use selectors::parser::SelectorParseErrorKind;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+#[derive(PartialEq)]
+enum CounterType {
+ Increment,
+ Set,
+ Reset,
+}
+
+impl CounterType {
+ fn default_value(&self) -> i32 {
+ match *self {
+ Self::Increment => 1,
+ Self::Reset | Self::Set => 0,
+ }
+ }
+}
+
+/// A specified value for the `counter-increment` property.
+pub type CounterIncrement = generics::GenericCounterIncrement<Integer>;
+
+impl Parse for CounterIncrement {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self::new(parse_counters(
+ context,
+ input,
+ CounterType::Increment,
+ )?))
+ }
+}
+
+/// A specified value for the `counter-set` property.
+pub type CounterSet = generics::GenericCounterSet<Integer>;
+
+impl Parse for CounterSet {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self::new(parse_counters(context, input, CounterType::Set)?))
+ }
+}
+
+/// A specified value for the `counter-reset` property.
+pub type CounterReset = generics::GenericCounterReset<Integer>;
+
+impl Parse for CounterReset {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(Self::new(parse_counters(
+ context,
+ input,
+ CounterType::Reset,
+ )?))
+ }
+}
+
+fn parse_counters<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ counter_type: CounterType,
+) -> Result<Vec<CounterPair<Integer>>, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(vec![]);
+ }
+
+ let mut counters = Vec::new();
+ loop {
+ let location = input.current_source_location();
+ let (name, is_reversed) = match input.next() {
+ Ok(&Token::Ident(ref ident)) => {
+ (CustomIdent::from_ident(location, ident, &["none"])?, false)
+ },
+ Ok(&Token::Function(ref name))
+ if counter_type == CounterType::Reset && name.eq_ignore_ascii_case("reversed") =>
+ {
+ input
+ .parse_nested_block(|input| Ok((CustomIdent::parse(input, &["none"])?, true)))?
+ },
+ Ok(t) => {
+ let t = t.clone();
+ return Err(location.new_unexpected_token_error(t));
+ },
+ Err(_) => break,
+ };
+
+ let value = match input.try_parse(|input| Integer::parse(context, input)) {
+ Ok(start) => {
+ if start.value == i32::min_value() {
+ // The spec says that values must be clamped to the valid range,
+ // and we reserve i32::min_value() as an internal magic value.
+ // https://drafts.csswg.org/css-lists/#auto-numbering
+ Integer::new(i32::min_value() + 1)
+ } else {
+ start
+ }
+ },
+ _ => Integer::new(if is_reversed {
+ i32::min_value()
+ } else {
+ counter_type.default_value()
+ }),
+ };
+ counters.push(CounterPair {
+ name,
+ value,
+ is_reversed,
+ });
+ }
+
+ if !counters.is_empty() {
+ Ok(counters)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+/// The specified value for the `content` property.
+pub type Content = generics::GenericContent<Image>;
+
+/// The specified value for a content item in the `content` property.
+pub type ContentItem = generics::GenericContentItem<Image>;
+
+impl Content {
+ #[cfg(feature = "servo-layout-2013")]
+ fn parse_counter_style(_: &ParserContext, input: &mut Parser) -> ListStyleType {
+ input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ ListStyleType::parse(input)
+ })
+ .unwrap_or(ListStyleType::Decimal)
+ }
+
+ #[cfg(feature = "gecko")]
+ fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyle {
+ input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ CounterStyle::parse(context, input)
+ })
+ .unwrap_or(CounterStyle::decimal())
+ }
+}
+
+impl Parse for Content {
+ // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote |
+ // no-close-quote ]+
+ // TODO: <uri>, attr(<identifier>)
+ #[cfg_attr(feature = "servo", allow(unused_mut))]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(generics::Content::Normal);
+ }
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(generics::Content::None);
+ }
+
+ let mut content = vec![];
+ let mut has_alt_content = false;
+ loop {
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+ {
+ if let Ok(image) = input.try_parse(|i| Image::parse_forbid_none(context, i)) {
+ content.push(generics::ContentItem::Image(image));
+ continue;
+ }
+ }
+ match input.next() {
+ Ok(&Token::QuotedString(ref value)) => {
+ content.push(generics::ContentItem::String(
+ value.as_ref().to_owned().into(),
+ ));
+ },
+ Ok(&Token::Function(ref name)) => {
+ let result = match_ignore_ascii_case! { &name,
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ "counter" => input.parse_nested_block(|input| {
+ let name = CustomIdent::parse(input, &[])?;
+ let style = Content::parse_counter_style(context, input);
+ Ok(generics::ContentItem::Counter(name, style))
+ }),
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ "counters" => input.parse_nested_block(|input| {
+ let name = CustomIdent::parse(input, &[])?;
+ input.expect_comma()?;
+ let separator = input.expect_string()?.as_ref().to_owned().into();
+ let style = Content::parse_counter_style(context, input);
+ Ok(generics::ContentItem::Counters(name, separator, style))
+ }),
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))]
+ "attr" => input.parse_nested_block(|input| {
+ Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?))
+ }),
+ _ => {
+ use style_traits::StyleParseErrorKind;
+ let name = name.clone();
+ return Err(input.new_custom_error(
+ StyleParseErrorKind::UnexpectedFunction(name),
+ ))
+ }
+ }?;
+ content.push(result);
+ },
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ Ok(&Token::Ident(ref ident)) => {
+ content.push(match_ignore_ascii_case! { &ident,
+ "open-quote" => generics::ContentItem::OpenQuote,
+ "close-quote" => generics::ContentItem::CloseQuote,
+ "no-open-quote" => generics::ContentItem::NoOpenQuote,
+ "no-close-quote" => generics::ContentItem::NoCloseQuote,
+ #[cfg(feature = "gecko")]
+ "-moz-alt-content" => {
+ has_alt_content = true;
+ generics::ContentItem::MozAltContent
+ },
+ "-moz-label-content" if context.chrome_rules_enabled() => {
+ generics::ContentItem::MozLabelContent
+ },
+ _ =>{
+ let ident = ident.clone();
+ return Err(input.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident)
+ ));
+ }
+ });
+ },
+ Err(_) => break,
+ Ok(t) => {
+ let t = t.clone();
+ return Err(input.new_unexpected_token_error(t));
+ },
+ }
+ }
+ // We don't allow to parse `-moz-alt-content` in multiple positions.
+ if content.is_empty() || (has_alt_content && content.len() != 1) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(generics::Content::Items(content.into()))
+ }
+}
diff --git a/servo/components/style/values/specified/easing.rs b/servo/components/style/values/specified/easing.rs
new file mode 100644
index 0000000000..5e4d8ae1ea
--- /dev/null
+++ b/servo/components/style/values/specified/easing.rs
@@ -0,0 +1,192 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS Easing functions.
+use crate::parser::{Parse, ParserContext};
+use crate::piecewise_linear::{PiecewiseLinearFunction, PiecewiseLinearFunctionBuilder};
+use crate::values::computed::easing::TimingFunction as ComputedTimingFunction;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::easing::TimingFunction as GenericTimingFunction;
+use crate::values::generics::easing::{StepPosition, TimingKeyword};
+use crate::values::specified::{Integer, Number, Percentage};
+use cssparser::{Delimiter, Parser, Token};
+use selectors::parser::SelectorParseErrorKind;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// A specified timing function.
+pub type TimingFunction = GenericTimingFunction<Integer, Number, PiecewiseLinearFunction>;
+
+impl Parse for TimingFunction {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(keyword) = input.try_parse(TimingKeyword::parse) {
+ return Ok(GenericTimingFunction::Keyword(keyword));
+ }
+ if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) {
+ let position = match_ignore_ascii_case! { &ident,
+ "step-start" => StepPosition::Start,
+ "step-end" => StepPosition::End,
+ _ => {
+ return Err(input.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ ));
+ },
+ };
+ return Ok(GenericTimingFunction::Steps(Integer::new(1), position));
+ }
+ let location = input.current_source_location();
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(move |i| {
+ match_ignore_ascii_case! { &function,
+ "cubic-bezier" => Self::parse_cubic_bezier(context, i),
+ "steps" => Self::parse_steps(context, i),
+ "linear" => Self::parse_linear_function(context, i),
+ _ => Err(location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))),
+ }
+ })
+ }
+}
+
+impl TimingFunction {
+ fn parse_cubic_bezier<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let x1 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let y1 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let x2 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let y2 = Number::parse(context, input)?;
+
+ if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 })
+ }
+
+ fn parse_steps<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let steps = Integer::parse_positive(context, input)?;
+ let position = input
+ .try_parse(|i| {
+ i.expect_comma()?;
+ StepPosition::parse(context, i)
+ })
+ .unwrap_or(StepPosition::End);
+
+ // jump-none accepts a positive integer greater than 1.
+ // FIXME(emilio): The spec asks us to avoid rejecting it at parse
+ // time except until computed value time.
+ //
+ // It's not totally clear it's worth it though, and no other browser
+ // does this.
+ if position == StepPosition::JumpNone && steps.value() <= 1 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(GenericTimingFunction::Steps(steps, position))
+ }
+
+ fn parse_linear_function<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut builder = PiecewiseLinearFunctionBuilder::default();
+ let mut num_specified_stops = 0;
+ // Closely follows `parse_comma_separated`, but can generate multiple entries for one comma-separated entry.
+ loop {
+ input.parse_until_before(Delimiter::Comma, |i| {
+ let builder = &mut builder;
+ let mut input_start = i.try_parse(|i| Percentage::parse(context, i)).ok();
+ let mut input_end = i.try_parse(|i| Percentage::parse(context, i)).ok();
+
+ let output = Number::parse(context, i)?;
+ if input_start.is_none() {
+ debug_assert!(input_end.is_none(), "Input end parsed without input start?");
+ input_start = i.try_parse(|i| Percentage::parse(context, i)).ok();
+ input_end = i.try_parse(|i| Percentage::parse(context, i)).ok();
+ }
+ builder.push(output.into(), input_start.map(|v| v.get()).into());
+ num_specified_stops += 1;
+ if input_end.is_some() {
+ debug_assert!(
+ input_start.is_some(),
+ "Input end valid but not input start?"
+ );
+ builder.push(output.into(), input_end.map(|v| v.get()).into());
+ }
+
+ Ok(())
+ })?;
+
+ match input.next() {
+ Err(_) => break,
+ Ok(&Token::Comma) => continue,
+ Ok(_) => unreachable!(),
+ }
+ }
+ // By spec, specifying only a single stop makes the function invalid, even if that single entry may generate
+ // two entries.
+ if num_specified_stops < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(GenericTimingFunction::LinearFunction(builder.build()))
+ }
+}
+
+// We need this for converting the specified TimingFunction into computed TimingFunction without
+// Context (for some FFIs in glue.rs). In fact, we don't really need Context to get the computed
+// value of TimingFunction.
+impl TimingFunction {
+ /// Generate the ComputedTimingFunction without Context.
+ pub fn to_computed_value_without_context(&self) -> ComputedTimingFunction {
+ match &self {
+ GenericTimingFunction::Steps(steps, pos) => {
+ GenericTimingFunction::Steps(steps.value(), *pos)
+ },
+ GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => {
+ GenericTimingFunction::CubicBezier {
+ x1: x1.get(),
+ y1: y1.get(),
+ x2: x2.get(),
+ y2: y2.get(),
+ }
+ },
+ GenericTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword),
+ GenericTimingFunction::LinearFunction(function) => {
+ GenericTimingFunction::LinearFunction(function.clone())
+ },
+ }
+ }
+}
+
+impl ToComputedValue for TimingFunction {
+ type ComputedValue = ComputedTimingFunction;
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ self.to_computed_value_without_context()
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match &computed {
+ ComputedTimingFunction::Steps(steps, pos) => Self::Steps(Integer::new(*steps), *pos),
+ ComputedTimingFunction::CubicBezier { x1, y1, x2, y2 } => Self::CubicBezier {
+ x1: Number::new(*x1),
+ y1: Number::new(*y1),
+ x2: Number::new(*x2),
+ y2: Number::new(*y2),
+ },
+ ComputedTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword),
+ ComputedTimingFunction::LinearFunction(function) => {
+ GenericTimingFunction::LinearFunction(function.clone())
+ },
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/effects.rs b/servo/components/style/values/specified/effects.rs
new file mode 100644
index 0000000000..0453582768
--- /dev/null
+++ b/servo/components/style/values/specified/effects.rs
@@ -0,0 +1,453 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values related to effects.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::effects::BoxShadow as ComputedBoxShadow;
+use crate::values::computed::effects::SimpleShadow as ComputedSimpleShadow;
+#[cfg(feature = "gecko")]
+use crate::values::computed::url::ComputedUrl;
+use crate::values::computed::Angle as ComputedAngle;
+use crate::values::computed::CSSPixelLength as ComputedCSSPixelLength;
+use crate::values::computed::Filter as ComputedFilter;
+use crate::values::computed::NonNegativeLength as ComputedNonNegativeLength;
+use crate::values::computed::NonNegativeNumber as ComputedNonNegativeNumber;
+use crate::values::computed::ZeroToOneNumber as ComputedZeroToOneNumber;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::effects::BoxShadow as GenericBoxShadow;
+use crate::values::generics::effects::Filter as GenericFilter;
+use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow;
+use crate::values::generics::NonNegative;
+use crate::values::specified::color::Color;
+use crate::values::specified::length::{Length, NonNegativeLength};
+#[cfg(feature = "gecko")]
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::{Angle, Number, NumberOrPercentage};
+#[cfg(feature = "servo")]
+use crate::values::Impossible;
+use crate::Zero;
+use cssparser::{self, BasicParseErrorKind, Parser, Token};
+use style_traits::{ParseError, StyleParseErrorKind, ValueParseErrorKind};
+
+/// A specified value for a single shadow of the `box-shadow` property.
+pub type BoxShadow =
+ GenericBoxShadow<Option<Color>, Length, Option<NonNegativeLength>, Option<Length>>;
+
+/// A specified value for a single `filter`.
+#[cfg(feature = "gecko")]
+pub type SpecifiedFilter = GenericFilter<
+ Angle,
+ NonNegativeFactor,
+ ZeroToOneFactor,
+ NonNegativeLength,
+ SimpleShadow,
+ SpecifiedUrl,
+>;
+
+/// A specified value for a single `filter`.
+#[cfg(feature = "servo")]
+pub type SpecifiedFilter = GenericFilter<
+ Angle,
+ NonNegativeFactor,
+ ZeroToOneFactor,
+ NonNegativeLength,
+ Impossible,
+ Impossible,
+>;
+
+pub use self::SpecifiedFilter as Filter;
+
+/// A value for the `<factor>` parts in `Filter`.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct NonNegativeFactor(NumberOrPercentage);
+
+/// A value for the `<factor>` parts in `Filter` which clamps to one.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct ZeroToOneFactor(NumberOrPercentage);
+
+/// Clamp the value to 1 if the value is over 100%.
+#[inline]
+fn clamp_to_one(number: NumberOrPercentage) -> NumberOrPercentage {
+ match number {
+ NumberOrPercentage::Percentage(percent) => {
+ NumberOrPercentage::Percentage(percent.clamp_to_hundred())
+ },
+ NumberOrPercentage::Number(number) => NumberOrPercentage::Number(number.clamp_to_one()),
+ }
+}
+
+macro_rules! factor_impl_common {
+ ($ty:ty, $computed_ty:ty) => {
+ impl $ty {
+ #[inline]
+ fn one() -> Self {
+ Self(NumberOrPercentage::Number(Number::new(1.)))
+ }
+ }
+
+ impl ToComputedValue for $ty {
+ type ComputedValue = $computed_ty;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::values::computed::NumberOrPercentage;
+ match self.0.to_computed_value(context) {
+ NumberOrPercentage::Number(n) => n.into(),
+ NumberOrPercentage::Percentage(p) => p.0.into(),
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self(NumberOrPercentage::Number(
+ ToComputedValue::from_computed_value(&computed.0),
+ ))
+ }
+ }
+ };
+}
+factor_impl_common!(NonNegativeFactor, ComputedNonNegativeNumber);
+factor_impl_common!(ZeroToOneFactor, ComputedZeroToOneNumber);
+
+impl Parse for NonNegativeFactor {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ NumberOrPercentage::parse_non_negative(context, input).map(Self)
+ }
+}
+
+impl Parse for ZeroToOneFactor {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ NumberOrPercentage::parse_non_negative(context, input)
+ .map(clamp_to_one)
+ .map(Self)
+ }
+}
+
+/// A specified value for the `drop-shadow()` filter.
+pub type SimpleShadow = GenericSimpleShadow<Option<Color>, Length, Option<NonNegativeLength>>;
+
+impl Parse for BoxShadow {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut lengths = None;
+ let mut color = None;
+ let mut inset = false;
+
+ loop {
+ if !inset {
+ if input
+ .try_parse(|input| input.expect_ident_matching("inset"))
+ .is_ok()
+ {
+ inset = true;
+ continue;
+ }
+ }
+ if lengths.is_none() {
+ let value = input.try_parse::<_, _, ParseError>(|i| {
+ let horizontal = Length::parse(context, i)?;
+ let vertical = Length::parse(context, i)?;
+ let (blur, spread) =
+ match i.try_parse(|i| Length::parse_non_negative(context, i)) {
+ Ok(blur) => {
+ let spread = i.try_parse(|i| Length::parse(context, i)).ok();
+ (Some(blur.into()), spread)
+ },
+ Err(_) => (None, None),
+ };
+ Ok((horizontal, vertical, blur, spread))
+ });
+ if let Ok(value) = value {
+ lengths = Some(value);
+ continue;
+ }
+ }
+ if color.is_none() {
+ if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) {
+ color = Some(value);
+ continue;
+ }
+ }
+ break;
+ }
+
+ let lengths =
+ lengths.ok_or(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?;
+ Ok(BoxShadow {
+ base: SimpleShadow {
+ color: color,
+ horizontal: lengths.0,
+ vertical: lengths.1,
+ blur: lengths.2,
+ },
+ spread: lengths.3,
+ inset: inset,
+ })
+ }
+}
+
+impl ToComputedValue for BoxShadow {
+ type ComputedValue = ComputedBoxShadow;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ ComputedBoxShadow {
+ base: self.base.to_computed_value(context),
+ spread: self
+ .spread
+ .as_ref()
+ .unwrap_or(&Length::zero())
+ .to_computed_value(context),
+ inset: self.inset,
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &ComputedBoxShadow) -> Self {
+ BoxShadow {
+ base: ToComputedValue::from_computed_value(&computed.base),
+ spread: Some(ToComputedValue::from_computed_value(&computed.spread)),
+ inset: computed.inset,
+ }
+ }
+}
+
+// We need this for converting the specified Filter into computed Filter without Context (for
+// some FFIs in glue.rs). This can fail because in some circumstances, we still need Context to
+// determine the computed value.
+impl Filter {
+ /// Generate the ComputedFilter without Context.
+ pub fn to_computed_value_without_context(&self) -> Result<ComputedFilter, ()> {
+ match *self {
+ Filter::Blur(ref length) => Ok(ComputedFilter::Blur(ComputedNonNegativeLength::new(
+ length.0.to_computed_pixel_length_without_context()?,
+ ))),
+ Filter::Brightness(ref factor) => Ok(ComputedFilter::Brightness(
+ ComputedNonNegativeNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Contrast(ref factor) => Ok(ComputedFilter::Contrast(
+ ComputedNonNegativeNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Grayscale(ref factor) => Ok(ComputedFilter::Grayscale(
+ ComputedZeroToOneNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::HueRotate(ref angle) => Ok(ComputedFilter::HueRotate(
+ ComputedAngle::from_degrees(angle.degrees()),
+ )),
+ Filter::Invert(ref factor) => Ok(ComputedFilter::Invert(
+ ComputedZeroToOneNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Opacity(ref factor) => Ok(ComputedFilter::Opacity(
+ ComputedZeroToOneNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Saturate(ref factor) => Ok(ComputedFilter::Saturate(
+ ComputedNonNegativeNumber::from(factor.0.to_number().get()),
+ )),
+ Filter::Sepia(ref factor) => Ok(ComputedFilter::Sepia(ComputedZeroToOneNumber::from(
+ factor.0.to_number().get(),
+ ))),
+ Filter::DropShadow(ref shadow) => {
+ if cfg!(feature = "gecko") {
+ let color = match shadow
+ .color
+ .as_ref()
+ .unwrap_or(&Color::currentcolor())
+ .to_computed_color(None)
+ {
+ Some(c) => c,
+ None => return Err(()),
+ };
+
+ let horizontal = ComputedCSSPixelLength::new(
+ shadow
+ .horizontal
+ .to_computed_pixel_length_without_context()?,
+ );
+ let vertical = ComputedCSSPixelLength::new(
+ shadow.vertical.to_computed_pixel_length_without_context()?,
+ );
+ let blur = ComputedNonNegativeLength::new(
+ shadow
+ .blur
+ .as_ref()
+ .unwrap_or(&NonNegativeLength::zero())
+ .0
+ .to_computed_pixel_length_without_context()?,
+ );
+
+ Ok(ComputedFilter::DropShadow(ComputedSimpleShadow {
+ color,
+ horizontal,
+ vertical,
+ blur,
+ }))
+ } else {
+ Err(())
+ }
+ },
+ Filter::Url(ref url) => {
+ if cfg!(feature = "gecko") {
+ Ok(ComputedFilter::Url(ComputedUrl(url.clone())))
+ } else {
+ Err(())
+ }
+ },
+ }
+ }
+}
+
+impl Parse for Filter {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ #[cfg(feature = "gecko")]
+ {
+ if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
+ return Ok(GenericFilter::Url(url));
+ }
+ }
+ let location = input.current_source_location();
+ let function = match input.expect_function() {
+ Ok(f) => f.clone(),
+ Err(cssparser::BasicParseError {
+ kind: BasicParseErrorKind::UnexpectedToken(t),
+ location,
+ }) => return Err(location.new_custom_error(ValueParseErrorKind::InvalidFilter(t))),
+ Err(e) => return Err(e.into()),
+ };
+ input.parse_nested_block(|i| {
+ match_ignore_ascii_case! { &*function,
+ "blur" => Ok(GenericFilter::Blur(
+ i.try_parse(|i| NonNegativeLength::parse(context, i))
+ .unwrap_or(Zero::zero()),
+ )),
+ "brightness" => Ok(GenericFilter::Brightness(
+ i.try_parse(|i| NonNegativeFactor::parse(context, i))
+ .unwrap_or(NonNegativeFactor::one()),
+ )),
+ "contrast" => Ok(GenericFilter::Contrast(
+ i.try_parse(|i| NonNegativeFactor::parse(context, i))
+ .unwrap_or(NonNegativeFactor::one()),
+ )),
+ "grayscale" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-grayscale
+ Ok(GenericFilter::Grayscale(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "hue-rotate" => {
+ // We allow unitless zero here, see:
+ // https://github.com/w3c/fxtf-drafts/issues/228
+ Ok(GenericFilter::HueRotate(
+ i.try_parse(|i| Angle::parse_with_unitless(context, i))
+ .unwrap_or(Zero::zero()),
+ ))
+ },
+ "invert" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-invert
+ Ok(GenericFilter::Invert(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "opacity" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-opacity
+ Ok(GenericFilter::Opacity(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "saturate" => Ok(GenericFilter::Saturate(
+ i.try_parse(|i| NonNegativeFactor::parse(context, i))
+ .unwrap_or(NonNegativeFactor::one()),
+ )),
+ "sepia" => {
+ // Values of amount over 100% are allowed but UAs must clamp the values to 1.
+ // https://drafts.fxtf.org/filter-effects/#funcdef-filter-sepia
+ Ok(GenericFilter::Sepia(
+ i.try_parse(|i| ZeroToOneFactor::parse(context, i))
+ .unwrap_or(ZeroToOneFactor::one()),
+ ))
+ },
+ "drop-shadow" => Ok(GenericFilter::DropShadow(Parse::parse(context, i)?)),
+ _ => Err(location.new_custom_error(
+ ValueParseErrorKind::InvalidFilter(Token::Function(function.clone()))
+ )),
+ }
+ })
+ }
+}
+
+impl Parse for SimpleShadow {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let color = input.try_parse(|i| Color::parse(context, i)).ok();
+ let horizontal = Length::parse(context, input)?;
+ let vertical = Length::parse(context, input)?;
+ let blur = input
+ .try_parse(|i| Length::parse_non_negative(context, i))
+ .ok();
+ let blur = blur.map(NonNegative::<Length>);
+ let color = color.or_else(|| input.try_parse(|i| Color::parse(context, i)).ok());
+
+ Ok(SimpleShadow {
+ color,
+ horizontal,
+ vertical,
+ blur,
+ })
+ }
+}
+
+impl ToComputedValue for SimpleShadow {
+ type ComputedValue = ComputedSimpleShadow;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ ComputedSimpleShadow {
+ color: self
+ .color
+ .as_ref()
+ .unwrap_or(&Color::currentcolor())
+ .to_computed_value(context),
+ horizontal: self.horizontal.to_computed_value(context),
+ vertical: self.vertical.to_computed_value(context),
+ blur: self
+ .blur
+ .as_ref()
+ .unwrap_or(&NonNegativeLength::zero())
+ .to_computed_value(context),
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ SimpleShadow {
+ color: Some(ToComputedValue::from_computed_value(&computed.color)),
+ horizontal: ToComputedValue::from_computed_value(&computed.horizontal),
+ vertical: ToComputedValue::from_computed_value(&computed.vertical),
+ blur: Some(ToComputedValue::from_computed_value(&computed.blur)),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/flex.rs b/servo/components/style/values/specified/flex.rs
new file mode 100644
index 0000000000..7c767cdf34
--- /dev/null
+++ b/servo/components/style/values/specified/flex.rs
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values related to flexbox.
+
+use crate::values::generics::flex::FlexBasis as GenericFlexBasis;
+use crate::values::specified::Size;
+
+/// A specified value for the `flex-basis` property.
+pub type FlexBasis = GenericFlexBasis<Size>;
+
+impl FlexBasis {
+ /// `auto`
+ #[inline]
+ pub fn auto() -> Self {
+ GenericFlexBasis::Size(Size::auto())
+ }
+
+ /// `0%`
+ #[inline]
+ pub fn zero_percent() -> Self {
+ GenericFlexBasis::Size(Size::zero_percent())
+ }
+}
diff --git a/servo/components/style/values/specified/font.rs b/servo/components/style/values/specified/font.rs
new file mode 100644
index 0000000000..2435682ce3
--- /dev/null
+++ b/servo/components/style/values/specified/font.rs
@@ -0,0 +1,2222 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified values for font properties
+
+use crate::context::QuirksMode;
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::font::{FamilyName, FontFamilyList, SingleFontFamily};
+use crate::values::computed::Percentage as ComputedPercentage;
+use crate::values::computed::{font as computed, Length, NonNegativeLength};
+use crate::values::computed::{CSSPixelLength, Context, ToComputedValue};
+use crate::values::generics::font::{
+ self as generics, FeatureTagValue, FontSettings, FontTag, GenericLineHeight, VariationValue,
+};
+use crate::values::generics::NonNegative;
+use crate::values::specified::length::{FontBaseSize, LineHeightBase, PX_PER_PT};
+use crate::values::specified::{AllowQuirks, Angle, Integer, LengthPercentage};
+use crate::values::specified::{
+ FontRelativeLength, NoCalcLength, NonNegativeLengthPercentage, NonNegativeNumber,
+ NonNegativePercentage, Number,
+};
+use crate::values::{serialize_atom_identifier, CustomIdent, SelectorParseErrorKind};
+use crate::Atom;
+use cssparser::{Parser, Token};
+#[cfg(feature = "gecko")]
+use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalSizeOf};
+use std::fmt::{self, Write};
+use style_traits::values::SequenceWriter;
+use style_traits::{CssWriter, KeywordsCollectFn, ParseError};
+use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+// FIXME(emilio): The system font code is copy-pasta, and should be cleaned up.
+macro_rules! system_font_methods {
+ ($ty:ident, $field:ident) => {
+ system_font_methods!($ty);
+
+ fn compute_system(&self, _context: &Context) -> <$ty as ToComputedValue>::ComputedValue {
+ debug_assert!(matches!(*self, $ty::System(..)));
+ #[cfg(feature = "gecko")]
+ {
+ _context.cached_system_font.as_ref().unwrap().$field.clone()
+ }
+ #[cfg(feature = "servo")]
+ {
+ unreachable!()
+ }
+ }
+ };
+
+ ($ty:ident) => {
+ /// Get a specified value that represents a system font.
+ pub fn system_font(f: SystemFont) -> Self {
+ $ty::System(f)
+ }
+
+ /// Retreive a SystemFont from the specified value.
+ pub fn get_system(&self) -> Option<SystemFont> {
+ if let $ty::System(s) = *self {
+ Some(s)
+ } else {
+ None
+ }
+ }
+ };
+}
+
+/// System fonts.
+#[repr(u8)]
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum SystemFont {
+ /// https://drafts.csswg.org/css-fonts/#valdef-font-caption
+ Caption,
+ /// https://drafts.csswg.org/css-fonts/#valdef-font-icon
+ Icon,
+ /// https://drafts.csswg.org/css-fonts/#valdef-font-menu
+ Menu,
+ /// https://drafts.csswg.org/css-fonts/#valdef-font-message-box
+ MessageBox,
+ /// https://drafts.csswg.org/css-fonts/#valdef-font-small-caption
+ SmallCaption,
+ /// https://drafts.csswg.org/css-fonts/#valdef-font-status-bar
+ StatusBar,
+ /// Internal system font, used by the `<menupopup>`s on macOS.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozPullDownMenu,
+ /// Internal system font, used for `<button>` elements.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozButton,
+ /// Internal font, used by `<select>` elements.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozList,
+ /// Internal font, used by `<input>` elements.
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozField,
+ #[css(skip)]
+ End, // Just for indexing purposes.
+}
+
+const DEFAULT_SCRIPT_MIN_SIZE_PT: u32 = 8;
+const DEFAULT_SCRIPT_SIZE_MULTIPLIER: f64 = 0.71;
+
+/// The minimum font-weight value per:
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values
+pub const MIN_FONT_WEIGHT: f32 = 1.;
+
+/// The maximum font-weight value per:
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values
+pub const MAX_FONT_WEIGHT: f32 = 1000.;
+
+/// A specified font-weight value.
+///
+/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum FontWeight {
+ /// `<font-weight-absolute>`
+ Absolute(AbsoluteFontWeight),
+ /// Bolder variant
+ Bolder,
+ /// Lighter variant
+ Lighter,
+ /// System font variant.
+ #[css(skip)]
+ System(SystemFont),
+}
+
+impl FontWeight {
+ system_font_methods!(FontWeight, font_weight);
+
+ /// `normal`
+ #[inline]
+ pub fn normal() -> Self {
+ FontWeight::Absolute(AbsoluteFontWeight::Normal)
+ }
+
+ /// Get a specified FontWeight from a gecko keyword
+ pub fn from_gecko_keyword(kw: u32) -> Self {
+ debug_assert!(kw % 100 == 0);
+ debug_assert!(kw as f32 <= MAX_FONT_WEIGHT);
+ FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::new(kw as f32)))
+ }
+}
+
+impl ToComputedValue for FontWeight {
+ type ComputedValue = computed::FontWeight;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ FontWeight::Absolute(ref abs) => abs.compute(),
+ FontWeight::Bolder => context
+ .builder
+ .get_parent_font()
+ .clone_font_weight()
+ .bolder(),
+ FontWeight::Lighter => context
+ .builder
+ .get_parent_font()
+ .clone_font_weight()
+ .lighter(),
+ FontWeight::System(_) => self.compute_system(context),
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &computed::FontWeight) -> Self {
+ FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::from_computed_value(
+ &computed.value(),
+ )))
+ }
+}
+
+/// An absolute font-weight value for a @font-face rule.
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-weight-absolute-values
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum AbsoluteFontWeight {
+ /// A `<number>`, with the additional constraints specified in:
+ ///
+ /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values
+ Weight(Number),
+ /// Normal font weight. Same as 400.
+ Normal,
+ /// Bold font weight. Same as 700.
+ Bold,
+}
+
+impl AbsoluteFontWeight {
+ /// Returns the computed value for this absolute font weight.
+ pub fn compute(&self) -> computed::FontWeight {
+ match *self {
+ AbsoluteFontWeight::Weight(weight) => computed::FontWeight::from_float(weight.get()),
+ AbsoluteFontWeight::Normal => computed::FontWeight::NORMAL,
+ AbsoluteFontWeight::Bold => computed::FontWeight::BOLD,
+ }
+ }
+}
+
+impl Parse for AbsoluteFontWeight {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(number) = input.try_parse(|input| Number::parse(context, input)) {
+ // We could add another AllowedNumericType value, but it doesn't
+ // seem worth it just for a single property with such a weird range,
+ // so we do the clamping here manually.
+ if !number.was_calc() &&
+ (number.get() < MIN_FONT_WEIGHT || number.get() > MAX_FONT_WEIGHT)
+ {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ return Ok(AbsoluteFontWeight::Weight(number));
+ }
+
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "normal" => AbsoluteFontWeight::Normal,
+ "bold" => AbsoluteFontWeight::Bold,
+ })
+ }
+}
+
+/// The specified value of the `font-style` property, without the system font
+/// crap.
+pub type SpecifiedFontStyle = generics::FontStyle<Angle>;
+
+impl ToCss for SpecifiedFontStyle {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ generics::FontStyle::Normal => dest.write_str("normal"),
+ generics::FontStyle::Italic => dest.write_str("italic"),
+ generics::FontStyle::Oblique(ref angle) => {
+ dest.write_str("oblique")?;
+ if *angle != Self::default_angle() {
+ dest.write_char(' ')?;
+ angle.to_css(dest)?;
+ }
+ Ok(())
+ },
+ }
+ }
+}
+
+impl Parse for SpecifiedFontStyle {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ "normal" => generics::FontStyle::Normal,
+ "italic" => generics::FontStyle::Italic,
+ "oblique" => {
+ let angle = input.try_parse(|input| Self::parse_angle(context, input))
+ .unwrap_or_else(|_| Self::default_angle());
+
+ generics::FontStyle::Oblique(angle)
+ },
+ })
+ }
+}
+
+impl ToComputedValue for SpecifiedFontStyle {
+ type ComputedValue = computed::FontStyle;
+
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ match *self {
+ Self::Normal => computed::FontStyle::NORMAL,
+ Self::Italic => computed::FontStyle::ITALIC,
+ Self::Oblique(ref angle) => computed::FontStyle::oblique(angle.degrees()),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ if *computed == computed::FontStyle::NORMAL {
+ return Self::Normal;
+ }
+ if *computed == computed::FontStyle::ITALIC {
+ return Self::Italic;
+ }
+ let degrees = computed.oblique_degrees();
+ generics::FontStyle::Oblique(Angle::from_degrees(degrees, /* was_calc = */ false))
+ }
+}
+
+/// From https://drafts.csswg.org/css-fonts-4/#valdef-font-style-oblique-angle:
+///
+/// Values less than -90deg or values greater than 90deg are
+/// invalid and are treated as parse errors.
+///
+/// The maximum angle value that `font-style: oblique` should compute to.
+pub const FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES: f32 = 90.;
+
+/// The minimum angle value that `font-style: oblique` should compute to.
+pub const FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES: f32 = -90.;
+
+impl SpecifiedFontStyle {
+ /// Gets a clamped angle in degrees from a specified Angle.
+ pub fn compute_angle_degrees(angle: &Angle) -> f32 {
+ angle
+ .degrees()
+ .max(FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES)
+ .min(FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES)
+ }
+
+ /// Parse a suitable angle for font-style: oblique.
+ pub fn parse_angle<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Angle, ParseError<'i>> {
+ let angle = Angle::parse(context, input)?;
+ if angle.was_calc() {
+ return Ok(angle);
+ }
+
+ let degrees = angle.degrees();
+ if degrees < FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES ||
+ degrees > FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES
+ {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ return Ok(angle);
+ }
+
+ /// The default angle for `font-style: oblique`.
+ pub fn default_angle() -> Angle {
+ Angle::from_degrees(
+ computed::FontStyle::DEFAULT_OBLIQUE_DEGREES as f32,
+ /* was_calc = */ false,
+ )
+ }
+}
+
+/// The specified value of the `font-style` property.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum FontStyle {
+ Specified(SpecifiedFontStyle),
+ #[css(skip)]
+ System(SystemFont),
+}
+
+impl FontStyle {
+ /// Return the `normal` value.
+ #[inline]
+ pub fn normal() -> Self {
+ FontStyle::Specified(generics::FontStyle::Normal)
+ }
+
+ system_font_methods!(FontStyle, font_style);
+}
+
+impl ToComputedValue for FontStyle {
+ type ComputedValue = computed::FontStyle;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ FontStyle::Specified(ref specified) => specified.to_computed_value(context),
+ FontStyle::System(..) => self.compute_system(context),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ FontStyle::Specified(SpecifiedFontStyle::from_computed_value(computed))
+ }
+}
+
+/// A value for the `font-stretch` property.
+///
+/// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop
+#[allow(missing_docs)]
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum FontStretch {
+ Stretch(NonNegativePercentage),
+ Keyword(FontStretchKeyword),
+ #[css(skip)]
+ System(SystemFont),
+}
+
+/// A keyword value for `font-stretch`.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum FontStretchKeyword {
+ Normal,
+ Condensed,
+ UltraCondensed,
+ ExtraCondensed,
+ SemiCondensed,
+ SemiExpanded,
+ Expanded,
+ ExtraExpanded,
+ UltraExpanded,
+}
+
+impl FontStretchKeyword {
+ /// Turns the keyword into a computed value.
+ pub fn compute(&self) -> computed::FontStretch {
+ computed::FontStretch::from_keyword(*self)
+ }
+
+ /// Does the opposite operation to `compute`, in order to serialize keywords
+ /// if possible.
+ pub fn from_percentage(p: f32) -> Option<Self> {
+ computed::FontStretch::from_percentage(p).as_keyword()
+ }
+}
+
+impl FontStretch {
+ /// `normal`.
+ pub fn normal() -> Self {
+ FontStretch::Keyword(FontStretchKeyword::Normal)
+ }
+
+ system_font_methods!(FontStretch, font_stretch);
+}
+
+impl ToComputedValue for FontStretch {
+ type ComputedValue = computed::FontStretch;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ FontStretch::Stretch(ref percentage) => {
+ let percentage = percentage.to_computed_value(context).0;
+ computed::FontStretch::from_percentage(percentage.0)
+ },
+ FontStretch::Keyword(ref kw) => kw.compute(),
+ FontStretch::System(_) => self.compute_system(context),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ FontStretch::Stretch(NonNegativePercentage::from_computed_value(&NonNegative(
+ computed.to_percentage(),
+ )))
+ }
+}
+
+#[cfg(feature = "gecko")]
+fn math_depth_enabled(_context: &ParserContext) -> bool {
+ static_prefs::pref!("layout.css.math-depth.enabled")
+}
+
+#[cfg(feature = "servo")]
+fn math_depth_enabled(_context: &ParserContext) -> bool {
+ false
+}
+
+/// CSS font keywords
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+ Serialize,
+ Deserialize,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum FontSizeKeyword {
+ #[css(keyword = "xx-small")]
+ XXSmall,
+ XSmall,
+ Small,
+ Medium,
+ Large,
+ XLarge,
+ #[css(keyword = "xx-large")]
+ XXLarge,
+ #[css(keyword = "xxx-large")]
+ XXXLarge,
+ /// Indicate whether to apply font-size: math is specified so that extra
+ /// scaling due to math-depth changes is applied during the cascade.
+ #[parse(condition = "math_depth_enabled")]
+ Math,
+ #[css(skip)]
+ None,
+}
+
+impl FontSizeKeyword {
+ /// Convert to an HTML <font size> value
+ #[inline]
+ pub fn html_size(self) -> u8 {
+ self as u8
+ }
+}
+
+impl Default for FontSizeKeyword {
+ fn default() -> Self {
+ FontSizeKeyword::Medium
+ }
+}
+
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ ToAnimatedValue,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))]
+/// Additional information for keyword-derived font sizes.
+pub struct KeywordInfo {
+ /// The keyword used
+ pub kw: FontSizeKeyword,
+ /// A factor to be multiplied by the computed size of the keyword
+ #[css(skip)]
+ pub factor: f32,
+ /// An additional fixed offset to add to the kw * factor in the case of
+ /// `calc()`.
+ #[css(skip)]
+ pub offset: CSSPixelLength,
+}
+
+impl KeywordInfo {
+ /// KeywordInfo value for font-size: medium
+ pub fn medium() -> Self {
+ Self::new(FontSizeKeyword::Medium)
+ }
+
+ /// KeywordInfo value for font-size: none
+ pub fn none() -> Self {
+ Self::new(FontSizeKeyword::None)
+ }
+
+ fn new(kw: FontSizeKeyword) -> Self {
+ KeywordInfo {
+ kw,
+ factor: 1.,
+ offset: CSSPixelLength::new(0.),
+ }
+ }
+
+ /// Computes the final size for this font-size keyword, accounting for
+ /// text-zoom.
+ fn to_computed_value(&self, context: &Context) -> CSSPixelLength {
+ debug_assert_ne!(self.kw, FontSizeKeyword::None);
+ debug_assert_ne!(self.kw, FontSizeKeyword::Math);
+ let base = context.maybe_zoom_text(self.kw.to_length(context).0);
+ base * self.factor + context.maybe_zoom_text(self.offset)
+ }
+
+ /// Given a parent keyword info (self), apply an additional factor/offset to
+ /// it.
+ fn compose(self, factor: f32) -> Self {
+ if self.kw == FontSizeKeyword::None {
+ return self;
+ }
+ KeywordInfo {
+ kw: self.kw,
+ factor: self.factor * factor,
+ offset: self.offset * factor,
+ }
+ }
+}
+
+impl SpecifiedValueInfo for KeywordInfo {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ <FontSizeKeyword as SpecifiedValueInfo>::collect_completion_keywords(f);
+ }
+}
+
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+/// A specified font-size value
+pub enum FontSize {
+ /// A length; e.g. 10px.
+ Length(LengthPercentage),
+ /// A keyword value, along with a ratio and absolute offset.
+ /// The ratio in any specified keyword value
+ /// will be 1 (with offset 0), but we cascade keywordness even
+ /// after font-relative (percent and em) values
+ /// have been applied, which is where the ratio
+ /// comes in. The offset comes in if we cascaded a calc value,
+ /// where the font-relative portion (em and percentage) will
+ /// go into the ratio, and the remaining units all computed together
+ /// will go into the offset.
+ /// See bug 1355707.
+ Keyword(KeywordInfo),
+ /// font-size: smaller
+ Smaller,
+ /// font-size: larger
+ Larger,
+ /// Derived from a specified system font.
+ #[css(skip)]
+ System(SystemFont),
+}
+
+/// Specifies a prioritized list of font family names or generic family names.
+#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)]
+#[cfg_attr(feature = "servo", derive(Hash))]
+pub enum FontFamily {
+ /// List of `font-family`
+ #[css(comma)]
+ Values(#[css(iterable)] FontFamilyList),
+ /// System font
+ #[css(skip)]
+ System(SystemFont),
+}
+
+impl FontFamily {
+ system_font_methods!(FontFamily, font_family);
+}
+
+impl ToComputedValue for FontFamily {
+ type ComputedValue = computed::FontFamily;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ FontFamily::Values(ref list) => computed::FontFamily {
+ families: list.clone(),
+ is_system_font: false,
+ is_initial: false,
+ },
+ FontFamily::System(_) => self.compute_system(context),
+ }
+ }
+
+ fn from_computed_value(other: &computed::FontFamily) -> Self {
+ FontFamily::Values(other.families.clone())
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl MallocSizeOf for FontFamily {
+ fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize {
+ match *self {
+ FontFamily::Values(ref v) => {
+ // Although the family list is refcounted, we always attribute
+ // its size to the specified value.
+ v.list.unconditional_size_of(ops)
+ },
+ FontFamily::System(_) => 0,
+ }
+ }
+}
+
+impl Parse for FontFamily {
+ /// <family-name>#
+ /// <family-name> = <string> | [ <ident>+ ]
+ /// TODO: <generic-family>
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<FontFamily, ParseError<'i>> {
+ let values =
+ input.parse_comma_separated(|input| SingleFontFamily::parse(context, input))?;
+ Ok(FontFamily::Values(FontFamilyList {
+ list: crate::ArcSlice::from_iter(values.into_iter()),
+ }))
+ }
+}
+
+impl SpecifiedValueInfo for FontFamily {}
+
+/// `FamilyName::parse` is based on `SingleFontFamily::parse` and not the other
+/// way around because we want the former to exclude generic family keywords.
+impl Parse for FamilyName {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ match SingleFontFamily::parse(context, input) {
+ Ok(SingleFontFamily::FamilyName(name)) => Ok(name),
+ Ok(SingleFontFamily::Generic(_)) => {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ Err(e) => Err(e),
+ }
+ }
+}
+
+/// A factor for one of the font-size-adjust metrics, which may be either a number
+/// or the `from-font` keyword.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum FontSizeAdjustFactor {
+ /// An explicitly-specified number.
+ Number(NonNegativeNumber),
+ /// The from-font keyword: resolve the number from font metrics.
+ FromFont,
+}
+
+/// Specified value for font-size-adjust, intended to help
+/// preserve the readability of text when font fallback occurs.
+///
+/// https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop
+pub type FontSizeAdjust = generics::GenericFontSizeAdjust<FontSizeAdjustFactor>;
+
+impl Parse for FontSizeAdjust {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ // First check if we have an adjustment factor without a metrics-basis keyword.
+ if let Ok(factor) = input.try_parse(|i| FontSizeAdjustFactor::parse(context, i)) {
+ return Ok(Self::ExHeight(factor));
+ }
+
+ let ident = input.expect_ident()?;
+ let basis = match_ignore_ascii_case! { &ident,
+ "none" => return Ok(Self::None),
+ // Check for size adjustment basis keywords.
+ "ex-height" => Self::ExHeight,
+ "cap-height" => Self::CapHeight,
+ "ch-width" => Self::ChWidth,
+ "ic-width" => Self::IcWidth,
+ "ic-height" => Self::IcHeight,
+ // Unknown keyword.
+ _ => return Err(location.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ )),
+ };
+
+ Ok(basis(FontSizeAdjustFactor::parse(context, input)?))
+ }
+}
+
+/// This is the ratio applied for font-size: larger
+/// and smaller by both Firefox and Chrome
+const LARGER_FONT_SIZE_RATIO: f32 = 1.2;
+
+/// The default font size.
+pub const FONT_MEDIUM_PX: f32 = 16.0;
+/// The default line height.
+pub const FONT_MEDIUM_LINE_HEIGHT_PX: f32 = FONT_MEDIUM_PX * 1.2;
+
+impl FontSizeKeyword {
+ #[inline]
+ #[cfg(feature = "servo")]
+ fn to_length(&self, _: &Context) -> NonNegativeLength {
+ let medium = Length::new(FONT_MEDIUM_PX);
+ // https://drafts.csswg.org/css-fonts-3/#font-size-prop
+ NonNegative(match *self {
+ FontSizeKeyword::XXSmall => medium * 3.0 / 5.0,
+ FontSizeKeyword::XSmall => medium * 3.0 / 4.0,
+ FontSizeKeyword::Small => medium * 8.0 / 9.0,
+ FontSizeKeyword::Medium => medium,
+ FontSizeKeyword::Large => medium * 6.0 / 5.0,
+ FontSizeKeyword::XLarge => medium * 3.0 / 2.0,
+ FontSizeKeyword::XXLarge => medium * 2.0,
+ FontSizeKeyword::XXXLarge => medium * 3.0,
+ FontSizeKeyword::Math | FontSizeKeyword::None => unreachable!(),
+ })
+ }
+
+ #[cfg(feature = "gecko")]
+ #[inline]
+ fn to_length(&self, cx: &Context) -> NonNegativeLength {
+ let font = cx.style().get_font();
+ let family = &font.mFont.family.families;
+ let generic = family
+ .single_generic()
+ .unwrap_or(computed::GenericFontFamily::None);
+ let base_size = unsafe {
+ Atom::with(font.mLanguage.mRawPtr, |language| {
+ cx.device().base_size_for_generic(language, generic)
+ })
+ };
+ self.to_length_without_context(cx.quirks_mode, base_size)
+ }
+
+ /// Resolve a keyword length without any context, with explicit arguments.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn to_length_without_context(
+ &self,
+ quirks_mode: QuirksMode,
+ base_size: Length,
+ ) -> NonNegativeLength {
+ debug_assert_ne!(*self, FontSizeKeyword::Math);
+ // The tables in this function are originally from
+ // nsRuleNode::CalcFontPointSize in Gecko:
+ //
+ // https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3150
+ //
+ // Mapping from base size and HTML size to pixels
+ // The first index is (base_size - 9), the second is the
+ // HTML size. "0" is CSS keyword xx-small, not HTML size 0,
+ // since HTML size 0 is the same as 1.
+ //
+ // xxs xs s m l xl xxl -
+ // - 0/1 2 3 4 5 6 7
+ static FONT_SIZE_MAPPING: [[i32; 8]; 8] = [
+ [9, 9, 9, 9, 11, 14, 18, 27],
+ [9, 9, 9, 10, 12, 15, 20, 30],
+ [9, 9, 10, 11, 13, 17, 22, 33],
+ [9, 9, 10, 12, 14, 18, 24, 36],
+ [9, 10, 12, 13, 16, 20, 26, 39],
+ [9, 10, 12, 14, 17, 21, 28, 42],
+ [9, 10, 13, 15, 18, 23, 30, 45],
+ [9, 10, 13, 16, 18, 24, 32, 48],
+ ];
+
+ // This table gives us compatibility with WinNav4 for the default fonts only.
+ // In WinNav4, the default fonts were:
+ //
+ // Times/12pt == Times/16px at 96ppi
+ // Courier/10pt == Courier/13px at 96ppi
+ //
+ // xxs xs s m l xl xxl -
+ // - 1 2 3 4 5 6 7
+ static QUIRKS_FONT_SIZE_MAPPING: [[i32; 8]; 8] = [
+ [9, 9, 9, 9, 11, 14, 18, 28],
+ [9, 9, 9, 10, 12, 15, 20, 31],
+ [9, 9, 9, 11, 13, 17, 22, 34],
+ [9, 9, 10, 12, 14, 18, 24, 37],
+ [9, 9, 10, 13, 16, 20, 26, 40],
+ [9, 9, 11, 14, 17, 21, 28, 42],
+ [9, 10, 12, 15, 17, 23, 30, 45],
+ [9, 10, 13, 16, 18, 24, 32, 48],
+ ];
+
+ static FONT_SIZE_FACTORS: [i32; 8] = [60, 75, 89, 100, 120, 150, 200, 300];
+ let base_size_px = base_size.px().round() as i32;
+ let html_size = self.html_size() as usize;
+ NonNegative(if base_size_px >= 9 && base_size_px <= 16 {
+ let mapping = if quirks_mode == QuirksMode::Quirks {
+ QUIRKS_FONT_SIZE_MAPPING
+ } else {
+ FONT_SIZE_MAPPING
+ };
+ Length::new(mapping[(base_size_px - 9) as usize][html_size] as f32)
+ } else {
+ base_size * FONT_SIZE_FACTORS[html_size] as f32 / 100.0
+ })
+ }
+}
+
+impl FontSize {
+ /// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-font-size>
+ pub fn from_html_size(size: u8) -> Self {
+ FontSize::Keyword(KeywordInfo::new(match size {
+ // If value is less than 1, let it be 1.
+ 0 | 1 => FontSizeKeyword::XSmall,
+ 2 => FontSizeKeyword::Small,
+ 3 => FontSizeKeyword::Medium,
+ 4 => FontSizeKeyword::Large,
+ 5 => FontSizeKeyword::XLarge,
+ 6 => FontSizeKeyword::XXLarge,
+ // If value is greater than 7, let it be 7.
+ _ => FontSizeKeyword::XXXLarge,
+ }))
+ }
+
+ /// Compute it against a given base font size
+ pub fn to_computed_value_against(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> computed::FontSize {
+ let compose_keyword = |factor| {
+ context
+ .style()
+ .get_parent_font()
+ .clone_font_size()
+ .keyword_info
+ .compose(factor)
+ };
+ let mut info = KeywordInfo::none();
+ let size = match *self {
+ FontSize::Length(LengthPercentage::Length(ref l)) => {
+ if let NoCalcLength::FontRelative(ref value) = *l {
+ if let FontRelativeLength::Em(em) = *value {
+ // If the parent font was keyword-derived, this is
+ // too. Tack the em unit onto the factor
+ info = compose_keyword(em);
+ }
+ }
+ let result =
+ l.to_computed_value_with_base_size(context, base_size, line_height_base);
+ if l.should_zoom_text() {
+ context.maybe_zoom_text(result)
+ } else {
+ result
+ }
+ },
+ FontSize::Length(LengthPercentage::Percentage(pc)) => {
+ // If the parent font was keyword-derived, this is too.
+ // Tack the % onto the factor
+ info = compose_keyword(pc.0);
+ (base_size.resolve(context).computed_size() * pc.0).normalized()
+ },
+ FontSize::Length(LengthPercentage::Calc(ref calc)) => {
+ let calc = calc.to_computed_value_zoomed(context, base_size, line_height_base);
+ calc.resolve(base_size.resolve(context).computed_size())
+ },
+ FontSize::Keyword(i) => {
+ if i.kw == FontSizeKeyword::Math {
+ // Scaling is done in recompute_math_font_size_if_needed().
+ info = compose_keyword(1.);
+ info.kw = FontSizeKeyword::Math;
+ FontRelativeLength::Em(1.).to_computed_value(
+ context,
+ base_size,
+ line_height_base,
+ )
+ } else {
+ // As a specified keyword, this is keyword derived
+ info = i;
+ i.to_computed_value(context).clamp_to_non_negative()
+ }
+ },
+ FontSize::Smaller => {
+ info = compose_keyword(1. / LARGER_FONT_SIZE_RATIO);
+ FontRelativeLength::Em(1. / LARGER_FONT_SIZE_RATIO).to_computed_value(
+ context,
+ base_size,
+ line_height_base,
+ )
+ },
+ FontSize::Larger => {
+ info = compose_keyword(LARGER_FONT_SIZE_RATIO);
+ FontRelativeLength::Em(LARGER_FONT_SIZE_RATIO).to_computed_value(
+ context,
+ base_size,
+ line_height_base,
+ )
+ },
+
+ FontSize::System(_) => {
+ #[cfg(feature = "servo")]
+ {
+ unreachable!()
+ }
+ #[cfg(feature = "gecko")]
+ {
+ context
+ .cached_system_font
+ .as_ref()
+ .unwrap()
+ .font_size
+ .computed_size()
+ }
+ },
+ };
+ computed::FontSize {
+ computed_size: NonNegative(size),
+ used_size: NonNegative(size),
+ keyword_info: info,
+ }
+ }
+}
+
+impl ToComputedValue for FontSize {
+ type ComputedValue = computed::FontSize;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> computed::FontSize {
+ self.to_computed_value_against(
+ context,
+ FontBaseSize::InheritedStyle,
+ LineHeightBase::InheritedStyle,
+ )
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &computed::FontSize) -> Self {
+ FontSize::Length(LengthPercentage::Length(
+ ToComputedValue::from_computed_value(&computed.computed_size()),
+ ))
+ }
+}
+
+impl FontSize {
+ system_font_methods!(FontSize);
+
+ /// Get initial value for specified font size.
+ #[inline]
+ pub fn medium() -> Self {
+ FontSize::Keyword(KeywordInfo::medium())
+ }
+
+ /// Parses a font-size, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<FontSize, ParseError<'i>> {
+ if let Ok(lp) = input
+ .try_parse(|i| LengthPercentage::parse_non_negative_quirky(context, i, allow_quirks))
+ {
+ return Ok(FontSize::Length(lp));
+ }
+
+ if let Ok(kw) = input.try_parse(|i| FontSizeKeyword::parse(context, i)) {
+ return Ok(FontSize::Keyword(KeywordInfo::new(kw)));
+ }
+
+ try_match_ident_ignore_ascii_case! { input,
+ "smaller" => Ok(FontSize::Smaller),
+ "larger" => Ok(FontSize::Larger),
+ }
+ }
+}
+
+impl Parse for FontSize {
+ /// <length> | <percentage> | <absolute-size> | <relative-size>
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<FontSize, ParseError<'i>> {
+ FontSize::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+bitflags! {
+ #[derive(Clone, Copy)]
+ /// Flags of variant alternates in bit
+ struct VariantAlternatesParsingFlags: u8 {
+ /// None of variant alternates enabled
+ const NORMAL = 0;
+ /// Historical forms
+ const HISTORICAL_FORMS = 0x01;
+ /// Stylistic Alternates
+ const STYLISTIC = 0x02;
+ /// Stylistic Sets
+ const STYLESET = 0x04;
+ /// Character Variant
+ const CHARACTER_VARIANT = 0x08;
+ /// Swash glyphs
+ const SWASH = 0x10;
+ /// Ornaments glyphs
+ const ORNAMENTS = 0x20;
+ /// Annotation forms
+ const ANNOTATION = 0x40;
+ }
+}
+
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// Set of variant alternates
+pub enum VariantAlternates {
+ /// Enables display of stylistic alternates
+ #[css(function)]
+ Stylistic(CustomIdent),
+ /// Enables display with stylistic sets
+ #[css(comma, function)]
+ Styleset(#[css(iterable)] crate::OwnedSlice<CustomIdent>),
+ /// Enables display of specific character variants
+ #[css(comma, function)]
+ CharacterVariant(#[css(iterable)] crate::OwnedSlice<CustomIdent>),
+ /// Enables display of swash glyphs
+ #[css(function)]
+ Swash(CustomIdent),
+ /// Enables replacement of default glyphs with ornaments
+ #[css(function)]
+ Ornaments(CustomIdent),
+ /// Enables display of alternate annotation forms
+ #[css(function)]
+ Annotation(CustomIdent),
+ /// Enables display of historical forms
+ HistoricalForms,
+}
+
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+/// List of Variant Alternates
+pub struct FontVariantAlternates(
+ #[css(if_empty = "normal", iterable)] crate::OwnedSlice<VariantAlternates>,
+);
+
+impl FontVariantAlternates {
+ /// Returns the length of all variant alternates.
+ pub fn len(&self) -> usize {
+ self.0.iter().fold(0, |acc, alternate| match *alternate {
+ VariantAlternates::Swash(_) |
+ VariantAlternates::Stylistic(_) |
+ VariantAlternates::Ornaments(_) |
+ VariantAlternates::Annotation(_) => acc + 1,
+ VariantAlternates::Styleset(ref slice) |
+ VariantAlternates::CharacterVariant(ref slice) => acc + slice.len(),
+ _ => acc,
+ })
+ }
+}
+
+impl FontVariantAlternates {
+ #[inline]
+ /// Get initial specified value with VariantAlternatesList
+ pub fn get_initial_specified_value() -> Self {
+ Default::default()
+ }
+}
+
+impl Parse for FontVariantAlternates {
+ /// normal |
+ /// [ stylistic(<feature-value-name>) ||
+ /// historical-forms ||
+ /// styleset(<feature-value-name> #) ||
+ /// character-variant(<feature-value-name> #) ||
+ /// swash(<feature-value-name>) ||
+ /// ornaments(<feature-value-name>) ||
+ /// annotation(<feature-value-name>) ]
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<FontVariantAlternates, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(Default::default());
+ }
+
+ let mut stylistic = None;
+ let mut historical = None;
+ let mut styleset = None;
+ let mut character_variant = None;
+ let mut swash = None;
+ let mut ornaments = None;
+ let mut annotation = None;
+
+ // Parse values for the various alternate types in any order.
+ let mut parsed_alternates = VariantAlternatesParsingFlags::empty();
+ macro_rules! check_if_parsed(
+ ($input:expr, $flag:path) => (
+ if parsed_alternates.contains($flag) {
+ return Err($input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ parsed_alternates |= $flag;
+ )
+ );
+ while let Ok(_) = input.try_parse(|input| match *input.next()? {
+ Token::Ident(ref value) if value.eq_ignore_ascii_case("historical-forms") => {
+ check_if_parsed!(input, VariantAlternatesParsingFlags::HISTORICAL_FORMS);
+ historical = Some(VariantAlternates::HistoricalForms);
+ Ok(())
+ },
+ Token::Function(ref name) => {
+ let name = name.clone();
+ input.parse_nested_block(|i| {
+ match_ignore_ascii_case! { &name,
+ "swash" => {
+ check_if_parsed!(i, VariantAlternatesParsingFlags::SWASH);
+ let ident = CustomIdent::parse(i, &[])?;
+ swash = Some(VariantAlternates::Swash(ident));
+ Ok(())
+ },
+ "stylistic" => {
+ check_if_parsed!(i, VariantAlternatesParsingFlags::STYLISTIC);
+ let ident = CustomIdent::parse(i, &[])?;
+ stylistic = Some(VariantAlternates::Stylistic(ident));
+ Ok(())
+ },
+ "ornaments" => {
+ check_if_parsed!(i, VariantAlternatesParsingFlags::ORNAMENTS);
+ let ident = CustomIdent::parse(i, &[])?;
+ ornaments = Some(VariantAlternates::Ornaments(ident));
+ Ok(())
+ },
+ "annotation" => {
+ check_if_parsed!(i, VariantAlternatesParsingFlags::ANNOTATION);
+ let ident = CustomIdent::parse(i, &[])?;
+ annotation = Some(VariantAlternates::Annotation(ident));
+ Ok(())
+ },
+ "styleset" => {
+ check_if_parsed!(i, VariantAlternatesParsingFlags::STYLESET);
+ let idents = i.parse_comma_separated(|i| {
+ CustomIdent::parse(i, &[])
+ })?;
+ styleset = Some(VariantAlternates::Styleset(idents.into()));
+ Ok(())
+ },
+ "character-variant" => {
+ check_if_parsed!(i, VariantAlternatesParsingFlags::CHARACTER_VARIANT);
+ let idents = i.parse_comma_separated(|i| {
+ CustomIdent::parse(i, &[])
+ })?;
+ character_variant = Some(VariantAlternates::CharacterVariant(idents.into()));
+ Ok(())
+ },
+ _ => return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ })
+ },
+ _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }) {}
+
+ if parsed_alternates.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ // Collect the parsed values in canonical order, so that we'll serialize correctly.
+ let mut alternates = Vec::new();
+ macro_rules! push_if_some(
+ ($value:expr) => (
+ if let Some(v) = $value {
+ alternates.push(v);
+ }
+ )
+ );
+ push_if_some!(stylistic);
+ push_if_some!(historical);
+ push_if_some!(styleset);
+ push_if_some!(character_variant);
+ push_if_some!(swash);
+ push_if_some!(ornaments);
+ push_if_some!(annotation);
+
+ Ok(FontVariantAlternates(alternates.into()))
+ }
+}
+
+macro_rules! impl_variant_east_asian {
+ {
+ $(
+ $(#[$($meta:tt)+])*
+ $ident:ident / $css:expr => $gecko:ident = $value:expr,
+ )+
+ } => {
+ #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+ /// Variants for east asian variant
+ pub struct FontVariantEastAsian(u16);
+ bitflags! {
+ impl FontVariantEastAsian: u16 {
+ /// None of the features
+ const NORMAL = 0;
+ $(
+ $(#[$($meta)+])*
+ const $ident = $value;
+ )+
+ }
+ }
+
+ impl ToCss for FontVariantEastAsian {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_empty() {
+ return dest.write_str("normal");
+ }
+
+ let mut writer = SequenceWriter::new(dest, " ");
+ $(
+ if self.intersects(Self::$ident) {
+ writer.raw_item($css)?;
+ }
+ )+
+ Ok(())
+ }
+ }
+
+ /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn assert_variant_east_asian_matches() {
+ use crate::gecko_bindings::structs;
+ $(
+ debug_assert_eq!(structs::$gecko as u16, FontVariantEastAsian::$ident.bits());
+ )+
+ }
+
+ impl SpecifiedValueInfo for FontVariantEastAsian {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["normal", $($css,)+]);
+ }
+ }
+ }
+}
+
+impl_variant_east_asian! {
+ /// Enables rendering of JIS78 forms (OpenType feature: jp78)
+ JIS78 / "jis78" => NS_FONT_VARIANT_EAST_ASIAN_JIS78 = 0x01,
+ /// Enables rendering of JIS83 forms (OpenType feature: jp83).
+ JIS83 / "jis83" => NS_FONT_VARIANT_EAST_ASIAN_JIS83 = 0x02,
+ /// Enables rendering of JIS90 forms (OpenType feature: jp90).
+ JIS90 / "jis90" => NS_FONT_VARIANT_EAST_ASIAN_JIS90 = 0x04,
+ /// Enables rendering of JIS2004 forms (OpenType feature: jp04).
+ JIS04 / "jis04" => NS_FONT_VARIANT_EAST_ASIAN_JIS04 = 0x08,
+ /// Enables rendering of simplified forms (OpenType feature: smpl).
+ SIMPLIFIED / "simplified" => NS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED = 0x10,
+ /// Enables rendering of traditional forms (OpenType feature: trad).
+ TRADITIONAL / "traditional" => NS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL = 0x20,
+ /// Enables rendering of full-width variants (OpenType feature: fwid).
+ FULL_WIDTH / "full-width" => NS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH = 0x40,
+ /// Enables rendering of proportionally-spaced variants (OpenType feature: pwid).
+ PROPORTIONAL_WIDTH / "proportional-width" => NS_FONT_VARIANT_EAST_ASIAN_PROP_WIDTH = 0x80,
+ /// Enables display of ruby variant glyphs (OpenType feature: ruby).
+ RUBY / "ruby" => NS_FONT_VARIANT_EAST_ASIAN_RUBY = 0x100,
+}
+
+#[cfg(feature = "gecko")]
+impl FontVariantEastAsian {
+ /// Obtain a specified value from a Gecko keyword value
+ ///
+ /// Intended for use with presentation attributes, not style structs
+ pub fn from_gecko_keyword(kw: u16) -> Self {
+ Self::from_bits_truncate(kw)
+ }
+
+ /// Transform into gecko keyword
+ pub fn to_gecko_keyword(self) -> u16 {
+ self.bits()
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl_gecko_keyword_conversions!(FontVariantEastAsian, u16);
+
+impl Parse for FontVariantEastAsian {
+ /// normal | [ <east-asian-variant-values> || <east-asian-width-values> || ruby ]
+ /// <east-asian-variant-values> = [ jis78 | jis83 | jis90 | jis04 | simplified | traditional ]
+ /// <east-asian-width-values> = [ full-width | proportional-width ]
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut result = Self::empty();
+
+ if input
+ .try_parse(|input| input.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(result);
+ }
+
+ while let Ok(flag) = input.try_parse(|input| {
+ Ok(
+ match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?,
+ "jis78" =>
+ exclusive_value!((result, Self::JIS78 | Self::JIS83 |
+ Self::JIS90 | Self::JIS04 |
+ Self::SIMPLIFIED | Self::TRADITIONAL
+ ) => Self::JIS78),
+ "jis83" =>
+ exclusive_value!((result, Self::JIS78 | Self::JIS83 |
+ Self::JIS90 | Self::JIS04 |
+ Self::SIMPLIFIED | Self::TRADITIONAL
+ ) => Self::JIS83),
+ "jis90" =>
+ exclusive_value!((result, Self::JIS78 | Self::JIS83 |
+ Self::JIS90 | Self::JIS04 |
+ Self::SIMPLIFIED | Self::TRADITIONAL
+ ) => Self::JIS90),
+ "jis04" =>
+ exclusive_value!((result, Self::JIS78 | Self::JIS83 |
+ Self::JIS90 | Self::JIS04 |
+ Self::SIMPLIFIED | Self::TRADITIONAL
+ ) => Self::JIS04),
+ "simplified" =>
+ exclusive_value!((result, Self::JIS78 | Self::JIS83 |
+ Self::JIS90 | Self::JIS04 |
+ Self::SIMPLIFIED | Self::TRADITIONAL
+ ) => Self::SIMPLIFIED),
+ "traditional" =>
+ exclusive_value!((result, Self::JIS78 | Self::JIS83 |
+ Self::JIS90 | Self::JIS04 |
+ Self::SIMPLIFIED | Self::TRADITIONAL
+ ) => Self::TRADITIONAL),
+ "full-width" =>
+ exclusive_value!((result, Self::FULL_WIDTH |
+ Self::PROPORTIONAL_WIDTH
+ ) => Self::FULL_WIDTH),
+ "proportional-width" =>
+ exclusive_value!((result, Self::FULL_WIDTH |
+ Self::PROPORTIONAL_WIDTH
+ ) => Self::PROPORTIONAL_WIDTH),
+ "ruby" =>
+ exclusive_value!((result, Self::RUBY) => Self::RUBY),
+ _ => return Err(()),
+ },
+ )
+ }) {
+ result.insert(flag);
+ }
+
+ if !result.is_empty() {
+ Ok(result)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+macro_rules! impl_variant_ligatures {
+ {
+ $(
+ $(#[$($meta:tt)+])*
+ $ident:ident / $css:expr => $gecko:ident = $value:expr,
+ )+
+ } => {
+ #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+ /// Variants of ligatures
+ pub struct FontVariantLigatures(u16);
+ bitflags! {
+ impl FontVariantLigatures: u16 {
+ /// Specifies that common default features are enabled
+ const NORMAL = 0;
+ $(
+ $(#[$($meta)+])*
+ const $ident = $value;
+ )+
+ }
+ }
+
+ impl ToCss for FontVariantLigatures {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_empty() {
+ return dest.write_str("normal");
+ }
+ if self.contains(FontVariantLigatures::NONE) {
+ return dest.write_str("none");
+ }
+
+ let mut writer = SequenceWriter::new(dest, " ");
+ $(
+ if self.intersects(FontVariantLigatures::$ident) {
+ writer.raw_item($css)?;
+ }
+ )+
+ Ok(())
+ }
+ }
+
+ /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn assert_variant_ligatures_matches() {
+ use crate::gecko_bindings::structs;
+ $(
+ debug_assert_eq!(structs::$gecko as u16, FontVariantLigatures::$ident.bits());
+ )+
+ }
+
+ impl SpecifiedValueInfo for FontVariantLigatures {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["normal", $($css,)+]);
+ }
+ }
+ }
+}
+
+impl_variant_ligatures! {
+ /// Specifies that all types of ligatures and contextual forms
+ /// covered by this property are explicitly disabled
+ NONE / "none" => NS_FONT_VARIANT_LIGATURES_NONE = 0x01,
+ /// Enables display of common ligatures
+ COMMON_LIGATURES / "common-ligatures" => NS_FONT_VARIANT_LIGATURES_COMMON = 0x02,
+ /// Disables display of common ligatures
+ NO_COMMON_LIGATURES / "no-common-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_COMMON = 0x04,
+ /// Enables display of discretionary ligatures
+ DISCRETIONARY_LIGATURES / "discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_DISCRETIONARY = 0x08,
+ /// Disables display of discretionary ligatures
+ NO_DISCRETIONARY_LIGATURES / "no-discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_DISCRETIONARY = 0x10,
+ /// Enables display of historical ligatures
+ HISTORICAL_LIGATURES / "historical-ligatures" => NS_FONT_VARIANT_LIGATURES_HISTORICAL = 0x20,
+ /// Disables display of historical ligatures
+ NO_HISTORICAL_LIGATURES / "no-historical-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_HISTORICAL = 0x40,
+ /// Enables display of contextual alternates
+ CONTEXTUAL / "contextual" => NS_FONT_VARIANT_LIGATURES_CONTEXTUAL = 0x80,
+ /// Disables display of contextual alternates
+ NO_CONTEXTUAL / "no-contextual" => NS_FONT_VARIANT_LIGATURES_NO_CONTEXTUAL = 0x100,
+}
+
+#[cfg(feature = "gecko")]
+impl FontVariantLigatures {
+ /// Obtain a specified value from a Gecko keyword value
+ ///
+ /// Intended for use with presentation attributes, not style structs
+ pub fn from_gecko_keyword(kw: u16) -> Self {
+ Self::from_bits_truncate(kw)
+ }
+
+ /// Transform into gecko keyword
+ pub fn to_gecko_keyword(self) -> u16 {
+ self.bits()
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl_gecko_keyword_conversions!(FontVariantLigatures, u16);
+
+impl Parse for FontVariantLigatures {
+ /// normal | none |
+ /// [ <common-lig-values> ||
+ /// <discretionary-lig-values> ||
+ /// <historical-lig-values> ||
+ /// <contextual-alt-values> ]
+ /// <common-lig-values> = [ common-ligatures | no-common-ligatures ]
+ /// <discretionary-lig-values> = [ discretionary-ligatures | no-discretionary-ligatures ]
+ /// <historical-lig-values> = [ historical-ligatures | no-historical-ligatures ]
+ /// <contextual-alt-values> = [ contextual | no-contextual ]
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut result = Self::empty();
+ if input
+ .try_parse(|input| input.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(result);
+ }
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(Self::NONE);
+ }
+
+ while let Ok(flag) = input.try_parse(|input| {
+ Ok(
+ match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?,
+ "common-ligatures" =>
+ exclusive_value!((result, Self::COMMON_LIGATURES |
+ Self::NO_COMMON_LIGATURES
+ ) => Self::COMMON_LIGATURES),
+ "no-common-ligatures" =>
+ exclusive_value!((result, Self::COMMON_LIGATURES |
+ Self::NO_COMMON_LIGATURES
+ ) => Self::NO_COMMON_LIGATURES),
+ "discretionary-ligatures" =>
+ exclusive_value!((result, Self::DISCRETIONARY_LIGATURES |
+ Self::NO_DISCRETIONARY_LIGATURES
+ ) => Self::DISCRETIONARY_LIGATURES),
+ "no-discretionary-ligatures" =>
+ exclusive_value!((result, Self::DISCRETIONARY_LIGATURES |
+ Self::NO_DISCRETIONARY_LIGATURES
+ ) => Self::NO_DISCRETIONARY_LIGATURES),
+ "historical-ligatures" =>
+ exclusive_value!((result, Self::HISTORICAL_LIGATURES |
+ Self::NO_HISTORICAL_LIGATURES
+ ) => Self::HISTORICAL_LIGATURES),
+ "no-historical-ligatures" =>
+ exclusive_value!((result, Self::HISTORICAL_LIGATURES |
+ Self::NO_HISTORICAL_LIGATURES
+ ) => Self::NO_HISTORICAL_LIGATURES),
+ "contextual" =>
+ exclusive_value!((result, Self::CONTEXTUAL |
+ Self::NO_CONTEXTUAL
+ ) => Self::CONTEXTUAL),
+ "no-contextual" =>
+ exclusive_value!((result, Self::CONTEXTUAL |
+ Self::NO_CONTEXTUAL
+ ) => Self::NO_CONTEXTUAL),
+ _ => return Err(()),
+ },
+ )
+ }) {
+ result.insert(flag);
+ }
+
+ if !result.is_empty() {
+ Ok(result)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+macro_rules! impl_variant_numeric {
+ {
+ $(
+ $(#[$($meta:tt)+])*
+ $ident:ident / $css:expr => $gecko:ident = $value:expr,
+ )+
+ } => {
+ #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)]
+ /// Variants of numeric values
+ pub struct FontVariantNumeric(u8);
+ bitflags! {
+ impl FontVariantNumeric: u8 {
+ /// None of other variants are enabled.
+ const NORMAL = 0;
+ $(
+ $(#[$($meta)+])*
+ const $ident = $value;
+ )+
+ }
+ }
+
+ impl ToCss for FontVariantNumeric {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_empty() {
+ return dest.write_str("normal");
+ }
+
+ let mut writer = SequenceWriter::new(dest, " ");
+ $(
+ if self.intersects(FontVariantNumeric::$ident) {
+ writer.raw_item($css)?;
+ }
+ )+
+ Ok(())
+ }
+ }
+
+ /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value.
+ #[cfg(feature = "gecko")]
+ #[inline]
+ pub fn assert_variant_numeric_matches() {
+ use crate::gecko_bindings::structs;
+ $(
+ debug_assert_eq!(structs::$gecko as u8, FontVariantNumeric::$ident.bits());
+ )+
+ }
+
+ impl SpecifiedValueInfo for FontVariantNumeric {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["normal", $($css,)+]);
+ }
+ }
+ }
+}
+
+impl_variant_numeric! {
+ /// Enables display of lining numerals.
+ LINING_NUMS / "lining-nums" => NS_FONT_VARIANT_NUMERIC_LINING = 0x01,
+ /// Enables display of old-style numerals.
+ OLDSTYLE_NUMS / "oldstyle-nums" => NS_FONT_VARIANT_NUMERIC_OLDSTYLE = 0x02,
+ /// Enables display of proportional numerals.
+ PROPORTIONAL_NUMS / "proportional-nums" => NS_FONT_VARIANT_NUMERIC_PROPORTIONAL = 0x04,
+ /// Enables display of tabular numerals.
+ TABULAR_NUMS / "tabular-nums" => NS_FONT_VARIANT_NUMERIC_TABULAR = 0x08,
+ /// Enables display of lining diagonal fractions.
+ DIAGONAL_FRACTIONS / "diagonal-fractions" => NS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS = 0x10,
+ /// Enables display of lining stacked fractions.
+ STACKED_FRACTIONS / "stacked-fractions" => NS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS = 0x20,
+ /// Enables display of letter forms used with ordinal numbers.
+ ORDINAL / "ordinal" => NS_FONT_VARIANT_NUMERIC_ORDINAL = 0x80,
+ /// Enables display of slashed zeros.
+ SLASHED_ZERO / "slashed-zero" => NS_FONT_VARIANT_NUMERIC_SLASHZERO = 0x40,
+}
+
+#[cfg(feature = "gecko")]
+impl FontVariantNumeric {
+ /// Obtain a specified value from a Gecko keyword value
+ ///
+ /// Intended for use with presentation attributes, not style structs
+ pub fn from_gecko_keyword(kw: u8) -> Self {
+ Self::from_bits_truncate(kw)
+ }
+
+ /// Transform into gecko keyword
+ pub fn to_gecko_keyword(self) -> u8 {
+ self.bits()
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl_gecko_keyword_conversions!(FontVariantNumeric, u8);
+
+impl Parse for FontVariantNumeric {
+ /// normal |
+ /// [ <numeric-figure-values> ||
+ /// <numeric-spacing-values> ||
+ /// <numeric-fraction-values> ||
+ /// ordinal ||
+ /// slashed-zero ]
+ /// <numeric-figure-values> = [ lining-nums | oldstyle-nums ]
+ /// <numeric-spacing-values> = [ proportional-nums | tabular-nums ]
+ /// <numeric-fraction-values> = [ diagonal-fractions | stacked-fractions ]
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut result = Self::empty();
+
+ if input
+ .try_parse(|input| input.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(result);
+ }
+
+ while let Ok(flag) = input.try_parse(|input| {
+ Ok(
+ match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?,
+ "ordinal" =>
+ exclusive_value!((result, Self::ORDINAL) => Self::ORDINAL),
+ "slashed-zero" =>
+ exclusive_value!((result, Self::SLASHED_ZERO) => Self::SLASHED_ZERO),
+ "lining-nums" =>
+ exclusive_value!((result, Self::LINING_NUMS |
+ Self::OLDSTYLE_NUMS
+ ) => Self::LINING_NUMS),
+ "oldstyle-nums" =>
+ exclusive_value!((result, Self::LINING_NUMS |
+ Self::OLDSTYLE_NUMS
+ ) => Self::OLDSTYLE_NUMS),
+ "proportional-nums" =>
+ exclusive_value!((result, Self::PROPORTIONAL_NUMS |
+ Self::TABULAR_NUMS
+ ) => Self::PROPORTIONAL_NUMS),
+ "tabular-nums" =>
+ exclusive_value!((result, Self::PROPORTIONAL_NUMS |
+ Self::TABULAR_NUMS
+ ) => Self::TABULAR_NUMS),
+ "diagonal-fractions" =>
+ exclusive_value!((result, Self::DIAGONAL_FRACTIONS |
+ Self::STACKED_FRACTIONS
+ ) => Self::DIAGONAL_FRACTIONS),
+ "stacked-fractions" =>
+ exclusive_value!((result, Self::DIAGONAL_FRACTIONS |
+ Self::STACKED_FRACTIONS
+ ) => Self::STACKED_FRACTIONS),
+ _ => return Err(()),
+ },
+ )
+ }) {
+ result.insert(flag);
+ }
+
+ if !result.is_empty() {
+ Ok(result)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+/// This property provides low-level control over OpenType or TrueType font features.
+pub type FontFeatureSettings = FontSettings<FeatureTagValue<Integer>>;
+
+/// For font-language-override, use the same representation as the computed value.
+pub use crate::values::computed::font::FontLanguageOverride;
+
+impl Parse for FontLanguageOverride {
+ /// normal | <string>
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<FontLanguageOverride, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(FontLanguageOverride::normal());
+ }
+
+ let string = input.expect_string()?;
+
+ // The OpenType spec requires tags to be 1 to 4 ASCII characters:
+ // https://learn.microsoft.com/en-gb/typography/opentype/spec/otff#data-types
+ if string.is_empty() || string.len() > 4 || !string.is_ascii() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let mut bytes = [b' '; 4];
+ for (byte, str_byte) in bytes.iter_mut().zip(string.as_bytes()) {
+ *byte = *str_byte;
+ }
+
+ Ok(FontLanguageOverride(u32::from_be_bytes(bytes)))
+ }
+}
+
+/// A value for any of the font-synthesis-{weight,style,small-caps} properties.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum FontSynthesis {
+ /// This attribute may be synthesized if not supported by a face.
+ Auto,
+ /// Do not attempt to synthesis this style attribute.
+ None,
+}
+
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Allows authors to choose a palette from those supported by a color font
+/// (and potentially @font-palette-values overrides).
+pub struct FontPalette(Atom);
+
+#[allow(missing_docs)]
+impl FontPalette {
+ pub fn normal() -> Self {
+ Self(atom!("normal"))
+ }
+ pub fn light() -> Self {
+ Self(atom!("light"))
+ }
+ pub fn dark() -> Self {
+ Self(atom!("dark"))
+ }
+}
+
+impl Parse for FontPalette {
+ /// normal | light | dark | dashed-ident
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<FontPalette, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ match_ignore_ascii_case! { &ident,
+ "normal" => Ok(Self::normal()),
+ "light" => Ok(Self::light()),
+ "dark" => Ok(Self::dark()),
+ _ => if ident.starts_with("--") {
+ Ok(Self(Atom::from(ident.as_ref())))
+ } else {
+ Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())))
+ },
+ }
+ }
+}
+
+impl ToCss for FontPalette {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_atom_identifier(&self.0, dest)
+ }
+}
+
+/// This property provides low-level control over OpenType or TrueType font
+/// variations.
+pub type FontVariationSettings = FontSettings<VariationValue<Number>>;
+
+fn parse_one_feature_value<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<Integer, ParseError<'i>> {
+ if let Ok(integer) = input.try_parse(|i| Integer::parse_non_negative(context, i)) {
+ return Ok(integer);
+ }
+
+ try_match_ident_ignore_ascii_case! { input,
+ "on" => Ok(Integer::new(1)),
+ "off" => Ok(Integer::new(0)),
+ }
+}
+
+impl Parse for FeatureTagValue<Integer> {
+ /// https://drafts.csswg.org/css-fonts-4/#feature-tag-value
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let tag = FontTag::parse(context, input)?;
+ let value = input
+ .try_parse(|i| parse_one_feature_value(context, i))
+ .unwrap_or_else(|_| Integer::new(1));
+
+ Ok(Self { tag, value })
+ }
+}
+
+impl Parse for VariationValue<Number> {
+ /// This is the `<string> <number>` part of the font-variation-settings
+ /// syntax.
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let tag = FontTag::parse(context, input)?;
+ let value = Number::parse(context, input)?;
+ Ok(Self { tag, value })
+ }
+}
+
+/// A metrics override value for a @font-face descriptor
+///
+/// https://drafts.csswg.org/css-fonts/#font-metrics-override-desc
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum MetricsOverride {
+ /// A non-negative `<percentage>` of the computed font size
+ Override(NonNegativePercentage),
+ /// Normal metrics from the font.
+ Normal,
+}
+
+impl MetricsOverride {
+ #[inline]
+ /// Get default value with `normal`
+ pub fn normal() -> MetricsOverride {
+ MetricsOverride::Normal
+ }
+
+ /// The ToComputedValue implementation, used for @font-face descriptors.
+ ///
+ /// Valid override percentages must be non-negative; we return -1.0 to indicate
+ /// the absence of an override (i.e. 'normal').
+ #[inline]
+ pub fn compute(&self) -> ComputedPercentage {
+ match *self {
+ MetricsOverride::Normal => ComputedPercentage(-1.0),
+ MetricsOverride::Override(percent) => ComputedPercentage(percent.0.get()),
+ }
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+/// How to do font-size scaling.
+pub enum XTextScale {
+ /// Both min-font-size and text zoom are enabled.
+ All,
+ /// Text-only zoom is enabled, but min-font-size is not honored.
+ ZoomOnly,
+ /// Neither of them is enabled.
+ None,
+}
+
+impl XTextScale {
+ /// Returns whether text zoom is enabled.
+ #[inline]
+ pub fn text_zoom_enabled(self) -> bool {
+ self != Self::None
+ }
+}
+
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+/// Internal property that reflects the lang attribute
+pub struct XLang(#[css(skip)] pub Atom);
+
+impl XLang {
+ #[inline]
+ /// Get default value for `-x-lang`
+ pub fn get_initial_value() -> XLang {
+ XLang(atom!(""))
+ }
+}
+
+impl Parse for XLang {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<XLang, ParseError<'i>> {
+ debug_assert!(
+ false,
+ "Should be set directly by presentation attributes only."
+ );
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+/// Specifies the minimum font size allowed due to changes in scriptlevel.
+/// Ref: https://wiki.mozilla.org/MathML:mstyle
+pub struct MozScriptMinSize(pub NoCalcLength);
+
+impl MozScriptMinSize {
+ #[inline]
+ /// Calculate initial value of -moz-script-min-size.
+ pub fn get_initial_value() -> Length {
+ Length::new(DEFAULT_SCRIPT_MIN_SIZE_PT as f32 * PX_PER_PT)
+ }
+}
+
+impl Parse for MozScriptMinSize {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MozScriptMinSize, ParseError<'i>> {
+ debug_assert!(
+ false,
+ "Should be set directly by presentation attributes only."
+ );
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+/// A value for the `math-depth` property.
+/// https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum MathDepth {
+ /// Increment math-depth if math-style is compact.
+ AutoAdd,
+
+ /// Add the function's argument to math-depth.
+ #[css(function)]
+ Add(Integer),
+
+ /// Set math-depth to the specified value.
+ Absolute(Integer),
+}
+
+impl Parse for MathDepth {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MathDepth, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("auto-add"))
+ .is_ok()
+ {
+ return Ok(MathDepth::AutoAdd);
+ }
+ if let Ok(math_depth_value) = input.try_parse(|input| Integer::parse(context, input)) {
+ return Ok(MathDepth::Absolute(math_depth_value));
+ }
+ input.expect_function_matching("add")?;
+ let math_depth_delta_value =
+ input.parse_nested_block(|input| Integer::parse(context, input))?;
+ Ok(MathDepth::Add(math_depth_delta_value))
+ }
+}
+
+#[cfg_attr(feature = "gecko", derive(MallocSizeOf))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+/// Specifies the multiplier to be used to adjust font size
+/// due to changes in scriptlevel.
+///
+/// Ref: https://www.w3.org/TR/MathML3/chapter3.html#presm.mstyle.attrs
+pub struct MozScriptSizeMultiplier(pub f32);
+
+impl MozScriptSizeMultiplier {
+ #[inline]
+ /// Get default value of `-moz-script-size-multiplier`
+ pub fn get_initial_value() -> MozScriptSizeMultiplier {
+ MozScriptSizeMultiplier(DEFAULT_SCRIPT_SIZE_MULTIPLIER as f32)
+ }
+}
+
+impl Parse for MozScriptSizeMultiplier {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MozScriptSizeMultiplier, ParseError<'i>> {
+ debug_assert!(
+ false,
+ "Should be set directly by presentation attributes only."
+ );
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+impl From<f32> for MozScriptSizeMultiplier {
+ fn from(v: f32) -> Self {
+ MozScriptSizeMultiplier(v)
+ }
+}
+
+impl From<MozScriptSizeMultiplier> for f32 {
+ fn from(v: MozScriptSizeMultiplier) -> f32 {
+ v.0
+ }
+}
+
+/// A specified value for the `line-height` property.
+pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>;
+
+impl ToComputedValue for LineHeight {
+ type ComputedValue = computed::LineHeight;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ GenericLineHeight::Normal => GenericLineHeight::Normal,
+ #[cfg(feature = "gecko")]
+ GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight,
+ GenericLineHeight::Number(number) => {
+ GenericLineHeight::Number(number.to_computed_value(context))
+ },
+ GenericLineHeight::Length(ref non_negative_lp) => {
+ let result = match non_negative_lp.0 {
+ LengthPercentage::Length(NoCalcLength::Absolute(ref abs)) => {
+ context.maybe_zoom_text(abs.to_computed_value(context))
+ },
+ LengthPercentage::Length(ref length) => {
+ // line-height units specifically resolve against parent's
+ // font and line-height properties, while the rest of font
+ // relative units still resolve against the element's own
+ // properties.
+ length.to_computed_value_with_base_size(
+ context,
+ FontBaseSize::CurrentStyle,
+ LineHeightBase::InheritedStyle,
+ )
+ },
+ LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0)
+ .to_computed_value(
+ context,
+ FontBaseSize::CurrentStyle,
+ LineHeightBase::InheritedStyle,
+ ),
+ LengthPercentage::Calc(ref calc) => {
+ let computed_calc = calc.to_computed_value_zoomed(
+ context,
+ FontBaseSize::CurrentStyle,
+ LineHeightBase::InheritedStyle,
+ );
+ let base = context.style().get_font().clone_font_size().computed_size();
+ computed_calc.resolve(base)
+ },
+ };
+ GenericLineHeight::Length(result.into())
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match *computed {
+ GenericLineHeight::Normal => GenericLineHeight::Normal,
+ #[cfg(feature = "gecko")]
+ GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight,
+ GenericLineHeight::Number(ref number) => {
+ GenericLineHeight::Number(NonNegativeNumber::from_computed_value(number))
+ },
+ GenericLineHeight::Length(ref length) => {
+ GenericLineHeight::Length(NoCalcLength::from_computed_value(&length.0).into())
+ },
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/gecko.rs b/servo/components/style/values/specified/gecko.rs
new file mode 100644
index 0000000000..e721add59c
--- /dev/null
+++ b/servo/components/style/values/specified/gecko.rs
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for legacy Gecko-only properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{self, Length, LengthPercentage};
+use crate::values::generics::rect::Rect;
+use cssparser::{Parser, Token};
+use std::fmt;
+use style_traits::values::SequenceWriter;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+fn parse_pixel_or_percent<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<LengthPercentage, ParseError<'i>> {
+ let location = input.current_source_location();
+ let token = input.next()?;
+ let value = match *token {
+ Token::Dimension {
+ value, ref unit, ..
+ } => {
+ match_ignore_ascii_case! { unit,
+ "px" => Ok(LengthPercentage::new_length(Length::new(value))),
+ _ => Err(()),
+ }
+ },
+ Token::Percentage { unit_value, .. } => Ok(LengthPercentage::new_percent(
+ computed::Percentage(unit_value),
+ )),
+ _ => Err(()),
+ };
+ value.map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+}
+
+/// The value of an IntersectionObserver's rootMargin property.
+///
+/// Only bare px or percentage values are allowed. Other length units and
+/// calc() values are not allowed.
+///
+/// <https://w3c.github.io/IntersectionObserver/#parse-a-root-margin>
+#[repr(transparent)]
+pub struct IntersectionObserverRootMargin(pub Rect<LengthPercentage>);
+
+impl Parse for IntersectionObserverRootMargin {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::Zero;
+ if input.is_exhausted() {
+ // If there are zero elements in tokens, set tokens to ["0px"].
+ return Ok(IntersectionObserverRootMargin(Rect::all(
+ LengthPercentage::zero(),
+ )));
+ }
+ let rect = Rect::parse_with(context, input, parse_pixel_or_percent)?;
+ Ok(IntersectionObserverRootMargin(rect))
+ }
+}
+
+// Strictly speaking this is not ToCss. It's serializing for DOM. But
+// we can just reuse the infrastructure of this.
+//
+// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin>
+impl ToCss for IntersectionObserverRootMargin {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ // We cannot use the ToCss impl of Rect, because that would
+ // merge items when they are equal. We want to list them all.
+ let mut writer = SequenceWriter::new(dest, " ");
+ let rect = &self.0;
+ writer.item(&rect.0)?;
+ writer.item(&rect.1)?;
+ writer.item(&rect.2)?;
+ writer.item(&rect.3)
+ }
+}
diff --git a/servo/components/style/values/specified/grid.rs b/servo/components/style/values/specified/grid.rs
new file mode 100644
index 0000000000..5c78ff399b
--- /dev/null
+++ b/servo/components/style/values/specified/grid.rs
@@ -0,0 +1,441 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the computed value of
+//! [grids](https://drafts.csswg.org/css-grid/)
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::grid::{GridTemplateComponent, ImplicitGridTracks, RepeatCount};
+use crate::values::generics::grid::{LineNameList, LineNameListValue, NameRepeat, TrackBreadth};
+use crate::values::generics::grid::{TrackList, TrackListValue, TrackRepeat, TrackSize};
+use crate::values::specified::{Integer, LengthPercentage};
+use crate::values::{CSSFloat, CustomIdent};
+use cssparser::{Parser, Token};
+use std::mem;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// Parse a single flexible length.
+pub fn parse_flex<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CSSFloat, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Dimension {
+ value, ref unit, ..
+ } if unit.eq_ignore_ascii_case("fr") && value.is_sign_positive() => Ok(value),
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+}
+
+impl<L> TrackBreadth<L> {
+ fn parse_keyword<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> {
+ #[derive(Parse)]
+ enum TrackKeyword {
+ Auto,
+ MaxContent,
+ MinContent,
+ }
+
+ Ok(match TrackKeyword::parse(input)? {
+ TrackKeyword::Auto => TrackBreadth::Auto,
+ TrackKeyword::MaxContent => TrackBreadth::MaxContent,
+ TrackKeyword::MinContent => TrackBreadth::MinContent,
+ })
+ }
+}
+
+impl Parse for TrackBreadth<LengthPercentage> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // FIXME: This and other callers in this file should use
+ // NonNegativeLengthPercentage instead.
+ //
+ // Though it seems these cannot be animated so it's ~ok.
+ if let Ok(lp) = input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) {
+ return Ok(TrackBreadth::Breadth(lp));
+ }
+
+ if let Ok(f) = input.try_parse(parse_flex) {
+ return Ok(TrackBreadth::Fr(f));
+ }
+
+ Self::parse_keyword(input)
+ }
+}
+
+impl Parse for TrackSize<LengthPercentage> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(b) = input.try_parse(|i| TrackBreadth::parse(context, i)) {
+ return Ok(TrackSize::Breadth(b));
+ }
+
+ if input
+ .try_parse(|i| i.expect_function_matching("minmax"))
+ .is_ok()
+ {
+ return input.parse_nested_block(|input| {
+ let inflexible_breadth =
+ match input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) {
+ Ok(lp) => TrackBreadth::Breadth(lp),
+ Err(..) => TrackBreadth::parse_keyword(input)?,
+ };
+
+ input.expect_comma()?;
+ Ok(TrackSize::Minmax(
+ inflexible_breadth,
+ TrackBreadth::parse(context, input)?,
+ ))
+ });
+ }
+
+ input.expect_function_matching("fit-content")?;
+ let lp = input.parse_nested_block(|i| LengthPercentage::parse_non_negative(context, i))?;
+ Ok(TrackSize::FitContent(TrackBreadth::Breadth(lp)))
+ }
+}
+
+impl Parse for ImplicitGridTracks<TrackSize<LengthPercentage>> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use style_traits::{Separator, Space};
+ let track_sizes = Space::parse(input, |i| TrackSize::parse(context, i))?;
+ if track_sizes.len() == 1 && track_sizes[0].is_initial() {
+ // A single track with the initial value is always represented by an empty slice.
+ return Ok(Default::default());
+ }
+ return Ok(ImplicitGridTracks(track_sizes.into()));
+ }
+}
+
+/// Parse the grid line names into a vector of owned strings.
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-line-names>
+pub fn parse_line_names<'i, 't>(
+ input: &mut Parser<'i, 't>,
+) -> Result<crate::OwnedSlice<CustomIdent>, ParseError<'i>> {
+ input.expect_square_bracket_block()?;
+ input.parse_nested_block(|input| {
+ let mut values = vec![];
+ while let Ok(ident) = input.try_parse(|i| CustomIdent::parse(i, &["span", "auto"])) {
+ values.push(ident);
+ }
+
+ Ok(values.into())
+ })
+}
+
+/// The type of `repeat` function (only used in parsing).
+///
+/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat>
+#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo)]
+#[cfg_attr(feature = "servo", derive(MallocSizeOf))]
+enum RepeatType {
+ /// [`<auto-repeat>`](https://drafts.csswg.org/css-grid/#typedef-auto-repeat)
+ Auto,
+ /// [`<track-repeat>`](https://drafts.csswg.org/css-grid/#typedef-track-repeat)
+ Normal,
+ /// [`<fixed-repeat>`](https://drafts.csswg.org/css-grid/#typedef-fixed-repeat)
+ Fixed,
+}
+
+impl TrackRepeat<LengthPercentage, Integer> {
+ fn parse_with_repeat_type<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<(Self, RepeatType), ParseError<'i>> {
+ input
+ .try_parse(|i| i.expect_function_matching("repeat").map_err(|e| e.into()))
+ .and_then(|_| {
+ input.parse_nested_block(|input| {
+ let count = RepeatCount::parse(context, input)?;
+ input.expect_comma()?;
+
+ let is_auto = count == RepeatCount::AutoFit || count == RepeatCount::AutoFill;
+ let mut repeat_type = if is_auto {
+ RepeatType::Auto
+ } else {
+ // <fixed-size> is a subset of <track-size>, so it should work for both
+ RepeatType::Fixed
+ };
+
+ let mut names = vec![];
+ let mut values = vec![];
+ let mut current_names;
+
+ loop {
+ current_names = input.try_parse(parse_line_names).unwrap_or_default();
+ if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) {
+ if !track_size.is_fixed() {
+ if is_auto {
+ // should be <fixed-size> for <auto-repeat>
+ return Err(input
+ .new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if repeat_type == RepeatType::Fixed {
+ repeat_type = RepeatType::Normal // <track-size> for sure
+ }
+ }
+
+ values.push(track_size);
+ names.push(current_names);
+ } else {
+ if values.is_empty() {
+ // expecting at least one <track-size>
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ );
+ }
+
+ names.push(current_names); // final `<line-names>`
+ break; // no more <track-size>, breaking
+ }
+ }
+
+ let repeat = TrackRepeat {
+ count,
+ track_sizes: values.into(),
+ line_names: names.into(),
+ };
+
+ Ok((repeat, repeat_type))
+ })
+ })
+ }
+}
+
+impl Parse for TrackList<LengthPercentage, Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut current_names = vec![];
+ let mut names = vec![];
+ let mut values = vec![];
+
+ // Whether we've parsed an `<auto-repeat>` value.
+ let mut auto_repeat_index = None;
+ // assume that everything is <fixed-size>. This flag is useful when we encounter <auto-repeat>
+ let mut at_least_one_not_fixed = false;
+ loop {
+ current_names
+ .extend_from_slice(&mut input.try_parse(parse_line_names).unwrap_or_default());
+ if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) {
+ if !track_size.is_fixed() {
+ at_least_one_not_fixed = true;
+ if auto_repeat_index.is_some() {
+ // <auto-track-list> only accepts <fixed-size> and <fixed-repeat>
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ let vec = mem::replace(&mut current_names, vec![]);
+ names.push(vec.into());
+ values.push(TrackListValue::TrackSize(track_size));
+ } else if let Ok((repeat, type_)) =
+ input.try_parse(|i| TrackRepeat::parse_with_repeat_type(context, i))
+ {
+ match type_ {
+ RepeatType::Normal => {
+ at_least_one_not_fixed = true;
+ if auto_repeat_index.is_some() {
+ // only <fixed-repeat>
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ );
+ }
+ },
+ RepeatType::Auto => {
+ if auto_repeat_index.is_some() || at_least_one_not_fixed {
+ // We've either seen <auto-repeat> earlier, or there's at least one non-fixed value
+ return Err(
+ input.new_custom_error(StyleParseErrorKind::UnspecifiedError)
+ );
+ }
+ auto_repeat_index = Some(values.len());
+ },
+ RepeatType::Fixed => {},
+ }
+
+ let vec = mem::replace(&mut current_names, vec![]);
+ names.push(vec.into());
+ values.push(TrackListValue::TrackRepeat(repeat));
+ } else {
+ if values.is_empty() && auto_repeat_index.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ names.push(current_names.into());
+ break;
+ }
+ }
+
+ Ok(TrackList {
+ auto_repeat_index: auto_repeat_index.unwrap_or(std::usize::MAX),
+ values: values.into(),
+ line_names: names.into(),
+ })
+ }
+}
+
+#[cfg(feature = "gecko")]
+#[inline]
+fn allow_grid_template_subgrids() -> bool {
+ true
+}
+
+#[cfg(feature = "servo")]
+#[inline]
+fn allow_grid_template_subgrids() -> bool {
+ false
+}
+
+#[cfg(feature = "gecko")]
+#[inline]
+fn allow_grid_template_masonry() -> bool {
+ static_prefs::pref!("layout.css.grid-template-masonry-value.enabled")
+}
+
+#[cfg(feature = "servo")]
+#[inline]
+fn allow_grid_template_masonry() -> bool {
+ false
+}
+
+impl Parse for GridTemplateComponent<LengthPercentage, Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(GridTemplateComponent::None);
+ }
+
+ Self::parse_without_none(context, input)
+ }
+}
+
+impl GridTemplateComponent<LengthPercentage, Integer> {
+ /// Parses a `GridTemplateComponent<LengthPercentage>` except `none` keyword.
+ pub fn parse_without_none<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if allow_grid_template_subgrids() {
+ if let Ok(t) = input.try_parse(|i| LineNameList::parse(context, i)) {
+ return Ok(GridTemplateComponent::Subgrid(Box::new(t)));
+ }
+ }
+ if allow_grid_template_masonry() {
+ if input
+ .try_parse(|i| i.expect_ident_matching("masonry"))
+ .is_ok()
+ {
+ return Ok(GridTemplateComponent::Masonry);
+ }
+ }
+ let track_list = TrackList::parse(context, input)?;
+ Ok(GridTemplateComponent::TrackList(Box::new(track_list)))
+ }
+}
+
+impl Parse for NameRepeat<Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("repeat")?;
+ input.parse_nested_block(|i| {
+ let count = RepeatCount::parse(context, i)?;
+ // NameRepeat doesn't accept `auto-fit`
+ // https://drafts.csswg.org/css-grid/#typedef-name-repeat
+ if matches!(count, RepeatCount::AutoFit) {
+ return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ i.expect_comma()?;
+ let mut names_list = vec![];
+ names_list.push(parse_line_names(i)?); // there should be at least one
+ while let Ok(names) = i.try_parse(parse_line_names) {
+ names_list.push(names);
+ }
+
+ Ok(NameRepeat {
+ count,
+ line_names: names_list.into(),
+ })
+ })
+ }
+}
+
+impl Parse for LineNameListValue<Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(repeat) = input.try_parse(|i| NameRepeat::parse(context, i)) {
+ return Ok(LineNameListValue::Repeat(repeat));
+ }
+
+ parse_line_names(input).map(LineNameListValue::LineNames)
+ }
+}
+
+impl LineNameListValue<Integer> {
+ /// Returns the length of `<line-names>` after expanding repeat(N, ...). This returns zero for
+ /// repeat(auto-fill, ...).
+ #[inline]
+ pub fn line_names_length(&self) -> usize {
+ match *self {
+ Self::LineNames(..) => 1,
+ Self::Repeat(ref r) => {
+ match r.count {
+ // Note: RepeatCount is always >= 1.
+ RepeatCount::Number(v) => r.line_names.len() * v.value() as usize,
+ _ => 0,
+ }
+ },
+ }
+ }
+}
+
+impl Parse for LineNameList<Integer> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_ident_matching("subgrid")?;
+
+ let mut auto_repeat = false;
+ let mut expanded_line_names_length = 0;
+ let mut line_names = vec![];
+ while let Ok(value) = input.try_parse(|i| LineNameListValue::parse(context, i)) {
+ match value {
+ LineNameListValue::Repeat(ref r) if r.is_auto_fill() => {
+ if auto_repeat {
+ // On a subgridded axis, the auto-fill keyword is only valid once per
+ // <line-name-list>.
+ // https://drafts.csswg.org/css-grid/#auto-repeat
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ auto_repeat = true;
+ },
+ _ => (),
+ };
+
+ expanded_line_names_length += value.line_names_length();
+ line_names.push(value);
+ }
+
+ Ok(LineNameList {
+ expanded_line_names_length,
+ line_names: line_names.into(),
+ })
+ }
+}
diff --git a/servo/components/style/values/specified/image.rs b/servo/components/style/values/specified/image.rs
new file mode 100644
index 0000000000..76bbbf85df
--- /dev/null
+++ b/servo/components/style/values/specified/image.rs
@@ -0,0 +1,1340 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the specified value of
+//! [`image`][image]s
+//!
+//! [image]: https://drafts.csswg.org/css-images/#image-values
+
+use crate::color::mix::ColorInterpolationMethod;
+use crate::parser::{Parse, ParserContext};
+use crate::stylesheets::CorsMode;
+use crate::values::generics::color::ColorMixFlags;
+use crate::values::generics::image::{
+ self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent,
+};
+use crate::values::generics::image::{GradientFlags, PaintWorklet};
+use crate::values::generics::position::Position as GenericPosition;
+use crate::values::generics::NonNegative;
+use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword};
+use crate::values::specified::position::{Position, PositionComponent, Side};
+use crate::values::specified::url::SpecifiedImageUrl;
+use crate::values::specified::{
+ Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength,
+ NonNegativeLengthPercentage, Resolution,
+};
+use crate::values::specified::{Number, NumberOrPercentage, Percentage};
+use crate::Atom;
+use cssparser::{Delimiter, Parser, Token};
+use selectors::parser::SelectorParseErrorKind;
+#[cfg(feature = "servo")]
+use servo_url::ServoUrl;
+use std::cmp::Ordering;
+use std::fmt::{self, Write};
+use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError};
+use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+#[inline]
+fn gradient_color_interpolation_method_enabled() -> bool {
+ static_prefs::pref!("layout.css.gradient-color-interpolation-method.enabled")
+}
+
+/// Specified values for an image according to CSS-IMAGES.
+/// <https://drafts.csswg.org/css-images/#image-values>
+pub type Image = generic::Image<Gradient, SpecifiedImageUrl, Color, Percentage, Resolution>;
+
+// Images should remain small, see https://github.com/servo/servo/pull/18430
+size_of_test!(Image, 16);
+
+/// Specified values for a CSS gradient.
+/// <https://drafts.csswg.org/css-images/#gradients>
+pub type Gradient = generic::Gradient<
+ LineDirection,
+ LengthPercentage,
+ NonNegativeLength,
+ NonNegativeLengthPercentage,
+ Position,
+ Angle,
+ AngleOrPercentage,
+ Color,
+>;
+
+/// Specified values for CSS cross-fade
+/// cross-fade( CrossFadeElement, ...)
+/// <https://drafts.csswg.org/css-images-4/#cross-fade-function>
+pub type CrossFade = generic::CrossFade<Image, Color, Percentage>;
+/// CrossFadeElement = percent? CrossFadeImage
+pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>;
+/// CrossFadeImage = image | color
+pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>;
+
+/// `image-set()`
+pub type ImageSet = generic::ImageSet<Image, Resolution>;
+
+/// Each of the arguments to `image-set()`
+pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>;
+
+type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>;
+
+impl Color {
+ fn has_modern_syntax(&self) -> bool {
+ match self {
+ Self::Absolute(absolute) => !absolute.color.is_legacy_syntax(),
+ Self::ColorMix(mix) => {
+ if mix.flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) {
+ true
+ } else {
+ mix.left.has_modern_syntax() || mix.right.has_modern_syntax()
+ }
+ },
+ Self::LightDark(ld) => ld.light.has_modern_syntax() || ld.dark.has_modern_syntax(),
+
+ // The default is that this color doesn't have any modern syntax.
+ _ => false,
+ }
+ }
+}
+
+fn default_color_interpolation_method<T>(
+ items: &[generic::GradientItem<Color, T>],
+) -> ColorInterpolationMethod {
+ let has_modern_syntax_item = items.iter().any(|item| match item {
+ generic::GenericGradientItem::SimpleColorStop(color) => color.has_modern_syntax(),
+ generic::GenericGradientItem::ComplexColorStop { color, .. } => color.has_modern_syntax(),
+ generic::GenericGradientItem::InterpolationHint(_) => false,
+ });
+
+ if has_modern_syntax_item {
+ ColorInterpolationMethod::oklab()
+ } else {
+ ColorInterpolationMethod::srgb()
+ }
+}
+
+#[cfg(feature = "gecko")]
+fn cross_fade_enabled() -> bool {
+ static_prefs::pref!("layout.css.cross-fade.enabled")
+}
+
+#[cfg(feature = "servo")]
+fn cross_fade_enabled() -> bool {
+ false
+}
+
+impl SpecifiedValueInfo for Gradient {
+ const SUPPORTED_TYPES: u8 = CssType::GRADIENT;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ // This list here should keep sync with that in Gradient::parse.
+ f(&[
+ "linear-gradient",
+ "-webkit-linear-gradient",
+ "-moz-linear-gradient",
+ "repeating-linear-gradient",
+ "-webkit-repeating-linear-gradient",
+ "-moz-repeating-linear-gradient",
+ "radial-gradient",
+ "-webkit-radial-gradient",
+ "-moz-radial-gradient",
+ "repeating-radial-gradient",
+ "-webkit-repeating-radial-gradient",
+ "-moz-repeating-radial-gradient",
+ "-webkit-gradient",
+ "conic-gradient",
+ "repeating-conic-gradient",
+ ]);
+ }
+}
+
+// Need to manually implement as whether or not cross-fade shows up in
+// completions & etc is dependent on it being enabled.
+impl<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> {
+ const SUPPORTED_TYPES: u8 = 0;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ if cross_fade_enabled() {
+ f(&["cross-fade"]);
+ }
+ }
+}
+
+impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> {
+ const SUPPORTED_TYPES: u8 = 0;
+
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["image-set"]);
+ }
+}
+
+/// A specified gradient line direction.
+///
+/// FIXME(emilio): This should be generic over Angle.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum LineDirection {
+ /// An angular direction.
+ Angle(Angle),
+ /// A horizontal direction.
+ Horizontal(HorizontalPositionKeyword),
+ /// A vertical direction.
+ Vertical(VerticalPositionKeyword),
+ /// A direction towards a corner of a box.
+ Corner(HorizontalPositionKeyword, VerticalPositionKeyword),
+}
+
+/// A specified ending shape.
+pub type EndingShape = generic::EndingShape<NonNegativeLength, NonNegativeLengthPercentage>;
+
+bitflags! {
+ #[derive(Clone, Copy)]
+ struct ParseImageFlags: u8 {
+ const FORBID_NONE = 1 << 0;
+ const FORBID_IMAGE_SET = 1 << 1;
+ const FORBID_NON_URL = 1 << 2;
+ }
+}
+
+impl Parse for Image {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Image::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::empty())
+ }
+}
+
+impl Image {
+ fn parse_with_cors_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Image, ParseError<'i>> {
+ if !flags.contains(ParseImageFlags::FORBID_NONE) &&
+ input.try_parse(|i| i.expect_ident_matching("none")).is_ok()
+ {
+ return Ok(generic::Image::None);
+ }
+
+ if let Ok(url) = input
+ .try_parse(|input| SpecifiedImageUrl::parse_with_cors_mode(context, input, cors_mode))
+ {
+ return Ok(generic::Image::Url(url));
+ }
+
+ if !flags.contains(ParseImageFlags::FORBID_IMAGE_SET) {
+ if let Ok(is) =
+ input.try_parse(|input| ImageSet::parse(context, input, cors_mode, flags))
+ {
+ return Ok(generic::Image::ImageSet(Box::new(is)));
+ }
+ }
+
+ if flags.contains(ParseImageFlags::FORBID_NON_URL) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) {
+ return Ok(generic::Image::Gradient(Box::new(gradient)));
+ }
+
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(|input| {
+ Ok(match_ignore_ascii_case! { &function,
+ #[cfg(feature = "servo-layout-2013")]
+ "paint" => Self::PaintWorklet(PaintWorklet::parse_args(context, input)?),
+ "cross-fade" if cross_fade_enabled() => Self::CrossFade(Box::new(CrossFade::parse_args(context, input, cors_mode, flags)?)),
+ #[cfg(feature = "gecko")]
+ "-moz-element" => Self::Element(Self::parse_element(input)?),
+ _ => return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function))),
+ })
+ })
+ }
+}
+
+impl Image {
+ /// Creates an already specified image value from an already resolved URL
+ /// for insertion in the cascade.
+ #[cfg(feature = "servo")]
+ pub fn for_cascade(url: ServoUrl) -> Self {
+ use crate::values::CssUrl;
+ generic::Image::Url(CssUrl::for_cascade(url))
+ }
+
+ /// Parses a `-moz-element(# <element-id>)`.
+ #[cfg(feature = "gecko")]
+ fn parse_element<'i>(input: &mut Parser<'i, '_>) -> Result<Atom, ParseError<'i>> {
+ let location = input.current_source_location();
+ Ok(match *input.next()? {
+ Token::IDHash(ref id) => Atom::from(id.as_ref()),
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ })
+ }
+
+ /// Provides an alternate method for parsing that associates the URL with
+ /// anonymous CORS headers.
+ pub fn parse_with_cors_anonymous<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Self::parse_with_cors_mode(
+ context,
+ input,
+ CorsMode::Anonymous,
+ ParseImageFlags::empty(),
+ )
+ }
+
+ /// Provides an alternate method for parsing, but forbidding `none`
+ pub fn parse_forbid_none<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Self::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::FORBID_NONE)
+ }
+
+ /// Provides an alternate method for parsing, but only for urls.
+ pub fn parse_only_url<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Image, ParseError<'i>> {
+ Self::parse_with_cors_mode(
+ context,
+ input,
+ CorsMode::None,
+ ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL,
+ )
+ }
+}
+
+impl CrossFade {
+ /// cross-fade() = cross-fade( <cf-image># )
+ fn parse_args<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ let elements = crate::OwnedSlice::from(input.parse_comma_separated(|input| {
+ CrossFadeElement::parse(context, input, cors_mode, flags)
+ })?);
+ Ok(Self { elements })
+ }
+}
+
+impl CrossFadeElement {
+ fn parse_percentage<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Option<Percentage> {
+ // We clamp our values here as this is the way that Safari and Chrome's
+ // implementation handle out-of-bounds percentages but whether or not
+ // this behavior follows the specification is still being discussed.
+ // See: <https://github.com/w3c/csswg-drafts/issues/5333>
+ input
+ .try_parse(|input| Percentage::parse_non_negative(context, input))
+ .ok()
+ .map(|p| p.clamp_to_hundred())
+ }
+
+ /// <cf-image> = <percentage>? && [ <image> | <color> ]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ // Try and parse a leading percent sign.
+ let mut percent = Self::parse_percentage(context, input);
+ // Parse the image
+ let image = CrossFadeImage::parse(context, input, cors_mode, flags)?;
+ // Try and parse a trailing percent sign.
+ if percent.is_none() {
+ percent = Self::parse_percentage(context, input);
+ }
+ Ok(Self {
+ percent: percent.into(),
+ image,
+ })
+ }
+}
+
+impl CrossFadeImage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(image) = input.try_parse(|input| {
+ Image::parse_with_cors_mode(
+ context,
+ input,
+ cors_mode,
+ flags | ParseImageFlags::FORBID_NONE,
+ )
+ }) {
+ return Ok(Self::Image(image));
+ }
+ Ok(Self::Color(Color::parse(context, input)?))
+ }
+}
+
+impl ImageSet {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ let function = input.expect_function()?;
+ match_ignore_ascii_case! { &function,
+ "-webkit-image-set" | "image-set" => {},
+ _ => {
+ let func = function.clone();
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func)));
+ }
+ }
+ let items = input.parse_nested_block(|input| {
+ input.parse_comma_separated(|input| {
+ ImageSetItem::parse(context, input, cors_mode, flags)
+ })
+ })?;
+ Ok(Self {
+ selected_index: std::usize::MAX,
+ items: items.into(),
+ })
+ }
+}
+
+impl ImageSetItem {
+ fn parse_type<'i>(p: &mut Parser<'i, '_>) -> Result<crate::OwnedStr, ParseError<'i>> {
+ p.expect_function_matching("type")?;
+ p.parse_nested_block(|input| Ok(input.expect_string()?.as_ref().to_owned().into()))
+ }
+
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ cors_mode: CorsMode,
+ flags: ParseImageFlags,
+ ) -> Result<Self, ParseError<'i>> {
+ let image = match input.try_parse(|i| i.expect_url_or_string()) {
+ Ok(url) => Image::Url(SpecifiedImageUrl::parse_from_string(
+ url.as_ref().into(),
+ context,
+ cors_mode,
+ )),
+ Err(..) => Image::parse_with_cors_mode(
+ context,
+ input,
+ cors_mode,
+ flags | ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_IMAGE_SET,
+ )?,
+ };
+
+ let mut resolution = input
+ .try_parse(|input| Resolution::parse(context, input))
+ .ok();
+ let mime_type = input.try_parse(Self::parse_type).ok();
+
+ // Try to parse resolution after type().
+ if mime_type.is_some() && resolution.is_none() {
+ resolution = input
+ .try_parse(|input| Resolution::parse(context, input))
+ .ok();
+ }
+
+ let resolution = resolution.unwrap_or_else(|| Resolution::from_x(1.0));
+ let has_mime_type = mime_type.is_some();
+ let mime_type = mime_type.unwrap_or_default();
+
+ Ok(Self {
+ image,
+ resolution,
+ has_mime_type,
+ mime_type,
+ })
+ }
+}
+
+impl Parse for Gradient {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ enum Shape {
+ Linear,
+ Radial,
+ Conic,
+ }
+
+ let func = input.expect_function()?;
+ let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func,
+ "linear-gradient" => {
+ (Shape::Linear, false, GradientCompatMode::Modern)
+ },
+ "-webkit-linear-gradient" => {
+ (Shape::Linear, false, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-linear-gradient" => {
+ (Shape::Linear, false, GradientCompatMode::Moz)
+ },
+ "repeating-linear-gradient" => {
+ (Shape::Linear, true, GradientCompatMode::Modern)
+ },
+ "-webkit-repeating-linear-gradient" => {
+ (Shape::Linear, true, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-repeating-linear-gradient" => {
+ (Shape::Linear, true, GradientCompatMode::Moz)
+ },
+ "radial-gradient" => {
+ (Shape::Radial, false, GradientCompatMode::Modern)
+ },
+ "-webkit-radial-gradient" => {
+ (Shape::Radial, false, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-radial-gradient" => {
+ (Shape::Radial, false, GradientCompatMode::Moz)
+ },
+ "repeating-radial-gradient" => {
+ (Shape::Radial, true, GradientCompatMode::Modern)
+ },
+ "-webkit-repeating-radial-gradient" => {
+ (Shape::Radial, true, GradientCompatMode::WebKit)
+ },
+ #[cfg(feature = "gecko")]
+ "-moz-repeating-radial-gradient" => {
+ (Shape::Radial, true, GradientCompatMode::Moz)
+ },
+ "conic-gradient" => {
+ (Shape::Conic, false, GradientCompatMode::Modern)
+ },
+ "repeating-conic-gradient" => {
+ (Shape::Conic, true, GradientCompatMode::Modern)
+ },
+ "-webkit-gradient" => {
+ return input.parse_nested_block(|i| {
+ Self::parse_webkit_gradient_argument(context, i)
+ });
+ },
+ _ => {
+ let func = func.clone();
+ return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func)));
+ }
+ };
+
+ Ok(input.parse_nested_block(|i| {
+ Ok(match shape {
+ Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?,
+ Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?,
+ Shape::Conic => Self::parse_conic(context, i, repeating)?,
+ })
+ })?)
+ }
+}
+
+impl Gradient {
+ fn parse_webkit_gradient_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::position::{
+ HorizontalPositionKeyword as X, VerticalPositionKeyword as Y,
+ };
+ type Point = GenericPosition<Component<X>, Component<Y>>;
+
+ #[derive(Clone, Copy, Parse)]
+ enum Component<S> {
+ Center,
+ Number(NumberOrPercentage),
+ Side(S),
+ }
+
+ impl LineDirection {
+ fn from_points(first: Point, second: Point) -> Self {
+ let h_ord = first.horizontal.partial_cmp(&second.horizontal);
+ let v_ord = first.vertical.partial_cmp(&second.vertical);
+ let (h, v) = match (h_ord, v_ord) {
+ (Some(h), Some(v)) => (h, v),
+ _ => return LineDirection::Vertical(Y::Bottom),
+ };
+ match (h, v) {
+ (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom),
+ (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right),
+ (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top),
+ (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top),
+ (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => {
+ LineDirection::Vertical(Y::Bottom)
+ },
+ (Ordering::Greater, Ordering::Less) => {
+ LineDirection::Corner(X::Left, Y::Bottom)
+ },
+ (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left),
+ (Ordering::Greater, Ordering::Greater) => {
+ LineDirection::Corner(X::Left, Y::Top)
+ },
+ }
+ }
+ }
+
+ impl From<Point> for Position {
+ fn from(point: Point) -> Self {
+ Self::new(point.horizontal.into(), point.vertical.into())
+ }
+ }
+
+ impl Parse for Point {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ input.try_parse(|i| {
+ let x = Component::parse(context, i)?;
+ let y = Component::parse(context, i)?;
+
+ Ok(Self::new(x, y))
+ })
+ }
+ }
+
+ impl<S: Side> From<Component<S>> for NumberOrPercentage {
+ fn from(component: Component<S>) -> Self {
+ match component {
+ Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)),
+ Component::Number(number) => number,
+ Component::Side(side) => {
+ let p = if side.is_start() {
+ Percentage::zero()
+ } else {
+ Percentage::hundred()
+ };
+ NumberOrPercentage::Percentage(p)
+ },
+ }
+ }
+ }
+
+ impl<S: Side> From<Component<S>> for PositionComponent<S> {
+ fn from(component: Component<S>) -> Self {
+ match component {
+ Component::Center => PositionComponent::Center,
+ Component::Number(NumberOrPercentage::Number(number)) => {
+ PositionComponent::Length(Length::from_px(number.value).into())
+ },
+ Component::Number(NumberOrPercentage::Percentage(p)) => {
+ PositionComponent::Length(p.into())
+ },
+ Component::Side(side) => PositionComponent::Side(side, None),
+ }
+ }
+ }
+
+ impl<S: Copy + Side> Component<S> {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ match (
+ NumberOrPercentage::from(*self),
+ NumberOrPercentage::from(*other),
+ ) {
+ (NumberOrPercentage::Percentage(a), NumberOrPercentage::Percentage(b)) => {
+ a.get().partial_cmp(&b.get())
+ },
+ (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => {
+ a.value.partial_cmp(&b.value)
+ },
+ (_, _) => None,
+ }
+ }
+ }
+
+ let ident = input.expect_ident_cloned()?;
+ input.expect_comma()?;
+
+ Ok(match_ignore_ascii_case! { &ident,
+ "linear" => {
+ let first = Point::parse(context, input)?;
+ input.expect_comma()?;
+ let second = Point::parse(context, input)?;
+
+ let direction = LineDirection::from_points(first, second);
+ let items = Gradient::parse_webkit_gradient_stops(context, input, false)?;
+
+ generic::Gradient::Linear {
+ direction,
+ color_interpolation_method: ColorInterpolationMethod::srgb(),
+ items,
+ // Legacy gradients always use srgb as a default.
+ flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ compat_mode: GradientCompatMode::Modern,
+ }
+ },
+ "radial" => {
+ let first_point = Point::parse(context, input)?;
+ input.expect_comma()?;
+ let first_radius = Number::parse_non_negative(context, input)?;
+ input.expect_comma()?;
+ let second_point = Point::parse(context, input)?;
+ input.expect_comma()?;
+ let second_radius = Number::parse_non_negative(context, input)?;
+
+ let (reverse_stops, point, radius) = if second_radius.value >= first_radius.value {
+ (false, second_point, second_radius)
+ } else {
+ (true, first_point, first_radius)
+ };
+
+ let rad = Circle::Radius(NonNegative(Length::from_px(radius.value)));
+ let shape = generic::EndingShape::Circle(rad);
+ let position: Position = point.into();
+ let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?;
+
+ generic::Gradient::Radial {
+ shape,
+ position,
+ color_interpolation_method: ColorInterpolationMethod::srgb(),
+ items,
+ // Legacy gradients always use srgb as a default.
+ flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ compat_mode: GradientCompatMode::Modern,
+ }
+ },
+ _ => {
+ let e = SelectorParseErrorKind::UnexpectedIdent(ident.clone());
+ return Err(input.new_custom_error(e));
+ },
+ })
+ }
+
+ fn parse_webkit_gradient_stops<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ reverse_stops: bool,
+ ) -> Result<LengthPercentageItemList, ParseError<'i>> {
+ let mut items = input
+ .try_parse(|i| {
+ i.expect_comma()?;
+ i.parse_comma_separated(|i| {
+ let function = i.expect_function()?.clone();
+ let (color, mut p) = i.parse_nested_block(|i| {
+ let p = match_ignore_ascii_case! { &function,
+ "color-stop" => {
+ let p = NumberOrPercentage::parse(context, i)?.to_percentage();
+ i.expect_comma()?;
+ p
+ },
+ "from" => Percentage::zero(),
+ "to" => Percentage::hundred(),
+ _ => {
+ return Err(i.new_custom_error(
+ StyleParseErrorKind::UnexpectedFunction(function.clone())
+ ))
+ },
+ };
+ let color = Color::parse(context, i)?;
+ if color == Color::CurrentColor {
+ return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok((color.into(), p))
+ })?;
+ if reverse_stops {
+ p.reverse();
+ }
+ Ok(generic::GradientItem::ComplexColorStop {
+ color,
+ position: p.into(),
+ })
+ })
+ })
+ .unwrap_or(vec![]);
+
+ if items.is_empty() {
+ items = vec![
+ generic::GradientItem::ComplexColorStop {
+ color: Color::transparent(),
+ position: LengthPercentage::zero_percent(),
+ },
+ generic::GradientItem::ComplexColorStop {
+ color: Color::transparent(),
+ position: LengthPercentage::hundred_percent(),
+ },
+ ];
+ } else if items.len() == 1 {
+ let first = items[0].clone();
+ items.push(first);
+ } else {
+ items.sort_by(|a, b| {
+ match (a, b) {
+ (
+ &generic::GradientItem::ComplexColorStop {
+ position: ref a_position,
+ ..
+ },
+ &generic::GradientItem::ComplexColorStop {
+ position: ref b_position,
+ ..
+ },
+ ) => match (a_position, b_position) {
+ (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) => {
+ return a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal);
+ },
+ _ => {},
+ },
+ _ => {},
+ }
+ if reverse_stops {
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ })
+ }
+ Ok(items.into())
+ }
+
+ /// Not used for -webkit-gradient syntax and conic-gradient
+ fn parse_stops<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<LengthPercentageItemList, ParseError<'i>> {
+ let items =
+ generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?;
+ if items.len() < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(items)
+ }
+
+ /// Try to parse a color interpolation method.
+ fn try_parse_color_interpolation_method<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Option<ColorInterpolationMethod> {
+ if gradient_color_interpolation_method_enabled() {
+ input
+ .try_parse(|i| ColorInterpolationMethod::parse(context, i))
+ .ok()
+ } else {
+ None
+ }
+ }
+
+ /// Parses a linear gradient.
+ /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword.
+ fn parse_linear<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ repeating: bool,
+ mut compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut flags = GradientFlags::empty();
+ flags.set(GradientFlags::REPEATING, repeating);
+
+ let mut color_interpolation_method =
+ Self::try_parse_color_interpolation_method(context, input);
+
+ let direction = input
+ .try_parse(|p| LineDirection::parse(context, p, &mut compat_mode))
+ .ok();
+
+ if direction.is_some() && color_interpolation_method.is_none() {
+ color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
+ }
+
+ // If either of the 2 options were specified, we require a comma.
+ if color_interpolation_method.is_some() || direction.is_some() {
+ input.expect_comma()?;
+ }
+
+ let items = Gradient::parse_stops(context, input)?;
+
+ let default = default_color_interpolation_method(&items);
+ let color_interpolation_method = color_interpolation_method.unwrap_or(default);
+ flags.set(
+ GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ default == color_interpolation_method,
+ );
+
+ let direction = direction.unwrap_or(match compat_mode {
+ GradientCompatMode::Modern => LineDirection::Vertical(VerticalPositionKeyword::Bottom),
+ _ => LineDirection::Vertical(VerticalPositionKeyword::Top),
+ });
+
+ Ok(Gradient::Linear {
+ direction,
+ color_interpolation_method,
+ items,
+ flags,
+ compat_mode,
+ })
+ }
+
+ /// Parses a radial gradient.
+ fn parse_radial<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ repeating: bool,
+ compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut flags = GradientFlags::empty();
+ flags.set(GradientFlags::REPEATING, repeating);
+
+ let mut color_interpolation_method =
+ Self::try_parse_color_interpolation_method(context, input);
+
+ let (shape, position) = match compat_mode {
+ GradientCompatMode::Modern => {
+ let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode));
+ let position = input.try_parse(|i| {
+ i.expect_ident_matching("at")?;
+ Position::parse(context, i)
+ });
+ (shape, position.ok())
+ },
+ _ => {
+ let position = input.try_parse(|i| Position::parse(context, i));
+ let shape = input.try_parse(|i| {
+ if position.is_ok() {
+ i.expect_comma()?;
+ }
+ EndingShape::parse(context, i, compat_mode)
+ });
+ (shape, position.ok())
+ },
+ };
+
+ let has_shape_or_position = shape.is_ok() || position.is_some();
+ if has_shape_or_position && color_interpolation_method.is_none() {
+ color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
+ }
+
+ if has_shape_or_position || color_interpolation_method.is_some() {
+ input.expect_comma()?;
+ }
+
+ let shape = shape.unwrap_or({
+ generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner))
+ });
+
+ let position = position.unwrap_or(Position::center());
+
+ let items = Gradient::parse_stops(context, input)?;
+
+ let default = default_color_interpolation_method(&items);
+ let color_interpolation_method = color_interpolation_method.unwrap_or(default);
+ flags.set(
+ GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ default == color_interpolation_method,
+ );
+
+ Ok(Gradient::Radial {
+ shape,
+ position,
+ color_interpolation_method,
+ items,
+ flags,
+ compat_mode,
+ })
+ }
+
+ /// Parse a conic gradient.
+ fn parse_conic<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ repeating: bool,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut flags = GradientFlags::empty();
+ flags.set(GradientFlags::REPEATING, repeating);
+
+ let mut color_interpolation_method =
+ Self::try_parse_color_interpolation_method(context, input);
+
+ let angle = input.try_parse(|i| {
+ i.expect_ident_matching("from")?;
+ // Spec allows unitless zero start angles
+ // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle
+ Angle::parse_with_unitless(context, i)
+ });
+ let position = input.try_parse(|i| {
+ i.expect_ident_matching("at")?;
+ Position::parse(context, i)
+ });
+
+ let has_angle_or_position = angle.is_ok() || position.is_ok();
+ if has_angle_or_position && color_interpolation_method.is_none() {
+ color_interpolation_method = Self::try_parse_color_interpolation_method(context, input);
+ }
+
+ if has_angle_or_position || color_interpolation_method.is_some() {
+ input.expect_comma()?;
+ }
+
+ let angle = angle.unwrap_or(Angle::zero());
+
+ let position = position.unwrap_or(Position::center());
+
+ let items = generic::GradientItem::parse_comma_separated(
+ context,
+ input,
+ AngleOrPercentage::parse_with_unitless,
+ )?;
+
+ if items.len() < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ let default = default_color_interpolation_method(&items);
+ let color_interpolation_method = color_interpolation_method.unwrap_or(default);
+ flags.set(
+ GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD,
+ default == color_interpolation_method,
+ );
+
+ Ok(Gradient::Conic {
+ angle,
+ position,
+ color_interpolation_method,
+ items,
+ flags,
+ })
+ }
+}
+
+impl generic::LineDirection for LineDirection {
+ fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool {
+ match *self {
+ LineDirection::Angle(ref angle) => angle.degrees() == 180.0,
+ LineDirection::Vertical(VerticalPositionKeyword::Bottom) => {
+ compat_mode == GradientCompatMode::Modern
+ },
+ LineDirection::Vertical(VerticalPositionKeyword::Top) => {
+ compat_mode != GradientCompatMode::Modern
+ },
+ _ => false,
+ }
+ }
+
+ fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result
+ where
+ W: Write,
+ {
+ match *self {
+ LineDirection::Angle(angle) => angle.to_css(dest),
+ LineDirection::Horizontal(x) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)
+ },
+ LineDirection::Vertical(y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ y.to_css(dest)
+ },
+ LineDirection::Corner(x, y) => {
+ if compat_mode == GradientCompatMode::Modern {
+ dest.write_str("to ")?;
+ }
+ x.to_css(dest)?;
+ dest.write_char(' ')?;
+ y.to_css(dest)
+ },
+ }
+ }
+}
+
+impl LineDirection {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ compat_mode: &mut GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ // Gradients allow unitless zero angles as an exception, see:
+ // https://github.com/w3c/csswg-drafts/issues/1162
+ if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) {
+ return Ok(LineDirection::Angle(angle));
+ }
+
+ input.try_parse(|i| {
+ let to_ident = i.try_parse(|i| i.expect_ident_matching("to"));
+ match *compat_mode {
+ // `to` keyword is mandatory in modern syntax.
+ GradientCompatMode::Modern => to_ident?,
+ // Fall back to Modern compatibility mode in case there is a `to` keyword.
+ // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like
+ // `linear-gradient(to ...)`.
+ GradientCompatMode::Moz if to_ident.is_ok() => {
+ *compat_mode = GradientCompatMode::Modern
+ },
+ // There is no `to` keyword in webkit prefixed syntax. If it's consumed,
+ // parsing should throw an error.
+ GradientCompatMode::WebKit if to_ident.is_ok() => {
+ return Err(
+ i.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("to".into()))
+ );
+ },
+ _ => {},
+ }
+
+ if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
+ if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) {
+ return Ok(LineDirection::Corner(x, y));
+ }
+ return Ok(LineDirection::Horizontal(x));
+ }
+ let y = VerticalPositionKeyword::parse(i)?;
+ if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) {
+ return Ok(LineDirection::Corner(x, y));
+ }
+ Ok(LineDirection::Vertical(y))
+ })
+ }
+}
+
+impl EndingShape {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
+ {
+ if input
+ .try_parse(|i| i.expect_ident_matching("circle"))
+ .is_ok()
+ {
+ return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
+ }
+ let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
+ }
+ if input
+ .try_parse(|i| i.expect_ident_matching("circle"))
+ .is_ok()
+ {
+ if let Ok(extent) =
+ input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
+ {
+ return Ok(generic::EndingShape::Circle(Circle::Extent(extent)));
+ }
+ if compat_mode == GradientCompatMode::Modern {
+ if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
+ return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
+ }
+ }
+ return Ok(generic::EndingShape::Circle(Circle::Extent(
+ ShapeExtent::FarthestCorner,
+ )));
+ }
+ if input
+ .try_parse(|i| i.expect_ident_matching("ellipse"))
+ .is_ok()
+ {
+ if let Ok(extent) =
+ input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode))
+ {
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent)));
+ }
+ if compat_mode == GradientCompatMode::Modern {
+ let pair: Result<_, ParseError> = input.try_parse(|i| {
+ let x = NonNegativeLengthPercentage::parse(context, i)?;
+ let y = NonNegativeLengthPercentage::parse(context, i)?;
+ Ok((x, y))
+ });
+ if let Ok((x, y)) = pair {
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y)));
+ }
+ }
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(
+ ShapeExtent::FarthestCorner,
+ )));
+ }
+ if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) {
+ if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
+ if compat_mode == GradientCompatMode::Modern {
+ let _ = input.try_parse(|i| i.expect_ident_matching("ellipse"));
+ }
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
+ NonNegative(LengthPercentage::from(length.0)),
+ y,
+ )));
+ }
+ if compat_mode == GradientCompatMode::Modern {
+ let y = input.try_parse(|i| {
+ i.expect_ident_matching("ellipse")?;
+ NonNegativeLengthPercentage::parse(context, i)
+ });
+ if let Ok(y) = y {
+ return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
+ NonNegative(LengthPercentage::from(length.0)),
+ y,
+ )));
+ }
+ let _ = input.try_parse(|i| i.expect_ident_matching("circle"));
+ }
+
+ return Ok(generic::EndingShape::Circle(Circle::Radius(length)));
+ }
+ input.try_parse(|i| {
+ let x = Percentage::parse_non_negative(context, i)?;
+ let y = if let Ok(y) = i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) {
+ if compat_mode == GradientCompatMode::Modern {
+ let _ = i.try_parse(|i| i.expect_ident_matching("ellipse"));
+ }
+ y
+ } else {
+ if compat_mode == GradientCompatMode::Modern {
+ i.expect_ident_matching("ellipse")?;
+ }
+ NonNegativeLengthPercentage::parse(context, i)?
+ };
+ Ok(generic::EndingShape::Ellipse(Ellipse::Radii(
+ NonNegative(LengthPercentage::from(x)),
+ y,
+ )))
+ })
+ }
+}
+
+impl ShapeExtent {
+ fn parse_with_compat_mode<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ compat_mode: GradientCompatMode,
+ ) -> Result<Self, ParseError<'i>> {
+ match Self::parse(input)? {
+ ShapeExtent::Contain | ShapeExtent::Cover
+ if compat_mode == GradientCompatMode::Modern =>
+ {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide),
+ ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner),
+ keyword => Ok(keyword),
+ }
+ }
+}
+
+impl<T> generic::GradientItem<Color, T> {
+ fn parse_comma_separated<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse_position: impl for<'i1, 't1> Fn(&ParserContext, &mut Parser<'i1, 't1>) -> Result<T, ParseError<'i1>>
+ + Copy,
+ ) -> Result<crate::OwnedSlice<Self>, ParseError<'i>> {
+ let mut items = Vec::new();
+ let mut seen_stop = false;
+
+ loop {
+ input.parse_until_before(Delimiter::Comma, |input| {
+ if seen_stop {
+ if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) {
+ seen_stop = false;
+ items.push(generic::GradientItem::InterpolationHint(hint));
+ return Ok(());
+ }
+ }
+
+ let stop = generic::ColorStop::parse(context, input, parse_position)?;
+
+ if let Ok(multi_position) = input.try_parse(|i| parse_position(context, i)) {
+ let stop_color = stop.color.clone();
+ items.push(stop.into_item());
+ items.push(
+ generic::ColorStop {
+ color: stop_color,
+ position: Some(multi_position),
+ }
+ .into_item(),
+ );
+ } else {
+ items.push(stop.into_item());
+ }
+
+ seen_stop = true;
+ Ok(())
+ })?;
+
+ match input.next() {
+ Err(_) => break,
+ Ok(&Token::Comma) => continue,
+ Ok(_) => unreachable!(),
+ }
+ }
+
+ if !seen_stop || items.len() < 2 {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(items.into())
+ }
+}
+
+impl<T> generic::ColorStop<Color, T> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ parse_position: impl for<'i1, 't1> Fn(
+ &ParserContext,
+ &mut Parser<'i1, 't1>,
+ ) -> Result<T, ParseError<'i1>>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(generic::ColorStop {
+ color: Color::parse(context, input)?,
+ position: input.try_parse(|i| parse_position(context, i)).ok(),
+ })
+ }
+}
+
+impl PaintWorklet {
+ #[cfg(feature = "servo")]
+ fn parse_args<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> {
+ use crate::custom_properties::SpecifiedValue;
+ let name = Atom::from(&**input.expect_ident()?);
+ let arguments = input
+ .try_parse(|input| {
+ input.expect_comma()?;
+ input.parse_comma_separated(SpecifiedValue::parse)
+ })
+ .unwrap_or_default();
+ Ok(Self { name, arguments })
+ }
+}
+
+/// https://drafts.csswg.org/css-images/#propdef-image-rendering
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum ImageRendering {
+ Auto,
+ Smooth,
+ #[parse(aliases = "-moz-crisp-edges")]
+ CrispEdges,
+ Pixelated,
+ // From the spec:
+ //
+ // This property previously accepted the values optimizeSpeed and
+ // optimizeQuality. These are now deprecated; a user agent must accept
+ // them as valid values but must treat them as having the same behavior
+ // as crisp-edges and smooth respectively, and authors must not use
+ // them.
+ //
+ Optimizespeed,
+ Optimizequality,
+}
diff --git a/servo/components/style/values/specified/length.rs b/servo/components/style/values/specified/length.rs
new file mode 100644
index 0000000000..d2e1d7d346
--- /dev/null
+++ b/servo/components/style/values/specified/length.rs
@@ -0,0 +1,2031 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! [Length values][length].
+//!
+//! [length]: https://drafts.csswg.org/css-values/#lengths
+
+use super::{AllowQuirks, Number, Percentage, ToComputedValue};
+use crate::computed_value_flags::ComputedValueFlags;
+use crate::font_metrics::{FontMetrics, FontMetricsOrientation};
+use crate::gecko_bindings::structs::GeckoFontMetrics;
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{self, CSSPixelLength, Context};
+use crate::values::generics::length as generics;
+use crate::values::generics::length::{
+ GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize,
+};
+use crate::values::generics::NonNegative;
+use crate::values::specified::calc::{self, CalcNode};
+use crate::values::specified::NonNegativeNumber;
+use crate::values::CSSFloat;
+use crate::{Zero, ZeroNoPercent};
+use app_units::AU_PER_PX;
+use cssparser::{Parser, Token};
+use std::cmp;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+pub use super::image::Image;
+pub use super::image::{EndingShape as GradientEndingShape, Gradient};
+pub use crate::values::specified::calc::CalcLengthPercentage;
+
+/// Number of pixels per inch
+pub const PX_PER_IN: CSSFloat = 96.;
+/// Number of pixels per centimeter
+pub const PX_PER_CM: CSSFloat = PX_PER_IN / 2.54;
+/// Number of pixels per millimeter
+pub const PX_PER_MM: CSSFloat = PX_PER_IN / 25.4;
+/// Number of pixels per quarter
+pub const PX_PER_Q: CSSFloat = PX_PER_MM / 4.;
+/// Number of pixels per point
+pub const PX_PER_PT: CSSFloat = PX_PER_IN / 72.;
+/// Number of pixels per pica
+pub const PX_PER_PC: CSSFloat = PX_PER_PT * 12.;
+
+/// A font relative length. Note that if any new value is
+/// added here, `custom_properties::NonCustomReferences::from_unit`
+/// must also be updated. Consult the comment in that function as to why.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum FontRelativeLength {
+ /// A "em" value: https://drafts.csswg.org/css-values/#em
+ #[css(dimension)]
+ Em(CSSFloat),
+ /// A "ex" value: https://drafts.csswg.org/css-values/#ex
+ #[css(dimension)]
+ Ex(CSSFloat),
+ /// A "ch" value: https://drafts.csswg.org/css-values/#ch
+ #[css(dimension)]
+ Ch(CSSFloat),
+ /// A "cap" value: https://drafts.csswg.org/css-values/#cap
+ #[css(dimension)]
+ Cap(CSSFloat),
+ /// An "ic" value: https://drafts.csswg.org/css-values/#ic
+ #[css(dimension)]
+ Ic(CSSFloat),
+ /// A "rem" value: https://drafts.csswg.org/css-values/#rem
+ #[css(dimension)]
+ Rem(CSSFloat),
+ /// A "lh" value: https://drafts.csswg.org/css-values/#lh
+ #[css(dimension)]
+ Lh(CSSFloat),
+ /// A "rlh" value: https://drafts.csswg.org/css-values/#lh
+ #[css(dimension)]
+ Rlh(CSSFloat),
+}
+
+/// A source to resolve font-relative units against
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum FontBaseSize {
+ /// Use the font-size of the current element.
+ CurrentStyle,
+ /// Use the inherited font-size.
+ InheritedStyle,
+}
+
+/// A source to resolve font-relative line-height units against.
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum LineHeightBase {
+ /// Use the line-height of the current element.
+ CurrentStyle,
+ /// Use the inherited line-height.
+ InheritedStyle,
+}
+
+impl FontBaseSize {
+ /// Calculate the actual size for a given context
+ pub fn resolve(&self, context: &Context) -> computed::FontSize {
+ match *self {
+ Self::CurrentStyle => context.style().get_font().clone_font_size(),
+ Self::InheritedStyle => context.style().get_parent_font().clone_font_size(),
+ }
+ }
+}
+
+impl FontRelativeLength {
+ /// Unit identifier for `em`.
+ pub const EM: &'static str = "em";
+ /// Unit identifier for `ex`.
+ pub const EX: &'static str = "ex";
+ /// Unit identifier for `ch`.
+ pub const CH: &'static str = "ch";
+ /// Unit identifier for `cap`.
+ pub const CAP: &'static str = "cap";
+ /// Unit identifier for `ic`.
+ pub const IC: &'static str = "ic";
+ /// Unit identifier for `rem`.
+ pub const REM: &'static str = "rem";
+ /// Unit identifier for `lh`.
+ pub const LH: &'static str = "lh";
+ /// Unit identifier for `rlh`.
+ pub const RLH: &'static str = "rlh";
+
+ /// Return the unitless, raw value.
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Em(v) |
+ Self::Ex(v) |
+ Self::Ch(v) |
+ Self::Cap(v) |
+ Self::Ic(v) |
+ Self::Rem(v) |
+ Self::Lh(v) |
+ Self::Rlh(v) => v,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Em(_) => Self::EM,
+ Self::Ex(_) => Self::EX,
+ Self::Ch(_) => Self::CH,
+ Self::Cap(_) => Self::CAP,
+ Self::Ic(_) => Self::IC,
+ Self::Rem(_) => Self::REM,
+ Self::Lh(_) => Self::LH,
+ Self::Rlh(_) => Self::RLH,
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::FontRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Em(one), &Em(other)) => Em(op(one, other)),
+ (&Ex(one), &Ex(other)) => Ex(op(one, other)),
+ (&Ch(one), &Ch(other)) => Ch(op(one, other)),
+ (&Cap(one), &Cap(other)) => Cap(op(one, other)),
+ (&Ic(one), &Ic(other)) => Ic(op(one, other)),
+ (&Rem(one), &Rem(other)) => Rem(op(one, other)),
+ (&Lh(one), &Lh(other)) => Lh(op(one, other)),
+ (&Rlh(one), &Rlh(other)) => Rlh(op(one, other)),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ match self {
+ Self::Em(x) => Self::Em(op(*x)),
+ Self::Ex(x) => Self::Ex(op(*x)),
+ Self::Ch(x) => Self::Ch(op(*x)),
+ Self::Cap(x) => Self::Cap(op(*x)),
+ Self::Ic(x) => Self::Ic(op(*x)),
+ Self::Rem(x) => Self::Rem(op(*x)),
+ Self::Lh(x) => Self::Lh(op(*x)),
+ Self::Rlh(x) => Self::Lh(op(*x)),
+ }
+ }
+
+ /// Computes the font-relative length.
+ pub fn to_computed_value(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> computed::Length {
+ let (reference_size, length) =
+ self.reference_font_size_and_length(context, base_size, line_height_base);
+ (reference_size * length).finite()
+ }
+
+ /// Computes the length, given a GeckoFontMetrics getter to resolve font-relative units.
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: impl Fn() -> GeckoFontMetrics,
+ ) -> Result<CSSFloat, ()> {
+ let metrics = get_font_metrics();
+ Ok(match *self {
+ Self::Em(v) => v * metrics.mComputedEmSize.px(),
+ Self::Ex(v) => v * metrics.mXSize.px(),
+ Self::Ch(v) => v * metrics.mChSize.px(),
+ Self::Cap(v) => v * metrics.mCapHeight.px(),
+ Self::Ic(v) => v * metrics.mIcWidth.px(),
+ // `lh`, `rlh` & `rem` are unsupported as we have no context for it.
+ Self::Rem(_) | Self::Lh(_) | Self::Rlh(_) => return Err(()),
+ })
+ }
+
+ /// Return reference font size.
+ ///
+ /// We use the base_size flag to pass a different size for computing
+ /// font-size and unconstrained font-size.
+ ///
+ /// This returns a pair, the first one is the reference font size, and the
+ /// second one is the unpacked relative length.
+ fn reference_font_size_and_length(
+ &self,
+ context: &Context,
+ base_size: FontBaseSize,
+ line_height_base: LineHeightBase,
+ ) -> (computed::Length, CSSFloat) {
+ fn query_font_metrics(
+ context: &Context,
+ base_size: FontBaseSize,
+ orientation: FontMetricsOrientation,
+ ) -> FontMetrics {
+ let retrieve_math_scales = false;
+ context.query_font_metrics(base_size, orientation, retrieve_math_scales)
+ }
+
+ let reference_font_size = base_size.resolve(context);
+ match *self {
+ Self::Em(length) => {
+ if context.for_non_inherited_property && base_size == FontBaseSize::CurrentStyle {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_font_size_dependency(reference_font_size.computed_size);
+ }
+
+ (reference_font_size.computed_size(), length)
+ },
+ Self::Ex(length) => {
+ // The x-height is an intrinsically horizontal metric.
+ let metrics =
+ query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal);
+ let reference_size = metrics.x_height.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#ex
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the x-height, a value of 0.5em must be
+ // assumed.
+ //
+ // (But note we use 0.5em of the used, not computed
+ // font-size)
+ reference_font_size.used_size() * 0.5
+ });
+ (reference_size, length)
+ },
+ Self::Ch(length) => {
+ // https://drafts.csswg.org/css-values/#ch:
+ //
+ // Equal to the used advance measure of the “0” (ZERO,
+ // U+0030) glyph in the font used to render it. (The advance
+ // measure of a glyph is its advance width or height,
+ // whichever is in the inline axis of the element.)
+ //
+ let metrics = query_font_metrics(
+ context,
+ base_size,
+ FontMetricsOrientation::MatchContextPreferHorizontal,
+ );
+ let reference_size = metrics.zero_advance_measure.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#ch
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the measure of the “0” glyph, it must be
+ // assumed to be 0.5em wide by 1em tall. Thus, the ch
+ // unit falls back to 0.5em in the general case, and to
+ // 1em when it would be typeset upright (i.e.
+ // writing-mode is vertical-rl or vertical-lr and
+ // text-orientation is upright).
+ //
+ // Same caveat about computed vs. used font-size applies
+ // above.
+ let wm = context.style().writing_mode;
+ if wm.is_vertical() && wm.is_upright() {
+ reference_font_size.used_size()
+ } else {
+ reference_font_size.used_size() * 0.5
+ }
+ });
+ (reference_size, length)
+ },
+ Self::Cap(length) => {
+ let metrics =
+ query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal);
+ let reference_size = metrics.cap_height.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#cap
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the cap-height, the font’s ascent must be
+ // used.
+ //
+ metrics.ascent
+ });
+ (reference_size, length)
+ },
+ Self::Ic(length) => {
+ let metrics = query_font_metrics(
+ context,
+ base_size,
+ FontMetricsOrientation::MatchContextPreferVertical,
+ );
+ let reference_size = metrics.ic_width.unwrap_or_else(|| {
+ // https://drafts.csswg.org/css-values/#ic
+ //
+ // In the cases where it is impossible or impractical to
+ // determine the ideographic advance measure, it must be
+ // assumed to be 1em.
+ //
+ // Same caveat about computed vs. used as for other
+ // metric-dependent units.
+ reference_font_size.used_size()
+ });
+ (reference_size, length)
+ },
+ Self::Rem(length) => {
+ // https://drafts.csswg.org/css-values/#rem:
+ //
+ // When specified on the font-size property of the root
+ // element, the rem units refer to the property's initial
+ // value.
+ //
+ let reference_size = if context.builder.is_root_element || context.in_media_query {
+ reference_font_size.computed_size()
+ } else {
+ context.device().root_font_size()
+ };
+ (reference_size, length)
+ },
+ Self::Lh(length) => {
+ // https://drafts.csswg.org/css-values-4/#lh
+ //
+ // When specified in media-query, the lh units refer to the
+ // initial values of font and line-height properties.
+ //
+ let reference_size = if context.in_media_query {
+ context
+ .device()
+ .calc_line_height(
+ &context.default_style().get_font(),
+ context.style().writing_mode,
+ None,
+ )
+ .0
+ } else {
+ let line_height = context.builder.calc_line_height(
+ context.device(),
+ line_height_base,
+ context.style().writing_mode,
+ );
+ if context.for_non_inherited_property &&
+ line_height_base == LineHeightBase::CurrentStyle
+ {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_line_height_dependency(line_height)
+ }
+ line_height.0
+ };
+ (reference_size, length)
+ },
+ Self::Rlh(length) => {
+ // https://drafts.csswg.org/css-values-4/#rlh
+ //
+ // When specified on the root element, the rlh units refer
+ // to the initial values of font and line-height properties.
+ //
+ let reference_size: CSSPixelLength =
+ if context.builder.is_root_element || context.in_media_query {
+ context
+ .device()
+ .calc_line_height(
+ &context.default_style().get_font(),
+ context.style().writing_mode,
+ None,
+ )
+ .0
+ } else {
+ context.device().root_line_height()
+ };
+ (reference_size, length)
+ },
+ }
+ }
+}
+
+/// https://drafts.csswg.org/css-values/#viewport-variants
+pub enum ViewportVariant {
+ /// https://drafts.csswg.org/css-values/#ua-default-viewport-size
+ UADefault,
+ /// https://drafts.csswg.org/css-values/#small-viewport-percentage-units
+ Small,
+ /// https://drafts.csswg.org/css-values/#large-viewport-percentage-units
+ Large,
+ /// https://drafts.csswg.org/css-values/#dynamic-viewport-percentage-units
+ Dynamic,
+}
+
+/// https://drafts.csswg.org/css-values/#viewport-relative-units
+#[derive(PartialEq)]
+enum ViewportUnit {
+ /// *vw units.
+ Vw,
+ /// *vh units.
+ Vh,
+ /// *vmin units.
+ Vmin,
+ /// *vmax units.
+ Vmax,
+ /// *vb units.
+ Vb,
+ /// *vi units.
+ Vi,
+}
+
+/// A viewport-relative length.
+///
+/// <https://drafts.csswg.org/css-values/#viewport-relative-lengths>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum ViewportPercentageLength {
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vw>
+ #[css(dimension)]
+ Vw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svw>
+ #[css(dimension)]
+ Svw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvw>
+ #[css(dimension)]
+ Lvw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvw>
+ #[css(dimension)]
+ Dvw(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vh>
+ #[css(dimension)]
+ Vh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svh>
+ #[css(dimension)]
+ Svh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvh>
+ #[css(dimension)]
+ Lvh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvh>
+ #[css(dimension)]
+ Dvh(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vmin>
+ #[css(dimension)]
+ Vmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svmin>
+ #[css(dimension)]
+ Svmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvmin>
+ #[css(dimension)]
+ Lvmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvmin>
+ #[css(dimension)]
+ Dvmin(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vmax>
+ #[css(dimension)]
+ Vmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svmax>
+ #[css(dimension)]
+ Svmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvmax>
+ #[css(dimension)]
+ Lvmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvmax>
+ #[css(dimension)]
+ Dvmax(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vb>
+ #[css(dimension)]
+ Vb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svb>
+ #[css(dimension)]
+ Svb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvb>
+ #[css(dimension)]
+ Lvb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvb>
+ #[css(dimension)]
+ Dvb(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-vi>
+ #[css(dimension)]
+ Vi(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-svi>
+ #[css(dimension)]
+ Svi(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-lvi>
+ #[css(dimension)]
+ Lvi(CSSFloat),
+ /// <https://drafts.csswg.org/css-values/#valdef-length-dvi>
+ #[css(dimension)]
+ Dvi(CSSFloat),
+}
+
+impl ViewportPercentageLength {
+ /// Return the unitless, raw value.
+ fn unitless_value(&self) -> CSSFloat {
+ self.unpack().2
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Vw(_) => "vw",
+ Self::Lvw(_) => "lvw",
+ Self::Svw(_) => "svw",
+ Self::Dvw(_) => "dvw",
+ Self::Vh(_) => "vh",
+ Self::Svh(_) => "svh",
+ Self::Lvh(_) => "lvh",
+ Self::Dvh(_) => "dvh",
+ Self::Vmin(_) => "vmin",
+ Self::Svmin(_) => "svmin",
+ Self::Lvmin(_) => "lvmin",
+ Self::Dvmin(_) => "dvmin",
+ Self::Vmax(_) => "vmax",
+ Self::Svmax(_) => "svmax",
+ Self::Lvmax(_) => "lvmax",
+ Self::Dvmax(_) => "dvmax",
+ Self::Vb(_) => "vb",
+ Self::Svb(_) => "svb",
+ Self::Lvb(_) => "lvb",
+ Self::Dvb(_) => "dvb",
+ Self::Vi(_) => "vi",
+ Self::Svi(_) => "svi",
+ Self::Lvi(_) => "lvi",
+ Self::Dvi(_) => "dvi",
+ }
+ }
+
+ fn unpack(&self) -> (ViewportVariant, ViewportUnit, CSSFloat) {
+ match *self {
+ Self::Vw(v) => (ViewportVariant::UADefault, ViewportUnit::Vw, v),
+ Self::Svw(v) => (ViewportVariant::Small, ViewportUnit::Vw, v),
+ Self::Lvw(v) => (ViewportVariant::Large, ViewportUnit::Vw, v),
+ Self::Dvw(v) => (ViewportVariant::Dynamic, ViewportUnit::Vw, v),
+ Self::Vh(v) => (ViewportVariant::UADefault, ViewportUnit::Vh, v),
+ Self::Svh(v) => (ViewportVariant::Small, ViewportUnit::Vh, v),
+ Self::Lvh(v) => (ViewportVariant::Large, ViewportUnit::Vh, v),
+ Self::Dvh(v) => (ViewportVariant::Dynamic, ViewportUnit::Vh, v),
+ Self::Vmin(v) => (ViewportVariant::UADefault, ViewportUnit::Vmin, v),
+ Self::Svmin(v) => (ViewportVariant::Small, ViewportUnit::Vmin, v),
+ Self::Lvmin(v) => (ViewportVariant::Large, ViewportUnit::Vmin, v),
+ Self::Dvmin(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmin, v),
+ Self::Vmax(v) => (ViewportVariant::UADefault, ViewportUnit::Vmax, v),
+ Self::Svmax(v) => (ViewportVariant::Small, ViewportUnit::Vmax, v),
+ Self::Lvmax(v) => (ViewportVariant::Large, ViewportUnit::Vmax, v),
+ Self::Dvmax(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmax, v),
+ Self::Vb(v) => (ViewportVariant::UADefault, ViewportUnit::Vb, v),
+ Self::Svb(v) => (ViewportVariant::Small, ViewportUnit::Vb, v),
+ Self::Lvb(v) => (ViewportVariant::Large, ViewportUnit::Vb, v),
+ Self::Dvb(v) => (ViewportVariant::Dynamic, ViewportUnit::Vb, v),
+ Self::Vi(v) => (ViewportVariant::UADefault, ViewportUnit::Vi, v),
+ Self::Svi(v) => (ViewportVariant::Small, ViewportUnit::Vi, v),
+ Self::Lvi(v) => (ViewportVariant::Large, ViewportUnit::Vi, v),
+ Self::Dvi(v) => (ViewportVariant::Dynamic, ViewportUnit::Vi, v),
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::ViewportPercentageLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Vw(one), &Vw(other)) => Vw(op(one, other)),
+ (&Svw(one), &Svw(other)) => Svw(op(one, other)),
+ (&Lvw(one), &Lvw(other)) => Lvw(op(one, other)),
+ (&Dvw(one), &Dvw(other)) => Dvw(op(one, other)),
+ (&Vh(one), &Vh(other)) => Vh(op(one, other)),
+ (&Svh(one), &Svh(other)) => Svh(op(one, other)),
+ (&Lvh(one), &Lvh(other)) => Lvh(op(one, other)),
+ (&Dvh(one), &Dvh(other)) => Dvh(op(one, other)),
+ (&Vmin(one), &Vmin(other)) => Vmin(op(one, other)),
+ (&Svmin(one), &Svmin(other)) => Svmin(op(one, other)),
+ (&Lvmin(one), &Lvmin(other)) => Lvmin(op(one, other)),
+ (&Dvmin(one), &Dvmin(other)) => Dvmin(op(one, other)),
+ (&Vmax(one), &Vmax(other)) => Vmax(op(one, other)),
+ (&Svmax(one), &Svmax(other)) => Svmax(op(one, other)),
+ (&Lvmax(one), &Lvmax(other)) => Lvmax(op(one, other)),
+ (&Dvmax(one), &Dvmax(other)) => Dvmax(op(one, other)),
+ (&Vb(one), &Vb(other)) => Vb(op(one, other)),
+ (&Svb(one), &Svb(other)) => Svb(op(one, other)),
+ (&Lvb(one), &Lvb(other)) => Lvb(op(one, other)),
+ (&Dvb(one), &Dvb(other)) => Dvb(op(one, other)),
+ (&Vi(one), &Vi(other)) => Vi(op(one, other)),
+ (&Svi(one), &Svi(other)) => Svi(op(one, other)),
+ (&Lvi(one), &Lvi(other)) => Lvi(op(one, other)),
+ (&Dvi(one), &Dvi(other)) => Dvi(op(one, other)),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) |
+ Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) |
+ Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) |
+ Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ match self {
+ Self::Vw(x) => Self::Vw(op(*x)),
+ Self::Svw(x) => Self::Svw(op(*x)),
+ Self::Lvw(x) => Self::Lvw(op(*x)),
+ Self::Dvw(x) => Self::Dvw(op(*x)),
+ Self::Vh(x) => Self::Vh(op(*x)),
+ Self::Svh(x) => Self::Svh(op(*x)),
+ Self::Lvh(x) => Self::Lvh(op(*x)),
+ Self::Dvh(x) => Self::Dvh(op(*x)),
+ Self::Vmin(x) => Self::Vmin(op(*x)),
+ Self::Svmin(x) => Self::Svmin(op(*x)),
+ Self::Lvmin(x) => Self::Lvmin(op(*x)),
+ Self::Dvmin(x) => Self::Dvmin(op(*x)),
+ Self::Vmax(x) => Self::Vmax(op(*x)),
+ Self::Svmax(x) => Self::Svmax(op(*x)),
+ Self::Lvmax(x) => Self::Lvmax(op(*x)),
+ Self::Dvmax(x) => Self::Dvmax(op(*x)),
+ Self::Vb(x) => Self::Vb(op(*x)),
+ Self::Svb(x) => Self::Svb(op(*x)),
+ Self::Lvb(x) => Self::Lvb(op(*x)),
+ Self::Dvb(x) => Self::Dvb(op(*x)),
+ Self::Vi(x) => Self::Vi(op(*x)),
+ Self::Svi(x) => Self::Svi(op(*x)),
+ Self::Lvi(x) => Self::Lvi(op(*x)),
+ Self::Dvi(x) => Self::Dvi(op(*x)),
+ }
+ }
+
+ /// Computes the given viewport-relative length for the given viewport size.
+ pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength {
+ let (variant, unit, factor) = self.unpack();
+ let size = context.viewport_size_for_viewport_unit_resolution(variant);
+ let length = match unit {
+ ViewportUnit::Vw => size.width,
+ ViewportUnit::Vh => size.height,
+ ViewportUnit::Vmin => cmp::min(size.width, size.height),
+ ViewportUnit::Vmax => cmp::max(size.width, size.height),
+ ViewportUnit::Vi | ViewportUnit::Vb => {
+ context
+ .rule_cache_conditions
+ .borrow_mut()
+ .set_writing_mode_dependency(context.builder.writing_mode);
+ if (unit == ViewportUnit::Vb) == context.style().writing_mode.is_vertical() {
+ size.width
+ } else {
+ size.height
+ }
+ },
+ };
+
+ // FIXME: Bug 1396535, we need to fix the extremely small viewport length for transform.
+ // See bug 989802. We truncate so that adding multiple viewport units
+ // that add up to 100 does not overflow due to rounding differences.
+ // We convert appUnits to CSS px manually here to avoid premature clamping by
+ // going through the Au type.
+ let trunc_scaled =
+ ((length.0 as f64 * factor as f64 / 100.).trunc() / AU_PER_PX as f64) as f32;
+ CSSPixelLength::new(crate::values::normalize(trunc_scaled))
+ }
+}
+
+/// HTML5 "character width", as defined in HTML5 § 14.5.4.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub struct CharacterWidth(pub i32);
+
+impl CharacterWidth {
+ /// Computes the given character width.
+ pub fn to_computed_value(&self, reference_font_size: computed::Length) -> computed::Length {
+ // This applies the *converting a character width to pixels* algorithm
+ // as specified in HTML5 § 14.5.4.
+ //
+ // TODO(pcwalton): Find these from the font.
+ let average_advance = reference_font_size * 0.5;
+ let max_advance = reference_font_size;
+ (average_advance * (self.0 as CSSFloat - 1.0) + max_advance).finite()
+ }
+}
+
+/// Represents an absolute length with its unit
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum AbsoluteLength {
+ /// An absolute length in pixels (px)
+ #[css(dimension)]
+ Px(CSSFloat),
+ /// An absolute length in inches (in)
+ #[css(dimension)]
+ In(CSSFloat),
+ /// An absolute length in centimeters (cm)
+ #[css(dimension)]
+ Cm(CSSFloat),
+ /// An absolute length in millimeters (mm)
+ #[css(dimension)]
+ Mm(CSSFloat),
+ /// An absolute length in quarter-millimeters (q)
+ #[css(dimension)]
+ Q(CSSFloat),
+ /// An absolute length in points (pt)
+ #[css(dimension)]
+ Pt(CSSFloat),
+ /// An absolute length in pica (pc)
+ #[css(dimension)]
+ Pc(CSSFloat),
+}
+
+impl AbsoluteLength {
+ /// Return the unitless, raw value.
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Px(v) |
+ Self::In(v) |
+ Self::Cm(v) |
+ Self::Mm(v) |
+ Self::Q(v) |
+ Self::Pt(v) |
+ Self::Pc(v) => v,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Px(_) => "px",
+ Self::In(_) => "in",
+ Self::Cm(_) => "cm",
+ Self::Mm(_) => "mm",
+ Self::Q(_) => "q",
+ Self::Pt(_) => "pt",
+ Self::Pc(_) => "pc",
+ }
+ }
+
+ /// Convert this into a pixel value.
+ #[inline]
+ pub fn to_px(&self) -> CSSFloat {
+ match *self {
+ Self::Px(value) => value,
+ Self::In(value) => value * PX_PER_IN,
+ Self::Cm(value) => value * PX_PER_CM,
+ Self::Mm(value) => value * PX_PER_MM,
+ Self::Q(value) => value * PX_PER_Q,
+ Self::Pt(value) => value * PX_PER_PT,
+ Self::Pc(value) => value * PX_PER_PC,
+ }
+ }
+
+ fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ Ok(Self::Px(op(self.to_px(), other.to_px())))
+ }
+
+ fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ Self::Px(op(self.to_px()))
+ }
+}
+
+impl ToComputedValue for AbsoluteLength {
+ type ComputedValue = CSSPixelLength;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ CSSPixelLength::new(context.builder.effective_zoom().zoom(self.to_px())).finite()
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Self::Px(computed.px())
+ }
+}
+
+impl PartialOrd for AbsoluteLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ self.to_px().partial_cmp(&other.to_px())
+ }
+}
+
+/// A container query length.
+///
+/// <https://drafts.csswg.org/css-contain-3/#container-lengths>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)]
+pub enum ContainerRelativeLength {
+ /// 1% of query container's width
+ #[css(dimension)]
+ Cqw(CSSFloat),
+ /// 1% of query container's height
+ #[css(dimension)]
+ Cqh(CSSFloat),
+ /// 1% of query container's inline size
+ #[css(dimension)]
+ Cqi(CSSFloat),
+ /// 1% of query container's block size
+ #[css(dimension)]
+ Cqb(CSSFloat),
+ /// The smaller value of `cqi` or `cqb`
+ #[css(dimension)]
+ Cqmin(CSSFloat),
+ /// The larger value of `cqi` or `cqb`
+ #[css(dimension)]
+ Cqmax(CSSFloat),
+}
+
+impl ContainerRelativeLength {
+ fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Cqw(v) |
+ Self::Cqh(v) |
+ Self::Cqi(v) |
+ Self::Cqb(v) |
+ Self::Cqmin(v) |
+ Self::Cqmax(v) => v,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Cqw(_) => "cqw",
+ Self::Cqh(_) => "cqh",
+ Self::Cqi(_) => "cqi",
+ Self::Cqb(_) => "cqb",
+ Self::Cqmin(_) => "cqmin",
+ Self::Cqmax(_) => "cqmax",
+ }
+ }
+
+ pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::ContainerRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Cqw(one), &Cqw(other)) => Cqw(op(one, other)),
+ (&Cqh(one), &Cqh(other)) => Cqh(op(one, other)),
+ (&Cqi(one), &Cqi(other)) => Cqi(op(one, other)),
+ (&Cqb(one), &Cqb(other)) => Cqb(op(one, other)),
+ (&Cqmin(one), &Cqmin(other)) => Cqmin(op(one, other)),
+ (&Cqmax(one), &Cqmax(other)) => Cqmax(op(one, other)),
+
+ // See https://github.com/rust-lang/rust/issues/68867, then
+ // https://github.com/rust-lang/rust/pull/95161. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ match self {
+ Self::Cqw(x) => Self::Cqw(op(*x)),
+ Self::Cqh(x) => Self::Cqh(op(*x)),
+ Self::Cqi(x) => Self::Cqi(op(*x)),
+ Self::Cqb(x) => Self::Cqb(op(*x)),
+ Self::Cqmin(x) => Self::Cqmin(op(*x)),
+ Self::Cqmax(x) => Self::Cqmax(op(*x)),
+ }
+ }
+
+ /// Computes the given container-relative length.
+ pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength {
+ if context.for_non_inherited_property {
+ context.rule_cache_conditions.borrow_mut().set_uncacheable();
+ }
+ context
+ .builder
+ .add_flags(ComputedValueFlags::USES_CONTAINER_UNITS);
+
+ let size = context.get_container_size_query();
+ let (factor, container_length) = match *self {
+ Self::Cqw(v) => (v, size.get_container_width(context)),
+ Self::Cqh(v) => (v, size.get_container_height(context)),
+ Self::Cqi(v) => (v, size.get_container_inline_size(context)),
+ Self::Cqb(v) => (v, size.get_container_block_size(context)),
+ Self::Cqmin(v) => (
+ v,
+ cmp::min(
+ size.get_container_inline_size(context),
+ size.get_container_block_size(context),
+ ),
+ ),
+ Self::Cqmax(v) => (
+ v,
+ cmp::max(
+ size.get_container_inline_size(context),
+ size.get_container_block_size(context),
+ ),
+ ),
+ };
+ CSSPixelLength::new((container_length.to_f64_px() * factor as f64 / 100.0) as f32).finite()
+ }
+}
+
+#[cfg(feature = "gecko")]
+fn are_container_queries_enabled() -> bool {
+ static_prefs::pref!("layout.css.container-queries.enabled")
+}
+#[cfg(feature = "servo")]
+fn are_container_queries_enabled() -> bool {
+ false
+}
+
+/// A `<length>` without taking `calc` expressions into account
+///
+/// <https://drafts.csswg.org/css-values/#lengths>
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub enum NoCalcLength {
+ /// An absolute length
+ ///
+ /// <https://drafts.csswg.org/css-values/#absolute-length>
+ Absolute(AbsoluteLength),
+
+ /// A font-relative length:
+ ///
+ /// <https://drafts.csswg.org/css-values/#font-relative-lengths>
+ FontRelative(FontRelativeLength),
+
+ /// A viewport-relative length.
+ ///
+ /// <https://drafts.csswg.org/css-values/#viewport-relative-lengths>
+ ViewportPercentage(ViewportPercentageLength),
+
+ /// A container query length.
+ ///
+ /// <https://drafts.csswg.org/css-contain-3/#container-lengths>
+ ContainerRelative(ContainerRelativeLength),
+ /// HTML5 "character width", as defined in HTML5 § 14.5.4.
+ ///
+ /// This cannot be specified by the user directly and is only generated by
+ /// `Stylist::synthesize_rules_for_legacy_attributes()`.
+ ServoCharacterWidth(CharacterWidth),
+}
+
+impl NoCalcLength {
+ /// Return the unitless, raw value.
+ pub fn unitless_value(&self) -> CSSFloat {
+ match *self {
+ Self::Absolute(v) => v.unitless_value(),
+ Self::FontRelative(v) => v.unitless_value(),
+ Self::ViewportPercentage(v) => v.unitless_value(),
+ Self::ContainerRelative(v) => v.unitless_value(),
+ Self::ServoCharacterWidth(c) => c.0 as f32,
+ }
+ }
+
+ // Return the unit, as a string.
+ fn unit(&self) -> &'static str {
+ match *self {
+ Self::Absolute(v) => v.unit(),
+ Self::FontRelative(v) => v.unit(),
+ Self::ViewportPercentage(v) => v.unit(),
+ Self::ContainerRelative(v) => v.unit(),
+ Self::ServoCharacterWidth(_) => "",
+ }
+ }
+
+ /// Returns whether the value of this length without unit is less than zero.
+ pub fn is_negative(&self) -> bool {
+ self.unitless_value().is_sign_negative()
+ }
+
+ /// Returns whether the value of this length without unit is equal to zero.
+ pub fn is_zero(&self) -> bool {
+ self.unitless_value() == 0.0
+ }
+
+ /// Returns whether the value of this length without unit is infinite.
+ pub fn is_infinite(&self) -> bool {
+ self.unitless_value().is_infinite()
+ }
+
+ /// Returns whether the value of this length without unit is NaN.
+ pub fn is_nan(&self) -> bool {
+ self.unitless_value().is_nan()
+ }
+
+ /// Whether text-only zoom should be applied to this length.
+ ///
+ /// Generally, font-dependent/relative units don't get text-only-zoomed,
+ /// because the font they're relative to should be zoomed already.
+ pub fn should_zoom_text(&self) -> bool {
+ match *self {
+ Self::Absolute(..) | Self::ViewportPercentage(..) | Self::ContainerRelative(..) => true,
+ Self::ServoCharacterWidth(..) | Self::FontRelative(..) => false,
+ }
+ }
+
+ /// Parse a given absolute or relative dimension.
+ pub fn parse_dimension(
+ context: &ParserContext,
+ value: CSSFloat,
+ unit: &str,
+ ) -> Result<Self, ()> {
+ Ok(match_ignore_ascii_case! { unit,
+ "px" => Self::Absolute(AbsoluteLength::Px(value)),
+ "in" => Self::Absolute(AbsoluteLength::In(value)),
+ "cm" => Self::Absolute(AbsoluteLength::Cm(value)),
+ "mm" => Self::Absolute(AbsoluteLength::Mm(value)),
+ "q" => Self::Absolute(AbsoluteLength::Q(value)),
+ "pt" => Self::Absolute(AbsoluteLength::Pt(value)),
+ "pc" => Self::Absolute(AbsoluteLength::Pc(value)),
+ // font-relative
+ "em" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Em(value)),
+ "ex" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ex(value)),
+ "ch" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ch(value)),
+ "cap" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Cap(value)),
+ "ic" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ic(value)),
+ "rem" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rem(value)),
+ "lh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Lh(value)),
+ "rlh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rlh(value)),
+ // viewport percentages
+ "vw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vw(value))
+ },
+ "svw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svw(value))
+ },
+ "lvw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvw(value))
+ },
+ "dvw" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvw(value))
+ },
+ "vh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vh(value))
+ },
+ "svh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svh(value))
+ },
+ "lvh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvh(value))
+ },
+ "dvh" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvh(value))
+ },
+ "vmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vmin(value))
+ },
+ "svmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svmin(value))
+ },
+ "lvmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvmin(value))
+ },
+ "dvmin" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvmin(value))
+ },
+ "vmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vmax(value))
+ },
+ "svmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svmax(value))
+ },
+ "lvmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvmax(value))
+ },
+ "dvmax" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvmax(value))
+ },
+ "vb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vb(value))
+ },
+ "svb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svb(value))
+ },
+ "lvb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvb(value))
+ },
+ "dvb" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvb(value))
+ },
+ "vi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Vi(value))
+ },
+ "svi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Svi(value))
+ },
+ "lvi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Lvi(value))
+ },
+ "dvi" if !context.in_page_rule() => {
+ Self::ViewportPercentage(ViewportPercentageLength::Dvi(value))
+ },
+ // Container query lengths. Inherit the limitation from viewport units since
+ // we may fall back to them.
+ "cqw" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqw(value))
+ },
+ "cqh" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqh(value))
+ },
+ "cqi" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqi(value))
+ },
+ "cqb" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqb(value))
+ },
+ "cqmin" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqmin(value))
+ },
+ "cqmax" if !context.in_page_rule() && are_container_queries_enabled() => {
+ Self::ContainerRelative(ContainerRelativeLength::Cqmax(value))
+ },
+ _ => return Err(()),
+ })
+ }
+
+ pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()>
+ where
+ O: Fn(f32, f32) -> f32,
+ {
+ use self::NoCalcLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return Err(());
+ }
+
+ Ok(match (self, other) {
+ (&Absolute(ref one), &Absolute(ref other)) => Absolute(one.try_op(other, op)?),
+ (&FontRelative(ref one), &FontRelative(ref other)) => {
+ FontRelative(one.try_op(other, op)?)
+ },
+ (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => {
+ ViewportPercentage(one.try_op(other, op)?)
+ },
+ (&ContainerRelative(ref one), &ContainerRelative(ref other)) => {
+ ContainerRelative(one.try_op(other, op)?)
+ },
+ (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => {
+ ServoCharacterWidth(CharacterWidth(op(one.0 as f32, other.0 as f32) as i32))
+ },
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Absolute(..) |
+ FontRelative(..) |
+ ViewportPercentage(..) |
+ ContainerRelative(..) |
+ ServoCharacterWidth(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in try_op()")
+ },
+ })
+ }
+
+ pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self {
+ use self::NoCalcLength::*;
+
+ match self {
+ Absolute(ref one) => Absolute(one.map(op)),
+ FontRelative(ref one) => FontRelative(one.map(op)),
+ ViewportPercentage(ref one) => ViewportPercentage(one.map(op)),
+ ContainerRelative(ref one) => ContainerRelative(one.map(op)),
+ ServoCharacterWidth(ref one) => {
+ ServoCharacterWidth(CharacterWidth(op(one.0 as f32) as i32))
+ },
+ }
+ }
+
+ /// Get a px value without context (so only absolute units can be handled).
+ #[inline]
+ pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()),
+ _ => Err(()),
+ }
+ }
+
+ /// Get a px value without a full style context; this can handle either
+ /// absolute or (if a font metrics getter is provided) font-relative units.
+ #[inline]
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>,
+ ) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()),
+ Self::FontRelative(fr) => {
+ if let Some(getter) = get_font_metrics {
+ fr.to_computed_pixel_length_with_font_metrics(getter)
+ } else {
+ Err(())
+ }
+ },
+ _ => Err(()),
+ }
+ }
+
+ /// Get an absolute length from a px value.
+ #[inline]
+ pub fn from_px(px_value: CSSFloat) -> NoCalcLength {
+ NoCalcLength::Absolute(AbsoluteLength::Px(px_value))
+ }
+}
+
+impl ToCss for NoCalcLength {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.unitless_value(),
+ self.unit(),
+ false,
+ dest,
+ )
+ }
+}
+
+impl SpecifiedValueInfo for NoCalcLength {}
+
+impl PartialOrd for NoCalcLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::NoCalcLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Absolute(ref one), &Absolute(ref other)) => one.to_px().partial_cmp(&other.to_px()),
+ (&FontRelative(ref one), &FontRelative(ref other)) => one.partial_cmp(other),
+ (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => {
+ one.partial_cmp(other)
+ },
+ (&ContainerRelative(ref one), &ContainerRelative(ref other)) => one.partial_cmp(other),
+ (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => {
+ one.0.partial_cmp(&other.0)
+ },
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Absolute(..) |
+ FontRelative(..) |
+ ViewportPercentage(..) |
+ ContainerRelative(..) |
+ ServoCharacterWidth(..) => {},
+ }
+ debug_unreachable!("Forgot an arm in partial_cmp?")
+ },
+ }
+ }
+}
+
+impl Zero for NoCalcLength {
+ fn zero() -> Self {
+ NoCalcLength::Absolute(AbsoluteLength::Px(0.))
+ }
+
+ fn is_zero(&self) -> bool {
+ NoCalcLength::is_zero(self)
+ }
+}
+
+/// An extension to `NoCalcLength` to parse `calc` expressions.
+/// This is commonly used for the `<length>` values.
+///
+/// <https://drafts.csswg.org/css-values/#lengths>
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum Length {
+ /// The internal length type that cannot parse `calc`
+ NoCalc(NoCalcLength),
+ /// A calc expression.
+ ///
+ /// <https://drafts.csswg.org/css-values/#calc-notation>
+ Calc(Box<CalcLengthPercentage>),
+}
+
+impl From<NoCalcLength> for Length {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ Length::NoCalc(len)
+ }
+}
+
+impl PartialOrd for FontRelativeLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::FontRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Em(ref one), &Em(ref other)) => one.partial_cmp(other),
+ (&Ex(ref one), &Ex(ref other)) => one.partial_cmp(other),
+ (&Ch(ref one), &Ch(ref other)) => one.partial_cmp(other),
+ (&Cap(ref one), &Cap(ref other)) => one.partial_cmp(other),
+ (&Ic(ref one), &Ic(ref other)) => one.partial_cmp(other),
+ (&Rem(ref one), &Rem(ref other)) => one.partial_cmp(other),
+ (&Lh(ref one), &Lh(ref other)) => one.partial_cmp(other),
+ (&Rlh(ref one), &Rlh(ref other)) => one.partial_cmp(other),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {},
+ }
+ debug_unreachable!("Forgot an arm in partial_cmp?")
+ },
+ }
+ }
+}
+
+impl PartialOrd for ContainerRelativeLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::ContainerRelativeLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Cqw(ref one), &Cqw(ref other)) => one.partial_cmp(other),
+ (&Cqh(ref one), &Cqh(ref other)) => one.partial_cmp(other),
+ (&Cqi(ref one), &Cqi(ref other)) => one.partial_cmp(other),
+ (&Cqb(ref one), &Cqb(ref other)) => one.partial_cmp(other),
+ (&Cqmin(ref one), &Cqmin(ref other)) => one.partial_cmp(other),
+ (&Cqmax(ref one), &Cqmax(ref other)) => one.partial_cmp(other),
+
+ // See https://github.com/rust-lang/rust/issues/68867, then
+ // https://github.com/rust-lang/rust/pull/95161. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {},
+ }
+ debug_unreachable!("Forgot to handle unit in partial_cmp()")
+ },
+ }
+ }
+}
+
+impl PartialOrd for ViewportPercentageLength {
+ fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+ use self::ViewportPercentageLength::*;
+
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return None;
+ }
+
+ match (self, other) {
+ (&Vw(ref one), &Vw(ref other)) => one.partial_cmp(other),
+ (&Svw(ref one), &Svw(ref other)) => one.partial_cmp(other),
+ (&Lvw(ref one), &Lvw(ref other)) => one.partial_cmp(other),
+ (&Dvw(ref one), &Dvw(ref other)) => one.partial_cmp(other),
+ (&Vh(ref one), &Vh(ref other)) => one.partial_cmp(other),
+ (&Svh(ref one), &Svh(ref other)) => one.partial_cmp(other),
+ (&Lvh(ref one), &Lvh(ref other)) => one.partial_cmp(other),
+ (&Dvh(ref one), &Dvh(ref other)) => one.partial_cmp(other),
+ (&Vmin(ref one), &Vmin(ref other)) => one.partial_cmp(other),
+ (&Svmin(ref one), &Svmin(ref other)) => one.partial_cmp(other),
+ (&Lvmin(ref one), &Lvmin(ref other)) => one.partial_cmp(other),
+ (&Dvmin(ref one), &Dvmin(ref other)) => one.partial_cmp(other),
+ (&Vmax(ref one), &Vmax(ref other)) => one.partial_cmp(other),
+ (&Svmax(ref one), &Svmax(ref other)) => one.partial_cmp(other),
+ (&Lvmax(ref one), &Lvmax(ref other)) => one.partial_cmp(other),
+ (&Dvmax(ref one), &Dvmax(ref other)) => one.partial_cmp(other),
+ (&Vb(ref one), &Vb(ref other)) => one.partial_cmp(other),
+ (&Svb(ref one), &Svb(ref other)) => one.partial_cmp(other),
+ (&Lvb(ref one), &Lvb(ref other)) => one.partial_cmp(other),
+ (&Dvb(ref one), &Dvb(ref other)) => one.partial_cmp(other),
+ (&Vi(ref one), &Vi(ref other)) => one.partial_cmp(other),
+ (&Svi(ref one), &Svi(ref other)) => one.partial_cmp(other),
+ (&Lvi(ref one), &Lvi(ref other)) => one.partial_cmp(other),
+ (&Dvi(ref one), &Dvi(ref other)) => one.partial_cmp(other),
+ // See https://github.com/rust-lang/rust/issues/68867. rustc isn't
+ // able to figure it own on its own so we help.
+ _ => unsafe {
+ match *self {
+ Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) |
+ Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) |
+ Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) |
+ Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {},
+ }
+ debug_unreachable!("Forgot an arm in partial_cmp?")
+ },
+ }
+ }
+}
+
+impl Length {
+ #[inline]
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ num_context: AllowedNumericType,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let token = input.next()?;
+ match *token {
+ Token::Dimension {
+ value, ref unit, ..
+ } if num_context.is_ok(context.parsing_mode, value) => {
+ NoCalcLength::parse_dimension(context, value, unit)
+ .map(Length::NoCalc)
+ .map_err(|()| location.new_unexpected_token_error(token.clone()))
+ },
+ Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => {
+ if value != 0. &&
+ !context.parsing_mode.allows_unitless_lengths() &&
+ !allow_quirks.allowed(context.quirks_mode)
+ {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(Length::NoCalc(NoCalcLength::Absolute(AbsoluteLength::Px(
+ value,
+ ))))
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let calc = CalcNode::parse_length(context, input, num_context, function)?;
+ Ok(Length::Calc(Box::new(calc)))
+ },
+ ref token => return Err(location.new_unexpected_token_error(token.clone())),
+ }
+ }
+
+ /// Parse a non-negative length
+ #[inline]
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_non_negative_quirky(context, input, AllowQuirks::No)
+ }
+
+ /// Parse a non-negative length, allowing quirks.
+ #[inline]
+ pub fn parse_non_negative_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(
+ context,
+ input,
+ AllowedNumericType::NonNegative,
+ allow_quirks,
+ )
+ }
+
+ /// Get an absolute length from a px value.
+ #[inline]
+ pub fn from_px(px_value: CSSFloat) -> Length {
+ Length::NoCalc(NoCalcLength::from_px(px_value))
+ }
+
+ /// Get a px value without context.
+ pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::NoCalc(ref l) => l.to_computed_pixel_length_without_context(),
+ Self::Calc(ref l) => l.to_computed_pixel_length_without_context(),
+ }
+ }
+
+ /// Get a px value, with an optional GeckoFontMetrics getter to resolve font-relative units.
+ pub fn to_computed_pixel_length_with_font_metrics(
+ &self,
+ get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>,
+ ) -> Result<CSSFloat, ()> {
+ match *self {
+ Self::NoCalc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics),
+ Self::Calc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics),
+ }
+ }
+}
+
+impl Parse for Length {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl Zero for Length {
+ fn zero() -> Self {
+ Length::NoCalc(NoCalcLength::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ // FIXME(emilio): Seems a bit weird to treat calc() unconditionally as
+ // non-zero here?
+ match *self {
+ Length::NoCalc(ref l) => l.is_zero(),
+ Length::Calc(..) => false,
+ }
+ }
+}
+
+impl Length {
+ /// Parses a length, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks)
+ }
+}
+
+/// A wrapper of Length, whose value must be >= 0.
+pub type NonNegativeLength = NonNegative<Length>;
+
+impl Parse for NonNegativeLength {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Length::parse_non_negative(context, input)?))
+ }
+}
+
+impl From<NoCalcLength> for NonNegativeLength {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ NonNegative(Length::NoCalc(len))
+ }
+}
+
+impl From<Length> for NonNegativeLength {
+ #[inline]
+ fn from(len: Length) -> Self {
+ NonNegative(len)
+ }
+}
+
+impl NonNegativeLength {
+ /// Get an absolute length from a px value.
+ #[inline]
+ pub fn from_px(px_value: CSSFloat) -> Self {
+ Length::from_px(px_value.max(0.)).into()
+ }
+
+ /// Parses a non-negative length, optionally with quirks.
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Length::parse_non_negative_quirky(
+ context,
+ input,
+ allow_quirks,
+ )?))
+ }
+}
+
+/// A `<length-percentage>` value. This can be either a `<length>`, a
+/// `<percentage>`, or a combination of both via `calc()`.
+///
+/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage
+#[allow(missing_docs)]
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum LengthPercentage {
+ Length(NoCalcLength),
+ Percentage(computed::Percentage),
+ Calc(Box<CalcLengthPercentage>),
+}
+
+impl From<Length> for LengthPercentage {
+ fn from(len: Length) -> LengthPercentage {
+ match len {
+ Length::NoCalc(l) => LengthPercentage::Length(l),
+ Length::Calc(l) => LengthPercentage::Calc(l),
+ }
+ }
+}
+
+impl From<NoCalcLength> for LengthPercentage {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ LengthPercentage::Length(len)
+ }
+}
+
+impl From<Percentage> for LengthPercentage {
+ #[inline]
+ fn from(pc: Percentage) -> Self {
+ if let Some(clamping_mode) = pc.calc_clamping_mode() {
+ LengthPercentage::Calc(Box::new(CalcLengthPercentage {
+ clamping_mode,
+ node: CalcNode::Leaf(calc::Leaf::Percentage(pc.get())),
+ }))
+ } else {
+ LengthPercentage::Percentage(computed::Percentage(pc.get()))
+ }
+ }
+}
+
+impl From<computed::Percentage> for LengthPercentage {
+ #[inline]
+ fn from(pc: computed::Percentage) -> Self {
+ LengthPercentage::Percentage(pc)
+ }
+}
+
+impl Parse for LengthPercentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl LengthPercentage {
+ #[inline]
+ /// Returns a `0%` value.
+ pub fn zero_percent() -> LengthPercentage {
+ LengthPercentage::Percentage(computed::Percentage::zero())
+ }
+
+ #[inline]
+ /// Returns a `100%` value.
+ pub fn hundred_percent() -> LengthPercentage {
+ LengthPercentage::Percentage(computed::Percentage::hundred())
+ }
+
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ num_context: AllowedNumericType,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let token = input.next()?;
+ match *token {
+ Token::Dimension {
+ value, ref unit, ..
+ } if num_context.is_ok(context.parsing_mode, value) => {
+ return NoCalcLength::parse_dimension(context, value, unit)
+ .map(LengthPercentage::Length)
+ .map_err(|()| location.new_unexpected_token_error(token.clone()));
+ },
+ Token::Percentage { unit_value, .. }
+ if num_context.is_ok(context.parsing_mode, unit_value) =>
+ {
+ return Ok(LengthPercentage::Percentage(computed::Percentage(
+ unit_value,
+ )));
+ },
+ Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => {
+ if value != 0. &&
+ !context.parsing_mode.allows_unitless_lengths() &&
+ !allow_quirks.allowed(context.quirks_mode)
+ {
+ return Err(location.new_unexpected_token_error(token.clone()));
+ } else {
+ return Ok(LengthPercentage::Length(NoCalcLength::from_px(value)));
+ }
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let calc =
+ CalcNode::parse_length_or_percentage(context, input, num_context, function)?;
+ Ok(LengthPercentage::Calc(Box::new(calc)))
+ },
+ _ => return Err(location.new_unexpected_token_error(token.clone())),
+ }
+ }
+
+ /// Parses allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks)
+ }
+
+ /// Parse a non-negative length.
+ ///
+ /// FIXME(emilio): This should be not public and we should use
+ /// NonNegativeLengthPercentage instead.
+ #[inline]
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_non_negative_quirky(context, input, AllowQuirks::No)
+ }
+
+ /// Parse a non-negative length, with quirks.
+ #[inline]
+ pub fn parse_non_negative_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(
+ context,
+ input,
+ AllowedNumericType::NonNegative,
+ allow_quirks,
+ )
+ }
+
+ /// Returns self as specified::calc::CalcNode.
+ /// Note that this expect the clamping_mode is AllowedNumericType::All for Calc. The caller
+ /// should take care about it when using this function.
+ fn to_calc_node(self) -> CalcNode {
+ match self {
+ LengthPercentage::Length(l) => CalcNode::Leaf(calc::Leaf::Length(l)),
+ LengthPercentage::Percentage(p) => CalcNode::Leaf(calc::Leaf::Percentage(p.0)),
+ LengthPercentage::Calc(p) => p.node,
+ }
+ }
+
+ /// Construct the value representing `calc(100% - self)`.
+ pub fn hundred_percent_minus(self, clamping_mode: AllowedNumericType) -> Self {
+ let mut sum = smallvec::SmallVec::<[CalcNode; 2]>::new();
+ sum.push(CalcNode::Leaf(calc::Leaf::Percentage(1.0)));
+
+ let mut node = self.to_calc_node();
+ node.negate();
+ sum.push(node);
+
+ let calc = CalcNode::Sum(sum.into_boxed_slice().into());
+ LengthPercentage::Calc(Box::new(
+ calc.into_length_or_percentage(clamping_mode).unwrap(),
+ ))
+ }
+}
+
+impl Zero for LengthPercentage {
+ fn zero() -> Self {
+ LengthPercentage::Length(NoCalcLength::zero())
+ }
+
+ fn is_zero(&self) -> bool {
+ match *self {
+ LengthPercentage::Length(l) => l.is_zero(),
+ LengthPercentage::Percentage(p) => p.0 == 0.0,
+ LengthPercentage::Calc(_) => false,
+ }
+ }
+}
+
+impl ZeroNoPercent for LengthPercentage {
+ fn is_zero_no_percent(&self) -> bool {
+ match *self {
+ LengthPercentage::Percentage(_) => false,
+ _ => self.is_zero(),
+ }
+ }
+}
+
+/// A specified type for `<length-percentage> | auto`.
+pub type LengthPercentageOrAuto = generics::LengthPercentageOrAuto<LengthPercentage>;
+
+impl LengthPercentageOrAuto {
+ /// Returns a value representing `0%`.
+ #[inline]
+ pub fn zero_percent() -> Self {
+ generics::LengthPercentageOrAuto::LengthPercentage(LengthPercentage::zero_percent())
+ }
+
+ /// Parses a length or a percentage, allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, |context, input| {
+ LengthPercentage::parse_quirky(context, input, allow_quirks)
+ })
+ }
+}
+
+/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0.
+pub type NonNegativeLengthPercentageOrAuto =
+ generics::LengthPercentageOrAuto<NonNegativeLengthPercentage>;
+
+impl NonNegativeLengthPercentageOrAuto {
+ /// Returns a value representing `0%`.
+ #[inline]
+ pub fn zero_percent() -> Self {
+ generics::LengthPercentageOrAuto::LengthPercentage(
+ NonNegativeLengthPercentage::zero_percent(),
+ )
+ }
+
+ /// Parses a non-negative length-percentage, allowing the unitless length
+ /// quirk.
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, |context, input| {
+ NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)
+ })
+ }
+}
+
+/// A wrapper of LengthPercentage, whose value must be >= 0.
+pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>;
+
+/// Either a NonNegativeLengthPercentage or the `normal` keyword.
+pub type NonNegativeLengthPercentageOrNormal =
+ GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>;
+
+impl From<NoCalcLength> for NonNegativeLengthPercentage {
+ #[inline]
+ fn from(len: NoCalcLength) -> Self {
+ NonNegative(LengthPercentage::from(len))
+ }
+}
+
+impl Parse for NonNegativeLengthPercentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl NonNegativeLengthPercentage {
+ #[inline]
+ /// Returns a `0%` value.
+ pub fn zero_percent() -> Self {
+ NonNegative(LengthPercentage::zero_percent())
+ }
+
+ /// Parses a length or a percentage, allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ LengthPercentage::parse_non_negative_quirky(context, input, allow_quirks).map(NonNegative)
+ }
+}
+
+/// Either a `<length>` or the `auto` keyword.
+///
+/// Note that we use LengthPercentage just for convenience, since it pretty much
+/// is everything we care about, but we could just add a similar LengthOrAuto
+/// instead if we think getting rid of this weirdness is worth it.
+pub type LengthOrAuto = generics::LengthPercentageOrAuto<Length>;
+
+impl LengthOrAuto {
+ /// Parses a length, allowing the unitless length quirk.
+ /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk>
+ #[inline]
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with(context, input, |context, input| {
+ Length::parse_quirky(context, input, allow_quirks)
+ })
+ }
+}
+
+/// Either a non-negative `<length>` or the `auto` keyword.
+pub type NonNegativeLengthOrAuto = generics::LengthPercentageOrAuto<NonNegativeLength>;
+
+/// Either a `<length>` or a `<number>`.
+pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>;
+
+/// A specified value for `min-width`, `min-height`, `width` or `height` property.
+pub type Size = GenericSize<NonNegativeLengthPercentage>;
+
+impl Parse for Size {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Size::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+macro_rules! parse_size_non_length {
+ ($size:ident, $input:expr, $auto_or_none:expr => $auto_or_none_ident:ident) => {{
+ let size = $input.try_parse(|input| {
+ Ok(try_match_ident_ignore_ascii_case! { input,
+ #[cfg(feature = "gecko")]
+ "min-content" | "-moz-min-content" => $size::MinContent,
+ #[cfg(feature = "gecko")]
+ "max-content" | "-moz-max-content" => $size::MaxContent,
+ #[cfg(feature = "gecko")]
+ "fit-content" | "-moz-fit-content" => $size::FitContent,
+ #[cfg(feature = "gecko")]
+ "-moz-available" => $size::MozAvailable,
+ $auto_or_none => $size::$auto_or_none_ident,
+ })
+ });
+ if size.is_ok() {
+ return size;
+ }
+ }};
+}
+
+#[cfg(feature = "gecko")]
+fn is_fit_content_function_enabled() -> bool {
+ static_prefs::pref!("layout.css.fit-content-function.enabled")
+}
+#[cfg(feature = "servo")]
+fn is_fit_content_function_enabled() -> bool {
+ false
+}
+
+macro_rules! parse_fit_content_function {
+ ($size:ident, $input:expr, $context:expr, $allow_quirks:expr) => {
+ if is_fit_content_function_enabled() {
+ if let Ok(length) = $input.try_parse(|input| {
+ input.expect_function_matching("fit-content")?;
+ input.parse_nested_block(|i| {
+ NonNegativeLengthPercentage::parse_quirky($context, i, $allow_quirks)
+ })
+ }) {
+ return Ok($size::FitContentFunction(length));
+ }
+ }
+ };
+}
+
+impl Size {
+ /// Parses, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_size_non_length!(Size, input, "auto" => Auto);
+ parse_fit_content_function!(Size, input, context, allow_quirks);
+
+ let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?;
+ Ok(GenericSize::LengthPercentage(length))
+ }
+
+ /// Returns `0%`.
+ #[inline]
+ pub fn zero_percent() -> Self {
+ GenericSize::LengthPercentage(NonNegativeLengthPercentage::zero_percent())
+ }
+}
+
+/// A specified value for `max-width` or `max-height` property.
+pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>;
+
+impl Parse for MaxSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ MaxSize::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl MaxSize {
+ /// Parses, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_size_non_length!(MaxSize, input, "none" => None);
+ parse_fit_content_function!(MaxSize, input, context, allow_quirks);
+
+ let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?;
+ Ok(GenericMaxSize::LengthPercentage(length))
+ }
+}
+
+/// A specified non-negative `<length>` | `<number>`.
+pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>;
diff --git a/servo/components/style/values/specified/list.rs b/servo/components/style/values/specified/list.rs
new file mode 100644
index 0000000000..693471e478
--- /dev/null
+++ b/servo/components/style/values/specified/list.rs
@@ -0,0 +1,202 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! `list` specified values.
+
+use crate::parser::{Parse, ParserContext};
+#[cfg(feature = "gecko")]
+use crate::values::generics::CounterStyle;
+#[cfg(feature = "gecko")]
+use crate::values::CustomIdent;
+use cssparser::{Parser, Token};
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// Specified and computed `list-style-type` property.
+#[cfg(feature = "gecko")]
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum ListStyleType {
+ /// `none`
+ None,
+ /// <counter-style>
+ CounterStyle(CounterStyle),
+ /// <string>
+ String(String),
+}
+
+#[cfg(feature = "gecko")]
+impl ListStyleType {
+ /// Initial specified value for `list-style-type`.
+ #[inline]
+ pub fn disc() -> Self {
+ ListStyleType::CounterStyle(CounterStyle::disc())
+ }
+
+ /// Convert from gecko keyword to list-style-type.
+ ///
+ /// This should only be used for mapping type attribute to
+ /// list-style-type, and thus only values possible in that
+ /// attribute is considered here.
+ pub fn from_gecko_keyword(value: u32) -> Self {
+ use crate::gecko_bindings::structs;
+ let v8 = value as u8;
+
+ if v8 == structs::ListStyle_None {
+ return ListStyleType::None;
+ }
+
+ ListStyleType::CounterStyle(CounterStyle::Name(CustomIdent(match v8 {
+ structs::ListStyle_Disc => atom!("disc"),
+ structs::ListStyle_Circle => atom!("circle"),
+ structs::ListStyle_Square => atom!("square"),
+ structs::ListStyle_Decimal => atom!("decimal"),
+ structs::ListStyle_LowerRoman => atom!("lower-roman"),
+ structs::ListStyle_UpperRoman => atom!("upper-roman"),
+ structs::ListStyle_LowerAlpha => atom!("lower-alpha"),
+ structs::ListStyle_UpperAlpha => atom!("upper-alpha"),
+ _ => unreachable!("Unknown counter style keyword value"),
+ })))
+ }
+
+ /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`)
+ #[inline]
+ pub fn is_bullet(&self) -> bool {
+ match self {
+ ListStyleType::CounterStyle(ref style) => style.is_bullet(),
+ _ => false,
+ }
+ }
+}
+
+#[cfg(feature = "gecko")]
+impl Parse for ListStyleType {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(style) = input.try_parse(|i| CounterStyle::parse(context, i)) {
+ return Ok(ListStyleType::CounterStyle(style));
+ }
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(ListStyleType::None);
+ }
+ Ok(ListStyleType::String(
+ input.expect_string()?.as_ref().to_owned(),
+ ))
+ }
+}
+
+/// A quote pair.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct QuotePair {
+ /// The opening quote.
+ pub opening: crate::OwnedStr,
+
+ /// The closing quote.
+ pub closing: crate::OwnedStr,
+}
+
+/// List of quote pairs for the specified/computed value of `quotes` property.
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct QuoteList(
+ #[css(iterable, if_empty = "none")]
+ #[ignore_malloc_size_of = "Arc"]
+ pub crate::ArcSlice<QuotePair>,
+);
+
+/// Specified and computed `quotes` property: `auto`, `none`, or a list
+/// of characters.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub enum Quotes {
+ /// list of quote pairs
+ QuoteList(QuoteList),
+ /// auto (use lang-dependent quote marks)
+ Auto,
+}
+
+impl Parse for Quotes {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Quotes, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("auto"))
+ .is_ok()
+ {
+ return Ok(Quotes::Auto);
+ }
+
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(Quotes::QuoteList(QuoteList::default()));
+ }
+
+ let mut quotes = Vec::new();
+ loop {
+ let location = input.current_source_location();
+ let opening = match input.next() {
+ Ok(&Token::QuotedString(ref value)) => value.as_ref().to_owned().into(),
+ Ok(t) => return Err(location.new_unexpected_token_error(t.clone())),
+ Err(_) => break,
+ };
+
+ let closing = input.expect_string()?.as_ref().to_owned().into();
+ quotes.push(QuotePair { opening, closing });
+ }
+
+ if !quotes.is_empty() {
+ Ok(Quotes::QuoteList(QuoteList(crate::ArcSlice::from_iter(
+ quotes.into_iter(),
+ ))))
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs
new file mode 100644
index 0000000000..7fc76b3c07
--- /dev/null
+++ b/servo/components/style/values/specified/mod.rs
@@ -0,0 +1,992 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified values.
+//!
+//! TODO(emilio): Enhance docs.
+
+use super::computed::transform::DirectionVector;
+use super::computed::{Context, ToComputedValue};
+use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks;
+use super::generics::grid::{GridLine as GenericGridLine, TrackBreadth as GenericTrackBreadth};
+use super::generics::grid::{TrackList as GenericTrackList, TrackSize as GenericTrackSize};
+use super::generics::transform::IsParallelTo;
+use super::generics::{self, GreaterThanOrEqualToOne, NonNegative};
+use super::{CSSFloat, CSSInteger};
+use crate::context::QuirksMode;
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified::calc::CalcNode;
+use crate::values::{serialize_atom_identifier, serialize_number, AtomString};
+use crate::{Atom, Namespace, One, Prefix, Zero};
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use std::ops::Add;
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+#[cfg(feature = "gecko")]
+pub use self::align::{AlignContent, AlignItems, AlignSelf, AlignTracks, ContentDistribution};
+#[cfg(feature = "gecko")]
+pub use self::align::{JustifyContent, JustifyItems, JustifySelf, JustifyTracks, SelfAlignment};
+pub use self::angle::{AllowUnitlessZeroAngle, Angle};
+pub use self::animation::{
+ AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState,
+ AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis,
+ ScrollTimelineName, TransitionProperty, ViewTimelineInset
+};
+pub use self::background::{BackgroundRepeat, BackgroundSize};
+pub use self::basic_shape::FillRule;
+pub use self::border::{
+ BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice,
+ BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle, LineWidth,
+};
+pub use self::box_::{
+ Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize,
+ ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow,
+ OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign,
+ ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter,
+ TouchAction, VerticalAlign, WillChange, Zoom,
+};
+pub use self::color::{
+ Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust,
+};
+pub use self::column::ColumnCount;
+pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet};
+pub use self::easing::TimingFunction;
+pub use self::effects::{BoxShadow, Filter, SimpleShadow};
+pub use self::flex::FlexBasis;
+pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle};
+pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric};
+pub use self::font::{
+ FontSize, FontSizeAdjust, FontSizeAdjustFactor, FontSizeKeyword, FontStretch, FontSynthesis,
+};
+pub use self::font::{FontVariantAlternates, FontWeight};
+pub use self::font::{FontVariantEastAsian, FontVariationSettings, LineHeight};
+pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale};
+pub use self::image::{EndingShape as GradientEndingShape, Gradient, Image, ImageRendering};
+pub use self::length::{AbsoluteLength, CalcLengthPercentage, CharacterWidth};
+pub use self::length::{FontRelativeLength, Length, LengthOrNumber, NonNegativeLengthOrNumber};
+pub use self::length::{LengthOrAuto, LengthPercentage, LengthPercentageOrAuto};
+pub use self::length::{MaxSize, Size};
+pub use self::length::{NoCalcLength, ViewportPercentageLength, ViewportVariant};
+pub use self::length::{
+ NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto,
+};
+#[cfg(feature = "gecko")]
+pub use self::list::ListStyleType;
+pub use self::list::Quotes;
+pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate};
+pub use self::outline::OutlineStyle;
+pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize};
+pub use self::percentage::{NonNegativePercentage, Percentage};
+pub use self::position::AspectRatio;
+pub use self::position::{GridAutoFlow, GridTemplateAreas, Position, PositionOrAuto};
+pub use self::position::{MasonryAutoFlow, MasonryItemOrder, MasonryPlacement};
+pub use self::position::{PositionComponent, ZIndex};
+pub use self::ratio::Ratio;
+pub use self::rect::NonNegativeLengthOrNumberRect;
+pub use self::resolution::Resolution;
+pub use self::svg::{DProperty, MozContextProperties};
+pub use self::svg::{SVGLength, SVGOpacity, SVGPaint};
+pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth};
+pub use self::svg_path::SVGPathData;
+pub use self::text::HyphenateCharacter;
+pub use self::text::RubyPosition;
+pub use self::text::TextAlignLast;
+pub use self::text::TextUnderlinePosition;
+pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextAlign, TextIndent};
+pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak};
+pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing};
+pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify, TextTransform};
+pub use self::time::Time;
+pub use self::transform::{Rotate, Scale, Transform};
+pub use self::transform::{TransformBox, TransformOrigin, TransformStyle, Translate};
+#[cfg(feature = "gecko")]
+pub use self::ui::CursorImage;
+pub use self::ui::{BoolInteger, Cursor, UserSelect};
+pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent;
+
+#[cfg(feature = "gecko")]
+pub mod align;
+pub mod angle;
+pub mod animation;
+pub mod background;
+pub mod basic_shape;
+pub mod border;
+#[path = "box.rs"]
+pub mod box_;
+pub mod calc;
+pub mod color;
+pub mod column;
+pub mod counters;
+pub mod easing;
+pub mod effects;
+pub mod flex;
+pub mod font;
+#[cfg(feature = "gecko")]
+pub mod gecko;
+pub mod grid;
+pub mod image;
+pub mod length;
+pub mod list;
+pub mod motion;
+pub mod outline;
+pub mod page;
+pub mod percentage;
+pub mod position;
+pub mod ratio;
+pub mod rect;
+pub mod resolution;
+pub mod source_size_list;
+pub mod svg;
+pub mod svg_path;
+pub mod table;
+pub mod text;
+pub mod time;
+pub mod transform;
+pub mod ui;
+pub mod url;
+
+/// <angle> | <percentage>
+/// https://drafts.csswg.org/css-values/#typedef-angle-percentage
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum AngleOrPercentage {
+ Percentage(Percentage),
+ Angle(Angle),
+}
+
+impl AngleOrPercentage {
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_unitless_zero: AllowUnitlessZeroAngle,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(per) = input.try_parse(|i| Percentage::parse(context, i)) {
+ return Ok(AngleOrPercentage::Percentage(per));
+ }
+
+ Angle::parse_internal(context, input, allow_unitless_zero).map(AngleOrPercentage::Angle)
+ }
+
+ /// Allow unitless angles, used for conic-gradients as specified by the spec.
+ /// https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle
+ pub fn parse_with_unitless<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::Yes)
+ }
+}
+
+impl Parse for AngleOrPercentage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::No)
+ }
+}
+
+/// Parse a `<number>` value, with a given clamping mode.
+fn parse_number_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+) -> Result<Number, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => {
+ Ok(Number {
+ value,
+ calc_clamping_mode: None,
+ })
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let value = CalcNode::parse_number(context, input, function)?;
+ Ok(Number {
+ value,
+ calc_clamping_mode: Some(clamping_mode),
+ })
+ },
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+}
+
+/// A CSS `<number>` specified value.
+///
+/// https://drafts.csswg.org/css-values-3/#number-value
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialOrd, ToShmem)]
+pub struct Number {
+ /// The numeric value itself.
+ value: CSSFloat,
+ /// If this number came from a calc() expression, this tells how clamping
+ /// should be done on the value.
+ calc_clamping_mode: Option<AllowedNumericType>,
+}
+
+impl Parse for Number {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+impl PartialEq<Number> for Number {
+ fn eq(&self, other: &Number) -> bool {
+ if self.calc_clamping_mode != other.calc_clamping_mode {
+ return false;
+ }
+
+ self.value == other.value || (self.value.is_nan() && other.value.is_nan())
+ }
+}
+
+impl Number {
+ /// Returns a new number with the value `val`.
+ #[inline]
+ fn new_with_clamping_mode(
+ value: CSSFloat,
+ calc_clamping_mode: Option<AllowedNumericType>,
+ ) -> Self {
+ Self {
+ value,
+ calc_clamping_mode,
+ }
+ }
+
+ /// Returns this percentage as a number.
+ pub fn to_percentage(&self) -> Percentage {
+ Percentage::new_with_clamping_mode(self.value, self.calc_clamping_mode)
+ }
+
+ /// Returns a new number with the value `val`.
+ #[inline]
+ pub fn new(val: CSSFloat) -> Self {
+ Self::new_with_clamping_mode(val, None)
+ }
+
+ /// Returns whether this number came from a `calc()` expression.
+ #[inline]
+ pub fn was_calc(&self) -> bool {
+ self.calc_clamping_mode.is_some()
+ }
+
+ /// Returns the numeric value, clamped if needed.
+ #[inline]
+ pub fn get(&self) -> f32 {
+ crate::values::normalize(
+ self.calc_clamping_mode
+ .map_or(self.value, |mode| mode.clamp(self.value)),
+ )
+ .min(f32::MAX)
+ .max(f32::MIN)
+ }
+
+ #[allow(missing_docs)]
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Number, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+
+ #[allow(missing_docs)]
+ pub fn parse_at_least_one<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Number, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne)
+ }
+
+ /// Clamp to 1.0 if the value is over 1.0.
+ #[inline]
+ pub fn clamp_to_one(self) -> Self {
+ Number {
+ value: self.value.min(1.),
+ calc_clamping_mode: self.calc_clamping_mode,
+ }
+ }
+}
+
+impl ToComputedValue for Number {
+ type ComputedValue = CSSFloat;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> CSSFloat {
+ self.get()
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &CSSFloat) -> Self {
+ Number {
+ value: *computed,
+ calc_clamping_mode: None,
+ }
+ }
+}
+
+impl ToCss for Number {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ serialize_number(self.value, self.calc_clamping_mode.is_some(), dest)
+ }
+}
+
+impl IsParallelTo for (Number, Number, Number) {
+ fn is_parallel_to(&self, vector: &DirectionVector) -> bool {
+ use euclid::approxeq::ApproxEq;
+ // If a and b is parallel, the angle between them is 0deg, so
+ // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0.
+ let self_vector = DirectionVector::new(self.0.get(), self.1.get(), self.2.get());
+ self_vector
+ .cross(*vector)
+ .square_length()
+ .approx_eq(&0.0f32)
+ }
+}
+
+impl SpecifiedValueInfo for Number {}
+
+impl Add for Number {
+ type Output = Self;
+
+ fn add(self, other: Self) -> Self {
+ Self::new(self.get() + other.get())
+ }
+}
+
+impl Zero for Number {
+ #[inline]
+ fn zero() -> Self {
+ Self::new(0.)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.get() == 0.
+ }
+}
+
+impl From<Number> for f32 {
+ #[inline]
+ fn from(n: Number) -> Self {
+ n.get()
+ }
+}
+
+impl From<Number> for f64 {
+ #[inline]
+ fn from(n: Number) -> Self {
+ n.get() as f64
+ }
+}
+
+/// A Number which is >= 0.0.
+pub type NonNegativeNumber = NonNegative<Number>;
+
+impl Parse for NonNegativeNumber {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ .map(NonNegative::<Number>)
+ }
+}
+
+impl One for NonNegativeNumber {
+ #[inline]
+ fn one() -> Self {
+ NonNegativeNumber::new(1.0)
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.get() == 1.0
+ }
+}
+
+impl NonNegativeNumber {
+ /// Returns a new non-negative number with the value `val`.
+ pub fn new(val: CSSFloat) -> Self {
+ NonNegative::<Number>(Number::new(val.max(0.)))
+ }
+
+ /// Returns the numeric value.
+ #[inline]
+ pub fn get(&self) -> f32 {
+ self.0.get()
+ }
+}
+
+/// An Integer which is >= 0.
+pub type NonNegativeInteger = NonNegative<Integer>;
+
+impl Parse for NonNegativeInteger {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Integer::parse_non_negative(context, input)?))
+ }
+}
+
+/// A Number which is >= 1.0.
+pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<Number>;
+
+impl Parse for GreaterThanOrEqualToOneNumber {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne)
+ .map(GreaterThanOrEqualToOne::<Number>)
+ }
+}
+
+/// <number> | <percentage>
+///
+/// Accepts only non-negative numbers.
+#[allow(missing_docs)]
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum NumberOrPercentage {
+ Percentage(Percentage),
+ Number(Number),
+}
+
+impl NumberOrPercentage {
+ fn parse_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ type_: AllowedNumericType,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(per) =
+ input.try_parse(|i| Percentage::parse_with_clamping_mode(context, i, type_))
+ {
+ return Ok(NumberOrPercentage::Percentage(per));
+ }
+
+ parse_number_with_clamping_mode(context, input, type_).map(NumberOrPercentage::Number)
+ }
+
+ /// Parse a non-negative number or percentage.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+
+ /// Convert the number or the percentage to a number.
+ pub fn to_percentage(self) -> Percentage {
+ match self {
+ Self::Percentage(p) => p,
+ Self::Number(n) => n.to_percentage(),
+ }
+ }
+
+ /// Convert the number or the percentage to a number.
+ pub fn to_number(self) -> Number {
+ match self {
+ Self::Percentage(p) => p.to_number(),
+ Self::Number(n) => n,
+ }
+ }
+}
+
+impl Parse for NumberOrPercentage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+/// A non-negative <number> | <percentage>.
+pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>;
+
+impl NonNegativeNumberOrPercentage {
+ /// Returns the `100%` value.
+ #[inline]
+ pub fn hundred_percent() -> Self {
+ NonNegative(NumberOrPercentage::Percentage(Percentage::hundred()))
+ }
+
+ /// Return a particular number.
+ #[inline]
+ pub fn new_number(n: f32) -> Self {
+ NonNegative(NumberOrPercentage::Number(Number::new(n)))
+ }
+}
+
+impl Parse for NonNegativeNumberOrPercentage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(NumberOrPercentage::parse_non_negative(
+ context, input,
+ )?))
+ }
+}
+
+/// The value of Opacity is <alpha-value>, which is "<number> | <percentage>".
+/// However, we serialize the specified value as number, so it's ok to store
+/// the Opacity as Number.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub struct Opacity(Number);
+
+impl Parse for Opacity {
+ /// Opacity accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
+ /// and then convert into an Number if it's a Percentage.
+ /// https://drafts.csswg.org/cssom/#serializing-css-values
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let number = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(Opacity(number))
+ }
+}
+
+impl ToComputedValue for Opacity {
+ type ComputedValue = CSSFloat;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> CSSFloat {
+ let value = self.0.to_computed_value(context);
+ if context.for_smil_animation {
+ // SMIL expects to be able to interpolate between out-of-range
+ // opacity values.
+ value
+ } else {
+ value.min(1.0).max(0.0)
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &CSSFloat) -> Self {
+ Opacity(Number::from_computed_value(computed))
+ }
+}
+
+/// A specified `<integer>`, optionally coming from a `calc()` expression.
+///
+/// <https://drafts.csswg.org/css-values/#integers>
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, ToShmem)]
+pub struct Integer {
+ value: CSSInteger,
+ was_calc: bool,
+}
+
+impl Zero for Integer {
+ #[inline]
+ fn zero() -> Self {
+ Self::new(0)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ self.value() == 0
+ }
+}
+
+impl One for Integer {
+ #[inline]
+ fn one() -> Self {
+ Self::new(1)
+ }
+
+ #[inline]
+ fn is_one(&self) -> bool {
+ self.value() == 1
+ }
+}
+
+impl PartialEq<i32> for Integer {
+ fn eq(&self, value: &i32) -> bool {
+ self.value() == *value
+ }
+}
+
+impl Integer {
+ /// Trivially constructs a new `Integer` value.
+ pub fn new(val: CSSInteger) -> Self {
+ Integer {
+ value: val,
+ was_calc: false,
+ }
+ }
+
+ /// Returns the integer value associated with this value.
+ pub fn value(&self) -> CSSInteger {
+ self.value
+ }
+
+ /// Trivially constructs a new integer value from a `calc()` expression.
+ fn from_calc(val: CSSInteger) -> Self {
+ Integer {
+ value: val,
+ was_calc: true,
+ }
+ }
+}
+
+impl Parse for Integer {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Number {
+ int_value: Some(v), ..
+ } => Ok(Integer::new(v)),
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let result = CalcNode::parse_integer(context, input, function)?;
+ Ok(Integer::from_calc(result))
+ },
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
+
+impl Integer {
+ /// Parse an integer value which is at least `min`.
+ pub fn parse_with_minimum<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ min: i32,
+ ) -> Result<Integer, ParseError<'i>> {
+ let value = Integer::parse(context, input)?;
+ // FIXME(emilio): The spec asks us to avoid rejecting it at parse
+ // time except until computed value time.
+ //
+ // It's not totally clear it's worth it though, and no other browser
+ // does this.
+ if value.value() < min {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(value)
+ }
+
+ /// Parse a non-negative integer.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Integer, ParseError<'i>> {
+ Integer::parse_with_minimum(context, input, 0)
+ }
+
+ /// Parse a positive integer (>= 1).
+ pub fn parse_positive<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Integer, ParseError<'i>> {
+ Integer::parse_with_minimum(context, input, 1)
+ }
+}
+
+impl ToComputedValue for Integer {
+ type ComputedValue = i32;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> i32 {
+ self.value
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &i32) -> Self {
+ Integer::new(*computed)
+ }
+}
+
+impl ToCss for Integer {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.was_calc {
+ dest.write_str("calc(")?;
+ }
+ self.value.to_css(dest)?;
+ if self.was_calc {
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+}
+
+impl SpecifiedValueInfo for Integer {}
+
+/// A wrapper of Integer, with value >= 1.
+pub type PositiveInteger = GreaterThanOrEqualToOne<Integer>;
+
+impl Parse for PositiveInteger {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne)
+ }
+}
+
+/// The specified value of a grid `<track-breadth>`
+pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>;
+
+/// The specified value of a grid `<track-size>`
+pub type TrackSize = GenericTrackSize<LengthPercentage>;
+
+/// The specified value of a grid `<track-size>+`
+pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>;
+
+/// The specified value of a grid `<track-list>`
+/// (could also be `<auto-track-list>` or `<explicit-track-list>`)
+pub type TrackList = GenericTrackList<LengthPercentage, Integer>;
+
+/// The specified value of a `<grid-line>`.
+pub type GridLine = GenericGridLine<Integer>;
+
+/// `<grid-template-rows> | <grid-template-columns>`
+pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>;
+
+/// rect(...)
+pub type ClipRect = generics::GenericClipRect<LengthOrAuto>;
+
+impl Parse for ClipRect {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl ClipRect {
+ /// Parses a rect(<top>, <left>, <bottom>, <right>), allowing quirks.
+ fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ input.expect_function_matching("rect")?;
+
+ fn parse_argument<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<LengthOrAuto, ParseError<'i>> {
+ LengthOrAuto::parse_quirky(context, input, allow_quirks)
+ }
+
+ input.parse_nested_block(|input| {
+ let top = parse_argument(context, input, allow_quirks)?;
+ let right;
+ let bottom;
+ let left;
+
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ right = parse_argument(context, input, allow_quirks)?;
+ input.expect_comma()?;
+ bottom = parse_argument(context, input, allow_quirks)?;
+ input.expect_comma()?;
+ left = parse_argument(context, input, allow_quirks)?;
+ } else {
+ right = parse_argument(context, input, allow_quirks)?;
+ bottom = parse_argument(context, input, allow_quirks)?;
+ left = parse_argument(context, input, allow_quirks)?;
+ }
+
+ Ok(ClipRect {
+ top,
+ right,
+ bottom,
+ left,
+ })
+ })
+ }
+}
+
+/// rect(...) | auto
+pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>;
+
+impl ClipRectOrAuto {
+ /// Parses a ClipRect or Auto, allowing quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(v) = input.try_parse(|i| ClipRect::parse_quirky(context, i, allow_quirks)) {
+ return Ok(generics::GenericClipRectOrAuto::Rect(v));
+ }
+ input.expect_ident_matching("auto")?;
+ Ok(generics::GenericClipRectOrAuto::Auto)
+ }
+}
+
+/// Whether quirks are allowed in this context.
+#[derive(Clone, Copy, PartialEq)]
+pub enum AllowQuirks {
+ /// Quirks are not allowed.
+ No,
+ /// Quirks are allowed, in quirks mode.
+ Yes,
+ /// Quirks are always allowed, used for SVG lengths.
+ Always,
+}
+
+impl AllowQuirks {
+ /// Returns `true` if quirks are allowed in this context.
+ pub fn allowed(self, quirks_mode: QuirksMode) -> bool {
+ match self {
+ AllowQuirks::Always => true,
+ AllowQuirks::No => false,
+ AllowQuirks::Yes => quirks_mode == QuirksMode::Quirks,
+ }
+ }
+}
+
+/// An attr(...) rule
+///
+/// `[namespace? `|`]? ident`
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(function)]
+#[repr(C)]
+pub struct Attr {
+ /// Optional namespace prefix.
+ pub namespace_prefix: Prefix,
+ /// Optional namespace URL.
+ pub namespace_url: Namespace,
+ /// Attribute name
+ pub attribute: Atom,
+ /// Fallback value
+ pub fallback: AtomString,
+}
+
+impl Parse for Attr {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Attr, ParseError<'i>> {
+ input.expect_function_matching("attr")?;
+ input.parse_nested_block(|i| Attr::parse_function(context, i))
+ }
+}
+
+/// Get the Namespace for a given prefix from the namespace map.
+fn get_namespace_for_prefix(prefix: &Prefix, context: &ParserContext) -> Option<Namespace> {
+ context.namespaces.prefixes.get(prefix).cloned()
+}
+
+/// Try to parse a namespace and return it if parsed, or none if there was not one present
+fn parse_namespace<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+) -> Result<(Prefix, Namespace), ParseError<'i>> {
+ let ns_prefix = match input.next()? {
+ Token::Ident(ref prefix) => Some(Prefix::from(prefix.as_ref())),
+ Token::Delim('|') => None,
+ _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ };
+
+ if ns_prefix.is_some() && !matches!(*input.next_including_whitespace()?, Token::Delim('|')) {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ if let Some(prefix) = ns_prefix {
+ let ns = match get_namespace_for_prefix(&prefix, context) {
+ Some(ns) => ns,
+ None => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ };
+ Ok((prefix, ns))
+ } else {
+ Ok((Prefix::default(), Namespace::default()))
+ }
+}
+
+impl Attr {
+ /// Parse contents of attr() assuming we have already parsed `attr` and are
+ /// within a parse_nested_block()
+ pub fn parse_function<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Attr, ParseError<'i>> {
+ // Syntax is `[namespace? '|']? ident [',' fallback]?`
+ let namespace = input
+ .try_parse(|input| parse_namespace(context, input))
+ .ok();
+ let namespace_is_some = namespace.is_some();
+ let (namespace_prefix, namespace_url) = namespace.unwrap_or_default();
+
+ // If there is a namespace, ensure no whitespace following '|'
+ let attribute = Atom::from(if namespace_is_some {
+ let location = input.current_source_location();
+ match *input.next_including_whitespace()? {
+ Token::Ident(ref ident) => ident.as_ref(),
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ } else {
+ input.expect_ident()?.as_ref()
+ });
+
+ // Fallback will always be a string value for now as we do not support
+ // attr() types yet.
+ let fallback = input
+ .try_parse(|input| -> Result<AtomString, ParseError<'i>> {
+ input.expect_comma()?;
+ Ok(input.expect_string()?.as_ref().into())
+ })
+ .unwrap_or_default();
+
+ Ok(Attr {
+ namespace_prefix,
+ namespace_url,
+ attribute,
+ fallback,
+ })
+ }
+}
+
+impl ToCss for Attr {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str("attr(")?;
+ if !self.namespace_prefix.is_empty() {
+ serialize_atom_identifier(&self.namespace_prefix, dest)?;
+ dest.write_char('|')?;
+ }
+ serialize_atom_identifier(&self.attribute, dest)?;
+
+ if !self.fallback.is_empty() {
+ dest.write_str(", ")?;
+ self.fallback.to_css(dest)?;
+ }
+
+ dest.write_char(')')
+ }
+}
diff --git a/servo/components/style/values/specified/motion.rs b/servo/components/style/values/specified/motion.rs
new file mode 100644
index 0000000000..98858c712c
--- /dev/null
+++ b/servo/components/style/values/specified/motion.rs
@@ -0,0 +1,343 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values that are related to motion path.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::motion::OffsetRotate as ComputedOffsetRotate;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::motion as generics;
+use crate::values::specified::basic_shape::BasicShape;
+use crate::values::specified::position::{HorizontalPosition, VerticalPosition};
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::{Angle, Position};
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+/// The specified value of ray() function.
+pub type RayFunction = generics::GenericRayFunction<Angle, Position>;
+
+/// The specified value of <offset-path>.
+pub type OffsetPathFunction =
+ generics::GenericOffsetPathFunction<BasicShape, RayFunction, SpecifiedUrl>;
+
+/// The specified value of `offset-path`.
+pub type OffsetPath = generics::GenericOffsetPath<OffsetPathFunction>;
+
+/// The specified value of `offset-position`.
+pub type OffsetPosition = generics::GenericOffsetPosition<HorizontalPosition, VerticalPosition>;
+
+/// The <coord-box> value, which defines the box that the <offset-path> sizes into.
+/// https://drafts.fxtf.org/motion-1/#valdef-offset-path-coord-box
+///
+/// <coord-box> = content-box | padding-box | border-box | fill-box | stroke-box | view-box
+/// https://drafts.csswg.org/css-box-4/#typedef-coord-box
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum CoordBox {
+ ContentBox,
+ PaddingBox,
+ BorderBox,
+ FillBox,
+ StrokeBox,
+ ViewBox,
+}
+
+impl CoordBox {
+ /// Returns true if it is default value, border-box.
+ #[inline]
+ pub fn is_default(&self) -> bool {
+ matches!(*self, Self::BorderBox)
+ }
+}
+
+impl Parse for RayFunction {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if !static_prefs::pref!("layout.css.motion-path-ray.enabled") {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ input.expect_function_matching("ray")?;
+ input.parse_nested_block(|i| Self::parse_function_arguments(context, i))
+ }
+}
+
+impl RayFunction {
+ /// Parse the inner arguments of a `ray` function.
+ fn parse_function_arguments<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::PositionOrAuto;
+
+ let mut angle = None;
+ let mut size = None;
+ let mut contain = false;
+ let mut position = None;
+ loop {
+ if angle.is_none() {
+ angle = input.try_parse(|i| Angle::parse(context, i)).ok();
+ }
+
+ if size.is_none() {
+ size = input.try_parse(generics::RaySize::parse).ok();
+ if size.is_some() {
+ continue;
+ }
+ }
+
+ if !contain {
+ contain = input
+ .try_parse(|i| i.expect_ident_matching("contain"))
+ .is_ok();
+ if contain {
+ continue;
+ }
+ }
+
+ if position.is_none() {
+ if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() {
+ let pos = Position::parse(context, input)?;
+ position = Some(PositionOrAuto::Position(pos));
+ }
+
+ if position.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ if angle.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(RayFunction {
+ angle: angle.unwrap(),
+ // If no <ray-size> is specified it defaults to closest-side.
+ size: size.unwrap_or(generics::RaySize::ClosestSide),
+ contain,
+ position: position.unwrap_or(PositionOrAuto::auto()),
+ })
+ }
+}
+
+impl Parse for OffsetPathFunction {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::specified::basic_shape::{AllowedBasicShapes, ShapeType};
+
+ // <offset-path> = <ray()> | <url> | <basic-shape>
+ // https://drafts.fxtf.org/motion-1/#typedef-offset-path
+
+ if static_prefs::pref!("layout.css.motion-path-ray.enabled") {
+ if let Ok(ray) = input.try_parse(|i| RayFunction::parse(context, i)) {
+ return Ok(OffsetPathFunction::Ray(ray));
+ }
+ }
+
+ if static_prefs::pref!("layout.css.motion-path-url.enabled") {
+ if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) {
+ return Ok(OffsetPathFunction::Url(url));
+ }
+ }
+
+ let allowed_shapes = if static_prefs::pref!("layout.css.motion-path-basic-shapes.enabled") {
+ AllowedBasicShapes::ALL
+ } else {
+ AllowedBasicShapes::PATH
+ };
+
+ BasicShape::parse(context, input, allowed_shapes, ShapeType::Outline)
+ .map(OffsetPathFunction::Shape)
+ }
+}
+
+impl Parse for OffsetPath {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Parse none.
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(OffsetPath::none());
+ }
+
+ let mut path = None;
+ let mut coord_box = None;
+ loop {
+ if path.is_none() {
+ path = input
+ .try_parse(|i| OffsetPathFunction::parse(context, i))
+ .ok();
+ }
+
+ if static_prefs::pref!("layout.css.motion-path-coord-box.enabled") &&
+ coord_box.is_none()
+ {
+ coord_box = input.try_parse(CoordBox::parse).ok();
+ if coord_box.is_some() {
+ continue;
+ }
+ }
+ break;
+ }
+
+ if let Some(p) = path {
+ return Ok(OffsetPath::OffsetPath {
+ path: Box::new(p),
+ coord_box: coord_box.unwrap_or(CoordBox::BorderBox),
+ });
+ }
+
+ match coord_box {
+ Some(c) => Ok(OffsetPath::CoordBox(c)),
+ None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+}
+
+/// The direction of offset-rotate.
+#[derive(
+ Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+#[repr(u8)]
+pub enum OffsetRotateDirection {
+ /// Unspecified direction keyword.
+ #[css(skip)]
+ None,
+ /// 0deg offset (face forward).
+ Auto,
+ /// 180deg offset (face backward).
+ Reverse,
+}
+
+impl OffsetRotateDirection {
+ /// Returns true if it is none (i.e. the keyword is not specified).
+ #[inline]
+ fn is_none(&self) -> bool {
+ *self == OffsetRotateDirection::None
+ }
+}
+
+#[inline]
+fn direction_specified_and_angle_is_zero(direction: &OffsetRotateDirection, angle: &Angle) -> bool {
+ !direction.is_none() && angle.is_zero()
+}
+
+/// The specified offset-rotate.
+/// The syntax is: "[ auto | reverse ] || <angle>"
+///
+/// https://drafts.fxtf.org/motion-1/#offset-rotate-property
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub struct OffsetRotate {
+ /// [auto | reverse].
+ #[css(skip_if = "OffsetRotateDirection::is_none")]
+ direction: OffsetRotateDirection,
+ /// <angle>.
+ /// If direction is None, this is a fixed angle which indicates a
+ /// constant clockwise rotation transformation applied to it by this
+ /// specified rotation angle. Otherwise, the angle will be added to
+ /// the angle of the direction in layout.
+ #[css(contextual_skip_if = "direction_specified_and_angle_is_zero")]
+ angle: Angle,
+}
+
+impl OffsetRotate {
+ /// Returns the initial value, auto.
+ #[inline]
+ pub fn auto() -> Self {
+ OffsetRotate {
+ direction: OffsetRotateDirection::Auto,
+ angle: Angle::zero(),
+ }
+ }
+
+ /// Returns true if self is auto 0deg.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ self.direction == OffsetRotateDirection::Auto && self.angle.is_zero()
+ }
+}
+
+impl Parse for OffsetRotate {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let mut direction = input.try_parse(OffsetRotateDirection::parse);
+ let angle = input.try_parse(|i| Angle::parse(context, i));
+ if direction.is_err() {
+ // The direction and angle could be any order, so give it a change to parse
+ // direction again.
+ direction = input.try_parse(OffsetRotateDirection::parse);
+ }
+
+ if direction.is_err() && angle.is_err() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(OffsetRotate {
+ direction: direction.unwrap_or(OffsetRotateDirection::None),
+ angle: angle.unwrap_or(Zero::zero()),
+ })
+ }
+}
+
+impl ToComputedValue for OffsetRotate {
+ type ComputedValue = ComputedOffsetRotate;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ use crate::values::computed::Angle as ComputedAngle;
+
+ ComputedOffsetRotate {
+ auto: !self.direction.is_none(),
+ angle: if self.direction == OffsetRotateDirection::Reverse {
+ // The computed value should always convert "reverse" into "auto".
+ // e.g. "reverse calc(20deg + 10deg)" => "auto 210deg"
+ self.angle.to_computed_value(context) + ComputedAngle::from_degrees(180.0)
+ } else {
+ self.angle.to_computed_value(context)
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ OffsetRotate {
+ direction: if computed.auto {
+ OffsetRotateDirection::Auto
+ } else {
+ OffsetRotateDirection::None
+ },
+ angle: ToComputedValue::from_computed_value(&computed.angle),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/outline.rs b/servo/components/style/values/specified/outline.rs
new file mode 100644
index 0000000000..6e5382d4c2
--- /dev/null
+++ b/servo/components/style/values/specified/outline.rs
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified values for outline properties
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified::BorderStyle;
+use cssparser::Parser;
+use selectors::parser::SelectorParseErrorKind;
+use style_traits::ParseError;
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Ord,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+/// <https://drafts.csswg.org/css-ui/#propdef-outline-style>
+pub enum OutlineStyle {
+ /// auto
+ Auto,
+ /// <border-style>
+ BorderStyle(BorderStyle),
+}
+
+impl OutlineStyle {
+ #[inline]
+ /// Get default value as None
+ pub fn none() -> OutlineStyle {
+ OutlineStyle::BorderStyle(BorderStyle::None)
+ }
+
+ #[inline]
+ /// Get value for None or Hidden
+ pub fn none_or_hidden(&self) -> bool {
+ match *self {
+ OutlineStyle::Auto => false,
+ OutlineStyle::BorderStyle(ref style) => style.none_or_hidden(),
+ }
+ }
+}
+
+impl Parse for OutlineStyle {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<OutlineStyle, ParseError<'i>> {
+ if let Ok(border_style) = input.try_parse(BorderStyle::parse) {
+ if let BorderStyle::Hidden = border_style {
+ return Err(input
+ .new_custom_error(SelectorParseErrorKind::UnexpectedIdent("hidden".into())));
+ }
+
+ return Ok(OutlineStyle::BorderStyle(border_style));
+ }
+
+ input.expect_ident_matching("auto")?;
+ Ok(OutlineStyle::Auto)
+ }
+}
diff --git a/servo/components/style/values/specified/page.rs b/servo/components/style/values/specified/page.rs
new file mode 100644
index 0000000000..76d9105e8f
--- /dev/null
+++ b/servo/components/style/values/specified/page.rs
@@ -0,0 +1,99 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified @page at-rule properties and named-page style properties
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::size::Size2D;
+use crate::values::specified::length::NonNegativeLength;
+use crate::values::{generics, CustomIdent};
+use cssparser::Parser;
+use style_traits::ParseError;
+
+pub use generics::page::PageOrientation;
+pub use generics::page::PageSizeOrientation;
+pub use generics::page::PaperSize;
+/// Specified value of the @page size descriptor
+pub type PageSize = generics::page::PageSize<Size2D<NonNegativeLength>>;
+
+impl Parse for PageSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Try to parse as <page-size> [ <orientation> ]
+ if let Ok(paper_size) = input.try_parse(PaperSize::parse) {
+ let orientation = input
+ .try_parse(PageSizeOrientation::parse)
+ .unwrap_or(PageSizeOrientation::Portrait);
+ return Ok(PageSize::PaperSize(paper_size, orientation));
+ }
+ // Try to parse as <orientation> [ <page-size> ]
+ if let Ok(orientation) = input.try_parse(PageSizeOrientation::parse) {
+ if let Ok(paper_size) = input.try_parse(PaperSize::parse) {
+ return Ok(PageSize::PaperSize(paper_size, orientation));
+ }
+ return Ok(PageSize::Orientation(orientation));
+ }
+ // Try to parse dimensions
+ if let Ok(size) =
+ input.try_parse(|i| Size2D::parse_with(context, i, NonNegativeLength::parse))
+ {
+ return Ok(PageSize::Size(size));
+ }
+ // auto value
+ input.expect_ident_matching("auto")?;
+ Ok(PageSize::Auto)
+ }
+}
+
+/// Page name value.
+///
+/// https://drafts.csswg.org/css-page-3/#using-named-pages
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum PageName {
+ /// `auto` value.
+ Auto,
+ /// Page name value
+ PageName(CustomIdent),
+}
+
+impl Parse for PageName {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ Ok(match_ignore_ascii_case! { ident,
+ "auto" => PageName::auto(),
+ _ => PageName::PageName(CustomIdent::from_ident(location, ident, &[])?),
+ })
+ }
+}
+
+impl PageName {
+ /// `auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ PageName::Auto
+ }
+
+ /// Whether this is the `auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, PageName::Auto)
+ }
+}
diff --git a/servo/components/style/values/specified/percentage.rs b/servo/components/style/values/specified/percentage.rs
new file mode 100644
index 0000000000..ccf16d6463
--- /dev/null
+++ b/servo/components/style/values/specified/percentage.rs
@@ -0,0 +1,225 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified percentages.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::percentage::Percentage as ComputedPercentage;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::NonNegative;
+use crate::values::specified::calc::CalcNode;
+use crate::values::specified::Number;
+use crate::values::{normalize, serialize_percentage, CSSFloat};
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss};
+
+/// A percentage value.
+#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Percentage {
+ /// The percentage value as a float.
+ ///
+ /// [0 .. 100%] maps to [0.0 .. 1.0]
+ value: CSSFloat,
+ /// If this percentage came from a calc() expression, this tells how
+ /// clamping should be done on the value.
+ calc_clamping_mode: Option<AllowedNumericType>,
+}
+
+impl ToCss for Percentage {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.calc_clamping_mode.is_some() {
+ dest.write_str("calc(")?;
+ }
+
+ serialize_percentage(self.value, dest)?;
+
+ if self.calc_clamping_mode.is_some() {
+ dest.write_char(')')?;
+ }
+ Ok(())
+ }
+}
+
+impl Percentage {
+ /// Creates a percentage from a numeric value.
+ pub(super) fn new_with_clamping_mode(
+ value: CSSFloat,
+ calc_clamping_mode: Option<AllowedNumericType>,
+ ) -> Self {
+ Self {
+ value,
+ calc_clamping_mode,
+ }
+ }
+
+ /// Creates a percentage from a numeric value.
+ pub fn new(value: CSSFloat) -> Self {
+ Self::new_with_clamping_mode(value, None)
+ }
+
+ /// `0%`
+ #[inline]
+ pub fn zero() -> Self {
+ Percentage {
+ value: 0.,
+ calc_clamping_mode: None,
+ }
+ }
+
+ /// `100%`
+ #[inline]
+ pub fn hundred() -> Self {
+ Percentage {
+ value: 1.,
+ calc_clamping_mode: None,
+ }
+ }
+
+ /// Gets the underlying value for this float.
+ pub fn get(&self) -> CSSFloat {
+ self.calc_clamping_mode
+ .map_or(self.value, |mode| mode.clamp(self.value))
+ }
+
+ /// Returns this percentage as a number.
+ pub fn to_number(&self) -> Number {
+ Number::new_with_clamping_mode(self.value, self.calc_clamping_mode)
+ }
+
+ /// Returns the calc() clamping mode for this percentage.
+ pub fn calc_clamping_mode(&self) -> Option<AllowedNumericType> {
+ self.calc_clamping_mode
+ }
+
+ /// Reverses this percentage, preserving calc-ness.
+ ///
+ /// For example: If it was 20%, convert it into 80%.
+ pub fn reverse(&mut self) {
+ let new_value = 1. - self.value;
+ self.value = new_value;
+ }
+
+ /// Parses a specific kind of percentage.
+ pub fn parse_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ num_context: AllowedNumericType,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Percentage { unit_value, .. }
+ if num_context.is_ok(context.parsing_mode, unit_value) =>
+ {
+ Ok(Percentage::new(unit_value))
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ let value = CalcNode::parse_percentage(context, input, function)?;
+ Ok(Percentage {
+ value,
+ calc_clamping_mode: Some(num_context),
+ })
+ },
+ ref t => Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+
+ /// Parses a percentage token, but rejects it if it's negative.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+
+ /// Parses a percentage token, but rejects it if it's negative or more than
+ /// 100%.
+ pub fn parse_zero_to_a_hundred<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::ZeroToOne)
+ }
+
+ /// Clamp to 100% if the value is over 100%.
+ #[inline]
+ pub fn clamp_to_hundred(self) -> Self {
+ Percentage {
+ value: self.value.min(1.),
+ calc_clamping_mode: self.calc_clamping_mode,
+ }
+ }
+}
+
+impl Parse for Percentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+impl ToComputedValue for Percentage {
+ type ComputedValue = ComputedPercentage;
+
+ #[inline]
+ fn to_computed_value(&self, _: &Context) -> Self::ComputedValue {
+ ComputedPercentage(normalize(self.get()))
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Percentage::new(computed.0)
+ }
+}
+
+impl SpecifiedValueInfo for Percentage {}
+
+/// Turns the percentage into a plain float.
+pub trait ToPercentage {
+ /// Returns whether this percentage used to be a calc().
+ fn is_calc(&self) -> bool {
+ false
+ }
+ /// Turns the percentage into a plain float.
+ fn to_percentage(&self) -> CSSFloat;
+}
+
+impl ToPercentage for Percentage {
+ fn is_calc(&self) -> bool {
+ self.calc_clamping_mode.is_some()
+ }
+
+ fn to_percentage(&self) -> CSSFloat {
+ self.get()
+ }
+}
+
+/// A wrapper of Percentage, whose value must be >= 0.
+pub type NonNegativePercentage = NonNegative<Percentage>;
+
+impl Parse for NonNegativePercentage {
+ #[inline]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Ok(NonNegative(Percentage::parse_non_negative(context, input)?))
+ }
+}
+
+impl NonNegativePercentage {
+ /// Convert to ComputedPercentage, for FontFaceRule size-adjust getter.
+ #[inline]
+ pub fn compute(&self) -> ComputedPercentage {
+ ComputedPercentage(self.0.get())
+ }
+}
diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs
new file mode 100644
index 0000000000..bab853d972
--- /dev/null
+++ b/servo/components/style/values/specified/position.rs
@@ -0,0 +1,955 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! CSS handling for the specified value of
+//! [`position`][position]s
+//!
+//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position
+
+use crate::parser::{Parse, ParserContext};
+use crate::selector_map::PrecomputedHashMap;
+use crate::str::HTML_SPACE_CHARACTERS;
+use crate::values::computed::LengthPercentage as ComputedLengthPercentage;
+use crate::values::computed::{Context, Percentage, ToComputedValue};
+use crate::values::generics::position::AspectRatio as GenericAspectRatio;
+use crate::values::generics::position::Position as GenericPosition;
+use crate::values::generics::position::PositionComponent as GenericPositionComponent;
+use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto;
+use crate::values::generics::position::ZIndex as GenericZIndex;
+use crate::values::specified::{AllowQuirks, Integer, LengthPercentage, NonNegativeNumber};
+use crate::{Atom, Zero};
+use cssparser::Parser;
+use selectors::parser::SelectorParseErrorKind;
+use servo_arc::Arc;
+use std::collections::hash_map::Entry;
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// The specified value of a CSS `<position>`
+pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>;
+
+/// The specified value of an `auto | <position>`.
+pub type PositionOrAuto = GenericPositionOrAuto<Position>;
+
+/// The specified value of a horizontal position.
+pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>;
+
+/// The specified value of a vertical position.
+pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>;
+
+/// The specified value of a component of a CSS `<position>`.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum PositionComponent<S> {
+ /// `center`
+ Center,
+ /// `<length-percentage>`
+ Length(LengthPercentage),
+ /// `<side> <length-percentage>?`
+ Side(S, Option<LengthPercentage>),
+}
+
+/// A keyword for the X direction.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum HorizontalPositionKeyword {
+ Left,
+ Right,
+}
+
+/// A keyword for the Y direction.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum VerticalPositionKeyword {
+ Top,
+ Bottom,
+}
+
+impl Parse for Position {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let position = Self::parse_three_value_quirky(context, input, AllowQuirks::No)?;
+ if position.is_three_value_syntax() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ Ok(position)
+ }
+}
+
+impl Position {
+ /// Parses a `<bg-position>`, with quirks.
+ pub fn parse_three_value_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ match input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) {
+ Ok(x_pos @ PositionComponent::Center) => {
+ if let Ok(y_pos) =
+ input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks))
+ {
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let x_pos = input
+ .try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks))
+ .unwrap_or(x_pos);
+ let y_pos = PositionComponent::Center;
+ return Ok(Self::new(x_pos, y_pos));
+ },
+ Ok(PositionComponent::Side(x_keyword, lp)) => {
+ if input
+ .try_parse(|i| i.expect_ident_matching("center"))
+ .is_ok()
+ {
+ let x_pos = PositionComponent::Side(x_keyword, lp);
+ let y_pos = PositionComponent::Center;
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
+ let y_lp = input
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ let x_pos = PositionComponent::Side(x_keyword, lp);
+ let y_pos = PositionComponent::Side(y_keyword, y_lp);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let x_pos = PositionComponent::Side(x_keyword, None);
+ let y_pos = lp.map_or(PositionComponent::Center, PositionComponent::Length);
+ return Ok(Self::new(x_pos, y_pos));
+ },
+ Ok(x_pos @ PositionComponent::Length(_)) => {
+ if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) {
+ let y_pos = PositionComponent::Side(y_keyword, None);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ if let Ok(y_lp) =
+ input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ {
+ let y_pos = PositionComponent::Length(y_lp);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let y_pos = PositionComponent::Center;
+ let _ = input.try_parse(|i| i.expect_ident_matching("center"));
+ return Ok(Self::new(x_pos, y_pos));
+ },
+ Err(_) => {},
+ }
+ let y_keyword = VerticalPositionKeyword::parse(input)?;
+ let lp_and_x_pos: Result<_, ParseError> = input.try_parse(|i| {
+ let y_lp = i
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) {
+ let x_lp = i
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ let x_pos = PositionComponent::Side(x_keyword, x_lp);
+ return Ok((y_lp, x_pos));
+ };
+ i.expect_ident_matching("center")?;
+ let x_pos = PositionComponent::Center;
+ Ok((y_lp, x_pos))
+ });
+ if let Ok((y_lp, x_pos)) = lp_and_x_pos {
+ let y_pos = PositionComponent::Side(y_keyword, y_lp);
+ return Ok(Self::new(x_pos, y_pos));
+ }
+ let x_pos = PositionComponent::Center;
+ let y_pos = PositionComponent::Side(y_keyword, None);
+ Ok(Self::new(x_pos, y_pos))
+ }
+
+ /// `center center`
+ #[inline]
+ pub fn center() -> Self {
+ Self::new(PositionComponent::Center, PositionComponent::Center)
+ }
+
+ /// Returns true if this uses a 3 value syntax.
+ #[inline]
+ fn is_three_value_syntax(&self) -> bool {
+ self.horizontal.component_count() != self.vertical.component_count()
+ }
+}
+
+impl ToCss for Position {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ match (&self.horizontal, &self.vertical) {
+ (
+ x_pos @ &PositionComponent::Side(_, Some(_)),
+ &PositionComponent::Length(ref y_lp),
+ ) => {
+ x_pos.to_css(dest)?;
+ dest.write_str(" top ")?;
+ y_lp.to_css(dest)
+ },
+ (
+ &PositionComponent::Length(ref x_lp),
+ y_pos @ &PositionComponent::Side(_, Some(_)),
+ ) => {
+ dest.write_str("left ")?;
+ x_lp.to_css(dest)?;
+ dest.write_char(' ')?;
+ y_pos.to_css(dest)
+ },
+ (x_pos, y_pos) => {
+ x_pos.to_css(dest)?;
+ dest.write_char(' ')?;
+ y_pos.to_css(dest)
+ },
+ }
+ }
+}
+
+impl<S: Parse> Parse for PositionComponent<S> {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_quirky(context, input, AllowQuirks::No)
+ }
+}
+
+impl<S: Parse> PositionComponent<S> {
+ /// Parses a component of a CSS position, with quirks.
+ pub fn parse_quirky<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ allow_quirks: AllowQuirks,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("center"))
+ .is_ok()
+ {
+ return Ok(PositionComponent::Center);
+ }
+ if let Ok(lp) =
+ input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ {
+ return Ok(PositionComponent::Length(lp));
+ }
+ let keyword = S::parse(context, input)?;
+ let lp = input
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks))
+ .ok();
+ Ok(PositionComponent::Side(keyword, lp))
+ }
+}
+
+impl<S> GenericPositionComponent for PositionComponent<S> {
+ fn is_center(&self) -> bool {
+ match *self {
+ PositionComponent::Center => true,
+ PositionComponent::Length(LengthPercentage::Percentage(ref per)) => per.0 == 0.5,
+ // 50% from any side is still the center.
+ PositionComponent::Side(_, Some(LengthPercentage::Percentage(ref per))) => per.0 == 0.5,
+ _ => false,
+ }
+ }
+}
+
+impl<S> PositionComponent<S> {
+ /// `0%`
+ pub fn zero() -> Self {
+ PositionComponent::Length(LengthPercentage::Percentage(Percentage::zero()))
+ }
+
+ /// Returns the count of this component.
+ fn component_count(&self) -> usize {
+ match *self {
+ PositionComponent::Length(..) | PositionComponent::Center => 1,
+ PositionComponent::Side(_, ref lp) => {
+ if lp.is_some() {
+ 2
+ } else {
+ 1
+ }
+ },
+ }
+ }
+}
+
+impl<S: Side> ToComputedValue for PositionComponent<S> {
+ type ComputedValue = ComputedLengthPercentage;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ PositionComponent::Center => ComputedLengthPercentage::new_percent(Percentage(0.5)),
+ PositionComponent::Side(ref keyword, None) => {
+ let p = Percentage(if keyword.is_start() { 0. } else { 1. });
+ ComputedLengthPercentage::new_percent(p)
+ },
+ PositionComponent::Side(ref keyword, Some(ref length)) if !keyword.is_start() => {
+ let length = length.to_computed_value(context);
+ // We represent `<end-side> <length>` as `calc(100% - <length>)`.
+ ComputedLengthPercentage::hundred_percent_minus(length, AllowedNumericType::All)
+ },
+ PositionComponent::Side(_, Some(ref length)) |
+ PositionComponent::Length(ref length) => length.to_computed_value(context),
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ PositionComponent::Length(ToComputedValue::from_computed_value(computed))
+ }
+}
+
+impl<S: Side> PositionComponent<S> {
+ /// The initial specified value of a position component, i.e. the start side.
+ pub fn initial_specified_value() -> Self {
+ PositionComponent::Side(S::start(), None)
+ }
+}
+
+/// Represents a side, either horizontal or vertical, of a CSS position.
+pub trait Side {
+ /// Returns the start side.
+ fn start() -> Self;
+
+ /// Returns whether this side is the start side.
+ fn is_start(&self) -> bool;
+}
+
+impl Side for HorizontalPositionKeyword {
+ #[inline]
+ fn start() -> Self {
+ HorizontalPositionKeyword::Left
+ }
+
+ #[inline]
+ fn is_start(&self) -> bool {
+ *self == Self::start()
+ }
+}
+
+impl Side for VerticalPositionKeyword {
+ #[inline]
+ fn start() -> Self {
+ VerticalPositionKeyword::Top
+ }
+
+ #[inline]
+ fn is_start(&self) -> bool {
+ *self == Self::start()
+ }
+}
+
+/// Controls how the auto-placement algorithm works specifying exactly how auto-placed items
+/// get flowed into the grid.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "row,column,dense")]
+#[repr(C)]
+pub struct GridAutoFlow(u8);
+bitflags! {
+ impl GridAutoFlow: u8 {
+ /// 'row' - mutually exclusive with 'column'
+ const ROW = 1 << 0;
+ /// 'column' - mutually exclusive with 'row'
+ const COLUMN = 1 << 1;
+ /// 'dense'
+ const DENSE = 1 << 2;
+ }
+}
+
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+/// Masonry auto-placement algorithm packing.
+pub enum MasonryPlacement {
+ /// Place the item in the track(s) with the smallest extent so far.
+ Pack,
+ /// Place the item after the last item, from start to end.
+ Next,
+}
+
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+/// Masonry auto-placement algorithm item sorting option.
+pub enum MasonryItemOrder {
+ /// Place all items with a definite placement before auto-placed items.
+ DefiniteFirst,
+ /// Place items in `order-modified document order`.
+ Ordered,
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Controls how the Masonry layout algorithm works
+/// specifying exactly how auto-placed items get flowed in the masonry axis.
+pub struct MasonryAutoFlow {
+ /// Specify how to pick a auto-placement track.
+ #[css(contextual_skip_if = "is_pack_with_non_default_order")]
+ pub placement: MasonryPlacement,
+ /// Specify how to pick an item to place.
+ #[css(skip_if = "is_item_order_definite_first")]
+ pub order: MasonryItemOrder,
+}
+
+#[inline]
+fn is_pack_with_non_default_order(placement: &MasonryPlacement, order: &MasonryItemOrder) -> bool {
+ *placement == MasonryPlacement::Pack && *order != MasonryItemOrder::DefiniteFirst
+}
+
+#[inline]
+fn is_item_order_definite_first(order: &MasonryItemOrder) -> bool {
+ *order == MasonryItemOrder::DefiniteFirst
+}
+
+impl MasonryAutoFlow {
+ #[inline]
+ /// Get initial `masonry-auto-flow` value.
+ pub fn initial() -> MasonryAutoFlow {
+ MasonryAutoFlow {
+ placement: MasonryPlacement::Pack,
+ order: MasonryItemOrder::DefiniteFirst,
+ }
+ }
+}
+
+impl Parse for MasonryAutoFlow {
+ /// [ definite-first | ordered ] || [ pack | next ]
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MasonryAutoFlow, ParseError<'i>> {
+ let mut value = MasonryAutoFlow::initial();
+ let mut got_placement = false;
+ let mut got_order = false;
+ while !input.is_exhausted() {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ let success = match_ignore_ascii_case! { &ident,
+ "pack" if !got_placement => {
+ got_placement = true;
+ true
+ },
+ "next" if !got_placement => {
+ value.placement = MasonryPlacement::Next;
+ got_placement = true;
+ true
+ },
+ "definite-first" if !got_order => {
+ got_order = true;
+ true
+ },
+ "ordered" if !got_order => {
+ value.order = MasonryItemOrder::Ordered;
+ got_order = true;
+ true
+ },
+ _ => false
+ };
+ if !success {
+ return Err(location
+ .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())));
+ }
+ }
+
+ if got_placement || got_order {
+ Ok(value)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+// TODO: Can be derived with some care.
+impl Parse for GridAutoFlow {
+ /// [ row | column ] || dense
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<GridAutoFlow, ParseError<'i>> {
+ let mut track = None;
+ let mut dense = GridAutoFlow::empty();
+
+ while !input.is_exhausted() {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+ let success = match_ignore_ascii_case! { &ident,
+ "row" if track.is_none() => {
+ track = Some(GridAutoFlow::ROW);
+ true
+ },
+ "column" if track.is_none() => {
+ track = Some(GridAutoFlow::COLUMN);
+ true
+ },
+ "dense" if dense.is_empty() => {
+ dense = GridAutoFlow::DENSE;
+ true
+ },
+ _ => false,
+ };
+ if !success {
+ return Err(location
+ .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())));
+ }
+ }
+
+ if track.is_some() || !dense.is_empty() {
+ Ok(track.unwrap_or(GridAutoFlow::ROW) | dense)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+impl ToCss for GridAutoFlow {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if *self == GridAutoFlow::ROW {
+ return dest.write_str("row");
+ }
+
+ if *self == GridAutoFlow::COLUMN {
+ return dest.write_str("column");
+ }
+
+ if *self == GridAutoFlow::ROW | GridAutoFlow::DENSE {
+ return dest.write_str("dense");
+ }
+
+ if *self == GridAutoFlow::COLUMN | GridAutoFlow::DENSE {
+ return dest.write_str("column dense");
+ }
+
+ debug_assert!(false, "Unknown or invalid grid-autoflow value");
+ Ok(())
+ }
+}
+
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// https://drafts.csswg.org/css-grid/#named-grid-area
+pub struct TemplateAreas {
+ /// `named area` containing for each template area
+ #[css(skip)]
+ pub areas: crate::OwnedSlice<NamedArea>,
+ /// The simplified CSS strings for serialization purpose.
+ /// https://drafts.csswg.org/css-grid/#serialize-template
+ // Note: We also use the length of `strings` when computing the explicit grid end line number
+ // (i.e. row number).
+ #[css(iterable)]
+ pub strings: crate::OwnedSlice<crate::OwnedStr>,
+ /// The number of columns of the grid.
+ #[css(skip)]
+ pub width: u32,
+}
+
+/// Parser for grid template areas.
+#[derive(Default)]
+pub struct TemplateAreasParser {
+ areas: Vec<NamedArea>,
+ area_indices: PrecomputedHashMap<Atom, usize>,
+ strings: Vec<crate::OwnedStr>,
+ width: u32,
+ row: u32,
+}
+
+impl TemplateAreasParser {
+ /// Parse a single string.
+ pub fn try_parse_string<'i>(
+ &mut self,
+ input: &mut Parser<'i, '_>,
+ ) -> Result<(), ParseError<'i>> {
+ input.try_parse(|input| {
+ self.parse_string(input.expect_string()?)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ })
+ }
+
+ /// Parse a single string.
+ fn parse_string(&mut self, string: &str) -> Result<(), ()> {
+ self.row += 1;
+ let mut simplified_string = String::new();
+ let mut current_area_index: Option<usize> = None;
+ let mut column = 0u32;
+ for token in TemplateAreasTokenizer(string) {
+ column += 1;
+ if column > 1 {
+ simplified_string.push(' ');
+ }
+ let name = if let Some(token) = token? {
+ simplified_string.push_str(token);
+ Atom::from(token)
+ } else {
+ if let Some(index) = current_area_index.take() {
+ if self.areas[index].columns.end != column {
+ return Err(());
+ }
+ }
+ simplified_string.push('.');
+ continue;
+ };
+ if let Some(index) = current_area_index {
+ if self.areas[index].name == name {
+ if self.areas[index].rows.start == self.row {
+ self.areas[index].columns.end += 1;
+ }
+ continue;
+ }
+ if self.areas[index].columns.end != column {
+ return Err(());
+ }
+ }
+ match self.area_indices.entry(name) {
+ Entry::Occupied(ref e) => {
+ let index = *e.get();
+ if self.areas[index].columns.start != column ||
+ self.areas[index].rows.end != self.row
+ {
+ return Err(());
+ }
+ self.areas[index].rows.end += 1;
+ current_area_index = Some(index);
+ },
+ Entry::Vacant(v) => {
+ let index = self.areas.len();
+ let name = v.key().clone();
+ v.insert(index);
+ self.areas.push(NamedArea {
+ name,
+ columns: UnsignedRange {
+ start: column,
+ end: column + 1,
+ },
+ rows: UnsignedRange {
+ start: self.row,
+ end: self.row + 1,
+ },
+ });
+ current_area_index = Some(index);
+ },
+ }
+ }
+ if column == 0 {
+ // Each string must produce a valid token.
+ // https://github.com/w3c/csswg-drafts/issues/5110
+ return Err(());
+ }
+ if let Some(index) = current_area_index {
+ if self.areas[index].columns.end != column + 1 {
+ debug_assert_ne!(self.areas[index].rows.start, self.row);
+ return Err(());
+ }
+ }
+ if self.row == 1 {
+ self.width = column;
+ } else if self.width != column {
+ return Err(());
+ }
+
+ self.strings.push(simplified_string.into());
+ Ok(())
+ }
+
+ /// Return the parsed template areas.
+ pub fn finish(self) -> Result<TemplateAreas, ()> {
+ if self.strings.is_empty() {
+ return Err(());
+ }
+ Ok(TemplateAreas {
+ areas: self.areas.into(),
+ strings: self.strings.into(),
+ width: self.width,
+ })
+ }
+}
+
+impl TemplateAreas {
+ fn parse_internal(input: &mut Parser) -> Result<Self, ()> {
+ let mut parser = TemplateAreasParser::default();
+ while parser.try_parse_string(input).is_ok() {}
+ parser.finish()
+ }
+}
+
+impl Parse for TemplateAreas {
+ fn parse<'i, 't>(
+ _: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_internal(input)
+ .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+}
+
+/// Arc type for `Arc<TemplateAreas>`
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct TemplateAreasArc(#[ignore_malloc_size_of = "Arc"] pub Arc<TemplateAreas>);
+
+impl Parse for TemplateAreasArc {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let parsed = TemplateAreas::parse(context, input)?;
+ Ok(TemplateAreasArc(Arc::new(parsed)))
+ }
+}
+
+/// A range of rows or columns. Using this instead of std::ops::Range for FFI
+/// purposes.
+#[repr(C)]
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub struct UnsignedRange {
+ /// The start of the range.
+ pub start: u32,
+ /// The end of the range.
+ pub end: u32,
+}
+
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Not associated with any particular grid item, but can be referenced from the
+/// grid-placement properties.
+pub struct NamedArea {
+ /// Name of the `named area`
+ pub name: Atom,
+ /// Rows of the `named area`
+ pub rows: UnsignedRange,
+ /// Columns of the `named area`
+ pub columns: UnsignedRange,
+}
+
+/// Tokenize the string into a list of the tokens,
+/// using longest-match semantics
+struct TemplateAreasTokenizer<'a>(&'a str);
+
+impl<'a> Iterator for TemplateAreasTokenizer<'a> {
+ type Item = Result<Option<&'a str>, ()>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ let rest = self.0.trim_start_matches(HTML_SPACE_CHARACTERS);
+ if rest.is_empty() {
+ return None;
+ }
+ if rest.starts_with('.') {
+ self.0 = &rest[rest.find(|c| c != '.').unwrap_or(rest.len())..];
+ return Some(Ok(None));
+ }
+ if !rest.starts_with(is_name_code_point) {
+ return Some(Err(()));
+ }
+ let token_len = rest.find(|c| !is_name_code_point(c)).unwrap_or(rest.len());
+ let token = &rest[..token_len];
+ self.0 = &rest[token_len..];
+ Some(Ok(Some(token)))
+ }
+}
+
+fn is_name_code_point(c: char) -> bool {
+ c >= 'A' && c <= 'Z' ||
+ c >= 'a' && c <= 'z' ||
+ c >= '\u{80}' ||
+ c == '_' ||
+ c >= '0' && c <= '9' ||
+ c == '-'
+}
+
+/// This property specifies named grid areas.
+///
+/// The syntax of this property also provides a visualization of the structure
+/// of the grid, making the overall layout of the grid container easier to
+/// understand.
+#[repr(C, u8)]
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+pub enum GridTemplateAreas {
+ /// The `none` value.
+ None,
+ /// The actual value.
+ Areas(TemplateAreasArc),
+}
+
+impl GridTemplateAreas {
+ #[inline]
+ /// Get default value as `none`
+ pub fn none() -> GridTemplateAreas {
+ GridTemplateAreas::None
+ }
+}
+
+/// A specified value for the `z-index` property.
+pub type ZIndex = GenericZIndex<Integer>;
+
+/// A specified value for the `aspect-ratio` property.
+pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>;
+
+impl Parse for AspectRatio {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::values::generics::position::PreferredRatio;
+ use crate::values::specified::Ratio;
+
+ let location = input.current_source_location();
+ let mut auto = input.try_parse(|i| i.expect_ident_matching("auto"));
+ let ratio = input.try_parse(|i| Ratio::parse(context, i));
+ if auto.is_err() {
+ auto = input.try_parse(|i| i.expect_ident_matching("auto"));
+ }
+
+ if auto.is_err() && ratio.is_err() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(AspectRatio {
+ auto: auto.is_ok(),
+ ratio: match ratio {
+ Ok(ratio) => PreferredRatio::Ratio(ratio),
+ Err(..) => PreferredRatio::None,
+ },
+ })
+ }
+}
+
+impl AspectRatio {
+ /// Returns Self by a valid ratio.
+ pub fn from_mapped_ratio(w: f32, h: f32) -> Self {
+ use crate::values::generics::position::PreferredRatio;
+ use crate::values::generics::ratio::Ratio;
+ AspectRatio {
+ auto: true,
+ ratio: PreferredRatio::Ratio(Ratio(
+ NonNegativeNumber::new(w),
+ NonNegativeNumber::new(h),
+ )),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/ratio.rs b/servo/components/style/values/specified/ratio.rs
new file mode 100644
index 0000000000..4cdddd452e
--- /dev/null
+++ b/servo/components/style/values/specified/ratio.rs
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for <ratio>.
+//!
+//! [ratio]: https://drafts.csswg.org/css-values/#ratios
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::ratio::Ratio as GenericRatio;
+use crate::values::specified::NonNegativeNumber;
+use crate::One;
+use cssparser::Parser;
+use style_traits::ParseError;
+
+/// A specified <ratio> value.
+pub type Ratio = GenericRatio<NonNegativeNumber>;
+
+impl Parse for Ratio {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let a = NonNegativeNumber::parse(context, input)?;
+ let b = match input.try_parse(|input| input.expect_delim('/')) {
+ Ok(()) => NonNegativeNumber::parse(context, input)?,
+ _ => One::one(),
+ };
+
+ Ok(GenericRatio(a, b))
+ }
+}
diff --git a/servo/components/style/values/specified/rect.rs b/servo/components/style/values/specified/rect.rs
new file mode 100644
index 0000000000..7955ecaa48
--- /dev/null
+++ b/servo/components/style/values/specified/rect.rs
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS borders.
+
+use crate::values::generics::rect::Rect;
+use crate::values::specified::length::NonNegativeLengthOrNumber;
+
+/// A specified rectangle made of four `<length-or-number>` values.
+pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>;
diff --git a/servo/components/style/values/specified/resolution.rs b/servo/components/style/values/specified/resolution.rs
new file mode 100644
index 0000000000..74f100972a
--- /dev/null
+++ b/servo/components/style/values/specified/resolution.rs
@@ -0,0 +1,141 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Resolution values:
+//!
+//! https://drafts.csswg.org/css-values/#resolution
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::specified::CalcNode;
+use crate::values::CSSFloat;
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// A specified resolution.
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)]
+pub struct Resolution {
+ value: CSSFloat,
+ unit: ResolutionUnit,
+ was_calc: bool,
+}
+
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+enum ResolutionUnit {
+ /// Dots per inch.
+ Dpi,
+ /// An alias unit for dots per pixel.
+ X,
+ /// Dots per pixel.
+ Dppx,
+ /// Dots per centimeter.
+ Dpcm,
+}
+
+impl ResolutionUnit {
+ fn as_str(self) -> &'static str {
+ match self {
+ Self::Dpi => "dpi",
+ Self::X => "x",
+ Self::Dppx => "dppx",
+ Self::Dpcm => "dpcm",
+ }
+ }
+}
+
+impl Resolution {
+ /// Returns a resolution value from dppx units.
+ pub fn from_dppx(value: CSSFloat) -> Self {
+ Self {
+ value,
+ unit: ResolutionUnit::Dppx,
+ was_calc: false,
+ }
+ }
+
+ /// Returns a resolution value from dppx units.
+ pub fn from_x(value: CSSFloat) -> Self {
+ Self {
+ value,
+ unit: ResolutionUnit::X,
+ was_calc: false,
+ }
+ }
+
+ /// Returns a resolution value from dppx units.
+ pub fn from_dppx_calc(value: CSSFloat) -> Self {
+ Self {
+ value,
+ unit: ResolutionUnit::Dppx,
+ was_calc: true,
+ }
+ }
+
+ /// Convert this resolution value to dppx units.
+ pub fn dppx(&self) -> CSSFloat {
+ match self.unit {
+ ResolutionUnit::X | ResolutionUnit::Dppx => self.value,
+ _ => self.dpi() / 96.0,
+ }
+ }
+
+ /// Convert this resolution value to dpi units.
+ pub fn dpi(&self) -> CSSFloat {
+ match self.unit {
+ ResolutionUnit::Dpi => self.value,
+ ResolutionUnit::X | ResolutionUnit::Dppx => self.value * 96.0,
+ ResolutionUnit::Dpcm => self.value * 2.54,
+ }
+ }
+
+ /// Parse a resolution given a value and unit.
+ pub fn parse_dimension<'i, 't>(value: CSSFloat, unit: &str) -> Result<Self, ()> {
+ let unit = match_ignore_ascii_case! { &unit,
+ "dpi" => ResolutionUnit::Dpi,
+ "dppx" => ResolutionUnit::Dppx,
+ "dpcm" => ResolutionUnit::Dpcm,
+ "x" => ResolutionUnit::X,
+ _ => return Err(())
+ };
+ Ok(Self {
+ value,
+ unit,
+ was_calc: false,
+ })
+ }
+}
+
+impl ToCss for Resolution {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.value,
+ self.unit.as_str(),
+ self.was_calc,
+ dest,
+ )
+ }
+}
+
+impl Parse for Resolution {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ match *input.next()? {
+ Token::Dimension {
+ value, ref unit, ..
+ } if value >= 0. => Self::parse_dimension(value, unit)
+ .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse_resolution(context, input, function)
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/source_size_list.rs b/servo/components/style/values/specified/source_size_list.rs
new file mode 100644
index 0000000000..ac47461cc4
--- /dev/null
+++ b/servo/components/style/values/specified/source_size_list.rs
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! https://html.spec.whatwg.org/multipage/#source-size-list
+
+use crate::media_queries::Device;
+use crate::parser::{Parse, ParserContext};
+use crate::queries::{FeatureType, QueryCondition};
+use crate::values::computed::{self, ToComputedValue};
+use crate::values::specified::{Length, NoCalcLength, ViewportPercentageLength};
+use app_units::Au;
+use cssparser::{Delimiter, Parser, Token};
+use selectors::context::QuirksMode;
+use style_traits::ParseError;
+
+/// A value for a `<source-size>`:
+///
+/// https://html.spec.whatwg.org/multipage/#source-size
+#[derive(Debug)]
+pub struct SourceSize {
+ condition: QueryCondition,
+ value: Length,
+}
+
+impl Parse for SourceSize {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let condition = QueryCondition::parse(context, input, FeatureType::Media)?;
+ let value = Length::parse_non_negative(context, input)?;
+ Ok(Self { condition, value })
+ }
+}
+
+/// A value for a `<source-size-list>`:
+///
+/// https://html.spec.whatwg.org/multipage/#source-size-list
+#[derive(Debug)]
+pub struct SourceSizeList {
+ source_sizes: Vec<SourceSize>,
+ value: Option<Length>,
+}
+
+impl SourceSizeList {
+ /// Create an empty `SourceSizeList`, which can be used as a fall-back.
+ pub fn empty() -> Self {
+ Self {
+ source_sizes: vec![],
+ value: None,
+ }
+ }
+
+ /// Evaluate this <source-size-list> to get the final viewport length.
+ pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> Au {
+ computed::Context::for_media_query_evaluation(device, quirks_mode, |context| {
+ let matching_source_size = self.source_sizes.iter().find(|source_size| {
+ source_size
+ .condition
+ .matches(context)
+ .to_bool(/* unknown = */ false)
+ });
+
+ match matching_source_size {
+ Some(source_size) => source_size.value.to_computed_value(context),
+ None => match self.value {
+ Some(ref v) => v.to_computed_value(context),
+ None => Length::NoCalc(NoCalcLength::ViewportPercentage(
+ ViewportPercentageLength::Vw(100.),
+ ))
+ .to_computed_value(context),
+ },
+ }
+ })
+ .into()
+ }
+}
+
+enum SourceSizeOrLength {
+ SourceSize(SourceSize),
+ Length(Length),
+}
+
+impl Parse for SourceSizeOrLength {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(size) = input.try_parse(|input| SourceSize::parse(context, input)) {
+ return Ok(SourceSizeOrLength::SourceSize(size));
+ }
+
+ let length = Length::parse_non_negative(context, input)?;
+ Ok(SourceSizeOrLength::Length(length))
+ }
+}
+
+impl SourceSizeList {
+ /// NOTE(emilio): This doesn't match the grammar in the spec, see:
+ ///
+ /// https://html.spec.whatwg.org/multipage/#parsing-a-sizes-attribute
+ pub fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Self {
+ let mut source_sizes = vec![];
+
+ loop {
+ let result = input.parse_until_before(Delimiter::Comma, |input| {
+ SourceSizeOrLength::parse(context, input)
+ });
+
+ match result {
+ Ok(SourceSizeOrLength::Length(value)) => {
+ return Self {
+ source_sizes,
+ value: Some(value),
+ };
+ },
+ Ok(SourceSizeOrLength::SourceSize(source_size)) => {
+ source_sizes.push(source_size);
+ },
+ Err(..) => {},
+ }
+
+ match input.next() {
+ Ok(&Token::Comma) => {},
+ Err(..) => break,
+ _ => unreachable!(),
+ }
+ }
+
+ SourceSizeList {
+ source_sizes,
+ value: None,
+ }
+ }
+}
diff --git a/servo/components/style/values/specified/svg.rs b/servo/components/style/values/specified/svg.rs
new file mode 100644
index 0000000000..8ab2dbb223
--- /dev/null
+++ b/servo/components/style/values/specified/svg.rs
@@ -0,0 +1,404 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for SVG properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::svg as generic;
+use crate::values::specified::color::Color;
+use crate::values::specified::url::SpecifiedUrl;
+use crate::values::specified::AllowQuirks;
+use crate::values::specified::LengthPercentage;
+use crate::values::specified::SVGPathData;
+use crate::values::specified::{NonNegativeLengthPercentage, Opacity};
+use crate::values::CustomIdent;
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::{CommaWithSpace, CssWriter, ParseError, Separator};
+use style_traits::{StyleParseErrorKind, ToCss};
+
+/// Specified SVG Paint value
+pub type SVGPaint = generic::GenericSVGPaint<Color, SpecifiedUrl>;
+
+/// <length> | <percentage> | <number> | context-value
+pub type SVGLength = generic::GenericSVGLength<LengthPercentage>;
+
+/// A non-negative version of SVGLength.
+pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>;
+
+/// [ <length> | <percentage> | <number> ]# | context-value
+pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>;
+
+/// Whether the `context-value` value is enabled.
+#[cfg(feature = "gecko")]
+pub fn is_context_value_enabled() -> bool {
+ static_prefs::pref!("gfx.font_rendering.opentype_svg.enabled")
+}
+
+/// Whether the `context-value` value is enabled.
+#[cfg(not(feature = "gecko"))]
+pub fn is_context_value_enabled() -> bool {
+ false
+}
+
+macro_rules! parse_svg_length {
+ ($ty:ty, $lp:ty) => {
+ impl Parse for $ty {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(lp) =
+ input.try_parse(|i| <$lp>::parse_quirky(context, i, AllowQuirks::Always))
+ {
+ return Ok(generic::SVGLength::LengthPercentage(lp));
+ }
+
+ try_match_ident_ignore_ascii_case! { input,
+ "context-value" if is_context_value_enabled() => {
+ Ok(generic::SVGLength::ContextValue)
+ },
+ }
+ }
+ }
+ };
+}
+
+parse_svg_length!(SVGLength, LengthPercentage);
+parse_svg_length!(SVGWidth, NonNegativeLengthPercentage);
+
+impl Parse for SVGStrokeDashArray {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if let Ok(values) = input.try_parse(|i| {
+ CommaWithSpace::parse(i, |i| {
+ NonNegativeLengthPercentage::parse_quirky(context, i, AllowQuirks::Always)
+ })
+ }) {
+ return Ok(generic::SVGStrokeDashArray::Values(values.into()));
+ }
+
+ try_match_ident_ignore_ascii_case! { input,
+ "context-value" if is_context_value_enabled() => {
+ Ok(generic::SVGStrokeDashArray::ContextValue)
+ },
+ "none" => Ok(generic::SVGStrokeDashArray::Values(Default::default())),
+ }
+ }
+}
+
+/// <opacity-value> | context-fill-opacity | context-stroke-opacity
+pub type SVGOpacity = generic::SVGOpacity<Opacity>;
+
+/// The specified value for a single CSS paint-order property.
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, ToCss)]
+pub enum PaintOrder {
+ /// `normal` variant
+ Normal = 0,
+ /// `fill` variant
+ Fill = 1,
+ /// `stroke` variant
+ Stroke = 2,
+ /// `markers` variant
+ Markers = 3,
+}
+
+/// Number of non-normal components
+pub const PAINT_ORDER_COUNT: u8 = 3;
+
+/// Number of bits for each component
+pub const PAINT_ORDER_SHIFT: u8 = 2;
+
+/// Mask with above bits set
+pub const PAINT_ORDER_MASK: u8 = 0b11;
+
+/// The specified value is tree `PaintOrder` values packed into the
+/// bitfields below, as a six-bit field, of 3 two-bit pairs
+///
+/// Each pair can be set to FILL, STROKE, or MARKERS
+/// Lowest significant bit pairs are highest priority.
+/// `normal` is the empty bitfield. The three pairs are
+/// never zero in any case other than `normal`.
+///
+/// Higher priority values, i.e. the values specified first,
+/// will be painted first (and may be covered by paintings of lower priority)
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct SVGPaintOrder(pub u8);
+
+impl SVGPaintOrder {
+ /// Get default `paint-order` with `0`
+ pub fn normal() -> Self {
+ SVGPaintOrder(0)
+ }
+
+ /// Get variant of `paint-order`
+ pub fn order_at(&self, pos: u8) -> PaintOrder {
+ match (self.0 >> pos * PAINT_ORDER_SHIFT) & PAINT_ORDER_MASK {
+ 0 => PaintOrder::Normal,
+ 1 => PaintOrder::Fill,
+ 2 => PaintOrder::Stroke,
+ 3 => PaintOrder::Markers,
+ _ => unreachable!("this cannot happen"),
+ }
+ }
+}
+
+impl Parse for SVGPaintOrder {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<SVGPaintOrder, ParseError<'i>> {
+ if let Ok(()) = input.try_parse(|i| i.expect_ident_matching("normal")) {
+ return Ok(SVGPaintOrder::normal());
+ }
+
+ let mut value = 0;
+ // bitfield representing what we've seen so far
+ // bit 1 is fill, bit 2 is stroke, bit 3 is markers
+ let mut seen = 0;
+ let mut pos = 0;
+
+ loop {
+ let result: Result<_, ParseError> = input.try_parse(|input| {
+ try_match_ident_ignore_ascii_case! { input,
+ "fill" => Ok(PaintOrder::Fill),
+ "stroke" => Ok(PaintOrder::Stroke),
+ "markers" => Ok(PaintOrder::Markers),
+ }
+ });
+
+ match result {
+ Ok(val) => {
+ if (seen & (1 << val as u8)) != 0 {
+ // don't parse the same ident twice
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ value |= (val as u8) << (pos * PAINT_ORDER_SHIFT);
+ seen |= 1 << (val as u8);
+ pos += 1;
+ },
+ Err(_) => break,
+ }
+ }
+
+ if value == 0 {
+ // Couldn't find any keyword
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ // fill in rest
+ for i in pos..PAINT_ORDER_COUNT {
+ for paint in 1..(PAINT_ORDER_COUNT + 1) {
+ // if not seen, set bit at position, mark as seen
+ if (seen & (1 << paint)) == 0 {
+ seen |= 1 << paint;
+ value |= paint << (i * PAINT_ORDER_SHIFT);
+ break;
+ }
+ }
+ }
+
+ Ok(SVGPaintOrder(value))
+ }
+}
+
+impl ToCss for SVGPaintOrder {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.0 == 0 {
+ return dest.write_str("normal");
+ }
+
+ let mut last_pos_to_serialize = 0;
+ for i in (1..PAINT_ORDER_COUNT).rev() {
+ let component = self.order_at(i);
+ let earlier_component = self.order_at(i - 1);
+ if component < earlier_component {
+ last_pos_to_serialize = i - 1;
+ break;
+ }
+ }
+
+ for pos in 0..last_pos_to_serialize + 1 {
+ if pos != 0 {
+ dest.write_char(' ')?
+ }
+ self.order_at(pos).to_css(dest)?;
+ }
+ Ok(())
+ }
+}
+
+/// The context properties we understand.
+#[derive(
+ Clone,
+ Copy,
+ Eq,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ContextPropertyBits(u8);
+bitflags! {
+ impl ContextPropertyBits: u8 {
+ /// `fill`
+ const FILL = 1 << 0;
+ /// `stroke`
+ const STROKE = 1 << 1;
+ /// `fill-opacity`
+ const FILL_OPACITY = 1 << 2;
+ /// `stroke-opacity`
+ const STROKE_OPACITY = 1 << 3;
+ }
+}
+
+/// Specified MozContextProperties value.
+/// Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-context-properties)
+#[derive(
+ Clone,
+ Debug,
+ Default,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct MozContextProperties {
+ #[css(iterable, if_empty = "none")]
+ #[ignore_malloc_size_of = "Arc"]
+ idents: crate::ArcSlice<CustomIdent>,
+ #[css(skip)]
+ bits: ContextPropertyBits,
+}
+
+impl Parse for MozContextProperties {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<MozContextProperties, ParseError<'i>> {
+ let mut values = vec![];
+ let mut bits = ContextPropertyBits::empty();
+ loop {
+ {
+ let location = input.current_source_location();
+ let ident = input.expect_ident()?;
+
+ if ident.eq_ignore_ascii_case("none") && values.is_empty() {
+ return Ok(Self::default());
+ }
+
+ let ident = CustomIdent::from_ident(location, ident, &["all", "none", "auto"])?;
+
+ if ident.0 == atom!("fill") {
+ bits.insert(ContextPropertyBits::FILL);
+ } else if ident.0 == atom!("stroke") {
+ bits.insert(ContextPropertyBits::STROKE);
+ } else if ident.0 == atom!("fill-opacity") {
+ bits.insert(ContextPropertyBits::FILL_OPACITY);
+ } else if ident.0 == atom!("stroke-opacity") {
+ bits.insert(ContextPropertyBits::STROKE_OPACITY);
+ }
+
+ values.push(ident);
+ }
+
+ let location = input.current_source_location();
+ match input.next() {
+ Ok(&Token::Comma) => continue,
+ Err(..) => break,
+ Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
+ }
+ }
+
+ if values.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(MozContextProperties {
+ idents: crate::ArcSlice::from_iter(values.into_iter()),
+ bits,
+ })
+ }
+}
+
+/// The svg d property type.
+///
+/// https://svgwg.org/svg2-draft/paths.html#TheDProperty
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum DProperty {
+ /// Path value for path(<string>) or just a <string>.
+ #[css(function)]
+ Path(SVGPathData),
+ /// None value.
+ #[animation(error)]
+ None,
+}
+
+impl DProperty {
+ /// return none.
+ #[inline]
+ pub fn none() -> Self {
+ DProperty::None
+ }
+}
+
+impl Parse for DProperty {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Parse none.
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(DProperty::none());
+ }
+
+ // Parse possible functions.
+ input.expect_function_matching("path")?;
+ let path_data = input.parse_nested_block(|i| Parse::parse(context, i))?;
+ Ok(DProperty::Path(path_data))
+ }
+}
diff --git a/servo/components/style/values/specified/svg_path.rs b/servo/components/style/values/specified/svg_path.rs
new file mode 100644
index 0000000000..1eb9866dd1
--- /dev/null
+++ b/servo/components/style/values/specified/svg_path.rs
@@ -0,0 +1,1029 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for SVG Path.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::animated::{lists, Animate, Procedure, ToAnimatedZero};
+use crate::values::distance::{ComputeSquaredDistance, SquaredDistance};
+use crate::values::CSSFloat;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use std::iter::{Cloned, Peekable};
+use std::slice;
+use style_traits::values::SequenceWriter;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+
+/// Whether to allow empty string in the parser.
+#[derive(Clone, Debug, Eq, PartialEq)]
+#[allow(missing_docs)]
+pub enum AllowEmpty {
+ Yes,
+ No,
+}
+
+/// The SVG path data.
+///
+/// https://www.w3.org/TR/SVG11/paths.html#PathData
+#[derive(
+ Clone,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct SVGPathData(
+ // TODO(emilio): Should probably measure this somehow only from the
+ // specified values.
+ #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice<PathCommand>,
+);
+
+impl SVGPathData {
+ /// Get the array of PathCommand.
+ #[inline]
+ pub fn commands(&self) -> &[PathCommand] {
+ &self.0
+ }
+
+ /// Create a normalized copy of this path by converting each relative
+ /// command to an absolute command.
+ pub fn normalize(&self) -> Self {
+ let mut state = PathTraversalState {
+ subpath_start: CoordPair::new(0.0, 0.0),
+ pos: CoordPair::new(0.0, 0.0),
+ };
+ let iter = self.0.iter().map(|seg| seg.normalize(&mut state));
+ SVGPathData(crate::ArcSlice::from_iter(iter))
+ }
+
+ // FIXME: Bug 1714238, we may drop this once we use the same data structure for both SVG and
+ // CSS.
+ /// Decode the svg path raw data from Gecko.
+ #[cfg(feature = "gecko")]
+ pub fn decode_from_f32_array(path: &[f32]) -> Result<Self, ()> {
+ use crate::gecko_bindings::structs::dom::SVGPathSeg_Binding::*;
+
+ let mut result: Vec<PathCommand> = Vec::new();
+ let mut i: usize = 0;
+ while i < path.len() {
+ // See EncodeType() and DecodeType() in SVGPathSegUtils.h.
+ // We are using reinterpret_cast<> to encode and decode between u32 and f32, so here we
+ // use to_bits() to decode the type.
+ let seg_type = path[i].to_bits() as u16;
+ i = i + 1;
+ match seg_type {
+ PATHSEG_CLOSEPATH => result.push(PathCommand::ClosePath),
+ PATHSEG_MOVETO_ABS | PATHSEG_MOVETO_REL => {
+ debug_assert!(i + 1 < path.len());
+ result.push(PathCommand::MoveTo {
+ point: CoordPair::new(path[i], path[i + 1]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_MOVETO_ABS),
+ });
+ i = i + 2;
+ },
+ PATHSEG_LINETO_ABS | PATHSEG_LINETO_REL => {
+ debug_assert!(i + 1 < path.len());
+ result.push(PathCommand::LineTo {
+ point: CoordPair::new(path[i], path[i + 1]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_ABS),
+ });
+ i = i + 2;
+ },
+ PATHSEG_CURVETO_CUBIC_ABS | PATHSEG_CURVETO_CUBIC_REL => {
+ debug_assert!(i + 5 < path.len());
+ result.push(PathCommand::CurveTo {
+ control1: CoordPair::new(path[i], path[i + 1]),
+ control2: CoordPair::new(path[i + 2], path[i + 3]),
+ point: CoordPair::new(path[i + 4], path[i + 5]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_ABS),
+ });
+ i = i + 6;
+ },
+ PATHSEG_CURVETO_QUADRATIC_ABS | PATHSEG_CURVETO_QUADRATIC_REL => {
+ debug_assert!(i + 3 < path.len());
+ result.push(PathCommand::QuadBezierCurveTo {
+ control1: CoordPair::new(path[i], path[i + 1]),
+ point: CoordPair::new(path[i + 2], path[i + 3]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_ABS),
+ });
+ i = i + 4;
+ },
+ PATHSEG_ARC_ABS | PATHSEG_ARC_REL => {
+ debug_assert!(i + 6 < path.len());
+ result.push(PathCommand::EllipticalArc {
+ rx: path[i],
+ ry: path[i + 1],
+ angle: path[i + 2],
+ large_arc_flag: ArcFlag(path[i + 3] != 0.0f32),
+ sweep_flag: ArcFlag(path[i + 4] != 0.0f32),
+ point: CoordPair::new(path[i + 5], path[i + 6]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_ARC_ABS),
+ });
+ i = i + 7;
+ },
+ PATHSEG_LINETO_HORIZONTAL_ABS | PATHSEG_LINETO_HORIZONTAL_REL => {
+ debug_assert!(i < path.len());
+ result.push(PathCommand::HorizontalLineTo {
+ x: path[i],
+ absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_HORIZONTAL_ABS),
+ });
+ i = i + 1;
+ },
+ PATHSEG_LINETO_VERTICAL_ABS | PATHSEG_LINETO_VERTICAL_REL => {
+ debug_assert!(i < path.len());
+ result.push(PathCommand::VerticalLineTo {
+ y: path[i],
+ absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_VERTICAL_ABS),
+ });
+ i = i + 1;
+ },
+ PATHSEG_CURVETO_CUBIC_SMOOTH_ABS | PATHSEG_CURVETO_CUBIC_SMOOTH_REL => {
+ debug_assert!(i + 3 < path.len());
+ result.push(PathCommand::SmoothCurveTo {
+ control2: CoordPair::new(path[i], path[i + 1]),
+ point: CoordPair::new(path[i + 2], path[i + 3]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_SMOOTH_ABS),
+ });
+ i = i + 4;
+ },
+ PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS | PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL => {
+ debug_assert!(i + 1 < path.len());
+ result.push(PathCommand::SmoothQuadBezierCurveTo {
+ point: CoordPair::new(path[i], path[i + 1]),
+ absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS),
+ });
+ i = i + 2;
+ },
+ PATHSEG_UNKNOWN | _ => return Err(()),
+ }
+ }
+
+ Ok(SVGPathData(crate::ArcSlice::from_iter(result.into_iter())))
+ }
+
+ /// Parse this SVG path string with the argument that indicates whether we should allow the
+ /// empty string.
+ // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make
+ // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.)
+ // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident
+ // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable
+ // str::Char iterator to check each character.
+ pub fn parse<'i, 't>(
+ input: &mut Parser<'i, 't>,
+ allow_empty: AllowEmpty,
+ ) -> Result<Self, ParseError<'i>> {
+ let location = input.current_source_location();
+ let path_string = input.expect_string()?.as_ref();
+
+ // Parse the svg path string as multiple sub-paths.
+ let mut path_parser = PathParser::new(path_string);
+ while skip_wsp(&mut path_parser.chars) {
+ if path_parser.parse_subpath().is_err() {
+ return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+ }
+
+ // The css-shapes-1 says a path data string that does conform but defines an empty path is
+ // invalid and causes the entire path() to be invalid, so we use the argement to decide
+ // whether we should allow the empty string.
+ // https://drafts.csswg.org/css-shapes-1/#typedef-basic-shape
+ if matches!(allow_empty, AllowEmpty::No) && path_parser.path.is_empty() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ Ok(SVGPathData(crate::ArcSlice::from_iter(
+ path_parser.path.into_iter(),
+ )))
+ }
+}
+
+impl ToCss for SVGPathData {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ dest.write_char('"')?;
+ {
+ let mut writer = SequenceWriter::new(dest, " ");
+ for command in self.commands() {
+ writer.item(command)?;
+ }
+ }
+ dest.write_char('"')
+ }
+}
+
+impl Parse for SVGPathData {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // Note that the EBNF allows the path data string in the d property to be empty, so we
+ // don't reject empty SVG path data.
+ // https://svgwg.org/svg2-draft/single-page.html#paths-PathDataBNF
+ SVGPathData::parse(input, AllowEmpty::Yes)
+ }
+}
+
+impl Animate for SVGPathData {
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ if self.0.len() != other.0.len() {
+ return Err(());
+ }
+
+ // FIXME(emilio): This allocates three copies of the path, that's not
+ // great! Specially, once we're normalized once, we don't need to
+ // re-normalize again.
+ let left = self.normalize();
+ let right = other.normalize();
+
+ let items: Vec<_> = lists::by_computed_value::animate(&left.0, &right.0, procedure)?;
+ Ok(SVGPathData(crate::ArcSlice::from_iter(items.into_iter())))
+ }
+}
+
+impl ComputeSquaredDistance for SVGPathData {
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ if self.0.len() != other.0.len() {
+ return Err(());
+ }
+ let left = self.normalize();
+ let right = other.normalize();
+ lists::by_computed_value::squared_distance(&left.0, &right.0)
+ }
+}
+
+/// The SVG path command.
+/// The fields of these commands are self-explanatory, so we skip the documents.
+/// Note: the index of the control points, e.g. control1, control2, are mapping to the control
+/// points of the Bézier curve in the spec.
+///
+/// https://www.w3.org/TR/SVG11/paths.html#PathData
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(C, u8)]
+pub enum PathCommand {
+ /// The unknown type.
+ /// https://www.w3.org/TR/SVG/paths.html#__svg__SVGPathSeg__PATHSEG_UNKNOWN
+ Unknown,
+ /// The "moveto" command.
+ MoveTo {
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The "lineto" command.
+ LineTo {
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The horizontal "lineto" command.
+ HorizontalLineTo { x: CSSFloat, absolute: IsAbsolute },
+ /// The vertical "lineto" command.
+ VerticalLineTo { y: CSSFloat, absolute: IsAbsolute },
+ /// The cubic Bézier curve command.
+ CurveTo {
+ control1: CoordPair,
+ control2: CoordPair,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The smooth curve command.
+ SmoothCurveTo {
+ control2: CoordPair,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The quadratic Bézier curve command.
+ QuadBezierCurveTo {
+ control1: CoordPair,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The smooth quadratic Bézier curve command.
+ SmoothQuadBezierCurveTo {
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The elliptical arc curve command.
+ EllipticalArc {
+ rx: CSSFloat,
+ ry: CSSFloat,
+ angle: CSSFloat,
+ large_arc_flag: ArcFlag,
+ sweep_flag: ArcFlag,
+ point: CoordPair,
+ absolute: IsAbsolute,
+ },
+ /// The "closepath" command.
+ ClosePath,
+}
+
+/// For internal SVGPath normalization.
+#[allow(missing_docs)]
+struct PathTraversalState {
+ subpath_start: CoordPair,
+ pos: CoordPair,
+}
+
+impl PathCommand {
+ /// Create a normalized copy of this PathCommand. Absolute commands will be copied as-is while
+ /// for relative commands an equivalent absolute command will be returned.
+ ///
+ /// See discussion: https://github.com/w3c/svgwg/issues/321
+ fn normalize(&self, state: &mut PathTraversalState) -> Self {
+ use self::PathCommand::*;
+ match *self {
+ Unknown => Unknown,
+ ClosePath => {
+ state.pos = state.subpath_start;
+ ClosePath
+ },
+ MoveTo {
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ state.subpath_start = point;
+ MoveTo {
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ LineTo {
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ LineTo {
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ HorizontalLineTo { mut x, absolute } => {
+ if !absolute.is_yes() {
+ x += state.pos.x;
+ }
+ state.pos.x = x;
+ HorizontalLineTo {
+ x,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ VerticalLineTo { mut y, absolute } => {
+ if !absolute.is_yes() {
+ y += state.pos.y;
+ }
+ state.pos.y = y;
+ VerticalLineTo {
+ y,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ CurveTo {
+ mut control1,
+ mut control2,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ control1 += state.pos;
+ control2 += state.pos;
+ point += state.pos;
+ }
+ state.pos = point;
+ CurveTo {
+ control1,
+ control2,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ SmoothCurveTo {
+ mut control2,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ control2 += state.pos;
+ point += state.pos;
+ }
+ state.pos = point;
+ SmoothCurveTo {
+ control2,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ QuadBezierCurveTo {
+ mut control1,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ control1 += state.pos;
+ point += state.pos;
+ }
+ state.pos = point;
+ QuadBezierCurveTo {
+ control1,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ SmoothQuadBezierCurveTo {
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ SmoothQuadBezierCurveTo {
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ EllipticalArc {
+ rx,
+ ry,
+ angle,
+ large_arc_flag,
+ sweep_flag,
+ mut point,
+ absolute,
+ } => {
+ if !absolute.is_yes() {
+ point += state.pos;
+ }
+ state.pos = point;
+ EllipticalArc {
+ rx,
+ ry,
+ angle,
+ large_arc_flag,
+ sweep_flag,
+ point,
+ absolute: IsAbsolute::Yes,
+ }
+ },
+ }
+ }
+}
+
+impl ToCss for PathCommand {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ use self::PathCommand::*;
+ match *self {
+ Unknown => dest.write_char('X'),
+ ClosePath => dest.write_char('Z'),
+ MoveTo { point, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'M' } else { 'm' })?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ LineTo { point, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'L' } else { 'l' })?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ CurveTo {
+ control1,
+ control2,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'C' } else { 'c' })?;
+ dest.write_char(' ')?;
+ control1.to_css(dest)?;
+ dest.write_char(' ')?;
+ control2.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ QuadBezierCurveTo {
+ control1,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'Q' } else { 'q' })?;
+ dest.write_char(' ')?;
+ control1.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ EllipticalArc {
+ rx,
+ ry,
+ angle,
+ large_arc_flag,
+ sweep_flag,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'A' } else { 'a' })?;
+ dest.write_char(' ')?;
+ rx.to_css(dest)?;
+ dest.write_char(' ')?;
+ ry.to_css(dest)?;
+ dest.write_char(' ')?;
+ angle.to_css(dest)?;
+ dest.write_char(' ')?;
+ large_arc_flag.to_css(dest)?;
+ dest.write_char(' ')?;
+ sweep_flag.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ HorizontalLineTo { x, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'H' } else { 'h' })?;
+ dest.write_char(' ')?;
+ x.to_css(dest)
+ },
+ VerticalLineTo { y, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'V' } else { 'v' })?;
+ dest.write_char(' ')?;
+ y.to_css(dest)
+ },
+ SmoothCurveTo {
+ control2,
+ point,
+ absolute,
+ } => {
+ dest.write_char(if absolute.is_yes() { 'S' } else { 's' })?;
+ dest.write_char(' ')?;
+ control2.to_css(dest)?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ SmoothQuadBezierCurveTo { point, absolute } => {
+ dest.write_char(if absolute.is_yes() { 'T' } else { 't' })?;
+ dest.write_char(' ')?;
+ point.to_css(dest)
+ },
+ }
+ }
+}
+
+/// The path command absolute type.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum IsAbsolute {
+ Yes,
+ No,
+}
+
+impl IsAbsolute {
+ /// Return true if this is IsAbsolute::Yes.
+ #[inline]
+ pub fn is_yes(&self) -> bool {
+ *self == IsAbsolute::Yes
+ }
+
+ /// Return Yes if value is true. Otherwise, return No.
+ #[inline]
+ fn new(value: bool) -> Self {
+ if value {
+ IsAbsolute::Yes
+ } else {
+ IsAbsolute::No
+ }
+ }
+}
+
+/// The path coord type.
+#[allow(missing_docs)]
+#[derive(
+ AddAssign,
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedZero,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct CoordPair {
+ x: CSSFloat,
+ y: CSSFloat,
+}
+
+impl CoordPair {
+ /// Create a CoordPair.
+ #[inline]
+ pub fn new(x: CSSFloat, y: CSSFloat) -> Self {
+ CoordPair { x, y }
+ }
+}
+
+/// The EllipticalArc flag type.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+pub struct ArcFlag(bool);
+
+impl ToCss for ArcFlag {
+ #[inline]
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: fmt::Write,
+ {
+ (self.0 as i32).to_css(dest)
+ }
+}
+
+impl Animate for ArcFlag {
+ #[inline]
+ fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> {
+ (self.0 as i32)
+ .animate(&(other.0 as i32), procedure)
+ .map(|v| ArcFlag(v > 0))
+ }
+}
+
+impl ComputeSquaredDistance for ArcFlag {
+ #[inline]
+ fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> {
+ (self.0 as i32).compute_squared_distance(&(other.0 as i32))
+ }
+}
+
+impl ToAnimatedZero for ArcFlag {
+ #[inline]
+ fn to_animated_zero(&self) -> Result<Self, ()> {
+ // The 2 ArcFlags in EllipticalArc determine which one of the 4 different arcs will be
+ // used. (i.e. From 4 combinations). In other words, if we change the flag, we get a
+ // different arc. Therefore, we return *self.
+ // https://svgwg.org/svg2-draft/paths.html#PathDataEllipticalArcCommands
+ Ok(*self)
+ }
+}
+
+/// SVG Path parser.
+struct PathParser<'a> {
+ chars: Peekable<Cloned<slice::Iter<'a, u8>>>,
+ path: Vec<PathCommand>,
+}
+
+macro_rules! parse_arguments {
+ (
+ $parser:ident,
+ $abs:ident,
+ $enum:ident,
+ [ $para:ident => $func:ident $(, $other_para:ident => $other_func:ident)* ]
+ ) => {
+ {
+ loop {
+ let $para = $func(&mut $parser.chars)?;
+ $(
+ skip_comma_wsp(&mut $parser.chars);
+ let $other_para = $other_func(&mut $parser.chars)?;
+ )*
+ $parser.path.push(PathCommand::$enum { $para $(, $other_para)*, $abs });
+
+ // End of string or the next character is a possible new command.
+ if !skip_wsp(&mut $parser.chars) ||
+ $parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) {
+ break;
+ }
+ skip_comma_wsp(&mut $parser.chars);
+ }
+ Ok(())
+ }
+ }
+}
+
+impl<'a> PathParser<'a> {
+ /// Return a PathParser.
+ #[inline]
+ fn new(string: &'a str) -> Self {
+ PathParser {
+ chars: string.as_bytes().iter().cloned().peekable(),
+ path: Vec::new(),
+ }
+ }
+
+ /// Parse a sub-path.
+ fn parse_subpath(&mut self) -> Result<(), ()> {
+ // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path
+ // (i.e. not a valid moveto-drawto-command-group).
+ self.parse_moveto()?;
+
+ // Handle other commands.
+ loop {
+ skip_wsp(&mut self.chars);
+ if self.chars.peek().map_or(true, |&m| m == b'M' || m == b'm') {
+ break;
+ }
+
+ let command = self.chars.next().unwrap();
+ let abs = if command.is_ascii_uppercase() {
+ IsAbsolute::Yes
+ } else {
+ IsAbsolute::No
+ };
+
+ skip_wsp(&mut self.chars);
+ match command {
+ b'Z' | b'z' => self.parse_closepath(),
+ b'L' | b'l' => self.parse_lineto(abs),
+ b'H' | b'h' => self.parse_h_lineto(abs),
+ b'V' | b'v' => self.parse_v_lineto(abs),
+ b'C' | b'c' => self.parse_curveto(abs),
+ b'S' | b's' => self.parse_smooth_curveto(abs),
+ b'Q' | b'q' => self.parse_quadratic_bezier_curveto(abs),
+ b'T' | b't' => self.parse_smooth_quadratic_bezier_curveto(abs),
+ b'A' | b'a' => self.parse_elliptical_arc(abs),
+ _ => return Err(()),
+ }?;
+ }
+ Ok(())
+ }
+
+ /// Parse "moveto" command.
+ fn parse_moveto(&mut self) -> Result<(), ()> {
+ let command = match self.chars.next() {
+ Some(c) if c == b'M' || c == b'm' => c,
+ _ => return Err(()),
+ };
+
+ skip_wsp(&mut self.chars);
+ let point = parse_coord(&mut self.chars)?;
+ let absolute = if command == b'M' {
+ IsAbsolute::Yes
+ } else {
+ IsAbsolute::No
+ };
+ self.path.push(PathCommand::MoveTo { point, absolute });
+
+ // End of string or the next character is a possible new command.
+ if !skip_wsp(&mut self.chars) || self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic())
+ {
+ return Ok(());
+ }
+ skip_comma_wsp(&mut self.chars);
+
+ // If a moveto is followed by multiple pairs of coordinates, the subsequent
+ // pairs are treated as implicit lineto commands.
+ self.parse_lineto(absolute)
+ }
+
+ /// Parse "closepath" command.
+ fn parse_closepath(&mut self) -> Result<(), ()> {
+ self.path.push(PathCommand::ClosePath);
+ Ok(())
+ }
+
+ /// Parse "lineto" command.
+ fn parse_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, LineTo, [ point => parse_coord ])
+ }
+
+ /// Parse horizontal "lineto" command.
+ fn parse_h_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, HorizontalLineTo, [ x => parse_number ])
+ }
+
+ /// Parse vertical "lineto" command.
+ fn parse_v_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, VerticalLineTo, [ y => parse_number ])
+ }
+
+ /// Parse cubic Bézier curve command.
+ fn parse_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, CurveTo, [
+ control1 => parse_coord, control2 => parse_coord, point => parse_coord
+ ])
+ }
+
+ /// Parse smooth "curveto" command.
+ fn parse_smooth_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, SmoothCurveTo, [
+ control2 => parse_coord, point => parse_coord
+ ])
+ }
+
+ /// Parse quadratic Bézier curve command.
+ fn parse_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, QuadBezierCurveTo, [
+ control1 => parse_coord, point => parse_coord
+ ])
+ }
+
+ /// Parse smooth quadratic Bézier curveto command.
+ fn parse_smooth_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ parse_arguments!(self, absolute, SmoothQuadBezierCurveTo, [ point => parse_coord ])
+ }
+
+ /// Parse elliptical arc curve command.
+ fn parse_elliptical_arc(&mut self, absolute: IsAbsolute) -> Result<(), ()> {
+ // Parse a flag whose value is '0' or '1'; otherwise, return Err(()).
+ let parse_flag = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() {
+ Some(c) if c == b'0' || c == b'1' => Ok(ArcFlag(c == b'1')),
+ _ => Err(()),
+ };
+ parse_arguments!(self, absolute, EllipticalArc, [
+ rx => parse_number,
+ ry => parse_number,
+ angle => parse_number,
+ large_arc_flag => parse_flag,
+ sweep_flag => parse_flag,
+ point => parse_coord
+ ])
+ }
+}
+
+/// Parse a pair of numbers into CoordPair.
+fn parse_coord(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CoordPair, ()> {
+ let x = parse_number(iter)?;
+ skip_comma_wsp(iter);
+ let y = parse_number(iter)?;
+ Ok(CoordPair::new(x, y))
+}
+
+/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed
+/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating
+/// point number. In other words, the logic here is similar with that of
+/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the
+/// input is a Peekable and we only accept an integer of a floating point number.
+///
+/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF
+fn parse_number(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CSSFloat, ()> {
+ // 1. Check optional sign.
+ let sign = if iter
+ .peek()
+ .map_or(false, |&sign| sign == b'+' || sign == b'-')
+ {
+ if iter.next().unwrap() == b'-' {
+ -1.
+ } else {
+ 1.
+ }
+ } else {
+ 1.
+ };
+
+ // 2. Check integer part.
+ let mut integral_part: f64 = 0.;
+ let got_dot = if !iter.peek().map_or(false, |&n| n == b'.') {
+ // If the first digit in integer part is neither a dot nor a digit, this is not a number.
+ if iter.peek().map_or(true, |n| !n.is_ascii_digit()) {
+ return Err(());
+ }
+
+ while iter.peek().map_or(false, |n| n.is_ascii_digit()) {
+ integral_part = integral_part * 10. + (iter.next().unwrap() - b'0') as f64;
+ }
+
+ iter.peek().map_or(false, |&n| n == b'.')
+ } else {
+ true
+ };
+
+ // 3. Check fractional part.
+ let mut fractional_part: f64 = 0.;
+ if got_dot {
+ // Consume '.'.
+ iter.next();
+ // If the first digit in fractional part is not a digit, this is not a number.
+ if iter.peek().map_or(true, |n| !n.is_ascii_digit()) {
+ return Err(());
+ }
+
+ let mut factor = 0.1;
+ while iter.peek().map_or(false, |n| n.is_ascii_digit()) {
+ fractional_part += (iter.next().unwrap() - b'0') as f64 * factor;
+ factor *= 0.1;
+ }
+ }
+
+ let mut value = sign * (integral_part + fractional_part);
+
+ // 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to
+ // treat the numbers after 'E' or 'e' are in the exponential part.
+ if iter.peek().map_or(false, |&exp| exp == b'E' || exp == b'e') {
+ // Consume 'E' or 'e'.
+ iter.next();
+ let exp_sign = if iter
+ .peek()
+ .map_or(false, |&sign| sign == b'+' || sign == b'-')
+ {
+ if iter.next().unwrap() == b'-' {
+ -1.
+ } else {
+ 1.
+ }
+ } else {
+ 1.
+ };
+
+ let mut exp: f64 = 0.;
+ while iter.peek().map_or(false, |n| n.is_ascii_digit()) {
+ exp = exp * 10. + (iter.next().unwrap() - b'0') as f64;
+ }
+
+ value *= f64::powf(10., exp * exp_sign);
+ }
+
+ if value.is_finite() {
+ Ok(value.min(f32::MAX as f64).max(f32::MIN as f64) as CSSFloat)
+ } else {
+ Err(())
+ }
+}
+
+/// Skip all svg whitespaces, and return true if |iter| hasn't finished.
+#[inline]
+fn skip_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool {
+ // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}.
+ // However, SVG 2 has one extra whitespace: \u{C}.
+ // Therefore, we follow the newest spec for the definition of whitespace,
+ // i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}.
+ while iter.peek().map_or(false, |c| c.is_ascii_whitespace()) {
+ iter.next();
+ }
+ iter.peek().is_some()
+}
+
+/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished.
+#[inline]
+fn skip_comma_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool {
+ if !skip_wsp(iter) {
+ return false;
+ }
+
+ if *iter.peek().unwrap() != b',' {
+ return true;
+ }
+ iter.next();
+
+ skip_wsp(iter)
+}
diff --git a/servo/components/style/values/specified/table.rs b/servo/components/style/values/specified/table.rs
new file mode 100644
index 0000000000..88f917ac78
--- /dev/null
+++ b/servo/components/style/values/specified/table.rs
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values related to tables.
+
+/// Specified values for the `caption-side` property.
+///
+/// Note that despite having "physical" names, these are actually interpreted
+/// according to the table's writing-mode: Top and Bottom are treated as
+/// block-start and -end respectively.
+///
+/// https://drafts.csswg.org/css-tables/#propdef-caption-side
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Ord,
+ Parse,
+ PartialEq,
+ PartialOrd,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum CaptionSide {
+ Top,
+ Bottom,
+}
diff --git a/servo/components/style/values/specified/text.rs b/servo/components/style/values/specified/text.rs
new file mode 100644
index 0000000000..0e70bd26ac
--- /dev/null
+++ b/servo/components/style/values/specified/text.rs
@@ -0,0 +1,1193 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for text properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode;
+use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle;
+use crate::values::computed::text::TextOverflow as ComputedTextOverflow;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::generics::text::InitialLetter as GenericInitialLetter;
+use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing};
+use crate::values::specified::length::{Length, LengthPercentage};
+use crate::values::specified::{AllowQuirks, Integer, Number};
+use cssparser::{Parser, Token};
+use icu_segmenter::GraphemeClusterSegmenter;
+use selectors::parser::SelectorParseErrorKind;
+use std::fmt::{self, Write};
+use style_traits::values::SequenceWriter;
+use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss};
+use style_traits::{KeywordsCollectFn, SpecifiedValueInfo};
+
+/// A specified type for the `initial-letter` property.
+pub type InitialLetter = GenericInitialLetter<Number, Integer>;
+
+/// A specified value for the `letter-spacing` property.
+pub type LetterSpacing = Spacing<Length>;
+
+/// A specified value for the `word-spacing` property.
+pub type WordSpacing = Spacing<LengthPercentage>;
+
+/// A value for the `hyphenate-character` property.
+#[derive(
+ Clone,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum HyphenateCharacter {
+ /// `auto`
+ Auto,
+ /// `<string>`
+ String(crate::OwnedStr),
+}
+
+impl Parse for InitialLetter {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|i| i.expect_ident_matching("normal"))
+ .is_ok()
+ {
+ return Ok(GenericInitialLetter::Normal);
+ }
+ let size = Number::parse_at_least_one(context, input)?;
+ let sink = input
+ .try_parse(|i| Integer::parse_positive(context, i))
+ .ok();
+ Ok(GenericInitialLetter::Specified(size, sink))
+ }
+}
+
+impl Parse for LetterSpacing {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Spacing::parse_with(context, input, |c, i| {
+ Length::parse_quirky(c, i, AllowQuirks::Yes)
+ })
+ }
+}
+
+impl Parse for WordSpacing {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Spacing::parse_with(context, input, |c, i| {
+ LengthPercentage::parse_quirky(c, i, AllowQuirks::Yes)
+ })
+ }
+}
+
+/// A generic value for the `text-overflow` property.
+#[derive(
+ Clone,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C, u8)]
+pub enum TextOverflowSide {
+ /// Clip inline content.
+ Clip,
+ /// Render ellipsis to represent clipped inline content.
+ Ellipsis,
+ /// Render a given string to represent clipped inline content.
+ String(crate::OwnedStr),
+}
+
+#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+/// text-overflow. Specifies rendering when inline content overflows its line box edge.
+pub struct TextOverflow {
+ /// First value. Applies to end line box edge if no second is supplied; line-left edge otherwise.
+ pub first: TextOverflowSide,
+ /// Second value. Applies to the line-right edge if supplied.
+ pub second: Option<TextOverflowSide>,
+}
+
+impl Parse for TextOverflow {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<TextOverflow, ParseError<'i>> {
+ let first = TextOverflowSide::parse(context, input)?;
+ let second = input
+ .try_parse(|input| TextOverflowSide::parse(context, input))
+ .ok();
+ Ok(TextOverflow { first, second })
+ }
+}
+
+impl ToComputedValue for TextOverflow {
+ type ComputedValue = ComputedTextOverflow;
+
+ #[inline]
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ if let Some(ref second) = self.second {
+ Self::ComputedValue {
+ first: self.first.clone(),
+ second: second.clone(),
+ sides_are_logical: false,
+ }
+ } else {
+ Self::ComputedValue {
+ first: TextOverflowSide::Clip,
+ second: self.first.clone(),
+ sides_are_logical: true,
+ }
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ if computed.sides_are_logical {
+ assert_eq!(computed.first, TextOverflowSide::Clip);
+ TextOverflow {
+ first: computed.second.clone(),
+ second: None,
+ }
+ } else {
+ TextOverflow {
+ first: computed.first.clone(),
+ second: Some(computed.second.clone()),
+ }
+ }
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ Serialize,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(single = "none", mixed = "underline,overline,line-through,blink"))]
+#[repr(C)]
+/// Specified keyword values for the text-decoration-line property.
+pub struct TextDecorationLine(u8);
+bitflags! {
+ impl TextDecorationLine: u8 {
+ /// No text decoration line is specified.
+ const NONE = 0;
+ /// underline
+ const UNDERLINE = 1 << 0;
+ /// overline
+ const OVERLINE = 1 << 1;
+ /// line-through
+ const LINE_THROUGH = 1 << 2;
+ /// blink
+ const BLINK = 1 << 3;
+ /// Only set by presentation attributes
+ ///
+ /// Setting this will mean that text-decorations use the color
+ /// specified by `color` in quirks mode.
+ ///
+ /// For example, this gives <a href=foo><font color="red">text</font></a>
+ /// a red text decoration
+ #[cfg(feature = "gecko")]
+ const COLOR_OVERRIDE = 0x10;
+ }
+}
+
+impl Default for TextDecorationLine {
+ fn default() -> Self {
+ TextDecorationLine::NONE
+ }
+}
+
+impl TextDecorationLine {
+ #[inline]
+ /// Returns the initial value of text-decoration-line
+ pub fn none() -> Self {
+ TextDecorationLine::NONE
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Specified value of the text-transform property, stored in two parts:
+/// the case-related transforms (mutually exclusive, only one may be in effect), and others (non-exclusive).
+pub struct TextTransform {
+ /// Case transform, if any.
+ pub case_: TextTransformCase,
+ /// Non-case transforms.
+ pub other_: TextTransformOther,
+}
+
+impl TextTransform {
+ #[inline]
+ /// Returns the initial value of text-transform
+ pub fn none() -> Self {
+ TextTransform {
+ case_: TextTransformCase::None,
+ other_: TextTransformOther::empty(),
+ }
+ }
+ #[inline]
+ /// Returns whether the value is 'none'
+ pub fn is_none(&self) -> bool {
+ self.case_ == TextTransformCase::None && self.other_.is_empty()
+ }
+}
+
+// TODO: This can be simplified by deriving it.
+impl Parse for TextTransform {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut result = TextTransform::none();
+
+ // Case keywords are mutually exclusive; other transforms may co-occur.
+ loop {
+ let location = input.current_source_location();
+ let ident = match input.next() {
+ Ok(&Token::Ident(ref ident)) => ident,
+ Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
+ Err(..) => break,
+ };
+
+ match_ignore_ascii_case! { ident,
+ "none" if result.is_none() => {
+ return Ok(result);
+ },
+ "uppercase" if result.case_ == TextTransformCase::None => {
+ result.case_ = TextTransformCase::Uppercase
+ },
+ "lowercase" if result.case_ == TextTransformCase::None => {
+ result.case_ = TextTransformCase::Lowercase
+ },
+ "capitalize" if result.case_ == TextTransformCase::None => {
+ result.case_ = TextTransformCase::Capitalize
+ },
+ "math-auto" if result.case_ == TextTransformCase::None &&
+ result.other_.is_empty() => {
+ result.case_ = TextTransformCase::MathAuto;
+ return Ok(result);
+ },
+ "full-width" if !result.other_.intersects(TextTransformOther::FULL_WIDTH) => {
+ result.other_.insert(TextTransformOther::FULL_WIDTH)
+ },
+ "full-size-kana" if !result.other_.intersects(TextTransformOther::FULL_SIZE_KANA) => {
+ result.other_.insert(TextTransformOther::FULL_SIZE_KANA)
+ },
+ _ => return Err(location.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ )),
+ }
+ }
+
+ if result.is_none() {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ } else {
+ Ok(result)
+ }
+ }
+}
+
+impl ToCss for TextTransform {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_none() {
+ return dest.write_str("none");
+ }
+
+ if self.case_ != TextTransformCase::None {
+ self.case_.to_css(dest)?;
+ if !self.other_.is_empty() {
+ dest.write_char(' ')?;
+ }
+ }
+
+ self.other_.to_css(dest)
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.)
+pub enum TextTransformCase {
+ /// No case transform.
+ None,
+ /// All uppercase.
+ Uppercase,
+ /// All lowercase.
+ Lowercase,
+ /// Capitalize each word.
+ Capitalize,
+ /// Automatic italicization of math variables.
+ MathAuto,
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ Serialize,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[css(bitflags(mixed = "full-width,full-size-kana"))]
+#[repr(C)]
+/// Specified keyword values for non-case transforms in the text-transform property. (Non-exclusive.)
+pub struct TextTransformOther(u8);
+bitflags! {
+ impl TextTransformOther: u8 {
+ /// full-width
+ const FULL_WIDTH = 1 << 0;
+ /// full-size-kana
+ const FULL_SIZE_KANA = 1 << 1;
+ }
+}
+
+/// Specified and computed value of text-align-last.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum TextAlignLast {
+ Auto,
+ Start,
+ End,
+ Left,
+ Right,
+ Center,
+ Justify,
+}
+
+/// Specified value of text-align keyword value.
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ Hash,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+#[repr(u8)]
+pub enum TextAlignKeyword {
+ Start,
+ Left,
+ Right,
+ Center,
+ #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))]
+ Justify,
+ #[css(skip)]
+ #[cfg(feature = "gecko")]
+ Char,
+ End,
+ #[cfg(feature = "gecko")]
+ MozCenter,
+ #[cfg(feature = "gecko")]
+ MozLeft,
+ #[cfg(feature = "gecko")]
+ MozRight,
+ #[cfg(feature = "servo-layout-2013")]
+ ServoCenter,
+ #[cfg(feature = "servo-layout-2013")]
+ ServoLeft,
+ #[cfg(feature = "servo-layout-2013")]
+ ServoRight,
+}
+
+/// Specified value of text-align property.
+#[derive(
+ Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem,
+)]
+pub enum TextAlign {
+ /// Keyword value of text-align property.
+ Keyword(TextAlignKeyword),
+ /// `match-parent` value of text-align property. It has a different handling
+ /// unlike other keywords.
+ #[cfg(feature = "gecko")]
+ MatchParent,
+ /// This is how we implement the following HTML behavior from
+ /// https://html.spec.whatwg.org/#tables-2:
+ ///
+ /// User agents are expected to have a rule in their user agent style sheet
+ /// that matches th elements that have a parent node whose computed value
+ /// for the 'text-align' property is its initial value, whose declaration
+ /// block consists of just a single declaration that sets the 'text-align'
+ /// property to the value 'center'.
+ ///
+ /// Since selectors can't depend on the ancestor styles, we implement it with a
+ /// magic value that computes to the right thing. Since this is an
+ /// implementation detail, it shouldn't be exposed to web content.
+ #[cfg(feature = "gecko")]
+ #[parse(condition = "ParserContext::chrome_rules_enabled")]
+ MozCenterOrInherit,
+}
+
+impl ToComputedValue for TextAlign {
+ type ComputedValue = TextAlignKeyword;
+
+ #[inline]
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ match *self {
+ TextAlign::Keyword(key) => key,
+ #[cfg(feature = "gecko")]
+ TextAlign::MatchParent => {
+ // on the root <html> element we should still respect the dir
+ // but the parent dir of that element is LTR even if it's <html dir=rtl>
+ // and will only be RTL if certain prefs have been set.
+ // In that case, the default behavior here will set it to left,
+ // but we want to set it to right -- instead set it to the default (`start`),
+ // which will do the right thing in this case (but not the general case)
+ if _context.builder.is_root_element {
+ return TextAlignKeyword::Start;
+ }
+ let parent = _context
+ .builder
+ .get_parent_inherited_text()
+ .clone_text_align();
+ let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr();
+ match (parent, ltr) {
+ (TextAlignKeyword::Start, true) => TextAlignKeyword::Left,
+ (TextAlignKeyword::Start, false) => TextAlignKeyword::Right,
+ (TextAlignKeyword::End, true) => TextAlignKeyword::Right,
+ (TextAlignKeyword::End, false) => TextAlignKeyword::Left,
+ _ => parent,
+ }
+ },
+ #[cfg(feature = "gecko")]
+ TextAlign::MozCenterOrInherit => {
+ let parent = _context
+ .builder
+ .get_parent_inherited_text()
+ .clone_text_align();
+ if parent == TextAlignKeyword::Start {
+ TextAlignKeyword::Center
+ } else {
+ parent
+ }
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ TextAlign::Keyword(*computed)
+ }
+}
+
+fn fill_mode_is_default_and_shape_exists(
+ fill: &TextEmphasisFillMode,
+ shape: &Option<TextEmphasisShapeKeyword>,
+) -> bool {
+ shape.is_some() && fill.is_filled()
+}
+
+/// Specified value of text-emphasis-style property.
+///
+/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style
+#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+#[allow(missing_docs)]
+pub enum TextEmphasisStyle {
+ /// [ <fill> || <shape> ]
+ Keyword {
+ #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")]
+ fill: TextEmphasisFillMode,
+ shape: Option<TextEmphasisShapeKeyword>,
+ },
+ /// `none`
+ None,
+ /// `<string>` (of which only the first grapheme cluster will be used).
+ String(crate::OwnedStr),
+}
+
+/// Fill mode for the text-emphasis-style property
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TextEmphasisFillMode {
+ /// `filled`
+ Filled,
+ /// `open`
+ Open,
+}
+
+impl TextEmphasisFillMode {
+ /// Whether the value is `filled`.
+ #[inline]
+ pub fn is_filled(&self) -> bool {
+ matches!(*self, TextEmphasisFillMode::Filled)
+ }
+}
+
+/// Shape keyword for the text-emphasis-style property
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TextEmphasisShapeKeyword {
+ /// `dot`
+ Dot,
+ /// `circle`
+ Circle,
+ /// `double-circle`
+ DoubleCircle,
+ /// `triangle`
+ Triangle,
+ /// `sesame`
+ Sesame,
+}
+
+impl ToComputedValue for TextEmphasisStyle {
+ type ComputedValue = ComputedTextEmphasisStyle;
+
+ #[inline]
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ TextEmphasisStyle::Keyword { fill, shape } => {
+ let shape = shape.unwrap_or_else(|| {
+ // FIXME(emilio, bug 1572958): This should set the
+ // rule_cache_conditions properly.
+ //
+ // Also should probably use WritingMode::is_vertical rather
+ // than the computed value of the `writing-mode` property.
+ if context.style().get_inherited_box().clone_writing_mode() ==
+ SpecifiedWritingMode::HorizontalTb
+ {
+ TextEmphasisShapeKeyword::Circle
+ } else {
+ TextEmphasisShapeKeyword::Sesame
+ }
+ });
+ ComputedTextEmphasisStyle::Keyword { fill, shape }
+ },
+ TextEmphasisStyle::None => ComputedTextEmphasisStyle::None,
+ TextEmphasisStyle::String(ref s) => {
+ // FIXME(emilio): Doing this at computed value time seems wrong.
+ // The spec doesn't say that this should be a computed-value
+ // time operation. This is observable from getComputedStyle().
+ //
+ // Note that the first grapheme cluster boundary should always be the start of the string.
+ let first_grapheme_end = GraphemeClusterSegmenter::new().segment_str(s).nth(1).unwrap_or(0);
+ ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into())
+ },
+ }
+ }
+
+ #[inline]
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ match *computed {
+ ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword {
+ fill,
+ shape: Some(shape),
+ },
+ ComputedTextEmphasisStyle::None => TextEmphasisStyle::None,
+ ComputedTextEmphasisStyle::String(ref string) => {
+ TextEmphasisStyle::String(string.clone())
+ },
+ }
+ }
+}
+
+impl Parse for TextEmphasisStyle {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(TextEmphasisStyle::None);
+ }
+
+ if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) {
+ // Handle <string>
+ return Ok(TextEmphasisStyle::String(s.into()));
+ }
+
+ // Handle a pair of keywords
+ let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
+ let fill = input.try_parse(TextEmphasisFillMode::parse).ok();
+ if shape.is_none() {
+ shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok();
+ }
+
+ if shape.is_none() && fill.is_none() {
+ return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError));
+ }
+
+ // If a shape keyword is specified but neither filled nor open is
+ // specified, filled is assumed.
+ let fill = fill.unwrap_or(TextEmphasisFillMode::Filled);
+
+ // We cannot do the same because the default `<shape>` depends on the
+ // computed writing-mode.
+ Ok(TextEmphasisStyle::Keyword { fill, shape })
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ Parse,
+ Serialize,
+ SpecifiedValueInfo,
+ ToCss,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(C)]
+#[css(bitflags(
+ mixed = "over,under,left,right",
+ validate_mixed = "Self::validate_and_simplify"
+))]
+/// Values for text-emphasis-position:
+/// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property>
+pub struct TextEmphasisPosition(u8);
+bitflags! {
+ impl TextEmphasisPosition: u8 {
+ /// Draws marks to the right of the text in vertical writing mode.
+ const OVER = 1 << 0;
+ /// Draw marks under the text in horizontal writing mode.
+ const UNDER = 1 << 1;
+ /// Draw marks to the left of the text in vertical writing mode.
+ const LEFT = 1 << 2;
+ /// Draws marks to the right of the text in vertical writing mode.
+ const RIGHT = 1 << 3;
+ }
+}
+
+impl TextEmphasisPosition {
+ fn validate_and_simplify(&mut self) -> bool {
+ if self.intersects(Self::OVER) == self.intersects(Self::UNDER) {
+ return false;
+ }
+
+ if self.intersects(Self::LEFT) {
+ return !self.intersects(Self::RIGHT);
+ }
+
+ self.remove(Self::RIGHT); // Right is the default
+ true
+ }
+}
+
+/// Values for the `word-break` property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum WordBreak {
+ Normal,
+ BreakAll,
+ KeepAll,
+ /// The break-word value, needed for compat.
+ ///
+ /// Specifying `word-break: break-word` makes `overflow-wrap` behave as
+ /// `anywhere`, and `word-break` behave like `normal`.
+ #[cfg(feature = "gecko")]
+ BreakWord,
+}
+
+/// Values for the `text-justify` CSS property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum TextJustify {
+ Auto,
+ None,
+ InterWord,
+ // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute
+ // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias.
+ #[parse(aliases = "distribute")]
+ InterCharacter,
+}
+
+/// Values for the `-moz-control-character-visibility` CSS property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum MozControlCharacterVisibility {
+ Hidden,
+ Visible,
+}
+
+impl Default for MozControlCharacterVisibility {
+ fn default() -> Self {
+ if static_prefs::pref!("layout.css.control-characters.visible") {
+ Self::Visible
+ } else {
+ Self::Hidden
+ }
+ }
+}
+
+/// Values for the `line-break` property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum LineBreak {
+ Auto,
+ Loose,
+ Normal,
+ Strict,
+ Anywhere,
+}
+
+/// Values for the `overflow-wrap` property.
+#[repr(u8)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum OverflowWrap {
+ Normal,
+ BreakWord,
+ Anywhere,
+}
+
+/// A specified value for the `text-indent` property
+/// which takes the grammar of [<length-percentage>] && hanging? && each-line?
+///
+/// https://drafts.csswg.org/css-text/#propdef-text-indent
+pub type TextIndent = GenericTextIndent<LengthPercentage>;
+
+impl Parse for TextIndent {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut length = None;
+ let mut hanging = false;
+ let mut each_line = false;
+
+ // The length-percentage and the two possible keywords can occur in any order.
+ while !input.is_exhausted() {
+ // If we haven't seen a length yet, try to parse one.
+ if length.is_none() {
+ if let Ok(len) = input
+ .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes))
+ {
+ length = Some(len);
+ continue;
+ }
+ }
+
+ if static_prefs::pref!("layout.css.text-indent-keywords.enabled") {
+ // Check for the keywords (boolean flags).
+ try_match_ident_ignore_ascii_case! { input,
+ "hanging" if !hanging => hanging = true,
+ "each-line" if !each_line => each_line = true,
+ }
+ continue;
+ }
+
+ // If we reach here, there must be something that we failed to parse;
+ // just break and let the caller deal with it.
+ break;
+ }
+
+ // The length-percentage value is required for the declaration to be valid.
+ if let Some(length) = length {
+ Ok(Self {
+ length,
+ hanging,
+ each_line,
+ })
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+/// Implements text-decoration-skip-ink which takes the keywords auto | none | all
+///
+/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property
+#[repr(u8)]
+#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum TextDecorationSkipInk {
+ Auto,
+ None,
+ All,
+}
+
+/// Implements type for `text-decoration-thickness` property
+pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>;
+
+impl TextDecorationLength {
+ /// `Auto` value.
+ #[inline]
+ pub fn auto() -> Self {
+ GenericTextDecorationLength::Auto
+ }
+
+ /// Whether this is the `Auto` value.
+ #[inline]
+ pub fn is_auto(&self) -> bool {
+ matches!(*self, GenericTextDecorationLength::Auto)
+ }
+}
+
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[value_info(other_values = "auto,from-font,under,left,right")]
+#[repr(C)]
+/// Specified keyword values for the text-underline-position property.
+/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives
+/// `auto | [ from-font | under ] || [ left | right ]`.)
+/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property
+pub struct TextUnderlinePosition(u8);
+bitflags! {
+ impl TextUnderlinePosition: u8 {
+ /// Use automatic positioning below the alphabetic baseline.
+ const AUTO = 0;
+ /// Use underline position from the first available font.
+ const FROM_FONT = 1 << 0;
+ /// Below the glyph box.
+ const UNDER = 1 << 1;
+ /// In vertical mode, place to the left of the text.
+ const LEFT = 1 << 2;
+ /// In vertical mode, place to the right of the text.
+ const RIGHT = 1 << 3;
+ }
+}
+
+// TODO: This can be derived with some care.
+impl Parse for TextUnderlinePosition {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<TextUnderlinePosition, ParseError<'i>> {
+ let mut result = TextUnderlinePosition::empty();
+
+ loop {
+ let location = input.current_source_location();
+ let ident = match input.next() {
+ Ok(&Token::Ident(ref ident)) => ident,
+ Ok(other) => return Err(location.new_unexpected_token_error(other.clone())),
+ Err(..) => break,
+ };
+
+ match_ignore_ascii_case! { ident,
+ "auto" if result.is_empty() => {
+ return Ok(result);
+ },
+ "from-font" if !result.intersects(TextUnderlinePosition::FROM_FONT |
+ TextUnderlinePosition::UNDER) => {
+ result.insert(TextUnderlinePosition::FROM_FONT);
+ },
+ "under" if !result.intersects(TextUnderlinePosition::FROM_FONT |
+ TextUnderlinePosition::UNDER) => {
+ result.insert(TextUnderlinePosition::UNDER);
+ },
+ "left" if !result.intersects(TextUnderlinePosition::LEFT |
+ TextUnderlinePosition::RIGHT) => {
+ result.insert(TextUnderlinePosition::LEFT);
+ },
+ "right" if !result.intersects(TextUnderlinePosition::LEFT |
+ TextUnderlinePosition::RIGHT) => {
+ result.insert(TextUnderlinePosition::RIGHT);
+ },
+ _ => return Err(location.new_custom_error(
+ SelectorParseErrorKind::UnexpectedIdent(ident.clone())
+ )),
+ }
+ }
+
+ if !result.is_empty() {
+ Ok(result)
+ } else {
+ Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ }
+ }
+}
+
+impl ToCss for TextUnderlinePosition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ if self.is_empty() {
+ return dest.write_str("auto");
+ }
+
+ let mut writer = SequenceWriter::new(dest, " ");
+ let mut any = false;
+
+ macro_rules! maybe_write {
+ ($ident:ident => $str:expr) => {
+ if self.contains(TextUnderlinePosition::$ident) {
+ any = true;
+ writer.raw_item($str)?;
+ }
+ };
+ }
+
+ maybe_write!(FROM_FONT => "from-font");
+ maybe_write!(UNDER => "under");
+ maybe_write!(LEFT => "left");
+ maybe_write!(RIGHT => "right");
+
+ debug_assert!(any);
+
+ Ok(())
+ }
+}
+
+/// Values for `ruby-position` property
+#[repr(u8)]
+#[derive(
+ Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem,
+)]
+#[allow(missing_docs)]
+pub enum RubyPosition {
+ AlternateOver,
+ AlternateUnder,
+ Over,
+ Under,
+}
+
+impl Parse for RubyPosition {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<RubyPosition, ParseError<'i>> {
+ // Parse alternate before
+ let alternate = input
+ .try_parse(|i| i.expect_ident_matching("alternate"))
+ .is_ok();
+ if alternate && input.is_exhausted() {
+ return Ok(RubyPosition::AlternateOver);
+ }
+ // Parse over / under
+ let over = try_match_ident_ignore_ascii_case! { input,
+ "over" => true,
+ "under" => false,
+ };
+ // Parse alternate after
+ let alternate = alternate ||
+ input
+ .try_parse(|i| i.expect_ident_matching("alternate"))
+ .is_ok();
+
+ Ok(match (over, alternate) {
+ (true, true) => RubyPosition::AlternateOver,
+ (false, true) => RubyPosition::AlternateUnder,
+ (true, false) => RubyPosition::Over,
+ (false, false) => RubyPosition::Under,
+ })
+ }
+}
+
+impl ToCss for RubyPosition {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(match self {
+ RubyPosition::AlternateOver => "alternate",
+ RubyPosition::AlternateUnder => "alternate under",
+ RubyPosition::Over => "over",
+ RubyPosition::Under => "under",
+ })
+ }
+}
+
+impl SpecifiedValueInfo for RubyPosition {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["alternate", "over", "under"])
+ }
+}
diff --git a/servo/components/style/values/specified/time.rs b/servo/components/style/values/specified/time.rs
new file mode 100644
index 0000000000..3061ebddcc
--- /dev/null
+++ b/servo/components/style/values/specified/time.rs
@@ -0,0 +1,183 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified time values.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::time::Time as ComputedTime;
+use crate::values::computed::{Context, ToComputedValue};
+use crate::values::specified::calc::CalcNode;
+use crate::values::CSSFloat;
+use crate::Zero;
+use cssparser::{Parser, Token};
+use std::fmt::{self, Write};
+use style_traits::values::specified::AllowedNumericType;
+use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss};
+
+/// A time value according to CSS-VALUES § 6.2.
+#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)]
+pub struct Time {
+ seconds: CSSFloat,
+ unit: TimeUnit,
+ calc_clamping_mode: Option<AllowedNumericType>,
+}
+
+/// A time unit.
+#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)]
+pub enum TimeUnit {
+ /// `s`
+ Second,
+ /// `ms`
+ Millisecond,
+}
+
+impl Time {
+ /// Returns a time value that represents `seconds` seconds.
+ pub fn from_seconds_with_calc_clamping_mode(
+ seconds: CSSFloat,
+ calc_clamping_mode: Option<AllowedNumericType>,
+ ) -> Self {
+ Time {
+ seconds,
+ unit: TimeUnit::Second,
+ calc_clamping_mode,
+ }
+ }
+
+ /// Returns a time value that represents `seconds` seconds.
+ pub fn from_seconds(seconds: CSSFloat) -> Self {
+ Self::from_seconds_with_calc_clamping_mode(seconds, None)
+ }
+
+ /// Returns the time in fractional seconds.
+ pub fn seconds(self) -> CSSFloat {
+ self.seconds
+ }
+
+ /// Returns the unit of the time.
+ #[inline]
+ pub fn unit(&self) -> &'static str {
+ match self.unit {
+ TimeUnit::Second => "s",
+ TimeUnit::Millisecond => "ms",
+ }
+ }
+
+ #[inline]
+ fn unitless_value(&self) -> CSSFloat {
+ match self.unit {
+ TimeUnit::Second => self.seconds,
+ TimeUnit::Millisecond => self.seconds * 1000.,
+ }
+ }
+
+ /// Parses a time according to CSS-VALUES § 6.2.
+ pub fn parse_dimension(value: CSSFloat, unit: &str) -> Result<Time, ()> {
+ let (seconds, unit) = match_ignore_ascii_case! { unit,
+ "s" => (value, TimeUnit::Second),
+ "ms" => (value / 1000.0, TimeUnit::Millisecond),
+ _ => return Err(())
+ };
+
+ Ok(Time {
+ seconds,
+ unit,
+ calc_clamping_mode: None,
+ })
+ }
+
+ fn parse_with_clamping_mode<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ clamping_mode: AllowedNumericType,
+ ) -> Result<Self, ParseError<'i>> {
+ use style_traits::ParsingMode;
+
+ let location = input.current_source_location();
+ match *input.next()? {
+ // Note that we generally pass ParserContext to is_ok() to check
+ // that the ParserMode of the ParserContext allows all numeric
+ // values for SMIL regardless of clamping_mode, but in this Time
+ // value case, the value does not animate for SMIL at all, so we use
+ // ParsingMode::DEFAULT directly.
+ Token::Dimension {
+ value, ref unit, ..
+ } if clamping_mode.is_ok(ParsingMode::DEFAULT, value) => {
+ Time::parse_dimension(value, unit)
+ .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError))
+ },
+ Token::Function(ref name) => {
+ let function = CalcNode::math_function(context, name, location)?;
+ CalcNode::parse_time(context, input, clamping_mode, function)
+ },
+ ref t => return Err(location.new_unexpected_token_error(t.clone())),
+ }
+ }
+
+ /// Parses a non-negative time value.
+ pub fn parse_non_negative<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative)
+ }
+}
+
+impl Zero for Time {
+ #[inline]
+ fn zero() -> Self {
+ Self::from_seconds(0.0)
+ }
+
+ #[inline]
+ fn is_zero(&self) -> bool {
+ // The unit doesn't matter, i.e. `s` and `ms` are the same for zero.
+ self.seconds == 0.0 && self.calc_clamping_mode.is_none()
+ }
+}
+
+impl ToComputedValue for Time {
+ type ComputedValue = ComputedTime;
+
+ fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue {
+ let seconds = self
+ .calc_clamping_mode
+ .map_or(self.seconds(), |mode| mode.clamp(self.seconds()));
+
+ ComputedTime::from_seconds(crate::values::normalize(seconds))
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ Time {
+ seconds: computed.seconds(),
+ unit: TimeUnit::Second,
+ calc_clamping_mode: None,
+ }
+ }
+}
+
+impl Parse for Time {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Self::parse_with_clamping_mode(context, input, AllowedNumericType::All)
+ }
+}
+
+impl ToCss for Time {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ crate::values::serialize_specified_dimension(
+ self.unitless_value(),
+ self.unit(),
+ self.calc_clamping_mode.is_some(),
+ dest,
+ )
+ }
+}
+
+impl SpecifiedValueInfo for Time {}
diff --git a/servo/components/style/values/specified/transform.rs b/servo/components/style/values/specified/transform.rs
new file mode 100644
index 0000000000..ec5e286bc2
--- /dev/null
+++ b/servo/components/style/values/specified/transform.rs
@@ -0,0 +1,530 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for CSS values that are related to transformations.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::computed::{Context, LengthPercentage as ComputedLengthPercentage};
+use crate::values::computed::{Percentage as ComputedPercentage, ToComputedValue};
+use crate::values::generics::transform as generic;
+use crate::values::generics::transform::{Matrix, Matrix3D};
+use crate::values::specified::position::{
+ HorizontalPositionKeyword, Side, VerticalPositionKeyword,
+};
+use crate::values::specified::{
+ self, Angle, Integer, Length, LengthPercentage, Number, NumberOrPercentage,
+};
+use crate::Zero;
+use cssparser::Parser;
+use style_traits::{ParseError, StyleParseErrorKind};
+
+pub use crate::values::generics::transform::TransformStyle;
+
+/// A single operation in a specified CSS `transform`
+pub type TransformOperation =
+ generic::TransformOperation<Angle, Number, Length, Integer, LengthPercentage>;
+
+/// A specified CSS `transform`
+pub type Transform = generic::Transform<TransformOperation>;
+
+/// The specified value of a CSS `<transform-origin>`
+pub type TransformOrigin = generic::TransformOrigin<
+ OriginComponent<HorizontalPositionKeyword>,
+ OriginComponent<VerticalPositionKeyword>,
+ Length,
+>;
+
+#[cfg(feature = "gecko")]
+fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
+ static_prefs::pref!("layout.css.transform-box-content-stroke.enabled")
+}
+
+#[cfg(feature = "servo")]
+fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool {
+ false
+}
+
+/// The specified value of `transform-box`.
+/// https://drafts.csswg.org/css-transforms-1/#transform-box
+// Note: Once we ship everything, we can drop this and just use single_keyword for tranform-box.
+#[allow(missing_docs)]
+#[derive(
+ Animate,
+ Clone,
+ ComputeSquaredDistance,
+ Copy,
+ Debug,
+ Deserialize,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ Serialize,
+ SpecifiedValueInfo,
+ ToAnimatedValue,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum TransformBox {
+ #[parse(condition = "all_transform_boxes_are_enabled")]
+ ContentBox,
+ BorderBox,
+ FillBox,
+ #[parse(condition = "all_transform_boxes_are_enabled")]
+ StrokeBox,
+ ViewBox,
+}
+
+impl TransformOrigin {
+ /// Returns the initial specified value for `transform-origin`.
+ #[inline]
+ pub fn initial_value() -> Self {
+ Self::new(
+ OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
+ OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))),
+ Length::zero(),
+ )
+ }
+
+ /// Returns the `0 0` value.
+ pub fn zero_zero() -> Self {
+ Self::new(
+ OriginComponent::Length(LengthPercentage::zero()),
+ OriginComponent::Length(LengthPercentage::zero()),
+ Length::zero(),
+ )
+ }
+}
+
+impl Transform {
+ /// Internal parse function for deciding if we wish to accept prefixed values or not
+ ///
+ /// `transform` allows unitless zero angles as an exception, see:
+ /// https://github.com/w3c/csswg-drafts/issues/1162
+ fn parse_internal<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use style_traits::{Separator, Space};
+
+ if input
+ .try_parse(|input| input.expect_ident_matching("none"))
+ .is_ok()
+ {
+ return Ok(generic::Transform::none());
+ }
+
+ Ok(generic::Transform(
+ Space::parse(input, |input| {
+ let function = input.expect_function()?.clone();
+ input.parse_nested_block(|input| {
+ let location = input.current_source_location();
+ let result = match_ignore_ascii_case! { &function,
+ "matrix" => {
+ let a = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let b = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let c = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let d = Number::parse(context, input)?;
+ input.expect_comma()?;
+ // Standard matrix parsing.
+ let e = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let f = Number::parse(context, input)?;
+ Ok(generic::TransformOperation::Matrix(Matrix { a, b, c, d, e, f }))
+ },
+ "matrix3d" => {
+ let m11 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m12 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m13 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m14 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m21 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m22 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m23 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m24 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m31 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m32 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m33 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m34 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ // Standard matrix3d parsing.
+ let m41 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m42 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m43 = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let m44 = Number::parse(context, input)?;
+ Ok(generic::TransformOperation::Matrix3D(Matrix3D {
+ m11, m12, m13, m14,
+ m21, m22, m23, m24,
+ m31, m32, m33, m34,
+ m41, m42, m43, m44,
+ }))
+ },
+ "translate" => {
+ let sx = specified::LengthPercentage::parse(context, input)?;
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ let sy = specified::LengthPercentage::parse(context, input)?;
+ Ok(generic::TransformOperation::Translate(sx, sy))
+ } else {
+ Ok(generic::TransformOperation::Translate(sx, Zero::zero()))
+ }
+ },
+ "translatex" => {
+ let tx = specified::LengthPercentage::parse(context, input)?;
+ Ok(generic::TransformOperation::TranslateX(tx))
+ },
+ "translatey" => {
+ let ty = specified::LengthPercentage::parse(context, input)?;
+ Ok(generic::TransformOperation::TranslateY(ty))
+ },
+ "translatez" => {
+ let tz = specified::Length::parse(context, input)?;
+ Ok(generic::TransformOperation::TranslateZ(tz))
+ },
+ "translate3d" => {
+ let tx = specified::LengthPercentage::parse(context, input)?;
+ input.expect_comma()?;
+ let ty = specified::LengthPercentage::parse(context, input)?;
+ input.expect_comma()?;
+ let tz = specified::Length::parse(context, input)?;
+ Ok(generic::TransformOperation::Translate3D(tx, ty, tz))
+ },
+ "scale" => {
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ let sy = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::Scale(sx, sy))
+ } else {
+ Ok(generic::TransformOperation::Scale(sx, sx))
+ }
+ },
+ "scalex" => {
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::ScaleX(sx))
+ },
+ "scaley" => {
+ let sy = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::ScaleY(sy))
+ },
+ "scalez" => {
+ let sz = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::ScaleZ(sz))
+ },
+ "scale3d" => {
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ input.expect_comma()?;
+ let sy = NumberOrPercentage::parse(context, input)?.to_number();
+ input.expect_comma()?;
+ let sz = NumberOrPercentage::parse(context, input)?.to_number();
+ Ok(generic::TransformOperation::Scale3D(sx, sy, sz))
+ },
+ "rotate" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::Rotate(theta))
+ },
+ "rotatex" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::RotateX(theta))
+ },
+ "rotatey" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::RotateY(theta))
+ },
+ "rotatez" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::RotateZ(theta))
+ },
+ "rotate3d" => {
+ let ax = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let ay = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let az = Number::parse(context, input)?;
+ input.expect_comma()?;
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ // TODO(gw): Check that the axis can be normalized.
+ Ok(generic::TransformOperation::Rotate3D(ax, ay, az, theta))
+ },
+ "skew" => {
+ let ax = specified::Angle::parse_with_unitless(context, input)?;
+ if input.try_parse(|input| input.expect_comma()).is_ok() {
+ let ay = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::Skew(ax, ay))
+ } else {
+ Ok(generic::TransformOperation::Skew(ax, Zero::zero()))
+ }
+ },
+ "skewx" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::SkewX(theta))
+ },
+ "skewy" => {
+ let theta = specified::Angle::parse_with_unitless(context, input)?;
+ Ok(generic::TransformOperation::SkewY(theta))
+ },
+ "perspective" => {
+ let p = match input.try_parse(|input| specified::Length::parse_non_negative(context, input)) {
+ Ok(p) => generic::PerspectiveFunction::Length(p),
+ Err(..) => {
+ input.expect_ident_matching("none")?;
+ generic::PerspectiveFunction::None
+ }
+ };
+ Ok(generic::TransformOperation::Perspective(p))
+ },
+ _ => Err(()),
+ };
+ result.map_err(|()| {
+ location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(
+ function.clone(),
+ ))
+ })
+ })
+ })?
+ .into(),
+ ))
+ }
+}
+
+impl Parse for Transform {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ Transform::parse_internal(context, input)
+ }
+}
+
+/// The specified value of a component of a CSS `<transform-origin>`.
+#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)]
+pub enum OriginComponent<S> {
+ /// `center`
+ Center,
+ /// `<length-percentage>`
+ Length(LengthPercentage),
+ /// `<side>`
+ Side(S),
+}
+
+impl Parse for TransformOrigin {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let parse_depth = |input: &mut Parser| {
+ input
+ .try_parse(|i| Length::parse(context, i))
+ .unwrap_or(Length::zero())
+ };
+ match input.try_parse(|i| OriginComponent::parse(context, i)) {
+ Ok(x_origin @ OriginComponent::Center) => {
+ if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let y_origin = OriginComponent::Center;
+ if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
+ let x_origin = OriginComponent::Side(x_keyword);
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let depth = Length::from_px(0.);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ },
+ Ok(x_origin) => {
+ if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) {
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let y_origin = OriginComponent::Center;
+ let depth = Length::from_px(0.);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ },
+ Err(_) => {},
+ }
+ let y_keyword = VerticalPositionKeyword::parse(input)?;
+ let y_origin = OriginComponent::Side(y_keyword);
+ if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) {
+ let x_origin = OriginComponent::Side(x_keyword);
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ if input
+ .try_parse(|i| i.expect_ident_matching("center"))
+ .is_ok()
+ {
+ let x_origin = OriginComponent::Center;
+ let depth = parse_depth(input);
+ return Ok(Self::new(x_origin, y_origin, depth));
+ }
+ let x_origin = OriginComponent::Center;
+ let depth = Length::from_px(0.);
+ Ok(Self::new(x_origin, y_origin, depth))
+ }
+}
+
+impl<S> ToComputedValue for OriginComponent<S>
+where
+ S: Side,
+{
+ type ComputedValue = ComputedLengthPercentage;
+
+ fn to_computed_value(&self, context: &Context) -> Self::ComputedValue {
+ match *self {
+ OriginComponent::Center => {
+ ComputedLengthPercentage::new_percent(ComputedPercentage(0.5))
+ },
+ OriginComponent::Length(ref length) => length.to_computed_value(context),
+ OriginComponent::Side(ref keyword) => {
+ let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. });
+ ComputedLengthPercentage::new_percent(p)
+ },
+ }
+ }
+
+ fn from_computed_value(computed: &Self::ComputedValue) -> Self {
+ OriginComponent::Length(ToComputedValue::from_computed_value(computed))
+ }
+}
+
+impl<S> OriginComponent<S> {
+ /// `0%`
+ pub fn zero() -> Self {
+ OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage::zero()))
+ }
+}
+
+/// A specified CSS `rotate`
+pub type Rotate = generic::Rotate<Number, Angle>;
+
+impl Parse for Rotate {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(generic::Rotate::None);
+ }
+
+ // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>.
+ //
+ // The rotate axis and angle could be in any order, so we parse angle twice to cover
+ // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}`
+ let angle = input
+ .try_parse(|i| specified::Angle::parse(context, i))
+ .ok();
+ let axis = input
+ .try_parse(|i| {
+ Ok(try_match_ident_ignore_ascii_case! { i,
+ "x" => (Number::new(1.), Number::new(0.), Number::new(0.)),
+ "y" => (Number::new(0.), Number::new(1.), Number::new(0.)),
+ "z" => (Number::new(0.), Number::new(0.), Number::new(1.)),
+ })
+ })
+ .or_else(|_: ParseError| -> Result<_, ParseError> {
+ input.try_parse(|i| {
+ Ok((
+ Number::parse(context, i)?,
+ Number::parse(context, i)?,
+ Number::parse(context, i)?,
+ ))
+ })
+ })
+ .ok();
+ let angle = match angle {
+ Some(a) => a,
+ None => specified::Angle::parse(context, input)?,
+ };
+
+ Ok(match axis {
+ Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle),
+ None => generic::Rotate::Rotate(angle),
+ })
+ }
+}
+
+/// A specified CSS `translate`
+pub type Translate = generic::Translate<LengthPercentage, Length>;
+
+impl Parse for Translate {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(generic::Translate::None);
+ }
+
+ let tx = specified::LengthPercentage::parse(context, input)?;
+ if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) {
+ if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) {
+ // 'translate: <length-percentage> <length-percentage> <length>'
+ return Ok(generic::Translate::Translate(tx, ty, tz));
+ }
+
+ // translate: <length-percentage> <length-percentage>'
+ return Ok(generic::Translate::Translate(
+ tx,
+ ty,
+ specified::Length::zero(),
+ ));
+ }
+
+ // 'translate: <length-percentage> '
+ Ok(generic::Translate::Translate(
+ tx,
+ specified::LengthPercentage::zero(),
+ specified::Length::zero(),
+ ))
+ }
+}
+
+/// A specified CSS `scale`
+pub type Scale = generic::Scale<Number>;
+
+impl Parse for Scale {
+ /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage,
+ /// and then convert into an Number if it's a Percentage.
+ /// https://github.com/w3c/csswg-drafts/pull/4396
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
+ return Ok(generic::Scale::None);
+ }
+
+ let sx = NumberOrPercentage::parse(context, input)?.to_number();
+ if let Ok(sy) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
+ let sy = sy.to_number();
+ if let Ok(sz) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) {
+ // 'scale: <number> <number> <number>'
+ return Ok(generic::Scale::Scale(sx, sy, sz.to_number()));
+ }
+
+ // 'scale: <number> <number>'
+ return Ok(generic::Scale::Scale(sx, sy, Number::new(1.0)));
+ }
+
+ // 'scale: <number>'
+ Ok(generic::Scale::Scale(sx, sx, Number::new(1.0)))
+ }
+}
diff --git a/servo/components/style/values/specified/ui.rs b/servo/components/style/values/specified/ui.rs
new file mode 100644
index 0000000000..2237335ec4
--- /dev/null
+++ b/servo/components/style/values/specified/ui.rs
@@ -0,0 +1,257 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Specified types for UI properties.
+
+use crate::parser::{Parse, ParserContext};
+use crate::values::generics::ui as generics;
+use crate::values::specified::color::Color;
+use crate::values::specified::image::Image;
+use crate::values::specified::Number;
+use cssparser::Parser;
+use std::fmt::{self, Write};
+use style_traits::{
+ CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss,
+};
+
+/// A specified value for the `cursor` property.
+pub type Cursor = generics::GenericCursor<CursorImage>;
+
+/// A specified value for item of `image cursors`.
+pub type CursorImage = generics::GenericCursorImage<Image, Number>;
+
+impl Parse for Cursor {
+ /// cursor: [<url> [<number> <number>]?]# [auto | default | ...]
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ let mut images = vec![];
+ loop {
+ match input.try_parse(|input| CursorImage::parse(context, input)) {
+ Ok(image) => images.push(image),
+ Err(_) => break,
+ }
+ input.expect_comma()?;
+ }
+ Ok(Self {
+ images: images.into(),
+ keyword: CursorKind::parse(input)?,
+ })
+ }
+}
+
+impl Parse for CursorImage {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ use crate::Zero;
+
+ let image = Image::parse_only_url(context, input)?;
+ let mut has_hotspot = false;
+ let mut hotspot_x = Number::zero();
+ let mut hotspot_y = Number::zero();
+
+ if let Ok(x) = input.try_parse(|input| Number::parse(context, input)) {
+ has_hotspot = true;
+ hotspot_x = x;
+ hotspot_y = Number::parse(context, input)?;
+ }
+
+ Ok(Self {
+ image,
+ has_hotspot,
+ hotspot_x,
+ hotspot_y,
+ })
+ }
+}
+
+// This trait is manually implemented because we don't support the whole <image>
+// syntax for cursors
+impl SpecifiedValueInfo for CursorImage {
+ fn collect_completion_keywords(f: KeywordsCollectFn) {
+ f(&["url", "image-set"]);
+ }
+}
+/// Specified value of `-moz-force-broken-image-icon`
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ MallocSizeOf,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(transparent)]
+pub struct BoolInteger(pub bool);
+
+impl BoolInteger {
+ /// Returns 0
+ #[inline]
+ pub fn zero() -> Self {
+ Self(false)
+ }
+}
+
+impl Parse for BoolInteger {
+ fn parse<'i, 't>(
+ _context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ // We intentionally don't support calc values here.
+ match input.expect_integer()? {
+ 0 => Ok(Self(false)),
+ 1 => Ok(Self(true)),
+ _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)),
+ }
+ }
+}
+
+impl ToCss for BoolInteger {
+ fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
+ where
+ W: Write,
+ {
+ dest.write_str(if self.0 { "1" } else { "0" })
+ }
+}
+
+/// A specified value for `scrollbar-color` property
+pub type ScrollbarColor = generics::ScrollbarColor<Color>;
+
+impl Parse for ScrollbarColor {
+ fn parse<'i, 't>(
+ context: &ParserContext,
+ input: &mut Parser<'i, 't>,
+ ) -> Result<Self, ParseError<'i>> {
+ if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() {
+ return Ok(generics::ScrollbarColor::Auto);
+ }
+ Ok(generics::ScrollbarColor::Colors {
+ thumb: Color::parse(context, input)?,
+ track: Color::parse(context, input)?,
+ })
+ }
+}
+
+/// The specified value for the `user-select` property.
+///
+/// https://drafts.csswg.org/css-ui-4/#propdef-user-select
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum UserSelect {
+ Auto,
+ Text,
+ #[parse(aliases = "-moz-none")]
+ None,
+ /// Force selection of all children.
+ All,
+}
+
+/// The keywords allowed in the Cursor property.
+///
+/// https://drafts.csswg.org/css-ui-4/#propdef-cursor
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum CursorKind {
+ None,
+ Default,
+ Pointer,
+ ContextMenu,
+ Help,
+ Progress,
+ Wait,
+ Cell,
+ Crosshair,
+ Text,
+ VerticalText,
+ Alias,
+ Copy,
+ Move,
+ NoDrop,
+ NotAllowed,
+ #[parse(aliases = "-moz-grab")]
+ Grab,
+ #[parse(aliases = "-moz-grabbing")]
+ Grabbing,
+ EResize,
+ NResize,
+ NeResize,
+ NwResize,
+ SResize,
+ SeResize,
+ SwResize,
+ WResize,
+ EwResize,
+ NsResize,
+ NeswResize,
+ NwseResize,
+ ColResize,
+ RowResize,
+ AllScroll,
+ #[parse(aliases = "-moz-zoom-in")]
+ ZoomIn,
+ #[parse(aliases = "-moz-zoom-out")]
+ ZoomOut,
+ Auto,
+}
+
+/// The keywords allowed in the -moz-theme property.
+#[allow(missing_docs)]
+#[derive(
+ Clone,
+ Copy,
+ Debug,
+ Eq,
+ FromPrimitive,
+ MallocSizeOf,
+ Parse,
+ PartialEq,
+ SpecifiedValueInfo,
+ ToComputedValue,
+ ToCss,
+ ToResolvedValue,
+ ToShmem,
+)]
+#[repr(u8)]
+pub enum MozTheme {
+ /// Choose the default (maybe native) rendering.
+ Auto,
+ /// Choose the non-native rendering.
+ NonNative,
+}
diff --git a/servo/components/style/values/specified/url.rs b/servo/components/style/values/specified/url.rs
new file mode 100644
index 0000000000..17ecbe0d5e
--- /dev/null
+++ b/servo/components/style/values/specified/url.rs
@@ -0,0 +1,15 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+//! Common handling for the specified value CSS url() values.
+
+use crate::values::generics::url::GenericUrlOrNone;
+
+#[cfg(feature = "gecko")]
+pub use crate::gecko::url::{SpecifiedImageUrl, SpecifiedUrl};
+#[cfg(feature = "servo")]
+pub use crate::servo::url::{SpecifiedImageUrl, SpecifiedUrl};
+
+/// Specified <url> | <none>
+pub type UrlOrNone = GenericUrlOrNone<SpecifiedUrl>;