diff options
Diffstat (limited to 'servo/components/style/values/specified')
38 files changed, 20955 insertions, 0 deletions
diff --git a/servo/components/style/values/specified/align.rs b/servo/components/style/values/specified/align.rs new file mode 100644 index 0000000000..60eca4556b --- /dev/null +++ b/servo/components/style/values/specified/align.rs @@ -0,0 +1,820 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Values for CSS Box Alignment properties +//! +//! https://drafts.csswg.org/css-align/ + +use crate::parser::{Parse, ParserContext}; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, ToCss}; + +/// Constants shared by multiple CSS Box Alignment properties +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(C)] +pub struct AlignFlags(u8); +bitflags! { + impl AlignFlags: u8 { + // Enumeration stored in the lower 5 bits: + /// {align,justify}-{content,items,self}: 'auto' + const AUTO = 0; + /// 'normal' + const NORMAL = 1; + /// 'start' + const START = 2; + /// 'end' + const END = 3; + /// 'flex-start' + const FLEX_START = 4; + /// 'flex-end' + const FLEX_END = 5; + /// 'center' + const CENTER = 6; + /// 'left' + const LEFT = 7; + /// 'right' + const RIGHT = 8; + /// 'baseline' + const BASELINE = 9; + /// 'last-baseline' + const LAST_BASELINE = 10; + /// 'stretch' + const STRETCH = 11; + /// 'self-start' + const SELF_START = 12; + /// 'self-end' + const SELF_END = 13; + /// 'space-between' + const SPACE_BETWEEN = 14; + /// 'space-around' + const SPACE_AROUND = 15; + /// 'space-evenly' + const SPACE_EVENLY = 16; + + // Additional flags stored in the upper bits: + /// 'legacy' (mutually exclusive w. SAFE & UNSAFE) + const LEGACY = 1 << 5; + /// 'safe' + const SAFE = 1 << 6; + /// 'unsafe' (mutually exclusive w. SAFE) + const UNSAFE = 1 << 7; + + /// Mask for the additional flags above. + const FLAG_BITS = 0b11100000; + } +} + +impl AlignFlags { + /// Returns the enumeration value stored in the lower 5 bits. + #[inline] + fn value(&self) -> Self { + *self & !AlignFlags::FLAG_BITS + } +} + +impl ToCss for AlignFlags { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + let extra_flags = *self & AlignFlags::FLAG_BITS; + let value = self.value(); + + match extra_flags { + AlignFlags::LEGACY => { + dest.write_str("legacy")?; + if value.is_empty() { + return Ok(()); + } + dest.write_char(' ')?; + }, + AlignFlags::SAFE => dest.write_str("safe ")?, + AlignFlags::UNSAFE => dest.write_str("unsafe ")?, + _ => { + debug_assert_eq!(extra_flags, AlignFlags::empty()); + }, + } + + dest.write_str(match value { + AlignFlags::AUTO => "auto", + AlignFlags::NORMAL => "normal", + AlignFlags::START => "start", + AlignFlags::END => "end", + AlignFlags::FLEX_START => "flex-start", + AlignFlags::FLEX_END => "flex-end", + AlignFlags::CENTER => "center", + AlignFlags::LEFT => "left", + AlignFlags::RIGHT => "right", + AlignFlags::BASELINE => "baseline", + AlignFlags::LAST_BASELINE => "last baseline", + AlignFlags::STRETCH => "stretch", + AlignFlags::SELF_START => "self-start", + AlignFlags::SELF_END => "self-end", + AlignFlags::SPACE_BETWEEN => "space-between", + AlignFlags::SPACE_AROUND => "space-around", + AlignFlags::SPACE_EVENLY => "space-evenly", + _ => unreachable!(), + }) + } +} + +/// An axis direction, either inline (for the `justify` properties) or block, +/// (for the `align` properties). +#[derive(Clone, Copy, PartialEq)] +pub enum AxisDirection { + /// Block direction. + Block, + /// Inline direction. + Inline, +} + +/// Shared value for the `align-content` and `justify-content` properties. +/// +/// <https://drafts.csswg.org/css-align/#content-distribution> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +pub struct ContentDistribution { + primary: AlignFlags, + // FIXME(https://github.com/w3c/csswg-drafts/issues/1002): This will need to + // accept fallback alignment, eventually. +} + +impl ContentDistribution { + /// The initial value 'normal' + #[inline] + pub fn normal() -> Self { + Self::new(AlignFlags::NORMAL) + } + + /// `start` + #[inline] + pub fn start() -> Self { + Self::new(AlignFlags::START) + } + + /// The initial value 'normal' + #[inline] + pub fn new(primary: AlignFlags) -> Self { + Self { primary } + } + + /// Returns whether this value is a <baseline-position>. + pub fn is_baseline_position(&self) -> bool { + matches!( + self.primary.value(), + AlignFlags::BASELINE | AlignFlags::LAST_BASELINE + ) + } + + /// The primary alignment + #[inline] + pub fn primary(self) -> AlignFlags { + self.primary + } + + /// Parse a value for align-content / justify-content. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update the `list_keywords` function below + // when this function is updated. + + // Try to parse normal first + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(ContentDistribution::normal()); + } + + // Parse <baseline-position>, but only on the block axis. + if axis == AxisDirection::Block { + if let Ok(value) = input.try_parse(parse_baseline) { + return Ok(ContentDistribution::new(value)); + } + } + + // <content-distribution> + if let Ok(value) = input.try_parse(parse_content_distribution) { + return Ok(ContentDistribution::new(value)); + } + + // <overflow-position>? <content-position> + let overflow_position = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + + let content_position = try_match_ident_ignore_ascii_case! { input, + "start" => AlignFlags::START, + "end" => AlignFlags::END, + "flex-start" => AlignFlags::FLEX_START, + "flex-end" => AlignFlags::FLEX_END, + "center" => AlignFlags::CENTER, + "left" if axis == AxisDirection::Inline => AlignFlags::LEFT, + "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT, + }; + + Ok(ContentDistribution::new( + content_position | overflow_position, + )) + } + + fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + f(&["normal"]); + if axis == AxisDirection::Block { + list_baseline_keywords(f); + } + list_content_distribution_keywords(f); + list_overflow_position_keywords(f); + f(&["start", "end", "flex-start", "flex-end", "center"]); + if axis == AxisDirection::Inline { + f(&["left", "right"]); + } + } +} + +/// Value for the `align-content` property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-content> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct AlignContent(pub ContentDistribution); + +impl Parse for AlignContent { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(AlignContent(ContentDistribution::parse( + input, + AxisDirection::Block, + )?)) + } +} + +impl SpecifiedValueInfo for AlignContent { + fn collect_completion_keywords(f: KeywordsCollectFn) { + ContentDistribution::list_keywords(f, AxisDirection::Block); + } +} + +/// Value for the `align-tracks` property. +/// +/// <https://github.com/w3c/csswg-drafts/issues/4650> +#[derive( + Clone, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +#[css(comma)] +pub struct AlignTracks(#[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<AlignContent>); + +impl Parse for AlignTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(|input| AlignContent::parse(context, input))?; + Ok(AlignTracks(values.into())) + } +} + +/// Value for the `justify-content` property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-justify-content> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct JustifyContent(pub ContentDistribution); + +impl Parse for JustifyContent { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(JustifyContent(ContentDistribution::parse( + input, + AxisDirection::Inline, + )?)) + } +} + +impl SpecifiedValueInfo for JustifyContent { + fn collect_completion_keywords(f: KeywordsCollectFn) { + ContentDistribution::list_keywords(f, AxisDirection::Inline); + } +} +/// Value for the `justify-tracks` property. +/// +/// <https://github.com/w3c/csswg-drafts/issues/4650> +#[derive( + Clone, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +#[css(comma)] +pub struct JustifyTracks( + #[css(iterable, if_empty = "normal")] pub crate::OwnedSlice<JustifyContent>, +); + +impl Parse for JustifyTracks { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let values = input.parse_comma_separated(|input| JustifyContent::parse(context, input))?; + Ok(JustifyTracks(values.into())) + } +} + +/// <https://drafts.csswg.org/css-align/#self-alignment> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct SelfAlignment(pub AlignFlags); + +impl SelfAlignment { + /// The initial value 'auto' + #[inline] + pub fn auto() -> Self { + SelfAlignment(AlignFlags::AUTO) + } + + /// Returns whether this value is valid for both axis directions. + pub fn is_valid_on_both_axes(&self) -> bool { + match self.0.value() { + // left | right are only allowed on the inline axis. + AlignFlags::LEFT | AlignFlags::RIGHT => false, + + _ => true, + } + } + + /// Parse a self-alignment value on one of the axis. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update the `list_keywords` function below + // when this function is updated. + + // <baseline-position> + // + // It's weird that this accepts <baseline-position>, but not + // justify-content... + if let Ok(value) = input.try_parse(parse_baseline) { + return Ok(SelfAlignment(value)); + } + + // auto | normal | stretch + if let Ok(value) = input.try_parse(parse_auto_normal_stretch) { + return Ok(SelfAlignment(value)); + } + + // <overflow-position>? <self-position> + let overflow_position = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, axis)?; + Ok(SelfAlignment(overflow_position | self_position)) + } + + fn list_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + list_baseline_keywords(f); + list_auto_normal_stretch(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, axis); + } +} + +/// The specified value of the align-self property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-self> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct AlignSelf(pub SelfAlignment); + +impl Parse for AlignSelf { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(AlignSelf(SelfAlignment::parse( + input, + AxisDirection::Block, + )?)) + } +} + +impl SpecifiedValueInfo for AlignSelf { + fn collect_completion_keywords(f: KeywordsCollectFn) { + SelfAlignment::list_keywords(f, AxisDirection::Block); + } +} + +/// The specified value of the justify-self property. +/// +/// <https://drafts.csswg.org/css-align/#propdef-justify-self> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct JustifySelf(pub SelfAlignment); + +impl Parse for JustifySelf { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + Ok(JustifySelf(SelfAlignment::parse( + input, + AxisDirection::Inline, + )?)) + } +} + +impl SpecifiedValueInfo for JustifySelf { + fn collect_completion_keywords(f: KeywordsCollectFn) { + SelfAlignment::list_keywords(f, AxisDirection::Inline); + } +} + +/// Value of the `align-items` property +/// +/// <https://drafts.csswg.org/css-align/#propdef-align-items> +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct AlignItems(pub AlignFlags); + +impl AlignItems { + /// The initial value 'normal' + #[inline] + pub fn normal() -> Self { + AlignItems(AlignFlags::NORMAL) + } +} + +impl Parse for AlignItems { + // normal | stretch | <baseline-position> | + // <overflow-position>? <self-position> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // <baseline-position> + if let Ok(baseline) = input.try_parse(parse_baseline) { + return Ok(AlignItems(baseline)); + } + + // normal | stretch + if let Ok(value) = input.try_parse(parse_normal_stretch) { + return Ok(AlignItems(value)); + } + // <overflow-position>? <self-position> + let overflow = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, AxisDirection::Block)?; + Ok(AlignItems(self_position | overflow)) + } +} + +impl SpecifiedValueInfo for AlignItems { + fn collect_completion_keywords(f: KeywordsCollectFn) { + list_baseline_keywords(f); + list_normal_stretch(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, AxisDirection::Block); + } +} + +/// Value of the `justify-items` property +/// +/// <https://drafts.csswg.org/css-align/#justify-items-property> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToCss, ToResolvedValue, ToShmem)] +#[repr(C)] +pub struct JustifyItems(pub AlignFlags); + +impl JustifyItems { + /// The initial value 'legacy' + #[inline] + pub fn legacy() -> Self { + JustifyItems(AlignFlags::LEGACY) + } + + /// The value 'normal' + #[inline] + pub fn normal() -> Self { + JustifyItems(AlignFlags::NORMAL) + } +} + +impl Parse for JustifyItems { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // NOTE Please also update `impl SpecifiedValueInfo` below when + // this function is updated. + + // <baseline-position> + // + // It's weird that this accepts <baseline-position>, but not + // justify-content... + if let Ok(baseline) = input.try_parse(parse_baseline) { + return Ok(JustifyItems(baseline)); + } + + // normal | stretch + if let Ok(value) = input.try_parse(parse_normal_stretch) { + return Ok(JustifyItems(value)); + } + + // legacy | [ legacy && [ left | right | center ] ] + if let Ok(value) = input.try_parse(parse_legacy) { + return Ok(JustifyItems(value)); + } + + // <overflow-position>? <self-position> + let overflow = input + .try_parse(parse_overflow_position) + .unwrap_or(AlignFlags::empty()); + let self_position = parse_self_position(input, AxisDirection::Inline)?; + Ok(JustifyItems(overflow | self_position)) + } +} + +impl SpecifiedValueInfo for JustifyItems { + fn collect_completion_keywords(f: KeywordsCollectFn) { + list_baseline_keywords(f); + list_normal_stretch(f); + list_legacy_keywords(f); + list_overflow_position_keywords(f); + list_self_position_keywords(f, AxisDirection::Inline); + } +} + +// auto | normal | stretch +fn parse_auto_normal_stretch<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_auto_normal_stretch` function + // below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "auto" => Ok(AlignFlags::AUTO), + "normal" => Ok(AlignFlags::NORMAL), + "stretch" => Ok(AlignFlags::STRETCH), + } +} + +fn list_auto_normal_stretch(f: KeywordsCollectFn) { + f(&["auto", "normal", "stretch"]); +} + +// normal | stretch +fn parse_normal_stretch<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_normal_stretch` function below + // when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "normal" => Ok(AlignFlags::NORMAL), + "stretch" => Ok(AlignFlags::STRETCH), + } +} + +fn list_normal_stretch(f: KeywordsCollectFn) { + f(&["normal", "stretch"]); +} + +// <baseline-position> +fn parse_baseline<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_baseline_keywords` function + // below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "baseline" => Ok(AlignFlags::BASELINE), + "first" => { + input.expect_ident_matching("baseline")?; + Ok(AlignFlags::BASELINE) + }, + "last" => { + input.expect_ident_matching("baseline")?; + Ok(AlignFlags::LAST_BASELINE) + }, + } +} + +fn list_baseline_keywords(f: KeywordsCollectFn) { + f(&["baseline", "first baseline", "last baseline"]); +} + +// <content-distribution> +fn parse_content_distribution<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_content_distribution_keywords` + // function below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "stretch" => Ok(AlignFlags::STRETCH), + "space-between" => Ok(AlignFlags::SPACE_BETWEEN), + "space-around" => Ok(AlignFlags::SPACE_AROUND), + "space-evenly" => Ok(AlignFlags::SPACE_EVENLY), + } +} + +fn list_content_distribution_keywords(f: KeywordsCollectFn) { + f(&["stretch", "space-between", "space-around", "space-evenly"]); +} + +// <overflow-position> +fn parse_overflow_position<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_overflow_position_keywords` + // function below when this function is updated. + try_match_ident_ignore_ascii_case! { input, + "safe" => Ok(AlignFlags::SAFE), + "unsafe" => Ok(AlignFlags::UNSAFE), + } +} + +fn list_overflow_position_keywords(f: KeywordsCollectFn) { + f(&["safe", "unsafe"]); +} + +// <self-position> | left | right in the inline axis. +fn parse_self_position<'i, 't>( + input: &mut Parser<'i, 't>, + axis: AxisDirection, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_self_position_keywords` + // function below when this function is updated. + Ok(try_match_ident_ignore_ascii_case! { input, + "start" => AlignFlags::START, + "end" => AlignFlags::END, + "flex-start" => AlignFlags::FLEX_START, + "flex-end" => AlignFlags::FLEX_END, + "center" => AlignFlags::CENTER, + "self-start" => AlignFlags::SELF_START, + "self-end" => AlignFlags::SELF_END, + "left" if axis == AxisDirection::Inline => AlignFlags::LEFT, + "right" if axis == AxisDirection::Inline => AlignFlags::RIGHT, + }) +} + +fn list_self_position_keywords(f: KeywordsCollectFn, axis: AxisDirection) { + f(&[ + "start", + "end", + "flex-start", + "flex-end", + "center", + "self-start", + "self-end", + ]); + if axis == AxisDirection::Inline { + f(&["left", "right"]); + } +} + +fn parse_left_right_center<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_legacy_keywords` function below + // when this function is updated. + Ok(try_match_ident_ignore_ascii_case! { input, + "left" => AlignFlags::LEFT, + "right" => AlignFlags::RIGHT, + "center" => AlignFlags::CENTER, + }) +} + +// legacy | [ legacy && [ left | right | center ] ] +fn parse_legacy<'i, 't>(input: &mut Parser<'i, 't>) -> Result<AlignFlags, ParseError<'i>> { + // NOTE Please also update the `list_legacy_keywords` function below + // when this function is updated. + let flags = try_match_ident_ignore_ascii_case! { input, + "legacy" => { + let flags = input.try_parse(parse_left_right_center) + .unwrap_or(AlignFlags::empty()); + + return Ok(AlignFlags::LEGACY | flags) + }, + "left" => AlignFlags::LEFT, + "right" => AlignFlags::RIGHT, + "center" => AlignFlags::CENTER, + }; + + input.expect_ident_matching("legacy")?; + Ok(AlignFlags::LEGACY | flags) +} + +fn list_legacy_keywords(f: KeywordsCollectFn) { + f(&["legacy", "left", "right", "center"]); +} diff --git a/servo/components/style/values/specified/angle.rs b/servo/components/style/values/specified/angle.rs new file mode 100644 index 0000000000..fb4554eb85 --- /dev/null +++ b/servo/components/style/values/specified/angle.rs @@ -0,0 +1,276 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified angles. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::angle::Angle as ComputedAngle; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::CSSFloat; +use crate::Zero; +use cssparser::{Parser, Token}; +use std::f32::consts::PI; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss}; + +/// A specified angle dimension. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, ToCss, ToShmem)] +pub enum AngleDimension { + /// An angle with degree unit. + #[css(dimension)] + Deg(CSSFloat), + /// An angle with gradian unit. + #[css(dimension)] + Grad(CSSFloat), + /// An angle with radian unit. + #[css(dimension)] + Rad(CSSFloat), + /// An angle with turn unit. + #[css(dimension)] + Turn(CSSFloat), +} + +impl Zero for AngleDimension { + fn zero() -> Self { + AngleDimension::Deg(0.) + } + + fn is_zero(&self) -> bool { + self.unitless_value() == 0.0 + } +} + +impl AngleDimension { + /// Returns the amount of degrees this angle represents. + #[inline] + fn degrees(&self) -> CSSFloat { + const DEG_PER_RAD: f32 = 180.0 / PI; + const DEG_PER_TURN: f32 = 360.0; + const DEG_PER_GRAD: f32 = 180.0 / 200.0; + + match *self { + AngleDimension::Deg(d) => d, + AngleDimension::Rad(rad) => rad * DEG_PER_RAD, + AngleDimension::Turn(turns) => turns * DEG_PER_TURN, + AngleDimension::Grad(gradians) => gradians * DEG_PER_GRAD, + } + } + + fn unitless_value(&self) -> CSSFloat { + match *self { + AngleDimension::Deg(v) | + AngleDimension::Rad(v) | + AngleDimension::Turn(v) | + AngleDimension::Grad(v) => v, + } + } + + fn unit(&self) -> &'static str { + match *self { + AngleDimension::Deg(_) => "deg", + AngleDimension::Rad(_) => "rad", + AngleDimension::Turn(_) => "turn", + AngleDimension::Grad(_) => "grad", + } + } +} + +/// A specified Angle value, which is just the angle dimension, plus whether it +/// was specified as `calc()` or not. +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Angle { + value: AngleDimension, + was_calc: bool, +} + +impl Zero for Angle { + fn zero() -> Self { + Self { + value: Zero::zero(), + was_calc: false, + } + } + + fn is_zero(&self) -> bool { + self.value.is_zero() + } +} + +impl ToCss for Angle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.value.unitless_value(), + self.value.unit(), + self.was_calc, + dest, + ) + } +} + +impl ToComputedValue for Angle { + type ComputedValue = ComputedAngle; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + let degrees = self.degrees(); + + // NaN and +-infinity should degenerate to 0: https://github.com/w3c/csswg-drafts/issues/6105 + ComputedAngle::from_degrees(if degrees.is_finite() { degrees } else { 0.0 }) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Angle { + value: AngleDimension::Deg(computed.degrees()), + was_calc: false, + } + } +} + +impl Angle { + /// Creates an angle with the given value in degrees. + #[inline] + pub fn from_degrees(value: CSSFloat, was_calc: bool) -> Self { + Angle { + value: AngleDimension::Deg(value), + was_calc, + } + } + + /// Creates an angle with the given value in radians. + #[inline] + pub fn from_radians(value: CSSFloat) -> Self { + Angle { + value: AngleDimension::Rad(value), + was_calc: false, + } + } + + /// Return `0deg`. + pub fn zero() -> Self { + Self::from_degrees(0.0, false) + } + + /// Returns the value of the angle in degrees, mostly for `calc()`. + #[inline] + pub fn degrees(&self) -> CSSFloat { + self.value.degrees() + } + + /// Returns the value of the angle in radians. + #[inline] + pub fn radians(&self) -> CSSFloat { + const RAD_PER_DEG: f32 = PI / 180.0; + self.value.degrees() * RAD_PER_DEG + } + + /// Whether this specified angle came from a `calc()` expression. + #[inline] + pub fn was_calc(&self) -> bool { + self.was_calc + } + + /// Returns an `Angle` parsed from a `calc()` expression. + pub fn from_calc(degrees: CSSFloat) -> Self { + Angle { + value: AngleDimension::Deg(degrees), + was_calc: true, + } + } + + /// Returns the unit of the angle. + #[inline] + pub fn unit(&self) -> &'static str { + self.value.unit() + } +} + +/// Whether to allow parsing an unitless zero as a valid angle. +/// +/// This should always be `No`, except for exceptions like: +/// +/// https://github.com/w3c/fxtf-drafts/issues/228 +/// +/// See also: https://github.com/w3c/csswg-drafts/issues/1162. +#[allow(missing_docs)] +pub enum AllowUnitlessZeroAngle { + Yes, + No, +} + +impl Parse for Angle { + /// Parses an angle according to CSS-VALUES § 6.1. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowUnitlessZeroAngle::No) + } +} + +impl Angle { + /// Parse an `<angle>` value given a value and an unit. + pub fn parse_dimension(value: CSSFloat, unit: &str, was_calc: bool) -> Result<Angle, ()> { + let value = match_ignore_ascii_case! { unit, + "deg" => AngleDimension::Deg(value), + "grad" => AngleDimension::Grad(value), + "turn" => AngleDimension::Turn(value), + "rad" => AngleDimension::Rad(value), + _ => return Err(()) + }; + + Ok(Self { value, was_calc }) + } + + /// Parse an `<angle>` allowing unitless zero to represent a zero angle. + /// + /// See the comment in `AllowUnitlessZeroAngle` for why. + #[inline] + pub fn parse_with_unitless<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowUnitlessZeroAngle::Yes) + } + + pub(super) fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_unitless_zero: AllowUnitlessZeroAngle, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let t = input.next()?; + let allow_unitless_zero = matches!(allow_unitless_zero, AllowUnitlessZeroAngle::Yes); + match *t { + Token::Dimension { + value, ref unit, .. + } => { + match Angle::parse_dimension(value, unit, /* from_calc = */ false) { + Ok(angle) => Ok(angle), + Err(()) => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse_angle(context, input, function) + }, + Token::Number { value, .. } if value == 0. && allow_unitless_zero => Ok(Angle::zero()), + ref t => { + let t = t.clone(); + Err(input.new_unexpected_token_error(t)) + }, + } + } +} + +impl SpecifiedValueInfo for Angle {} diff --git a/servo/components/style/values/specified/animation.rs b/servo/components/style/values/specified/animation.rs new file mode 100644 index 0000000000..e7bbf26fb3 --- /dev/null +++ b/servo/components/style/values/specified/animation.rs @@ -0,0 +1,463 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for properties related to animations and transitions. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::{NonCustomPropertyId, PropertyId, ShorthandId}; +use crate::values::generics::animation as generics; +use crate::values::specified::{LengthPercentage, NonNegativeNumber}; +use crate::values::{CustomIdent, KeyframesName, TimelineName}; +use crate::Atom; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{ + CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss, +}; + +/// A given transition property, that is either `All`, a longhand or shorthand +/// property, or an unsupported or custom property. +#[derive( + Clone, Debug, Eq, Hash, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[repr(u8)] +pub enum TransitionProperty { + /// A non-custom property. + NonCustom(NonCustomPropertyId), + /// A custom property. + Custom(Atom), + /// Unrecognized property which could be any non-transitionable, custom property, or + /// unknown property. + Unsupported(CustomIdent), +} + +impl ToCss for TransitionProperty { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + TransitionProperty::NonCustom(ref id) => id.to_css(dest), + TransitionProperty::Custom(ref name) => { + dest.write_str("--")?; + crate::values::serialize_atom_name(name, dest) + }, + TransitionProperty::Unsupported(ref i) => i.to_css(dest), + } + } +} + +impl Parse for TransitionProperty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + let id = match PropertyId::parse_ignoring_rule_type(&ident, context) { + Ok(id) => id, + Err(..) => { + // None is not acceptable as a single transition-property. + return Ok(TransitionProperty::Unsupported(CustomIdent::from_ident( + location, + ident, + &["none"], + )?)); + }, + }; + + Ok(match id { + PropertyId::NonCustom(id) => TransitionProperty::NonCustom(id.unaliased()), + PropertyId::Custom(name) => TransitionProperty::Custom(name), + }) + } +} + +impl SpecifiedValueInfo for TransitionProperty { + fn collect_completion_keywords(f: KeywordsCollectFn) { + // `transition-property` can actually accept all properties and + // arbitrary identifiers, but `all` is a special one we'd like + // to list. + f(&["all"]); + } +} + +impl TransitionProperty { + /// Returns the `none` value. + #[inline] + pub fn none() -> Self { + TransitionProperty::Unsupported(CustomIdent(atom!("none"))) + } + + /// Returns whether we're the `none` value. + #[inline] + pub fn is_none(&self) -> bool { + matches!(*self, TransitionProperty::Unsupported(ref ident) if ident.0 == atom!("none")) + } + + /// Returns `all`. + #[inline] + pub fn all() -> Self { + TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand(ShorthandId::All)) + } + + /// Returns true if it is `all`. + #[inline] + pub fn is_all(&self) -> bool { + self == &TransitionProperty::NonCustom(NonCustomPropertyId::from_shorthand( + ShorthandId::All, + )) + } +} + +/// https://drafts.csswg.org/css-animations/#animation-iteration-count +#[derive(Copy, Clone, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AnimationIterationCount { + /// A `<number>` value. + Number(NonNegativeNumber), + /// The `infinite` keyword. + Infinite, +} + +impl AnimationIterationCount { + /// Returns the value `1.0`. + #[inline] + pub fn one() -> Self { + Self::Number(NonNegativeNumber::new(1.0)) + } +} + +/// A value for the `animation-name` property. +#[derive( + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "none")] +#[repr(C)] +pub struct AnimationName(pub KeyframesName); + +impl AnimationName { + /// Get the name of the animation as an `Atom`. + pub fn as_atom(&self) -> Option<&Atom> { + if self.is_none() { + return None; + } + Some(self.0.as_atom()) + } + + /// Returns the `none` value. + pub fn none() -> Self { + AnimationName(KeyframesName::none()) + } + + /// Returns whether this is the none value. + pub fn is_none(&self) -> bool { + self.0.is_none() + } +} + +impl Parse for AnimationName { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(name) = input.try_parse(|input| KeyframesName::parse(context, input)) { + return Ok(AnimationName(name)); + } + + input.expect_ident_matching("none")?; + Ok(AnimationName(KeyframesName::none())) + } +} + +/// https://drafts.csswg.org/css-animations/#propdef-animation-direction +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationDirection { + Normal, + Reverse, + Alternate, + AlternateReverse, +} + +/// https://drafts.csswg.org/css-animations/#animation-play-state +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationPlayState { + Running, + Paused, +} + +/// https://drafts.csswg.org/css-animations/#propdef-animation-fill-mode +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationFillMode { + None, + Forwards, + Backwards, + Both, +} + +/// https://drafts.csswg.org/css-animations-2/#animation-composition +#[derive(Copy, Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] +#[repr(u8)] +#[allow(missing_docs)] +pub enum AnimationComposition { + Replace, + Add, + Accumulate, +} + +/// A value for the <Scroller> used in scroll(). +/// +/// https://drafts.csswg.org/scroll-animations-1/rewrite#typedef-scroller +#[derive( + Copy, + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Scroller { + /// The nearest ancestor scroll container. (Default.) + Nearest, + /// The document viewport as the scroll container. + Root, + /// Specifies to use the element’s own principal box as the scroll container. + #[css(keyword = "self")] + SelfElement, +} + +impl Scroller { + /// Returns true if it is default. + #[inline] + fn is_default(&self) -> bool { + matches!(*self, Self::Nearest) + } +} + +impl Default for Scroller { + fn default() -> Self { + Self::Nearest + } +} + +/// A value for the <Axis> used in scroll(), or a value for {scroll|view}-timeline-axis. +/// +/// https://drafts.csswg.org/scroll-animations-1/#typedef-axis +/// https://drafts.csswg.org/scroll-animations-1/#scroll-timeline-axis +/// https://drafts.csswg.org/scroll-animations-1/#view-timeline-axis +#[derive( + Copy, + Clone, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollAxis { + /// The block axis of the scroll container. (Default.) + Block = 0, + /// The inline axis of the scroll container. + Inline = 1, + /// The vertical block axis of the scroll container. + Vertical = 2, + /// The horizontal axis of the scroll container. + Horizontal = 3, +} + +impl ScrollAxis { + /// Returns true if it is default. + #[inline] + pub fn is_default(&self) -> bool { + matches!(*self, Self::Block) + } +} + +impl Default for ScrollAxis { + fn default() -> Self { + Self::Block + } +} + +/// The scroll() notation. +/// https://drafts.csswg.org/scroll-animations-1/#scroll-notation +#[derive( + Copy, + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(function = "scroll")] +#[repr(C)] +pub struct ScrollFunction { + /// The scroll container element whose scroll position drives the progress of the timeline. + #[css(skip_if = "Scroller::is_default")] + pub scroller: Scroller, + /// The axis of scrolling that drives the progress of the timeline. + #[css(skip_if = "ScrollAxis::is_default")] + pub axis: ScrollAxis, +} + +impl ScrollFunction { + /// Parse the inner function arguments of `scroll()`. + fn parse_arguments<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + // <scroll()> = scroll( [ <scroller> || <axis> ]? ) + // https://drafts.csswg.org/scroll-animations-1/#funcdef-scroll + let mut scroller = None; + let mut axis = None; + loop { + if scroller.is_none() { + scroller = input.try_parse(Scroller::parse).ok(); + } + + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + if axis.is_some() { + continue; + } + } + break; + } + + Ok(Self { + scroller: scroller.unwrap_or_default(), + axis: axis.unwrap_or_default(), + }) + } +} + +impl generics::ViewFunction<LengthPercentage> { + /// Parse the inner function arguments of `view()`. + fn parse_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // <view()> = view( [ <axis> || <'view-timeline-inset'> ]? ) + // https://drafts.csswg.org/scroll-animations-1/#funcdef-view + let mut axis = None; + let mut inset = None; + loop { + if axis.is_none() { + axis = input.try_parse(ScrollAxis::parse).ok(); + } + + if inset.is_none() { + inset = input + .try_parse(|i| ViewTimelineInset::parse(context, i)) + .ok(); + if inset.is_some() { + continue; + } + } + break; + } + + Ok(Self { + inset: inset.unwrap_or_default(), + axis: axis.unwrap_or_default(), + }) + } +} + +/// A specified value for the `animation-timeline` property. +pub type AnimationTimeline = generics::GenericAnimationTimeline<LengthPercentage>; + +impl Parse for AnimationTimeline { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::generics::animation::ViewFunction; + + // <single-animation-timeline> = auto | none | <custom-ident> | <scroll()> | <view()> + // https://drafts.csswg.org/css-animations-2/#typedef-single-animation-timeline + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(Self::Auto); + } + + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(AnimationTimeline::Timeline(TimelineName::none())); + } + + if let Ok(name) = input.try_parse(|i| TimelineName::parse(context, i)) { + return Ok(AnimationTimeline::Timeline(name)); + } + + // Parse possible functions + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "scroll" => ScrollFunction::parse_arguments(i).map(Self::Scroll), + "view" => ViewFunction::parse_arguments(context, i).map(Self::View), + _ => { + Err(location.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + } + }) + } +} + +/// A value for the scroll-timeline-name or view-timeline-name. +pub type ScrollTimelineName = AnimationName; + +/// A specified value for the `view-timeline-inset` property. +pub type ViewTimelineInset = generics::GenericViewTimelineInset<LengthPercentage>; + +impl Parse for ViewTimelineInset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::LengthPercentageOrAuto; + + let start = LengthPercentageOrAuto::parse(context, input)?; + let end = match input.try_parse(|input| LengthPercentageOrAuto::parse(context, input)) { + Ok(end) => end, + Err(_) => start.clone(), + }; + + Ok(Self { start, end }) + } +} diff --git a/servo/components/style/values/specified/background.rs b/servo/components/style/values/specified/background.rs new file mode 100644 index 0000000000..39a5a85193 --- /dev/null +++ b/servo/components/style/values/specified/background.rs @@ -0,0 +1,143 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to backgrounds. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::background::BackgroundSize as GenericBackgroundSize; +use crate::values::specified::length::{ + NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, ToCss}; + +/// A specified value for the `background-size` property. +pub type BackgroundSize = GenericBackgroundSize<NonNegativeLengthPercentage>; + +impl Parse for BackgroundSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(width) = input.try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i)) + { + let height = input + .try_parse(|i| NonNegativeLengthPercentageOrAuto::parse(context, i)) + .unwrap_or(NonNegativeLengthPercentageOrAuto::auto()); + return Ok(GenericBackgroundSize::ExplicitSize { width, height }); + } + Ok(try_match_ident_ignore_ascii_case! { input, + "cover" => GenericBackgroundSize::Cover, + "contain" => GenericBackgroundSize::Contain, + }) + } +} + +/// One of the keywords for `background-repeat`. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[value_info(other_values = "repeat-x,repeat-y")] +pub enum BackgroundRepeatKeyword { + Repeat, + Space, + Round, + NoRepeat, +} + +/// The value of the `background-repeat` property, with `repeat-x` / `repeat-y` +/// represented as the combination of `no-repeat` and `repeat` in the opposite +/// axes. +/// +/// https://drafts.csswg.org/css-backgrounds/#the-background-repeat +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct BackgroundRepeat(pub BackgroundRepeatKeyword, pub BackgroundRepeatKeyword); + +impl BackgroundRepeat { + /// Returns the `repeat repeat` value. + pub fn repeat() -> Self { + BackgroundRepeat( + BackgroundRepeatKeyword::Repeat, + BackgroundRepeatKeyword::Repeat, + ) + } +} + +impl ToCss for BackgroundRepeat { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match (self.0, self.1) { + (BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat) => { + dest.write_str("repeat-x") + }, + (BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat) => { + dest.write_str("repeat-y") + }, + (horizontal, vertical) => { + horizontal.to_css(dest)?; + if horizontal != vertical { + dest.write_char(' ')?; + vertical.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for BackgroundRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let ident = input.expect_ident_cloned()?; + + match_ignore_ascii_case! { &ident, + "repeat-x" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::Repeat, BackgroundRepeatKeyword::NoRepeat)); + }, + "repeat-y" => { + return Ok(BackgroundRepeat(BackgroundRepeatKeyword::NoRepeat, BackgroundRepeatKeyword::Repeat)); + }, + _ => {}, + } + + let horizontal = match BackgroundRepeatKeyword::from_ident(&ident) { + Ok(h) => h, + Err(()) => { + return Err( + input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone())) + ); + }, + }; + + let vertical = input.try_parse(BackgroundRepeatKeyword::parse).ok(); + Ok(BackgroundRepeat(horizontal, vertical.unwrap_or(horizontal))) + } +} diff --git a/servo/components/style/values/specified/basic_shape.rs b/servo/components/style/values/specified/basic_shape.rs new file mode 100644 index 0000000000..526296b735 --- /dev/null +++ b/servo/components/style/values/specified/basic_shape.rs @@ -0,0 +1,719 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the specified value of +//! [`basic-shape`][basic-shape]s +//! +//! [basic-shape]: https://drafts.csswg.org/css-shapes/#typedef-basic-shape + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::basic_shape::InsetRect as ComputedInsetRect; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::basic_shape as generic; +use crate::values::generics::basic_shape::{Path, PolygonCoord}; +use crate::values::generics::position::{GenericPosition, GenericPositionOrAuto}; +use crate::values::generics::rect::Rect; +use crate::values::specified::border::BorderRadius; +use crate::values::specified::image::Image; +use crate::values::specified::length::LengthPercentageOrAuto; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{LengthPercentage, NonNegativeLengthPercentage, SVGPathData}; +use crate::Zero; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A specified alias for FillRule. +pub use crate::values::generics::basic_shape::FillRule; + +/// A specified `clip-path` value. +pub type ClipPath = generic::GenericClipPath<BasicShape, SpecifiedUrl>; + +/// A specified `shape-outside` value. +pub type ShapeOutside = generic::GenericShapeOutside<BasicShape, Image>; + +/// A specified value for `at <position>` in circle() and ellipse(). +// Note: its computed value is the same as computed::position::Position. We just want to always use +// LengthPercentage as the type of its components, for basic shapes. +pub type ShapePosition = GenericPosition<LengthPercentage, LengthPercentage>; + +/// A specified basic shape. +pub type BasicShape = generic::GenericBasicShape< + ShapePosition, + LengthPercentage, + NonNegativeLengthPercentage, + BasicShapeRect, +>; + +/// The specified value of `inset()`. +pub type InsetRect = generic::GenericInsetRect<LengthPercentage, NonNegativeLengthPercentage>; + +/// A specified circle. +pub type Circle = generic::Circle<ShapePosition, NonNegativeLengthPercentage>; + +/// A specified ellipse. +pub type Ellipse = generic::Ellipse<ShapePosition, NonNegativeLengthPercentage>; + +/// The specified value of `ShapeRadius`. +pub type ShapeRadius = generic::ShapeRadius<NonNegativeLengthPercentage>; + +/// The specified value of `Polygon`. +pub type Polygon = generic::GenericPolygon<LengthPercentage>; + +/// The specified value of `xywh()`. +/// Defines a rectangle via offsets from the top and left edge of the reference box, and a +/// specified width and height. +/// +/// The four <length-percentage>s define, respectively, the inset from the left edge of the +/// reference box, the inset from the top edge of the reference box, the width of the rectangle, +/// and the height of the rectangle. +/// +/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-xywh +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +pub struct Xywh { + /// The left edge of the reference box. + pub x: LengthPercentage, + /// The top edge of the reference box. + pub y: LengthPercentage, + /// The specified width. + pub width: NonNegativeLengthPercentage, + /// The specified height. + pub height: NonNegativeLengthPercentage, + /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle + /// using the border-radius shorthand syntax. + pub round: BorderRadius, +} + +/// Defines a rectangle via insets from the top and left edges of the reference box. +/// +/// https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +#[repr(C)] +pub struct ShapeRectFunction { + /// The four <length-percentage>s define the position of the top, right, bottom, and left edges + /// of a rectangle, respectively, as insets from the top edge of the reference box (for the + /// first and third values) or the left edge of the reference box (for the second and fourth + /// values). + /// + /// An auto value makes the edge of the box coincide with the corresponding edge of the + /// reference box: it’s equivalent to 0% as the first (top) or fourth (left) value, and + /// equivalent to 100% as the second (right) or third (bottom) value. + pub rect: Rect<LengthPercentageOrAuto>, + /// The optional <border-radius> argument(s) define rounded corners for the inset rectangle + /// using the border-radius shorthand syntax. + pub round: BorderRadius, +} + +/// The specified value of <basic-shape-rect>. +/// <basic-shape-rect> = <inset()> | <rect()> | <xywh()> +/// +/// https://drafts.csswg.org/css-shapes-1/#supported-basic-shapes +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum BasicShapeRect { + /// Defines an inset rectangle via insets from each edge of the reference box. + Inset(InsetRect), + /// Defines a xywh function. + #[css(function)] + Xywh(Xywh), + /// Defines a rect function. + #[css(function)] + Rect(ShapeRectFunction), +} + +/// For filled shapes, we use fill-rule, and store it for path() and polygon(). +/// For outline shapes, we should ignore fill-rule. +/// +/// https://github.com/w3c/fxtf-drafts/issues/512 +/// https://github.com/w3c/csswg-drafts/issues/7390 +/// https://github.com/w3c/csswg-drafts/issues/3468 +pub enum ShapeType { + /// The CSS property uses filled shapes. The default behavior. + Filled, + /// The CSS property uses outline shapes. This is especially useful for offset-path. + Outline, +} + +bitflags! { + /// The flags to represent which basic shapes we would like to support. + /// + /// Different properties may use different subsets of <basic-shape>: + /// e.g. + /// clip-path: all basic shapes. + /// motion-path: all basic shapes (but ignore fill-rule). + /// shape-outside: inset(), circle(), ellipse(), polygon(). + /// + /// Also there are some properties we don't support for now: + /// shape-inside: inset(), circle(), ellipse(), polygon(). + /// SVG shape-inside and shape-subtract: circle(), ellipse(), polygon(). + /// + /// The spec issue proposes some better ways to clarify the usage of basic shapes, so for now + /// we use the bitflags to choose the supported basic shapes for each property at the parse + /// time. + /// https://github.com/w3c/csswg-drafts/issues/7390 + #[derive(Clone, Copy)] + #[repr(C)] + pub struct AllowedBasicShapes: u8 { + /// inset(). + const INSET = 1 << 0; + /// xywh(). + const XYWH = 1 << 1; + /// rect(). + const RECT = 1 << 2; + /// circle(). + const CIRCLE = 1 << 3; + /// ellipse(). + const ELLIPSE = 1 << 4; + /// polygon(). + const POLYGON = 1 << 5; + /// path(). + const PATH = 1 << 6; + // TODO: Bug 1823463. Add shape(). + // const SHAPE = 1 << 7; + + /// All flags. + const ALL = + Self::INSET.bits() | + Self::XYWH.bits() | + Self::RECT.bits() | + Self::CIRCLE.bits() | + Self::ELLIPSE.bits() | + Self::POLYGON.bits() | + Self::PATH.bits(); + + /// For shape-outside. + const SHAPE_OUTSIDE = + Self::INSET.bits() | + Self::CIRCLE.bits() | + Self::ELLIPSE.bits() | + Self::POLYGON.bits(); + } +} + +/// A helper for both clip-path and shape-outside parsing of shapes. +fn parse_shape_or_box<'i, 't, R, ReferenceBox>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + to_shape: impl FnOnce(Box<BasicShape>, ReferenceBox) -> R, + to_reference_box: impl FnOnce(ReferenceBox) -> R, + flags: AllowedBasicShapes, +) -> Result<R, ParseError<'i>> +where + ReferenceBox: Default + Parse, +{ + let mut shape = None; + let mut ref_box = None; + loop { + if shape.is_none() { + shape = input + .try_parse(|i| BasicShape::parse(context, i, flags, ShapeType::Filled)) + .ok(); + } + + if ref_box.is_none() { + ref_box = input.try_parse(|i| ReferenceBox::parse(context, i)).ok(); + if ref_box.is_some() { + continue; + } + } + break; + } + + if let Some(shp) = shape { + return Ok(to_shape(Box::new(shp), ref_box.unwrap_or_default())); + } + + match ref_box { + Some(r) => Ok(to_reference_box(r)), + None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } +} + +impl Parse for ClipPath { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ClipPath::None); + } + + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(ClipPath::Url(url)); + } + + parse_shape_or_box( + context, + input, + ClipPath::Shape, + ClipPath::Box, + AllowedBasicShapes::ALL, + ) + } +} + +impl Parse for ShapeOutside { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Need to parse this here so that `Image::parse_with_cors_anonymous` + // doesn't parse it. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ShapeOutside::None); + } + + if let Ok(image) = input.try_parse(|i| Image::parse_with_cors_anonymous(context, i)) { + debug_assert_ne!(image, Image::None); + return Ok(ShapeOutside::Image(image)); + } + + parse_shape_or_box( + context, + input, + ShapeOutside::Shape, + ShapeOutside::Box, + AllowedBasicShapes::SHAPE_OUTSIDE, + ) + } +} + +impl BasicShape { + /// Parse with some parameters. + /// 1. The supported <basic-shape>. + /// 2. The type of shapes. Should we ignore fill-rule? + /// 3. The default value of `at <position>`. + pub fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + flags: AllowedBasicShapes, + shape_type: ShapeType, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "inset" if flags.contains(AllowedBasicShapes::INSET) => { + InsetRect::parse_function_arguments(context, i) + .map(BasicShapeRect::Inset) + .map(BasicShape::Rect) + }, + "xywh" + if flags.contains(AllowedBasicShapes::XYWH) + && static_prefs::pref!("layout.css.basic-shape-xywh.enabled") => + { + Xywh::parse_function_arguments(context, i) + .map(BasicShapeRect::Xywh) + .map(BasicShape::Rect) + }, + "rect" + if flags.contains(AllowedBasicShapes::RECT) + && static_prefs::pref!("layout.css.basic-shape-rect.enabled") => + { + ShapeRectFunction::parse_function_arguments(context, i) + .map(BasicShapeRect::Rect) + .map(BasicShape::Rect) + }, + "circle" if flags.contains(AllowedBasicShapes::CIRCLE) => { + Circle::parse_function_arguments(context, i) + .map(BasicShape::Circle) + }, + "ellipse" if flags.contains(AllowedBasicShapes::ELLIPSE) => { + Ellipse::parse_function_arguments(context, i) + .map(BasicShape::Ellipse) + }, + "polygon" if flags.contains(AllowedBasicShapes::POLYGON) => { + Polygon::parse_function_arguments(context, i, shape_type) + .map(BasicShape::Polygon) + }, + "path" if flags.contains(AllowedBasicShapes::PATH) => { + Path::parse_function_arguments(i, shape_type).map(BasicShape::Path) + }, + _ => Err(location + .new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))), + } + }) + } +} + +impl Parse for InsetRect { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("inset")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +fn parse_round<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<BorderRadius, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("round")) + .is_ok() + { + return BorderRadius::parse(context, input); + } + + Ok(BorderRadius::zero()) +} + +impl InsetRect { + /// Parse the inner function arguments of `inset()` + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let rect = Rect::parse_with(context, input, LengthPercentage::parse)?; + let round = parse_round(context, input)?; + Ok(generic::InsetRect { rect, round }) + } +} + +impl ToCss for ShapePosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.horizontal.to_css(dest)?; + dest.write_char(' ')?; + self.vertical.to_css(dest) + } +} + +fn parse_at_position<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<GenericPositionOrAuto<ShapePosition>, ParseError<'i>> { + use crate::values::specified::position::{Position, Side}; + use crate::values::specified::{AllowedNumericType, Percentage, PositionComponent}; + + fn convert_to_length_percentage<S: Side>(c: PositionComponent<S>) -> LengthPercentage { + // Convert the value when parsing, to make sure we serialize it properly for both + // specified and computed values. + // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization + match c { + // Since <position> keywords stand in for percentages, keywords without an offset + // turn into percentages. + PositionComponent::Center => LengthPercentage::from(Percentage::new(0.5)), + PositionComponent::Side(keyword, None) => { + Percentage::new(if keyword.is_start() { 0. } else { 1. }).into() + }, + // Per spec issue, https://github.com/w3c/csswg-drafts/issues/8695, the part of + // "avoiding calc() expressions where possible" and "avoiding calc() + // transformations" will be removed from the spec, and we should follow the + // css-values-4 for position, i.e. we make it as length-percentage always. + // https://drafts.csswg.org/css-shapes-1/#basic-shape-serialization. + // https://drafts.csswg.org/css-values-4/#typedef-position + PositionComponent::Side(keyword, Some(length)) => { + if keyword.is_start() { + length + } else { + length.hundred_percent_minus(AllowedNumericType::All) + } + }, + PositionComponent::Length(length) => length, + } + } + + if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + Position::parse(context, input).map(|pos| { + GenericPositionOrAuto::Position(ShapePosition::new( + convert_to_length_percentage(pos.horizontal), + convert_to_length_percentage(pos.vertical), + )) + }) + } else { + // `at <position>` is omitted. + Ok(GenericPositionOrAuto::Auto) + } +} + +impl Parse for Circle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("circle")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Circle { + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let radius = input + .try_parse(|i| ShapeRadius::parse(context, i)) + .unwrap_or_default(); + let position = parse_at_position(context, input)?; + + Ok(generic::Circle { radius, position }) + } +} + +impl Parse for Ellipse { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("ellipse")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl Ellipse { + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let (semiaxis_x, semiaxis_y) = input + .try_parse(|i| -> Result<_, ParseError> { + Ok(( + ShapeRadius::parse(context, i)?, + ShapeRadius::parse(context, i)?, + )) + }) + .unwrap_or_default(); + let position = parse_at_position(context, input)?; + + Ok(generic::Ellipse { + semiaxis_x, + semiaxis_y, + position, + }) + } +} + +fn parse_fill_rule<'i, 't>(input: &mut Parser<'i, 't>, shape_type: ShapeType) -> FillRule { + match shape_type { + // Per [1] and [2], we ignore `<fill-rule>` for outline shapes, so always use a default + // value. + // [1] https://github.com/w3c/csswg-drafts/issues/3468 + // [2] https://github.com/w3c/csswg-drafts/issues/7390 + // + // Also, per [3] and [4], we would like the ignore `<file-rule>` from outline shapes, e.g. + // offset-path, which means we don't parse it when setting `ShapeType::Outline`. + // This should be web compatible because the shipped "offset-path:path()" doesn't have + // `<fill-rule>` and "offset-path:polygon()" is a new feature and still behind the + // preference. + // [3] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1545393321 + // [4] https://github.com/w3c/fxtf-drafts/issues/512#issuecomment-1555330929 + ShapeType::Outline => Default::default(), + ShapeType::Filled => input + .try_parse(|i| -> Result<_, ParseError> { + let fill = FillRule::parse(i)?; + i.expect_comma()?; + Ok(fill) + }) + .unwrap_or_default(), + } +} + +impl Parse for Polygon { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("polygon")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i, ShapeType::Filled)) + } +} + +impl Polygon { + /// Parse the inner arguments of a `polygon` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + shape_type: ShapeType, + ) -> Result<Self, ParseError<'i>> { + let fill = parse_fill_rule(input, shape_type); + let coordinates = input + .parse_comma_separated(|i| { + Ok(PolygonCoord( + LengthPercentage::parse(context, i)?, + LengthPercentage::parse(context, i)?, + )) + })? + .into(); + + Ok(Polygon { fill, coordinates }) + } +} + +impl Path { + /// Parse the inner arguments of a `path` function. + fn parse_function_arguments<'i, 't>( + input: &mut Parser<'i, 't>, + shape_type: ShapeType, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::svg_path::AllowEmpty; + + let fill = parse_fill_rule(input, shape_type); + let path = SVGPathData::parse(input, AllowEmpty::No)?; + Ok(Path { fill, path }) + } +} + +fn round_to_css<W>(round: &BorderRadius, dest: &mut CssWriter<W>) -> fmt::Result +where + W: Write, +{ + if !round.is_zero() { + dest.write_str(" round ")?; + round.to_css(dest)?; + } + Ok(()) +} + +impl ToCss for Xywh { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.x.to_css(dest)?; + dest.write_char(' ')?; + self.y.to_css(dest)?; + dest.write_char(' ')?; + self.width.to_css(dest)?; + dest.write_char(' ')?; + self.height.to_css(dest)?; + round_to_css(&self.round, dest) + } +} + +impl Xywh { + /// Parse the inner function arguments of `xywh()`. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let x = LengthPercentage::parse(context, input)?; + let y = LengthPercentage::parse(context, input)?; + let width = NonNegativeLengthPercentage::parse(context, input)?; + let height = NonNegativeLengthPercentage::parse(context, input)?; + let round = parse_round(context, input)?; + Ok(Xywh { + x, + y, + width, + height, + round, + }) + } +} + +impl ToCss for ShapeRectFunction { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.rect.0.to_css(dest)?; + dest.write_char(' ')?; + self.rect.1.to_css(dest)?; + dest.write_char(' ')?; + self.rect.2.to_css(dest)?; + dest.write_char(' ')?; + self.rect.3.to_css(dest)?; + round_to_css(&self.round, dest) + } +} + +impl ShapeRectFunction { + /// Parse the inner function arguments of `rect()`. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let rect = Rect::parse_all_components_with(context, input, LengthPercentageOrAuto::parse)?; + let round = parse_round(context, input)?; + Ok(ShapeRectFunction { rect, round }) + } +} + +impl ToComputedValue for BasicShapeRect { + type ComputedValue = ComputedInsetRect; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::LengthPercentage; + use crate::values::computed::LengthPercentageOrAuto; + use style_traits::values::specified::AllowedNumericType; + + match self { + Self::Inset(ref inset) => inset.to_computed_value(context), + Self::Xywh(ref xywh) => { + // Given `xywh(x y w h)`, construct the equivalent inset() function, + // `inset(y calc(100% - x - w) calc(100% - y - h) x)`. + // + // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values + // https://github.com/w3c/csswg-drafts/issues/9053 + let x = xywh.x.to_computed_value(context); + let y = xywh.y.to_computed_value(context); + let w = xywh.width.to_computed_value(context); + let h = xywh.height.to_computed_value(context); + // calc(100% - x - w). + let right = LengthPercentage::hundred_percent_minus_list( + &[&x, &w.0], + AllowedNumericType::All, + ); + // calc(100% - y - h). + let bottom = LengthPercentage::hundred_percent_minus_list( + &[&y, &h.0], + AllowedNumericType::All, + ); + + ComputedInsetRect { + rect: Rect::new(y, right, bottom, x), + round: xywh.round.to_computed_value(context), + } + }, + Self::Rect(ref rect) => { + // Given `rect(t r b l)`, the equivalent function is + // `inset(t calc(100% - r) calc(100% - b) l)`. + // + // https://drafts.csswg.org/css-shapes-1/#basic-shape-computed-values + fn compute_top_or_left(v: LengthPercentageOrAuto) -> LengthPercentage { + match v { + // it’s equivalent to 0% as the first (top) or fourth (left) value. + // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect + LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), + LengthPercentageOrAuto::LengthPercentage(lp) => lp, + } + } + fn compute_bottom_or_right(v: LengthPercentageOrAuto) -> LengthPercentage { + match v { + // It's equivalent to 100% as the second (right) or third (bottom) value. + // So calc(100% - 100%) = 0%. + // https://drafts.csswg.org/css-shapes-1/#funcdef-basic-shape-rect + LengthPercentageOrAuto::Auto => LengthPercentage::zero_percent(), + LengthPercentageOrAuto::LengthPercentage(lp) => { + LengthPercentage::hundred_percent_minus(lp, AllowedNumericType::All) + }, + } + } + + let round = rect.round.to_computed_value(context); + let rect = rect.rect.to_computed_value(context); + let rect = Rect::new( + compute_top_or_left(rect.0), + compute_bottom_or_right(rect.1), + compute_bottom_or_right(rect.2), + compute_top_or_left(rect.3), + ); + + ComputedInsetRect { rect, round } + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Inset(ToComputedValue::from_computed_value(computed)) + } +} diff --git a/servo/components/style/values/specified/border.rs b/servo/components/style/values/specified/border.rs new file mode 100644 index 0000000000..a4660c7f60 --- /dev/null +++ b/servo/components/style/values/specified/border.rs @@ -0,0 +1,398 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to borders. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::border::BorderCornerRadius as GenericBorderCornerRadius; +use crate::values::generics::border::BorderImageSideWidth as GenericBorderImageSideWidth; +use crate::values::generics::border::BorderImageSlice as GenericBorderImageSlice; +use crate::values::generics::border::BorderRadius as GenericBorderRadius; +use crate::values::generics::border::BorderSpacing as GenericBorderSpacing; +use crate::values::generics::rect::Rect; +use crate::values::generics::size::Size2D; +use crate::values::specified::length::{Length, NonNegativeLength, NonNegativeLengthPercentage}; +use crate::values::specified::Color; +use crate::values::specified::{AllowQuirks, NonNegativeNumber, NonNegativeNumberOrPercentage}; +use crate::Zero; +use app_units::Au; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{values::SequenceWriter, CssWriter, ParseError, ToCss}; + +/// A specified value for a single side of a `border-style` property. +/// +/// The order here corresponds to the integer values from the border conflict +/// resolution rules in CSS 2.1 § 17.6.2.1. Higher values override lower values. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Ord, + Parse, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BorderStyle { + Hidden, + None, + Inset, + Groove, + Outset, + Ridge, + Dotted, + Dashed, + Solid, + Double, +} + +impl BorderStyle { + /// Whether this border style is either none or hidden. + #[inline] + pub fn none_or_hidden(&self) -> bool { + matches!(*self, BorderStyle::None | BorderStyle::Hidden) + } +} + +/// A specified value for the `border-image-width` property. +pub type BorderImageWidth = Rect<BorderImageSideWidth>; + +/// A specified value for a single side of a `border-image-width` property. +pub type BorderImageSideWidth = + GenericBorderImageSideWidth<NonNegativeLengthPercentage, NonNegativeNumber>; + +/// A specified value for the `border-image-slice` property. +pub type BorderImageSlice = GenericBorderImageSlice<NonNegativeNumberOrPercentage>; + +/// A specified value for the `border-radius` property. +pub type BorderRadius = GenericBorderRadius<NonNegativeLengthPercentage>; + +/// A specified value for the `border-*-radius` longhand properties. +pub type BorderCornerRadius = GenericBorderCornerRadius<NonNegativeLengthPercentage>; + +/// A specified value for the `border-spacing` longhand properties. +pub type BorderSpacing = GenericBorderSpacing<NonNegativeLength>; + +impl BorderImageSlice { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + GenericBorderImageSlice { + offsets: Rect::all(NonNegativeNumberOrPercentage::hundred_percent()), + fill: false, + } + } +} + +/// https://drafts.csswg.org/css-backgrounds-3/#typedef-line-width +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum LineWidth { + /// `thin` + Thin, + /// `medium` + Medium, + /// `thick` + Thick, + /// `<length>` + Length(NonNegativeLength), +} + +impl LineWidth { + /// Returns the `0px` value. + #[inline] + pub fn zero() -> Self { + Self::Length(NonNegativeLength::zero()) + } + + fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if let Ok(length) = + input.try_parse(|i| NonNegativeLength::parse_quirky(context, i, allow_quirks)) + { + return Ok(Self::Length(length)); + } + Ok(try_match_ident_ignore_ascii_case! { input, + "thin" => Self::Thin, + "medium" => Self::Medium, + "thick" => Self::Thick, + }) + } +} + +impl Parse for LineWidth { + fn parse<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for LineWidth { + type ComputedValue = app_units::Au; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + // https://drafts.csswg.org/css-backgrounds-3/#line-width + Self::Thin => Au::from_px(1), + Self::Medium => Au::from_px(3), + Self::Thick => Au::from_px(5), + Self::Length(ref length) => Au::from_f32_px(length.to_computed_value(context).px()), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Length(NonNegativeLength::from_px(computed.to_f32_px())) + } +} + +/// A specified value for a single side of the `border-width` property. The difference between this +/// and LineWidth is whether we snap to device pixels or not. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct BorderSideWidth(LineWidth); + +impl BorderSideWidth { + /// Returns the `medium` value. + pub fn medium() -> Self { + Self(LineWidth::Medium) + } + + /// Returns a bare px value from the argument. + pub fn from_px(px: f32) -> Self { + Self(LineWidth::Length(Length::from_px(px).into())) + } + + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Ok(Self(LineWidth::parse_quirky(context, input, allow_quirks)?)) + } +} + +impl Parse for BorderSideWidth { + fn parse<'i>( + context: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ToComputedValue for BorderSideWidth { + type ComputedValue = app_units::Au; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let width = self.0.to_computed_value(context); + // Round `width` down to the nearest device pixel, but any non-zero value that would round + // down to zero is clamped to 1 device pixel. + if width == Au(0) { + return width; + } + + let au_per_dev_px = context.device().app_units_per_device_pixel(); + std::cmp::max( + Au(au_per_dev_px), + Au(width.0 / au_per_dev_px * au_per_dev_px), + ) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self(LineWidth::from_computed_value(computed)) + } +} + +impl BorderImageSideWidth { + /// Returns `1`. + #[inline] + pub fn one() -> Self { + GenericBorderImageSideWidth::Number(NonNegativeNumber::new(1.)) + } +} + +impl Parse for BorderImageSlice { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok(); + let offsets = Rect::parse_with(context, input, NonNegativeNumberOrPercentage::parse)?; + if !fill { + fill = input.try_parse(|i| i.expect_ident_matching("fill")).is_ok(); + } + Ok(GenericBorderImageSlice { offsets, fill }) + } +} + +impl Parse for BorderRadius { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let widths = Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)?; + let heights = if input.try_parse(|i| i.expect_delim('/')).is_ok() { + Rect::parse_with(context, input, NonNegativeLengthPercentage::parse)? + } else { + widths.clone() + }; + + Ok(GenericBorderRadius { + top_left: BorderCornerRadius::new(widths.0, heights.0), + top_right: BorderCornerRadius::new(widths.1, heights.1), + bottom_right: BorderCornerRadius::new(widths.2, heights.2), + bottom_left: BorderCornerRadius::new(widths.3, heights.3), + }) + } +} + +impl Parse for BorderCornerRadius { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size2D::parse_with(context, input, NonNegativeLengthPercentage::parse) + .map(GenericBorderCornerRadius) + } +} + +impl Parse for BorderSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size2D::parse_with(context, input, |context, input| { + NonNegativeLength::parse_quirky(context, input, AllowQuirks::Yes) + }) + .map(GenericBorderSpacing) + } +} + +/// A single border-image-repeat keyword. +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum BorderImageRepeatKeyword { + Stretch, + Repeat, + Round, + Space, +} + +/// The specified value for the `border-image-repeat` property. +/// +/// https://drafts.csswg.org/css-backgrounds/#the-border-image-repeat +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct BorderImageRepeat(pub BorderImageRepeatKeyword, pub BorderImageRepeatKeyword); + +impl ToCss for BorderImageRepeat { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.0.to_css(dest)?; + if self.0 != self.1 { + dest.write_char(' ')?; + self.1.to_css(dest)?; + } + Ok(()) + } +} + +impl BorderImageRepeat { + /// Returns the `stretch` value. + #[inline] + pub fn stretch() -> Self { + BorderImageRepeat( + BorderImageRepeatKeyword::Stretch, + BorderImageRepeatKeyword::Stretch, + ) + } +} + +impl Parse for BorderImageRepeat { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let horizontal = BorderImageRepeatKeyword::parse(input)?; + let vertical = input.try_parse(BorderImageRepeatKeyword::parse).ok(); + Ok(BorderImageRepeat( + horizontal, + vertical.unwrap_or(horizontal), + )) + } +} + +/// Serializes a border shorthand value composed of width/style/color. +pub fn serialize_directional_border<W>( + dest: &mut CssWriter<W>, + width: &BorderSideWidth, + style: &BorderStyle, + color: &Color, +) -> fmt::Result +where + W: Write, +{ + let has_style = *style != BorderStyle::None; + let has_color = *color != Color::CurrentColor; + let has_width = *width != BorderSideWidth::medium(); + if !has_style && !has_color && !has_width { + return width.to_css(dest); + } + let mut writer = SequenceWriter::new(dest, " "); + if has_width { + writer.item(width)?; + } + if has_style { + writer.item(style)?; + } + if has_color { + writer.item(color)?; + } + Ok(()) +} diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs new file mode 100644 index 0000000000..8414591c2b --- /dev/null +++ b/servo/components/style/values/specified/box.rs @@ -0,0 +1,1945 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for box properties. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::{LonghandId, PropertyDeclarationId, PropertyId}; +use crate::values::generics::box_::{ + GenericContainIntrinsicSize, GenericLineClamp, GenericPerspective, GenericVerticalAlign, + VerticalAlignKeyword, +}; +use crate::values::specified::length::{LengthPercentage, NonNegativeLength}; +use crate::values::specified::{AllowQuirks, Integer, NonNegativeNumberOrPercentage}; +use crate::values::CustomIdent; +use cssparser::Parser; +use num_traits::FromPrimitive; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(not(feature = "servo-layout-2020"))] +fn flexbox_enabled() -> bool { + true +} + +#[cfg(feature = "servo-layout-2020")] +fn flexbox_enabled() -> bool { + servo_config::prefs::pref_map() + .get("layout.flexbox.enabled") + .as_bool() + .unwrap_or(false) +} + +/// Defines an element’s display type, which consists of +/// the two basic qualities of how an element generates boxes +/// <https://drafts.csswg.org/css-display/#propdef-display> +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayOutside { + None = 0, + Inline, + Block, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCaption, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + InternalTable, + #[cfg(feature = "gecko")] + InternalRuby, +} + +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, Eq, FromPrimitive, Hash, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum DisplayInside { + None = 0, + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Contents, + Flow, + FlowRoot, + Flex, + #[cfg(feature = "gecko")] + Grid, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Table, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRowGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumn, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableColumnGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableHeaderGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableFooterGroup, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableRow, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + TableCell, + #[cfg(feature = "gecko")] + Ruby, + #[cfg(feature = "gecko")] + RubyBase, + #[cfg(feature = "gecko")] + RubyBaseContainer, + #[cfg(feature = "gecko")] + RubyText, + #[cfg(feature = "gecko")] + RubyTextContainer, + #[cfg(feature = "gecko")] + WebkitBox, +} + +impl DisplayInside { + fn is_valid_for_list_item(self) -> bool { + match self { + DisplayInside::Flow => true, + #[cfg(feature = "gecko")] + DisplayInside::FlowRoot => true, + _ => false, + } + } + + /// https://drafts.csswg.org/css-display/#inside-model: + /// If <display-outside> is omitted, the element’s outside display type defaults to block + /// — except for ruby, which defaults to inline. + fn default_display_outside(self) -> DisplayOutside { + match self { + #[cfg(feature = "gecko")] + DisplayInside::Ruby => DisplayOutside::Inline, + _ => DisplayOutside::Block, + } + } +} + +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + PartialEq, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct Display(u16); + +/// Gecko-only impl block for Display (shared stuff later in this file): +#[allow(missing_docs)] +#[allow(non_upper_case_globals)] +impl Display { + // Our u16 bits are used as follows: LOOOOOOOIIIIIIII + pub const LIST_ITEM_MASK: u16 = 0b1000000000000000; + pub const OUTSIDE_MASK: u16 = 0b0111111100000000; + pub const INSIDE_MASK: u16 = 0b0000000011111111; + pub const OUTSIDE_SHIFT: u16 = 8; + + /// https://drafts.csswg.org/css-display/#the-display-properties + /// ::new() inlined so cbindgen can use it + pub const None: Self = + Self(((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::None as u16); + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + pub const Contents: Self = Self( + ((DisplayOutside::None as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Contents as u16, + ); + pub const Inline: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16); + pub const InlineBlock: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16, + ); + pub const Block: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16); + #[cfg(feature = "gecko")] + pub const FlowRoot: Self = Self( + ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::FlowRoot as u16, + ); + pub const Flex: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16); + pub const InlineFlex: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flex as u16); + #[cfg(feature = "gecko")] + pub const Grid: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16); + #[cfg(feature = "gecko")] + pub const InlineGrid: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Grid as u16); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const Table: Self = + Self(((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const InlineTable: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Table as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCaption: Self = Self( + ((DisplayOutside::TableCaption as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Flow as u16, + ); + #[cfg(feature = "gecko")] + pub const Ruby: Self = + Self(((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::Ruby as u16); + #[cfg(feature = "gecko")] + pub const WebkitBox: Self = Self( + ((DisplayOutside::Block as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16, + ); + #[cfg(feature = "gecko")] + pub const WebkitInlineBox: Self = Self( + ((DisplayOutside::Inline as u16) << Self::OUTSIDE_SHIFT) | DisplayInside::WebkitBox as u16, + ); + + // Internal table boxes. + + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRowGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableRowGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableHeaderGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableHeaderGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableFooterGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableFooterGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumn: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableColumn as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableColumnGroup: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableColumnGroup as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableRow: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableRow as u16, + ); + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + pub const TableCell: Self = Self( + ((DisplayOutside::InternalTable as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::TableCell as u16, + ); + + /// Internal ruby boxes. + #[cfg(feature = "gecko")] + pub const RubyBase: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyBase as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyBaseContainer: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyBaseContainer as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyText: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyText as u16, + ); + #[cfg(feature = "gecko")] + pub const RubyTextContainer: Self = Self( + ((DisplayOutside::InternalRuby as u16) << Self::OUTSIDE_SHIFT) | + DisplayInside::RubyTextContainer as u16, + ); + + /// Make a raw display value from <display-outside> and <display-inside> values. + #[inline] + const fn new(outside: DisplayOutside, inside: DisplayInside) -> Self { + Self((outside as u16) << Self::OUTSIDE_SHIFT | inside as u16) + } + + /// Make a display enum value from <display-outside> and <display-inside> values. + #[inline] + fn from3(outside: DisplayOutside, inside: DisplayInside, list_item: bool) -> Self { + let v = Self::new(outside, inside); + if !list_item { + return v; + } + Self(v.0 | Self::LIST_ITEM_MASK) + } + + /// Accessor for the <display-inside> value. + #[inline] + pub fn inside(&self) -> DisplayInside { + DisplayInside::from_u16(self.0 & Self::INSIDE_MASK).unwrap() + } + + /// Accessor for the <display-outside> value. + #[inline] + pub fn outside(&self) -> DisplayOutside { + DisplayOutside::from_u16((self.0 & Self::OUTSIDE_MASK) >> Self::OUTSIDE_SHIFT).unwrap() + } + + /// Returns the raw underlying u16 value. + #[inline] + pub const fn to_u16(&self) -> u16 { + self.0 + } + + /// Whether this is `display: inline` (or `inline list-item`). + #[inline] + pub fn is_inline_flow(&self) -> bool { + self.outside() == DisplayOutside::Inline && self.inside() == DisplayInside::Flow + } + + /// Returns whether this `display` value is some kind of list-item. + #[inline] + pub const fn is_list_item(&self) -> bool { + (self.0 & Self::LIST_ITEM_MASK) != 0 + } + + /// Returns whether this `display` value is a ruby level container. + pub fn is_ruby_level_container(&self) -> bool { + match *self { + #[cfg(feature = "gecko")] + Display::RubyBaseContainer | Display::RubyTextContainer => true, + _ => false, + } + } + + /// Returns whether this `display` value is one of the types for ruby. + pub fn is_ruby_type(&self) -> bool { + match self.inside() { + #[cfg(feature = "gecko")] + DisplayInside::Ruby | + DisplayInside::RubyBase | + DisplayInside::RubyText | + DisplayInside::RubyBaseContainer | + DisplayInside::RubyTextContainer => true, + _ => false, + } + } +} + +/// Shared Display impl for both Gecko and Servo. +impl Display { + /// The initial display value. + #[inline] + pub fn inline() -> Self { + Display::Inline + } + + /// <https://drafts.csswg.org/css2/visuren.html#x13> + #[cfg(feature = "servo")] + #[inline] + pub fn is_atomic_inline_level(&self) -> bool { + match *self { + Display::InlineBlock | Display::InlineFlex => true, + #[cfg(any(feature = "servo-layout-2013"))] + Display::InlineTable => true, + _ => false, + } + } + + /// Returns whether this `display` value is the display of a flex or + /// grid container. + /// + /// This is used to implement various style fixups. + pub fn is_item_container(&self) -> bool { + match self.inside() { + DisplayInside::Flex => true, + #[cfg(feature = "gecko")] + DisplayInside::Grid => true, + _ => false, + } + } + + /// Returns whether an element with this display type is a line + /// participant, which means it may lay its children on the same + /// line as itself. + pub fn is_line_participant(&self) -> bool { + if self.is_inline_flow() { + return true; + } + match *self { + #[cfg(feature = "gecko")] + Display::Contents | Display::Ruby | Display::RubyBaseContainer => true, + _ => false, + } + } + + /// Convert this display into an equivalent block display. + /// + /// Also used for :root style adjustments. + pub fn equivalent_block_display(&self, _is_root_element: bool) -> Self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + { + // Special handling for `contents` and `list-item`s on the root element. + if _is_root_element && (self.is_contents() || self.is_list_item()) { + return Display::Block; + } + } + + match self.outside() { + DisplayOutside::Inline => { + let inside = match self.inside() { + // `inline-block` blockifies to `block` rather than + // `flow-root`, for legacy reasons. + DisplayInside::FlowRoot => DisplayInside::Flow, + inside => inside, + }; + Display::from3(DisplayOutside::Block, inside, self.is_list_item()) + }, + DisplayOutside::Block | DisplayOutside::None => *self, + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + _ => Display::Block, + } + } + + /// Convert this display into an equivalent inline-outside display. + /// https://drafts.csswg.org/css-display/#inlinify + #[cfg(feature = "gecko")] + pub fn inlinify(&self) -> Self { + match self.outside() { + DisplayOutside::Block => { + let inside = match self.inside() { + // `display: block` inlinifies to `display: inline-block`, + // rather than `inline`, for legacy reasons. + DisplayInside::Flow => DisplayInside::FlowRoot, + inside => inside, + }; + Display::from3(DisplayOutside::Inline, inside, self.is_list_item()) + }, + _ => *self, + } + } + + /// Returns true if the value is `Contents` + #[inline] + pub fn is_contents(&self) -> bool { + match *self { + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + Display::Contents => true, + _ => false, + } + } + + /// Returns true if the value is `None` + #[inline] + pub fn is_none(&self) -> bool { + *self == Display::None + } +} + +enum DisplayKeyword { + Full(Display), + Inside(DisplayInside), + Outside(DisplayOutside), + ListItem, +} + +impl DisplayKeyword { + fn parse<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + use self::DisplayKeyword::*; + Ok(try_match_ident_ignore_ascii_case! { input, + "none" => Full(Display::None), + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "contents" => Full(Display::Contents), + "inline-block" => Full(Display::InlineBlock), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "inline-table" => Full(Display::InlineTable), + "-webkit-flex" if flexbox_enabled() => Full(Display::Flex), + "inline-flex" | "-webkit-inline-flex" if flexbox_enabled() => Full(Display::InlineFlex), + #[cfg(feature = "gecko")] + "inline-grid" => Full(Display::InlineGrid), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-caption" => Full(Display::TableCaption), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row-group" => Full(Display::TableRowGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-header-group" => Full(Display::TableHeaderGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-footer-group" => Full(Display::TableFooterGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column" => Full(Display::TableColumn), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-column-group" => Full(Display::TableColumnGroup), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-row" => Full(Display::TableRow), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table-cell" => Full(Display::TableCell), + #[cfg(feature = "gecko")] + "ruby-base" => Full(Display::RubyBase), + #[cfg(feature = "gecko")] + "ruby-base-container" => Full(Display::RubyBaseContainer), + #[cfg(feature = "gecko")] + "ruby-text" => Full(Display::RubyText), + #[cfg(feature = "gecko")] + "ruby-text-container" => Full(Display::RubyTextContainer), + #[cfg(feature = "gecko")] + "-webkit-box" => Full(Display::WebkitBox), + #[cfg(feature = "gecko")] + "-webkit-inline-box" => Full(Display::WebkitInlineBox), + + /// <display-outside> = block | inline | run-in + /// https://drafts.csswg.org/css-display/#typedef-display-outside + "block" => Outside(DisplayOutside::Block), + "inline" => Outside(DisplayOutside::Inline), + + "list-item" => ListItem, + + /// <display-inside> = flow | flow-root | table | flex | grid | ruby + /// https://drafts.csswg.org/css-display/#typedef-display-inside + "flow" => Inside(DisplayInside::Flow), + "flex" if flexbox_enabled() => Inside(DisplayInside::Flex), + #[cfg(any(feature = "servo-layout-2020", feature = "gecko"))] + "flow-root" => Inside(DisplayInside::FlowRoot), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + "table" => Inside(DisplayInside::Table), + #[cfg(feature = "gecko")] + "grid" => Inside(DisplayInside::Grid), + #[cfg(feature = "gecko")] + "ruby" => Inside(DisplayInside::Ruby), + }) + } +} + +impl ToCss for Display { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + let outside = self.outside(); + let inside = self.inside(); + match *self { + Display::Block | Display::Inline => outside.to_css(dest), + Display::InlineBlock => dest.write_str("inline-block"), + #[cfg(feature = "gecko")] + Display::WebkitInlineBox => dest.write_str("-webkit-inline-box"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + Display::TableCaption => dest.write_str("table-caption"), + _ => match (outside, inside) { + #[cfg(feature = "gecko")] + (DisplayOutside::Inline, DisplayInside::Grid) => dest.write_str("inline-grid"), + (DisplayOutside::Inline, DisplayInside::Flex) => dest.write_str("inline-flex"), + #[cfg(any(feature = "servo-layout-2013", feature = "gecko"))] + (DisplayOutside::Inline, DisplayInside::Table) => dest.write_str("inline-table"), + #[cfg(feature = "gecko")] + (DisplayOutside::Block, DisplayInside::Ruby) => dest.write_str("block ruby"), + (_, inside) => { + if self.is_list_item() { + if outside != DisplayOutside::Block { + outside.to_css(dest)?; + dest.write_char(' ')?; + } + if inside != DisplayInside::Flow { + inside.to_css(dest)?; + dest.write_char(' ')?; + } + dest.write_str("list-item") + } else { + inside.to_css(dest) + } + }, + }, + } + } +} + +impl Parse for Display { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Display, ParseError<'i>> { + let mut got_list_item = false; + let mut inside = None; + let mut outside = None; + match DisplayKeyword::parse(input)? { + DisplayKeyword::Full(d) => return Ok(d), + DisplayKeyword::Outside(o) => { + outside = Some(o); + }, + DisplayKeyword::Inside(i) => { + inside = Some(i); + }, + DisplayKeyword::ListItem => { + got_list_item = true; + }, + }; + + while let Ok(kw) = input.try_parse(DisplayKeyword::parse) { + match kw { + DisplayKeyword::ListItem if !got_list_item => { + got_list_item = true; + }, + DisplayKeyword::Outside(o) if outside.is_none() => { + outside = Some(o); + }, + DisplayKeyword::Inside(i) if inside.is_none() => { + inside = Some(i); + }, + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + let inside = inside.unwrap_or(DisplayInside::Flow); + let outside = outside.unwrap_or_else(|| inside.default_display_outside()); + if got_list_item && !inside.is_valid_for_list_item() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + return Ok(Display::from3(outside, inside, got_list_item)); + } +} + +impl SpecifiedValueInfo for Display { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&[ + "block", + "contents", + "flex", + "flow-root", + "flow-root list-item", + "grid", + "inline", + "inline-block", + "inline-flex", + "inline-grid", + "inline-table", + "inline list-item", + "inline flow-root list-item", + "list-item", + "none", + "block ruby", + "ruby", + "ruby-base", + "ruby-base-container", + "ruby-text", + "ruby-text-container", + "table", + "table-caption", + "table-cell", + "table-column", + "table-column-group", + "table-footer-group", + "table-header-group", + "table-row", + "table-row-group", + "-webkit-box", + "-webkit-inline-box", + ]); + } +} + +/// A specified value for the `contain-intrinsic-size` property. +pub type ContainIntrinsicSize = GenericContainIntrinsicSize<NonNegativeLength>; + +/// A specified value for the `line-clamp` property. +pub type LineClamp = GenericLineClamp<Integer>; + +/// A specified value for the `vertical-align` property. +pub type VerticalAlign = GenericVerticalAlign<LengthPercentage>; + +impl Parse for VerticalAlign { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) + { + return Ok(GenericVerticalAlign::Length(lp)); + } + + Ok(GenericVerticalAlign::Keyword(VerticalAlignKeyword::parse( + input, + )?)) + } +} + +/// A specified value for the `baseline-source` property. +/// https://drafts.csswg.org/css-inline-3/#baseline-source +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToShmem, + ToComputedValue, + ToResolvedValue, +)] +#[repr(u8)] +pub enum BaselineSource { + /// `Last` for `inline-block`, `First` otherwise. + Auto, + /// Use first baseline for alignment. + First, + /// Use last baseline for alignment. + Last, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-axis +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAxis { + X, + Y, + Block, + Inline, + Both, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#snap-strictness +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapStrictness { + #[css(skip)] + None, // Used to represent scroll-snap-type: none. It's not parsed. + Mandatory, + Proximity, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-type +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapType { + axis: ScrollSnapAxis, + strictness: ScrollSnapStrictness, +} + +impl ScrollSnapType { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + Self { + axis: ScrollSnapAxis::Both, + strictness: ScrollSnapStrictness::None, + } + } +} + +impl Parse for ScrollSnapType { + /// none | [ x | y | block | inline | both ] [ mandatory | proximity ]? + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(ScrollSnapType::none()); + } + + let axis = ScrollSnapAxis::parse(input)?; + let strictness = input + .try_parse(ScrollSnapStrictness::parse) + .unwrap_or(ScrollSnapStrictness::Proximity); + Ok(Self { axis, strictness }) + } +} + +impl ToCss for ScrollSnapType { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.strictness == ScrollSnapStrictness::None { + return dest.write_str("none"); + } + self.axis.to_css(dest)?; + if self.strictness != ScrollSnapStrictness::Proximity { + dest.write_char(' ')?; + self.strictness.to_css(dest)?; + } + Ok(()) + } +} + +/// Specified value of scroll-snap-align keyword value. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapAlignKeyword { + None, + Start, + End, + Center, +} + +/// https://drafts.csswg.org/css-scroll-snap-1/#scroll-snap-align +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ScrollSnapAlign { + block: ScrollSnapAlignKeyword, + inline: ScrollSnapAlignKeyword, +} + +impl ScrollSnapAlign { + /// Returns `none`. + #[inline] + pub fn none() -> Self { + ScrollSnapAlign { + block: ScrollSnapAlignKeyword::None, + inline: ScrollSnapAlignKeyword::None, + } + } +} + +impl Parse for ScrollSnapAlign { + /// [ none | start | end | center ]{1,2} + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<ScrollSnapAlign, ParseError<'i>> { + let block = ScrollSnapAlignKeyword::parse(input)?; + let inline = input + .try_parse(ScrollSnapAlignKeyword::parse) + .unwrap_or(block); + Ok(ScrollSnapAlign { block, inline }) + } +} + +impl ToCss for ScrollSnapAlign { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + self.block.to_css(dest)?; + if self.block != self.inline { + dest.write_char(' ')?; + self.inline.to_css(dest)?; + } + Ok(()) + } +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ScrollSnapStop { + Normal, + Always, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverscrollBehavior { + Auto, + Contain, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowAnchor { + Auto, + None, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum OverflowClipBox { + PaddingBox, + ContentBox, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(comma)] +#[repr(C)] +/// Provides a rendering hint to the user agent, stating what kinds of changes +/// the author expects to perform on the element. +/// +/// `auto` is represented by an empty `features` list. +/// +/// <https://drafts.csswg.org/css-will-change/#will-change> +pub struct WillChange { + /// The features that are supposed to change. + /// + /// TODO(emilio): Consider using ArcSlice since we just clone them from the + /// specified value? That'd save an allocation, which could be worth it. + #[css(iterable, if_empty = "auto")] + features: crate::OwnedSlice<CustomIdent>, + /// A bitfield with the kind of change that the value will create, based + /// on the above field. + #[css(skip)] + bits: WillChangeBits, +} + +impl WillChange { + #[inline] + /// Get default value of `will-change` as `auto` + pub fn auto() -> Self { + Self::default() + } +} + +/// The change bits that we care about. +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct WillChangeBits(u16); +bitflags! { + impl WillChangeBits: u16 { + /// Whether a property which can create a stacking context **on any + /// box** will change. + const STACKING_CONTEXT_UNCONDITIONAL = 1 << 0; + /// Whether `transform` or related properties will change. + const TRANSFORM = 1 << 1; + /// Whether `scroll-position` will change. + const SCROLL = 1 << 2; + /// Whether `contain` will change. + const CONTAIN = 1 << 3; + /// Whether `opacity` will change. + const OPACITY = 1 << 4; + /// Whether `perspective` will change. + const PERSPECTIVE = 1 << 5; + /// Whether `z-index` will change. + const Z_INDEX = 1 << 6; + /// Whether any property which creates a containing block for non-svg + /// text frames will change. + const FIXPOS_CB_NON_SVG = 1 << 7; + /// Whether the position property will change. + const POSITION = 1 << 8; + } +} + +fn change_bits_for_longhand(longhand: LonghandId) -> WillChangeBits { + match longhand { + LonghandId::Opacity => WillChangeBits::OPACITY, + LonghandId::Contain => WillChangeBits::CONTAIN, + LonghandId::Perspective => WillChangeBits::PERSPECTIVE, + LonghandId::Position => { + WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::POSITION + }, + LonghandId::ZIndex => WillChangeBits::Z_INDEX, + LonghandId::Transform | + LonghandId::TransformStyle | + LonghandId::Translate | + LonghandId::Rotate | + LonghandId::Scale | + LonghandId::OffsetPath => WillChangeBits::TRANSFORM, + LonghandId::BackdropFilter | LonghandId::Filter => { + WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL | WillChangeBits::FIXPOS_CB_NON_SVG + }, + LonghandId::MixBlendMode | + LonghandId::Isolation | + LonghandId::MaskImage | + LonghandId::ClipPath => WillChangeBits::STACKING_CONTEXT_UNCONDITIONAL, + _ => WillChangeBits::empty(), + } +} + +fn change_bits_for_maybe_property(ident: &str, context: &ParserContext) -> WillChangeBits { + let id = match PropertyId::parse_ignoring_rule_type(ident, context) { + Ok(id) => id, + Err(..) => return WillChangeBits::empty(), + }; + + match id.as_shorthand() { + Ok(shorthand) => shorthand + .longhands() + .fold(WillChangeBits::empty(), |flags, p| { + flags | change_bits_for_longhand(p) + }), + Err(PropertyDeclarationId::Longhand(longhand)) => change_bits_for_longhand(longhand), + Err(PropertyDeclarationId::Custom(..)) => WillChangeBits::empty(), + } +} + +impl Parse for WillChange { + /// auto | <animateable-feature># + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(Self::default()); + } + + let mut bits = WillChangeBits::empty(); + let custom_idents = input.parse_comma_separated(|i| { + let location = i.current_source_location(); + let parser_ident = i.expect_ident()?; + let ident = CustomIdent::from_ident( + location, + parser_ident, + &["will-change", "none", "all", "auto"], + )?; + + if context.in_ua_sheet() && ident.0 == atom!("-moz-fixed-pos-containing-block") { + bits |= WillChangeBits::FIXPOS_CB_NON_SVG; + } else if ident.0 == atom!("scroll-position") { + bits |= WillChangeBits::SCROLL; + } else { + bits |= change_bits_for_maybe_property(&parser_ident, context); + } + Ok(ident) + })?; + + Ok(Self { + features: custom_idents.into(), + bits, + }) + } +} + +/// Values for the `touch-action` property. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags(single = "none,auto,manipulation", mixed = "pan-x,pan-y,pinch-zoom"))] +#[repr(C)] +pub struct TouchAction(u8); +bitflags! { + impl TouchAction: u8 { + /// `none` variant + const NONE = 1 << 0; + /// `auto` variant + const AUTO = 1 << 1; + /// `pan-x` variant + const PAN_X = 1 << 2; + /// `pan-y` variant + const PAN_Y = 1 << 3; + /// `manipulation` variant + const MANIPULATION = 1 << 4; + /// `pinch-zoom` variant + const PINCH_ZOOM = 1 << 5; + } +} + +impl TouchAction { + #[inline] + /// Get default `touch-action` as `auto` + pub fn auto() -> TouchAction { + TouchAction::AUTO + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags( + single = "none,strict,content", + mixed = "size,layout,style,paint,inline-size", + overlapping_bits +))] +#[repr(C)] +/// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property +pub struct Contain(u8); +bitflags! { + impl Contain: u8 { + /// `none` variant, just for convenience. + const NONE = 0; + /// `inline-size` variant, turns on single-axis inline size containment + const INLINE_SIZE = 1 << 0; + /// `block-size` variant, turns on single-axis block size containment, internal only + const BLOCK_SIZE = 1 << 1; + /// `layout` variant, turns on layout containment + const LAYOUT = 1 << 2; + /// `style` variant, turns on style containment + const STYLE = 1 << 3; + /// `paint` variant, turns on paint containment + const PAINT = 1 << 4; + /// 'size' variant, turns on size containment + const SIZE = 1 << 5 | Contain::INLINE_SIZE.bits() | Contain::BLOCK_SIZE.bits(); + /// `content` variant, turns on layout and paint containment + const CONTENT = 1 << 6 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits(); + /// `strict` variant, turns on all types of containment + const STRICT = 1 << 7 | Contain::LAYOUT.bits() | Contain::STYLE.bits() | Contain::PAINT.bits() | Contain::SIZE.bits(); + } +} + +impl Parse for ContainIntrinsicSize { + /// none | <length> | auto <length> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(l) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + return Ok(Self::Length(l)); + } + + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(Self::AutoNone); + } + + let l = NonNegativeLength::parse(context, input)?; + return Ok(Self::AutoLength(l)); + } + + input.expect_ident_matching("none")?; + Ok(Self::None) + } +} + +impl Parse for LineClamp { + /// none | <positive-integer> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(i) = + input.try_parse(|i| crate::values::specified::PositiveInteger::parse(context, i)) + { + return Ok(Self(i.0)); + } + input.expect_ident_matching("none")?; + Ok(Self::none()) + } +} + +/// https://drafts.csswg.org/css-contain-2/#content-visibility +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ContentVisibility { + /// `auto` variant, the element turns on layout containment, style containment, and paint + /// containment. In addition, if the element is not relevant to the user (such as by being + /// offscreen) it also skips its content + Auto, + /// `hidden` variant, the element skips its content + Hidden, + /// 'visible' variant, no effect + Visible, +} + +#[derive( + Clone, + Copy, + Debug, + PartialEq, + Eq, + MallocSizeOf, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + Parse, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +#[allow(missing_docs)] +/// https://drafts.csswg.org/css-contain-3/#container-type +pub enum ContainerType { + /// The `normal` variant. + Normal, + /// The `inline-size` variant. + InlineSize, + /// The `size` variant. + Size, +} + +impl ContainerType { + /// Is this container-type: normal? + pub fn is_normal(self) -> bool { + self == Self::Normal + } + + /// Is this type containing size in any way? + pub fn is_size_container_type(self) -> bool { + !self.is_normal() + } +} + +/// https://drafts.csswg.org/css-contain-3/#container-name +#[repr(transparent)] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub struct ContainerName(#[css(iterable, if_empty = "none")] pub crate::OwnedSlice<CustomIdent>); + +impl ContainerName { + /// Return the `none` value. + pub fn none() -> Self { + Self(Default::default()) + } + + /// Returns whether this is the `none` value. + pub fn is_none(&self) -> bool { + self.0.is_empty() + } + + fn parse_internal<'i>( + input: &mut Parser<'i, '_>, + for_query: bool, + ) -> Result<Self, ParseError<'i>> { + let mut idents = vec![]; + let location = input.current_source_location(); + let first = input.expect_ident()?; + if !for_query && first.eq_ignore_ascii_case("none") { + return Ok(Self::none()); + } + const DISALLOWED_CONTAINER_NAMES: &'static [&'static str] = &["none", "not", "or", "and"]; + idents.push(CustomIdent::from_ident( + location, + first, + DISALLOWED_CONTAINER_NAMES, + )?); + if !for_query { + while let Ok(name) = + input.try_parse(|input| CustomIdent::parse(input, DISALLOWED_CONTAINER_NAMES)) + { + idents.push(name); + } + } + Ok(ContainerName(idents.into())) + } + + /// https://github.com/w3c/csswg-drafts/issues/7203 + /// Only a single name allowed in @container rule. + /// Disallow none for container-name in @container rule. + pub fn parse_for_query<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(input, /* for_query = */ true) + } +} + +impl Parse for ContainerName { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(input, /* for_query = */ false) + } +} + +/// A specified value for the `perspective` property. +pub type Perspective = GenericPerspective<NonNegativeLength>; + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css-box/#propdef-float +pub enum Float { + Left, + Right, + None, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +/// https://drafts.csswg.org/css2/#propdef-clear +pub enum Clear { + None, + Left, + Right, + Both, + // https://drafts.csswg.org/css-logical-props/#float-clear + InlineStart, + InlineEnd, +} + +/// https://drafts.csswg.org/css-ui/#propdef-resize +#[allow(missing_docs)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum Resize { + None, + Both, + Horizontal, + Vertical, + // https://drafts.csswg.org/css-logical-1/#resize + Inline, + Block, +} + +/// The value for the `appearance` property. +/// +/// https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-appearance +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Appearance { + /// No appearance at all. + None, + /// Default appearance for the element. + /// + /// This value doesn't make sense for -moz-default-appearance, but we don't bother to guard + /// against parsing it. + Auto, + /// A searchfield. + Searchfield, + /// A multi-line text field, e.g. HTML <textarea>. + Textarea, + /// A checkbox element. + Checkbox, + /// A radio element within a radio group. + Radio, + /// A dropdown list. + Menulist, + /// List boxes. + Listbox, + /// A horizontal meter bar. + Meter, + /// A horizontal progress bar. + ProgressBar, + /// A typical dialog button. + Button, + /// A single-line text field, e.g. HTML <input type=text>. + Textfield, + /// The dropdown button(s) that open up a dropdown list. + MenulistButton, + /// Various arrows that go in buttons + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowDown, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowNext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowPrevious, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ButtonArrowUp, + /// A dual toolbar button (e.g., a Back button with a dropdown) + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Dualbutton, + /// Menu Popup background. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Menupopup, + /// The meter bar's meter indicator. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Meterchunk, + /// The "arrowed" part of the dropdown button that open up a dropdown list. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMenulistArrowButton, + /// For HTML's <input type=number> + #[parse(condition = "ParserContext::chrome_rules_enabled")] + NumberInput, + /// The progress bar's progress indicator + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Progresschunk, + /// nsRangeFrame and its subparts + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Range, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + RangeThumb, + /// The scrollbar slider + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarHorizontal, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarVertical, + /// A scrollbar button (up/down/left/right). + /// Keep these in order (some code casts these values to `int` in order to + /// compare them against each other). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonUp, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonDown, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonLeft, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarbuttonRight, + /// The scrollbar thumb. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarthumbHorizontal, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbarthumbVertical, + /// The scrollbar track. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbartrackHorizontal, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ScrollbartrackVertical, + /// The scroll corner + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Scrollcorner, + /// A separator. Can be horizontal or vertical. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Separator, + /// A spin control (up/down control for time/date pickers). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Spinner, + /// The up button of a spin control. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + SpinnerUpbutton, + /// The down button of a spin control. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + SpinnerDownbutton, + /// The textfield of a spin control + #[parse(condition = "ParserContext::chrome_rules_enabled")] + SpinnerTextfield, + /// A splitter. Can be horizontal or vertical. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Splitter, + /// A status bar in a main application window. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Statusbar, + /// A single tab in a tab widget. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tab, + /// A single pane (inside the tabpanels container). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tabpanel, + /// The tab panels container. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tabpanels, + /// The tabs scroll arrows (left/right). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + TabScrollArrowBack, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + TabScrollArrowForward, + /// A toolbar in an application window. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Toolbar, + /// A single toolbar button (with no associated dropdown). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Toolbarbutton, + /// The dropdown portion of a toolbar button + #[parse(condition = "ParserContext::chrome_rules_enabled")] + ToolbarbuttonDropdown, + /// The toolbox that contains the toolbars. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Toolbox, + /// A tooltip. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Tooltip, + /// A listbox or tree widget header + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeheader, + /// An individual header cell + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeheadercell, + /// A tree item. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeitem, + /// A tree widget branch line + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeline, + /// A tree widget twisty. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treetwisty, + /// Open tree widget twisty. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treetwistyopen, + /// A tree widget. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + Treeview, + + /// Mac help button. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacHelpButton, + + /// An appearance value for the root, so that we can get unified toolbar looks (which require a + /// transparent gecko background) without really using the whole transparency set-up which + /// otherwise loses window borders, see bug 1870481. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacUnifiedToolbarWindow, + + /// Windows themed window frame elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonBox, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonClose, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonMaximize, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonMinimize, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowButtonRestore, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowTitlebar, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowTitlebarMaximized, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozWindowDecorations, + + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDisclosureButtonClosed, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDisclosureButtonOpen, + + /// A themed focus outline (for outline:auto). + /// + /// This isn't exposed to CSS at all, just here for convenience. + #[css(skip)] + FocusOutline, + + /// A dummy variant that should be last to let the GTK widget do hackery. + #[css(skip)] + Count, +} + +/// A kind of break between two boxes. +/// +/// https://drafts.csswg.org/css-break/#break-between +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BreakBetween { + Always, + Auto, + Page, + Avoid, + Left, + Right, +} + +impl BreakBetween { + /// Parse a legacy break-between value for `page-break-{before,after}`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + #[inline] + pub(crate) fn parse_legacy<'i>( + _: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + let break_value = BreakBetween::parse(input)?; + match break_value { + BreakBetween::Always => Ok(BreakBetween::Page), + BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => { + Ok(break_value) + }, + BreakBetween::Page => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + } + + /// Serialize a legacy break-between value for `page-break-*`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + BreakBetween::Auto | BreakBetween::Avoid | BreakBetween::Left | BreakBetween::Right => { + self.to_css(dest) + }, + BreakBetween::Page => dest.write_str("always"), + BreakBetween::Always => Ok(()), + } + } +} + +/// A kind of break within a box. +/// +/// https://drafts.csswg.org/css-break/#break-within +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum BreakWithin { + Auto, + Avoid, + AvoidPage, + AvoidColumn, +} + +impl BreakWithin { + /// Parse a legacy break-between value for `page-break-inside`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + #[inline] + pub(crate) fn parse_legacy<'i>( + _: &ParserContext, + input: &mut Parser<'i, '_>, + ) -> Result<Self, ParseError<'i>> { + let break_value = BreakWithin::parse(input)?; + match break_value { + BreakWithin::Auto | BreakWithin::Avoid => Ok(break_value), + BreakWithin::AvoidPage | BreakWithin::AvoidColumn => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + } + } + + /// Serialize a legacy break-between value for `page-break-inside`. + /// + /// See https://drafts.csswg.org/css-break/#page-break-properties. + pub(crate) fn to_css_legacy<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + BreakWithin::Auto | BreakWithin::Avoid => self.to_css(dest), + BreakWithin::AvoidPage | BreakWithin::AvoidColumn => Ok(()), + } + } +} + +/// The value for the `overflow-x` / `overflow-y` properties. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum Overflow { + Visible, + Hidden, + Scroll, + Auto, + #[cfg(feature = "gecko")] + Clip, +} + +// This can be derived once we remove or keep `-moz-hidden-unscrollable` +// indefinitely. +impl Parse for Overflow { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "visible" => Self::Visible, + "hidden" => Self::Hidden, + "scroll" => Self::Scroll, + "auto" | "overlay" => Self::Auto, + #[cfg(feature = "gecko")] + "clip" => Self::Clip, + #[cfg(feature = "gecko")] + "-moz-hidden-unscrollable" if static_prefs::pref!("layout.css.overflow-moz-hidden-unscrollable.enabled") => { + Overflow::Clip + }, + }) + } +} + +impl Overflow { + /// Return true if the value will create a scrollable box. + #[inline] + pub fn is_scrollable(&self) -> bool { + matches!(*self, Self::Hidden | Self::Scroll | Self::Auto) + } + /// Convert the value to a scrollable value if it's not already scrollable. + /// This maps `visible` to `auto` and `clip` to `hidden`. + #[inline] + pub fn to_scrollable(&self) -> Self { + match *self { + Self::Hidden | Self::Scroll | Self::Auto => *self, + Self::Visible => Self::Auto, + #[cfg(feature = "gecko")] + Self::Clip => Self::Hidden, + } + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[css(bitflags( + single = "auto", + mixed = "stable,both-edges", + validate_mixed = "Self::has_stable" +))] +/// Values for scrollbar-gutter: +/// <https://drafts.csswg.org/css-overflow-3/#scrollbar-gutter-property> +pub struct ScrollbarGutter(u8); +bitflags! { + impl ScrollbarGutter: u8 { + /// `auto` variant. Just for convenience if there is no flag set. + const AUTO = 0; + /// `stable` variant. + const STABLE = 1 << 0; + /// `both-edges` variant. + const BOTH_EDGES = 1 << 1; + } +} + +impl ScrollbarGutter { + #[inline] + fn has_stable(&self) -> bool { + self.intersects(Self::STABLE) + } +} + +/// A specified value for the zoom property. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, Parse, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum Zoom { + Normal, + /// An internal value that resets the effective zoom to 1. Used for scrollbar parts, which + /// disregard zoom. We use this name because WebKit has this value exposed to the web. + #[parse(condition = "ParserContext::in_ua_sheet")] + Document, + Value(NonNegativeNumberOrPercentage), +} + +impl Zoom { + /// Return a particular number value of the zoom property. + #[inline] + pub fn new_number(n: f32) -> Self { + Self::Value(NonNegativeNumberOrPercentage::new_number(n)) + } +} diff --git a/servo/components/style/values/specified/calc.rs b/servo/components/style/values/specified/calc.rs new file mode 100644 index 0000000000..2660864319 --- /dev/null +++ b/servo/components/style/values/specified/calc.rs @@ -0,0 +1,1086 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Calc expressions][calc]. +//! +//! [calc]: https://drafts.csswg.org/css-values/#calc-notation + +use crate::color::parsing::{AngleOrNumber, NumberOrPercentage}; +use crate::parser::ParserContext; +use crate::values::generics::calc::{ + self as generic, CalcNodeLeaf, CalcUnits, MinMaxOp, ModRemOp, PositivePercentageBasis, + RoundingStrategy, SortKey, +}; +use crate::values::specified::length::{AbsoluteLength, FontRelativeLength, NoCalcLength}; +use crate::values::specified::length::{ContainerRelativeLength, ViewportPercentageLength}; +use crate::values::specified::{self, Angle, Resolution, Time}; +use crate::values::{serialize_number, serialize_percentage, CSSFloat, CSSInteger}; +use cssparser::{CowRcStr, Parser, Token}; +use smallvec::SmallVec; +use std::cmp; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +/// The name of the mathematical function that we're parsing. +#[derive(Clone, Copy, Debug, Parse)] +pub enum MathFunction { + /// `calc()`: https://drafts.csswg.org/css-values-4/#funcdef-calc + Calc, + /// `min()`: https://drafts.csswg.org/css-values-4/#funcdef-min + Min, + /// `max()`: https://drafts.csswg.org/css-values-4/#funcdef-max + Max, + /// `clamp()`: https://drafts.csswg.org/css-values-4/#funcdef-clamp + Clamp, + /// `round()`: https://drafts.csswg.org/css-values-4/#funcdef-round + Round, + /// `mod()`: https://drafts.csswg.org/css-values-4/#funcdef-mod + Mod, + /// `rem()`: https://drafts.csswg.org/css-values-4/#funcdef-rem + Rem, + /// `sin()`: https://drafts.csswg.org/css-values-4/#funcdef-sin + Sin, + /// `cos()`: https://drafts.csswg.org/css-values-4/#funcdef-cos + Cos, + /// `tan()`: https://drafts.csswg.org/css-values-4/#funcdef-tan + Tan, + /// `asin()`: https://drafts.csswg.org/css-values-4/#funcdef-asin + Asin, + /// `acos()`: https://drafts.csswg.org/css-values-4/#funcdef-acos + Acos, + /// `atan()`: https://drafts.csswg.org/css-values-4/#funcdef-atan + Atan, + /// `atan2()`: https://drafts.csswg.org/css-values-4/#funcdef-atan2 + Atan2, + /// `pow()`: https://drafts.csswg.org/css-values-4/#funcdef-pow + Pow, + /// `sqrt()`: https://drafts.csswg.org/css-values-4/#funcdef-sqrt + Sqrt, + /// `hypot()`: https://drafts.csswg.org/css-values-4/#funcdef-hypot + Hypot, + /// `log()`: https://drafts.csswg.org/css-values-4/#funcdef-log + Log, + /// `exp()`: https://drafts.csswg.org/css-values-4/#funcdef-exp + Exp, + /// `abs()`: https://drafts.csswg.org/css-values-4/#funcdef-abs + Abs, + /// `sign()`: https://drafts.csswg.org/css-values-4/#funcdef-sign + Sign, +} + +/// A leaf node inside a `Calc` expression's AST. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum Leaf { + /// `<length>` + Length(NoCalcLength), + /// `<angle>` + Angle(Angle), + /// `<time>` + Time(Time), + /// `<resolution>` + Resolution(Resolution), + /// `<percentage>` + Percentage(CSSFloat), + /// `<number>` + Number(CSSFloat), +} + +impl Leaf { + fn as_length(&self) -> Option<&NoCalcLength> { + match *self { + Self::Length(ref l) => Some(l), + _ => None, + } + } +} + +impl ToCss for Leaf { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Self::Length(ref l) => l.to_css(dest), + Self::Number(n) => serialize_number(n, /* was_calc = */ false, dest), + Self::Resolution(ref r) => r.to_css(dest), + Self::Percentage(p) => serialize_percentage(p, dest), + Self::Angle(ref a) => a.to_css(dest), + Self::Time(ref t) => t.to_css(dest), + } + } +} + +/// A struct to hold a simplified `<length>` or `<percentage>` expression. +/// +/// In some cases, e.g. DOMMatrix, we support calc(), but reject all the +/// relative lengths, and to_computed_pixel_length_without_context() handles +/// this case. Therefore, if you want to add a new field, please make sure this +/// function work properly. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +#[allow(missing_docs)] +pub struct CalcLengthPercentage { + #[css(skip)] + pub clamping_mode: AllowedNumericType, + pub node: CalcNode, +} + +impl CalcLengthPercentage { + fn same_unit_length_as(a: &Self, b: &Self) -> Option<(CSSFloat, CSSFloat)> { + debug_assert_eq!(a.clamping_mode, b.clamping_mode); + debug_assert_eq!(a.clamping_mode, AllowedNumericType::All); + + let a = a.node.as_leaf()?; + let b = b.node.as_leaf()?; + + if a.sort_key() != b.sort_key() { + return None; + } + + let a = a.as_length()?.unitless_value(); + let b = b.as_length()?.unitless_value(); + return Some((a, b)); + } +} + +impl SpecifiedValueInfo for CalcLengthPercentage {} + +impl generic::CalcNodeLeaf for Leaf { + fn unit(&self) -> CalcUnits { + match self { + Leaf::Length(_) => CalcUnits::LENGTH, + Leaf::Angle(_) => CalcUnits::ANGLE, + Leaf::Time(_) => CalcUnits::TIME, + Leaf::Resolution(_) => CalcUnits::RESOLUTION, + Leaf::Percentage(_) => CalcUnits::PERCENTAGE, + Leaf::Number(_) => CalcUnits::empty(), + } + } + + fn unitless_value(&self) -> f32 { + match *self { + Self::Length(ref l) => l.unitless_value(), + Self::Percentage(n) | Self::Number(n) => n, + Self::Resolution(ref r) => r.dppx(), + Self::Angle(ref a) => a.degrees(), + Self::Time(ref t) => t.seconds(), + } + } + + fn new_number(value: f32) -> Self { + Self::Number(value) + } + + fn compare(&self, other: &Self, basis: PositivePercentageBasis) -> Option<cmp::Ordering> { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + if matches!(self, Percentage(..)) && matches!(basis, PositivePercentageBasis::Unknown) { + return None; + } + + let self_negative = self.is_negative(); + if self_negative != other.is_negative() { + return Some(if self_negative { cmp::Ordering::Less } else { cmp::Ordering::Greater }); + } + + match (self, other) { + (&Percentage(ref one), &Percentage(ref other)) => one.partial_cmp(other), + (&Length(ref one), &Length(ref other)) => one.partial_cmp(other), + (&Angle(ref one), &Angle(ref other)) => one.degrees().partial_cmp(&other.degrees()), + (&Time(ref one), &Time(ref other)) => one.seconds().partial_cmp(&other.seconds()), + (&Resolution(ref one), &Resolution(ref other)) => one.dppx().partial_cmp(&other.dppx()), + (&Number(ref one), &Number(ref other)) => one.partial_cmp(other), + _ => { + match *self { + Length(..) | Percentage(..) | Angle(..) | Time(..) | Number(..) | + Resolution(..) => {}, + } + unsafe { + debug_unreachable!("Forgot a branch?"); + } + }, + } + } + + fn as_number(&self) -> Option<f32> { + match *self { + Leaf::Length(_) | + Leaf::Angle(_) | + Leaf::Time(_) | + Leaf::Resolution(_) | + Leaf::Percentage(_) => None, + Leaf::Number(value) => Some(value), + } + } + + fn sort_key(&self) -> SortKey { + match *self { + Self::Number(..) => SortKey::Number, + Self::Percentage(..) => SortKey::Percentage, + Self::Time(..) => SortKey::Sec, + Self::Resolution(..) => SortKey::Dppx, + Self::Angle(..) => SortKey::Deg, + Self::Length(ref l) => match *l { + NoCalcLength::Absolute(..) => SortKey::Px, + NoCalcLength::FontRelative(ref relative) => match *relative { + FontRelativeLength::Ch(..) => SortKey::Ch, + FontRelativeLength::Em(..) => SortKey::Em, + FontRelativeLength::Ex(..) => SortKey::Ex, + FontRelativeLength::Cap(..) => SortKey::Cap, + FontRelativeLength::Ic(..) => SortKey::Ic, + FontRelativeLength::Rem(..) => SortKey::Rem, + FontRelativeLength::Lh(..) => SortKey::Lh, + FontRelativeLength::Rlh(..) => SortKey::Rlh, + }, + NoCalcLength::ViewportPercentage(ref vp) => match *vp { + ViewportPercentageLength::Vh(..) => SortKey::Vh, + ViewportPercentageLength::Svh(..) => SortKey::Svh, + ViewportPercentageLength::Lvh(..) => SortKey::Lvh, + ViewportPercentageLength::Dvh(..) => SortKey::Dvh, + ViewportPercentageLength::Vw(..) => SortKey::Vw, + ViewportPercentageLength::Svw(..) => SortKey::Svw, + ViewportPercentageLength::Lvw(..) => SortKey::Lvw, + ViewportPercentageLength::Dvw(..) => SortKey::Dvw, + ViewportPercentageLength::Vmax(..) => SortKey::Vmax, + ViewportPercentageLength::Svmax(..) => SortKey::Svmax, + ViewportPercentageLength::Lvmax(..) => SortKey::Lvmax, + ViewportPercentageLength::Dvmax(..) => SortKey::Dvmax, + ViewportPercentageLength::Vmin(..) => SortKey::Vmin, + ViewportPercentageLength::Svmin(..) => SortKey::Svmin, + ViewportPercentageLength::Lvmin(..) => SortKey::Lvmin, + ViewportPercentageLength::Dvmin(..) => SortKey::Dvmin, + ViewportPercentageLength::Vb(..) => SortKey::Vb, + ViewportPercentageLength::Svb(..) => SortKey::Svb, + ViewportPercentageLength::Lvb(..) => SortKey::Lvb, + ViewportPercentageLength::Dvb(..) => SortKey::Dvb, + ViewportPercentageLength::Vi(..) => SortKey::Vi, + ViewportPercentageLength::Svi(..) => SortKey::Svi, + ViewportPercentageLength::Lvi(..) => SortKey::Lvi, + ViewportPercentageLength::Dvi(..) => SortKey::Dvi, + }, + NoCalcLength::ContainerRelative(ref cq) => match *cq { + ContainerRelativeLength::Cqw(..) => SortKey::Cqw, + ContainerRelativeLength::Cqh(..) => SortKey::Cqh, + ContainerRelativeLength::Cqi(..) => SortKey::Cqi, + ContainerRelativeLength::Cqb(..) => SortKey::Cqb, + ContainerRelativeLength::Cqmin(..) => SortKey::Cqmin, + ContainerRelativeLength::Cqmax(..) => SortKey::Cqmax, + }, + NoCalcLength::ServoCharacterWidth(..) => unreachable!(), + }, + } + } + + fn simplify(&mut self) { + if let Self::Length(NoCalcLength::Absolute(ref mut abs)) = *self { + *abs = AbsoluteLength::Px(abs.to_px()); + } + } + + /// Tries to merge one sum to another, that is, perform `x` + `y`. + /// + /// Only handles leaf nodes, it's the caller's responsibility to simplify + /// them before calling this if needed. + fn try_sum_in_place(&mut self, other: &Self) -> Result<(), ()> { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + match (self, other) { + (&mut Number(ref mut one), &Number(ref other)) | + (&mut Percentage(ref mut one), &Percentage(ref other)) => { + *one += *other; + }, + (&mut Angle(ref mut one), &Angle(ref other)) => { + *one = specified::Angle::from_calc(one.degrees() + other.degrees()); + }, + (&mut Time(ref mut one), &Time(ref other)) => { + *one = specified::Time::from_seconds(one.seconds() + other.seconds()); + }, + (&mut Resolution(ref mut one), &Resolution(ref other)) => { + *one = specified::Resolution::from_dppx(one.dppx() + other.dppx()); + }, + (&mut Length(ref mut one), &Length(ref other)) => { + *one = one.try_op(other, std::ops::Add::add)?; + }, + _ => { + match *other { + Number(..) | Percentage(..) | Angle(..) | Time(..) | Resolution(..) | + Length(..) => {}, + } + unsafe { + debug_unreachable!(); + } + }, + } + + Ok(()) + } + + fn try_product_in_place(&mut self, other: &mut Self) -> bool { + if let Self::Number(ref mut left) = *self { + if let Self::Number(ref right) = *other { + // Both sides are numbers, so we can just modify the left side. + *left *= *right; + true + } else { + // The right side is not a number, so the result should be in the units of the right + // side. + other.map(|v| v * *left); + std::mem::swap(self, other); + true + } + } else if let Self::Number(ref right) = *other { + // The left side is not a number, but the right side is, so the result is the left + // side unit. + self.map(|v| v * *right); + true + } else { + // Neither side is a number, so a product is not possible. + false + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::Leaf::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + match (self, other) { + (&Number(one), &Number(other)) => { + return Ok(Leaf::Number(op(one, other))); + }, + (&Percentage(one), &Percentage(other)) => { + return Ok(Leaf::Percentage(op(one, other))); + }, + (&Angle(ref one), &Angle(ref other)) => { + return Ok(Leaf::Angle(specified::Angle::from_calc(op( + one.degrees(), + other.degrees(), + )))); + }, + (&Resolution(ref one), &Resolution(ref other)) => { + return Ok(Leaf::Resolution(specified::Resolution::from_dppx(op( + one.dppx(), + other.dppx(), + )))); + }, + (&Time(ref one), &Time(ref other)) => { + return Ok(Leaf::Time(specified::Time::from_seconds(op( + one.seconds(), + other.seconds(), + )))); + }, + (&Length(ref one), &Length(ref other)) => { + return Ok(Leaf::Length(one.try_op(other, op)?)); + }, + _ => { + match *other { + Number(..) | Percentage(..) | Angle(..) | Time(..) | Length(..) | + Resolution(..) => {}, + } + unsafe { + debug_unreachable!(); + } + }, + } + } + + fn map(&mut self, mut op: impl FnMut(f32) -> f32) { + match self { + Leaf::Length(one) => *one = one.map(op), + Leaf::Angle(one) => *one = specified::Angle::from_calc(op(one.degrees())), + Leaf::Time(one) => *one = specified::Time::from_seconds(op(one.seconds())), + Leaf::Resolution(one) => *one = specified::Resolution::from_dppx(op(one.dppx())), + Leaf::Percentage(one) => *one = op(*one), + Leaf::Number(one) => *one = op(*one), + } + } +} + +/// A calc node representation for specified values. +pub type CalcNode = generic::GenericCalcNode<Leaf>; + +impl CalcNode { + /// Tries to parse a single element in the expression, that is, a + /// `<length>`, `<angle>`, `<time>`, `<percentage>`, `<resolution>`, etc. + /// + /// May return a "complex" `CalcNode`, in the presence of a parenthesized + /// expression, for example. + fn parse_one<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match input.next()? { + &Token::Number { value, .. } => Ok(CalcNode::Leaf(Leaf::Number(value))), + &Token::Dimension { + value, ref unit, .. + } => { + if allowed_units.intersects(CalcUnits::LENGTH) { + if let Ok(l) = NoCalcLength::parse_dimension(context, value, unit) { + return Ok(CalcNode::Leaf(Leaf::Length(l))); + } + } + if allowed_units.intersects(CalcUnits::ANGLE) { + if let Ok(a) = Angle::parse_dimension(value, unit, /* from_calc = */ true) { + return Ok(CalcNode::Leaf(Leaf::Angle(a))); + } + } + if allowed_units.intersects(CalcUnits::TIME) { + if let Ok(t) = Time::parse_dimension(value, unit) { + return Ok(CalcNode::Leaf(Leaf::Time(t))); + } + } + if allowed_units.intersects(CalcUnits::RESOLUTION) { + if let Ok(t) = Resolution::parse_dimension(value, unit) { + return Ok(CalcNode::Leaf(Leaf::Resolution(t))); + } + } + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }, + &Token::Percentage { unit_value, .. } + if allowed_units.intersects(CalcUnits::PERCENTAGE) => + { + Ok(CalcNode::Leaf(Leaf::Percentage(unit_value))) + }, + &Token::ParenthesisBlock => input.parse_nested_block(|input| { + CalcNode::parse_argument(context, input, allowed_units) + }), + &Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse(context, input, function, allowed_units) + }, + &Token::Ident(ref ident) => { + let number = match_ignore_ascii_case! { &**ident, + "e" => std::f32::consts::E, + "pi" => std::f32::consts::PI, + "infinity" => f32::INFINITY, + "-infinity" => f32::NEG_INFINITY, + "nan" => f32::NAN, + _ => return Err(location.new_unexpected_token_error(Token::Ident(ident.clone()))), + }; + Ok(CalcNode::Leaf(Leaf::Number(number))) + }, + t => Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parse a top-level `calc` expression, with all nested sub-expressions. + /// + /// This is in charge of parsing, for example, `2 + 3 * 100%`. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + input.parse_nested_block(|input| { + match function { + MathFunction::Calc => Self::parse_argument(context, input, allowed_units), + MathFunction::Clamp => { + let min = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let center = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let max = Self::parse_argument(context, input, allowed_units)?; + Ok(Self::Clamp { + min: Box::new(min), + center: Box::new(center), + max: Box::new(max), + }) + }, + MathFunction::Round => { + let strategy = input.try_parse(parse_rounding_strategy); + + // <rounding-strategy> = nearest | up | down | to-zero + // https://drafts.csswg.org/css-values-4/#calc-syntax + fn parse_rounding_strategy<'i, 't>( + input: &mut Parser<'i, 't>, + ) -> Result<RoundingStrategy, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "nearest" => RoundingStrategy::Nearest, + "up" => RoundingStrategy::Up, + "down" => RoundingStrategy::Down, + "to-zero" => RoundingStrategy::ToZero, + }) + } + + if strategy.is_ok() { + input.expect_comma()?; + } + + let value = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let step = Self::parse_argument(context, input, allowed_units)?; + + Ok(Self::Round { + strategy: strategy.unwrap_or(RoundingStrategy::Nearest), + value: Box::new(value), + step: Box::new(step), + }) + }, + MathFunction::Mod | MathFunction::Rem => { + let dividend = Self::parse_argument(context, input, allowed_units)?; + input.expect_comma()?; + let divisor = Self::parse_argument(context, input, allowed_units)?; + + let op = match function { + MathFunction::Mod => ModRemOp::Mod, + MathFunction::Rem => ModRemOp::Rem, + _ => unreachable!(), + }; + Ok(Self::ModRem { + dividend: Box::new(dividend), + divisor: Box::new(divisor), + op, + }) + }, + MathFunction::Min | MathFunction::Max => { + // TODO(emilio): The common case for parse_comma_separated + // is just one element, but for min / max is two, really... + // + // Consider adding an API to cssparser to specify the + // initial vector capacity? + let arguments = input.parse_comma_separated(|input| { + Self::parse_argument(context, input, allowed_units) + })?; + + let op = match function { + MathFunction::Min => MinMaxOp::Min, + MathFunction::Max => MinMaxOp::Max, + _ => unreachable!(), + }; + + Ok(Self::MinMax(arguments.into(), op)) + }, + MathFunction::Sin | MathFunction::Cos | MathFunction::Tan => { + let a = Self::parse_angle_argument(context, input)?; + + let number = match function { + MathFunction::Sin => a.sin(), + MathFunction::Cos => a.cos(), + MathFunction::Tan => a.tan(), + _ => unsafe { + debug_unreachable!("We just checked!"); + }, + }; + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Asin | MathFunction::Acos | MathFunction::Atan => { + let a = Self::parse_number_argument(context, input)?; + + let radians = match function { + MathFunction::Asin => a.asin(), + MathFunction::Acos => a.acos(), + MathFunction::Atan => a.atan(), + _ => unsafe { + debug_unreachable!("We just checked!"); + }, + }; + + Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians)))) + }, + MathFunction::Atan2 => { + let a = Self::parse_argument(context, input, CalcUnits::ALL)?; + input.expect_comma()?; + let b = Self::parse_argument(context, input, CalcUnits::ALL)?; + + let radians = Self::try_resolve(input, || { + if let Ok(a) = a.to_number() { + let b = b.to_number()?; + return Ok(a.atan2(b)); + } + + if let Ok(a) = a.to_percentage() { + let b = b.to_percentage()?; + return Ok(a.atan2(b)); + } + + if let Ok(a) = a.to_time(None) { + let b = b.to_time(None)?; + return Ok(a.seconds().atan2(b.seconds())); + } + + if let Ok(a) = a.to_angle() { + let b = b.to_angle()?; + return Ok(a.radians().atan2(b.radians())); + } + + if let Ok(a) = a.to_resolution() { + let b = b.to_resolution()?; + return Ok(a.dppx().atan2(b.dppx())); + } + + let a = a.into_length_or_percentage(AllowedNumericType::All)?; + let b = b.into_length_or_percentage(AllowedNumericType::All)?; + let (a, b) = CalcLengthPercentage::same_unit_length_as(&a, &b).ok_or(())?; + + Ok(a.atan2(b)) + })?; + + Ok(Self::Leaf(Leaf::Angle(Angle::from_radians(radians)))) + }, + MathFunction::Pow => { + let a = Self::parse_number_argument(context, input)?; + input.expect_comma()?; + let b = Self::parse_number_argument(context, input)?; + + let number = a.powf(b); + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Sqrt => { + let a = Self::parse_number_argument(context, input)?; + + let number = a.sqrt(); + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Hypot => { + let arguments = input.parse_comma_separated(|input| { + Self::parse_argument(context, input, allowed_units) + })?; + + Ok(Self::Hypot(arguments.into())) + }, + MathFunction::Log => { + let a = Self::parse_number_argument(context, input)?; + let b = input + .try_parse(|input| { + input.expect_comma()?; + Self::parse_number_argument(context, input) + }) + .ok(); + + let number = match b { + Some(b) => a.log(b), + None => a.ln(), + }; + + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Exp => { + let a = Self::parse_number_argument(context, input)?; + let number = a.exp(); + Ok(Self::Leaf(Leaf::Number(number))) + }, + MathFunction::Abs => { + let node = Self::parse_argument(context, input, allowed_units)?; + Ok(Self::Abs(Box::new(node))) + }, + MathFunction::Sign => { + // The sign of a percentage is dependent on the percentage basis, so if + // percentages aren't allowed (so there's no basis) we shouldn't allow them in + // sign(). The rest of the units are safe tho. + let sign_units = allowed_units | (CalcUnits::ALL - CalcUnits::PERCENTAGE); + let node = Self::parse_argument(context, input, sign_units)?; + Ok(Self::Sign(Box::new(node))) + }, + } + }) + } + + fn parse_angle_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<CSSFloat, ParseError<'i>> { + let argument = Self::parse_argument(context, input, CalcUnits::ANGLE)?; + argument + .to_number() + .or_else(|()| Ok(argument.to_angle()?.radians())) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + fn parse_number_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse_argument(context, input, CalcUnits::empty())? + .to_number() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + fn parse_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + let mut sum = SmallVec::<[CalcNode; 1]>::new(); + sum.push(Self::parse_product(context, input, allowed_units)?); + + loop { + let start = input.state(); + match input.next_including_whitespace() { + Ok(&Token::WhiteSpace(_)) => { + if input.is_exhausted() { + break; // allow trailing whitespace + } + match *input.next()? { + Token::Delim('+') => { + sum.push(Self::parse_product(context, input, allowed_units)?); + }, + Token::Delim('-') => { + let mut rhs = Self::parse_product(context, input, allowed_units)?; + rhs.negate(); + sum.push(rhs); + }, + _ => { + input.reset(&start); + break; + }, + } + }, + _ => { + input.reset(&start); + break; + }, + } + } + + Ok(if sum.len() == 1 { + sum.drain(..).next().unwrap() + } else { + Self::Sum(sum.into_boxed_slice().into()) + }) + } + + /// Parse a top-level `calc` expression, and all the products that may + /// follow, and stop as soon as a non-product expression is found. + /// + /// This should parse correctly: + /// + /// * `2` + /// * `2 * 2` + /// * `2 * 2 + 2` (but will leave the `+ 2` unparsed). + /// + fn parse_product<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allowed_units: CalcUnits, + ) -> Result<Self, ParseError<'i>> { + let mut product = SmallVec::<[CalcNode; 1]>::new(); + product.push(Self::parse_one(context, input, allowed_units)?); + + loop { + let start = input.state(); + match input.next() { + Ok(&Token::Delim('*')) => { + let mut rhs = Self::parse_one(context, input, allowed_units)?; + + // We can unwrap here, becuase we start the function by adding a node to + // the list. + if !product.last_mut().unwrap().try_product_in_place(&mut rhs) { + product.push(rhs); + } + }, + Ok(&Token::Delim('/')) => { + let rhs = Self::parse_one(context, input, allowed_units)?; + + enum InPlaceDivisionResult { + /// The right was merged into the left. + Merged, + /// The right is not a number or could not be resolved, so the left is + /// unchanged. + Unchanged, + /// The right was resolved, but was not a number, so the calculation is + /// invalid. + Invalid, + } + + fn try_division_in_place( + left: &mut CalcNode, + right: &CalcNode, + ) -> InPlaceDivisionResult { + if let Ok(resolved) = right.resolve() { + if let Some(number) = resolved.as_number() { + if number != 1.0 && left.is_product_distributive() { + left.map(|l| l / number); + return InPlaceDivisionResult::Merged; + } + } else { + return InPlaceDivisionResult::Invalid; + } + } + InPlaceDivisionResult::Unchanged + } + + // The right hand side of a division *must* be a number, so if we can + // already resolve it, then merge it with the last node on the product list. + // We can unwrap here, becuase we start the function by adding a node to + // the list. + match try_division_in_place(product.last_mut().unwrap(), &rhs) { + InPlaceDivisionResult::Merged => {}, + InPlaceDivisionResult::Unchanged => { + product.push(Self::Invert(Box::new(rhs))) + }, + InPlaceDivisionResult::Invalid => { + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ) + }, + } + }, + _ => { + input.reset(&start); + break; + }, + } + } + + Ok(if product.len() == 1 { + product.drain(..).next().unwrap() + } else { + Self::Product(product.into_boxed_slice().into()) + }) + } + + fn try_resolve<'i, 't, F>( + input: &Parser<'i, 't>, + closure: F, + ) -> Result<CSSFloat, ParseError<'i>> + where + F: FnOnce() -> Result<CSSFloat, ()>, + { + closure().map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Tries to simplify this expression into a `<length>` or `<percentage>` + /// value. + pub fn into_length_or_percentage( + mut self, + clamping_mode: AllowedNumericType, + ) -> Result<CalcLengthPercentage, ()> { + self.simplify_and_sort(); + + // Although we allow numbers inside CalcLengthPercentage, calculations that resolve to a + // number result is still not allowed. + let unit = self.unit()?; + if !CalcUnits::LENGTH_PERCENTAGE.intersects(unit) { + Err(()) + } else { + Ok(CalcLengthPercentage { + clamping_mode, + node: self, + }) + } + } + + /// Tries to simplify this expression into a `<time>` value. + fn to_time(&self, clamping_mode: Option<AllowedNumericType>) -> Result<Time, ()> { + let seconds = if let Leaf::Time(time) = self.resolve()? { + time.seconds() + } else { + return Err(()); + }; + + Ok(Time::from_seconds_with_calc_clamping_mode( + seconds, + clamping_mode, + )) + } + + /// Tries to simplify the expression into a `<resolution>` value. + fn to_resolution(&self) -> Result<Resolution, ()> { + let dppx = if let Leaf::Resolution(resolution) = self.resolve()? { + resolution.dppx() + } else { + return Err(()); + }; + + Ok(Resolution::from_dppx_calc(dppx)) + } + + /// Tries to simplify this expression into an `Angle` value. + fn to_angle(&self) -> Result<Angle, ()> { + let degrees = if let Leaf::Angle(angle) = self.resolve()? { + angle.degrees() + } else { + return Err(()); + }; + + let result = Angle::from_calc(degrees); + Ok(result) + } + + /// Tries to simplify this expression into a `<number>` value. + fn to_number(&self) -> Result<CSSFloat, ()> { + let number = if let Leaf::Number(number) = self.resolve()? { + number + } else { + return Err(()); + }; + + let result = number; + + Ok(result) + } + + /// Tries to simplify this expression into a `<percentage>` value. + fn to_percentage(&self) -> Result<CSSFloat, ()> { + if let Leaf::Percentage(percentage) = self.resolve()? { + Ok(percentage) + } else { + Err(()) + } + } + + /// Given a function name, and the location from where the token came from, + /// return a mathematical function corresponding to that name or an error. + #[inline] + pub fn math_function<'i>( + _: &ParserContext, + name: &CowRcStr<'i>, + location: cssparser::SourceLocation, + ) -> Result<MathFunction, ParseError<'i>> { + let function = match MathFunction::from_ident(&*name) { + Ok(f) => f, + Err(()) => { + return Err(location.new_unexpected_token_error(Token::Function(name.clone()))) + }, + }; + + Ok(function) + } + + /// Convenience parsing function for integers. + pub fn parse_integer<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSInteger, ParseError<'i>> { + Self::parse_number(context, input, function).map(|n| (n + 0.5).floor() as CSSInteger) + } + + /// Convenience parsing function for `<length> | <percentage>`. + pub fn parse_length_or_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<CalcLengthPercentage, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::LENGTH_PERCENTAGE)? + .into_length_or_percentage(clamping_mode) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for percentages. + pub fn parse_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::PERCENTAGE)? + .to_percentage() + .map(crate::values::normalize) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<length>`. + pub fn parse_length<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<CalcLengthPercentage, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::LENGTH)? + .into_length_or_percentage(clamping_mode) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<number>`. + pub fn parse_number<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<CSSFloat, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::empty())? + .to_number() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<angle>`. + pub fn parse_angle<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<Angle, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::ANGLE)? + .to_angle() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<time>`. + pub fn parse_time<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + function: MathFunction, + ) -> Result<Time, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::TIME)? + .to_time(Some(clamping_mode)) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<resolution>`. + pub fn parse_resolution<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<Resolution, ParseError<'i>> { + Self::parse(context, input, function, CalcUnits::RESOLUTION)? + .to_resolution() + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + + /// Convenience parsing function for `<number>` or `<percentage>`. + pub fn parse_number_or_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<NumberOrPercentage, ParseError<'i>> { + let node = Self::parse(context, input, function, CalcUnits::PERCENTAGE)?; + + if let Ok(value) = node.to_number() { + return Ok(NumberOrPercentage::Number { value }); + } + + match node.to_percentage() { + Ok(unit_value) => Ok(NumberOrPercentage::Percentage { unit_value }), + Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + /// Convenience parsing function for `<number>` or `<angle>`. + pub fn parse_angle_or_number<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + function: MathFunction, + ) -> Result<AngleOrNumber, ParseError<'i>> { + let node = Self::parse(context, input, function, CalcUnits::ANGLE)?; + + if let Ok(angle) = node.to_angle() { + let degrees = angle.degrees(); + return Ok(AngleOrNumber::Angle { degrees }); + } + + match node.to_number() { + Ok(value) => Ok(AngleOrNumber::Number { value }), + Err(()) => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} diff --git a/servo/components/style/values/specified/color.rs b/servo/components/style/values/specified/color.rs new file mode 100644 index 0000000000..3a19a2f4a3 --- /dev/null +++ b/servo/components/style/values/specified/color.rs @@ -0,0 +1,1175 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified color values. + +use super::AllowQuirks; +use crate::color::parsing::{ + self, AngleOrNumber, Color as CSSParserColor, FromParsedColor, NumberOrPercentage, +}; +use crate::color::{mix::ColorInterpolationMethod, AbsoluteColor, ColorSpace}; +use crate::media_queries::Device; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Color as ComputedColor, Context, ToComputedValue}; +use crate::values::generics::color::{ + ColorMixFlags, GenericCaretColor, GenericColorMix, GenericColorOrAuto, +}; +use crate::values::specified::calc::CalcNode; +use crate::values::specified::Percentage; +use crate::values::{normalize, CustomIdent}; +use cssparser::{color::PredefinedColorSpace, BasicParseErrorKind, ParseErrorKind, Parser, Token}; +use itoa; +use std::fmt::{self, Write}; +use std::io::Write as IoWrite; +use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError, StyleParseErrorKind}; +use style_traits::{SpecifiedValueInfo, ToCss, ValueParseErrorKind}; + +/// A specified color-mix(). +pub type ColorMix = GenericColorMix<Color, Percentage>; + +impl ColorMix { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + preserve_authored: PreserveAuthored, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("color-mix")?; + + input.parse_nested_block(|input| { + let interpolation = ColorInterpolationMethod::parse(context, input)?; + input.expect_comma()?; + + let try_parse_percentage = |input: &mut Parser| -> Option<Percentage> { + input + .try_parse(|input| Percentage::parse_zero_to_a_hundred(context, input)) + .ok() + }; + + let mut left_percentage = try_parse_percentage(input); + + let left = Color::parse_internal(context, input, preserve_authored)?; + if left_percentage.is_none() { + left_percentage = try_parse_percentage(input); + } + + input.expect_comma()?; + + let mut right_percentage = try_parse_percentage(input); + + let right = Color::parse_internal(context, input, preserve_authored)?; + + if right_percentage.is_none() { + right_percentage = try_parse_percentage(input); + } + + let right_percentage = right_percentage + .unwrap_or_else(|| Percentage::new(1.0 - left_percentage.map_or(0.5, |p| p.get()))); + + let left_percentage = + left_percentage.unwrap_or_else(|| Percentage::new(1.0 - right_percentage.get())); + + if left_percentage.get() + right_percentage.get() <= 0.0 { + // If the percentages sum to zero, the function is invalid. + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // Pass RESULT_IN_MODERN_SYNTAX here, because the result of the color-mix() function + // should always be in the modern color syntax to allow for out of gamut results and + // to preserve floating point precision. + Ok(ColorMix { + interpolation, + left, + left_percentage, + right, + right_percentage, + flags: ColorMixFlags::NORMALIZE_WEIGHTS | ColorMixFlags::RESULT_IN_MODERN_SYNTAX, + }) + }) + } +} + +/// Container holding an absolute color and the text specified by an author. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Absolute { + /// The specified color. + pub color: AbsoluteColor, + /// Authored representation. + pub authored: Option<Box<str>>, +} + +impl ToCss for Absolute { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if let Some(ref authored) = self.authored { + dest.write_str(authored) + } else { + self.color.to_css(dest) + } + } +} + +/// Specified color value +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum Color { + /// The 'currentColor' keyword + CurrentColor, + /// An absolute color. + /// https://w3c.github.io/csswg-drafts/css-color-4/#typedef-absolute-color-function + Absolute(Box<Absolute>), + /// A system color. + #[cfg(feature = "gecko")] + System(SystemColor), + /// A color mix. + ColorMix(Box<ColorMix>), + /// A light-dark() color. + LightDark(Box<LightDark>), + /// Quirksmode-only rule for inheriting color from the body + #[cfg(feature = "gecko")] + InheritFromBodyQuirk, +} + +/// A light-dark(<light-color>, <dark-color>) function. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem, ToCss)] +#[css(function, comma)] +pub struct LightDark { + /// The <color> that is returned when using a light theme. + pub light: Color, + /// The <color> that is returned when using a dark theme. + pub dark: Color, +} + +impl LightDark { + fn compute(&self, cx: &Context) -> ComputedColor { + let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme(); + let dark = cx.device().is_dark_color_scheme(&style_color_scheme); + let used = if dark { &self.dark } else { &self.light }; + used.to_computed_value(cx) + } + + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + preserve_authored: PreserveAuthored, + ) -> Result<Self, ParseError<'i>> { + let enabled = + context.chrome_rules_enabled() || static_prefs::pref!("layout.css.light-dark.enabled"); + if !enabled { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + input.expect_function_matching("light-dark")?; + input.parse_nested_block(|input| { + let light = Color::parse_internal(context, input, preserve_authored)?; + input.expect_comma()?; + let dark = Color::parse_internal(context, input, preserve_authored)?; + Ok(LightDark { light, dark }) + }) + } +} + +impl From<AbsoluteColor> for Color { + #[inline] + fn from(value: AbsoluteColor) -> Self { + Self::from_absolute_color(value) + } +} + +/// System colors. A bunch of these are ad-hoc, others come from Windows: +/// +/// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsyscolor +/// +/// Others are HTML/CSS specific. Spec is: +/// +/// https://drafts.csswg.org/css-color/#css-system-colors +/// https://drafts.csswg.org/css-color/#deprecated-system-colors +#[allow(missing_docs)] +#[cfg(feature = "gecko")] +#[derive(Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, ToCss, ToShmem)] +#[repr(u8)] +pub enum SystemColor { + Activeborder, + /// Background in the (active) titlebar. + Activecaption, + Appworkspace, + Background, + Buttonface, + Buttonhighlight, + Buttonshadow, + Buttontext, + Buttonborder, + /// Text color in the (active) titlebar. + Captiontext, + #[parse(aliases = "-moz-field")] + Field, + /// Used for disabled field backgrounds. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozDisabledfield, + #[parse(aliases = "-moz-fieldtext")] + Fieldtext, + + Mark, + Marktext, + + /// Combobox widgets + MozComboboxtext, + MozCombobox, + + Graytext, + Highlight, + Highlighttext, + Inactiveborder, + /// Background in the (inactive) titlebar. + Inactivecaption, + /// Text color in the (inactive) titlebar. + Inactivecaptiontext, + Infobackground, + Infotext, + Menu, + Menutext, + Scrollbar, + Threeddarkshadow, + Threedface, + Threedhighlight, + Threedlightshadow, + Threedshadow, + Window, + Windowframe, + Windowtext, + #[parse(aliases = "-moz-default-color")] + Canvastext, + #[parse(aliases = "-moz-default-background-color")] + Canvas, + MozDialog, + MozDialogtext, + /// Used for selected but not focused cell backgrounds. + #[parse(aliases = "-moz-html-cellhighlight")] + MozCellhighlight, + /// Used for selected but not focused cell text. + #[parse(aliases = "-moz-html-cellhighlighttext")] + MozCellhighlighttext, + /// Used for selected and focused html cell backgrounds. + Selecteditem, + /// Used for selected and focused html cell text. + Selecteditemtext, + /// Used to button text background when hovered. + MozButtonhoverface, + /// Used to button text color when hovered. + MozButtonhovertext, + /// Used for menu item backgrounds when hovered. + MozMenuhover, + /// Used for menu item backgrounds when hovered and disabled. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMenuhoverdisabled, + /// Used for menu item text when hovered. + MozMenuhovertext, + /// Used for menubar item text when hovered. + MozMenubarhovertext, + + /// On platforms where these colors are the same as -moz-field, use + /// -moz-fieldtext as foreground color + MozEventreerow, + MozOddtreerow, + + /// Used for button text when pressed. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButtonactivetext, + + /// Used for button background when pressed. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButtonactiveface, + + /// Used for button background when disabled. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButtondisabledface, + + /// Colors used for the header bar (sorta like the tab bar / menubar). + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbar, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbartext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbarinactive, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozHeaderbarinactivetext, + + /// Foreground color of default buttons. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDefaultbuttontext, + /// Ring color around text fields and lists. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacFocusring, + /// Text color of disabled text on toolbars. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozMacDisabledtoolbartext, + /// The background of a sidebar. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozSidebar, + /// The foreground color of a sidebar. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozSidebartext, + /// The border color of a sidebar. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozSidebarborder, + + /// Theme accent color. + /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolor + Accentcolor, + + /// Foreground for the accent color. + /// https://drafts.csswg.org/css-color-4/#valdef-system-color-accentcolortext + Accentcolortext, + + /// The background-color for :autofill-ed inputs. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozAutofillBackground, + + /// Hyperlink color extracted from the system, not affected by the browser.anchor_color user + /// pref. + /// + /// There is no OS-specified safe background color for this text, but it is used regularly + /// within Windows and the Gnome DE on Dialog and Window colors. + #[css(skip)] + MozNativehyperlinktext, + + /// As above, but visited link color. + #[css(skip)] + MozNativevisitedhyperlinktext, + + #[parse(aliases = "-moz-hyperlinktext")] + Linktext, + #[parse(aliases = "-moz-activehyperlinktext")] + Activetext, + #[parse(aliases = "-moz-visitedhyperlinktext")] + Visitedtext, + + /// Color of tree column headers + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheader, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheadertext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderhover, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderhovertext, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderactive, + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozColheaderactivetext, + + #[parse(condition = "ParserContext::chrome_rules_enabled")] + TextSelectDisabledBackground, + #[css(skip)] + TextSelectAttentionBackground, + #[css(skip)] + TextSelectAttentionForeground, + #[css(skip)] + TextHighlightBackground, + #[css(skip)] + TextHighlightForeground, + #[css(skip)] + IMERawInputBackground, + #[css(skip)] + IMERawInputForeground, + #[css(skip)] + IMERawInputUnderline, + #[css(skip)] + IMESelectedRawTextBackground, + #[css(skip)] + IMESelectedRawTextForeground, + #[css(skip)] + IMESelectedRawTextUnderline, + #[css(skip)] + IMEConvertedTextBackground, + #[css(skip)] + IMEConvertedTextForeground, + #[css(skip)] + IMEConvertedTextUnderline, + #[css(skip)] + IMESelectedConvertedTextBackground, + #[css(skip)] + IMESelectedConvertedTextForeground, + #[css(skip)] + IMESelectedConvertedTextUnderline, + #[css(skip)] + SpellCheckerUnderline, + #[css(skip)] + ThemedScrollbar, + #[css(skip)] + ThemedScrollbarInactive, + #[css(skip)] + ThemedScrollbarThumb, + #[css(skip)] + ThemedScrollbarThumbHover, + #[css(skip)] + ThemedScrollbarThumbActive, + #[css(skip)] + ThemedScrollbarThumbInactive, + + #[css(skip)] + End, // Just for array-indexing purposes. +} + +#[cfg(feature = "gecko")] +impl SystemColor { + #[inline] + fn compute(&self, cx: &Context) -> ComputedColor { + use crate::gecko::values::convert_nscolor_to_absolute_color; + use crate::gecko_bindings::bindings; + + // TODO: We should avoid cloning here most likely, though it's cheap-ish. + let style_color_scheme = cx.style().get_inherited_ui().clone_color_scheme(); + let color = cx.device().system_nscolor(*self, &style_color_scheme); + if color == bindings::NS_SAME_AS_FOREGROUND_COLOR { + return ComputedColor::currentcolor(); + } + ComputedColor::Absolute(convert_nscolor_to_absolute_color(color)) + } +} + +impl FromParsedColor for Color { + fn from_current_color() -> Self { + Color::CurrentColor + } + + fn from_rgba(r: u8, g: u8, b: u8, a: f32) -> Self { + AbsoluteColor::srgb_legacy(r, g, b, a).into() + } + + fn from_hsl( + hue: Option<f32>, + saturation: Option<f32>, + lightness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Hsl, hue, saturation, lightness, alpha).into() + } + + fn from_hwb( + hue: Option<f32>, + whiteness: Option<f32>, + blackness: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Hwb, hue, whiteness, blackness, alpha).into() + } + + fn from_lab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Lab, lightness, a, b, alpha).into() + } + + fn from_lch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Lch, lightness, chroma, hue, alpha).into() + } + + fn from_oklab( + lightness: Option<f32>, + a: Option<f32>, + b: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Oklab, lightness, a, b, alpha).into() + } + + fn from_oklch( + lightness: Option<f32>, + chroma: Option<f32>, + hue: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(ColorSpace::Oklch, lightness, chroma, hue, alpha).into() + } + + fn from_color_function( + color_space: PredefinedColorSpace, + c1: Option<f32>, + c2: Option<f32>, + c3: Option<f32>, + alpha: Option<f32>, + ) -> Self { + AbsoluteColor::new(color_space.into(), c1, c2, c3, alpha).into() + } +} + +struct ColorParser<'a, 'b: 'a>(&'a ParserContext<'b>); +impl<'a, 'b: 'a, 'i: 'a> parsing::ColorParser<'i> for ColorParser<'a, 'b> { + type Output = Color; + type Error = StyleParseErrorKind<'i>; + + fn parse_angle_or_number<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<AngleOrNumber, ParseError<'i>> { + use crate::values::specified::Angle; + + let location = input.current_source_location(); + let token = input.next()?.clone(); + match token { + Token::Dimension { + value, ref unit, .. + } => { + let angle = Angle::parse_dimension(value, unit, /* from_calc = */ false); + + let degrees = match angle { + Ok(angle) => angle.degrees(), + Err(()) => return Err(location.new_unexpected_token_error(token.clone())), + }; + + Ok(AngleOrNumber::Angle { degrees }) + }, + Token::Number { value, .. } => Ok(AngleOrNumber::Number { value }), + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + CalcNode::parse_angle_or_number(self.0, input, function) + }, + t => return Err(location.new_unexpected_token_error(t)), + } + } + + fn parse_percentage<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { + Ok(Percentage::parse(self.0, input)?.get()) + } + + fn parse_number<'t>(&self, input: &mut Parser<'i, 't>) -> Result<f32, ParseError<'i>> { + use crate::values::specified::Number; + + Ok(Number::parse(self.0, input)?.get()) + } + + fn parse_number_or_percentage<'t>( + &self, + input: &mut Parser<'i, 't>, + ) -> Result<NumberOrPercentage, ParseError<'i>> { + let location = input.current_source_location(); + + match *input.next()? { + Token::Number { value, .. } => Ok(NumberOrPercentage::Number { value }), + Token::Percentage { unit_value, .. } => { + Ok(NumberOrPercentage::Percentage { unit_value }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(self.0, name, location)?; + CalcNode::parse_number_or_percentage(self.0, input, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +/// Whether to preserve authored colors during parsing. That's useful only if we +/// plan to serialize the color back. +#[derive(Copy, Clone)] +enum PreserveAuthored { + No, + Yes, +} + +impl Parse for Color { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, PreserveAuthored::Yes) + } +} + +impl Color { + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + preserve_authored: PreserveAuthored, + ) -> Result<Self, ParseError<'i>> { + let authored = match preserve_authored { + PreserveAuthored::No => None, + PreserveAuthored::Yes => { + // Currently we only store authored value for color keywords, + // because all browsers serialize those values as keywords for + // specified value. + let start = input.state(); + let authored = input.expect_ident_cloned().ok(); + input.reset(&start); + authored + }, + }; + + let color_parser = ColorParser(&*context); + match input.try_parse(|i| parsing::parse_color_with(&color_parser, i)) { + Ok(mut color) => { + if let Color::Absolute(ref mut absolute) = color { + // Because we can't set the `authored` value at construction time, we have to set it + // here. + absolute.authored = authored.map(|s| s.to_ascii_lowercase().into_boxed_str()); + } + Ok(color) + }, + Err(e) => { + #[cfg(feature = "gecko")] + { + if let Ok(system) = input.try_parse(|i| SystemColor::parse(context, i)) { + return Ok(Color::System(system)); + } + } + + if let Ok(mix) = input.try_parse(|i| ColorMix::parse(context, i, preserve_authored)) + { + return Ok(Color::ColorMix(Box::new(mix))); + } + + if let Ok(ld) = input.try_parse(|i| LightDark::parse(context, i, preserve_authored)) + { + return Ok(Color::LightDark(Box::new(ld))); + } + + match e.kind { + ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t)) => { + Err(e.location.new_custom_error(StyleParseErrorKind::ValueError( + ValueParseErrorKind::InvalidColor(t), + ))) + }, + _ => Err(e), + } + }, + } + } + + /// Returns whether a given color is valid for authors. + pub fn is_valid(context: &ParserContext, input: &mut Parser) -> bool { + input + .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No)) + .is_ok() + } + + /// Tries to parse a color and compute it with a given device. + pub fn parse_and_compute( + context: &ParserContext, + input: &mut Parser, + device: Option<&Device>, + ) -> Option<ComputedColor> { + use crate::error_reporting::ContextualParseError; + let start = input.position(); + let result = input + .parse_entirely(|input| Self::parse_internal(context, input, PreserveAuthored::No)); + + let specified = match result { + Ok(s) => s, + Err(e) => { + if !context.error_reporting_enabled() { + return None; + } + // Ignore other kinds of errors that might be reported, such as + // ParseErrorKind::Basic(BasicParseErrorKind::UnexpectedToken), + // since Gecko didn't use to report those to the error console. + // + // TODO(emilio): Revise whether we want to keep this at all, we + // use this only for canvas, this warnings are disabled by + // default and not available on OffscreenCanvas anyways... + if let ParseErrorKind::Custom(StyleParseErrorKind::ValueError(..)) = e.kind { + let location = e.location.clone(); + let error = ContextualParseError::UnsupportedValue(input.slice_from(start), e); + context.log_css_error(location, error); + } + return None; + }, + }; + + match device { + Some(device) => { + Context::for_media_query_evaluation(device, device.quirks_mode(), |context| { + specified.to_computed_color(Some(&context)) + }) + }, + None => specified.to_computed_color(None), + } + } +} + +impl ToCss for Color { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + Color::CurrentColor => cssparser::ToCss::to_css(&CSSParserColor::CurrentColor, dest), + Color::Absolute(ref absolute) => absolute.to_css(dest), + Color::ColorMix(ref mix) => mix.to_css(dest), + Color::LightDark(ref ld) => ld.to_css(dest), + #[cfg(feature = "gecko")] + Color::System(system) => system.to_css(dest), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => Ok(()), + } + } +} + +impl Color { + /// Returns whether this color is allowed in forced-colors mode. + pub fn honored_in_forced_colors_mode(&self, allow_transparent: bool) -> bool { + match *self { + Self::InheritFromBodyQuirk => false, + Self::CurrentColor | Color::System(..) => true, + Self::Absolute(ref absolute) => allow_transparent && absolute.color.is_transparent(), + Self::LightDark(ref ld) => { + ld.light.honored_in_forced_colors_mode(allow_transparent) && + ld.dark.honored_in_forced_colors_mode(allow_transparent) + }, + Self::ColorMix(ref mix) => { + mix.left.honored_in_forced_colors_mode(allow_transparent) && + mix.right.honored_in_forced_colors_mode(allow_transparent) + }, + } + } + + /// Returns currentcolor value. + #[inline] + pub fn currentcolor() -> Self { + Self::CurrentColor + } + + /// Returns transparent value. + #[inline] + pub fn transparent() -> Self { + // We should probably set authored to "transparent", but maybe it doesn't matter. + Self::from_absolute_color(AbsoluteColor::TRANSPARENT_BLACK) + } + + /// Create a color from an [`AbsoluteColor`]. + pub fn from_absolute_color(color: AbsoluteColor) -> Self { + Color::Absolute(Box::new(Absolute { + color, + authored: None, + })) + } + + /// Parse a color, with quirks. + /// + /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk> + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| Self::parse(context, i)).or_else(|e| { + if !allow_quirks.allowed(context.quirks_mode) { + return Err(e); + } + Color::parse_quirky_color(input).map_err(|_| e) + }) + } + + fn parse_hash<'i>( + bytes: &[u8], + loc: &cssparser::SourceLocation, + ) -> Result<Self, ParseError<'i>> { + match cssparser::color::parse_hash_color(bytes) { + Ok((r, g, b, a)) => Ok(Self::from_rgba(r, g, b, a)), + Err(()) => Err(loc.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } + + /// Parse a <quirky-color> value. + /// + /// <https://quirks.spec.whatwg.org/#the-hashless-hex-color-quirk> + fn parse_quirky_color<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let (value, unit) = match *input.next()? { + Token::Number { + int_value: Some(integer), + .. + } => (integer, None), + Token::Dimension { + int_value: Some(integer), + ref unit, + .. + } => (integer, Some(unit)), + Token::Ident(ref ident) => { + if ident.len() != 3 && ident.len() != 6 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Self::parse_hash(ident.as_bytes(), &location); + }, + ref t => { + return Err(location.new_unexpected_token_error(t.clone())); + }, + }; + if value < 0 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + let length = if value <= 9 { + 1 + } else if value <= 99 { + 2 + } else if value <= 999 { + 3 + } else if value <= 9999 { + 4 + } else if value <= 99999 { + 5 + } else if value <= 999999 { + 6 + } else { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }; + let total = length + unit.as_ref().map_or(0, |d| d.len()); + if total > 6 { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + let mut serialization = [b'0'; 6]; + let space_padding = 6 - total; + let mut written = space_padding; + let mut buf = itoa::Buffer::new(); + let s = buf.format(value); + (&mut serialization[written..]) + .write_all(s.as_bytes()) + .unwrap(); + written += s.len(); + if let Some(unit) = unit { + written += (&mut serialization[written..]) + .write(unit.as_bytes()) + .unwrap(); + } + debug_assert_eq!(written, 6); + Self::parse_hash(&serialization, &location) + } +} + +impl Color { + /// Converts this Color into a ComputedColor. + /// + /// If `context` is `None`, and the specified color requires data from + /// the context to resolve, then `None` is returned. + pub fn to_computed_color(&self, context: Option<&Context>) -> Option<ComputedColor> { + Some(match *self { + Color::CurrentColor => ComputedColor::CurrentColor, + Color::Absolute(ref absolute) => { + let mut color = absolute.color; + + // Computed lightness values can not be NaN. + if matches!( + color.color_space, + ColorSpace::Lab | ColorSpace::Oklab | ColorSpace::Lch | ColorSpace::Oklch + ) { + color.components.0 = normalize(color.components.0); + } + + // Computed RGB and XYZ components can not be NaN. + if !color.is_legacy_syntax() && color.color_space.is_rgb_or_xyz_like() { + color.components = color.components.map(normalize); + } + + color.alpha = normalize(color.alpha); + + ComputedColor::Absolute(color) + }, + Color::LightDark(ref ld) => ld.compute(context?), + Color::ColorMix(ref mix) => { + use crate::values::computed::percentage::Percentage; + + let left = mix.left.to_computed_color(context)?; + let right = mix.right.to_computed_color(context)?; + + ComputedColor::from_color_mix(GenericColorMix { + interpolation: mix.interpolation, + left, + left_percentage: Percentage(mix.left_percentage.get()), + right, + right_percentage: Percentage(mix.right_percentage.get()), + flags: mix.flags, + }) + }, + #[cfg(feature = "gecko")] + Color::System(system) => system.compute(context?), + #[cfg(feature = "gecko")] + Color::InheritFromBodyQuirk => { + ComputedColor::Absolute(context?.device().body_text_color()) + }, + }) + } +} + +impl ToComputedValue for Color { + type ComputedValue = ComputedColor; + + fn to_computed_value(&self, context: &Context) -> ComputedColor { + self.to_computed_color(Some(context)).unwrap() + } + + fn from_computed_value(computed: &ComputedColor) -> Self { + match *computed { + ComputedColor::Absolute(ref color) => Self::from_absolute_color(color.clone()), + ComputedColor::CurrentColor => Color::CurrentColor, + ComputedColor::ColorMix(ref mix) => { + Color::ColorMix(Box::new(ToComputedValue::from_computed_value(&**mix))) + }, + } + } +} + +impl SpecifiedValueInfo for Color { + const SUPPORTED_TYPES: u8 = CssType::COLOR; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + // We are not going to insert all the color names here. Caller and + // devtools should take care of them. XXX Actually, transparent + // should probably be handled that way as well. + // XXX `currentColor` should really be `currentcolor`. But let's + // keep it consistent with the old system for now. + f(&[ + "rgb", + "rgba", + "hsl", + "hsla", + "hwb", + "currentColor", + "transparent", + "color-mix", + "color", + "lab", + "lch", + "oklab", + "oklch", + ]); + } +} + +/// Specified value for the "color" property, which resolves the `currentcolor` +/// keyword to the parent color instead of self's color. +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct ColorPropertyValue(pub Color); + +impl ToComputedValue for ColorPropertyValue { + type ComputedValue = AbsoluteColor; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + let current_color = context.builder.get_parent_inherited_text().clone_color(); + self.0 + .to_computed_value(context) + .resolve_to_absolute(¤t_color) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + ColorPropertyValue(Color::from_absolute_color(*computed).into()) + } +} + +impl Parse for ColorPropertyValue { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Color::parse_quirky(context, input, AllowQuirks::Yes).map(ColorPropertyValue) + } +} + +/// auto | <color> +pub type ColorOrAuto = GenericColorOrAuto<Color>; + +/// caret-color +pub type CaretColor = GenericCaretColor<Color>; + +impl Parse for CaretColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + ColorOrAuto::parse(context, input).map(GenericCaretColor) + } +} + +/// Various flags to represent the color-scheme property in an efficient +/// way. +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[value_info(other_values = "light,dark,only")] +pub struct ColorSchemeFlags(u8); +bitflags! { + impl ColorSchemeFlags: u8 { + /// Whether the author specified `light`. + const LIGHT = 1 << 0; + /// Whether the author specified `dark`. + const DARK = 1 << 1; + /// Whether the author specified `only`. + const ONLY = 1 << 2; + } +} + +/// <https://drafts.csswg.org/css-color-adjust/#color-scheme-prop> +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[value_info(other_values = "normal")] +pub struct ColorScheme { + #[ignore_malloc_size_of = "Arc"] + idents: crate::ArcSlice<CustomIdent>, + bits: ColorSchemeFlags, +} + +impl ColorScheme { + /// Returns the `normal` value. + pub fn normal() -> Self { + Self { + idents: Default::default(), + bits: ColorSchemeFlags::empty(), + } + } + + /// Returns the raw bitfield. + pub fn raw_bits(&self) -> u8 { + self.bits.bits() + } +} + +impl Parse for ColorScheme { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut idents = vec![]; + let mut bits = ColorSchemeFlags::empty(); + + let mut location = input.current_source_location(); + while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { + let mut is_only = false; + match_ignore_ascii_case! { &ident, + "normal" => { + if idents.is_empty() && bits.is_empty() { + return Ok(Self::normal()); + } + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + }, + "light" => bits.insert(ColorSchemeFlags::LIGHT), + "dark" => bits.insert(ColorSchemeFlags::DARK), + "only" => { + if bits.intersects(ColorSchemeFlags::ONLY) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + bits.insert(ColorSchemeFlags::ONLY); + is_only = true; + }, + _ => {}, + }; + + if is_only { + if !idents.is_empty() { + // Only is allowed either at the beginning or at the end, + // but not in the middle. + break; + } + } else { + idents.push(CustomIdent::from_ident(location, &ident, &[])?); + } + location = input.current_source_location(); + } + + if idents.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(Self { + idents: crate::ArcSlice::from_iter(idents.into_iter()), + bits, + }) + } +} + +impl ToCss for ColorScheme { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.idents.is_empty() { + debug_assert!(self.bits.is_empty()); + return dest.write_str("normal"); + } + let mut first = true; + for ident in self.idents.iter() { + if !first { + dest.write_char(' ')?; + } + first = false; + ident.to_css(dest)?; + } + if self.bits.intersects(ColorSchemeFlags::ONLY) { + dest.write_str(" only")?; + } + Ok(()) + } +} + +/// https://drafts.csswg.org/css-color-adjust/#print-color-adjust +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum PrintColorAdjust { + /// Ignore backgrounds and darken text. + Economy, + /// Respect specified colors. + Exact, +} + +/// https://drafts.csswg.org/css-color-adjust-1/#forced-color-adjust-prop +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ForcedColorAdjust { + /// Adjust colors if needed. + Auto, + /// Respect specified colors. + None, +} diff --git a/servo/components/style/values/specified/column.rs b/servo/components/style/values/specified/column.rs new file mode 100644 index 0000000000..2dd7bb0144 --- /dev/null +++ b/servo/components/style/values/specified/column.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for the column properties. + +use crate::values::generics::column::ColumnCount as GenericColumnCount; +use crate::values::specified::PositiveInteger; + +/// A specified type for `column-count` values. +pub type ColumnCount = GenericColumnCount<PositiveInteger>; diff --git a/servo/components/style/values/specified/counters.rs b/servo/components/style/values/specified/counters.rs new file mode 100644 index 0000000000..9d8261ce6c --- /dev/null +++ b/servo/components/style/values/specified/counters.rs @@ -0,0 +1,279 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for counter properties. + +#[cfg(feature = "servo-layout-2013")] +use crate::computed_values::list_style_type::T as ListStyleType; +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::counters as generics; +use crate::values::generics::counters::CounterPair; +#[cfg(feature = "gecko")] +use crate::values::generics::CounterStyle; +use crate::values::specified::image::Image; +#[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] +use crate::values::specified::Attr; +use crate::values::specified::Integer; +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +#[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +#[derive(PartialEq)] +enum CounterType { + Increment, + Set, + Reset, +} + +impl CounterType { + fn default_value(&self) -> i32 { + match *self { + Self::Increment => 1, + Self::Reset | Self::Set => 0, + } + } +} + +/// A specified value for the `counter-increment` property. +pub type CounterIncrement = generics::GenericCounterIncrement<Integer>; + +impl Parse for CounterIncrement { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters( + context, + input, + CounterType::Increment, + )?)) + } +} + +/// A specified value for the `counter-set` property. +pub type CounterSet = generics::GenericCounterSet<Integer>; + +impl Parse for CounterSet { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters(context, input, CounterType::Set)?)) + } +} + +/// A specified value for the `counter-reset` property. +pub type CounterReset = generics::GenericCounterReset<Integer>; + +impl Parse for CounterReset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(Self::new(parse_counters( + context, + input, + CounterType::Reset, + )?)) + } +} + +fn parse_counters<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + counter_type: CounterType, +) -> Result<Vec<CounterPair<Integer>>, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(vec![]); + } + + let mut counters = Vec::new(); + loop { + let location = input.current_source_location(); + let (name, is_reversed) = match input.next() { + Ok(&Token::Ident(ref ident)) => { + (CustomIdent::from_ident(location, ident, &["none"])?, false) + }, + Ok(&Token::Function(ref name)) + if counter_type == CounterType::Reset && name.eq_ignore_ascii_case("reversed") => + { + input + .parse_nested_block(|input| Ok((CustomIdent::parse(input, &["none"])?, true)))? + }, + Ok(t) => { + let t = t.clone(); + return Err(location.new_unexpected_token_error(t)); + }, + Err(_) => break, + }; + + let value = match input.try_parse(|input| Integer::parse(context, input)) { + Ok(start) => { + if start.value == i32::min_value() { + // The spec says that values must be clamped to the valid range, + // and we reserve i32::min_value() as an internal magic value. + // https://drafts.csswg.org/css-lists/#auto-numbering + Integer::new(i32::min_value() + 1) + } else { + start + } + }, + _ => Integer::new(if is_reversed { + i32::min_value() + } else { + counter_type.default_value() + }), + }; + counters.push(CounterPair { + name, + value, + is_reversed, + }); + } + + if !counters.is_empty() { + Ok(counters) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// The specified value for the `content` property. +pub type Content = generics::GenericContent<Image>; + +/// The specified value for a content item in the `content` property. +pub type ContentItem = generics::GenericContentItem<Image>; + +impl Content { + #[cfg(feature = "servo-layout-2013")] + fn parse_counter_style(_: &ParserContext, input: &mut Parser) -> ListStyleType { + input + .try_parse(|input| { + input.expect_comma()?; + ListStyleType::parse(input) + }) + .unwrap_or(ListStyleType::Decimal) + } + + #[cfg(feature = "gecko")] + fn parse_counter_style(context: &ParserContext, input: &mut Parser) -> CounterStyle { + input + .try_parse(|input| { + input.expect_comma()?; + CounterStyle::parse(context, input) + }) + .unwrap_or(CounterStyle::decimal()) + } +} + +impl Parse for Content { + // normal | none | [ <string> | <counter> | open-quote | close-quote | no-open-quote | + // no-close-quote ]+ + // TODO: <uri>, attr(<identifier>) + #[cfg_attr(feature = "servo", allow(unused_mut))] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(generics::Content::Normal); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(generics::Content::None); + } + + let mut content = vec![]; + let mut has_alt_content = false; + loop { + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + { + if let Ok(image) = input.try_parse(|i| Image::parse_forbid_none(context, i)) { + content.push(generics::ContentItem::Image(image)); + continue; + } + } + match input.next() { + Ok(&Token::QuotedString(ref value)) => { + content.push(generics::ContentItem::String( + value.as_ref().to_owned().into(), + )); + }, + Ok(&Token::Function(ref name)) => { + let result = match_ignore_ascii_case! { &name, + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + "counter" => input.parse_nested_block(|input| { + let name = CustomIdent::parse(input, &[])?; + let style = Content::parse_counter_style(context, input); + Ok(generics::ContentItem::Counter(name, style)) + }), + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + "counters" => input.parse_nested_block(|input| { + let name = CustomIdent::parse(input, &[])?; + input.expect_comma()?; + let separator = input.expect_string()?.as_ref().to_owned().into(); + let style = Content::parse_counter_style(context, input); + Ok(generics::ContentItem::Counters(name, separator, style)) + }), + #[cfg(any(feature = "gecko", feature = "servo-layout-2020"))] + "attr" => input.parse_nested_block(|input| { + Ok(generics::ContentItem::Attr(Attr::parse_function(context, input)?)) + }), + _ => { + use style_traits::StyleParseErrorKind; + let name = name.clone(); + return Err(input.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(name), + )) + } + }?; + content.push(result); + }, + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + Ok(&Token::Ident(ref ident)) => { + content.push(match_ignore_ascii_case! { &ident, + "open-quote" => generics::ContentItem::OpenQuote, + "close-quote" => generics::ContentItem::CloseQuote, + "no-open-quote" => generics::ContentItem::NoOpenQuote, + "no-close-quote" => generics::ContentItem::NoCloseQuote, + #[cfg(feature = "gecko")] + "-moz-alt-content" => { + has_alt_content = true; + generics::ContentItem::MozAltContent + }, + "-moz-label-content" if context.chrome_rules_enabled() => { + generics::ContentItem::MozLabelContent + }, + _ =>{ + let ident = ident.clone(); + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident) + )); + } + }); + }, + Err(_) => break, + Ok(t) => { + let t = t.clone(); + return Err(input.new_unexpected_token_error(t)); + }, + } + } + // We don't allow to parse `-moz-alt-content` in multiple positions. + if content.is_empty() || (has_alt_content && content.len() != 1) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(generics::Content::Items(content.into())) + } +} diff --git a/servo/components/style/values/specified/easing.rs b/servo/components/style/values/specified/easing.rs new file mode 100644 index 0000000000..5e4d8ae1ea --- /dev/null +++ b/servo/components/style/values/specified/easing.rs @@ -0,0 +1,192 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS Easing functions. +use crate::parser::{Parse, ParserContext}; +use crate::piecewise_linear::{PiecewiseLinearFunction, PiecewiseLinearFunctionBuilder}; +use crate::values::computed::easing::TimingFunction as ComputedTimingFunction; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::easing::TimingFunction as GenericTimingFunction; +use crate::values::generics::easing::{StepPosition, TimingKeyword}; +use crate::values::specified::{Integer, Number, Percentage}; +use cssparser::{Delimiter, Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// A specified timing function. +pub type TimingFunction = GenericTimingFunction<Integer, Number, PiecewiseLinearFunction>; + +impl Parse for TimingFunction { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(keyword) = input.try_parse(TimingKeyword::parse) { + return Ok(GenericTimingFunction::Keyword(keyword)); + } + if let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { + let position = match_ignore_ascii_case! { &ident, + "step-start" => StepPosition::Start, + "step-end" => StepPosition::End, + _ => { + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )); + }, + }; + return Ok(GenericTimingFunction::Steps(Integer::new(1), position)); + } + let location = input.current_source_location(); + let function = input.expect_function()?.clone(); + input.parse_nested_block(move |i| { + match_ignore_ascii_case! { &function, + "cubic-bezier" => Self::parse_cubic_bezier(context, i), + "steps" => Self::parse_steps(context, i), + "linear" => Self::parse_linear_function(context, i), + _ => Err(location.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function.clone()))), + } + }) + } +} + +impl TimingFunction { + fn parse_cubic_bezier<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let x1 = Number::parse(context, input)?; + input.expect_comma()?; + let y1 = Number::parse(context, input)?; + input.expect_comma()?; + let x2 = Number::parse(context, input)?; + input.expect_comma()?; + let y2 = Number::parse(context, input)?; + + if x1.get() < 0.0 || x1.get() > 1.0 || x2.get() < 0.0 || x2.get() > 1.0 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(GenericTimingFunction::CubicBezier { x1, y1, x2, y2 }) + } + + fn parse_steps<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let steps = Integer::parse_positive(context, input)?; + let position = input + .try_parse(|i| { + i.expect_comma()?; + StepPosition::parse(context, i) + }) + .unwrap_or(StepPosition::End); + + // jump-none accepts a positive integer greater than 1. + // FIXME(emilio): The spec asks us to avoid rejecting it at parse + // time except until computed value time. + // + // It's not totally clear it's worth it though, and no other browser + // does this. + if position == StepPosition::JumpNone && steps.value() <= 1 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(GenericTimingFunction::Steps(steps, position)) + } + + fn parse_linear_function<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut builder = PiecewiseLinearFunctionBuilder::default(); + let mut num_specified_stops = 0; + // Closely follows `parse_comma_separated`, but can generate multiple entries for one comma-separated entry. + loop { + input.parse_until_before(Delimiter::Comma, |i| { + let builder = &mut builder; + let mut input_start = i.try_parse(|i| Percentage::parse(context, i)).ok(); + let mut input_end = i.try_parse(|i| Percentage::parse(context, i)).ok(); + + let output = Number::parse(context, i)?; + if input_start.is_none() { + debug_assert!(input_end.is_none(), "Input end parsed without input start?"); + input_start = i.try_parse(|i| Percentage::parse(context, i)).ok(); + input_end = i.try_parse(|i| Percentage::parse(context, i)).ok(); + } + builder.push(output.into(), input_start.map(|v| v.get()).into()); + num_specified_stops += 1; + if input_end.is_some() { + debug_assert!( + input_start.is_some(), + "Input end valid but not input start?" + ); + builder.push(output.into(), input_end.map(|v| v.get()).into()); + } + + Ok(()) + })?; + + match input.next() { + Err(_) => break, + Ok(&Token::Comma) => continue, + Ok(_) => unreachable!(), + } + } + // By spec, specifying only a single stop makes the function invalid, even if that single entry may generate + // two entries. + if num_specified_stops < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(GenericTimingFunction::LinearFunction(builder.build())) + } +} + +// We need this for converting the specified TimingFunction into computed TimingFunction without +// Context (for some FFIs in glue.rs). In fact, we don't really need Context to get the computed +// value of TimingFunction. +impl TimingFunction { + /// Generate the ComputedTimingFunction without Context. + pub fn to_computed_value_without_context(&self) -> ComputedTimingFunction { + match &self { + GenericTimingFunction::Steps(steps, pos) => { + GenericTimingFunction::Steps(steps.value(), *pos) + }, + GenericTimingFunction::CubicBezier { x1, y1, x2, y2 } => { + GenericTimingFunction::CubicBezier { + x1: x1.get(), + y1: y1.get(), + x2: x2.get(), + y2: y2.get(), + } + }, + GenericTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword), + GenericTimingFunction::LinearFunction(function) => { + GenericTimingFunction::LinearFunction(function.clone()) + }, + } + } +} + +impl ToComputedValue for TimingFunction { + type ComputedValue = ComputedTimingFunction; + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + self.to_computed_value_without_context() + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match &computed { + ComputedTimingFunction::Steps(steps, pos) => Self::Steps(Integer::new(*steps), *pos), + ComputedTimingFunction::CubicBezier { x1, y1, x2, y2 } => Self::CubicBezier { + x1: Number::new(*x1), + y1: Number::new(*y1), + x2: Number::new(*x2), + y2: Number::new(*y2), + }, + ComputedTimingFunction::Keyword(keyword) => GenericTimingFunction::Keyword(*keyword), + ComputedTimingFunction::LinearFunction(function) => { + GenericTimingFunction::LinearFunction(function.clone()) + }, + } + } +} diff --git a/servo/components/style/values/specified/effects.rs b/servo/components/style/values/specified/effects.rs new file mode 100644 index 0000000000..0453582768 --- /dev/null +++ b/servo/components/style/values/specified/effects.rs @@ -0,0 +1,453 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to effects. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::effects::BoxShadow as ComputedBoxShadow; +use crate::values::computed::effects::SimpleShadow as ComputedSimpleShadow; +#[cfg(feature = "gecko")] +use crate::values::computed::url::ComputedUrl; +use crate::values::computed::Angle as ComputedAngle; +use crate::values::computed::CSSPixelLength as ComputedCSSPixelLength; +use crate::values::computed::Filter as ComputedFilter; +use crate::values::computed::NonNegativeLength as ComputedNonNegativeLength; +use crate::values::computed::NonNegativeNumber as ComputedNonNegativeNumber; +use crate::values::computed::ZeroToOneNumber as ComputedZeroToOneNumber; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::effects::BoxShadow as GenericBoxShadow; +use crate::values::generics::effects::Filter as GenericFilter; +use crate::values::generics::effects::SimpleShadow as GenericSimpleShadow; +use crate::values::generics::NonNegative; +use crate::values::specified::color::Color; +use crate::values::specified::length::{Length, NonNegativeLength}; +#[cfg(feature = "gecko")] +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, Number, NumberOrPercentage}; +#[cfg(feature = "servo")] +use crate::values::Impossible; +use crate::Zero; +use cssparser::{self, BasicParseErrorKind, Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind, ValueParseErrorKind}; + +/// A specified value for a single shadow of the `box-shadow` property. +pub type BoxShadow = + GenericBoxShadow<Option<Color>, Length, Option<NonNegativeLength>, Option<Length>>; + +/// A specified value for a single `filter`. +#[cfg(feature = "gecko")] +pub type SpecifiedFilter = GenericFilter< + Angle, + NonNegativeFactor, + ZeroToOneFactor, + NonNegativeLength, + SimpleShadow, + SpecifiedUrl, +>; + +/// A specified value for a single `filter`. +#[cfg(feature = "servo")] +pub type SpecifiedFilter = GenericFilter< + Angle, + NonNegativeFactor, + ZeroToOneFactor, + NonNegativeLength, + Impossible, + Impossible, +>; + +pub use self::SpecifiedFilter as Filter; + +/// A value for the `<factor>` parts in `Filter`. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct NonNegativeFactor(NumberOrPercentage); + +/// A value for the `<factor>` parts in `Filter` which clamps to one. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct ZeroToOneFactor(NumberOrPercentage); + +/// Clamp the value to 1 if the value is over 100%. +#[inline] +fn clamp_to_one(number: NumberOrPercentage) -> NumberOrPercentage { + match number { + NumberOrPercentage::Percentage(percent) => { + NumberOrPercentage::Percentage(percent.clamp_to_hundred()) + }, + NumberOrPercentage::Number(number) => NumberOrPercentage::Number(number.clamp_to_one()), + } +} + +macro_rules! factor_impl_common { + ($ty:ty, $computed_ty:ty) => { + impl $ty { + #[inline] + fn one() -> Self { + Self(NumberOrPercentage::Number(Number::new(1.))) + } + } + + impl ToComputedValue for $ty { + type ComputedValue = $computed_ty; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::NumberOrPercentage; + match self.0.to_computed_value(context) { + NumberOrPercentage::Number(n) => n.into(), + NumberOrPercentage::Percentage(p) => p.0.into(), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self(NumberOrPercentage::Number( + ToComputedValue::from_computed_value(&computed.0), + )) + } + } + }; +} +factor_impl_common!(NonNegativeFactor, ComputedNonNegativeNumber); +factor_impl_common!(ZeroToOneFactor, ComputedZeroToOneNumber); + +impl Parse for NonNegativeFactor { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + NumberOrPercentage::parse_non_negative(context, input).map(Self) + } +} + +impl Parse for ZeroToOneFactor { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + NumberOrPercentage::parse_non_negative(context, input) + .map(clamp_to_one) + .map(Self) + } +} + +/// A specified value for the `drop-shadow()` filter. +pub type SimpleShadow = GenericSimpleShadow<Option<Color>, Length, Option<NonNegativeLength>>; + +impl Parse for BoxShadow { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut lengths = None; + let mut color = None; + let mut inset = false; + + loop { + if !inset { + if input + .try_parse(|input| input.expect_ident_matching("inset")) + .is_ok() + { + inset = true; + continue; + } + } + if lengths.is_none() { + let value = input.try_parse::<_, _, ParseError>(|i| { + let horizontal = Length::parse(context, i)?; + let vertical = Length::parse(context, i)?; + let (blur, spread) = + match i.try_parse(|i| Length::parse_non_negative(context, i)) { + Ok(blur) => { + let spread = i.try_parse(|i| Length::parse(context, i)).ok(); + (Some(blur.into()), spread) + }, + Err(_) => (None, None), + }; + Ok((horizontal, vertical, blur, spread)) + }); + if let Ok(value) = value { + lengths = Some(value); + continue; + } + } + if color.is_none() { + if let Ok(value) = input.try_parse(|i| Color::parse(context, i)) { + color = Some(value); + continue; + } + } + break; + } + + let lengths = + lengths.ok_or(input.new_custom_error(StyleParseErrorKind::UnspecifiedError))?; + Ok(BoxShadow { + base: SimpleShadow { + color: color, + horizontal: lengths.0, + vertical: lengths.1, + blur: lengths.2, + }, + spread: lengths.3, + inset: inset, + }) + } +} + +impl ToComputedValue for BoxShadow { + type ComputedValue = ComputedBoxShadow; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedBoxShadow { + base: self.base.to_computed_value(context), + spread: self + .spread + .as_ref() + .unwrap_or(&Length::zero()) + .to_computed_value(context), + inset: self.inset, + } + } + + #[inline] + fn from_computed_value(computed: &ComputedBoxShadow) -> Self { + BoxShadow { + base: ToComputedValue::from_computed_value(&computed.base), + spread: Some(ToComputedValue::from_computed_value(&computed.spread)), + inset: computed.inset, + } + } +} + +// We need this for converting the specified Filter into computed Filter without Context (for +// some FFIs in glue.rs). This can fail because in some circumstances, we still need Context to +// determine the computed value. +impl Filter { + /// Generate the ComputedFilter without Context. + pub fn to_computed_value_without_context(&self) -> Result<ComputedFilter, ()> { + match *self { + Filter::Blur(ref length) => Ok(ComputedFilter::Blur(ComputedNonNegativeLength::new( + length.0.to_computed_pixel_length_without_context()?, + ))), + Filter::Brightness(ref factor) => Ok(ComputedFilter::Brightness( + ComputedNonNegativeNumber::from(factor.0.to_number().get()), + )), + Filter::Contrast(ref factor) => Ok(ComputedFilter::Contrast( + ComputedNonNegativeNumber::from(factor.0.to_number().get()), + )), + Filter::Grayscale(ref factor) => Ok(ComputedFilter::Grayscale( + ComputedZeroToOneNumber::from(factor.0.to_number().get()), + )), + Filter::HueRotate(ref angle) => Ok(ComputedFilter::HueRotate( + ComputedAngle::from_degrees(angle.degrees()), + )), + Filter::Invert(ref factor) => Ok(ComputedFilter::Invert( + ComputedZeroToOneNumber::from(factor.0.to_number().get()), + )), + Filter::Opacity(ref factor) => Ok(ComputedFilter::Opacity( + ComputedZeroToOneNumber::from(factor.0.to_number().get()), + )), + Filter::Saturate(ref factor) => Ok(ComputedFilter::Saturate( + ComputedNonNegativeNumber::from(factor.0.to_number().get()), + )), + Filter::Sepia(ref factor) => Ok(ComputedFilter::Sepia(ComputedZeroToOneNumber::from( + factor.0.to_number().get(), + ))), + Filter::DropShadow(ref shadow) => { + if cfg!(feature = "gecko") { + let color = match shadow + .color + .as_ref() + .unwrap_or(&Color::currentcolor()) + .to_computed_color(None) + { + Some(c) => c, + None => return Err(()), + }; + + let horizontal = ComputedCSSPixelLength::new( + shadow + .horizontal + .to_computed_pixel_length_without_context()?, + ); + let vertical = ComputedCSSPixelLength::new( + shadow.vertical.to_computed_pixel_length_without_context()?, + ); + let blur = ComputedNonNegativeLength::new( + shadow + .blur + .as_ref() + .unwrap_or(&NonNegativeLength::zero()) + .0 + .to_computed_pixel_length_without_context()?, + ); + + Ok(ComputedFilter::DropShadow(ComputedSimpleShadow { + color, + horizontal, + vertical, + blur, + })) + } else { + Err(()) + } + }, + Filter::Url(ref url) => { + if cfg!(feature = "gecko") { + Ok(ComputedFilter::Url(ComputedUrl(url.clone()))) + } else { + Err(()) + } + }, + } + } +} + +impl Parse for Filter { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + #[cfg(feature = "gecko")] + { + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(GenericFilter::Url(url)); + } + } + let location = input.current_source_location(); + let function = match input.expect_function() { + Ok(f) => f.clone(), + Err(cssparser::BasicParseError { + kind: BasicParseErrorKind::UnexpectedToken(t), + location, + }) => return Err(location.new_custom_error(ValueParseErrorKind::InvalidFilter(t))), + Err(e) => return Err(e.into()), + }; + input.parse_nested_block(|i| { + match_ignore_ascii_case! { &*function, + "blur" => Ok(GenericFilter::Blur( + i.try_parse(|i| NonNegativeLength::parse(context, i)) + .unwrap_or(Zero::zero()), + )), + "brightness" => Ok(GenericFilter::Brightness( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "contrast" => Ok(GenericFilter::Contrast( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "grayscale" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-grayscale + Ok(GenericFilter::Grayscale( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "hue-rotate" => { + // We allow unitless zero here, see: + // https://github.com/w3c/fxtf-drafts/issues/228 + Ok(GenericFilter::HueRotate( + i.try_parse(|i| Angle::parse_with_unitless(context, i)) + .unwrap_or(Zero::zero()), + )) + }, + "invert" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-invert + Ok(GenericFilter::Invert( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "opacity" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-opacity + Ok(GenericFilter::Opacity( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "saturate" => Ok(GenericFilter::Saturate( + i.try_parse(|i| NonNegativeFactor::parse(context, i)) + .unwrap_or(NonNegativeFactor::one()), + )), + "sepia" => { + // Values of amount over 100% are allowed but UAs must clamp the values to 1. + // https://drafts.fxtf.org/filter-effects/#funcdef-filter-sepia + Ok(GenericFilter::Sepia( + i.try_parse(|i| ZeroToOneFactor::parse(context, i)) + .unwrap_or(ZeroToOneFactor::one()), + )) + }, + "drop-shadow" => Ok(GenericFilter::DropShadow(Parse::parse(context, i)?)), + _ => Err(location.new_custom_error( + ValueParseErrorKind::InvalidFilter(Token::Function(function.clone())) + )), + } + }) + } +} + +impl Parse for SimpleShadow { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let color = input.try_parse(|i| Color::parse(context, i)).ok(); + let horizontal = Length::parse(context, input)?; + let vertical = Length::parse(context, input)?; + let blur = input + .try_parse(|i| Length::parse_non_negative(context, i)) + .ok(); + let blur = blur.map(NonNegative::<Length>); + let color = color.or_else(|| input.try_parse(|i| Color::parse(context, i)).ok()); + + Ok(SimpleShadow { + color, + horizontal, + vertical, + blur, + }) + } +} + +impl ToComputedValue for SimpleShadow { + type ComputedValue = ComputedSimpleShadow; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + ComputedSimpleShadow { + color: self + .color + .as_ref() + .unwrap_or(&Color::currentcolor()) + .to_computed_value(context), + horizontal: self.horizontal.to_computed_value(context), + vertical: self.vertical.to_computed_value(context), + blur: self + .blur + .as_ref() + .unwrap_or(&NonNegativeLength::zero()) + .to_computed_value(context), + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + SimpleShadow { + color: Some(ToComputedValue::from_computed_value(&computed.color)), + horizontal: ToComputedValue::from_computed_value(&computed.horizontal), + vertical: ToComputedValue::from_computed_value(&computed.vertical), + blur: Some(ToComputedValue::from_computed_value(&computed.blur)), + } + } +} diff --git a/servo/components/style/values/specified/flex.rs b/servo/components/style/values/specified/flex.rs new file mode 100644 index 0000000000..7c767cdf34 --- /dev/null +++ b/servo/components/style/values/specified/flex.rs @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to flexbox. + +use crate::values::generics::flex::FlexBasis as GenericFlexBasis; +use crate::values::specified::Size; + +/// A specified value for the `flex-basis` property. +pub type FlexBasis = GenericFlexBasis<Size>; + +impl FlexBasis { + /// `auto` + #[inline] + pub fn auto() -> Self { + GenericFlexBasis::Size(Size::auto()) + } + + /// `0%` + #[inline] + pub fn zero_percent() -> Self { + GenericFlexBasis::Size(Size::zero_percent()) + } +} diff --git a/servo/components/style/values/specified/font.rs b/servo/components/style/values/specified/font.rs new file mode 100644 index 0000000000..2435682ce3 --- /dev/null +++ b/servo/components/style/values/specified/font.rs @@ -0,0 +1,2222 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified values for font properties + +use crate::context::QuirksMode; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::font::{FamilyName, FontFamilyList, SingleFontFamily}; +use crate::values::computed::Percentage as ComputedPercentage; +use crate::values::computed::{font as computed, Length, NonNegativeLength}; +use crate::values::computed::{CSSPixelLength, Context, ToComputedValue}; +use crate::values::generics::font::{ + self as generics, FeatureTagValue, FontSettings, FontTag, GenericLineHeight, VariationValue, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::length::{FontBaseSize, LineHeightBase, PX_PER_PT}; +use crate::values::specified::{AllowQuirks, Angle, Integer, LengthPercentage}; +use crate::values::specified::{ + FontRelativeLength, NoCalcLength, NonNegativeLengthPercentage, NonNegativeNumber, + NonNegativePercentage, Number, +}; +use crate::values::{serialize_atom_identifier, CustomIdent, SelectorParseErrorKind}; +use crate::Atom; +use cssparser::{Parser, Token}; +#[cfg(feature = "gecko")] +use malloc_size_of::{MallocSizeOf, MallocSizeOfOps, MallocUnconditionalSizeOf}; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +// FIXME(emilio): The system font code is copy-pasta, and should be cleaned up. +macro_rules! system_font_methods { + ($ty:ident, $field:ident) => { + system_font_methods!($ty); + + fn compute_system(&self, _context: &Context) -> <$ty as ToComputedValue>::ComputedValue { + debug_assert!(matches!(*self, $ty::System(..))); + #[cfg(feature = "gecko")] + { + _context.cached_system_font.as_ref().unwrap().$field.clone() + } + #[cfg(feature = "servo")] + { + unreachable!() + } + } + }; + + ($ty:ident) => { + /// Get a specified value that represents a system font. + pub fn system_font(f: SystemFont) -> Self { + $ty::System(f) + } + + /// Retreive a SystemFont from the specified value. + pub fn get_system(&self) -> Option<SystemFont> { + if let $ty::System(s) = *self { + Some(s) + } else { + None + } + } + }; +} + +/// System fonts. +#[repr(u8)] +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum SystemFont { + /// https://drafts.csswg.org/css-fonts/#valdef-font-caption + Caption, + /// https://drafts.csswg.org/css-fonts/#valdef-font-icon + Icon, + /// https://drafts.csswg.org/css-fonts/#valdef-font-menu + Menu, + /// https://drafts.csswg.org/css-fonts/#valdef-font-message-box + MessageBox, + /// https://drafts.csswg.org/css-fonts/#valdef-font-small-caption + SmallCaption, + /// https://drafts.csswg.org/css-fonts/#valdef-font-status-bar + StatusBar, + /// Internal system font, used by the `<menupopup>`s on macOS. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozPullDownMenu, + /// Internal system font, used for `<button>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozButton, + /// Internal font, used by `<select>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozList, + /// Internal font, used by `<input>` elements. + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozField, + #[css(skip)] + End, // Just for indexing purposes. +} + +const DEFAULT_SCRIPT_MIN_SIZE_PT: u32 = 8; +const DEFAULT_SCRIPT_SIZE_MULTIPLIER: f64 = 0.71; + +/// The minimum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MIN_FONT_WEIGHT: f32 = 1.; + +/// The maximum font-weight value per: +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values +pub const MAX_FONT_WEIGHT: f32 = 1000.; + +/// A specified font-weight value. +/// +/// https://drafts.csswg.org/css-fonts-4/#propdef-font-weight +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontWeight { + /// `<font-weight-absolute>` + Absolute(AbsoluteFontWeight), + /// Bolder variant + Bolder, + /// Lighter variant + Lighter, + /// System font variant. + #[css(skip)] + System(SystemFont), +} + +impl FontWeight { + system_font_methods!(FontWeight, font_weight); + + /// `normal` + #[inline] + pub fn normal() -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Normal) + } + + /// Get a specified FontWeight from a gecko keyword + pub fn from_gecko_keyword(kw: u32) -> Self { + debug_assert!(kw % 100 == 0); + debug_assert!(kw as f32 <= MAX_FONT_WEIGHT); + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::new(kw as f32))) + } +} + +impl ToComputedValue for FontWeight { + type ComputedValue = computed::FontWeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontWeight::Absolute(ref abs) => abs.compute(), + FontWeight::Bolder => context + .builder + .get_parent_font() + .clone_font_weight() + .bolder(), + FontWeight::Lighter => context + .builder + .get_parent_font() + .clone_font_weight() + .lighter(), + FontWeight::System(_) => self.compute_system(context), + } + } + + #[inline] + fn from_computed_value(computed: &computed::FontWeight) -> Self { + FontWeight::Absolute(AbsoluteFontWeight::Weight(Number::from_computed_value( + &computed.value(), + ))) + } +} + +/// An absolute font-weight value for a @font-face rule. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-weight-absolute-values +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AbsoluteFontWeight { + /// A `<number>`, with the additional constraints specified in: + /// + /// https://drafts.csswg.org/css-fonts-4/#font-weight-numeric-values + Weight(Number), + /// Normal font weight. Same as 400. + Normal, + /// Bold font weight. Same as 700. + Bold, +} + +impl AbsoluteFontWeight { + /// Returns the computed value for this absolute font weight. + pub fn compute(&self) -> computed::FontWeight { + match *self { + AbsoluteFontWeight::Weight(weight) => computed::FontWeight::from_float(weight.get()), + AbsoluteFontWeight::Normal => computed::FontWeight::NORMAL, + AbsoluteFontWeight::Bold => computed::FontWeight::BOLD, + } + } +} + +impl Parse for AbsoluteFontWeight { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(number) = input.try_parse(|input| Number::parse(context, input)) { + // We could add another AllowedNumericType value, but it doesn't + // seem worth it just for a single property with such a weird range, + // so we do the clamping here manually. + if !number.was_calc() && + (number.get() < MIN_FONT_WEIGHT || number.get() > MAX_FONT_WEIGHT) + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(AbsoluteFontWeight::Weight(number)); + } + + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => AbsoluteFontWeight::Normal, + "bold" => AbsoluteFontWeight::Bold, + }) + } +} + +/// The specified value of the `font-style` property, without the system font +/// crap. +pub type SpecifiedFontStyle = generics::FontStyle<Angle>; + +impl ToCss for SpecifiedFontStyle { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match *self { + generics::FontStyle::Normal => dest.write_str("normal"), + generics::FontStyle::Italic => dest.write_str("italic"), + generics::FontStyle::Oblique(ref angle) => { + dest.write_str("oblique")?; + if *angle != Self::default_angle() { + dest.write_char(' ')?; + angle.to_css(dest)?; + } + Ok(()) + }, + } + } +} + +impl Parse for SpecifiedFontStyle { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(try_match_ident_ignore_ascii_case! { input, + "normal" => generics::FontStyle::Normal, + "italic" => generics::FontStyle::Italic, + "oblique" => { + let angle = input.try_parse(|input| Self::parse_angle(context, input)) + .unwrap_or_else(|_| Self::default_angle()); + + generics::FontStyle::Oblique(angle) + }, + }) + } +} + +impl ToComputedValue for SpecifiedFontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + match *self { + Self::Normal => computed::FontStyle::NORMAL, + Self::Italic => computed::FontStyle::ITALIC, + Self::Oblique(ref angle) => computed::FontStyle::oblique(angle.degrees()), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if *computed == computed::FontStyle::NORMAL { + return Self::Normal; + } + if *computed == computed::FontStyle::ITALIC { + return Self::Italic; + } + let degrees = computed.oblique_degrees(); + generics::FontStyle::Oblique(Angle::from_degrees(degrees, /* was_calc = */ false)) + } +} + +/// From https://drafts.csswg.org/css-fonts-4/#valdef-font-style-oblique-angle: +/// +/// Values less than -90deg or values greater than 90deg are +/// invalid and are treated as parse errors. +/// +/// The maximum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES: f32 = 90.; + +/// The minimum angle value that `font-style: oblique` should compute to. +pub const FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES: f32 = -90.; + +impl SpecifiedFontStyle { + /// Gets a clamped angle in degrees from a specified Angle. + pub fn compute_angle_degrees(angle: &Angle) -> f32 { + angle + .degrees() + .max(FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES) + .min(FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES) + } + + /// Parse a suitable angle for font-style: oblique. + pub fn parse_angle<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Angle, ParseError<'i>> { + let angle = Angle::parse(context, input)?; + if angle.was_calc() { + return Ok(angle); + } + + let degrees = angle.degrees(); + if degrees < FONT_STYLE_OBLIQUE_MIN_ANGLE_DEGREES || + degrees > FONT_STYLE_OBLIQUE_MAX_ANGLE_DEGREES + { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + return Ok(angle); + } + + /// The default angle for `font-style: oblique`. + pub fn default_angle() -> Angle { + Angle::from_degrees( + computed::FontStyle::DEFAULT_OBLIQUE_DEGREES as f32, + /* was_calc = */ false, + ) + } +} + +/// The specified value of the `font-style` property. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum FontStyle { + Specified(SpecifiedFontStyle), + #[css(skip)] + System(SystemFont), +} + +impl FontStyle { + /// Return the `normal` value. + #[inline] + pub fn normal() -> Self { + FontStyle::Specified(generics::FontStyle::Normal) + } + + system_font_methods!(FontStyle, font_style); +} + +impl ToComputedValue for FontStyle { + type ComputedValue = computed::FontStyle; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStyle::Specified(ref specified) => specified.to_computed_value(context), + FontStyle::System(..) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStyle::Specified(SpecifiedFontStyle::from_computed_value(computed)) + } +} + +/// A value for the `font-stretch` property. +/// +/// https://drafts.csswg.org/css-fonts-4/#font-stretch-prop +#[allow(missing_docs)] +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontStretch { + Stretch(NonNegativePercentage), + Keyword(FontStretchKeyword), + #[css(skip)] + System(SystemFont), +} + +/// A keyword value for `font-stretch`. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[allow(missing_docs)] +pub enum FontStretchKeyword { + Normal, + Condensed, + UltraCondensed, + ExtraCondensed, + SemiCondensed, + SemiExpanded, + Expanded, + ExtraExpanded, + UltraExpanded, +} + +impl FontStretchKeyword { + /// Turns the keyword into a computed value. + pub fn compute(&self) -> computed::FontStretch { + computed::FontStretch::from_keyword(*self) + } + + /// Does the opposite operation to `compute`, in order to serialize keywords + /// if possible. + pub fn from_percentage(p: f32) -> Option<Self> { + computed::FontStretch::from_percentage(p).as_keyword() + } +} + +impl FontStretch { + /// `normal`. + pub fn normal() -> Self { + FontStretch::Keyword(FontStretchKeyword::Normal) + } + + system_font_methods!(FontStretch, font_stretch); +} + +impl ToComputedValue for FontStretch { + type ComputedValue = computed::FontStretch; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontStretch::Stretch(ref percentage) => { + let percentage = percentage.to_computed_value(context).0; + computed::FontStretch::from_percentage(percentage.0) + }, + FontStretch::Keyword(ref kw) => kw.compute(), + FontStretch::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + FontStretch::Stretch(NonNegativePercentage::from_computed_value(&NonNegative( + computed.to_percentage(), + ))) + } +} + +#[cfg(feature = "gecko")] +fn math_depth_enabled(_context: &ParserContext) -> bool { + static_prefs::pref!("layout.css.math-depth.enabled") +} + +#[cfg(feature = "servo")] +fn math_depth_enabled(_context: &ParserContext) -> bool { + false +} + +/// CSS font keywords +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, + Serialize, + Deserialize, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum FontSizeKeyword { + #[css(keyword = "xx-small")] + XXSmall, + XSmall, + Small, + Medium, + Large, + XLarge, + #[css(keyword = "xx-large")] + XXLarge, + #[css(keyword = "xxx-large")] + XXXLarge, + /// Indicate whether to apply font-size: math is specified so that extra + /// scaling due to math-depth changes is applied during the cascade. + #[parse(condition = "math_depth_enabled")] + Math, + #[css(skip)] + None, +} + +impl FontSizeKeyword { + /// Convert to an HTML <font size> value + #[inline] + pub fn html_size(self) -> u8 { + self as u8 + } +} + +impl Default for FontSizeKeyword { + fn default() -> Self { + FontSizeKeyword::Medium + } +} + +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + MallocSizeOf, + PartialEq, + ToAnimatedValue, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[cfg_attr(feature = "servo", derive(Serialize, Deserialize))] +/// Additional information for keyword-derived font sizes. +pub struct KeywordInfo { + /// The keyword used + pub kw: FontSizeKeyword, + /// A factor to be multiplied by the computed size of the keyword + #[css(skip)] + pub factor: f32, + /// An additional fixed offset to add to the kw * factor in the case of + /// `calc()`. + #[css(skip)] + pub offset: CSSPixelLength, +} + +impl KeywordInfo { + /// KeywordInfo value for font-size: medium + pub fn medium() -> Self { + Self::new(FontSizeKeyword::Medium) + } + + /// KeywordInfo value for font-size: none + pub fn none() -> Self { + Self::new(FontSizeKeyword::None) + } + + fn new(kw: FontSizeKeyword) -> Self { + KeywordInfo { + kw, + factor: 1., + offset: CSSPixelLength::new(0.), + } + } + + /// Computes the final size for this font-size keyword, accounting for + /// text-zoom. + fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + debug_assert_ne!(self.kw, FontSizeKeyword::None); + debug_assert_ne!(self.kw, FontSizeKeyword::Math); + let base = context.maybe_zoom_text(self.kw.to_length(context).0); + base * self.factor + context.maybe_zoom_text(self.offset) + } + + /// Given a parent keyword info (self), apply an additional factor/offset to + /// it. + fn compose(self, factor: f32) -> Self { + if self.kw == FontSizeKeyword::None { + return self; + } + KeywordInfo { + kw: self.kw, + factor: self.factor * factor, + offset: self.offset * factor, + } + } +} + +impl SpecifiedValueInfo for KeywordInfo { + fn collect_completion_keywords(f: KeywordsCollectFn) { + <FontSizeKeyword as SpecifiedValueInfo>::collect_completion_keywords(f); + } +} + +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// A specified font-size value +pub enum FontSize { + /// A length; e.g. 10px. + Length(LengthPercentage), + /// A keyword value, along with a ratio and absolute offset. + /// The ratio in any specified keyword value + /// will be 1 (with offset 0), but we cascade keywordness even + /// after font-relative (percent and em) values + /// have been applied, which is where the ratio + /// comes in. The offset comes in if we cascaded a calc value, + /// where the font-relative portion (em and percentage) will + /// go into the ratio, and the remaining units all computed together + /// will go into the offset. + /// See bug 1355707. + Keyword(KeywordInfo), + /// font-size: smaller + Smaller, + /// font-size: larger + Larger, + /// Derived from a specified system font. + #[css(skip)] + System(SystemFont), +} + +/// Specifies a prioritized list of font family names or generic family names. +#[derive(Clone, Debug, Eq, PartialEq, ToCss, ToShmem)] +#[cfg_attr(feature = "servo", derive(Hash))] +pub enum FontFamily { + /// List of `font-family` + #[css(comma)] + Values(#[css(iterable)] FontFamilyList), + /// System font + #[css(skip)] + System(SystemFont), +} + +impl FontFamily { + system_font_methods!(FontFamily, font_family); +} + +impl ToComputedValue for FontFamily { + type ComputedValue = computed::FontFamily; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + FontFamily::Values(ref list) => computed::FontFamily { + families: list.clone(), + is_system_font: false, + is_initial: false, + }, + FontFamily::System(_) => self.compute_system(context), + } + } + + fn from_computed_value(other: &computed::FontFamily) -> Self { + FontFamily::Values(other.families.clone()) + } +} + +#[cfg(feature = "gecko")] +impl MallocSizeOf for FontFamily { + fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { + match *self { + FontFamily::Values(ref v) => { + // Although the family list is refcounted, we always attribute + // its size to the specified value. + v.list.unconditional_size_of(ops) + }, + FontFamily::System(_) => 0, + } + } +} + +impl Parse for FontFamily { + /// <family-name># + /// <family-name> = <string> | [ <ident>+ ] + /// TODO: <generic-family> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontFamily, ParseError<'i>> { + let values = + input.parse_comma_separated(|input| SingleFontFamily::parse(context, input))?; + Ok(FontFamily::Values(FontFamilyList { + list: crate::ArcSlice::from_iter(values.into_iter()), + })) + } +} + +impl SpecifiedValueInfo for FontFamily {} + +/// `FamilyName::parse` is based on `SingleFontFamily::parse` and not the other +/// way around because we want the former to exclude generic family keywords. +impl Parse for FamilyName { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + match SingleFontFamily::parse(context, input) { + Ok(SingleFontFamily::FamilyName(name)) => Ok(name), + Ok(SingleFontFamily::Generic(_)) => { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Err(e) => Err(e), + } + } +} + +/// A factor for one of the font-size-adjust metrics, which may be either a number +/// or the `from-font` keyword. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum FontSizeAdjustFactor { + /// An explicitly-specified number. + Number(NonNegativeNumber), + /// The from-font keyword: resolve the number from font metrics. + FromFont, +} + +/// Specified value for font-size-adjust, intended to help +/// preserve the readability of text when font fallback occurs. +/// +/// https://drafts.csswg.org/css-fonts-5/#font-size-adjust-prop +pub type FontSizeAdjust = generics::GenericFontSizeAdjust<FontSizeAdjustFactor>; + +impl Parse for FontSizeAdjust { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + // First check if we have an adjustment factor without a metrics-basis keyword. + if let Ok(factor) = input.try_parse(|i| FontSizeAdjustFactor::parse(context, i)) { + return Ok(Self::ExHeight(factor)); + } + + let ident = input.expect_ident()?; + let basis = match_ignore_ascii_case! { &ident, + "none" => return Ok(Self::None), + // Check for size adjustment basis keywords. + "ex-height" => Self::ExHeight, + "cap-height" => Self::CapHeight, + "ch-width" => Self::ChWidth, + "ic-width" => Self::IcWidth, + "ic-height" => Self::IcHeight, + // Unknown keyword. + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + }; + + Ok(basis(FontSizeAdjustFactor::parse(context, input)?)) + } +} + +/// This is the ratio applied for font-size: larger +/// and smaller by both Firefox and Chrome +const LARGER_FONT_SIZE_RATIO: f32 = 1.2; + +/// The default font size. +pub const FONT_MEDIUM_PX: f32 = 16.0; +/// The default line height. +pub const FONT_MEDIUM_LINE_HEIGHT_PX: f32 = FONT_MEDIUM_PX * 1.2; + +impl FontSizeKeyword { + #[inline] + #[cfg(feature = "servo")] + fn to_length(&self, _: &Context) -> NonNegativeLength { + let medium = Length::new(FONT_MEDIUM_PX); + // https://drafts.csswg.org/css-fonts-3/#font-size-prop + NonNegative(match *self { + FontSizeKeyword::XXSmall => medium * 3.0 / 5.0, + FontSizeKeyword::XSmall => medium * 3.0 / 4.0, + FontSizeKeyword::Small => medium * 8.0 / 9.0, + FontSizeKeyword::Medium => medium, + FontSizeKeyword::Large => medium * 6.0 / 5.0, + FontSizeKeyword::XLarge => medium * 3.0 / 2.0, + FontSizeKeyword::XXLarge => medium * 2.0, + FontSizeKeyword::XXXLarge => medium * 3.0, + FontSizeKeyword::Math | FontSizeKeyword::None => unreachable!(), + }) + } + + #[cfg(feature = "gecko")] + #[inline] + fn to_length(&self, cx: &Context) -> NonNegativeLength { + let font = cx.style().get_font(); + let family = &font.mFont.family.families; + let generic = family + .single_generic() + .unwrap_or(computed::GenericFontFamily::None); + let base_size = unsafe { + Atom::with(font.mLanguage.mRawPtr, |language| { + cx.device().base_size_for_generic(language, generic) + }) + }; + self.to_length_without_context(cx.quirks_mode, base_size) + } + + /// Resolve a keyword length without any context, with explicit arguments. + #[cfg(feature = "gecko")] + #[inline] + pub fn to_length_without_context( + &self, + quirks_mode: QuirksMode, + base_size: Length, + ) -> NonNegativeLength { + debug_assert_ne!(*self, FontSizeKeyword::Math); + // The tables in this function are originally from + // nsRuleNode::CalcFontPointSize in Gecko: + // + // https://searchfox.org/mozilla-central/rev/c05d9d61188d32b8/layout/style/nsRuleNode.cpp#3150 + // + // Mapping from base size and HTML size to pixels + // The first index is (base_size - 9), the second is the + // HTML size. "0" is CSS keyword xx-small, not HTML size 0, + // since HTML size 0 is the same as 1. + // + // xxs xs s m l xl xxl - + // - 0/1 2 3 4 5 6 7 + static FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 27], + [9, 9, 9, 10, 12, 15, 20, 30], + [9, 9, 10, 11, 13, 17, 22, 33], + [9, 9, 10, 12, 14, 18, 24, 36], + [9, 10, 12, 13, 16, 20, 26, 39], + [9, 10, 12, 14, 17, 21, 28, 42], + [9, 10, 13, 15, 18, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + // This table gives us compatibility with WinNav4 for the default fonts only. + // In WinNav4, the default fonts were: + // + // Times/12pt == Times/16px at 96ppi + // Courier/10pt == Courier/13px at 96ppi + // + // xxs xs s m l xl xxl - + // - 1 2 3 4 5 6 7 + static QUIRKS_FONT_SIZE_MAPPING: [[i32; 8]; 8] = [ + [9, 9, 9, 9, 11, 14, 18, 28], + [9, 9, 9, 10, 12, 15, 20, 31], + [9, 9, 9, 11, 13, 17, 22, 34], + [9, 9, 10, 12, 14, 18, 24, 37], + [9, 9, 10, 13, 16, 20, 26, 40], + [9, 9, 11, 14, 17, 21, 28, 42], + [9, 10, 12, 15, 17, 23, 30, 45], + [9, 10, 13, 16, 18, 24, 32, 48], + ]; + + static FONT_SIZE_FACTORS: [i32; 8] = [60, 75, 89, 100, 120, 150, 200, 300]; + let base_size_px = base_size.px().round() as i32; + let html_size = self.html_size() as usize; + NonNegative(if base_size_px >= 9 && base_size_px <= 16 { + let mapping = if quirks_mode == QuirksMode::Quirks { + QUIRKS_FONT_SIZE_MAPPING + } else { + FONT_SIZE_MAPPING + }; + Length::new(mapping[(base_size_px - 9) as usize][html_size] as f32) + } else { + base_size * FONT_SIZE_FACTORS[html_size] as f32 / 100.0 + }) + } +} + +impl FontSize { + /// <https://html.spec.whatwg.org/multipage/#rules-for-parsing-a-legacy-font-size> + pub fn from_html_size(size: u8) -> Self { + FontSize::Keyword(KeywordInfo::new(match size { + // If value is less than 1, let it be 1. + 0 | 1 => FontSizeKeyword::XSmall, + 2 => FontSizeKeyword::Small, + 3 => FontSizeKeyword::Medium, + 4 => FontSizeKeyword::Large, + 5 => FontSizeKeyword::XLarge, + 6 => FontSizeKeyword::XXLarge, + // If value is greater than 7, let it be 7. + _ => FontSizeKeyword::XXXLarge, + })) + } + + /// Compute it against a given base font size + pub fn to_computed_value_against( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> computed::FontSize { + let compose_keyword = |factor| { + context + .style() + .get_parent_font() + .clone_font_size() + .keyword_info + .compose(factor) + }; + let mut info = KeywordInfo::none(); + let size = match *self { + FontSize::Length(LengthPercentage::Length(ref l)) => { + if let NoCalcLength::FontRelative(ref value) = *l { + if let FontRelativeLength::Em(em) = *value { + // If the parent font was keyword-derived, this is + // too. Tack the em unit onto the factor + info = compose_keyword(em); + } + } + let result = + l.to_computed_value_with_base_size(context, base_size, line_height_base); + if l.should_zoom_text() { + context.maybe_zoom_text(result) + } else { + result + } + }, + FontSize::Length(LengthPercentage::Percentage(pc)) => { + // If the parent font was keyword-derived, this is too. + // Tack the % onto the factor + info = compose_keyword(pc.0); + (base_size.resolve(context).computed_size() * pc.0).normalized() + }, + FontSize::Length(LengthPercentage::Calc(ref calc)) => { + let calc = calc.to_computed_value_zoomed(context, base_size, line_height_base); + calc.resolve(base_size.resolve(context).computed_size()) + }, + FontSize::Keyword(i) => { + if i.kw == FontSizeKeyword::Math { + // Scaling is done in recompute_math_font_size_if_needed(). + info = compose_keyword(1.); + info.kw = FontSizeKeyword::Math; + FontRelativeLength::Em(1.).to_computed_value( + context, + base_size, + line_height_base, + ) + } else { + // As a specified keyword, this is keyword derived + info = i; + i.to_computed_value(context).clamp_to_non_negative() + } + }, + FontSize::Smaller => { + info = compose_keyword(1. / LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(1. / LARGER_FONT_SIZE_RATIO).to_computed_value( + context, + base_size, + line_height_base, + ) + }, + FontSize::Larger => { + info = compose_keyword(LARGER_FONT_SIZE_RATIO); + FontRelativeLength::Em(LARGER_FONT_SIZE_RATIO).to_computed_value( + context, + base_size, + line_height_base, + ) + }, + + FontSize::System(_) => { + #[cfg(feature = "servo")] + { + unreachable!() + } + #[cfg(feature = "gecko")] + { + context + .cached_system_font + .as_ref() + .unwrap() + .font_size + .computed_size() + } + }, + }; + computed::FontSize { + computed_size: NonNegative(size), + used_size: NonNegative(size), + keyword_info: info, + } + } +} + +impl ToComputedValue for FontSize { + type ComputedValue = computed::FontSize; + + #[inline] + fn to_computed_value(&self, context: &Context) -> computed::FontSize { + self.to_computed_value_against( + context, + FontBaseSize::InheritedStyle, + LineHeightBase::InheritedStyle, + ) + } + + #[inline] + fn from_computed_value(computed: &computed::FontSize) -> Self { + FontSize::Length(LengthPercentage::Length( + ToComputedValue::from_computed_value(&computed.computed_size()), + )) + } +} + +impl FontSize { + system_font_methods!(FontSize); + + /// Get initial value for specified font size. + #[inline] + pub fn medium() -> Self { + FontSize::Keyword(KeywordInfo::medium()) + } + + /// Parses a font-size, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<FontSize, ParseError<'i>> { + if let Ok(lp) = input + .try_parse(|i| LengthPercentage::parse_non_negative_quirky(context, i, allow_quirks)) + { + return Ok(FontSize::Length(lp)); + } + + if let Ok(kw) = input.try_parse(|i| FontSizeKeyword::parse(context, i)) { + return Ok(FontSize::Keyword(KeywordInfo::new(kw))); + } + + try_match_ident_ignore_ascii_case! { input, + "smaller" => Ok(FontSize::Smaller), + "larger" => Ok(FontSize::Larger), + } + } +} + +impl Parse for FontSize { + /// <length> | <percentage> | <absolute-size> | <relative-size> + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontSize, ParseError<'i>> { + FontSize::parse_quirky(context, input, AllowQuirks::No) + } +} + +bitflags! { + #[derive(Clone, Copy)] + /// Flags of variant alternates in bit + struct VariantAlternatesParsingFlags: u8 { + /// None of variant alternates enabled + const NORMAL = 0; + /// Historical forms + const HISTORICAL_FORMS = 0x01; + /// Stylistic Alternates + const STYLISTIC = 0x02; + /// Stylistic Sets + const STYLESET = 0x04; + /// Character Variant + const CHARACTER_VARIANT = 0x08; + /// Swash glyphs + const SWASH = 0x10; + /// Ornaments glyphs + const ORNAMENTS = 0x20; + /// Annotation forms + const ANNOTATION = 0x40; + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// Set of variant alternates +pub enum VariantAlternates { + /// Enables display of stylistic alternates + #[css(function)] + Stylistic(CustomIdent), + /// Enables display with stylistic sets + #[css(comma, function)] + Styleset(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of specific character variants + #[css(comma, function)] + CharacterVariant(#[css(iterable)] crate::OwnedSlice<CustomIdent>), + /// Enables display of swash glyphs + #[css(function)] + Swash(CustomIdent), + /// Enables replacement of default glyphs with ornaments + #[css(function)] + Ornaments(CustomIdent), + /// Enables display of alternate annotation forms + #[css(function)] + Annotation(CustomIdent), + /// Enables display of historical forms + HistoricalForms, +} + +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +/// List of Variant Alternates +pub struct FontVariantAlternates( + #[css(if_empty = "normal", iterable)] crate::OwnedSlice<VariantAlternates>, +); + +impl FontVariantAlternates { + /// Returns the length of all variant alternates. + pub fn len(&self) -> usize { + self.0.iter().fold(0, |acc, alternate| match *alternate { + VariantAlternates::Swash(_) | + VariantAlternates::Stylistic(_) | + VariantAlternates::Ornaments(_) | + VariantAlternates::Annotation(_) => acc + 1, + VariantAlternates::Styleset(ref slice) | + VariantAlternates::CharacterVariant(ref slice) => acc + slice.len(), + _ => acc, + }) + } +} + +impl FontVariantAlternates { + #[inline] + /// Get initial specified value with VariantAlternatesList + pub fn get_initial_specified_value() -> Self { + Default::default() + } +} + +impl Parse for FontVariantAlternates { + /// normal | + /// [ stylistic(<feature-value-name>) || + /// historical-forms || + /// styleset(<feature-value-name> #) || + /// character-variant(<feature-value-name> #) || + /// swash(<feature-value-name>) || + /// ornaments(<feature-value-name>) || + /// annotation(<feature-value-name>) ] + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontVariantAlternates, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(Default::default()); + } + + let mut stylistic = None; + let mut historical = None; + let mut styleset = None; + let mut character_variant = None; + let mut swash = None; + let mut ornaments = None; + let mut annotation = None; + + // Parse values for the various alternate types in any order. + let mut parsed_alternates = VariantAlternatesParsingFlags::empty(); + macro_rules! check_if_parsed( + ($input:expr, $flag:path) => ( + if parsed_alternates.contains($flag) { + return Err($input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + parsed_alternates |= $flag; + ) + ); + while let Ok(_) = input.try_parse(|input| match *input.next()? { + Token::Ident(ref value) if value.eq_ignore_ascii_case("historical-forms") => { + check_if_parsed!(input, VariantAlternatesParsingFlags::HISTORICAL_FORMS); + historical = Some(VariantAlternates::HistoricalForms); + Ok(()) + }, + Token::Function(ref name) => { + let name = name.clone(); + input.parse_nested_block(|i| { + match_ignore_ascii_case! { &name, + "swash" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::SWASH); + let ident = CustomIdent::parse(i, &[])?; + swash = Some(VariantAlternates::Swash(ident)); + Ok(()) + }, + "stylistic" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLISTIC); + let ident = CustomIdent::parse(i, &[])?; + stylistic = Some(VariantAlternates::Stylistic(ident)); + Ok(()) + }, + "ornaments" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ORNAMENTS); + let ident = CustomIdent::parse(i, &[])?; + ornaments = Some(VariantAlternates::Ornaments(ident)); + Ok(()) + }, + "annotation" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::ANNOTATION); + let ident = CustomIdent::parse(i, &[])?; + annotation = Some(VariantAlternates::Annotation(ident)); + Ok(()) + }, + "styleset" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::STYLESET); + let idents = i.parse_comma_separated(|i| { + CustomIdent::parse(i, &[]) + })?; + styleset = Some(VariantAlternates::Styleset(idents.into())); + Ok(()) + }, + "character-variant" => { + check_if_parsed!(i, VariantAlternatesParsingFlags::CHARACTER_VARIANT); + let idents = i.parse_comma_separated(|i| { + CustomIdent::parse(i, &[]) + })?; + character_variant = Some(VariantAlternates::CharacterVariant(idents.into())); + Ok(()) + }, + _ => return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + }) + }, + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }) {} + + if parsed_alternates.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // Collect the parsed values in canonical order, so that we'll serialize correctly. + let mut alternates = Vec::new(); + macro_rules! push_if_some( + ($value:expr) => ( + if let Some(v) = $value { + alternates.push(v); + } + ) + ); + push_if_some!(stylistic); + push_if_some!(historical); + push_if_some!(styleset); + push_if_some!(character_variant); + push_if_some!(swash); + push_if_some!(ornaments); + push_if_some!(annotation); + + Ok(FontVariantAlternates(alternates.into())) + } +} + +macro_rules! impl_variant_east_asian { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants for east asian variant + pub struct FontVariantEastAsian(u16); + bitflags! { + impl FontVariantEastAsian: u16 { + /// None of the features + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantEastAsian { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(Self::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_east_asian_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, FontVariantEastAsian::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantEastAsian { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_east_asian! { + /// Enables rendering of JIS78 forms (OpenType feature: jp78) + JIS78 / "jis78" => NS_FONT_VARIANT_EAST_ASIAN_JIS78 = 0x01, + /// Enables rendering of JIS83 forms (OpenType feature: jp83). + JIS83 / "jis83" => NS_FONT_VARIANT_EAST_ASIAN_JIS83 = 0x02, + /// Enables rendering of JIS90 forms (OpenType feature: jp90). + JIS90 / "jis90" => NS_FONT_VARIANT_EAST_ASIAN_JIS90 = 0x04, + /// Enables rendering of JIS2004 forms (OpenType feature: jp04). + JIS04 / "jis04" => NS_FONT_VARIANT_EAST_ASIAN_JIS04 = 0x08, + /// Enables rendering of simplified forms (OpenType feature: smpl). + SIMPLIFIED / "simplified" => NS_FONT_VARIANT_EAST_ASIAN_SIMPLIFIED = 0x10, + /// Enables rendering of traditional forms (OpenType feature: trad). + TRADITIONAL / "traditional" => NS_FONT_VARIANT_EAST_ASIAN_TRADITIONAL = 0x20, + /// Enables rendering of full-width variants (OpenType feature: fwid). + FULL_WIDTH / "full-width" => NS_FONT_VARIANT_EAST_ASIAN_FULL_WIDTH = 0x40, + /// Enables rendering of proportionally-spaced variants (OpenType feature: pwid). + PROPORTIONAL_WIDTH / "proportional-width" => NS_FONT_VARIANT_EAST_ASIAN_PROP_WIDTH = 0x80, + /// Enables display of ruby variant glyphs (OpenType feature: ruby). + RUBY / "ruby" => NS_FONT_VARIANT_EAST_ASIAN_RUBY = 0x100, +} + +#[cfg(feature = "gecko")] +impl FontVariantEastAsian { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantEastAsian, u16); + +impl Parse for FontVariantEastAsian { + /// normal | [ <east-asian-variant-values> || <east-asian-width-values> || ruby ] + /// <east-asian-variant-values> = [ jis78 | jis83 | jis90 | jis04 | simplified | traditional ] + /// <east-asian-width-values> = [ full-width | proportional-width ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "jis78" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS78), + "jis83" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS83), + "jis90" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS90), + "jis04" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::JIS04), + "simplified" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::SIMPLIFIED), + "traditional" => + exclusive_value!((result, Self::JIS78 | Self::JIS83 | + Self::JIS90 | Self::JIS04 | + Self::SIMPLIFIED | Self::TRADITIONAL + ) => Self::TRADITIONAL), + "full-width" => + exclusive_value!((result, Self::FULL_WIDTH | + Self::PROPORTIONAL_WIDTH + ) => Self::FULL_WIDTH), + "proportional-width" => + exclusive_value!((result, Self::FULL_WIDTH | + Self::PROPORTIONAL_WIDTH + ) => Self::PROPORTIONAL_WIDTH), + "ruby" => + exclusive_value!((result, Self::RUBY) => Self::RUBY), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_ligatures { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants of ligatures + pub struct FontVariantLigatures(u16); + bitflags! { + impl FontVariantLigatures: u16 { + /// Specifies that common default features are enabled + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantLigatures { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + if self.contains(FontVariantLigatures::NONE) { + return dest.write_str("none"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(FontVariantLigatures::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_ligatures_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u16, FontVariantLigatures::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantLigatures { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_ligatures! { + /// Specifies that all types of ligatures and contextual forms + /// covered by this property are explicitly disabled + NONE / "none" => NS_FONT_VARIANT_LIGATURES_NONE = 0x01, + /// Enables display of common ligatures + COMMON_LIGATURES / "common-ligatures" => NS_FONT_VARIANT_LIGATURES_COMMON = 0x02, + /// Disables display of common ligatures + NO_COMMON_LIGATURES / "no-common-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_COMMON = 0x04, + /// Enables display of discretionary ligatures + DISCRETIONARY_LIGATURES / "discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_DISCRETIONARY = 0x08, + /// Disables display of discretionary ligatures + NO_DISCRETIONARY_LIGATURES / "no-discretionary-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_DISCRETIONARY = 0x10, + /// Enables display of historical ligatures + HISTORICAL_LIGATURES / "historical-ligatures" => NS_FONT_VARIANT_LIGATURES_HISTORICAL = 0x20, + /// Disables display of historical ligatures + NO_HISTORICAL_LIGATURES / "no-historical-ligatures" => NS_FONT_VARIANT_LIGATURES_NO_HISTORICAL = 0x40, + /// Enables display of contextual alternates + CONTEXTUAL / "contextual" => NS_FONT_VARIANT_LIGATURES_CONTEXTUAL = 0x80, + /// Disables display of contextual alternates + NO_CONTEXTUAL / "no-contextual" => NS_FONT_VARIANT_LIGATURES_NO_CONTEXTUAL = 0x100, +} + +#[cfg(feature = "gecko")] +impl FontVariantLigatures { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u16) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u16 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantLigatures, u16); + +impl Parse for FontVariantLigatures { + /// normal | none | + /// [ <common-lig-values> || + /// <discretionary-lig-values> || + /// <historical-lig-values> || + /// <contextual-alt-values> ] + /// <common-lig-values> = [ common-ligatures | no-common-ligatures ] + /// <discretionary-lig-values> = [ discretionary-ligatures | no-discretionary-ligatures ] + /// <historical-lig-values> = [ historical-ligatures | no-historical-ligatures ] + /// <contextual-alt-values> = [ contextual | no-contextual ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(Self::NONE); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "common-ligatures" => + exclusive_value!((result, Self::COMMON_LIGATURES | + Self::NO_COMMON_LIGATURES + ) => Self::COMMON_LIGATURES), + "no-common-ligatures" => + exclusive_value!((result, Self::COMMON_LIGATURES | + Self::NO_COMMON_LIGATURES + ) => Self::NO_COMMON_LIGATURES), + "discretionary-ligatures" => + exclusive_value!((result, Self::DISCRETIONARY_LIGATURES | + Self::NO_DISCRETIONARY_LIGATURES + ) => Self::DISCRETIONARY_LIGATURES), + "no-discretionary-ligatures" => + exclusive_value!((result, Self::DISCRETIONARY_LIGATURES | + Self::NO_DISCRETIONARY_LIGATURES + ) => Self::NO_DISCRETIONARY_LIGATURES), + "historical-ligatures" => + exclusive_value!((result, Self::HISTORICAL_LIGATURES | + Self::NO_HISTORICAL_LIGATURES + ) => Self::HISTORICAL_LIGATURES), + "no-historical-ligatures" => + exclusive_value!((result, Self::HISTORICAL_LIGATURES | + Self::NO_HISTORICAL_LIGATURES + ) => Self::NO_HISTORICAL_LIGATURES), + "contextual" => + exclusive_value!((result, Self::CONTEXTUAL | + Self::NO_CONTEXTUAL + ) => Self::CONTEXTUAL), + "no-contextual" => + exclusive_value!((result, Self::CONTEXTUAL | + Self::NO_CONTEXTUAL + ) => Self::NO_CONTEXTUAL), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +macro_rules! impl_variant_numeric { + { + $( + $(#[$($meta:tt)+])* + $ident:ident / $css:expr => $gecko:ident = $value:expr, + )+ + } => { + #[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem)] + /// Variants of numeric values + pub struct FontVariantNumeric(u8); + bitflags! { + impl FontVariantNumeric: u8 { + /// None of other variants are enabled. + const NORMAL = 0; + $( + $(#[$($meta)+])* + const $ident = $value; + )+ + } + } + + impl ToCss for FontVariantNumeric { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("normal"); + } + + let mut writer = SequenceWriter::new(dest, " "); + $( + if self.intersects(FontVariantNumeric::$ident) { + writer.raw_item($css)?; + } + )+ + Ok(()) + } + } + + /// Asserts that all variant-east-asian matches its NS_FONT_VARIANT_EAST_ASIAN_* value. + #[cfg(feature = "gecko")] + #[inline] + pub fn assert_variant_numeric_matches() { + use crate::gecko_bindings::structs; + $( + debug_assert_eq!(structs::$gecko as u8, FontVariantNumeric::$ident.bits()); + )+ + } + + impl SpecifiedValueInfo for FontVariantNumeric { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["normal", $($css,)+]); + } + } + } +} + +impl_variant_numeric! { + /// Enables display of lining numerals. + LINING_NUMS / "lining-nums" => NS_FONT_VARIANT_NUMERIC_LINING = 0x01, + /// Enables display of old-style numerals. + OLDSTYLE_NUMS / "oldstyle-nums" => NS_FONT_VARIANT_NUMERIC_OLDSTYLE = 0x02, + /// Enables display of proportional numerals. + PROPORTIONAL_NUMS / "proportional-nums" => NS_FONT_VARIANT_NUMERIC_PROPORTIONAL = 0x04, + /// Enables display of tabular numerals. + TABULAR_NUMS / "tabular-nums" => NS_FONT_VARIANT_NUMERIC_TABULAR = 0x08, + /// Enables display of lining diagonal fractions. + DIAGONAL_FRACTIONS / "diagonal-fractions" => NS_FONT_VARIANT_NUMERIC_DIAGONAL_FRACTIONS = 0x10, + /// Enables display of lining stacked fractions. + STACKED_FRACTIONS / "stacked-fractions" => NS_FONT_VARIANT_NUMERIC_STACKED_FRACTIONS = 0x20, + /// Enables display of letter forms used with ordinal numbers. + ORDINAL / "ordinal" => NS_FONT_VARIANT_NUMERIC_ORDINAL = 0x80, + /// Enables display of slashed zeros. + SLASHED_ZERO / "slashed-zero" => NS_FONT_VARIANT_NUMERIC_SLASHZERO = 0x40, +} + +#[cfg(feature = "gecko")] +impl FontVariantNumeric { + /// Obtain a specified value from a Gecko keyword value + /// + /// Intended for use with presentation attributes, not style structs + pub fn from_gecko_keyword(kw: u8) -> Self { + Self::from_bits_truncate(kw) + } + + /// Transform into gecko keyword + pub fn to_gecko_keyword(self) -> u8 { + self.bits() + } +} + +#[cfg(feature = "gecko")] +impl_gecko_keyword_conversions!(FontVariantNumeric, u8); + +impl Parse for FontVariantNumeric { + /// normal | + /// [ <numeric-figure-values> || + /// <numeric-spacing-values> || + /// <numeric-fraction-values> || + /// ordinal || + /// slashed-zero ] + /// <numeric-figure-values> = [ lining-nums | oldstyle-nums ] + /// <numeric-spacing-values> = [ proportional-nums | tabular-nums ] + /// <numeric-fraction-values> = [ diagonal-fractions | stacked-fractions ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = Self::empty(); + + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(result); + } + + while let Ok(flag) = input.try_parse(|input| { + Ok( + match_ignore_ascii_case! { &input.expect_ident().map_err(|_| ())?, + "ordinal" => + exclusive_value!((result, Self::ORDINAL) => Self::ORDINAL), + "slashed-zero" => + exclusive_value!((result, Self::SLASHED_ZERO) => Self::SLASHED_ZERO), + "lining-nums" => + exclusive_value!((result, Self::LINING_NUMS | + Self::OLDSTYLE_NUMS + ) => Self::LINING_NUMS), + "oldstyle-nums" => + exclusive_value!((result, Self::LINING_NUMS | + Self::OLDSTYLE_NUMS + ) => Self::OLDSTYLE_NUMS), + "proportional-nums" => + exclusive_value!((result, Self::PROPORTIONAL_NUMS | + Self::TABULAR_NUMS + ) => Self::PROPORTIONAL_NUMS), + "tabular-nums" => + exclusive_value!((result, Self::PROPORTIONAL_NUMS | + Self::TABULAR_NUMS + ) => Self::TABULAR_NUMS), + "diagonal-fractions" => + exclusive_value!((result, Self::DIAGONAL_FRACTIONS | + Self::STACKED_FRACTIONS + ) => Self::DIAGONAL_FRACTIONS), + "stacked-fractions" => + exclusive_value!((result, Self::DIAGONAL_FRACTIONS | + Self::STACKED_FRACTIONS + ) => Self::STACKED_FRACTIONS), + _ => return Err(()), + }, + ) + }) { + result.insert(flag); + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// This property provides low-level control over OpenType or TrueType font features. +pub type FontFeatureSettings = FontSettings<FeatureTagValue<Integer>>; + +/// For font-language-override, use the same representation as the computed value. +pub use crate::values::computed::font::FontLanguageOverride; + +impl Parse for FontLanguageOverride { + /// normal | <string> + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontLanguageOverride, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("normal")) + .is_ok() + { + return Ok(FontLanguageOverride::normal()); + } + + let string = input.expect_string()?; + + // The OpenType spec requires tags to be 1 to 4 ASCII characters: + // https://learn.microsoft.com/en-gb/typography/opentype/spec/otff#data-types + if string.is_empty() || string.len() > 4 || !string.is_ascii() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let mut bytes = [b' '; 4]; + for (byte, str_byte) in bytes.iter_mut().zip(string.as_bytes()) { + *byte = *str_byte; + } + + Ok(FontLanguageOverride(u32::from_be_bytes(bytes))) + } +} + +/// A value for any of the font-synthesis-{weight,style,small-caps} properties. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum FontSynthesis { + /// This attribute may be synthesized if not supported by a face. + Auto, + /// Do not attempt to synthesis this style attribute. + None, +} + +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Allows authors to choose a palette from those supported by a color font +/// (and potentially @font-palette-values overrides). +pub struct FontPalette(Atom); + +#[allow(missing_docs)] +impl FontPalette { + pub fn normal() -> Self { + Self(atom!("normal")) + } + pub fn light() -> Self { + Self(atom!("light")) + } + pub fn dark() -> Self { + Self(atom!("dark")) + } +} + +impl Parse for FontPalette { + /// normal | light | dark | dashed-ident + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<FontPalette, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + match_ignore_ascii_case! { &ident, + "normal" => Ok(Self::normal()), + "light" => Ok(Self::light()), + "dark" => Ok(Self::dark()), + _ => if ident.starts_with("--") { + Ok(Self(Atom::from(ident.as_ref()))) + } else { + Err(location.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))) + }, + } + } +} + +impl ToCss for FontPalette { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_atom_identifier(&self.0, dest) + } +} + +/// This property provides low-level control over OpenType or TrueType font +/// variations. +pub type FontVariationSettings = FontSettings<VariationValue<Number>>; + +fn parse_one_feature_value<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<Integer, ParseError<'i>> { + if let Ok(integer) = input.try_parse(|i| Integer::parse_non_negative(context, i)) { + return Ok(integer); + } + + try_match_ident_ignore_ascii_case! { input, + "on" => Ok(Integer::new(1)), + "off" => Ok(Integer::new(0)), + } +} + +impl Parse for FeatureTagValue<Integer> { + /// https://drafts.csswg.org/css-fonts-4/#feature-tag-value + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = input + .try_parse(|i| parse_one_feature_value(context, i)) + .unwrap_or_else(|_| Integer::new(1)); + + Ok(Self { tag, value }) + } +} + +impl Parse for VariationValue<Number> { + /// This is the `<string> <number>` part of the font-variation-settings + /// syntax. + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let tag = FontTag::parse(context, input)?; + let value = Number::parse(context, input)?; + Ok(Self { tag, value }) + } +} + +/// A metrics override value for a @font-face descriptor +/// +/// https://drafts.csswg.org/css-fonts/#font-metrics-override-desc +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum MetricsOverride { + /// A non-negative `<percentage>` of the computed font size + Override(NonNegativePercentage), + /// Normal metrics from the font. + Normal, +} + +impl MetricsOverride { + #[inline] + /// Get default value with `normal` + pub fn normal() -> MetricsOverride { + MetricsOverride::Normal + } + + /// The ToComputedValue implementation, used for @font-face descriptors. + /// + /// Valid override percentages must be non-negative; we return -1.0 to indicate + /// the absence of an override (i.e. 'normal'). + #[inline] + pub fn compute(&self) -> ComputedPercentage { + match *self { + MetricsOverride::Normal => ComputedPercentage(-1.0), + MetricsOverride::Override(percent) => ComputedPercentage(percent.0.get()), + } + } +} + +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +/// How to do font-size scaling. +pub enum XTextScale { + /// Both min-font-size and text zoom are enabled. + All, + /// Text-only zoom is enabled, but min-font-size is not honored. + ZoomOnly, + /// Neither of them is enabled. + None, +} + +impl XTextScale { + /// Returns whether text zoom is enabled. + #[inline] + pub fn text_zoom_enabled(self) -> bool { + self != Self::None + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Internal property that reflects the lang attribute +pub struct XLang(#[css(skip)] pub Atom); + +impl XLang { + #[inline] + /// Get default value for `-x-lang` + pub fn get_initial_value() -> XLang { + XLang(atom!("")) + } +} + +impl Parse for XLang { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<XLang, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// Specifies the minimum font size allowed due to changes in scriptlevel. +/// Ref: https://wiki.mozilla.org/MathML:mstyle +pub struct MozScriptMinSize(pub NoCalcLength); + +impl MozScriptMinSize { + #[inline] + /// Calculate initial value of -moz-script-min-size. + pub fn get_initial_value() -> Length { + Length::new(DEFAULT_SCRIPT_MIN_SIZE_PT as f32 * PX_PER_PT) + } +} + +impl Parse for MozScriptMinSize { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptMinSize, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// A value for the `math-depth` property. +/// https://mathml-refresh.github.io/mathml-core/#the-math-script-level-property +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum MathDepth { + /// Increment math-depth if math-style is compact. + AutoAdd, + + /// Add the function's argument to math-depth. + #[css(function)] + Add(Integer), + + /// Set math-depth to the specified value. + Absolute(Integer), +} + +impl Parse for MathDepth { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MathDepth, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("auto-add")) + .is_ok() + { + return Ok(MathDepth::AutoAdd); + } + if let Ok(math_depth_value) = input.try_parse(|input| Integer::parse(context, input)) { + return Ok(MathDepth::Absolute(math_depth_value)); + } + input.expect_function_matching("add")?; + let math_depth_delta_value = + input.parse_nested_block(|input| Integer::parse(context, input))?; + Ok(MathDepth::Add(math_depth_delta_value)) + } +} + +#[cfg_attr(feature = "gecko", derive(MallocSizeOf))] +#[derive( + Clone, + Copy, + Debug, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Specifies the multiplier to be used to adjust font size +/// due to changes in scriptlevel. +/// +/// Ref: https://www.w3.org/TR/MathML3/chapter3.html#presm.mstyle.attrs +pub struct MozScriptSizeMultiplier(pub f32); + +impl MozScriptSizeMultiplier { + #[inline] + /// Get default value of `-moz-script-size-multiplier` + pub fn get_initial_value() -> MozScriptSizeMultiplier { + MozScriptSizeMultiplier(DEFAULT_SCRIPT_SIZE_MULTIPLIER as f32) + } +} + +impl Parse for MozScriptSizeMultiplier { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozScriptSizeMultiplier, ParseError<'i>> { + debug_assert!( + false, + "Should be set directly by presentation attributes only." + ); + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +impl From<f32> for MozScriptSizeMultiplier { + fn from(v: f32) -> Self { + MozScriptSizeMultiplier(v) + } +} + +impl From<MozScriptSizeMultiplier> for f32 { + fn from(v: MozScriptSizeMultiplier) -> f32 { + v.0 + } +} + +/// A specified value for the `line-height` property. +pub type LineHeight = GenericLineHeight<NonNegativeNumber, NonNegativeLengthPercentage>; + +impl ToComputedValue for LineHeight { + type ComputedValue = computed::LineHeight; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(number) => { + GenericLineHeight::Number(number.to_computed_value(context)) + }, + GenericLineHeight::Length(ref non_negative_lp) => { + let result = match non_negative_lp.0 { + LengthPercentage::Length(NoCalcLength::Absolute(ref abs)) => { + context.maybe_zoom_text(abs.to_computed_value(context)) + }, + LengthPercentage::Length(ref length) => { + // line-height units specifically resolve against parent's + // font and line-height properties, while the rest of font + // relative units still resolve against the element's own + // properties. + length.to_computed_value_with_base_size( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ) + }, + LengthPercentage::Percentage(ref p) => FontRelativeLength::Em(p.0) + .to_computed_value( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ), + LengthPercentage::Calc(ref calc) => { + let computed_calc = calc.to_computed_value_zoomed( + context, + FontBaseSize::CurrentStyle, + LineHeightBase::InheritedStyle, + ); + let base = context.style().get_font().clone_font_size().computed_size(); + computed_calc.resolve(base) + }, + }; + GenericLineHeight::Length(result.into()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + GenericLineHeight::Normal => GenericLineHeight::Normal, + #[cfg(feature = "gecko")] + GenericLineHeight::MozBlockHeight => GenericLineHeight::MozBlockHeight, + GenericLineHeight::Number(ref number) => { + GenericLineHeight::Number(NonNegativeNumber::from_computed_value(number)) + }, + GenericLineHeight::Length(ref length) => { + GenericLineHeight::Length(NoCalcLength::from_computed_value(&length.0).into()) + }, + } + } +} diff --git a/servo/components/style/values/specified/gecko.rs b/servo/components/style/values/specified/gecko.rs new file mode 100644 index 0000000000..e721add59c --- /dev/null +++ b/servo/components/style/values/specified/gecko.rs @@ -0,0 +1,82 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for legacy Gecko-only properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, Length, LengthPercentage}; +use crate::values::generics::rect::Rect; +use cssparser::{Parser, Token}; +use std::fmt; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +fn parse_pixel_or_percent<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<LengthPercentage, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + let value = match *token { + Token::Dimension { + value, ref unit, .. + } => { + match_ignore_ascii_case! { unit, + "px" => Ok(LengthPercentage::new_length(Length::new(value))), + _ => Err(()), + } + }, + Token::Percentage { unit_value, .. } => Ok(LengthPercentage::new_percent( + computed::Percentage(unit_value), + )), + _ => Err(()), + }; + value.map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) +} + +/// The value of an IntersectionObserver's rootMargin property. +/// +/// Only bare px or percentage values are allowed. Other length units and +/// calc() values are not allowed. +/// +/// <https://w3c.github.io/IntersectionObserver/#parse-a-root-margin> +#[repr(transparent)] +pub struct IntersectionObserverRootMargin(pub Rect<LengthPercentage>); + +impl Parse for IntersectionObserverRootMargin { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::Zero; + if input.is_exhausted() { + // If there are zero elements in tokens, set tokens to ["0px"]. + return Ok(IntersectionObserverRootMargin(Rect::all( + LengthPercentage::zero(), + ))); + } + let rect = Rect::parse_with(context, input, parse_pixel_or_percent)?; + Ok(IntersectionObserverRootMargin(rect)) + } +} + +// Strictly speaking this is not ToCss. It's serializing for DOM. But +// we can just reuse the infrastructure of this. +// +// <https://w3c.github.io/IntersectionObserver/#dom-intersectionobserver-rootmargin> +impl ToCss for IntersectionObserverRootMargin { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + // We cannot use the ToCss impl of Rect, because that would + // merge items when they are equal. We want to list them all. + let mut writer = SequenceWriter::new(dest, " "); + let rect = &self.0; + writer.item(&rect.0)?; + writer.item(&rect.1)?; + writer.item(&rect.2)?; + writer.item(&rect.3) + } +} diff --git a/servo/components/style/values/specified/grid.rs b/servo/components/style/values/specified/grid.rs new file mode 100644 index 0000000000..5c78ff399b --- /dev/null +++ b/servo/components/style/values/specified/grid.rs @@ -0,0 +1,441 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the computed value of +//! [grids](https://drafts.csswg.org/css-grid/) + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::grid::{GridTemplateComponent, ImplicitGridTracks, RepeatCount}; +use crate::values::generics::grid::{LineNameList, LineNameListValue, NameRepeat, TrackBreadth}; +use crate::values::generics::grid::{TrackList, TrackListValue, TrackRepeat, TrackSize}; +use crate::values::specified::{Integer, LengthPercentage}; +use crate::values::{CSSFloat, CustomIdent}; +use cssparser::{Parser, Token}; +use std::mem; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// Parse a single flexible length. +pub fn parse_flex<'i, 't>(input: &mut Parser<'i, 't>) -> Result<CSSFloat, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Dimension { + value, ref unit, .. + } if unit.eq_ignore_ascii_case("fr") && value.is_sign_positive() => Ok(value), + ref t => Err(location.new_unexpected_token_error(t.clone())), + } +} + +impl<L> TrackBreadth<L> { + fn parse_keyword<'i, 't>(input: &mut Parser<'i, 't>) -> Result<Self, ParseError<'i>> { + #[derive(Parse)] + enum TrackKeyword { + Auto, + MaxContent, + MinContent, + } + + Ok(match TrackKeyword::parse(input)? { + TrackKeyword::Auto => TrackBreadth::Auto, + TrackKeyword::MaxContent => TrackBreadth::MaxContent, + TrackKeyword::MinContent => TrackBreadth::MinContent, + }) + } +} + +impl Parse for TrackBreadth<LengthPercentage> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // FIXME: This and other callers in this file should use + // NonNegativeLengthPercentage instead. + // + // Though it seems these cannot be animated so it's ~ok. + if let Ok(lp) = input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) { + return Ok(TrackBreadth::Breadth(lp)); + } + + if let Ok(f) = input.try_parse(parse_flex) { + return Ok(TrackBreadth::Fr(f)); + } + + Self::parse_keyword(input) + } +} + +impl Parse for TrackSize<LengthPercentage> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(b) = input.try_parse(|i| TrackBreadth::parse(context, i)) { + return Ok(TrackSize::Breadth(b)); + } + + if input + .try_parse(|i| i.expect_function_matching("minmax")) + .is_ok() + { + return input.parse_nested_block(|input| { + let inflexible_breadth = + match input.try_parse(|i| LengthPercentage::parse_non_negative(context, i)) { + Ok(lp) => TrackBreadth::Breadth(lp), + Err(..) => TrackBreadth::parse_keyword(input)?, + }; + + input.expect_comma()?; + Ok(TrackSize::Minmax( + inflexible_breadth, + TrackBreadth::parse(context, input)?, + )) + }); + } + + input.expect_function_matching("fit-content")?; + let lp = input.parse_nested_block(|i| LengthPercentage::parse_non_negative(context, i))?; + Ok(TrackSize::FitContent(TrackBreadth::Breadth(lp))) + } +} + +impl Parse for ImplicitGridTracks<TrackSize<LengthPercentage>> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use style_traits::{Separator, Space}; + let track_sizes = Space::parse(input, |i| TrackSize::parse(context, i))?; + if track_sizes.len() == 1 && track_sizes[0].is_initial() { + // A single track with the initial value is always represented by an empty slice. + return Ok(Default::default()); + } + return Ok(ImplicitGridTracks(track_sizes.into())); + } +} + +/// Parse the grid line names into a vector of owned strings. +/// +/// <https://drafts.csswg.org/css-grid/#typedef-line-names> +pub fn parse_line_names<'i, 't>( + input: &mut Parser<'i, 't>, +) -> Result<crate::OwnedSlice<CustomIdent>, ParseError<'i>> { + input.expect_square_bracket_block()?; + input.parse_nested_block(|input| { + let mut values = vec![]; + while let Ok(ident) = input.try_parse(|i| CustomIdent::parse(i, &["span", "auto"])) { + values.push(ident); + } + + Ok(values.into()) + }) +} + +/// The type of `repeat` function (only used in parsing). +/// +/// <https://drafts.csswg.org/css-grid/#typedef-track-repeat> +#[derive(Clone, Copy, Debug, PartialEq, SpecifiedValueInfo)] +#[cfg_attr(feature = "servo", derive(MallocSizeOf))] +enum RepeatType { + /// [`<auto-repeat>`](https://drafts.csswg.org/css-grid/#typedef-auto-repeat) + Auto, + /// [`<track-repeat>`](https://drafts.csswg.org/css-grid/#typedef-track-repeat) + Normal, + /// [`<fixed-repeat>`](https://drafts.csswg.org/css-grid/#typedef-fixed-repeat) + Fixed, +} + +impl TrackRepeat<LengthPercentage, Integer> { + fn parse_with_repeat_type<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<(Self, RepeatType), ParseError<'i>> { + input + .try_parse(|i| i.expect_function_matching("repeat").map_err(|e| e.into())) + .and_then(|_| { + input.parse_nested_block(|input| { + let count = RepeatCount::parse(context, input)?; + input.expect_comma()?; + + let is_auto = count == RepeatCount::AutoFit || count == RepeatCount::AutoFill; + let mut repeat_type = if is_auto { + RepeatType::Auto + } else { + // <fixed-size> is a subset of <track-size>, so it should work for both + RepeatType::Fixed + }; + + let mut names = vec![]; + let mut values = vec![]; + let mut current_names; + + loop { + current_names = input.try_parse(parse_line_names).unwrap_or_default(); + if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + if is_auto { + // should be <fixed-size> for <auto-repeat> + return Err(input + .new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if repeat_type == RepeatType::Fixed { + repeat_type = RepeatType::Normal // <track-size> for sure + } + } + + values.push(track_size); + names.push(current_names); + } else { + if values.is_empty() { + // expecting at least one <track-size> + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + + names.push(current_names); // final `<line-names>` + break; // no more <track-size>, breaking + } + } + + let repeat = TrackRepeat { + count, + track_sizes: values.into(), + line_names: names.into(), + }; + + Ok((repeat, repeat_type)) + }) + }) + } +} + +impl Parse for TrackList<LengthPercentage, Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut current_names = vec![]; + let mut names = vec![]; + let mut values = vec![]; + + // Whether we've parsed an `<auto-repeat>` value. + let mut auto_repeat_index = None; + // assume that everything is <fixed-size>. This flag is useful when we encounter <auto-repeat> + let mut at_least_one_not_fixed = false; + loop { + current_names + .extend_from_slice(&mut input.try_parse(parse_line_names).unwrap_or_default()); + if let Ok(track_size) = input.try_parse(|i| TrackSize::parse(context, i)) { + if !track_size.is_fixed() { + at_least_one_not_fixed = true; + if auto_repeat_index.is_some() { + // <auto-track-list> only accepts <fixed-size> and <fixed-repeat> + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + let vec = mem::replace(&mut current_names, vec![]); + names.push(vec.into()); + values.push(TrackListValue::TrackSize(track_size)); + } else if let Ok((repeat, type_)) = + input.try_parse(|i| TrackRepeat::parse_with_repeat_type(context, i)) + { + match type_ { + RepeatType::Normal => { + at_least_one_not_fixed = true; + if auto_repeat_index.is_some() { + // only <fixed-repeat> + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + }, + RepeatType::Auto => { + if auto_repeat_index.is_some() || at_least_one_not_fixed { + // We've either seen <auto-repeat> earlier, or there's at least one non-fixed value + return Err( + input.new_custom_error(StyleParseErrorKind::UnspecifiedError) + ); + } + auto_repeat_index = Some(values.len()); + }, + RepeatType::Fixed => {}, + } + + let vec = mem::replace(&mut current_names, vec![]); + names.push(vec.into()); + values.push(TrackListValue::TrackRepeat(repeat)); + } else { + if values.is_empty() && auto_repeat_index.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + names.push(current_names.into()); + break; + } + } + + Ok(TrackList { + auto_repeat_index: auto_repeat_index.unwrap_or(std::usize::MAX), + values: values.into(), + line_names: names.into(), + }) + } +} + +#[cfg(feature = "gecko")] +#[inline] +fn allow_grid_template_subgrids() -> bool { + true +} + +#[cfg(feature = "servo")] +#[inline] +fn allow_grid_template_subgrids() -> bool { + false +} + +#[cfg(feature = "gecko")] +#[inline] +fn allow_grid_template_masonry() -> bool { + static_prefs::pref!("layout.css.grid-template-masonry-value.enabled") +} + +#[cfg(feature = "servo")] +#[inline] +fn allow_grid_template_masonry() -> bool { + false +} + +impl Parse for GridTemplateComponent<LengthPercentage, Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(GridTemplateComponent::None); + } + + Self::parse_without_none(context, input) + } +} + +impl GridTemplateComponent<LengthPercentage, Integer> { + /// Parses a `GridTemplateComponent<LengthPercentage>` except `none` keyword. + pub fn parse_without_none<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if allow_grid_template_subgrids() { + if let Ok(t) = input.try_parse(|i| LineNameList::parse(context, i)) { + return Ok(GridTemplateComponent::Subgrid(Box::new(t))); + } + } + if allow_grid_template_masonry() { + if input + .try_parse(|i| i.expect_ident_matching("masonry")) + .is_ok() + { + return Ok(GridTemplateComponent::Masonry); + } + } + let track_list = TrackList::parse(context, input)?; + Ok(GridTemplateComponent::TrackList(Box::new(track_list))) + } +} + +impl Parse for NameRepeat<Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("repeat")?; + input.parse_nested_block(|i| { + let count = RepeatCount::parse(context, i)?; + // NameRepeat doesn't accept `auto-fit` + // https://drafts.csswg.org/css-grid/#typedef-name-repeat + if matches!(count, RepeatCount::AutoFit) { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + i.expect_comma()?; + let mut names_list = vec![]; + names_list.push(parse_line_names(i)?); // there should be at least one + while let Ok(names) = i.try_parse(parse_line_names) { + names_list.push(names); + } + + Ok(NameRepeat { + count, + line_names: names_list.into(), + }) + }) + } +} + +impl Parse for LineNameListValue<Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(repeat) = input.try_parse(|i| NameRepeat::parse(context, i)) { + return Ok(LineNameListValue::Repeat(repeat)); + } + + parse_line_names(input).map(LineNameListValue::LineNames) + } +} + +impl LineNameListValue<Integer> { + /// Returns the length of `<line-names>` after expanding repeat(N, ...). This returns zero for + /// repeat(auto-fill, ...). + #[inline] + pub fn line_names_length(&self) -> usize { + match *self { + Self::LineNames(..) => 1, + Self::Repeat(ref r) => { + match r.count { + // Note: RepeatCount is always >= 1. + RepeatCount::Number(v) => r.line_names.len() * v.value() as usize, + _ => 0, + } + }, + } + } +} + +impl Parse for LineNameList<Integer> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.expect_ident_matching("subgrid")?; + + let mut auto_repeat = false; + let mut expanded_line_names_length = 0; + let mut line_names = vec![]; + while let Ok(value) = input.try_parse(|i| LineNameListValue::parse(context, i)) { + match value { + LineNameListValue::Repeat(ref r) if r.is_auto_fill() => { + if auto_repeat { + // On a subgridded axis, the auto-fill keyword is only valid once per + // <line-name-list>. + // https://drafts.csswg.org/css-grid/#auto-repeat + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + auto_repeat = true; + }, + _ => (), + }; + + expanded_line_names_length += value.line_names_length(); + line_names.push(value); + } + + Ok(LineNameList { + expanded_line_names_length, + line_names: line_names.into(), + }) + } +} diff --git a/servo/components/style/values/specified/image.rs b/servo/components/style/values/specified/image.rs new file mode 100644 index 0000000000..76bbbf85df --- /dev/null +++ b/servo/components/style/values/specified/image.rs @@ -0,0 +1,1340 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the specified value of +//! [`image`][image]s +//! +//! [image]: https://drafts.csswg.org/css-images/#image-values + +use crate::color::mix::ColorInterpolationMethod; +use crate::parser::{Parse, ParserContext}; +use crate::stylesheets::CorsMode; +use crate::values::generics::color::ColorMixFlags; +use crate::values::generics::image::{ + self as generic, Circle, Ellipse, GradientCompatMode, ShapeExtent, +}; +use crate::values::generics::image::{GradientFlags, PaintWorklet}; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::NonNegative; +use crate::values::specified::position::{HorizontalPositionKeyword, VerticalPositionKeyword}; +use crate::values::specified::position::{Position, PositionComponent, Side}; +use crate::values::specified::url::SpecifiedImageUrl; +use crate::values::specified::{ + Angle, AngleOrPercentage, Color, Length, LengthPercentage, NonNegativeLength, + NonNegativeLengthPercentage, Resolution, +}; +use crate::values::specified::{Number, NumberOrPercentage, Percentage}; +use crate::Atom; +use cssparser::{Delimiter, Parser, Token}; +use selectors::parser::SelectorParseErrorKind; +#[cfg(feature = "servo")] +use servo_url::ServoUrl; +use std::cmp::Ordering; +use std::fmt::{self, Write}; +use style_traits::{CssType, CssWriter, KeywordsCollectFn, ParseError}; +use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[inline] +fn gradient_color_interpolation_method_enabled() -> bool { + static_prefs::pref!("layout.css.gradient-color-interpolation-method.enabled") +} + +/// Specified values for an image according to CSS-IMAGES. +/// <https://drafts.csswg.org/css-images/#image-values> +pub type Image = generic::Image<Gradient, SpecifiedImageUrl, Color, Percentage, Resolution>; + +// Images should remain small, see https://github.com/servo/servo/pull/18430 +size_of_test!(Image, 16); + +/// Specified values for a CSS gradient. +/// <https://drafts.csswg.org/css-images/#gradients> +pub type Gradient = generic::Gradient< + LineDirection, + LengthPercentage, + NonNegativeLength, + NonNegativeLengthPercentage, + Position, + Angle, + AngleOrPercentage, + Color, +>; + +/// Specified values for CSS cross-fade +/// cross-fade( CrossFadeElement, ...) +/// <https://drafts.csswg.org/css-images-4/#cross-fade-function> +pub type CrossFade = generic::CrossFade<Image, Color, Percentage>; +/// CrossFadeElement = percent? CrossFadeImage +pub type CrossFadeElement = generic::CrossFadeElement<Image, Color, Percentage>; +/// CrossFadeImage = image | color +pub type CrossFadeImage = generic::CrossFadeImage<Image, Color>; + +/// `image-set()` +pub type ImageSet = generic::ImageSet<Image, Resolution>; + +/// Each of the arguments to `image-set()` +pub type ImageSetItem = generic::ImageSetItem<Image, Resolution>; + +type LengthPercentageItemList = crate::OwnedSlice<generic::GradientItem<Color, LengthPercentage>>; + +impl Color { + fn has_modern_syntax(&self) -> bool { + match self { + Self::Absolute(absolute) => !absolute.color.is_legacy_syntax(), + Self::ColorMix(mix) => { + if mix.flags.contains(ColorMixFlags::RESULT_IN_MODERN_SYNTAX) { + true + } else { + mix.left.has_modern_syntax() || mix.right.has_modern_syntax() + } + }, + Self::LightDark(ld) => ld.light.has_modern_syntax() || ld.dark.has_modern_syntax(), + + // The default is that this color doesn't have any modern syntax. + _ => false, + } + } +} + +fn default_color_interpolation_method<T>( + items: &[generic::GradientItem<Color, T>], +) -> ColorInterpolationMethod { + let has_modern_syntax_item = items.iter().any(|item| match item { + generic::GenericGradientItem::SimpleColorStop(color) => color.has_modern_syntax(), + generic::GenericGradientItem::ComplexColorStop { color, .. } => color.has_modern_syntax(), + generic::GenericGradientItem::InterpolationHint(_) => false, + }); + + if has_modern_syntax_item { + ColorInterpolationMethod::oklab() + } else { + ColorInterpolationMethod::srgb() + } +} + +#[cfg(feature = "gecko")] +fn cross_fade_enabled() -> bool { + static_prefs::pref!("layout.css.cross-fade.enabled") +} + +#[cfg(feature = "servo")] +fn cross_fade_enabled() -> bool { + false +} + +impl SpecifiedValueInfo for Gradient { + const SUPPORTED_TYPES: u8 = CssType::GRADIENT; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + // This list here should keep sync with that in Gradient::parse. + f(&[ + "linear-gradient", + "-webkit-linear-gradient", + "-moz-linear-gradient", + "repeating-linear-gradient", + "-webkit-repeating-linear-gradient", + "-moz-repeating-linear-gradient", + "radial-gradient", + "-webkit-radial-gradient", + "-moz-radial-gradient", + "repeating-radial-gradient", + "-webkit-repeating-radial-gradient", + "-moz-repeating-radial-gradient", + "-webkit-gradient", + "conic-gradient", + "repeating-conic-gradient", + ]); + } +} + +// Need to manually implement as whether or not cross-fade shows up in +// completions & etc is dependent on it being enabled. +impl<Image, Color, Percentage> SpecifiedValueInfo for generic::CrossFade<Image, Color, Percentage> { + const SUPPORTED_TYPES: u8 = 0; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + if cross_fade_enabled() { + f(&["cross-fade"]); + } + } +} + +impl<Image, Resolution> SpecifiedValueInfo for generic::ImageSet<Image, Resolution> { + const SUPPORTED_TYPES: u8 = 0; + + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["image-set"]); + } +} + +/// A specified gradient line direction. +/// +/// FIXME(emilio): This should be generic over Angle. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum LineDirection { + /// An angular direction. + Angle(Angle), + /// A horizontal direction. + Horizontal(HorizontalPositionKeyword), + /// A vertical direction. + Vertical(VerticalPositionKeyword), + /// A direction towards a corner of a box. + Corner(HorizontalPositionKeyword, VerticalPositionKeyword), +} + +/// A specified ending shape. +pub type EndingShape = generic::EndingShape<NonNegativeLength, NonNegativeLengthPercentage>; + +bitflags! { + #[derive(Clone, Copy)] + struct ParseImageFlags: u8 { + const FORBID_NONE = 1 << 0; + const FORBID_IMAGE_SET = 1 << 1; + const FORBID_NON_URL = 1 << 2; + } +} + +impl Parse for Image { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Image::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::empty()) + } +} + +impl Image { + fn parse_with_cors_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Image, ParseError<'i>> { + if !flags.contains(ParseImageFlags::FORBID_NONE) && + input.try_parse(|i| i.expect_ident_matching("none")).is_ok() + { + return Ok(generic::Image::None); + } + + if let Ok(url) = input + .try_parse(|input| SpecifiedImageUrl::parse_with_cors_mode(context, input, cors_mode)) + { + return Ok(generic::Image::Url(url)); + } + + if !flags.contains(ParseImageFlags::FORBID_IMAGE_SET) { + if let Ok(is) = + input.try_parse(|input| ImageSet::parse(context, input, cors_mode, flags)) + { + return Ok(generic::Image::ImageSet(Box::new(is))); + } + } + + if flags.contains(ParseImageFlags::FORBID_NON_URL) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if let Ok(gradient) = input.try_parse(|i| Gradient::parse(context, i)) { + return Ok(generic::Image::Gradient(Box::new(gradient))); + } + + let function = input.expect_function()?.clone(); + input.parse_nested_block(|input| { + Ok(match_ignore_ascii_case! { &function, + #[cfg(feature = "servo-layout-2013")] + "paint" => Self::PaintWorklet(PaintWorklet::parse_args(context, input)?), + "cross-fade" if cross_fade_enabled() => Self::CrossFade(Box::new(CrossFade::parse_args(context, input, cors_mode, flags)?)), + #[cfg(feature = "gecko")] + "-moz-element" => Self::Element(Self::parse_element(input)?), + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(function))), + }) + }) + } +} + +impl Image { + /// Creates an already specified image value from an already resolved URL + /// for insertion in the cascade. + #[cfg(feature = "servo")] + pub fn for_cascade(url: ServoUrl) -> Self { + use crate::values::CssUrl; + generic::Image::Url(CssUrl::for_cascade(url)) + } + + /// Parses a `-moz-element(# <element-id>)`. + #[cfg(feature = "gecko")] + fn parse_element<'i>(input: &mut Parser<'i, '_>) -> Result<Atom, ParseError<'i>> { + let location = input.current_source_location(); + Ok(match *input.next()? { + Token::IDHash(ref id) => Atom::from(id.as_ref()), + ref t => return Err(location.new_unexpected_token_error(t.clone())), + }) + } + + /// Provides an alternate method for parsing that associates the URL with + /// anonymous CORS headers. + pub fn parse_with_cors_anonymous<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Self::parse_with_cors_mode( + context, + input, + CorsMode::Anonymous, + ParseImageFlags::empty(), + ) + } + + /// Provides an alternate method for parsing, but forbidding `none` + pub fn parse_forbid_none<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Self::parse_with_cors_mode(context, input, CorsMode::None, ParseImageFlags::FORBID_NONE) + } + + /// Provides an alternate method for parsing, but only for urls. + pub fn parse_only_url<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Image, ParseError<'i>> { + Self::parse_with_cors_mode( + context, + input, + CorsMode::None, + ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_NON_URL, + ) + } +} + +impl CrossFade { + /// cross-fade() = cross-fade( <cf-image># ) + fn parse_args<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + let elements = crate::OwnedSlice::from(input.parse_comma_separated(|input| { + CrossFadeElement::parse(context, input, cors_mode, flags) + })?); + Ok(Self { elements }) + } +} + +impl CrossFadeElement { + fn parse_percentage<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Option<Percentage> { + // We clamp our values here as this is the way that Safari and Chrome's + // implementation handle out-of-bounds percentages but whether or not + // this behavior follows the specification is still being discussed. + // See: <https://github.com/w3c/csswg-drafts/issues/5333> + input + .try_parse(|input| Percentage::parse_non_negative(context, input)) + .ok() + .map(|p| p.clamp_to_hundred()) + } + + /// <cf-image> = <percentage>? && [ <image> | <color> ] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + // Try and parse a leading percent sign. + let mut percent = Self::parse_percentage(context, input); + // Parse the image + let image = CrossFadeImage::parse(context, input, cors_mode, flags)?; + // Try and parse a trailing percent sign. + if percent.is_none() { + percent = Self::parse_percentage(context, input); + } + Ok(Self { + percent: percent.into(), + image, + }) + } +} + +impl CrossFadeImage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + if let Ok(image) = input.try_parse(|input| { + Image::parse_with_cors_mode( + context, + input, + cors_mode, + flags | ParseImageFlags::FORBID_NONE, + ) + }) { + return Ok(Self::Image(image)); + } + Ok(Self::Color(Color::parse(context, input)?)) + } +} + +impl ImageSet { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + let function = input.expect_function()?; + match_ignore_ascii_case! { &function, + "-webkit-image-set" | "image-set" => {}, + _ => { + let func = function.clone(); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func))); + } + } + let items = input.parse_nested_block(|input| { + input.parse_comma_separated(|input| { + ImageSetItem::parse(context, input, cors_mode, flags) + }) + })?; + Ok(Self { + selected_index: std::usize::MAX, + items: items.into(), + }) + } +} + +impl ImageSetItem { + fn parse_type<'i>(p: &mut Parser<'i, '_>) -> Result<crate::OwnedStr, ParseError<'i>> { + p.expect_function_matching("type")?; + p.parse_nested_block(|input| Ok(input.expect_string()?.as_ref().to_owned().into())) + } + + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + cors_mode: CorsMode, + flags: ParseImageFlags, + ) -> Result<Self, ParseError<'i>> { + let image = match input.try_parse(|i| i.expect_url_or_string()) { + Ok(url) => Image::Url(SpecifiedImageUrl::parse_from_string( + url.as_ref().into(), + context, + cors_mode, + )), + Err(..) => Image::parse_with_cors_mode( + context, + input, + cors_mode, + flags | ParseImageFlags::FORBID_NONE | ParseImageFlags::FORBID_IMAGE_SET, + )?, + }; + + let mut resolution = input + .try_parse(|input| Resolution::parse(context, input)) + .ok(); + let mime_type = input.try_parse(Self::parse_type).ok(); + + // Try to parse resolution after type(). + if mime_type.is_some() && resolution.is_none() { + resolution = input + .try_parse(|input| Resolution::parse(context, input)) + .ok(); + } + + let resolution = resolution.unwrap_or_else(|| Resolution::from_x(1.0)); + let has_mime_type = mime_type.is_some(); + let mime_type = mime_type.unwrap_or_default(); + + Ok(Self { + image, + resolution, + has_mime_type, + mime_type, + }) + } +} + +impl Parse for Gradient { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + enum Shape { + Linear, + Radial, + Conic, + } + + let func = input.expect_function()?; + let (shape, repeating, compat_mode) = match_ignore_ascii_case! { &func, + "linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::Modern) + }, + "-webkit-linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-linear-gradient" => { + (Shape::Linear, false, GradientCompatMode::Moz) + }, + "repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::Modern) + }, + "-webkit-repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-repeating-linear-gradient" => { + (Shape::Linear, true, GradientCompatMode::Moz) + }, + "radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::Modern) + }, + "-webkit-radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-radial-gradient" => { + (Shape::Radial, false, GradientCompatMode::Moz) + }, + "repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::Modern) + }, + "-webkit-repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::WebKit) + }, + #[cfg(feature = "gecko")] + "-moz-repeating-radial-gradient" => { + (Shape::Radial, true, GradientCompatMode::Moz) + }, + "conic-gradient" => { + (Shape::Conic, false, GradientCompatMode::Modern) + }, + "repeating-conic-gradient" => { + (Shape::Conic, true, GradientCompatMode::Modern) + }, + "-webkit-gradient" => { + return input.parse_nested_block(|i| { + Self::parse_webkit_gradient_argument(context, i) + }); + }, + _ => { + let func = func.clone(); + return Err(input.new_custom_error(StyleParseErrorKind::UnexpectedFunction(func))); + } + }; + + Ok(input.parse_nested_block(|i| { + Ok(match shape { + Shape::Linear => Self::parse_linear(context, i, repeating, compat_mode)?, + Shape::Radial => Self::parse_radial(context, i, repeating, compat_mode)?, + Shape::Conic => Self::parse_conic(context, i, repeating)?, + }) + })?) + } +} + +impl Gradient { + fn parse_webkit_gradient_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::position::{ + HorizontalPositionKeyword as X, VerticalPositionKeyword as Y, + }; + type Point = GenericPosition<Component<X>, Component<Y>>; + + #[derive(Clone, Copy, Parse)] + enum Component<S> { + Center, + Number(NumberOrPercentage), + Side(S), + } + + impl LineDirection { + fn from_points(first: Point, second: Point) -> Self { + let h_ord = first.horizontal.partial_cmp(&second.horizontal); + let v_ord = first.vertical.partial_cmp(&second.vertical); + let (h, v) = match (h_ord, v_ord) { + (Some(h), Some(v)) => (h, v), + _ => return LineDirection::Vertical(Y::Bottom), + }; + match (h, v) { + (Ordering::Less, Ordering::Less) => LineDirection::Corner(X::Right, Y::Bottom), + (Ordering::Less, Ordering::Equal) => LineDirection::Horizontal(X::Right), + (Ordering::Less, Ordering::Greater) => LineDirection::Corner(X::Right, Y::Top), + (Ordering::Equal, Ordering::Greater) => LineDirection::Vertical(Y::Top), + (Ordering::Equal, Ordering::Equal) | (Ordering::Equal, Ordering::Less) => { + LineDirection::Vertical(Y::Bottom) + }, + (Ordering::Greater, Ordering::Less) => { + LineDirection::Corner(X::Left, Y::Bottom) + }, + (Ordering::Greater, Ordering::Equal) => LineDirection::Horizontal(X::Left), + (Ordering::Greater, Ordering::Greater) => { + LineDirection::Corner(X::Left, Y::Top) + }, + } + } + } + + impl From<Point> for Position { + fn from(point: Point) -> Self { + Self::new(point.horizontal.into(), point.vertical.into()) + } + } + + impl Parse for Point { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + input.try_parse(|i| { + let x = Component::parse(context, i)?; + let y = Component::parse(context, i)?; + + Ok(Self::new(x, y)) + }) + } + } + + impl<S: Side> From<Component<S>> for NumberOrPercentage { + fn from(component: Component<S>) -> Self { + match component { + Component::Center => NumberOrPercentage::Percentage(Percentage::new(0.5)), + Component::Number(number) => number, + Component::Side(side) => { + let p = if side.is_start() { + Percentage::zero() + } else { + Percentage::hundred() + }; + NumberOrPercentage::Percentage(p) + }, + } + } + } + + impl<S: Side> From<Component<S>> for PositionComponent<S> { + fn from(component: Component<S>) -> Self { + match component { + Component::Center => PositionComponent::Center, + Component::Number(NumberOrPercentage::Number(number)) => { + PositionComponent::Length(Length::from_px(number.value).into()) + }, + Component::Number(NumberOrPercentage::Percentage(p)) => { + PositionComponent::Length(p.into()) + }, + Component::Side(side) => PositionComponent::Side(side, None), + } + } + } + + impl<S: Copy + Side> Component<S> { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + match ( + NumberOrPercentage::from(*self), + NumberOrPercentage::from(*other), + ) { + (NumberOrPercentage::Percentage(a), NumberOrPercentage::Percentage(b)) => { + a.get().partial_cmp(&b.get()) + }, + (NumberOrPercentage::Number(a), NumberOrPercentage::Number(b)) => { + a.value.partial_cmp(&b.value) + }, + (_, _) => None, + } + } + } + + let ident = input.expect_ident_cloned()?; + input.expect_comma()?; + + Ok(match_ignore_ascii_case! { &ident, + "linear" => { + let first = Point::parse(context, input)?; + input.expect_comma()?; + let second = Point::parse(context, input)?; + + let direction = LineDirection::from_points(first, second); + let items = Gradient::parse_webkit_gradient_stops(context, input, false)?; + + generic::Gradient::Linear { + direction, + color_interpolation_method: ColorInterpolationMethod::srgb(), + items, + // Legacy gradients always use srgb as a default. + flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + compat_mode: GradientCompatMode::Modern, + } + }, + "radial" => { + let first_point = Point::parse(context, input)?; + input.expect_comma()?; + let first_radius = Number::parse_non_negative(context, input)?; + input.expect_comma()?; + let second_point = Point::parse(context, input)?; + input.expect_comma()?; + let second_radius = Number::parse_non_negative(context, input)?; + + let (reverse_stops, point, radius) = if second_radius.value >= first_radius.value { + (false, second_point, second_radius) + } else { + (true, first_point, first_radius) + }; + + let rad = Circle::Radius(NonNegative(Length::from_px(radius.value))); + let shape = generic::EndingShape::Circle(rad); + let position: Position = point.into(); + let items = Gradient::parse_webkit_gradient_stops(context, input, reverse_stops)?; + + generic::Gradient::Radial { + shape, + position, + color_interpolation_method: ColorInterpolationMethod::srgb(), + items, + // Legacy gradients always use srgb as a default. + flags: generic::GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + compat_mode: GradientCompatMode::Modern, + } + }, + _ => { + let e = SelectorParseErrorKind::UnexpectedIdent(ident.clone()); + return Err(input.new_custom_error(e)); + }, + }) + } + + fn parse_webkit_gradient_stops<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + reverse_stops: bool, + ) -> Result<LengthPercentageItemList, ParseError<'i>> { + let mut items = input + .try_parse(|i| { + i.expect_comma()?; + i.parse_comma_separated(|i| { + let function = i.expect_function()?.clone(); + let (color, mut p) = i.parse_nested_block(|i| { + let p = match_ignore_ascii_case! { &function, + "color-stop" => { + let p = NumberOrPercentage::parse(context, i)?.to_percentage(); + i.expect_comma()?; + p + }, + "from" => Percentage::zero(), + "to" => Percentage::hundred(), + _ => { + return Err(i.new_custom_error( + StyleParseErrorKind::UnexpectedFunction(function.clone()) + )) + }, + }; + let color = Color::parse(context, i)?; + if color == Color::CurrentColor { + return Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok((color.into(), p)) + })?; + if reverse_stops { + p.reverse(); + } + Ok(generic::GradientItem::ComplexColorStop { + color, + position: p.into(), + }) + }) + }) + .unwrap_or(vec![]); + + if items.is_empty() { + items = vec![ + generic::GradientItem::ComplexColorStop { + color: Color::transparent(), + position: LengthPercentage::zero_percent(), + }, + generic::GradientItem::ComplexColorStop { + color: Color::transparent(), + position: LengthPercentage::hundred_percent(), + }, + ]; + } else if items.len() == 1 { + let first = items[0].clone(); + items.push(first); + } else { + items.sort_by(|a, b| { + match (a, b) { + ( + &generic::GradientItem::ComplexColorStop { + position: ref a_position, + .. + }, + &generic::GradientItem::ComplexColorStop { + position: ref b_position, + .. + }, + ) => match (a_position, b_position) { + (&LengthPercentage::Percentage(a), &LengthPercentage::Percentage(b)) => { + return a.0.partial_cmp(&b.0).unwrap_or(Ordering::Equal); + }, + _ => {}, + }, + _ => {}, + } + if reverse_stops { + Ordering::Greater + } else { + Ordering::Less + } + }) + } + Ok(items.into()) + } + + /// Not used for -webkit-gradient syntax and conic-gradient + fn parse_stops<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<LengthPercentageItemList, ParseError<'i>> { + let items = + generic::GradientItem::parse_comma_separated(context, input, LengthPercentage::parse)?; + if items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(items) + } + + /// Try to parse a color interpolation method. + fn try_parse_color_interpolation_method<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Option<ColorInterpolationMethod> { + if gradient_color_interpolation_method_enabled() { + input + .try_parse(|i| ColorInterpolationMethod::parse(context, i)) + .ok() + } else { + None + } + } + + /// Parses a linear gradient. + /// GradientCompatMode can change during `-moz-` prefixed gradient parsing if it come across a `to` keyword. + fn parse_linear<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + mut compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + let mut flags = GradientFlags::empty(); + flags.set(GradientFlags::REPEATING, repeating); + + let mut color_interpolation_method = + Self::try_parse_color_interpolation_method(context, input); + + let direction = input + .try_parse(|p| LineDirection::parse(context, p, &mut compat_mode)) + .ok(); + + if direction.is_some() && color_interpolation_method.is_none() { + color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); + } + + // If either of the 2 options were specified, we require a comma. + if color_interpolation_method.is_some() || direction.is_some() { + input.expect_comma()?; + } + + let items = Gradient::parse_stops(context, input)?; + + let default = default_color_interpolation_method(&items); + let color_interpolation_method = color_interpolation_method.unwrap_or(default); + flags.set( + GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + default == color_interpolation_method, + ); + + let direction = direction.unwrap_or(match compat_mode { + GradientCompatMode::Modern => LineDirection::Vertical(VerticalPositionKeyword::Bottom), + _ => LineDirection::Vertical(VerticalPositionKeyword::Top), + }); + + Ok(Gradient::Linear { + direction, + color_interpolation_method, + items, + flags, + compat_mode, + }) + } + + /// Parses a radial gradient. + fn parse_radial<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + let mut flags = GradientFlags::empty(); + flags.set(GradientFlags::REPEATING, repeating); + + let mut color_interpolation_method = + Self::try_parse_color_interpolation_method(context, input); + + let (shape, position) = match compat_mode { + GradientCompatMode::Modern => { + let shape = input.try_parse(|i| EndingShape::parse(context, i, compat_mode)); + let position = input.try_parse(|i| { + i.expect_ident_matching("at")?; + Position::parse(context, i) + }); + (shape, position.ok()) + }, + _ => { + let position = input.try_parse(|i| Position::parse(context, i)); + let shape = input.try_parse(|i| { + if position.is_ok() { + i.expect_comma()?; + } + EndingShape::parse(context, i, compat_mode) + }); + (shape, position.ok()) + }, + }; + + let has_shape_or_position = shape.is_ok() || position.is_some(); + if has_shape_or_position && color_interpolation_method.is_none() { + color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); + } + + if has_shape_or_position || color_interpolation_method.is_some() { + input.expect_comma()?; + } + + let shape = shape.unwrap_or({ + generic::EndingShape::Ellipse(Ellipse::Extent(ShapeExtent::FarthestCorner)) + }); + + let position = position.unwrap_or(Position::center()); + + let items = Gradient::parse_stops(context, input)?; + + let default = default_color_interpolation_method(&items); + let color_interpolation_method = color_interpolation_method.unwrap_or(default); + flags.set( + GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + default == color_interpolation_method, + ); + + Ok(Gradient::Radial { + shape, + position, + color_interpolation_method, + items, + flags, + compat_mode, + }) + } + + /// Parse a conic gradient. + fn parse_conic<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + repeating: bool, + ) -> Result<Self, ParseError<'i>> { + let mut flags = GradientFlags::empty(); + flags.set(GradientFlags::REPEATING, repeating); + + let mut color_interpolation_method = + Self::try_parse_color_interpolation_method(context, input); + + let angle = input.try_parse(|i| { + i.expect_ident_matching("from")?; + // Spec allows unitless zero start angles + // https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle + Angle::parse_with_unitless(context, i) + }); + let position = input.try_parse(|i| { + i.expect_ident_matching("at")?; + Position::parse(context, i) + }); + + let has_angle_or_position = angle.is_ok() || position.is_ok(); + if has_angle_or_position && color_interpolation_method.is_none() { + color_interpolation_method = Self::try_parse_color_interpolation_method(context, input); + } + + if has_angle_or_position || color_interpolation_method.is_some() { + input.expect_comma()?; + } + + let angle = angle.unwrap_or(Angle::zero()); + + let position = position.unwrap_or(Position::center()); + + let items = generic::GradientItem::parse_comma_separated( + context, + input, + AngleOrPercentage::parse_with_unitless, + )?; + + if items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + let default = default_color_interpolation_method(&items); + let color_interpolation_method = color_interpolation_method.unwrap_or(default); + flags.set( + GradientFlags::HAS_DEFAULT_COLOR_INTERPOLATION_METHOD, + default == color_interpolation_method, + ); + + Ok(Gradient::Conic { + angle, + position, + color_interpolation_method, + items, + flags, + }) + } +} + +impl generic::LineDirection for LineDirection { + fn points_downwards(&self, compat_mode: GradientCompatMode) -> bool { + match *self { + LineDirection::Angle(ref angle) => angle.degrees() == 180.0, + LineDirection::Vertical(VerticalPositionKeyword::Bottom) => { + compat_mode == GradientCompatMode::Modern + }, + LineDirection::Vertical(VerticalPositionKeyword::Top) => { + compat_mode != GradientCompatMode::Modern + }, + _ => false, + } + } + + fn to_css<W>(&self, dest: &mut CssWriter<W>, compat_mode: GradientCompatMode) -> fmt::Result + where + W: Write, + { + match *self { + LineDirection::Angle(angle) => angle.to_css(dest), + LineDirection::Horizontal(x) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest) + }, + LineDirection::Vertical(y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + y.to_css(dest) + }, + LineDirection::Corner(x, y) => { + if compat_mode == GradientCompatMode::Modern { + dest.write_str("to ")?; + } + x.to_css(dest)?; + dest.write_char(' ')?; + y.to_css(dest) + }, + } + } +} + +impl LineDirection { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + compat_mode: &mut GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + // Gradients allow unitless zero angles as an exception, see: + // https://github.com/w3c/csswg-drafts/issues/1162 + if let Ok(angle) = input.try_parse(|i| Angle::parse_with_unitless(context, i)) { + return Ok(LineDirection::Angle(angle)); + } + + input.try_parse(|i| { + let to_ident = i.try_parse(|i| i.expect_ident_matching("to")); + match *compat_mode { + // `to` keyword is mandatory in modern syntax. + GradientCompatMode::Modern => to_ident?, + // Fall back to Modern compatibility mode in case there is a `to` keyword. + // According to Gecko, `-moz-linear-gradient(to ...)` should serialize like + // `linear-gradient(to ...)`. + GradientCompatMode::Moz if to_ident.is_ok() => { + *compat_mode = GradientCompatMode::Modern + }, + // There is no `to` keyword in webkit prefixed syntax. If it's consumed, + // parsing should throw an error. + GradientCompatMode::WebKit if to_ident.is_ok() => { + return Err( + i.new_custom_error(SelectorParseErrorKind::UnexpectedIdent("to".into())) + ); + }, + _ => {}, + } + + if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { + if let Ok(y) = i.try_parse(VerticalPositionKeyword::parse) { + return Ok(LineDirection::Corner(x, y)); + } + return Ok(LineDirection::Horizontal(x)); + } + let y = VerticalPositionKeyword::parse(i)?; + if let Ok(x) = i.try_parse(HorizontalPositionKeyword::parse) { + return Ok(LineDirection::Corner(x, y)); + } + Ok(LineDirection::Vertical(y)) + }) + } +} + +impl EndingShape { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + if let Ok(extent) = input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + if input + .try_parse(|i| i.expect_ident_matching("circle")) + .is_ok() + { + return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); + } + let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); + } + if input + .try_parse(|i| i.expect_ident_matching("circle")) + .is_ok() + { + if let Ok(extent) = + input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + return Ok(generic::EndingShape::Circle(Circle::Extent(extent))); + } + if compat_mode == GradientCompatMode::Modern { + if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + return Ok(generic::EndingShape::Circle(Circle::Radius(length))); + } + } + return Ok(generic::EndingShape::Circle(Circle::Extent( + ShapeExtent::FarthestCorner, + ))); + } + if input + .try_parse(|i| i.expect_ident_matching("ellipse")) + .is_ok() + { + if let Ok(extent) = + input.try_parse(|i| ShapeExtent::parse_with_compat_mode(i, compat_mode)) + { + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent(extent))); + } + if compat_mode == GradientCompatMode::Modern { + let pair: Result<_, ParseError> = input.try_parse(|i| { + let x = NonNegativeLengthPercentage::parse(context, i)?; + let y = NonNegativeLengthPercentage::parse(context, i)?; + Ok((x, y)) + }); + if let Ok((x, y)) = pair { + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii(x, y))); + } + } + return Ok(generic::EndingShape::Ellipse(Ellipse::Extent( + ShapeExtent::FarthestCorner, + ))); + } + if let Ok(length) = input.try_parse(|i| NonNegativeLength::parse(context, i)) { + if let Ok(y) = input.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { + if compat_mode == GradientCompatMode::Modern { + let _ = input.try_parse(|i| i.expect_ident_matching("ellipse")); + } + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(length.0)), + y, + ))); + } + if compat_mode == GradientCompatMode::Modern { + let y = input.try_parse(|i| { + i.expect_ident_matching("ellipse")?; + NonNegativeLengthPercentage::parse(context, i) + }); + if let Ok(y) = y { + return Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(length.0)), + y, + ))); + } + let _ = input.try_parse(|i| i.expect_ident_matching("circle")); + } + + return Ok(generic::EndingShape::Circle(Circle::Radius(length))); + } + input.try_parse(|i| { + let x = Percentage::parse_non_negative(context, i)?; + let y = if let Ok(y) = i.try_parse(|i| NonNegativeLengthPercentage::parse(context, i)) { + if compat_mode == GradientCompatMode::Modern { + let _ = i.try_parse(|i| i.expect_ident_matching("ellipse")); + } + y + } else { + if compat_mode == GradientCompatMode::Modern { + i.expect_ident_matching("ellipse")?; + } + NonNegativeLengthPercentage::parse(context, i)? + }; + Ok(generic::EndingShape::Ellipse(Ellipse::Radii( + NonNegative(LengthPercentage::from(x)), + y, + ))) + }) + } +} + +impl ShapeExtent { + fn parse_with_compat_mode<'i, 't>( + input: &mut Parser<'i, 't>, + compat_mode: GradientCompatMode, + ) -> Result<Self, ParseError<'i>> { + match Self::parse(input)? { + ShapeExtent::Contain | ShapeExtent::Cover + if compat_mode == GradientCompatMode::Modern => + { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + ShapeExtent::Contain => Ok(ShapeExtent::ClosestSide), + ShapeExtent::Cover => Ok(ShapeExtent::FarthestCorner), + keyword => Ok(keyword), + } + } +} + +impl<T> generic::GradientItem<Color, T> { + fn parse_comma_separated<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse_position: impl for<'i1, 't1> Fn(&ParserContext, &mut Parser<'i1, 't1>) -> Result<T, ParseError<'i1>> + + Copy, + ) -> Result<crate::OwnedSlice<Self>, ParseError<'i>> { + let mut items = Vec::new(); + let mut seen_stop = false; + + loop { + input.parse_until_before(Delimiter::Comma, |input| { + if seen_stop { + if let Ok(hint) = input.try_parse(|i| parse_position(context, i)) { + seen_stop = false; + items.push(generic::GradientItem::InterpolationHint(hint)); + return Ok(()); + } + } + + let stop = generic::ColorStop::parse(context, input, parse_position)?; + + if let Ok(multi_position) = input.try_parse(|i| parse_position(context, i)) { + let stop_color = stop.color.clone(); + items.push(stop.into_item()); + items.push( + generic::ColorStop { + color: stop_color, + position: Some(multi_position), + } + .into_item(), + ); + } else { + items.push(stop.into_item()); + } + + seen_stop = true; + Ok(()) + })?; + + match input.next() { + Err(_) => break, + Ok(&Token::Comma) => continue, + Ok(_) => unreachable!(), + } + } + + if !seen_stop || items.len() < 2 { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(items.into()) + } +} + +impl<T> generic::ColorStop<Color, T> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + parse_position: impl for<'i1, 't1> Fn( + &ParserContext, + &mut Parser<'i1, 't1>, + ) -> Result<T, ParseError<'i1>>, + ) -> Result<Self, ParseError<'i>> { + Ok(generic::ColorStop { + color: Color::parse(context, input)?, + position: input.try_parse(|i| parse_position(context, i)).ok(), + }) + } +} + +impl PaintWorklet { + #[cfg(feature = "servo")] + fn parse_args<'i>(input: &mut Parser<'i, '_>) -> Result<Self, ParseError<'i>> { + use crate::custom_properties::SpecifiedValue; + let name = Atom::from(&**input.expect_ident()?); + let arguments = input + .try_parse(|input| { + input.expect_comma()?; + input.parse_comma_separated(SpecifiedValue::parse) + }) + .unwrap_or_default(); + Ok(Self { name, arguments }) + } +} + +/// https://drafts.csswg.org/css-images/#propdef-image-rendering +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum ImageRendering { + Auto, + Smooth, + #[parse(aliases = "-moz-crisp-edges")] + CrispEdges, + Pixelated, + // From the spec: + // + // This property previously accepted the values optimizeSpeed and + // optimizeQuality. These are now deprecated; a user agent must accept + // them as valid values but must treat them as having the same behavior + // as crisp-edges and smooth respectively, and authors must not use + // them. + // + Optimizespeed, + Optimizequality, +} diff --git a/servo/components/style/values/specified/length.rs b/servo/components/style/values/specified/length.rs new file mode 100644 index 0000000000..d2e1d7d346 --- /dev/null +++ b/servo/components/style/values/specified/length.rs @@ -0,0 +1,2031 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! [Length values][length]. +//! +//! [length]: https://drafts.csswg.org/css-values/#lengths + +use super::{AllowQuirks, Number, Percentage, ToComputedValue}; +use crate::computed_value_flags::ComputedValueFlags; +use crate::font_metrics::{FontMetrics, FontMetricsOrientation}; +use crate::gecko_bindings::structs::GeckoFontMetrics; +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{self, CSSPixelLength, Context}; +use crate::values::generics::length as generics; +use crate::values::generics::length::{ + GenericLengthOrNumber, GenericLengthPercentageOrNormal, GenericMaxSize, GenericSize, +}; +use crate::values::generics::NonNegative; +use crate::values::specified::calc::{self, CalcNode}; +use crate::values::specified::NonNegativeNumber; +use crate::values::CSSFloat; +use crate::{Zero, ZeroNoPercent}; +use app_units::AU_PER_PX; +use cssparser::{Parser, Token}; +use std::cmp; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +pub use super::image::Image; +pub use super::image::{EndingShape as GradientEndingShape, Gradient}; +pub use crate::values::specified::calc::CalcLengthPercentage; + +/// Number of pixels per inch +pub const PX_PER_IN: CSSFloat = 96.; +/// Number of pixels per centimeter +pub const PX_PER_CM: CSSFloat = PX_PER_IN / 2.54; +/// Number of pixels per millimeter +pub const PX_PER_MM: CSSFloat = PX_PER_IN / 25.4; +/// Number of pixels per quarter +pub const PX_PER_Q: CSSFloat = PX_PER_MM / 4.; +/// Number of pixels per point +pub const PX_PER_PT: CSSFloat = PX_PER_IN / 72.; +/// Number of pixels per pica +pub const PX_PER_PC: CSSFloat = PX_PER_PT * 12.; + +/// A font relative length. Note that if any new value is +/// added here, `custom_properties::NonCustomReferences::from_unit` +/// must also be updated. Consult the comment in that function as to why. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum FontRelativeLength { + /// A "em" value: https://drafts.csswg.org/css-values/#em + #[css(dimension)] + Em(CSSFloat), + /// A "ex" value: https://drafts.csswg.org/css-values/#ex + #[css(dimension)] + Ex(CSSFloat), + /// A "ch" value: https://drafts.csswg.org/css-values/#ch + #[css(dimension)] + Ch(CSSFloat), + /// A "cap" value: https://drafts.csswg.org/css-values/#cap + #[css(dimension)] + Cap(CSSFloat), + /// An "ic" value: https://drafts.csswg.org/css-values/#ic + #[css(dimension)] + Ic(CSSFloat), + /// A "rem" value: https://drafts.csswg.org/css-values/#rem + #[css(dimension)] + Rem(CSSFloat), + /// A "lh" value: https://drafts.csswg.org/css-values/#lh + #[css(dimension)] + Lh(CSSFloat), + /// A "rlh" value: https://drafts.csswg.org/css-values/#lh + #[css(dimension)] + Rlh(CSSFloat), +} + +/// A source to resolve font-relative units against +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum FontBaseSize { + /// Use the font-size of the current element. + CurrentStyle, + /// Use the inherited font-size. + InheritedStyle, +} + +/// A source to resolve font-relative line-height units against. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum LineHeightBase { + /// Use the line-height of the current element. + CurrentStyle, + /// Use the inherited line-height. + InheritedStyle, +} + +impl FontBaseSize { + /// Calculate the actual size for a given context + pub fn resolve(&self, context: &Context) -> computed::FontSize { + match *self { + Self::CurrentStyle => context.style().get_font().clone_font_size(), + Self::InheritedStyle => context.style().get_parent_font().clone_font_size(), + } + } +} + +impl FontRelativeLength { + /// Unit identifier for `em`. + pub const EM: &'static str = "em"; + /// Unit identifier for `ex`. + pub const EX: &'static str = "ex"; + /// Unit identifier for `ch`. + pub const CH: &'static str = "ch"; + /// Unit identifier for `cap`. + pub const CAP: &'static str = "cap"; + /// Unit identifier for `ic`. + pub const IC: &'static str = "ic"; + /// Unit identifier for `rem`. + pub const REM: &'static str = "rem"; + /// Unit identifier for `lh`. + pub const LH: &'static str = "lh"; + /// Unit identifier for `rlh`. + pub const RLH: &'static str = "rlh"; + + /// Return the unitless, raw value. + fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Em(v) | + Self::Ex(v) | + Self::Ch(v) | + Self::Cap(v) | + Self::Ic(v) | + Self::Rem(v) | + Self::Lh(v) | + Self::Rlh(v) => v, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Em(_) => Self::EM, + Self::Ex(_) => Self::EX, + Self::Ch(_) => Self::CH, + Self::Cap(_) => Self::CAP, + Self::Ic(_) => Self::IC, + Self::Rem(_) => Self::REM, + Self::Lh(_) => Self::LH, + Self::Rlh(_) => Self::RLH, + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::FontRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Em(one), &Em(other)) => Em(op(one, other)), + (&Ex(one), &Ex(other)) => Ex(op(one, other)), + (&Ch(one), &Ch(other)) => Ch(op(one, other)), + (&Cap(one), &Cap(other)) => Cap(op(one, other)), + (&Ic(one), &Ic(other)) => Ic(op(one, other)), + (&Rem(one), &Rem(other)) => Rem(op(one, other)), + (&Lh(one), &Lh(other)) => Lh(op(one, other)), + (&Rlh(one), &Rlh(other)) => Rlh(op(one, other)), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + match self { + Self::Em(x) => Self::Em(op(*x)), + Self::Ex(x) => Self::Ex(op(*x)), + Self::Ch(x) => Self::Ch(op(*x)), + Self::Cap(x) => Self::Cap(op(*x)), + Self::Ic(x) => Self::Ic(op(*x)), + Self::Rem(x) => Self::Rem(op(*x)), + Self::Lh(x) => Self::Lh(op(*x)), + Self::Rlh(x) => Self::Lh(op(*x)), + } + } + + /// Computes the font-relative length. + pub fn to_computed_value( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> computed::Length { + let (reference_size, length) = + self.reference_font_size_and_length(context, base_size, line_height_base); + (reference_size * length).finite() + } + + /// Computes the length, given a GeckoFontMetrics getter to resolve font-relative units. + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: impl Fn() -> GeckoFontMetrics, + ) -> Result<CSSFloat, ()> { + let metrics = get_font_metrics(); + Ok(match *self { + Self::Em(v) => v * metrics.mComputedEmSize.px(), + Self::Ex(v) => v * metrics.mXSize.px(), + Self::Ch(v) => v * metrics.mChSize.px(), + Self::Cap(v) => v * metrics.mCapHeight.px(), + Self::Ic(v) => v * metrics.mIcWidth.px(), + // `lh`, `rlh` & `rem` are unsupported as we have no context for it. + Self::Rem(_) | Self::Lh(_) | Self::Rlh(_) => return Err(()), + }) + } + + /// Return reference font size. + /// + /// We use the base_size flag to pass a different size for computing + /// font-size and unconstrained font-size. + /// + /// This returns a pair, the first one is the reference font size, and the + /// second one is the unpacked relative length. + fn reference_font_size_and_length( + &self, + context: &Context, + base_size: FontBaseSize, + line_height_base: LineHeightBase, + ) -> (computed::Length, CSSFloat) { + fn query_font_metrics( + context: &Context, + base_size: FontBaseSize, + orientation: FontMetricsOrientation, + ) -> FontMetrics { + let retrieve_math_scales = false; + context.query_font_metrics(base_size, orientation, retrieve_math_scales) + } + + let reference_font_size = base_size.resolve(context); + match *self { + Self::Em(length) => { + if context.for_non_inherited_property && base_size == FontBaseSize::CurrentStyle { + context + .rule_cache_conditions + .borrow_mut() + .set_font_size_dependency(reference_font_size.computed_size); + } + + (reference_font_size.computed_size(), length) + }, + Self::Ex(length) => { + // The x-height is an intrinsically horizontal metric. + let metrics = + query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal); + let reference_size = metrics.x_height.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ex + // + // In the cases where it is impossible or impractical to + // determine the x-height, a value of 0.5em must be + // assumed. + // + // (But note we use 0.5em of the used, not computed + // font-size) + reference_font_size.used_size() * 0.5 + }); + (reference_size, length) + }, + Self::Ch(length) => { + // https://drafts.csswg.org/css-values/#ch: + // + // Equal to the used advance measure of the “0” (ZERO, + // U+0030) glyph in the font used to render it. (The advance + // measure of a glyph is its advance width or height, + // whichever is in the inline axis of the element.) + // + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::MatchContextPreferHorizontal, + ); + let reference_size = metrics.zero_advance_measure.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ch + // + // In the cases where it is impossible or impractical to + // determine the measure of the “0” glyph, it must be + // assumed to be 0.5em wide by 1em tall. Thus, the ch + // unit falls back to 0.5em in the general case, and to + // 1em when it would be typeset upright (i.e. + // writing-mode is vertical-rl or vertical-lr and + // text-orientation is upright). + // + // Same caveat about computed vs. used font-size applies + // above. + let wm = context.style().writing_mode; + if wm.is_vertical() && wm.is_upright() { + reference_font_size.used_size() + } else { + reference_font_size.used_size() * 0.5 + } + }); + (reference_size, length) + }, + Self::Cap(length) => { + let metrics = + query_font_metrics(context, base_size, FontMetricsOrientation::Horizontal); + let reference_size = metrics.cap_height.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#cap + // + // In the cases where it is impossible or impractical to + // determine the cap-height, the font’s ascent must be + // used. + // + metrics.ascent + }); + (reference_size, length) + }, + Self::Ic(length) => { + let metrics = query_font_metrics( + context, + base_size, + FontMetricsOrientation::MatchContextPreferVertical, + ); + let reference_size = metrics.ic_width.unwrap_or_else(|| { + // https://drafts.csswg.org/css-values/#ic + // + // In the cases where it is impossible or impractical to + // determine the ideographic advance measure, it must be + // assumed to be 1em. + // + // Same caveat about computed vs. used as for other + // metric-dependent units. + reference_font_size.used_size() + }); + (reference_size, length) + }, + Self::Rem(length) => { + // https://drafts.csswg.org/css-values/#rem: + // + // When specified on the font-size property of the root + // element, the rem units refer to the property's initial + // value. + // + let reference_size = if context.builder.is_root_element || context.in_media_query { + reference_font_size.computed_size() + } else { + context.device().root_font_size() + }; + (reference_size, length) + }, + Self::Lh(length) => { + // https://drafts.csswg.org/css-values-4/#lh + // + // When specified in media-query, the lh units refer to the + // initial values of font and line-height properties. + // + let reference_size = if context.in_media_query { + context + .device() + .calc_line_height( + &context.default_style().get_font(), + context.style().writing_mode, + None, + ) + .0 + } else { + let line_height = context.builder.calc_line_height( + context.device(), + line_height_base, + context.style().writing_mode, + ); + if context.for_non_inherited_property && + line_height_base == LineHeightBase::CurrentStyle + { + context + .rule_cache_conditions + .borrow_mut() + .set_line_height_dependency(line_height) + } + line_height.0 + }; + (reference_size, length) + }, + Self::Rlh(length) => { + // https://drafts.csswg.org/css-values-4/#rlh + // + // When specified on the root element, the rlh units refer + // to the initial values of font and line-height properties. + // + let reference_size: CSSPixelLength = + if context.builder.is_root_element || context.in_media_query { + context + .device() + .calc_line_height( + &context.default_style().get_font(), + context.style().writing_mode, + None, + ) + .0 + } else { + context.device().root_line_height() + }; + (reference_size, length) + }, + } + } +} + +/// https://drafts.csswg.org/css-values/#viewport-variants +pub enum ViewportVariant { + /// https://drafts.csswg.org/css-values/#ua-default-viewport-size + UADefault, + /// https://drafts.csswg.org/css-values/#small-viewport-percentage-units + Small, + /// https://drafts.csswg.org/css-values/#large-viewport-percentage-units + Large, + /// https://drafts.csswg.org/css-values/#dynamic-viewport-percentage-units + Dynamic, +} + +/// https://drafts.csswg.org/css-values/#viewport-relative-units +#[derive(PartialEq)] +enum ViewportUnit { + /// *vw units. + Vw, + /// *vh units. + Vh, + /// *vmin units. + Vmin, + /// *vmax units. + Vmax, + /// *vb units. + Vb, + /// *vi units. + Vi, +} + +/// A viewport-relative length. +/// +/// <https://drafts.csswg.org/css-values/#viewport-relative-lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum ViewportPercentageLength { + /// <https://drafts.csswg.org/css-values/#valdef-length-vw> + #[css(dimension)] + Vw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svw> + #[css(dimension)] + Svw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvw> + #[css(dimension)] + Lvw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvw> + #[css(dimension)] + Dvw(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vh> + #[css(dimension)] + Vh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svh> + #[css(dimension)] + Svh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvh> + #[css(dimension)] + Lvh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvh> + #[css(dimension)] + Dvh(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vmin> + #[css(dimension)] + Vmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svmin> + #[css(dimension)] + Svmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvmin> + #[css(dimension)] + Lvmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvmin> + #[css(dimension)] + Dvmin(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vmax> + #[css(dimension)] + Vmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svmax> + #[css(dimension)] + Svmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvmax> + #[css(dimension)] + Lvmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvmax> + #[css(dimension)] + Dvmax(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vb> + #[css(dimension)] + Vb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svb> + #[css(dimension)] + Svb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvb> + #[css(dimension)] + Lvb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvb> + #[css(dimension)] + Dvb(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-vi> + #[css(dimension)] + Vi(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-svi> + #[css(dimension)] + Svi(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-lvi> + #[css(dimension)] + Lvi(CSSFloat), + /// <https://drafts.csswg.org/css-values/#valdef-length-dvi> + #[css(dimension)] + Dvi(CSSFloat), +} + +impl ViewportPercentageLength { + /// Return the unitless, raw value. + fn unitless_value(&self) -> CSSFloat { + self.unpack().2 + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Vw(_) => "vw", + Self::Lvw(_) => "lvw", + Self::Svw(_) => "svw", + Self::Dvw(_) => "dvw", + Self::Vh(_) => "vh", + Self::Svh(_) => "svh", + Self::Lvh(_) => "lvh", + Self::Dvh(_) => "dvh", + Self::Vmin(_) => "vmin", + Self::Svmin(_) => "svmin", + Self::Lvmin(_) => "lvmin", + Self::Dvmin(_) => "dvmin", + Self::Vmax(_) => "vmax", + Self::Svmax(_) => "svmax", + Self::Lvmax(_) => "lvmax", + Self::Dvmax(_) => "dvmax", + Self::Vb(_) => "vb", + Self::Svb(_) => "svb", + Self::Lvb(_) => "lvb", + Self::Dvb(_) => "dvb", + Self::Vi(_) => "vi", + Self::Svi(_) => "svi", + Self::Lvi(_) => "lvi", + Self::Dvi(_) => "dvi", + } + } + + fn unpack(&self) -> (ViewportVariant, ViewportUnit, CSSFloat) { + match *self { + Self::Vw(v) => (ViewportVariant::UADefault, ViewportUnit::Vw, v), + Self::Svw(v) => (ViewportVariant::Small, ViewportUnit::Vw, v), + Self::Lvw(v) => (ViewportVariant::Large, ViewportUnit::Vw, v), + Self::Dvw(v) => (ViewportVariant::Dynamic, ViewportUnit::Vw, v), + Self::Vh(v) => (ViewportVariant::UADefault, ViewportUnit::Vh, v), + Self::Svh(v) => (ViewportVariant::Small, ViewportUnit::Vh, v), + Self::Lvh(v) => (ViewportVariant::Large, ViewportUnit::Vh, v), + Self::Dvh(v) => (ViewportVariant::Dynamic, ViewportUnit::Vh, v), + Self::Vmin(v) => (ViewportVariant::UADefault, ViewportUnit::Vmin, v), + Self::Svmin(v) => (ViewportVariant::Small, ViewportUnit::Vmin, v), + Self::Lvmin(v) => (ViewportVariant::Large, ViewportUnit::Vmin, v), + Self::Dvmin(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmin, v), + Self::Vmax(v) => (ViewportVariant::UADefault, ViewportUnit::Vmax, v), + Self::Svmax(v) => (ViewportVariant::Small, ViewportUnit::Vmax, v), + Self::Lvmax(v) => (ViewportVariant::Large, ViewportUnit::Vmax, v), + Self::Dvmax(v) => (ViewportVariant::Dynamic, ViewportUnit::Vmax, v), + Self::Vb(v) => (ViewportVariant::UADefault, ViewportUnit::Vb, v), + Self::Svb(v) => (ViewportVariant::Small, ViewportUnit::Vb, v), + Self::Lvb(v) => (ViewportVariant::Large, ViewportUnit::Vb, v), + Self::Dvb(v) => (ViewportVariant::Dynamic, ViewportUnit::Vb, v), + Self::Vi(v) => (ViewportVariant::UADefault, ViewportUnit::Vi, v), + Self::Svi(v) => (ViewportVariant::Small, ViewportUnit::Vi, v), + Self::Lvi(v) => (ViewportVariant::Large, ViewportUnit::Vi, v), + Self::Dvi(v) => (ViewportVariant::Dynamic, ViewportUnit::Vi, v), + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::ViewportPercentageLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Vw(one), &Vw(other)) => Vw(op(one, other)), + (&Svw(one), &Svw(other)) => Svw(op(one, other)), + (&Lvw(one), &Lvw(other)) => Lvw(op(one, other)), + (&Dvw(one), &Dvw(other)) => Dvw(op(one, other)), + (&Vh(one), &Vh(other)) => Vh(op(one, other)), + (&Svh(one), &Svh(other)) => Svh(op(one, other)), + (&Lvh(one), &Lvh(other)) => Lvh(op(one, other)), + (&Dvh(one), &Dvh(other)) => Dvh(op(one, other)), + (&Vmin(one), &Vmin(other)) => Vmin(op(one, other)), + (&Svmin(one), &Svmin(other)) => Svmin(op(one, other)), + (&Lvmin(one), &Lvmin(other)) => Lvmin(op(one, other)), + (&Dvmin(one), &Dvmin(other)) => Dvmin(op(one, other)), + (&Vmax(one), &Vmax(other)) => Vmax(op(one, other)), + (&Svmax(one), &Svmax(other)) => Svmax(op(one, other)), + (&Lvmax(one), &Lvmax(other)) => Lvmax(op(one, other)), + (&Dvmax(one), &Dvmax(other)) => Dvmax(op(one, other)), + (&Vb(one), &Vb(other)) => Vb(op(one, other)), + (&Svb(one), &Svb(other)) => Svb(op(one, other)), + (&Lvb(one), &Lvb(other)) => Lvb(op(one, other)), + (&Dvb(one), &Dvb(other)) => Dvb(op(one, other)), + (&Vi(one), &Vi(other)) => Vi(op(one, other)), + (&Svi(one), &Svi(other)) => Svi(op(one, other)), + (&Lvi(one), &Lvi(other)) => Lvi(op(one, other)), + (&Dvi(one), &Dvi(other)) => Dvi(op(one, other)), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) | + Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) | + Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) | + Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + match self { + Self::Vw(x) => Self::Vw(op(*x)), + Self::Svw(x) => Self::Svw(op(*x)), + Self::Lvw(x) => Self::Lvw(op(*x)), + Self::Dvw(x) => Self::Dvw(op(*x)), + Self::Vh(x) => Self::Vh(op(*x)), + Self::Svh(x) => Self::Svh(op(*x)), + Self::Lvh(x) => Self::Lvh(op(*x)), + Self::Dvh(x) => Self::Dvh(op(*x)), + Self::Vmin(x) => Self::Vmin(op(*x)), + Self::Svmin(x) => Self::Svmin(op(*x)), + Self::Lvmin(x) => Self::Lvmin(op(*x)), + Self::Dvmin(x) => Self::Dvmin(op(*x)), + Self::Vmax(x) => Self::Vmax(op(*x)), + Self::Svmax(x) => Self::Svmax(op(*x)), + Self::Lvmax(x) => Self::Lvmax(op(*x)), + Self::Dvmax(x) => Self::Dvmax(op(*x)), + Self::Vb(x) => Self::Vb(op(*x)), + Self::Svb(x) => Self::Svb(op(*x)), + Self::Lvb(x) => Self::Lvb(op(*x)), + Self::Dvb(x) => Self::Dvb(op(*x)), + Self::Vi(x) => Self::Vi(op(*x)), + Self::Svi(x) => Self::Svi(op(*x)), + Self::Lvi(x) => Self::Lvi(op(*x)), + Self::Dvi(x) => Self::Dvi(op(*x)), + } + } + + /// Computes the given viewport-relative length for the given viewport size. + pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + let (variant, unit, factor) = self.unpack(); + let size = context.viewport_size_for_viewport_unit_resolution(variant); + let length = match unit { + ViewportUnit::Vw => size.width, + ViewportUnit::Vh => size.height, + ViewportUnit::Vmin => cmp::min(size.width, size.height), + ViewportUnit::Vmax => cmp::max(size.width, size.height), + ViewportUnit::Vi | ViewportUnit::Vb => { + context + .rule_cache_conditions + .borrow_mut() + .set_writing_mode_dependency(context.builder.writing_mode); + if (unit == ViewportUnit::Vb) == context.style().writing_mode.is_vertical() { + size.width + } else { + size.height + } + }, + }; + + // FIXME: Bug 1396535, we need to fix the extremely small viewport length for transform. + // See bug 989802. We truncate so that adding multiple viewport units + // that add up to 100 does not overflow due to rounding differences. + // We convert appUnits to CSS px manually here to avoid premature clamping by + // going through the Au type. + let trunc_scaled = + ((length.0 as f64 * factor as f64 / 100.).trunc() / AU_PER_PX as f64) as f32; + CSSPixelLength::new(crate::values::normalize(trunc_scaled)) + } +} + +/// HTML5 "character width", as defined in HTML5 § 14.5.4. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub struct CharacterWidth(pub i32); + +impl CharacterWidth { + /// Computes the given character width. + pub fn to_computed_value(&self, reference_font_size: computed::Length) -> computed::Length { + // This applies the *converting a character width to pixels* algorithm + // as specified in HTML5 § 14.5.4. + // + // TODO(pcwalton): Find these from the font. + let average_advance = reference_font_size * 0.5; + let max_advance = reference_font_size; + (average_advance * (self.0 as CSSFloat - 1.0) + max_advance).finite() + } +} + +/// Represents an absolute length with its unit +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum AbsoluteLength { + /// An absolute length in pixels (px) + #[css(dimension)] + Px(CSSFloat), + /// An absolute length in inches (in) + #[css(dimension)] + In(CSSFloat), + /// An absolute length in centimeters (cm) + #[css(dimension)] + Cm(CSSFloat), + /// An absolute length in millimeters (mm) + #[css(dimension)] + Mm(CSSFloat), + /// An absolute length in quarter-millimeters (q) + #[css(dimension)] + Q(CSSFloat), + /// An absolute length in points (pt) + #[css(dimension)] + Pt(CSSFloat), + /// An absolute length in pica (pc) + #[css(dimension)] + Pc(CSSFloat), +} + +impl AbsoluteLength { + /// Return the unitless, raw value. + fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Px(v) | + Self::In(v) | + Self::Cm(v) | + Self::Mm(v) | + Self::Q(v) | + Self::Pt(v) | + Self::Pc(v) => v, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Px(_) => "px", + Self::In(_) => "in", + Self::Cm(_) => "cm", + Self::Mm(_) => "mm", + Self::Q(_) => "q", + Self::Pt(_) => "pt", + Self::Pc(_) => "pc", + } + } + + /// Convert this into a pixel value. + #[inline] + pub fn to_px(&self) -> CSSFloat { + match *self { + Self::Px(value) => value, + Self::In(value) => value * PX_PER_IN, + Self::Cm(value) => value * PX_PER_CM, + Self::Mm(value) => value * PX_PER_MM, + Self::Q(value) => value * PX_PER_Q, + Self::Pt(value) => value * PX_PER_PT, + Self::Pc(value) => value * PX_PER_PC, + } + } + + fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + Ok(Self::Px(op(self.to_px(), other.to_px()))) + } + + fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + Self::Px(op(self.to_px())) + } +} + +impl ToComputedValue for AbsoluteLength { + type ComputedValue = CSSPixelLength; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + CSSPixelLength::new(context.builder.effective_zoom().zoom(self.to_px())).finite() + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Self::Px(computed.px()) + } +} + +impl PartialOrd for AbsoluteLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + self.to_px().partial_cmp(&other.to_px()) + } +} + +/// A container query length. +/// +/// <https://drafts.csswg.org/css-contain-3/#container-lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToCss, ToShmem)] +pub enum ContainerRelativeLength { + /// 1% of query container's width + #[css(dimension)] + Cqw(CSSFloat), + /// 1% of query container's height + #[css(dimension)] + Cqh(CSSFloat), + /// 1% of query container's inline size + #[css(dimension)] + Cqi(CSSFloat), + /// 1% of query container's block size + #[css(dimension)] + Cqb(CSSFloat), + /// The smaller value of `cqi` or `cqb` + #[css(dimension)] + Cqmin(CSSFloat), + /// The larger value of `cqi` or `cqb` + #[css(dimension)] + Cqmax(CSSFloat), +} + +impl ContainerRelativeLength { + fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Cqw(v) | + Self::Cqh(v) | + Self::Cqi(v) | + Self::Cqb(v) | + Self::Cqmin(v) | + Self::Cqmax(v) => v, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Cqw(_) => "cqw", + Self::Cqh(_) => "cqh", + Self::Cqi(_) => "cqi", + Self::Cqb(_) => "cqb", + Self::Cqmin(_) => "cqmin", + Self::Cqmax(_) => "cqmax", + } + } + + pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::ContainerRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Cqw(one), &Cqw(other)) => Cqw(op(one, other)), + (&Cqh(one), &Cqh(other)) => Cqh(op(one, other)), + (&Cqi(one), &Cqi(other)) => Cqi(op(one, other)), + (&Cqb(one), &Cqb(other)) => Cqb(op(one, other)), + (&Cqmin(one), &Cqmin(other)) => Cqmin(op(one, other)), + (&Cqmax(one), &Cqmax(other)) => Cqmax(op(one, other)), + + // See https://github.com/rust-lang/rust/issues/68867, then + // https://github.com/rust-lang/rust/pull/95161. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + match self { + Self::Cqw(x) => Self::Cqw(op(*x)), + Self::Cqh(x) => Self::Cqh(op(*x)), + Self::Cqi(x) => Self::Cqi(op(*x)), + Self::Cqb(x) => Self::Cqb(op(*x)), + Self::Cqmin(x) => Self::Cqmin(op(*x)), + Self::Cqmax(x) => Self::Cqmax(op(*x)), + } + } + + /// Computes the given container-relative length. + pub fn to_computed_value(&self, context: &Context) -> CSSPixelLength { + if context.for_non_inherited_property { + context.rule_cache_conditions.borrow_mut().set_uncacheable(); + } + context + .builder + .add_flags(ComputedValueFlags::USES_CONTAINER_UNITS); + + let size = context.get_container_size_query(); + let (factor, container_length) = match *self { + Self::Cqw(v) => (v, size.get_container_width(context)), + Self::Cqh(v) => (v, size.get_container_height(context)), + Self::Cqi(v) => (v, size.get_container_inline_size(context)), + Self::Cqb(v) => (v, size.get_container_block_size(context)), + Self::Cqmin(v) => ( + v, + cmp::min( + size.get_container_inline_size(context), + size.get_container_block_size(context), + ), + ), + Self::Cqmax(v) => ( + v, + cmp::max( + size.get_container_inline_size(context), + size.get_container_block_size(context), + ), + ), + }; + CSSPixelLength::new((container_length.to_f64_px() * factor as f64 / 100.0) as f32).finite() + } +} + +#[cfg(feature = "gecko")] +fn are_container_queries_enabled() -> bool { + static_prefs::pref!("layout.css.container-queries.enabled") +} +#[cfg(feature = "servo")] +fn are_container_queries_enabled() -> bool { + false +} + +/// A `<length>` without taking `calc` expressions into account +/// +/// <https://drafts.csswg.org/css-values/#lengths> +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub enum NoCalcLength { + /// An absolute length + /// + /// <https://drafts.csswg.org/css-values/#absolute-length> + Absolute(AbsoluteLength), + + /// A font-relative length: + /// + /// <https://drafts.csswg.org/css-values/#font-relative-lengths> + FontRelative(FontRelativeLength), + + /// A viewport-relative length. + /// + /// <https://drafts.csswg.org/css-values/#viewport-relative-lengths> + ViewportPercentage(ViewportPercentageLength), + + /// A container query length. + /// + /// <https://drafts.csswg.org/css-contain-3/#container-lengths> + ContainerRelative(ContainerRelativeLength), + /// HTML5 "character width", as defined in HTML5 § 14.5.4. + /// + /// This cannot be specified by the user directly and is only generated by + /// `Stylist::synthesize_rules_for_legacy_attributes()`. + ServoCharacterWidth(CharacterWidth), +} + +impl NoCalcLength { + /// Return the unitless, raw value. + pub fn unitless_value(&self) -> CSSFloat { + match *self { + Self::Absolute(v) => v.unitless_value(), + Self::FontRelative(v) => v.unitless_value(), + Self::ViewportPercentage(v) => v.unitless_value(), + Self::ContainerRelative(v) => v.unitless_value(), + Self::ServoCharacterWidth(c) => c.0 as f32, + } + } + + // Return the unit, as a string. + fn unit(&self) -> &'static str { + match *self { + Self::Absolute(v) => v.unit(), + Self::FontRelative(v) => v.unit(), + Self::ViewportPercentage(v) => v.unit(), + Self::ContainerRelative(v) => v.unit(), + Self::ServoCharacterWidth(_) => "", + } + } + + /// Returns whether the value of this length without unit is less than zero. + pub fn is_negative(&self) -> bool { + self.unitless_value().is_sign_negative() + } + + /// Returns whether the value of this length without unit is equal to zero. + pub fn is_zero(&self) -> bool { + self.unitless_value() == 0.0 + } + + /// Returns whether the value of this length without unit is infinite. + pub fn is_infinite(&self) -> bool { + self.unitless_value().is_infinite() + } + + /// Returns whether the value of this length without unit is NaN. + pub fn is_nan(&self) -> bool { + self.unitless_value().is_nan() + } + + /// Whether text-only zoom should be applied to this length. + /// + /// Generally, font-dependent/relative units don't get text-only-zoomed, + /// because the font they're relative to should be zoomed already. + pub fn should_zoom_text(&self) -> bool { + match *self { + Self::Absolute(..) | Self::ViewportPercentage(..) | Self::ContainerRelative(..) => true, + Self::ServoCharacterWidth(..) | Self::FontRelative(..) => false, + } + } + + /// Parse a given absolute or relative dimension. + pub fn parse_dimension( + context: &ParserContext, + value: CSSFloat, + unit: &str, + ) -> Result<Self, ()> { + Ok(match_ignore_ascii_case! { unit, + "px" => Self::Absolute(AbsoluteLength::Px(value)), + "in" => Self::Absolute(AbsoluteLength::In(value)), + "cm" => Self::Absolute(AbsoluteLength::Cm(value)), + "mm" => Self::Absolute(AbsoluteLength::Mm(value)), + "q" => Self::Absolute(AbsoluteLength::Q(value)), + "pt" => Self::Absolute(AbsoluteLength::Pt(value)), + "pc" => Self::Absolute(AbsoluteLength::Pc(value)), + // font-relative + "em" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Em(value)), + "ex" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ex(value)), + "ch" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ch(value)), + "cap" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Cap(value)), + "ic" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Ic(value)), + "rem" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rem(value)), + "lh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Lh(value)), + "rlh" if context.parsing_mode.allows_font_relative_lengths() => Self::FontRelative(FontRelativeLength::Rlh(value)), + // viewport percentages + "vw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vw(value)) + }, + "svw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svw(value)) + }, + "lvw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvw(value)) + }, + "dvw" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvw(value)) + }, + "vh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vh(value)) + }, + "svh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svh(value)) + }, + "lvh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvh(value)) + }, + "dvh" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvh(value)) + }, + "vmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vmin(value)) + }, + "svmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svmin(value)) + }, + "lvmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvmin(value)) + }, + "dvmin" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvmin(value)) + }, + "vmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vmax(value)) + }, + "svmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svmax(value)) + }, + "lvmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvmax(value)) + }, + "dvmax" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvmax(value)) + }, + "vb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vb(value)) + }, + "svb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svb(value)) + }, + "lvb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvb(value)) + }, + "dvb" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvb(value)) + }, + "vi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Vi(value)) + }, + "svi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Svi(value)) + }, + "lvi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Lvi(value)) + }, + "dvi" if !context.in_page_rule() => { + Self::ViewportPercentage(ViewportPercentageLength::Dvi(value)) + }, + // Container query lengths. Inherit the limitation from viewport units since + // we may fall back to them. + "cqw" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqw(value)) + }, + "cqh" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqh(value)) + }, + "cqi" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqi(value)) + }, + "cqb" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqb(value)) + }, + "cqmin" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqmin(value)) + }, + "cqmax" if !context.in_page_rule() && are_container_queries_enabled() => { + Self::ContainerRelative(ContainerRelativeLength::Cqmax(value)) + }, + _ => return Err(()), + }) + } + + pub(crate) fn try_op<O>(&self, other: &Self, op: O) -> Result<Self, ()> + where + O: Fn(f32, f32) -> f32, + { + use self::NoCalcLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return Err(()); + } + + Ok(match (self, other) { + (&Absolute(ref one), &Absolute(ref other)) => Absolute(one.try_op(other, op)?), + (&FontRelative(ref one), &FontRelative(ref other)) => { + FontRelative(one.try_op(other, op)?) + }, + (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => { + ViewportPercentage(one.try_op(other, op)?) + }, + (&ContainerRelative(ref one), &ContainerRelative(ref other)) => { + ContainerRelative(one.try_op(other, op)?) + }, + (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => { + ServoCharacterWidth(CharacterWidth(op(one.0 as f32, other.0 as f32) as i32)) + }, + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Absolute(..) | + FontRelative(..) | + ViewportPercentage(..) | + ContainerRelative(..) | + ServoCharacterWidth(..) => {}, + } + debug_unreachable!("Forgot to handle unit in try_op()") + }, + }) + } + + pub(crate) fn map(&self, mut op: impl FnMut(f32) -> f32) -> Self { + use self::NoCalcLength::*; + + match self { + Absolute(ref one) => Absolute(one.map(op)), + FontRelative(ref one) => FontRelative(one.map(op)), + ViewportPercentage(ref one) => ViewportPercentage(one.map(op)), + ContainerRelative(ref one) => ContainerRelative(one.map(op)), + ServoCharacterWidth(ref one) => { + ServoCharacterWidth(CharacterWidth(op(one.0 as f32) as i32)) + }, + } + } + + /// Get a px value without context (so only absolute units can be handled). + #[inline] + pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> { + match *self { + Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()), + _ => Err(()), + } + } + + /// Get a px value without a full style context; this can handle either + /// absolute or (if a font metrics getter is provided) font-relative units. + #[inline] + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>, + ) -> Result<CSSFloat, ()> { + match *self { + Self::Absolute(len) => Ok(CSSPixelLength::new(len.to_px()).finite().px()), + Self::FontRelative(fr) => { + if let Some(getter) = get_font_metrics { + fr.to_computed_pixel_length_with_font_metrics(getter) + } else { + Err(()) + } + }, + _ => Err(()), + } + } + + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> NoCalcLength { + NoCalcLength::Absolute(AbsoluteLength::Px(px_value)) + } +} + +impl ToCss for NoCalcLength { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.unitless_value(), + self.unit(), + false, + dest, + ) + } +} + +impl SpecifiedValueInfo for NoCalcLength {} + +impl PartialOrd for NoCalcLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::NoCalcLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Absolute(ref one), &Absolute(ref other)) => one.to_px().partial_cmp(&other.to_px()), + (&FontRelative(ref one), &FontRelative(ref other)) => one.partial_cmp(other), + (&ViewportPercentage(ref one), &ViewportPercentage(ref other)) => { + one.partial_cmp(other) + }, + (&ContainerRelative(ref one), &ContainerRelative(ref other)) => one.partial_cmp(other), + (&ServoCharacterWidth(ref one), &ServoCharacterWidth(ref other)) => { + one.0.partial_cmp(&other.0) + }, + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Absolute(..) | + FontRelative(..) | + ViewportPercentage(..) | + ContainerRelative(..) | + ServoCharacterWidth(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Zero for NoCalcLength { + fn zero() -> Self { + NoCalcLength::Absolute(AbsoluteLength::Px(0.)) + } + + fn is_zero(&self) -> bool { + NoCalcLength::is_zero(self) + } +} + +/// An extension to `NoCalcLength` to parse `calc` expressions. +/// This is commonly used for the `<length>` values. +/// +/// <https://drafts.csswg.org/css-values/#lengths> +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum Length { + /// The internal length type that cannot parse `calc` + NoCalc(NoCalcLength), + /// A calc expression. + /// + /// <https://drafts.csswg.org/css-values/#calc-notation> + Calc(Box<CalcLengthPercentage>), +} + +impl From<NoCalcLength> for Length { + #[inline] + fn from(len: NoCalcLength) -> Self { + Length::NoCalc(len) + } +} + +impl PartialOrd for FontRelativeLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::FontRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Em(ref one), &Em(ref other)) => one.partial_cmp(other), + (&Ex(ref one), &Ex(ref other)) => one.partial_cmp(other), + (&Ch(ref one), &Ch(ref other)) => one.partial_cmp(other), + (&Cap(ref one), &Cap(ref other)) => one.partial_cmp(other), + (&Ic(ref one), &Ic(ref other)) => one.partial_cmp(other), + (&Rem(ref one), &Rem(ref other)) => one.partial_cmp(other), + (&Lh(ref one), &Lh(ref other)) => one.partial_cmp(other), + (&Rlh(ref one), &Rlh(ref other)) => one.partial_cmp(other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Em(..) | Ex(..) | Ch(..) | Cap(..) | Ic(..) | Rem(..) | Lh(..) | Rlh(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl PartialOrd for ContainerRelativeLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::ContainerRelativeLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Cqw(ref one), &Cqw(ref other)) => one.partial_cmp(other), + (&Cqh(ref one), &Cqh(ref other)) => one.partial_cmp(other), + (&Cqi(ref one), &Cqi(ref other)) => one.partial_cmp(other), + (&Cqb(ref one), &Cqb(ref other)) => one.partial_cmp(other), + (&Cqmin(ref one), &Cqmin(ref other)) => one.partial_cmp(other), + (&Cqmax(ref one), &Cqmax(ref other)) => one.partial_cmp(other), + + // See https://github.com/rust-lang/rust/issues/68867, then + // https://github.com/rust-lang/rust/pull/95161. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Cqw(..) | Cqh(..) | Cqi(..) | Cqb(..) | Cqmin(..) | Cqmax(..) => {}, + } + debug_unreachable!("Forgot to handle unit in partial_cmp()") + }, + } + } +} + +impl PartialOrd for ViewportPercentageLength { + fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> { + use self::ViewportPercentageLength::*; + + if std::mem::discriminant(self) != std::mem::discriminant(other) { + return None; + } + + match (self, other) { + (&Vw(ref one), &Vw(ref other)) => one.partial_cmp(other), + (&Svw(ref one), &Svw(ref other)) => one.partial_cmp(other), + (&Lvw(ref one), &Lvw(ref other)) => one.partial_cmp(other), + (&Dvw(ref one), &Dvw(ref other)) => one.partial_cmp(other), + (&Vh(ref one), &Vh(ref other)) => one.partial_cmp(other), + (&Svh(ref one), &Svh(ref other)) => one.partial_cmp(other), + (&Lvh(ref one), &Lvh(ref other)) => one.partial_cmp(other), + (&Dvh(ref one), &Dvh(ref other)) => one.partial_cmp(other), + (&Vmin(ref one), &Vmin(ref other)) => one.partial_cmp(other), + (&Svmin(ref one), &Svmin(ref other)) => one.partial_cmp(other), + (&Lvmin(ref one), &Lvmin(ref other)) => one.partial_cmp(other), + (&Dvmin(ref one), &Dvmin(ref other)) => one.partial_cmp(other), + (&Vmax(ref one), &Vmax(ref other)) => one.partial_cmp(other), + (&Svmax(ref one), &Svmax(ref other)) => one.partial_cmp(other), + (&Lvmax(ref one), &Lvmax(ref other)) => one.partial_cmp(other), + (&Dvmax(ref one), &Dvmax(ref other)) => one.partial_cmp(other), + (&Vb(ref one), &Vb(ref other)) => one.partial_cmp(other), + (&Svb(ref one), &Svb(ref other)) => one.partial_cmp(other), + (&Lvb(ref one), &Lvb(ref other)) => one.partial_cmp(other), + (&Dvb(ref one), &Dvb(ref other)) => one.partial_cmp(other), + (&Vi(ref one), &Vi(ref other)) => one.partial_cmp(other), + (&Svi(ref one), &Svi(ref other)) => one.partial_cmp(other), + (&Lvi(ref one), &Lvi(ref other)) => one.partial_cmp(other), + (&Dvi(ref one), &Dvi(ref other)) => one.partial_cmp(other), + // See https://github.com/rust-lang/rust/issues/68867. rustc isn't + // able to figure it own on its own so we help. + _ => unsafe { + match *self { + Vw(..) | Svw(..) | Lvw(..) | Dvw(..) | Vh(..) | Svh(..) | Lvh(..) | + Dvh(..) | Vmin(..) | Svmin(..) | Lvmin(..) | Dvmin(..) | Vmax(..) | + Svmax(..) | Lvmax(..) | Dvmax(..) | Vb(..) | Svb(..) | Lvb(..) | Dvb(..) | + Vi(..) | Svi(..) | Lvi(..) | Dvi(..) => {}, + } + debug_unreachable!("Forgot an arm in partial_cmp?") + }, + } + } +} + +impl Length { + #[inline] + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Dimension { + value, ref unit, .. + } if num_context.is_ok(context.parsing_mode, value) => { + NoCalcLength::parse_dimension(context, value, unit) + .map(Length::NoCalc) + .map_err(|()| location.new_unexpected_token_error(token.clone())) + }, + Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => { + if value != 0. && + !context.parsing_mode.allows_unitless_lengths() && + !allow_quirks.allowed(context.quirks_mode) + { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(Length::NoCalc(NoCalcLength::Absolute(AbsoluteLength::Px( + value, + )))) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let calc = CalcNode::parse_length(context, input, num_context, function)?; + Ok(Length::Calc(Box::new(calc))) + }, + ref token => return Err(location.new_unexpected_token_error(token.clone())), + } + } + + /// Parse a non-negative length + #[inline] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_non_negative_quirky(context, input, AllowQuirks::No) + } + + /// Parse a non-negative length, allowing quirks. + #[inline] + pub fn parse_non_negative_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal( + context, + input, + AllowedNumericType::NonNegative, + allow_quirks, + ) + } + + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> Length { + Length::NoCalc(NoCalcLength::from_px(px_value)) + } + + /// Get a px value without context. + pub fn to_computed_pixel_length_without_context(&self) -> Result<CSSFloat, ()> { + match *self { + Self::NoCalc(ref l) => l.to_computed_pixel_length_without_context(), + Self::Calc(ref l) => l.to_computed_pixel_length_without_context(), + } + } + + /// Get a px value, with an optional GeckoFontMetrics getter to resolve font-relative units. + pub fn to_computed_pixel_length_with_font_metrics( + &self, + get_font_metrics: Option<impl Fn() -> GeckoFontMetrics>, + ) -> Result<CSSFloat, ()> { + match *self { + Self::NoCalc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics), + Self::Calc(ref l) => l.to_computed_pixel_length_with_font_metrics(get_font_metrics), + } + } +} + +impl Parse for Length { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl Zero for Length { + fn zero() -> Self { + Length::NoCalc(NoCalcLength::zero()) + } + + fn is_zero(&self) -> bool { + // FIXME(emilio): Seems a bit weird to treat calc() unconditionally as + // non-zero here? + match *self { + Length::NoCalc(ref l) => l.is_zero(), + Length::Calc(..) => false, + } + } +} + +impl Length { + /// Parses a length, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks) + } +} + +/// A wrapper of Length, whose value must be >= 0. +pub type NonNegativeLength = NonNegative<Length>; + +impl Parse for NonNegativeLength { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Length::parse_non_negative(context, input)?)) + } +} + +impl From<NoCalcLength> for NonNegativeLength { + #[inline] + fn from(len: NoCalcLength) -> Self { + NonNegative(Length::NoCalc(len)) + } +} + +impl From<Length> for NonNegativeLength { + #[inline] + fn from(len: Length) -> Self { + NonNegative(len) + } +} + +impl NonNegativeLength { + /// Get an absolute length from a px value. + #[inline] + pub fn from_px(px_value: CSSFloat) -> Self { + Length::from_px(px_value.max(0.)).into() + } + + /// Parses a non-negative length, optionally with quirks. + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Length::parse_non_negative_quirky( + context, + input, + allow_quirks, + )?)) + } +} + +/// A `<length-percentage>` value. This can be either a `<length>`, a +/// `<percentage>`, or a combination of both via `calc()`. +/// +/// https://drafts.csswg.org/css-values-4/#typedef-length-percentage +#[allow(missing_docs)] +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum LengthPercentage { + Length(NoCalcLength), + Percentage(computed::Percentage), + Calc(Box<CalcLengthPercentage>), +} + +impl From<Length> for LengthPercentage { + fn from(len: Length) -> LengthPercentage { + match len { + Length::NoCalc(l) => LengthPercentage::Length(l), + Length::Calc(l) => LengthPercentage::Calc(l), + } + } +} + +impl From<NoCalcLength> for LengthPercentage { + #[inline] + fn from(len: NoCalcLength) -> Self { + LengthPercentage::Length(len) + } +} + +impl From<Percentage> for LengthPercentage { + #[inline] + fn from(pc: Percentage) -> Self { + if let Some(clamping_mode) = pc.calc_clamping_mode() { + LengthPercentage::Calc(Box::new(CalcLengthPercentage { + clamping_mode, + node: CalcNode::Leaf(calc::Leaf::Percentage(pc.get())), + })) + } else { + LengthPercentage::Percentage(computed::Percentage(pc.get())) + } + } +} + +impl From<computed::Percentage> for LengthPercentage { + #[inline] + fn from(pc: computed::Percentage) -> Self { + LengthPercentage::Percentage(pc) + } +} + +impl Parse for LengthPercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl LengthPercentage { + #[inline] + /// Returns a `0%` value. + pub fn zero_percent() -> LengthPercentage { + LengthPercentage::Percentage(computed::Percentage::zero()) + } + + #[inline] + /// Returns a `100%` value. + pub fn hundred_percent() -> LengthPercentage { + LengthPercentage::Percentage(computed::Percentage::hundred()) + } + + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let token = input.next()?; + match *token { + Token::Dimension { + value, ref unit, .. + } if num_context.is_ok(context.parsing_mode, value) => { + return NoCalcLength::parse_dimension(context, value, unit) + .map(LengthPercentage::Length) + .map_err(|()| location.new_unexpected_token_error(token.clone())); + }, + Token::Percentage { unit_value, .. } + if num_context.is_ok(context.parsing_mode, unit_value) => + { + return Ok(LengthPercentage::Percentage(computed::Percentage( + unit_value, + ))); + }, + Token::Number { value, .. } if num_context.is_ok(context.parsing_mode, value) => { + if value != 0. && + !context.parsing_mode.allows_unitless_lengths() && + !allow_quirks.allowed(context.quirks_mode) + { + return Err(location.new_unexpected_token_error(token.clone())); + } else { + return Ok(LengthPercentage::Length(NoCalcLength::from_px(value))); + } + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let calc = + CalcNode::parse_length_or_percentage(context, input, num_context, function)?; + Ok(LengthPercentage::Calc(Box::new(calc))) + }, + _ => return Err(location.new_unexpected_token_error(token.clone())), + } + } + + /// Parses allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(context, input, AllowedNumericType::All, allow_quirks) + } + + /// Parse a non-negative length. + /// + /// FIXME(emilio): This should be not public and we should use + /// NonNegativeLengthPercentage instead. + #[inline] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_non_negative_quirky(context, input, AllowQuirks::No) + } + + /// Parse a non-negative length, with quirks. + #[inline] + pub fn parse_non_negative_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal( + context, + input, + AllowedNumericType::NonNegative, + allow_quirks, + ) + } + + /// Returns self as specified::calc::CalcNode. + /// Note that this expect the clamping_mode is AllowedNumericType::All for Calc. The caller + /// should take care about it when using this function. + fn to_calc_node(self) -> CalcNode { + match self { + LengthPercentage::Length(l) => CalcNode::Leaf(calc::Leaf::Length(l)), + LengthPercentage::Percentage(p) => CalcNode::Leaf(calc::Leaf::Percentage(p.0)), + LengthPercentage::Calc(p) => p.node, + } + } + + /// Construct the value representing `calc(100% - self)`. + pub fn hundred_percent_minus(self, clamping_mode: AllowedNumericType) -> Self { + let mut sum = smallvec::SmallVec::<[CalcNode; 2]>::new(); + sum.push(CalcNode::Leaf(calc::Leaf::Percentage(1.0))); + + let mut node = self.to_calc_node(); + node.negate(); + sum.push(node); + + let calc = CalcNode::Sum(sum.into_boxed_slice().into()); + LengthPercentage::Calc(Box::new( + calc.into_length_or_percentage(clamping_mode).unwrap(), + )) + } +} + +impl Zero for LengthPercentage { + fn zero() -> Self { + LengthPercentage::Length(NoCalcLength::zero()) + } + + fn is_zero(&self) -> bool { + match *self { + LengthPercentage::Length(l) => l.is_zero(), + LengthPercentage::Percentage(p) => p.0 == 0.0, + LengthPercentage::Calc(_) => false, + } + } +} + +impl ZeroNoPercent for LengthPercentage { + fn is_zero_no_percent(&self) -> bool { + match *self { + LengthPercentage::Percentage(_) => false, + _ => self.is_zero(), + } + } +} + +/// A specified type for `<length-percentage> | auto`. +pub type LengthPercentageOrAuto = generics::LengthPercentageOrAuto<LengthPercentage>; + +impl LengthPercentageOrAuto { + /// Returns a value representing `0%`. + #[inline] + pub fn zero_percent() -> Self { + generics::LengthPercentageOrAuto::LengthPercentage(LengthPercentage::zero_percent()) + } + + /// Parses a length or a percentage, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + LengthPercentage::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// A wrapper of LengthPercentageOrAuto, whose value must be >= 0. +pub type NonNegativeLengthPercentageOrAuto = + generics::LengthPercentageOrAuto<NonNegativeLengthPercentage>; + +impl NonNegativeLengthPercentageOrAuto { + /// Returns a value representing `0%`. + #[inline] + pub fn zero_percent() -> Self { + generics::LengthPercentageOrAuto::LengthPercentage( + NonNegativeLengthPercentage::zero_percent(), + ) + } + + /// Parses a non-negative length-percentage, allowing the unitless length + /// quirk. + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// A wrapper of LengthPercentage, whose value must be >= 0. +pub type NonNegativeLengthPercentage = NonNegative<LengthPercentage>; + +/// Either a NonNegativeLengthPercentage or the `normal` keyword. +pub type NonNegativeLengthPercentageOrNormal = + GenericLengthPercentageOrNormal<NonNegativeLengthPercentage>; + +impl From<NoCalcLength> for NonNegativeLengthPercentage { + #[inline] + fn from(len: NoCalcLength) -> Self { + NonNegative(LengthPercentage::from(len)) + } +} + +impl Parse for NonNegativeLengthPercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl NonNegativeLengthPercentage { + #[inline] + /// Returns a `0%` value. + pub fn zero_percent() -> Self { + NonNegative(LengthPercentage::zero_percent()) + } + + /// Parses a length or a percentage, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + LengthPercentage::parse_non_negative_quirky(context, input, allow_quirks).map(NonNegative) + } +} + +/// Either a `<length>` or the `auto` keyword. +/// +/// Note that we use LengthPercentage just for convenience, since it pretty much +/// is everything we care about, but we could just add a similar LengthOrAuto +/// instead if we think getting rid of this weirdness is worth it. +pub type LengthOrAuto = generics::LengthPercentageOrAuto<Length>; + +impl LengthOrAuto { + /// Parses a length, allowing the unitless length quirk. + /// <https://quirks.spec.whatwg.org/#the-unitless-length-quirk> + #[inline] + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with(context, input, |context, input| { + Length::parse_quirky(context, input, allow_quirks) + }) + } +} + +/// Either a non-negative `<length>` or the `auto` keyword. +pub type NonNegativeLengthOrAuto = generics::LengthPercentageOrAuto<NonNegativeLength>; + +/// Either a `<length>` or a `<number>`. +pub type LengthOrNumber = GenericLengthOrNumber<Length, Number>; + +/// A specified value for `min-width`, `min-height`, `width` or `height` property. +pub type Size = GenericSize<NonNegativeLengthPercentage>; + +impl Parse for Size { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Size::parse_quirky(context, input, AllowQuirks::No) + } +} + +macro_rules! parse_size_non_length { + ($size:ident, $input:expr, $auto_or_none:expr => $auto_or_none_ident:ident) => {{ + let size = $input.try_parse(|input| { + Ok(try_match_ident_ignore_ascii_case! { input, + #[cfg(feature = "gecko")] + "min-content" | "-moz-min-content" => $size::MinContent, + #[cfg(feature = "gecko")] + "max-content" | "-moz-max-content" => $size::MaxContent, + #[cfg(feature = "gecko")] + "fit-content" | "-moz-fit-content" => $size::FitContent, + #[cfg(feature = "gecko")] + "-moz-available" => $size::MozAvailable, + $auto_or_none => $size::$auto_or_none_ident, + }) + }); + if size.is_ok() { + return size; + } + }}; +} + +#[cfg(feature = "gecko")] +fn is_fit_content_function_enabled() -> bool { + static_prefs::pref!("layout.css.fit-content-function.enabled") +} +#[cfg(feature = "servo")] +fn is_fit_content_function_enabled() -> bool { + false +} + +macro_rules! parse_fit_content_function { + ($size:ident, $input:expr, $context:expr, $allow_quirks:expr) => { + if is_fit_content_function_enabled() { + if let Ok(length) = $input.try_parse(|input| { + input.expect_function_matching("fit-content")?; + input.parse_nested_block(|i| { + NonNegativeLengthPercentage::parse_quirky($context, i, $allow_quirks) + }) + }) { + return Ok($size::FitContentFunction(length)); + } + } + }; +} + +impl Size { + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + parse_size_non_length!(Size, input, "auto" => Auto); + parse_fit_content_function!(Size, input, context, allow_quirks); + + let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?; + Ok(GenericSize::LengthPercentage(length)) + } + + /// Returns `0%`. + #[inline] + pub fn zero_percent() -> Self { + GenericSize::LengthPercentage(NonNegativeLengthPercentage::zero_percent()) + } +} + +/// A specified value for `max-width` or `max-height` property. +pub type MaxSize = GenericMaxSize<NonNegativeLengthPercentage>; + +impl Parse for MaxSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + MaxSize::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl MaxSize { + /// Parses, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + parse_size_non_length!(MaxSize, input, "none" => None); + parse_fit_content_function!(MaxSize, input, context, allow_quirks); + + let length = NonNegativeLengthPercentage::parse_quirky(context, input, allow_quirks)?; + Ok(GenericMaxSize::LengthPercentage(length)) + } +} + +/// A specified non-negative `<length>` | `<number>`. +pub type NonNegativeLengthOrNumber = GenericLengthOrNumber<NonNegativeLength, NonNegativeNumber>; diff --git a/servo/components/style/values/specified/list.rs b/servo/components/style/values/specified/list.rs new file mode 100644 index 0000000000..693471e478 --- /dev/null +++ b/servo/components/style/values/specified/list.rs @@ -0,0 +1,202 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! `list` specified values. + +use crate::parser::{Parse, ParserContext}; +#[cfg(feature = "gecko")] +use crate::values::generics::CounterStyle; +#[cfg(feature = "gecko")] +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// Specified and computed `list-style-type` property. +#[cfg(feature = "gecko")] +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum ListStyleType { + /// `none` + None, + /// <counter-style> + CounterStyle(CounterStyle), + /// <string> + String(String), +} + +#[cfg(feature = "gecko")] +impl ListStyleType { + /// Initial specified value for `list-style-type`. + #[inline] + pub fn disc() -> Self { + ListStyleType::CounterStyle(CounterStyle::disc()) + } + + /// Convert from gecko keyword to list-style-type. + /// + /// This should only be used for mapping type attribute to + /// list-style-type, and thus only values possible in that + /// attribute is considered here. + pub fn from_gecko_keyword(value: u32) -> Self { + use crate::gecko_bindings::structs; + let v8 = value as u8; + + if v8 == structs::ListStyle_None { + return ListStyleType::None; + } + + ListStyleType::CounterStyle(CounterStyle::Name(CustomIdent(match v8 { + structs::ListStyle_Disc => atom!("disc"), + structs::ListStyle_Circle => atom!("circle"), + structs::ListStyle_Square => atom!("square"), + structs::ListStyle_Decimal => atom!("decimal"), + structs::ListStyle_LowerRoman => atom!("lower-roman"), + structs::ListStyle_UpperRoman => atom!("upper-roman"), + structs::ListStyle_LowerAlpha => atom!("lower-alpha"), + structs::ListStyle_UpperAlpha => atom!("upper-alpha"), + _ => unreachable!("Unknown counter style keyword value"), + }))) + } + + /// Is this a bullet? (i.e. `list-style-type: disc|circle|square|disclosure-closed|disclosure-open`) + #[inline] + pub fn is_bullet(&self) -> bool { + match self { + ListStyleType::CounterStyle(ref style) => style.is_bullet(), + _ => false, + } + } +} + +#[cfg(feature = "gecko")] +impl Parse for ListStyleType { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(style) = input.try_parse(|i| CounterStyle::parse(context, i)) { + return Ok(ListStyleType::CounterStyle(style)); + } + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ListStyleType::None); + } + Ok(ListStyleType::String( + input.expect_string()?.as_ref().to_owned(), + )) + } +} + +/// A quote pair. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct QuotePair { + /// The opening quote. + pub opening: crate::OwnedStr, + + /// The closing quote. + pub closing: crate::OwnedStr, +} + +/// List of quote pairs for the specified/computed value of `quotes` property. +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct QuoteList( + #[css(iterable, if_empty = "none")] + #[ignore_malloc_size_of = "Arc"] + pub crate::ArcSlice<QuotePair>, +); + +/// Specified and computed `quotes` property: `auto`, `none`, or a list +/// of characters. +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub enum Quotes { + /// list of quote pairs + QuoteList(QuoteList), + /// auto (use lang-dependent quote marks) + Auto, +} + +impl Parse for Quotes { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Quotes, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("auto")) + .is_ok() + { + return Ok(Quotes::Auto); + } + + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(Quotes::QuoteList(QuoteList::default())); + } + + let mut quotes = Vec::new(); + loop { + let location = input.current_source_location(); + let opening = match input.next() { + Ok(&Token::QuotedString(ref value)) => value.as_ref().to_owned().into(), + Ok(t) => return Err(location.new_unexpected_token_error(t.clone())), + Err(_) => break, + }; + + let closing = input.expect_string()?.as_ref().to_owned().into(); + quotes.push(QuotePair { opening, closing }); + } + + if !quotes.is_empty() { + Ok(Quotes::QuoteList(QuoteList(crate::ArcSlice::from_iter( + quotes.into_iter(), + )))) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} diff --git a/servo/components/style/values/specified/mod.rs b/servo/components/style/values/specified/mod.rs new file mode 100644 index 0000000000..7fc76b3c07 --- /dev/null +++ b/servo/components/style/values/specified/mod.rs @@ -0,0 +1,992 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified values. +//! +//! TODO(emilio): Enhance docs. + +use super::computed::transform::DirectionVector; +use super::computed::{Context, ToComputedValue}; +use super::generics::grid::ImplicitGridTracks as GenericImplicitGridTracks; +use super::generics::grid::{GridLine as GenericGridLine, TrackBreadth as GenericTrackBreadth}; +use super::generics::grid::{TrackList as GenericTrackList, TrackSize as GenericTrackSize}; +use super::generics::transform::IsParallelTo; +use super::generics::{self, GreaterThanOrEqualToOne, NonNegative}; +use super::{CSSFloat, CSSInteger}; +use crate::context::QuirksMode; +use crate::parser::{Parse, ParserContext}; +use crate::values::specified::calc::CalcNode; +use crate::values::{serialize_atom_identifier, serialize_number, AtomString}; +use crate::{Atom, Namespace, One, Prefix, Zero}; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use std::ops::Add; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +#[cfg(feature = "gecko")] +pub use self::align::{AlignContent, AlignItems, AlignSelf, AlignTracks, ContentDistribution}; +#[cfg(feature = "gecko")] +pub use self::align::{JustifyContent, JustifyItems, JustifySelf, JustifyTracks, SelfAlignment}; +pub use self::angle::{AllowUnitlessZeroAngle, Angle}; +pub use self::animation::{ + AnimationIterationCount, AnimationName, AnimationTimeline, AnimationPlayState, + AnimationFillMode, AnimationComposition, AnimationDirection, ScrollAxis, + ScrollTimelineName, TransitionProperty, ViewTimelineInset +}; +pub use self::background::{BackgroundRepeat, BackgroundSize}; +pub use self::basic_shape::FillRule; +pub use self::border::{ + BorderCornerRadius, BorderImageRepeat, BorderImageSideWidth, BorderImageSlice, + BorderImageWidth, BorderRadius, BorderSideWidth, BorderSpacing, BorderStyle, LineWidth, +}; +pub use self::box_::{ + Appearance, BaselineSource, BreakBetween, BreakWithin, Clear, Contain, ContainIntrinsicSize, + ContainerName, ContainerType, ContentVisibility, Display, Float, LineClamp, Overflow, + OverflowAnchor, OverflowClipBox, OverscrollBehavior, Perspective, Resize, ScrollSnapAlign, + ScrollSnapAxis, ScrollSnapStop, ScrollSnapStrictness, ScrollSnapType, ScrollbarGutter, + TouchAction, VerticalAlign, WillChange, Zoom, +}; +pub use self::color::{ + Color, ColorOrAuto, ColorPropertyValue, ColorScheme, ForcedColorAdjust, PrintColorAdjust, +}; +pub use self::column::ColumnCount; +pub use self::counters::{Content, ContentItem, CounterIncrement, CounterReset, CounterSet}; +pub use self::easing::TimingFunction; +pub use self::effects::{BoxShadow, Filter, SimpleShadow}; +pub use self::flex::FlexBasis; +pub use self::font::{FontFamily, FontLanguageOverride, FontPalette, FontStyle}; +pub use self::font::{FontFeatureSettings, FontVariantLigatures, FontVariantNumeric}; +pub use self::font::{ + FontSize, FontSizeAdjust, FontSizeAdjustFactor, FontSizeKeyword, FontStretch, FontSynthesis, +}; +pub use self::font::{FontVariantAlternates, FontWeight}; +pub use self::font::{FontVariantEastAsian, FontVariationSettings, LineHeight}; +pub use self::font::{MathDepth, MozScriptMinSize, MozScriptSizeMultiplier, XLang, XTextScale}; +pub use self::image::{EndingShape as GradientEndingShape, Gradient, Image, ImageRendering}; +pub use self::length::{AbsoluteLength, CalcLengthPercentage, CharacterWidth}; +pub use self::length::{FontRelativeLength, Length, LengthOrNumber, NonNegativeLengthOrNumber}; +pub use self::length::{LengthOrAuto, LengthPercentage, LengthPercentageOrAuto}; +pub use self::length::{MaxSize, Size}; +pub use self::length::{NoCalcLength, ViewportPercentageLength, ViewportVariant}; +pub use self::length::{ + NonNegativeLength, NonNegativeLengthPercentage, NonNegativeLengthPercentageOrAuto, +}; +#[cfg(feature = "gecko")] +pub use self::list::ListStyleType; +pub use self::list::Quotes; +pub use self::motion::{OffsetPath, OffsetPosition, OffsetRotate}; +pub use self::outline::OutlineStyle; +pub use self::page::{PageName, PageOrientation, PageSize, PageSizeOrientation, PaperSize}; +pub use self::percentage::{NonNegativePercentage, Percentage}; +pub use self::position::AspectRatio; +pub use self::position::{GridAutoFlow, GridTemplateAreas, Position, PositionOrAuto}; +pub use self::position::{MasonryAutoFlow, MasonryItemOrder, MasonryPlacement}; +pub use self::position::{PositionComponent, ZIndex}; +pub use self::ratio::Ratio; +pub use self::rect::NonNegativeLengthOrNumberRect; +pub use self::resolution::Resolution; +pub use self::svg::{DProperty, MozContextProperties}; +pub use self::svg::{SVGLength, SVGOpacity, SVGPaint}; +pub use self::svg::{SVGPaintOrder, SVGStrokeDashArray, SVGWidth}; +pub use self::svg_path::SVGPathData; +pub use self::text::HyphenateCharacter; +pub use self::text::RubyPosition; +pub use self::text::TextAlignLast; +pub use self::text::TextUnderlinePosition; +pub use self::text::{InitialLetter, LetterSpacing, LineBreak, TextAlign, TextIndent}; +pub use self::text::{OverflowWrap, TextEmphasisPosition, TextEmphasisStyle, WordBreak}; +pub use self::text::{TextAlignKeyword, TextDecorationLine, TextOverflow, WordSpacing}; +pub use self::text::{TextDecorationLength, TextDecorationSkipInk, TextJustify, TextTransform}; +pub use self::time::Time; +pub use self::transform::{Rotate, Scale, Transform}; +pub use self::transform::{TransformBox, TransformOrigin, TransformStyle, Translate}; +#[cfg(feature = "gecko")] +pub use self::ui::CursorImage; +pub use self::ui::{BoolInteger, Cursor, UserSelect}; +pub use super::generics::grid::GridTemplateComponent as GenericGridTemplateComponent; + +#[cfg(feature = "gecko")] +pub mod align; +pub mod angle; +pub mod animation; +pub mod background; +pub mod basic_shape; +pub mod border; +#[path = "box.rs"] +pub mod box_; +pub mod calc; +pub mod color; +pub mod column; +pub mod counters; +pub mod easing; +pub mod effects; +pub mod flex; +pub mod font; +#[cfg(feature = "gecko")] +pub mod gecko; +pub mod grid; +pub mod image; +pub mod length; +pub mod list; +pub mod motion; +pub mod outline; +pub mod page; +pub mod percentage; +pub mod position; +pub mod ratio; +pub mod rect; +pub mod resolution; +pub mod source_size_list; +pub mod svg; +pub mod svg_path; +pub mod table; +pub mod text; +pub mod time; +pub mod transform; +pub mod ui; +pub mod url; + +/// <angle> | <percentage> +/// https://drafts.csswg.org/css-values/#typedef-angle-percentage +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum AngleOrPercentage { + Percentage(Percentage), + Angle(Angle), +} + +impl AngleOrPercentage { + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_unitless_zero: AllowUnitlessZeroAngle, + ) -> Result<Self, ParseError<'i>> { + if let Ok(per) = input.try_parse(|i| Percentage::parse(context, i)) { + return Ok(AngleOrPercentage::Percentage(per)); + } + + Angle::parse_internal(context, input, allow_unitless_zero).map(AngleOrPercentage::Angle) + } + + /// Allow unitless angles, used for conic-gradients as specified by the spec. + /// https://drafts.csswg.org/css-images-4/#valdef-conic-gradient-angle + pub fn parse_with_unitless<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::Yes) + } +} + +impl Parse for AngleOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + AngleOrPercentage::parse_internal(context, input, AllowUnitlessZeroAngle::No) + } +} + +/// Parse a `<number>` value, with a given clamping mode. +fn parse_number_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, +) -> Result<Number, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { value, .. } if clamping_mode.is_ok(context.parsing_mode, value) => { + Ok(Number { + value, + calc_clamping_mode: None, + }) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let value = CalcNode::parse_number(context, input, function)?; + Ok(Number { + value, + calc_clamping_mode: Some(clamping_mode), + }) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } +} + +/// A CSS `<number>` specified value. +/// +/// https://drafts.csswg.org/css-values-3/#number-value +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialOrd, ToShmem)] +pub struct Number { + /// The numeric value itself. + value: CSSFloat, + /// If this number came from a calc() expression, this tells how clamping + /// should be done on the value. + calc_clamping_mode: Option<AllowedNumericType>, +} + +impl Parse for Number { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl PartialEq<Number> for Number { + fn eq(&self, other: &Number) -> bool { + if self.calc_clamping_mode != other.calc_clamping_mode { + return false; + } + + self.value == other.value || (self.value.is_nan() && other.value.is_nan()) + } +} + +impl Number { + /// Returns a new number with the value `val`. + #[inline] + fn new_with_clamping_mode( + value: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Self { + value, + calc_clamping_mode, + } + } + + /// Returns this percentage as a number. + pub fn to_percentage(&self) -> Percentage { + Percentage::new_with_clamping_mode(self.value, self.calc_clamping_mode) + } + + /// Returns a new number with the value `val`. + #[inline] + pub fn new(val: CSSFloat) -> Self { + Self::new_with_clamping_mode(val, None) + } + + /// Returns whether this number came from a `calc()` expression. + #[inline] + pub fn was_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + /// Returns the numeric value, clamped if needed. + #[inline] + pub fn get(&self) -> f32 { + crate::values::normalize( + self.calc_clamping_mode + .map_or(self.value, |mode| mode.clamp(self.value)), + ) + .min(f32::MAX) + .max(f32::MIN) + } + + #[allow(missing_docs)] + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Number, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + #[allow(missing_docs)] + pub fn parse_at_least_one<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Number, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) + } + + /// Clamp to 1.0 if the value is over 1.0. + #[inline] + pub fn clamp_to_one(self) -> Self { + Number { + value: self.value.min(1.), + calc_clamping_mode: self.calc_clamping_mode, + } + } +} + +impl ToComputedValue for Number { + type ComputedValue = CSSFloat; + + #[inline] + fn to_computed_value(&self, _: &Context) -> CSSFloat { + self.get() + } + + #[inline] + fn from_computed_value(computed: &CSSFloat) -> Self { + Number { + value: *computed, + calc_clamping_mode: None, + } + } +} + +impl ToCss for Number { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + serialize_number(self.value, self.calc_clamping_mode.is_some(), dest) + } +} + +impl IsParallelTo for (Number, Number, Number) { + fn is_parallel_to(&self, vector: &DirectionVector) -> bool { + use euclid::approxeq::ApproxEq; + // If a and b is parallel, the angle between them is 0deg, so + // a x b = |a|*|b|*sin(0)*n = 0 * n, |a x b| == 0. + let self_vector = DirectionVector::new(self.0.get(), self.1.get(), self.2.get()); + self_vector + .cross(*vector) + .square_length() + .approx_eq(&0.0f32) + } +} + +impl SpecifiedValueInfo for Number {} + +impl Add for Number { + type Output = Self; + + fn add(self, other: Self) -> Self { + Self::new(self.get() + other.get()) + } +} + +impl Zero for Number { + #[inline] + fn zero() -> Self { + Self::new(0.) + } + + #[inline] + fn is_zero(&self) -> bool { + self.get() == 0. + } +} + +impl From<Number> for f32 { + #[inline] + fn from(n: Number) -> Self { + n.get() + } +} + +impl From<Number> for f64 { + #[inline] + fn from(n: Number) -> Self { + n.get() as f64 + } +} + +/// A Number which is >= 0.0. +pub type NonNegativeNumber = NonNegative<Number>; + +impl Parse for NonNegativeNumber { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + .map(NonNegative::<Number>) + } +} + +impl One for NonNegativeNumber { + #[inline] + fn one() -> Self { + NonNegativeNumber::new(1.0) + } + + #[inline] + fn is_one(&self) -> bool { + self.get() == 1.0 + } +} + +impl NonNegativeNumber { + /// Returns a new non-negative number with the value `val`. + pub fn new(val: CSSFloat) -> Self { + NonNegative::<Number>(Number::new(val.max(0.))) + } + + /// Returns the numeric value. + #[inline] + pub fn get(&self) -> f32 { + self.0.get() + } +} + +/// An Integer which is >= 0. +pub type NonNegativeInteger = NonNegative<Integer>; + +impl Parse for NonNegativeInteger { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Integer::parse_non_negative(context, input)?)) + } +} + +/// A Number which is >= 1.0. +pub type GreaterThanOrEqualToOneNumber = GreaterThanOrEqualToOne<Number>; + +impl Parse for GreaterThanOrEqualToOneNumber { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + parse_number_with_clamping_mode(context, input, AllowedNumericType::AtLeastOne) + .map(GreaterThanOrEqualToOne::<Number>) + } +} + +/// <number> | <percentage> +/// +/// Accepts only non-negative numbers. +#[allow(missing_docs)] +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum NumberOrPercentage { + Percentage(Percentage), + Number(Number), +} + +impl NumberOrPercentage { + fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + type_: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + if let Ok(per) = + input.try_parse(|i| Percentage::parse_with_clamping_mode(context, i, type_)) + { + return Ok(NumberOrPercentage::Percentage(per)); + } + + parse_number_with_clamping_mode(context, input, type_).map(NumberOrPercentage::Number) + } + + /// Parse a non-negative number or percentage. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + /// Convert the number or the percentage to a number. + pub fn to_percentage(self) -> Percentage { + match self { + Self::Percentage(p) => p, + Self::Number(n) => n.to_percentage(), + } + } + + /// Convert the number or the percentage to a number. + pub fn to_number(self) -> Number { + match self { + Self::Percentage(p) => p.to_number(), + Self::Number(n) => n, + } + } +} + +impl Parse for NumberOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +/// A non-negative <number> | <percentage>. +pub type NonNegativeNumberOrPercentage = NonNegative<NumberOrPercentage>; + +impl NonNegativeNumberOrPercentage { + /// Returns the `100%` value. + #[inline] + pub fn hundred_percent() -> Self { + NonNegative(NumberOrPercentage::Percentage(Percentage::hundred())) + } + + /// Return a particular number. + #[inline] + pub fn new_number(n: f32) -> Self { + NonNegative(NumberOrPercentage::Number(Number::new(n))) + } +} + +impl Parse for NonNegativeNumberOrPercentage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(NumberOrPercentage::parse_non_negative( + context, input, + )?)) + } +} + +/// The value of Opacity is <alpha-value>, which is "<number> | <percentage>". +/// However, we serialize the specified value as number, so it's ok to store +/// the Opacity as Number. +#[derive( + Clone, Copy, Debug, MallocSizeOf, PartialEq, PartialOrd, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub struct Opacity(Number); + +impl Parse for Opacity { + /// Opacity accepts <number> | <percentage>, so we parse it as NumberOrPercentage, + /// and then convert into an Number if it's a Percentage. + /// https://drafts.csswg.org/cssom/#serializing-css-values + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let number = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(Opacity(number)) + } +} + +impl ToComputedValue for Opacity { + type ComputedValue = CSSFloat; + + #[inline] + fn to_computed_value(&self, context: &Context) -> CSSFloat { + let value = self.0.to_computed_value(context); + if context.for_smil_animation { + // SMIL expects to be able to interpolate between out-of-range + // opacity values. + value + } else { + value.min(1.0).max(0.0) + } + } + + #[inline] + fn from_computed_value(computed: &CSSFloat) -> Self { + Opacity(Number::from_computed_value(computed)) + } +} + +/// A specified `<integer>`, optionally coming from a `calc()` expression. +/// +/// <https://drafts.csswg.org/css-values/#integers> +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, PartialOrd, ToShmem)] +pub struct Integer { + value: CSSInteger, + was_calc: bool, +} + +impl Zero for Integer { + #[inline] + fn zero() -> Self { + Self::new(0) + } + + #[inline] + fn is_zero(&self) -> bool { + self.value() == 0 + } +} + +impl One for Integer { + #[inline] + fn one() -> Self { + Self::new(1) + } + + #[inline] + fn is_one(&self) -> bool { + self.value() == 1 + } +} + +impl PartialEq<i32> for Integer { + fn eq(&self, value: &i32) -> bool { + self.value() == *value + } +} + +impl Integer { + /// Trivially constructs a new `Integer` value. + pub fn new(val: CSSInteger) -> Self { + Integer { + value: val, + was_calc: false, + } + } + + /// Returns the integer value associated with this value. + pub fn value(&self) -> CSSInteger { + self.value + } + + /// Trivially constructs a new integer value from a `calc()` expression. + fn from_calc(val: CSSInteger) -> Self { + Integer { + value: val, + was_calc: true, + } + } +} + +impl Parse for Integer { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Number { + int_value: Some(v), .. + } => Ok(Integer::new(v)), + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let result = CalcNode::parse_integer(context, input, function)?; + Ok(Integer::from_calc(result)) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } +} + +impl Integer { + /// Parse an integer value which is at least `min`. + pub fn parse_with_minimum<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + min: i32, + ) -> Result<Integer, ParseError<'i>> { + let value = Integer::parse(context, input)?; + // FIXME(emilio): The spec asks us to avoid rejecting it at parse + // time except until computed value time. + // + // It's not totally clear it's worth it though, and no other browser + // does this. + if value.value() < min { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(value) + } + + /// Parse a non-negative integer. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Integer, ParseError<'i>> { + Integer::parse_with_minimum(context, input, 0) + } + + /// Parse a positive integer (>= 1). + pub fn parse_positive<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Integer, ParseError<'i>> { + Integer::parse_with_minimum(context, input, 1) + } +} + +impl ToComputedValue for Integer { + type ComputedValue = i32; + + #[inline] + fn to_computed_value(&self, _: &Context) -> i32 { + self.value + } + + #[inline] + fn from_computed_value(computed: &i32) -> Self { + Integer::new(*computed) + } +} + +impl ToCss for Integer { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.was_calc { + dest.write_str("calc(")?; + } + self.value.to_css(dest)?; + if self.was_calc { + dest.write_char(')')?; + } + Ok(()) + } +} + +impl SpecifiedValueInfo for Integer {} + +/// A wrapper of Integer, with value >= 1. +pub type PositiveInteger = GreaterThanOrEqualToOne<Integer>; + +impl Parse for PositiveInteger { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Integer::parse_positive(context, input).map(GreaterThanOrEqualToOne) + } +} + +/// The specified value of a grid `<track-breadth>` +pub type TrackBreadth = GenericTrackBreadth<LengthPercentage>; + +/// The specified value of a grid `<track-size>` +pub type TrackSize = GenericTrackSize<LengthPercentage>; + +/// The specified value of a grid `<track-size>+` +pub type ImplicitGridTracks = GenericImplicitGridTracks<TrackSize>; + +/// The specified value of a grid `<track-list>` +/// (could also be `<auto-track-list>` or `<explicit-track-list>`) +pub type TrackList = GenericTrackList<LengthPercentage, Integer>; + +/// The specified value of a `<grid-line>`. +pub type GridLine = GenericGridLine<Integer>; + +/// `<grid-template-rows> | <grid-template-columns>` +pub type GridTemplateComponent = GenericGridTemplateComponent<LengthPercentage, Integer>; + +/// rect(...) +pub type ClipRect = generics::GenericClipRect<LengthOrAuto>; + +impl Parse for ClipRect { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl ClipRect { + /// Parses a rect(<top>, <left>, <bottom>, <right>), allowing quirks. + fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + input.expect_function_matching("rect")?; + + fn parse_argument<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<LengthOrAuto, ParseError<'i>> { + LengthOrAuto::parse_quirky(context, input, allow_quirks) + } + + input.parse_nested_block(|input| { + let top = parse_argument(context, input, allow_quirks)?; + let right; + let bottom; + let left; + + if input.try_parse(|input| input.expect_comma()).is_ok() { + right = parse_argument(context, input, allow_quirks)?; + input.expect_comma()?; + bottom = parse_argument(context, input, allow_quirks)?; + input.expect_comma()?; + left = parse_argument(context, input, allow_quirks)?; + } else { + right = parse_argument(context, input, allow_quirks)?; + bottom = parse_argument(context, input, allow_quirks)?; + left = parse_argument(context, input, allow_quirks)?; + } + + Ok(ClipRect { + top, + right, + bottom, + left, + }) + }) + } +} + +/// rect(...) | auto +pub type ClipRectOrAuto = generics::GenericClipRectOrAuto<ClipRect>; + +impl ClipRectOrAuto { + /// Parses a ClipRect or Auto, allowing quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if let Ok(v) = input.try_parse(|i| ClipRect::parse_quirky(context, i, allow_quirks)) { + return Ok(generics::GenericClipRectOrAuto::Rect(v)); + } + input.expect_ident_matching("auto")?; + Ok(generics::GenericClipRectOrAuto::Auto) + } +} + +/// Whether quirks are allowed in this context. +#[derive(Clone, Copy, PartialEq)] +pub enum AllowQuirks { + /// Quirks are not allowed. + No, + /// Quirks are allowed, in quirks mode. + Yes, + /// Quirks are always allowed, used for SVG lengths. + Always, +} + +impl AllowQuirks { + /// Returns `true` if quirks are allowed in this context. + pub fn allowed(self, quirks_mode: QuirksMode) -> bool { + match self { + AllowQuirks::Always => true, + AllowQuirks::No => false, + AllowQuirks::Yes => quirks_mode == QuirksMode::Quirks, + } + } +} + +/// An attr(...) rule +/// +/// `[namespace? `|`]? ident` +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(function)] +#[repr(C)] +pub struct Attr { + /// Optional namespace prefix. + pub namespace_prefix: Prefix, + /// Optional namespace URL. + pub namespace_url: Namespace, + /// Attribute name + pub attribute: Atom, + /// Fallback value + pub fallback: AtomString, +} + +impl Parse for Attr { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Attr, ParseError<'i>> { + input.expect_function_matching("attr")?; + input.parse_nested_block(|i| Attr::parse_function(context, i)) + } +} + +/// Get the Namespace for a given prefix from the namespace map. +fn get_namespace_for_prefix(prefix: &Prefix, context: &ParserContext) -> Option<Namespace> { + context.namespaces.prefixes.get(prefix).cloned() +} + +/// Try to parse a namespace and return it if parsed, or none if there was not one present +fn parse_namespace<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, +) -> Result<(Prefix, Namespace), ParseError<'i>> { + let ns_prefix = match input.next()? { + Token::Ident(ref prefix) => Some(Prefix::from(prefix.as_ref())), + Token::Delim('|') => None, + _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }; + + if ns_prefix.is_some() && !matches!(*input.next_including_whitespace()?, Token::Delim('|')) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + if let Some(prefix) = ns_prefix { + let ns = match get_namespace_for_prefix(&prefix, context) { + Some(ns) => ns, + None => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }; + Ok((prefix, ns)) + } else { + Ok((Prefix::default(), Namespace::default())) + } +} + +impl Attr { + /// Parse contents of attr() assuming we have already parsed `attr` and are + /// within a parse_nested_block() + pub fn parse_function<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Attr, ParseError<'i>> { + // Syntax is `[namespace? '|']? ident [',' fallback]?` + let namespace = input + .try_parse(|input| parse_namespace(context, input)) + .ok(); + let namespace_is_some = namespace.is_some(); + let (namespace_prefix, namespace_url) = namespace.unwrap_or_default(); + + // If there is a namespace, ensure no whitespace following '|' + let attribute = Atom::from(if namespace_is_some { + let location = input.current_source_location(); + match *input.next_including_whitespace()? { + Token::Ident(ref ident) => ident.as_ref(), + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } else { + input.expect_ident()?.as_ref() + }); + + // Fallback will always be a string value for now as we do not support + // attr() types yet. + let fallback = input + .try_parse(|input| -> Result<AtomString, ParseError<'i>> { + input.expect_comma()?; + Ok(input.expect_string()?.as_ref().into()) + }) + .unwrap_or_default(); + + Ok(Attr { + namespace_prefix, + namespace_url, + attribute, + fallback, + }) + } +} + +impl ToCss for Attr { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str("attr(")?; + if !self.namespace_prefix.is_empty() { + serialize_atom_identifier(&self.namespace_prefix, dest)?; + dest.write_char('|')?; + } + serialize_atom_identifier(&self.attribute, dest)?; + + if !self.fallback.is_empty() { + dest.write_str(", ")?; + self.fallback.to_css(dest)?; + } + + dest.write_char(')') + } +} diff --git a/servo/components/style/values/specified/motion.rs b/servo/components/style/values/specified/motion.rs new file mode 100644 index 0000000000..98858c712c --- /dev/null +++ b/servo/components/style/values/specified/motion.rs @@ -0,0 +1,343 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values that are related to motion path. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::motion::OffsetRotate as ComputedOffsetRotate; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::motion as generics; +use crate::values::specified::basic_shape::BasicShape; +use crate::values::specified::position::{HorizontalPosition, VerticalPosition}; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::{Angle, Position}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +/// The specified value of ray() function. +pub type RayFunction = generics::GenericRayFunction<Angle, Position>; + +/// The specified value of <offset-path>. +pub type OffsetPathFunction = + generics::GenericOffsetPathFunction<BasicShape, RayFunction, SpecifiedUrl>; + +/// The specified value of `offset-path`. +pub type OffsetPath = generics::GenericOffsetPath<OffsetPathFunction>; + +/// The specified value of `offset-position`. +pub type OffsetPosition = generics::GenericOffsetPosition<HorizontalPosition, VerticalPosition>; + +/// The <coord-box> value, which defines the box that the <offset-path> sizes into. +/// https://drafts.fxtf.org/motion-1/#valdef-offset-path-coord-box +/// +/// <coord-box> = content-box | padding-box | border-box | fill-box | stroke-box | view-box +/// https://drafts.csswg.org/css-box-4/#typedef-coord-box +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum CoordBox { + ContentBox, + PaddingBox, + BorderBox, + FillBox, + StrokeBox, + ViewBox, +} + +impl CoordBox { + /// Returns true if it is default value, border-box. + #[inline] + pub fn is_default(&self) -> bool { + matches!(*self, Self::BorderBox) + } +} + +impl Parse for RayFunction { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if !static_prefs::pref!("layout.css.motion-path-ray.enabled") { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + input.expect_function_matching("ray")?; + input.parse_nested_block(|i| Self::parse_function_arguments(context, i)) + } +} + +impl RayFunction { + /// Parse the inner arguments of a `ray` function. + fn parse_function_arguments<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::PositionOrAuto; + + let mut angle = None; + let mut size = None; + let mut contain = false; + let mut position = None; + loop { + if angle.is_none() { + angle = input.try_parse(|i| Angle::parse(context, i)).ok(); + } + + if size.is_none() { + size = input.try_parse(generics::RaySize::parse).ok(); + if size.is_some() { + continue; + } + } + + if !contain { + contain = input + .try_parse(|i| i.expect_ident_matching("contain")) + .is_ok(); + if contain { + continue; + } + } + + if position.is_none() { + if input.try_parse(|i| i.expect_ident_matching("at")).is_ok() { + let pos = Position::parse(context, input)?; + position = Some(PositionOrAuto::Position(pos)); + } + + if position.is_some() { + continue; + } + } + break; + } + + if angle.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(RayFunction { + angle: angle.unwrap(), + // If no <ray-size> is specified it defaults to closest-side. + size: size.unwrap_or(generics::RaySize::ClosestSide), + contain, + position: position.unwrap_or(PositionOrAuto::auto()), + }) + } +} + +impl Parse for OffsetPathFunction { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::specified::basic_shape::{AllowedBasicShapes, ShapeType}; + + // <offset-path> = <ray()> | <url> | <basic-shape> + // https://drafts.fxtf.org/motion-1/#typedef-offset-path + + if static_prefs::pref!("layout.css.motion-path-ray.enabled") { + if let Ok(ray) = input.try_parse(|i| RayFunction::parse(context, i)) { + return Ok(OffsetPathFunction::Ray(ray)); + } + } + + if static_prefs::pref!("layout.css.motion-path-url.enabled") { + if let Ok(url) = input.try_parse(|i| SpecifiedUrl::parse(context, i)) { + return Ok(OffsetPathFunction::Url(url)); + } + } + + let allowed_shapes = if static_prefs::pref!("layout.css.motion-path-basic-shapes.enabled") { + AllowedBasicShapes::ALL + } else { + AllowedBasicShapes::PATH + }; + + BasicShape::parse(context, input, allowed_shapes, ShapeType::Outline) + .map(OffsetPathFunction::Shape) + } +} + +impl Parse for OffsetPath { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Parse none. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(OffsetPath::none()); + } + + let mut path = None; + let mut coord_box = None; + loop { + if path.is_none() { + path = input + .try_parse(|i| OffsetPathFunction::parse(context, i)) + .ok(); + } + + if static_prefs::pref!("layout.css.motion-path-coord-box.enabled") && + coord_box.is_none() + { + coord_box = input.try_parse(CoordBox::parse).ok(); + if coord_box.is_some() { + continue; + } + } + break; + } + + if let Some(p) = path { + return Ok(OffsetPath::OffsetPath { + path: Box::new(p), + coord_box: coord_box.unwrap_or(CoordBox::BorderBox), + }); + } + + match coord_box { + Some(c) => Ok(OffsetPath::CoordBox(c)), + None => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} + +/// The direction of offset-rotate. +#[derive( + Clone, Copy, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +#[repr(u8)] +pub enum OffsetRotateDirection { + /// Unspecified direction keyword. + #[css(skip)] + None, + /// 0deg offset (face forward). + Auto, + /// 180deg offset (face backward). + Reverse, +} + +impl OffsetRotateDirection { + /// Returns true if it is none (i.e. the keyword is not specified). + #[inline] + fn is_none(&self) -> bool { + *self == OffsetRotateDirection::None + } +} + +#[inline] +fn direction_specified_and_angle_is_zero(direction: &OffsetRotateDirection, angle: &Angle) -> bool { + !direction.is_none() && angle.is_zero() +} + +/// The specified offset-rotate. +/// The syntax is: "[ auto | reverse ] || <angle>" +/// +/// https://drafts.fxtf.org/motion-1/#offset-rotate-property +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub struct OffsetRotate { + /// [auto | reverse]. + #[css(skip_if = "OffsetRotateDirection::is_none")] + direction: OffsetRotateDirection, + /// <angle>. + /// If direction is None, this is a fixed angle which indicates a + /// constant clockwise rotation transformation applied to it by this + /// specified rotation angle. Otherwise, the angle will be added to + /// the angle of the direction in layout. + #[css(contextual_skip_if = "direction_specified_and_angle_is_zero")] + angle: Angle, +} + +impl OffsetRotate { + /// Returns the initial value, auto. + #[inline] + pub fn auto() -> Self { + OffsetRotate { + direction: OffsetRotateDirection::Auto, + angle: Angle::zero(), + } + } + + /// Returns true if self is auto 0deg. + #[inline] + pub fn is_auto(&self) -> bool { + self.direction == OffsetRotateDirection::Auto && self.angle.is_zero() + } +} + +impl Parse for OffsetRotate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let mut direction = input.try_parse(OffsetRotateDirection::parse); + let angle = input.try_parse(|i| Angle::parse(context, i)); + if direction.is_err() { + // The direction and angle could be any order, so give it a change to parse + // direction again. + direction = input.try_parse(OffsetRotateDirection::parse); + } + + if direction.is_err() && angle.is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(OffsetRotate { + direction: direction.unwrap_or(OffsetRotateDirection::None), + angle: angle.unwrap_or(Zero::zero()), + }) + } +} + +impl ToComputedValue for OffsetRotate { + type ComputedValue = ComputedOffsetRotate; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + use crate::values::computed::Angle as ComputedAngle; + + ComputedOffsetRotate { + auto: !self.direction.is_none(), + angle: if self.direction == OffsetRotateDirection::Reverse { + // The computed value should always convert "reverse" into "auto". + // e.g. "reverse calc(20deg + 10deg)" => "auto 210deg" + self.angle.to_computed_value(context) + ComputedAngle::from_degrees(180.0) + } else { + self.angle.to_computed_value(context) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + OffsetRotate { + direction: if computed.auto { + OffsetRotateDirection::Auto + } else { + OffsetRotateDirection::None + }, + angle: ToComputedValue::from_computed_value(&computed.angle), + } + } +} diff --git a/servo/components/style/values/specified/outline.rs b/servo/components/style/values/specified/outline.rs new file mode 100644 index 0000000000..6e5382d4c2 --- /dev/null +++ b/servo/components/style/values/specified/outline.rs @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified values for outline properties + +use crate::parser::{Parse, ParserContext}; +use crate::values::specified::BorderStyle; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use style_traits::ParseError; + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Ord, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +/// <https://drafts.csswg.org/css-ui/#propdef-outline-style> +pub enum OutlineStyle { + /// auto + Auto, + /// <border-style> + BorderStyle(BorderStyle), +} + +impl OutlineStyle { + #[inline] + /// Get default value as None + pub fn none() -> OutlineStyle { + OutlineStyle::BorderStyle(BorderStyle::None) + } + + #[inline] + /// Get value for None or Hidden + pub fn none_or_hidden(&self) -> bool { + match *self { + OutlineStyle::Auto => false, + OutlineStyle::BorderStyle(ref style) => style.none_or_hidden(), + } + } +} + +impl Parse for OutlineStyle { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<OutlineStyle, ParseError<'i>> { + if let Ok(border_style) = input.try_parse(BorderStyle::parse) { + if let BorderStyle::Hidden = border_style { + return Err(input + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent("hidden".into()))); + } + + return Ok(OutlineStyle::BorderStyle(border_style)); + } + + input.expect_ident_matching("auto")?; + Ok(OutlineStyle::Auto) + } +} diff --git a/servo/components/style/values/specified/page.rs b/servo/components/style/values/specified/page.rs new file mode 100644 index 0000000000..76d9105e8f --- /dev/null +++ b/servo/components/style/values/specified/page.rs @@ -0,0 +1,99 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified @page at-rule properties and named-page style properties + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::size::Size2D; +use crate::values::specified::length::NonNegativeLength; +use crate::values::{generics, CustomIdent}; +use cssparser::Parser; +use style_traits::ParseError; + +pub use generics::page::PageOrientation; +pub use generics::page::PageSizeOrientation; +pub use generics::page::PaperSize; +/// Specified value of the @page size descriptor +pub type PageSize = generics::page::PageSize<Size2D<NonNegativeLength>>; + +impl Parse for PageSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Try to parse as <page-size> [ <orientation> ] + if let Ok(paper_size) = input.try_parse(PaperSize::parse) { + let orientation = input + .try_parse(PageSizeOrientation::parse) + .unwrap_or(PageSizeOrientation::Portrait); + return Ok(PageSize::PaperSize(paper_size, orientation)); + } + // Try to parse as <orientation> [ <page-size> ] + if let Ok(orientation) = input.try_parse(PageSizeOrientation::parse) { + if let Ok(paper_size) = input.try_parse(PaperSize::parse) { + return Ok(PageSize::PaperSize(paper_size, orientation)); + } + return Ok(PageSize::Orientation(orientation)); + } + // Try to parse dimensions + if let Ok(size) = + input.try_parse(|i| Size2D::parse_with(context, i, NonNegativeLength::parse)) + { + return Ok(PageSize::Size(size)); + } + // auto value + input.expect_ident_matching("auto")?; + Ok(PageSize::Auto) + } +} + +/// Page name value. +/// +/// https://drafts.csswg.org/css-page-3/#using-named-pages +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum PageName { + /// `auto` value. + Auto, + /// Page name value + PageName(CustomIdent), +} + +impl Parse for PageName { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + Ok(match_ignore_ascii_case! { ident, + "auto" => PageName::auto(), + _ => PageName::PageName(CustomIdent::from_ident(location, ident, &[])?), + }) + } +} + +impl PageName { + /// `auto` value. + #[inline] + pub fn auto() -> Self { + PageName::Auto + } + + /// Whether this is the `auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, PageName::Auto) + } +} diff --git a/servo/components/style/values/specified/percentage.rs b/servo/components/style/values/specified/percentage.rs new file mode 100644 index 0000000000..ccf16d6463 --- /dev/null +++ b/servo/components/style/values/specified/percentage.rs @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified percentages. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::percentage::Percentage as ComputedPercentage; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::NonNegative; +use crate::values::specified::calc::CalcNode; +use crate::values::specified::Number; +use crate::values::{normalize, serialize_percentage, CSSFloat}; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, ToCss}; + +/// A percentage value. +#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)] +pub struct Percentage { + /// The percentage value as a float. + /// + /// [0 .. 100%] maps to [0.0 .. 1.0] + value: CSSFloat, + /// If this percentage came from a calc() expression, this tells how + /// clamping should be done on the value. + calc_clamping_mode: Option<AllowedNumericType>, +} + +impl ToCss for Percentage { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.calc_clamping_mode.is_some() { + dest.write_str("calc(")?; + } + + serialize_percentage(self.value, dest)?; + + if self.calc_clamping_mode.is_some() { + dest.write_char(')')?; + } + Ok(()) + } +} + +impl Percentage { + /// Creates a percentage from a numeric value. + pub(super) fn new_with_clamping_mode( + value: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Self { + value, + calc_clamping_mode, + } + } + + /// Creates a percentage from a numeric value. + pub fn new(value: CSSFloat) -> Self { + Self::new_with_clamping_mode(value, None) + } + + /// `0%` + #[inline] + pub fn zero() -> Self { + Percentage { + value: 0., + calc_clamping_mode: None, + } + } + + /// `100%` + #[inline] + pub fn hundred() -> Self { + Percentage { + value: 1., + calc_clamping_mode: None, + } + } + + /// Gets the underlying value for this float. + pub fn get(&self) -> CSSFloat { + self.calc_clamping_mode + .map_or(self.value, |mode| mode.clamp(self.value)) + } + + /// Returns this percentage as a number. + pub fn to_number(&self) -> Number { + Number::new_with_clamping_mode(self.value, self.calc_clamping_mode) + } + + /// Returns the calc() clamping mode for this percentage. + pub fn calc_clamping_mode(&self) -> Option<AllowedNumericType> { + self.calc_clamping_mode + } + + /// Reverses this percentage, preserving calc-ness. + /// + /// For example: If it was 20%, convert it into 80%. + pub fn reverse(&mut self) { + let new_value = 1. - self.value; + self.value = new_value; + } + + /// Parses a specific kind of percentage. + pub fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + num_context: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Percentage { unit_value, .. } + if num_context.is_ok(context.parsing_mode, unit_value) => + { + Ok(Percentage::new(unit_value)) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + let value = CalcNode::parse_percentage(context, input, function)?; + Ok(Percentage { + value, + calc_clamping_mode: Some(num_context), + }) + }, + ref t => Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parses a percentage token, but rejects it if it's negative. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } + + /// Parses a percentage token, but rejects it if it's negative or more than + /// 100%. + pub fn parse_zero_to_a_hundred<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::ZeroToOne) + } + + /// Clamp to 100% if the value is over 100%. + #[inline] + pub fn clamp_to_hundred(self) -> Self { + Percentage { + value: self.value.min(1.), + calc_clamping_mode: self.calc_clamping_mode, + } + } +} + +impl Parse for Percentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl ToComputedValue for Percentage { + type ComputedValue = ComputedPercentage; + + #[inline] + fn to_computed_value(&self, _: &Context) -> Self::ComputedValue { + ComputedPercentage(normalize(self.get())) + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Percentage::new(computed.0) + } +} + +impl SpecifiedValueInfo for Percentage {} + +/// Turns the percentage into a plain float. +pub trait ToPercentage { + /// Returns whether this percentage used to be a calc(). + fn is_calc(&self) -> bool { + false + } + /// Turns the percentage into a plain float. + fn to_percentage(&self) -> CSSFloat; +} + +impl ToPercentage for Percentage { + fn is_calc(&self) -> bool { + self.calc_clamping_mode.is_some() + } + + fn to_percentage(&self) -> CSSFloat { + self.get() + } +} + +/// A wrapper of Percentage, whose value must be >= 0. +pub type NonNegativePercentage = NonNegative<Percentage>; + +impl Parse for NonNegativePercentage { + #[inline] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Ok(NonNegative(Percentage::parse_non_negative(context, input)?)) + } +} + +impl NonNegativePercentage { + /// Convert to ComputedPercentage, for FontFaceRule size-adjust getter. + #[inline] + pub fn compute(&self) -> ComputedPercentage { + ComputedPercentage(self.0.get()) + } +} diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs new file mode 100644 index 0000000000..bab853d972 --- /dev/null +++ b/servo/components/style/values/specified/position.rs @@ -0,0 +1,955 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! CSS handling for the specified value of +//! [`position`][position]s +//! +//! [position]: https://drafts.csswg.org/css-backgrounds-3/#position + +use crate::parser::{Parse, ParserContext}; +use crate::selector_map::PrecomputedHashMap; +use crate::str::HTML_SPACE_CHARACTERS; +use crate::values::computed::LengthPercentage as ComputedLengthPercentage; +use crate::values::computed::{Context, Percentage, ToComputedValue}; +use crate::values::generics::position::AspectRatio as GenericAspectRatio; +use crate::values::generics::position::Position as GenericPosition; +use crate::values::generics::position::PositionComponent as GenericPositionComponent; +use crate::values::generics::position::PositionOrAuto as GenericPositionOrAuto; +use crate::values::generics::position::ZIndex as GenericZIndex; +use crate::values::specified::{AllowQuirks, Integer, LengthPercentage, NonNegativeNumber}; +use crate::{Atom, Zero}; +use cssparser::Parser; +use selectors::parser::SelectorParseErrorKind; +use servo_arc::Arc; +use std::collections::hash_map::Entry; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// The specified value of a CSS `<position>` +pub type Position = GenericPosition<HorizontalPosition, VerticalPosition>; + +/// The specified value of an `auto | <position>`. +pub type PositionOrAuto = GenericPositionOrAuto<Position>; + +/// The specified value of a horizontal position. +pub type HorizontalPosition = PositionComponent<HorizontalPositionKeyword>; + +/// The specified value of a vertical position. +pub type VerticalPosition = PositionComponent<VerticalPositionKeyword>; + +/// The specified value of a component of a CSS `<position>`. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum PositionComponent<S> { + /// `center` + Center, + /// `<length-percentage>` + Length(LengthPercentage), + /// `<side> <length-percentage>?` + Side(S, Option<LengthPercentage>), +} + +/// A keyword for the X direction. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum HorizontalPositionKeyword { + Left, + Right, +} + +/// A keyword for the Y direction. +#[derive( + Clone, + Copy, + Debug, + Eq, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum VerticalPositionKeyword { + Top, + Bottom, +} + +impl Parse for Position { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let position = Self::parse_three_value_quirky(context, input, AllowQuirks::No)?; + if position.is_three_value_syntax() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + Ok(position) + } +} + +impl Position { + /// Parses a `<bg-position>`, with quirks. + pub fn parse_three_value_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + match input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) { + Ok(x_pos @ PositionComponent::Center) => { + if let Ok(y_pos) = + input.try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) + { + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = input + .try_parse(|i| PositionComponent::parse_quirky(context, i, allow_quirks)) + .unwrap_or(x_pos); + let y_pos = PositionComponent::Center; + return Ok(Self::new(x_pos, y_pos)); + }, + Ok(PositionComponent::Side(x_keyword, lp)) => { + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + let x_pos = PositionComponent::Side(x_keyword, lp); + let y_pos = PositionComponent::Center; + return Ok(Self::new(x_pos, y_pos)); + } + if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { + let y_lp = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + let x_pos = PositionComponent::Side(x_keyword, lp); + let y_pos = PositionComponent::Side(y_keyword, y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = PositionComponent::Side(x_keyword, None); + let y_pos = lp.map_or(PositionComponent::Center, PositionComponent::Length); + return Ok(Self::new(x_pos, y_pos)); + }, + Ok(x_pos @ PositionComponent::Length(_)) => { + if let Ok(y_keyword) = input.try_parse(VerticalPositionKeyword::parse) { + let y_pos = PositionComponent::Side(y_keyword, None); + return Ok(Self::new(x_pos, y_pos)); + } + if let Ok(y_lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + { + let y_pos = PositionComponent::Length(y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let y_pos = PositionComponent::Center; + let _ = input.try_parse(|i| i.expect_ident_matching("center")); + return Ok(Self::new(x_pos, y_pos)); + }, + Err(_) => {}, + } + let y_keyword = VerticalPositionKeyword::parse(input)?; + let lp_and_x_pos: Result<_, ParseError> = input.try_parse(|i| { + let y_lp = i + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + if let Ok(x_keyword) = i.try_parse(HorizontalPositionKeyword::parse) { + let x_lp = i + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + let x_pos = PositionComponent::Side(x_keyword, x_lp); + return Ok((y_lp, x_pos)); + }; + i.expect_ident_matching("center")?; + let x_pos = PositionComponent::Center; + Ok((y_lp, x_pos)) + }); + if let Ok((y_lp, x_pos)) = lp_and_x_pos { + let y_pos = PositionComponent::Side(y_keyword, y_lp); + return Ok(Self::new(x_pos, y_pos)); + } + let x_pos = PositionComponent::Center; + let y_pos = PositionComponent::Side(y_keyword, None); + Ok(Self::new(x_pos, y_pos)) + } + + /// `center center` + #[inline] + pub fn center() -> Self { + Self::new(PositionComponent::Center, PositionComponent::Center) + } + + /// Returns true if this uses a 3 value syntax. + #[inline] + fn is_three_value_syntax(&self) -> bool { + self.horizontal.component_count() != self.vertical.component_count() + } +} + +impl ToCss for Position { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + match (&self.horizontal, &self.vertical) { + ( + x_pos @ &PositionComponent::Side(_, Some(_)), + &PositionComponent::Length(ref y_lp), + ) => { + x_pos.to_css(dest)?; + dest.write_str(" top ")?; + y_lp.to_css(dest) + }, + ( + &PositionComponent::Length(ref x_lp), + y_pos @ &PositionComponent::Side(_, Some(_)), + ) => { + dest.write_str("left ")?; + x_lp.to_css(dest)?; + dest.write_char(' ')?; + y_pos.to_css(dest) + }, + (x_pos, y_pos) => { + x_pos.to_css(dest)?; + dest.write_char(' ')?; + y_pos.to_css(dest) + }, + } + } +} + +impl<S: Parse> Parse for PositionComponent<S> { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_quirky(context, input, AllowQuirks::No) + } +} + +impl<S: Parse> PositionComponent<S> { + /// Parses a component of a CSS position, with quirks. + pub fn parse_quirky<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + allow_quirks: AllowQuirks, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + return Ok(PositionComponent::Center); + } + if let Ok(lp) = + input.try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + { + return Ok(PositionComponent::Length(lp)); + } + let keyword = S::parse(context, input)?; + let lp = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, allow_quirks)) + .ok(); + Ok(PositionComponent::Side(keyword, lp)) + } +} + +impl<S> GenericPositionComponent for PositionComponent<S> { + fn is_center(&self) -> bool { + match *self { + PositionComponent::Center => true, + PositionComponent::Length(LengthPercentage::Percentage(ref per)) => per.0 == 0.5, + // 50% from any side is still the center. + PositionComponent::Side(_, Some(LengthPercentage::Percentage(ref per))) => per.0 == 0.5, + _ => false, + } + } +} + +impl<S> PositionComponent<S> { + /// `0%` + pub fn zero() -> Self { + PositionComponent::Length(LengthPercentage::Percentage(Percentage::zero())) + } + + /// Returns the count of this component. + fn component_count(&self) -> usize { + match *self { + PositionComponent::Length(..) | PositionComponent::Center => 1, + PositionComponent::Side(_, ref lp) => { + if lp.is_some() { + 2 + } else { + 1 + } + }, + } + } +} + +impl<S: Side> ToComputedValue for PositionComponent<S> { + type ComputedValue = ComputedLengthPercentage; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + PositionComponent::Center => ComputedLengthPercentage::new_percent(Percentage(0.5)), + PositionComponent::Side(ref keyword, None) => { + let p = Percentage(if keyword.is_start() { 0. } else { 1. }); + ComputedLengthPercentage::new_percent(p) + }, + PositionComponent::Side(ref keyword, Some(ref length)) if !keyword.is_start() => { + let length = length.to_computed_value(context); + // We represent `<end-side> <length>` as `calc(100% - <length>)`. + ComputedLengthPercentage::hundred_percent_minus(length, AllowedNumericType::All) + }, + PositionComponent::Side(_, Some(ref length)) | + PositionComponent::Length(ref length) => length.to_computed_value(context), + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + PositionComponent::Length(ToComputedValue::from_computed_value(computed)) + } +} + +impl<S: Side> PositionComponent<S> { + /// The initial specified value of a position component, i.e. the start side. + pub fn initial_specified_value() -> Self { + PositionComponent::Side(S::start(), None) + } +} + +/// Represents a side, either horizontal or vertical, of a CSS position. +pub trait Side { + /// Returns the start side. + fn start() -> Self; + + /// Returns whether this side is the start side. + fn is_start(&self) -> bool; +} + +impl Side for HorizontalPositionKeyword { + #[inline] + fn start() -> Self { + HorizontalPositionKeyword::Left + } + + #[inline] + fn is_start(&self) -> bool { + *self == Self::start() + } +} + +impl Side for VerticalPositionKeyword { + #[inline] + fn start() -> Self { + VerticalPositionKeyword::Top + } + + #[inline] + fn is_start(&self) -> bool { + *self == Self::start() + } +} + +/// Controls how the auto-placement algorithm works specifying exactly how auto-placed items +/// get flowed into the grid. +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "row,column,dense")] +#[repr(C)] +pub struct GridAutoFlow(u8); +bitflags! { + impl GridAutoFlow: u8 { + /// 'row' - mutually exclusive with 'column' + const ROW = 1 << 0; + /// 'column' - mutually exclusive with 'row' + const COLUMN = 1 << 1; + /// 'dense' + const DENSE = 1 << 2; + } +} + +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Masonry auto-placement algorithm packing. +pub enum MasonryPlacement { + /// Place the item in the track(s) with the smallest extent so far. + Pack, + /// Place the item after the last item, from start to end. + Next, +} + +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +/// Masonry auto-placement algorithm item sorting option. +pub enum MasonryItemOrder { + /// Place all items with a definite placement before auto-placed items. + DefiniteFirst, + /// Place items in `order-modified document order`. + Ordered, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Controls how the Masonry layout algorithm works +/// specifying exactly how auto-placed items get flowed in the masonry axis. +pub struct MasonryAutoFlow { + /// Specify how to pick a auto-placement track. + #[css(contextual_skip_if = "is_pack_with_non_default_order")] + pub placement: MasonryPlacement, + /// Specify how to pick an item to place. + #[css(skip_if = "is_item_order_definite_first")] + pub order: MasonryItemOrder, +} + +#[inline] +fn is_pack_with_non_default_order(placement: &MasonryPlacement, order: &MasonryItemOrder) -> bool { + *placement == MasonryPlacement::Pack && *order != MasonryItemOrder::DefiniteFirst +} + +#[inline] +fn is_item_order_definite_first(order: &MasonryItemOrder) -> bool { + *order == MasonryItemOrder::DefiniteFirst +} + +impl MasonryAutoFlow { + #[inline] + /// Get initial `masonry-auto-flow` value. + pub fn initial() -> MasonryAutoFlow { + MasonryAutoFlow { + placement: MasonryPlacement::Pack, + order: MasonryItemOrder::DefiniteFirst, + } + } +} + +impl Parse for MasonryAutoFlow { + /// [ definite-first | ordered ] || [ pack | next ] + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MasonryAutoFlow, ParseError<'i>> { + let mut value = MasonryAutoFlow::initial(); + let mut got_placement = false; + let mut got_order = false; + while !input.is_exhausted() { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let success = match_ignore_ascii_case! { &ident, + "pack" if !got_placement => { + got_placement = true; + true + }, + "next" if !got_placement => { + value.placement = MasonryPlacement::Next; + got_placement = true; + true + }, + "definite-first" if !got_order => { + got_order = true; + true + }, + "ordered" if !got_order => { + value.order = MasonryItemOrder::Ordered; + got_order = true; + true + }, + _ => false + }; + if !success { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + } + } + + if got_placement || got_order { + Ok(value) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +// TODO: Can be derived with some care. +impl Parse for GridAutoFlow { + /// [ row | column ] || dense + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<GridAutoFlow, ParseError<'i>> { + let mut track = None; + let mut dense = GridAutoFlow::empty(); + + while !input.is_exhausted() { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + let success = match_ignore_ascii_case! { &ident, + "row" if track.is_none() => { + track = Some(GridAutoFlow::ROW); + true + }, + "column" if track.is_none() => { + track = Some(GridAutoFlow::COLUMN); + true + }, + "dense" if dense.is_empty() => { + dense = GridAutoFlow::DENSE; + true + }, + _ => false, + }; + if !success { + return Err(location + .new_custom_error(SelectorParseErrorKind::UnexpectedIdent(ident.clone()))); + } + } + + if track.is_some() || !dense.is_empty() { + Ok(track.unwrap_or(GridAutoFlow::ROW) | dense) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for GridAutoFlow { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if *self == GridAutoFlow::ROW { + return dest.write_str("row"); + } + + if *self == GridAutoFlow::COLUMN { + return dest.write_str("column"); + } + + if *self == GridAutoFlow::ROW | GridAutoFlow::DENSE { + return dest.write_str("dense"); + } + + if *self == GridAutoFlow::COLUMN | GridAutoFlow::DENSE { + return dest.write_str("column dense"); + } + + debug_assert!(false, "Unknown or invalid grid-autoflow value"); + Ok(()) + } +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// https://drafts.csswg.org/css-grid/#named-grid-area +pub struct TemplateAreas { + /// `named area` containing for each template area + #[css(skip)] + pub areas: crate::OwnedSlice<NamedArea>, + /// The simplified CSS strings for serialization purpose. + /// https://drafts.csswg.org/css-grid/#serialize-template + // Note: We also use the length of `strings` when computing the explicit grid end line number + // (i.e. row number). + #[css(iterable)] + pub strings: crate::OwnedSlice<crate::OwnedStr>, + /// The number of columns of the grid. + #[css(skip)] + pub width: u32, +} + +/// Parser for grid template areas. +#[derive(Default)] +pub struct TemplateAreasParser { + areas: Vec<NamedArea>, + area_indices: PrecomputedHashMap<Atom, usize>, + strings: Vec<crate::OwnedStr>, + width: u32, + row: u32, +} + +impl TemplateAreasParser { + /// Parse a single string. + pub fn try_parse_string<'i>( + &mut self, + input: &mut Parser<'i, '_>, + ) -> Result<(), ParseError<'i>> { + input.try_parse(|input| { + self.parse_string(input.expect_string()?) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }) + } + + /// Parse a single string. + fn parse_string(&mut self, string: &str) -> Result<(), ()> { + self.row += 1; + let mut simplified_string = String::new(); + let mut current_area_index: Option<usize> = None; + let mut column = 0u32; + for token in TemplateAreasTokenizer(string) { + column += 1; + if column > 1 { + simplified_string.push(' '); + } + let name = if let Some(token) = token? { + simplified_string.push_str(token); + Atom::from(token) + } else { + if let Some(index) = current_area_index.take() { + if self.areas[index].columns.end != column { + return Err(()); + } + } + simplified_string.push('.'); + continue; + }; + if let Some(index) = current_area_index { + if self.areas[index].name == name { + if self.areas[index].rows.start == self.row { + self.areas[index].columns.end += 1; + } + continue; + } + if self.areas[index].columns.end != column { + return Err(()); + } + } + match self.area_indices.entry(name) { + Entry::Occupied(ref e) => { + let index = *e.get(); + if self.areas[index].columns.start != column || + self.areas[index].rows.end != self.row + { + return Err(()); + } + self.areas[index].rows.end += 1; + current_area_index = Some(index); + }, + Entry::Vacant(v) => { + let index = self.areas.len(); + let name = v.key().clone(); + v.insert(index); + self.areas.push(NamedArea { + name, + columns: UnsignedRange { + start: column, + end: column + 1, + }, + rows: UnsignedRange { + start: self.row, + end: self.row + 1, + }, + }); + current_area_index = Some(index); + }, + } + } + if column == 0 { + // Each string must produce a valid token. + // https://github.com/w3c/csswg-drafts/issues/5110 + return Err(()); + } + if let Some(index) = current_area_index { + if self.areas[index].columns.end != column + 1 { + debug_assert_ne!(self.areas[index].rows.start, self.row); + return Err(()); + } + } + if self.row == 1 { + self.width = column; + } else if self.width != column { + return Err(()); + } + + self.strings.push(simplified_string.into()); + Ok(()) + } + + /// Return the parsed template areas. + pub fn finish(self) -> Result<TemplateAreas, ()> { + if self.strings.is_empty() { + return Err(()); + } + Ok(TemplateAreas { + areas: self.areas.into(), + strings: self.strings.into(), + width: self.width, + }) + } +} + +impl TemplateAreas { + fn parse_internal(input: &mut Parser) -> Result<Self, ()> { + let mut parser = TemplateAreasParser::default(); + while parser.try_parse_string(input).is_ok() {} + parser.finish() + } +} + +impl Parse for TemplateAreas { + fn parse<'i, 't>( + _: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_internal(input) + .map_err(|()| input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } +} + +/// Arc type for `Arc<TemplateAreas>` +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct TemplateAreasArc(#[ignore_malloc_size_of = "Arc"] pub Arc<TemplateAreas>); + +impl Parse for TemplateAreasArc { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let parsed = TemplateAreas::parse(context, input)?; + Ok(TemplateAreasArc(Arc::new(parsed))) + } +} + +/// A range of rows or columns. Using this instead of std::ops::Range for FFI +/// purposes. +#[repr(C)] +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +pub struct UnsignedRange { + /// The start of the range. + pub start: u32, + /// The end of the range. + pub end: u32, +} + +#[derive( + Clone, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Not associated with any particular grid item, but can be referenced from the +/// grid-placement properties. +pub struct NamedArea { + /// Name of the `named area` + pub name: Atom, + /// Rows of the `named area` + pub rows: UnsignedRange, + /// Columns of the `named area` + pub columns: UnsignedRange, +} + +/// Tokenize the string into a list of the tokens, +/// using longest-match semantics +struct TemplateAreasTokenizer<'a>(&'a str); + +impl<'a> Iterator for TemplateAreasTokenizer<'a> { + type Item = Result<Option<&'a str>, ()>; + + fn next(&mut self) -> Option<Self::Item> { + let rest = self.0.trim_start_matches(HTML_SPACE_CHARACTERS); + if rest.is_empty() { + return None; + } + if rest.starts_with('.') { + self.0 = &rest[rest.find(|c| c != '.').unwrap_or(rest.len())..]; + return Some(Ok(None)); + } + if !rest.starts_with(is_name_code_point) { + return Some(Err(())); + } + let token_len = rest.find(|c| !is_name_code_point(c)).unwrap_or(rest.len()); + let token = &rest[..token_len]; + self.0 = &rest[token_len..]; + Some(Ok(Some(token))) + } +} + +fn is_name_code_point(c: char) -> bool { + c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z' || + c >= '\u{80}' || + c == '_' || + c >= '0' && c <= '9' || + c == '-' +} + +/// This property specifies named grid areas. +/// +/// The syntax of this property also provides a visualization of the structure +/// of the grid, making the overall layout of the grid container easier to +/// understand. +#[repr(C, u8)] +#[derive( + Clone, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +pub enum GridTemplateAreas { + /// The `none` value. + None, + /// The actual value. + Areas(TemplateAreasArc), +} + +impl GridTemplateAreas { + #[inline] + /// Get default value as `none` + pub fn none() -> GridTemplateAreas { + GridTemplateAreas::None + } +} + +/// A specified value for the `z-index` property. +pub type ZIndex = GenericZIndex<Integer>; + +/// A specified value for the `aspect-ratio` property. +pub type AspectRatio = GenericAspectRatio<NonNegativeNumber>; + +impl Parse for AspectRatio { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::values::generics::position::PreferredRatio; + use crate::values::specified::Ratio; + + let location = input.current_source_location(); + let mut auto = input.try_parse(|i| i.expect_ident_matching("auto")); + let ratio = input.try_parse(|i| Ratio::parse(context, i)); + if auto.is_err() { + auto = input.try_parse(|i| i.expect_ident_matching("auto")); + } + + if auto.is_err() && ratio.is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(AspectRatio { + auto: auto.is_ok(), + ratio: match ratio { + Ok(ratio) => PreferredRatio::Ratio(ratio), + Err(..) => PreferredRatio::None, + }, + }) + } +} + +impl AspectRatio { + /// Returns Self by a valid ratio. + pub fn from_mapped_ratio(w: f32, h: f32) -> Self { + use crate::values::generics::position::PreferredRatio; + use crate::values::generics::ratio::Ratio; + AspectRatio { + auto: true, + ratio: PreferredRatio::Ratio(Ratio( + NonNegativeNumber::new(w), + NonNegativeNumber::new(h), + )), + } + } +} diff --git a/servo/components/style/values/specified/ratio.rs b/servo/components/style/values/specified/ratio.rs new file mode 100644 index 0000000000..4cdddd452e --- /dev/null +++ b/servo/components/style/values/specified/ratio.rs @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for <ratio>. +//! +//! [ratio]: https://drafts.csswg.org/css-values/#ratios + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::ratio::Ratio as GenericRatio; +use crate::values::specified::NonNegativeNumber; +use crate::One; +use cssparser::Parser; +use style_traits::ParseError; + +/// A specified <ratio> value. +pub type Ratio = GenericRatio<NonNegativeNumber>; + +impl Parse for Ratio { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let a = NonNegativeNumber::parse(context, input)?; + let b = match input.try_parse(|input| input.expect_delim('/')) { + Ok(()) => NonNegativeNumber::parse(context, input)?, + _ => One::one(), + }; + + Ok(GenericRatio(a, b)) + } +} diff --git a/servo/components/style/values/specified/rect.rs b/servo/components/style/values/specified/rect.rs new file mode 100644 index 0000000000..7955ecaa48 --- /dev/null +++ b/servo/components/style/values/specified/rect.rs @@ -0,0 +1,11 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS borders. + +use crate::values::generics::rect::Rect; +use crate::values::specified::length::NonNegativeLengthOrNumber; + +/// A specified rectangle made of four `<length-or-number>` values. +pub type NonNegativeLengthOrNumberRect = Rect<NonNegativeLengthOrNumber>; diff --git a/servo/components/style/values/specified/resolution.rs b/servo/components/style/values/specified/resolution.rs new file mode 100644 index 0000000000..74f100972a --- /dev/null +++ b/servo/components/style/values/specified/resolution.rs @@ -0,0 +1,141 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Resolution values: +//! +//! https://drafts.csswg.org/css-values/#resolution + +use crate::parser::{Parse, ParserContext}; +use crate::values::specified::CalcNode; +use crate::values::CSSFloat; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A specified resolution. +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem)] +pub struct Resolution { + value: CSSFloat, + unit: ResolutionUnit, + was_calc: bool, +} + +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +enum ResolutionUnit { + /// Dots per inch. + Dpi, + /// An alias unit for dots per pixel. + X, + /// Dots per pixel. + Dppx, + /// Dots per centimeter. + Dpcm, +} + +impl ResolutionUnit { + fn as_str(self) -> &'static str { + match self { + Self::Dpi => "dpi", + Self::X => "x", + Self::Dppx => "dppx", + Self::Dpcm => "dpcm", + } + } +} + +impl Resolution { + /// Returns a resolution value from dppx units. + pub fn from_dppx(value: CSSFloat) -> Self { + Self { + value, + unit: ResolutionUnit::Dppx, + was_calc: false, + } + } + + /// Returns a resolution value from dppx units. + pub fn from_x(value: CSSFloat) -> Self { + Self { + value, + unit: ResolutionUnit::X, + was_calc: false, + } + } + + /// Returns a resolution value from dppx units. + pub fn from_dppx_calc(value: CSSFloat) -> Self { + Self { + value, + unit: ResolutionUnit::Dppx, + was_calc: true, + } + } + + /// Convert this resolution value to dppx units. + pub fn dppx(&self) -> CSSFloat { + match self.unit { + ResolutionUnit::X | ResolutionUnit::Dppx => self.value, + _ => self.dpi() / 96.0, + } + } + + /// Convert this resolution value to dpi units. + pub fn dpi(&self) -> CSSFloat { + match self.unit { + ResolutionUnit::Dpi => self.value, + ResolutionUnit::X | ResolutionUnit::Dppx => self.value * 96.0, + ResolutionUnit::Dpcm => self.value * 2.54, + } + } + + /// Parse a resolution given a value and unit. + pub fn parse_dimension<'i, 't>(value: CSSFloat, unit: &str) -> Result<Self, ()> { + let unit = match_ignore_ascii_case! { &unit, + "dpi" => ResolutionUnit::Dpi, + "dppx" => ResolutionUnit::Dppx, + "dpcm" => ResolutionUnit::Dpcm, + "x" => ResolutionUnit::X, + _ => return Err(()) + }; + Ok(Self { + value, + unit, + was_calc: false, + }) + } +} + +impl ToCss for Resolution { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.value, + self.unit.as_str(), + self.was_calc, + dest, + ) + } +} + +impl Parse for Resolution { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + match *input.next()? { + Token::Dimension { + value, ref unit, .. + } if value >= 0. => Self::parse_dimension(value, unit) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse_resolution(context, input, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } +} diff --git a/servo/components/style/values/specified/source_size_list.rs b/servo/components/style/values/specified/source_size_list.rs new file mode 100644 index 0000000000..ac47461cc4 --- /dev/null +++ b/servo/components/style/values/specified/source_size_list.rs @@ -0,0 +1,136 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! https://html.spec.whatwg.org/multipage/#source-size-list + +use crate::media_queries::Device; +use crate::parser::{Parse, ParserContext}; +use crate::queries::{FeatureType, QueryCondition}; +use crate::values::computed::{self, ToComputedValue}; +use crate::values::specified::{Length, NoCalcLength, ViewportPercentageLength}; +use app_units::Au; +use cssparser::{Delimiter, Parser, Token}; +use selectors::context::QuirksMode; +use style_traits::ParseError; + +/// A value for a `<source-size>`: +/// +/// https://html.spec.whatwg.org/multipage/#source-size +#[derive(Debug)] +pub struct SourceSize { + condition: QueryCondition, + value: Length, +} + +impl Parse for SourceSize { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let condition = QueryCondition::parse(context, input, FeatureType::Media)?; + let value = Length::parse_non_negative(context, input)?; + Ok(Self { condition, value }) + } +} + +/// A value for a `<source-size-list>`: +/// +/// https://html.spec.whatwg.org/multipage/#source-size-list +#[derive(Debug)] +pub struct SourceSizeList { + source_sizes: Vec<SourceSize>, + value: Option<Length>, +} + +impl SourceSizeList { + /// Create an empty `SourceSizeList`, which can be used as a fall-back. + pub fn empty() -> Self { + Self { + source_sizes: vec![], + value: None, + } + } + + /// Evaluate this <source-size-list> to get the final viewport length. + pub fn evaluate(&self, device: &Device, quirks_mode: QuirksMode) -> Au { + computed::Context::for_media_query_evaluation(device, quirks_mode, |context| { + let matching_source_size = self.source_sizes.iter().find(|source_size| { + source_size + .condition + .matches(context) + .to_bool(/* unknown = */ false) + }); + + match matching_source_size { + Some(source_size) => source_size.value.to_computed_value(context), + None => match self.value { + Some(ref v) => v.to_computed_value(context), + None => Length::NoCalc(NoCalcLength::ViewportPercentage( + ViewportPercentageLength::Vw(100.), + )) + .to_computed_value(context), + }, + } + }) + .into() + } +} + +enum SourceSizeOrLength { + SourceSize(SourceSize), + Length(Length), +} + +impl Parse for SourceSizeOrLength { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(size) = input.try_parse(|input| SourceSize::parse(context, input)) { + return Ok(SourceSizeOrLength::SourceSize(size)); + } + + let length = Length::parse_non_negative(context, input)?; + Ok(SourceSizeOrLength::Length(length)) + } +} + +impl SourceSizeList { + /// NOTE(emilio): This doesn't match the grammar in the spec, see: + /// + /// https://html.spec.whatwg.org/multipage/#parsing-a-sizes-attribute + pub fn parse<'i, 't>(context: &ParserContext, input: &mut Parser<'i, 't>) -> Self { + let mut source_sizes = vec![]; + + loop { + let result = input.parse_until_before(Delimiter::Comma, |input| { + SourceSizeOrLength::parse(context, input) + }); + + match result { + Ok(SourceSizeOrLength::Length(value)) => { + return Self { + source_sizes, + value: Some(value), + }; + }, + Ok(SourceSizeOrLength::SourceSize(source_size)) => { + source_sizes.push(source_size); + }, + Err(..) => {}, + } + + match input.next() { + Ok(&Token::Comma) => {}, + Err(..) => break, + _ => unreachable!(), + } + } + + SourceSizeList { + source_sizes, + value: None, + } + } +} diff --git a/servo/components/style/values/specified/svg.rs b/servo/components/style/values/specified/svg.rs new file mode 100644 index 0000000000..8ab2dbb223 --- /dev/null +++ b/servo/components/style/values/specified/svg.rs @@ -0,0 +1,404 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for SVG properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::svg as generic; +use crate::values::specified::color::Color; +use crate::values::specified::url::SpecifiedUrl; +use crate::values::specified::AllowQuirks; +use crate::values::specified::LengthPercentage; +use crate::values::specified::SVGPathData; +use crate::values::specified::{NonNegativeLengthPercentage, Opacity}; +use crate::values::CustomIdent; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::{CommaWithSpace, CssWriter, ParseError, Separator}; +use style_traits::{StyleParseErrorKind, ToCss}; + +/// Specified SVG Paint value +pub type SVGPaint = generic::GenericSVGPaint<Color, SpecifiedUrl>; + +/// <length> | <percentage> | <number> | context-value +pub type SVGLength = generic::GenericSVGLength<LengthPercentage>; + +/// A non-negative version of SVGLength. +pub type SVGWidth = generic::GenericSVGLength<NonNegativeLengthPercentage>; + +/// [ <length> | <percentage> | <number> ]# | context-value +pub type SVGStrokeDashArray = generic::GenericSVGStrokeDashArray<NonNegativeLengthPercentage>; + +/// Whether the `context-value` value is enabled. +#[cfg(feature = "gecko")] +pub fn is_context_value_enabled() -> bool { + static_prefs::pref!("gfx.font_rendering.opentype_svg.enabled") +} + +/// Whether the `context-value` value is enabled. +#[cfg(not(feature = "gecko"))] +pub fn is_context_value_enabled() -> bool { + false +} + +macro_rules! parse_svg_length { + ($ty:ty, $lp:ty) => { + impl Parse for $ty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(lp) = + input.try_parse(|i| <$lp>::parse_quirky(context, i, AllowQuirks::Always)) + { + return Ok(generic::SVGLength::LengthPercentage(lp)); + } + + try_match_ident_ignore_ascii_case! { input, + "context-value" if is_context_value_enabled() => { + Ok(generic::SVGLength::ContextValue) + }, + } + } + } + }; +} + +parse_svg_length!(SVGLength, LengthPercentage); +parse_svg_length!(SVGWidth, NonNegativeLengthPercentage); + +impl Parse for SVGStrokeDashArray { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if let Ok(values) = input.try_parse(|i| { + CommaWithSpace::parse(i, |i| { + NonNegativeLengthPercentage::parse_quirky(context, i, AllowQuirks::Always) + }) + }) { + return Ok(generic::SVGStrokeDashArray::Values(values.into())); + } + + try_match_ident_ignore_ascii_case! { input, + "context-value" if is_context_value_enabled() => { + Ok(generic::SVGStrokeDashArray::ContextValue) + }, + "none" => Ok(generic::SVGStrokeDashArray::Values(Default::default())), + } + } +} + +/// <opacity-value> | context-fill-opacity | context-stroke-opacity +pub type SVGOpacity = generic::SVGOpacity<Opacity>; + +/// The specified value for a single CSS paint-order property. +#[repr(u8)] +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, ToCss)] +pub enum PaintOrder { + /// `normal` variant + Normal = 0, + /// `fill` variant + Fill = 1, + /// `stroke` variant + Stroke = 2, + /// `markers` variant + Markers = 3, +} + +/// Number of non-normal components +pub const PAINT_ORDER_COUNT: u8 = 3; + +/// Number of bits for each component +pub const PAINT_ORDER_SHIFT: u8 = 2; + +/// Mask with above bits set +pub const PAINT_ORDER_MASK: u8 = 0b11; + +/// The specified value is tree `PaintOrder` values packed into the +/// bitfields below, as a six-bit field, of 3 two-bit pairs +/// +/// Each pair can be set to FILL, STROKE, or MARKERS +/// Lowest significant bit pairs are highest priority. +/// `normal` is the empty bitfield. The three pairs are +/// never zero in any case other than `normal`. +/// +/// Higher priority values, i.e. the values specified first, +/// will be painted first (and may be covered by paintings of lower priority) +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct SVGPaintOrder(pub u8); + +impl SVGPaintOrder { + /// Get default `paint-order` with `0` + pub fn normal() -> Self { + SVGPaintOrder(0) + } + + /// Get variant of `paint-order` + pub fn order_at(&self, pos: u8) -> PaintOrder { + match (self.0 >> pos * PAINT_ORDER_SHIFT) & PAINT_ORDER_MASK { + 0 => PaintOrder::Normal, + 1 => PaintOrder::Fill, + 2 => PaintOrder::Stroke, + 3 => PaintOrder::Markers, + _ => unreachable!("this cannot happen"), + } + } +} + +impl Parse for SVGPaintOrder { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<SVGPaintOrder, ParseError<'i>> { + if let Ok(()) = input.try_parse(|i| i.expect_ident_matching("normal")) { + return Ok(SVGPaintOrder::normal()); + } + + let mut value = 0; + // bitfield representing what we've seen so far + // bit 1 is fill, bit 2 is stroke, bit 3 is markers + let mut seen = 0; + let mut pos = 0; + + loop { + let result: Result<_, ParseError> = input.try_parse(|input| { + try_match_ident_ignore_ascii_case! { input, + "fill" => Ok(PaintOrder::Fill), + "stroke" => Ok(PaintOrder::Stroke), + "markers" => Ok(PaintOrder::Markers), + } + }); + + match result { + Ok(val) => { + if (seen & (1 << val as u8)) != 0 { + // don't parse the same ident twice + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + value |= (val as u8) << (pos * PAINT_ORDER_SHIFT); + seen |= 1 << (val as u8); + pos += 1; + }, + Err(_) => break, + } + } + + if value == 0 { + // Couldn't find any keyword + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // fill in rest + for i in pos..PAINT_ORDER_COUNT { + for paint in 1..(PAINT_ORDER_COUNT + 1) { + // if not seen, set bit at position, mark as seen + if (seen & (1 << paint)) == 0 { + seen |= 1 << paint; + value |= paint << (i * PAINT_ORDER_SHIFT); + break; + } + } + } + + Ok(SVGPaintOrder(value)) + } +} + +impl ToCss for SVGPaintOrder { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.0 == 0 { + return dest.write_str("normal"); + } + + let mut last_pos_to_serialize = 0; + for i in (1..PAINT_ORDER_COUNT).rev() { + let component = self.order_at(i); + let earlier_component = self.order_at(i - 1); + if component < earlier_component { + last_pos_to_serialize = i - 1; + break; + } + } + + for pos in 0..last_pos_to_serialize + 1 { + if pos != 0 { + dest.write_char(' ')? + } + self.order_at(pos).to_css(dest)?; + } + Ok(()) + } +} + +/// The context properties we understand. +#[derive( + Clone, + Copy, + Eq, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ContextPropertyBits(u8); +bitflags! { + impl ContextPropertyBits: u8 { + /// `fill` + const FILL = 1 << 0; + /// `stroke` + const STROKE = 1 << 1; + /// `fill-opacity` + const FILL_OPACITY = 1 << 2; + /// `stroke-opacity` + const STROKE_OPACITY = 1 << 3; + } +} + +/// Specified MozContextProperties value. +/// Nonstandard (https://developer.mozilla.org/en-US/docs/Web/CSS/-moz-context-properties) +#[derive( + Clone, + Debug, + Default, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct MozContextProperties { + #[css(iterable, if_empty = "none")] + #[ignore_malloc_size_of = "Arc"] + idents: crate::ArcSlice<CustomIdent>, + #[css(skip)] + bits: ContextPropertyBits, +} + +impl Parse for MozContextProperties { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<MozContextProperties, ParseError<'i>> { + let mut values = vec![]; + let mut bits = ContextPropertyBits::empty(); + loop { + { + let location = input.current_source_location(); + let ident = input.expect_ident()?; + + if ident.eq_ignore_ascii_case("none") && values.is_empty() { + return Ok(Self::default()); + } + + let ident = CustomIdent::from_ident(location, ident, &["all", "none", "auto"])?; + + if ident.0 == atom!("fill") { + bits.insert(ContextPropertyBits::FILL); + } else if ident.0 == atom!("stroke") { + bits.insert(ContextPropertyBits::STROKE); + } else if ident.0 == atom!("fill-opacity") { + bits.insert(ContextPropertyBits::FILL_OPACITY); + } else if ident.0 == atom!("stroke-opacity") { + bits.insert(ContextPropertyBits::STROKE_OPACITY); + } + + values.push(ident); + } + + let location = input.current_source_location(); + match input.next() { + Ok(&Token::Comma) => continue, + Err(..) => break, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + } + } + + if values.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(MozContextProperties { + idents: crate::ArcSlice::from_iter(values.into_iter()), + bits, + }) + } +} + +/// The svg d property type. +/// +/// https://svgwg.org/svg2-draft/paths.html#TheDProperty +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum DProperty { + /// Path value for path(<string>) or just a <string>. + #[css(function)] + Path(SVGPathData), + /// None value. + #[animation(error)] + None, +} + +impl DProperty { + /// return none. + #[inline] + pub fn none() -> Self { + DProperty::None + } +} + +impl Parse for DProperty { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Parse none. + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(DProperty::none()); + } + + // Parse possible functions. + input.expect_function_matching("path")?; + let path_data = input.parse_nested_block(|i| Parse::parse(context, i))?; + Ok(DProperty::Path(path_data)) + } +} diff --git a/servo/components/style/values/specified/svg_path.rs b/servo/components/style/values/specified/svg_path.rs new file mode 100644 index 0000000000..1eb9866dd1 --- /dev/null +++ b/servo/components/style/values/specified/svg_path.rs @@ -0,0 +1,1029 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for SVG Path. + +use crate::parser::{Parse, ParserContext}; +use crate::values::animated::{lists, Animate, Procedure, ToAnimatedZero}; +use crate::values::distance::{ComputeSquaredDistance, SquaredDistance}; +use crate::values::CSSFloat; +use cssparser::Parser; +use std::fmt::{self, Write}; +use std::iter::{Cloned, Peekable}; +use std::slice; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// Whether to allow empty string in the parser. +#[derive(Clone, Debug, Eq, PartialEq)] +#[allow(missing_docs)] +pub enum AllowEmpty { + Yes, + No, +} + +/// The SVG path data. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive( + Clone, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct SVGPathData( + // TODO(emilio): Should probably measure this somehow only from the + // specified values. + #[ignore_malloc_size_of = "Arc"] pub crate::ArcSlice<PathCommand>, +); + +impl SVGPathData { + /// Get the array of PathCommand. + #[inline] + pub fn commands(&self) -> &[PathCommand] { + &self.0 + } + + /// Create a normalized copy of this path by converting each relative + /// command to an absolute command. + pub fn normalize(&self) -> Self { + let mut state = PathTraversalState { + subpath_start: CoordPair::new(0.0, 0.0), + pos: CoordPair::new(0.0, 0.0), + }; + let iter = self.0.iter().map(|seg| seg.normalize(&mut state)); + SVGPathData(crate::ArcSlice::from_iter(iter)) + } + + // FIXME: Bug 1714238, we may drop this once we use the same data structure for both SVG and + // CSS. + /// Decode the svg path raw data from Gecko. + #[cfg(feature = "gecko")] + pub fn decode_from_f32_array(path: &[f32]) -> Result<Self, ()> { + use crate::gecko_bindings::structs::dom::SVGPathSeg_Binding::*; + + let mut result: Vec<PathCommand> = Vec::new(); + let mut i: usize = 0; + while i < path.len() { + // See EncodeType() and DecodeType() in SVGPathSegUtils.h. + // We are using reinterpret_cast<> to encode and decode between u32 and f32, so here we + // use to_bits() to decode the type. + let seg_type = path[i].to_bits() as u16; + i = i + 1; + match seg_type { + PATHSEG_CLOSEPATH => result.push(PathCommand::ClosePath), + PATHSEG_MOVETO_ABS | PATHSEG_MOVETO_REL => { + debug_assert!(i + 1 < path.len()); + result.push(PathCommand::MoveTo { + point: CoordPair::new(path[i], path[i + 1]), + absolute: IsAbsolute::new(seg_type == PATHSEG_MOVETO_ABS), + }); + i = i + 2; + }, + PATHSEG_LINETO_ABS | PATHSEG_LINETO_REL => { + debug_assert!(i + 1 < path.len()); + result.push(PathCommand::LineTo { + point: CoordPair::new(path[i], path[i + 1]), + absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_ABS), + }); + i = i + 2; + }, + PATHSEG_CURVETO_CUBIC_ABS | PATHSEG_CURVETO_CUBIC_REL => { + debug_assert!(i + 5 < path.len()); + result.push(PathCommand::CurveTo { + control1: CoordPair::new(path[i], path[i + 1]), + control2: CoordPair::new(path[i + 2], path[i + 3]), + point: CoordPair::new(path[i + 4], path[i + 5]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_ABS), + }); + i = i + 6; + }, + PATHSEG_CURVETO_QUADRATIC_ABS | PATHSEG_CURVETO_QUADRATIC_REL => { + debug_assert!(i + 3 < path.len()); + result.push(PathCommand::QuadBezierCurveTo { + control1: CoordPair::new(path[i], path[i + 1]), + point: CoordPair::new(path[i + 2], path[i + 3]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_ABS), + }); + i = i + 4; + }, + PATHSEG_ARC_ABS | PATHSEG_ARC_REL => { + debug_assert!(i + 6 < path.len()); + result.push(PathCommand::EllipticalArc { + rx: path[i], + ry: path[i + 1], + angle: path[i + 2], + large_arc_flag: ArcFlag(path[i + 3] != 0.0f32), + sweep_flag: ArcFlag(path[i + 4] != 0.0f32), + point: CoordPair::new(path[i + 5], path[i + 6]), + absolute: IsAbsolute::new(seg_type == PATHSEG_ARC_ABS), + }); + i = i + 7; + }, + PATHSEG_LINETO_HORIZONTAL_ABS | PATHSEG_LINETO_HORIZONTAL_REL => { + debug_assert!(i < path.len()); + result.push(PathCommand::HorizontalLineTo { + x: path[i], + absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_HORIZONTAL_ABS), + }); + i = i + 1; + }, + PATHSEG_LINETO_VERTICAL_ABS | PATHSEG_LINETO_VERTICAL_REL => { + debug_assert!(i < path.len()); + result.push(PathCommand::VerticalLineTo { + y: path[i], + absolute: IsAbsolute::new(seg_type == PATHSEG_LINETO_VERTICAL_ABS), + }); + i = i + 1; + }, + PATHSEG_CURVETO_CUBIC_SMOOTH_ABS | PATHSEG_CURVETO_CUBIC_SMOOTH_REL => { + debug_assert!(i + 3 < path.len()); + result.push(PathCommand::SmoothCurveTo { + control2: CoordPair::new(path[i], path[i + 1]), + point: CoordPair::new(path[i + 2], path[i + 3]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_CUBIC_SMOOTH_ABS), + }); + i = i + 4; + }, + PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS | PATHSEG_CURVETO_QUADRATIC_SMOOTH_REL => { + debug_assert!(i + 1 < path.len()); + result.push(PathCommand::SmoothQuadBezierCurveTo { + point: CoordPair::new(path[i], path[i + 1]), + absolute: IsAbsolute::new(seg_type == PATHSEG_CURVETO_QUADRATIC_SMOOTH_ABS), + }); + i = i + 2; + }, + PATHSEG_UNKNOWN | _ => return Err(()), + } + } + + Ok(SVGPathData(crate::ArcSlice::from_iter(result.into_iter()))) + } + + /// Parse this SVG path string with the argument that indicates whether we should allow the + /// empty string. + // We cannot use cssparser::Parser to parse a SVG path string because the spec wants to make + // the SVG path string as compact as possible. (i.e. The whitespaces may be dropped.) + // e.g. "M100 200L100 200" is a valid SVG path string. If we use tokenizer, the first ident + // is "M100", instead of "M", and this is not correct. Therefore, we use a Peekable + // str::Char iterator to check each character. + pub fn parse<'i, 't>( + input: &mut Parser<'i, 't>, + allow_empty: AllowEmpty, + ) -> Result<Self, ParseError<'i>> { + let location = input.current_source_location(); + let path_string = input.expect_string()?.as_ref(); + + // Parse the svg path string as multiple sub-paths. + let mut path_parser = PathParser::new(path_string); + while skip_wsp(&mut path_parser.chars) { + if path_parser.parse_subpath().is_err() { + return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + } + + // The css-shapes-1 says a path data string that does conform but defines an empty path is + // invalid and causes the entire path() to be invalid, so we use the argement to decide + // whether we should allow the empty string. + // https://drafts.csswg.org/css-shapes-1/#typedef-basic-shape + if matches!(allow_empty, AllowEmpty::No) && path_parser.path.is_empty() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + Ok(SVGPathData(crate::ArcSlice::from_iter( + path_parser.path.into_iter(), + ))) + } +} + +impl ToCss for SVGPathData { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + dest.write_char('"')?; + { + let mut writer = SequenceWriter::new(dest, " "); + for command in self.commands() { + writer.item(command)?; + } + } + dest.write_char('"') + } +} + +impl Parse for SVGPathData { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // Note that the EBNF allows the path data string in the d property to be empty, so we + // don't reject empty SVG path data. + // https://svgwg.org/svg2-draft/single-page.html#paths-PathDataBNF + SVGPathData::parse(input, AllowEmpty::Yes) + } +} + +impl Animate for SVGPathData { + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + if self.0.len() != other.0.len() { + return Err(()); + } + + // FIXME(emilio): This allocates three copies of the path, that's not + // great! Specially, once we're normalized once, we don't need to + // re-normalize again. + let left = self.normalize(); + let right = other.normalize(); + + let items: Vec<_> = lists::by_computed_value::animate(&left.0, &right.0, procedure)?; + Ok(SVGPathData(crate::ArcSlice::from_iter(items.into_iter()))) + } +} + +impl ComputeSquaredDistance for SVGPathData { + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + if self.0.len() != other.0.len() { + return Err(()); + } + let left = self.normalize(); + let right = other.normalize(); + lists::by_computed_value::squared_distance(&left.0, &right.0) + } +} + +/// The SVG path command. +/// The fields of these commands are self-explanatory, so we skip the documents. +/// Note: the index of the control points, e.g. control1, control2, are mapping to the control +/// points of the Bézier curve in the spec. +/// +/// https://www.w3.org/TR/SVG11/paths.html#PathData +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(C, u8)] +pub enum PathCommand { + /// The unknown type. + /// https://www.w3.org/TR/SVG/paths.html#__svg__SVGPathSeg__PATHSEG_UNKNOWN + Unknown, + /// The "moveto" command. + MoveTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The "lineto" command. + LineTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The horizontal "lineto" command. + HorizontalLineTo { x: CSSFloat, absolute: IsAbsolute }, + /// The vertical "lineto" command. + VerticalLineTo { y: CSSFloat, absolute: IsAbsolute }, + /// The cubic Bézier curve command. + CurveTo { + control1: CoordPair, + control2: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The smooth curve command. + SmoothCurveTo { + control2: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The quadratic Bézier curve command. + QuadBezierCurveTo { + control1: CoordPair, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The smooth quadratic Bézier curve command. + SmoothQuadBezierCurveTo { + point: CoordPair, + absolute: IsAbsolute, + }, + /// The elliptical arc curve command. + EllipticalArc { + rx: CSSFloat, + ry: CSSFloat, + angle: CSSFloat, + large_arc_flag: ArcFlag, + sweep_flag: ArcFlag, + point: CoordPair, + absolute: IsAbsolute, + }, + /// The "closepath" command. + ClosePath, +} + +/// For internal SVGPath normalization. +#[allow(missing_docs)] +struct PathTraversalState { + subpath_start: CoordPair, + pos: CoordPair, +} + +impl PathCommand { + /// Create a normalized copy of this PathCommand. Absolute commands will be copied as-is while + /// for relative commands an equivalent absolute command will be returned. + /// + /// See discussion: https://github.com/w3c/svgwg/issues/321 + fn normalize(&self, state: &mut PathTraversalState) -> Self { + use self::PathCommand::*; + match *self { + Unknown => Unknown, + ClosePath => { + state.pos = state.subpath_start; + ClosePath + }, + MoveTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + state.subpath_start = point; + MoveTo { + point, + absolute: IsAbsolute::Yes, + } + }, + LineTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + LineTo { + point, + absolute: IsAbsolute::Yes, + } + }, + HorizontalLineTo { mut x, absolute } => { + if !absolute.is_yes() { + x += state.pos.x; + } + state.pos.x = x; + HorizontalLineTo { + x, + absolute: IsAbsolute::Yes, + } + }, + VerticalLineTo { mut y, absolute } => { + if !absolute.is_yes() { + y += state.pos.y; + } + state.pos.y = y; + VerticalLineTo { + y, + absolute: IsAbsolute::Yes, + } + }, + CurveTo { + mut control1, + mut control2, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control1 += state.pos; + control2 += state.pos; + point += state.pos; + } + state.pos = point; + CurveTo { + control1, + control2, + point, + absolute: IsAbsolute::Yes, + } + }, + SmoothCurveTo { + mut control2, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control2 += state.pos; + point += state.pos; + } + state.pos = point; + SmoothCurveTo { + control2, + point, + absolute: IsAbsolute::Yes, + } + }, + QuadBezierCurveTo { + mut control1, + mut point, + absolute, + } => { + if !absolute.is_yes() { + control1 += state.pos; + point += state.pos; + } + state.pos = point; + QuadBezierCurveTo { + control1, + point, + absolute: IsAbsolute::Yes, + } + }, + SmoothQuadBezierCurveTo { + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + SmoothQuadBezierCurveTo { + point, + absolute: IsAbsolute::Yes, + } + }, + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + mut point, + absolute, + } => { + if !absolute.is_yes() { + point += state.pos; + } + state.pos = point; + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + point, + absolute: IsAbsolute::Yes, + } + }, + } + } +} + +impl ToCss for PathCommand { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + use self::PathCommand::*; + match *self { + Unknown => dest.write_char('X'), + ClosePath => dest.write_char('Z'), + MoveTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'M' } else { 'm' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + LineTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'L' } else { 'l' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + CurveTo { + control1, + control2, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'C' } else { 'c' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + QuadBezierCurveTo { + control1, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'Q' } else { 'q' })?; + dest.write_char(' ')?; + control1.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + EllipticalArc { + rx, + ry, + angle, + large_arc_flag, + sweep_flag, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'A' } else { 'a' })?; + dest.write_char(' ')?; + rx.to_css(dest)?; + dest.write_char(' ')?; + ry.to_css(dest)?; + dest.write_char(' ')?; + angle.to_css(dest)?; + dest.write_char(' ')?; + large_arc_flag.to_css(dest)?; + dest.write_char(' ')?; + sweep_flag.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + HorizontalLineTo { x, absolute } => { + dest.write_char(if absolute.is_yes() { 'H' } else { 'h' })?; + dest.write_char(' ')?; + x.to_css(dest) + }, + VerticalLineTo { y, absolute } => { + dest.write_char(if absolute.is_yes() { 'V' } else { 'v' })?; + dest.write_char(' ')?; + y.to_css(dest) + }, + SmoothCurveTo { + control2, + point, + absolute, + } => { + dest.write_char(if absolute.is_yes() { 'S' } else { 's' })?; + dest.write_char(' ')?; + control2.to_css(dest)?; + dest.write_char(' ')?; + point.to_css(dest) + }, + SmoothQuadBezierCurveTo { point, absolute } => { + dest.write_char(if absolute.is_yes() { 'T' } else { 't' })?; + dest.write_char(' ')?; + point.to_css(dest) + }, + } + } +} + +/// The path command absolute type. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum IsAbsolute { + Yes, + No, +} + +impl IsAbsolute { + /// Return true if this is IsAbsolute::Yes. + #[inline] + pub fn is_yes(&self) -> bool { + *self == IsAbsolute::Yes + } + + /// Return Yes if value is true. Otherwise, return No. + #[inline] + fn new(value: bool) -> Self { + if value { + IsAbsolute::Yes + } else { + IsAbsolute::No + } + } +} + +/// The path coord type. +#[allow(missing_docs)] +#[derive( + AddAssign, + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedZero, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct CoordPair { + x: CSSFloat, + y: CSSFloat, +} + +impl CoordPair { + /// Create a CoordPair. + #[inline] + pub fn new(x: CSSFloat, y: CSSFloat) -> Self { + CoordPair { x, y } + } +} + +/// The EllipticalArc flag type. +#[derive( + Clone, + Copy, + Debug, + Deserialize, + MallocSizeOf, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +pub struct ArcFlag(bool); + +impl ToCss for ArcFlag { + #[inline] + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: fmt::Write, + { + (self.0 as i32).to_css(dest) + } +} + +impl Animate for ArcFlag { + #[inline] + fn animate(&self, other: &Self, procedure: Procedure) -> Result<Self, ()> { + (self.0 as i32) + .animate(&(other.0 as i32), procedure) + .map(|v| ArcFlag(v > 0)) + } +} + +impl ComputeSquaredDistance for ArcFlag { + #[inline] + fn compute_squared_distance(&self, other: &Self) -> Result<SquaredDistance, ()> { + (self.0 as i32).compute_squared_distance(&(other.0 as i32)) + } +} + +impl ToAnimatedZero for ArcFlag { + #[inline] + fn to_animated_zero(&self) -> Result<Self, ()> { + // The 2 ArcFlags in EllipticalArc determine which one of the 4 different arcs will be + // used. (i.e. From 4 combinations). In other words, if we change the flag, we get a + // different arc. Therefore, we return *self. + // https://svgwg.org/svg2-draft/paths.html#PathDataEllipticalArcCommands + Ok(*self) + } +} + +/// SVG Path parser. +struct PathParser<'a> { + chars: Peekable<Cloned<slice::Iter<'a, u8>>>, + path: Vec<PathCommand>, +} + +macro_rules! parse_arguments { + ( + $parser:ident, + $abs:ident, + $enum:ident, + [ $para:ident => $func:ident $(, $other_para:ident => $other_func:ident)* ] + ) => { + { + loop { + let $para = $func(&mut $parser.chars)?; + $( + skip_comma_wsp(&mut $parser.chars); + let $other_para = $other_func(&mut $parser.chars)?; + )* + $parser.path.push(PathCommand::$enum { $para $(, $other_para)*, $abs }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut $parser.chars) || + $parser.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) { + break; + } + skip_comma_wsp(&mut $parser.chars); + } + Ok(()) + } + } +} + +impl<'a> PathParser<'a> { + /// Return a PathParser. + #[inline] + fn new(string: &'a str) -> Self { + PathParser { + chars: string.as_bytes().iter().cloned().peekable(), + path: Vec::new(), + } + } + + /// Parse a sub-path. + fn parse_subpath(&mut self) -> Result<(), ()> { + // Handle "moveto" Command first. If there is no "moveto", this is not a valid sub-path + // (i.e. not a valid moveto-drawto-command-group). + self.parse_moveto()?; + + // Handle other commands. + loop { + skip_wsp(&mut self.chars); + if self.chars.peek().map_or(true, |&m| m == b'M' || m == b'm') { + break; + } + + let command = self.chars.next().unwrap(); + let abs = if command.is_ascii_uppercase() { + IsAbsolute::Yes + } else { + IsAbsolute::No + }; + + skip_wsp(&mut self.chars); + match command { + b'Z' | b'z' => self.parse_closepath(), + b'L' | b'l' => self.parse_lineto(abs), + b'H' | b'h' => self.parse_h_lineto(abs), + b'V' | b'v' => self.parse_v_lineto(abs), + b'C' | b'c' => self.parse_curveto(abs), + b'S' | b's' => self.parse_smooth_curveto(abs), + b'Q' | b'q' => self.parse_quadratic_bezier_curveto(abs), + b'T' | b't' => self.parse_smooth_quadratic_bezier_curveto(abs), + b'A' | b'a' => self.parse_elliptical_arc(abs), + _ => return Err(()), + }?; + } + Ok(()) + } + + /// Parse "moveto" command. + fn parse_moveto(&mut self) -> Result<(), ()> { + let command = match self.chars.next() { + Some(c) if c == b'M' || c == b'm' => c, + _ => return Err(()), + }; + + skip_wsp(&mut self.chars); + let point = parse_coord(&mut self.chars)?; + let absolute = if command == b'M' { + IsAbsolute::Yes + } else { + IsAbsolute::No + }; + self.path.push(PathCommand::MoveTo { point, absolute }); + + // End of string or the next character is a possible new command. + if !skip_wsp(&mut self.chars) || self.chars.peek().map_or(true, |c| c.is_ascii_alphabetic()) + { + return Ok(()); + } + skip_comma_wsp(&mut self.chars); + + // If a moveto is followed by multiple pairs of coordinates, the subsequent + // pairs are treated as implicit lineto commands. + self.parse_lineto(absolute) + } + + /// Parse "closepath" command. + fn parse_closepath(&mut self) -> Result<(), ()> { + self.path.push(PathCommand::ClosePath); + Ok(()) + } + + /// Parse "lineto" command. + fn parse_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, LineTo, [ point => parse_coord ]) + } + + /// Parse horizontal "lineto" command. + fn parse_h_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, HorizontalLineTo, [ x => parse_number ]) + } + + /// Parse vertical "lineto" command. + fn parse_v_lineto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, VerticalLineTo, [ y => parse_number ]) + } + + /// Parse cubic Bézier curve command. + fn parse_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, CurveTo, [ + control1 => parse_coord, control2 => parse_coord, point => parse_coord + ]) + } + + /// Parse smooth "curveto" command. + fn parse_smooth_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, SmoothCurveTo, [ + control2 => parse_coord, point => parse_coord + ]) + } + + /// Parse quadratic Bézier curve command. + fn parse_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, QuadBezierCurveTo, [ + control1 => parse_coord, point => parse_coord + ]) + } + + /// Parse smooth quadratic Bézier curveto command. + fn parse_smooth_quadratic_bezier_curveto(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + parse_arguments!(self, absolute, SmoothQuadBezierCurveTo, [ point => parse_coord ]) + } + + /// Parse elliptical arc curve command. + fn parse_elliptical_arc(&mut self, absolute: IsAbsolute) -> Result<(), ()> { + // Parse a flag whose value is '0' or '1'; otherwise, return Err(()). + let parse_flag = |iter: &mut Peekable<Cloned<slice::Iter<u8>>>| match iter.next() { + Some(c) if c == b'0' || c == b'1' => Ok(ArcFlag(c == b'1')), + _ => Err(()), + }; + parse_arguments!(self, absolute, EllipticalArc, [ + rx => parse_number, + ry => parse_number, + angle => parse_number, + large_arc_flag => parse_flag, + sweep_flag => parse_flag, + point => parse_coord + ]) + } +} + +/// Parse a pair of numbers into CoordPair. +fn parse_coord(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CoordPair, ()> { + let x = parse_number(iter)?; + skip_comma_wsp(iter); + let y = parse_number(iter)?; + Ok(CoordPair::new(x, y)) +} + +/// This is a special version which parses the number for SVG Path. e.g. "M 0.6.5" should be parsed +/// as MoveTo with a coordinate of ("0.6", ".5"), instead of treating 0.6.5 as a non-valid floating +/// point number. In other words, the logic here is similar with that of +/// tokenizer::consume_numeric, which also consumes the number as many as possible, but here the +/// input is a Peekable and we only accept an integer of a floating point number. +/// +/// The "number" syntax in https://www.w3.org/TR/SVG/paths.html#PathDataBNF +fn parse_number(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> Result<CSSFloat, ()> { + // 1. Check optional sign. + let sign = if iter + .peek() + .map_or(false, |&sign| sign == b'+' || sign == b'-') + { + if iter.next().unwrap() == b'-' { + -1. + } else { + 1. + } + } else { + 1. + }; + + // 2. Check integer part. + let mut integral_part: f64 = 0.; + let got_dot = if !iter.peek().map_or(false, |&n| n == b'.') { + // If the first digit in integer part is neither a dot nor a digit, this is not a number. + if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { + return Err(()); + } + + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + integral_part = integral_part * 10. + (iter.next().unwrap() - b'0') as f64; + } + + iter.peek().map_or(false, |&n| n == b'.') + } else { + true + }; + + // 3. Check fractional part. + let mut fractional_part: f64 = 0.; + if got_dot { + // Consume '.'. + iter.next(); + // If the first digit in fractional part is not a digit, this is not a number. + if iter.peek().map_or(true, |n| !n.is_ascii_digit()) { + return Err(()); + } + + let mut factor = 0.1; + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + fractional_part += (iter.next().unwrap() - b'0') as f64 * factor; + factor *= 0.1; + } + } + + let mut value = sign * (integral_part + fractional_part); + + // 4. Check exp part. The segment name of SVG Path doesn't include 'E' or 'e', so it's ok to + // treat the numbers after 'E' or 'e' are in the exponential part. + if iter.peek().map_or(false, |&exp| exp == b'E' || exp == b'e') { + // Consume 'E' or 'e'. + iter.next(); + let exp_sign = if iter + .peek() + .map_or(false, |&sign| sign == b'+' || sign == b'-') + { + if iter.next().unwrap() == b'-' { + -1. + } else { + 1. + } + } else { + 1. + }; + + let mut exp: f64 = 0.; + while iter.peek().map_or(false, |n| n.is_ascii_digit()) { + exp = exp * 10. + (iter.next().unwrap() - b'0') as f64; + } + + value *= f64::powf(10., exp * exp_sign); + } + + if value.is_finite() { + Ok(value.min(f32::MAX as f64).max(f32::MIN as f64) as CSSFloat) + } else { + Err(()) + } +} + +/// Skip all svg whitespaces, and return true if |iter| hasn't finished. +#[inline] +fn skip_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { + // Note: SVG 1.1 defines the whitespaces as \u{9}, \u{20}, \u{A}, \u{D}. + // However, SVG 2 has one extra whitespace: \u{C}. + // Therefore, we follow the newest spec for the definition of whitespace, + // i.e. \u{9}, \u{20}, \u{A}, \u{C}, \u{D}. + while iter.peek().map_or(false, |c| c.is_ascii_whitespace()) { + iter.next(); + } + iter.peek().is_some() +} + +/// Skip all svg whitespaces and one comma, and return true if |iter| hasn't finished. +#[inline] +fn skip_comma_wsp(iter: &mut Peekable<Cloned<slice::Iter<u8>>>) -> bool { + if !skip_wsp(iter) { + return false; + } + + if *iter.peek().unwrap() != b',' { + return true; + } + iter.next(); + + skip_wsp(iter) +} diff --git a/servo/components/style/values/specified/table.rs b/servo/components/style/values/specified/table.rs new file mode 100644 index 0000000000..88f917ac78 --- /dev/null +++ b/servo/components/style/values/specified/table.rs @@ -0,0 +1,36 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values related to tables. + +/// Specified values for the `caption-side` property. +/// +/// Note that despite having "physical" names, these are actually interpreted +/// according to the table's writing-mode: Top and Bottom are treated as +/// block-start and -end respectively. +/// +/// https://drafts.csswg.org/css-tables/#propdef-caption-side +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Ord, + Parse, + PartialEq, + PartialOrd, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum CaptionSide { + Top, + Bottom, +} diff --git a/servo/components/style/values/specified/text.rs b/servo/components/style/values/specified/text.rs new file mode 100644 index 0000000000..0e70bd26ac --- /dev/null +++ b/servo/components/style/values/specified/text.rs @@ -0,0 +1,1193 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for text properties. + +use crate::parser::{Parse, ParserContext}; +use crate::properties::longhands::writing_mode::computed_value::T as SpecifiedWritingMode; +use crate::values::computed::text::TextEmphasisStyle as ComputedTextEmphasisStyle; +use crate::values::computed::text::TextOverflow as ComputedTextOverflow; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::generics::text::InitialLetter as GenericInitialLetter; +use crate::values::generics::text::{GenericTextDecorationLength, GenericTextIndent, Spacing}; +use crate::values::specified::length::{Length, LengthPercentage}; +use crate::values::specified::{AllowQuirks, Integer, Number}; +use cssparser::{Parser, Token}; +use icu_segmenter::GraphemeClusterSegmenter; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Write}; +use style_traits::values::SequenceWriter; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; +use style_traits::{KeywordsCollectFn, SpecifiedValueInfo}; + +/// A specified type for the `initial-letter` property. +pub type InitialLetter = GenericInitialLetter<Number, Integer>; + +/// A specified value for the `letter-spacing` property. +pub type LetterSpacing = Spacing<Length>; + +/// A specified value for the `word-spacing` property. +pub type WordSpacing = Spacing<LengthPercentage>; + +/// A value for the `hyphenate-character` property. +#[derive( + Clone, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum HyphenateCharacter { + /// `auto` + Auto, + /// `<string>` + String(crate::OwnedStr), +} + +impl Parse for InitialLetter { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|i| i.expect_ident_matching("normal")) + .is_ok() + { + return Ok(GenericInitialLetter::Normal); + } + let size = Number::parse_at_least_one(context, input)?; + let sink = input + .try_parse(|i| Integer::parse_positive(context, i)) + .ok(); + Ok(GenericInitialLetter::Specified(size, sink)) + } +} + +impl Parse for LetterSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Spacing::parse_with(context, input, |c, i| { + Length::parse_quirky(c, i, AllowQuirks::Yes) + }) + } +} + +impl Parse for WordSpacing { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Spacing::parse_with(context, input, |c, i| { + LengthPercentage::parse_quirky(c, i, AllowQuirks::Yes) + }) + } +} + +/// A generic value for the `text-overflow` property. +#[derive( + Clone, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C, u8)] +pub enum TextOverflowSide { + /// Clip inline content. + Clip, + /// Render ellipsis to represent clipped inline content. + Ellipsis, + /// Render a given string to represent clipped inline content. + String(crate::OwnedStr), +} + +#[derive(Clone, Debug, Eq, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +/// text-overflow. Specifies rendering when inline content overflows its line box edge. +pub struct TextOverflow { + /// First value. Applies to end line box edge if no second is supplied; line-left edge otherwise. + pub first: TextOverflowSide, + /// Second value. Applies to the line-right edge if supplied. + pub second: Option<TextOverflowSide>, +} + +impl Parse for TextOverflow { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TextOverflow, ParseError<'i>> { + let first = TextOverflowSide::parse(context, input)?; + let second = input + .try_parse(|input| TextOverflowSide::parse(context, input)) + .ok(); + Ok(TextOverflow { first, second }) + } +} + +impl ToComputedValue for TextOverflow { + type ComputedValue = ComputedTextOverflow; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + if let Some(ref second) = self.second { + Self::ComputedValue { + first: self.first.clone(), + second: second.clone(), + sides_are_logical: false, + } + } else { + Self::ComputedValue { + first: TextOverflowSide::Clip, + second: self.first.clone(), + sides_are_logical: true, + } + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + if computed.sides_are_logical { + assert_eq!(computed.first, TextOverflowSide::Clip); + TextOverflow { + first: computed.second.clone(), + second: None, + } + } else { + TextOverflow { + first: computed.first.clone(), + second: Some(computed.second.clone()), + } + } + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + Serialize, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags(single = "none", mixed = "underline,overline,line-through,blink"))] +#[repr(C)] +/// Specified keyword values for the text-decoration-line property. +pub struct TextDecorationLine(u8); +bitflags! { + impl TextDecorationLine: u8 { + /// No text decoration line is specified. + const NONE = 0; + /// underline + const UNDERLINE = 1 << 0; + /// overline + const OVERLINE = 1 << 1; + /// line-through + const LINE_THROUGH = 1 << 2; + /// blink + const BLINK = 1 << 3; + /// Only set by presentation attributes + /// + /// Setting this will mean that text-decorations use the color + /// specified by `color` in quirks mode. + /// + /// For example, this gives <a href=foo><font color="red">text</font></a> + /// a red text decoration + #[cfg(feature = "gecko")] + const COLOR_OVERRIDE = 0x10; + } +} + +impl Default for TextDecorationLine { + fn default() -> Self { + TextDecorationLine::NONE + } +} + +impl TextDecorationLine { + #[inline] + /// Returns the initial value of text-decoration-line + pub fn none() -> Self { + TextDecorationLine::NONE + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Specified value of the text-transform property, stored in two parts: +/// the case-related transforms (mutually exclusive, only one may be in effect), and others (non-exclusive). +pub struct TextTransform { + /// Case transform, if any. + pub case_: TextTransformCase, + /// Non-case transforms. + pub other_: TextTransformOther, +} + +impl TextTransform { + #[inline] + /// Returns the initial value of text-transform + pub fn none() -> Self { + TextTransform { + case_: TextTransformCase::None, + other_: TextTransformOther::empty(), + } + } + #[inline] + /// Returns whether the value is 'none' + pub fn is_none(&self) -> bool { + self.case_ == TextTransformCase::None && self.other_.is_empty() + } +} + +// TODO: This can be simplified by deriving it. +impl Parse for TextTransform { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut result = TextTransform::none(); + + // Case keywords are mutually exclusive; other transforms may co-occur. + loop { + let location = input.current_source_location(); + let ident = match input.next() { + Ok(&Token::Ident(ref ident)) => ident, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + Err(..) => break, + }; + + match_ignore_ascii_case! { ident, + "none" if result.is_none() => { + return Ok(result); + }, + "uppercase" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Uppercase + }, + "lowercase" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Lowercase + }, + "capitalize" if result.case_ == TextTransformCase::None => { + result.case_ = TextTransformCase::Capitalize + }, + "math-auto" if result.case_ == TextTransformCase::None && + result.other_.is_empty() => { + result.case_ = TextTransformCase::MathAuto; + return Ok(result); + }, + "full-width" if !result.other_.intersects(TextTransformOther::FULL_WIDTH) => { + result.other_.insert(TextTransformOther::FULL_WIDTH) + }, + "full-size-kana" if !result.other_.intersects(TextTransformOther::FULL_SIZE_KANA) => { + result.other_.insert(TextTransformOther::FULL_SIZE_KANA) + }, + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + } + } + + if result.is_none() { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } else { + Ok(result) + } + } +} + +impl ToCss for TextTransform { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_none() { + return dest.write_str("none"); + } + + if self.case_ != TextTransformCase::None { + self.case_.to_css(dest)?; + if !self.other_.is_empty() { + dest.write_char(' ')?; + } + } + + self.other_.to_css(dest) + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +/// Specified keyword values for case transforms in the text-transform property. (These are exclusive.) +pub enum TextTransformCase { + /// No case transform. + None, + /// All uppercase. + Uppercase, + /// All lowercase. + Lowercase, + /// Capitalize each word. + Capitalize, + /// Automatic italicization of math variables. + MathAuto, +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + Serialize, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[css(bitflags(mixed = "full-width,full-size-kana"))] +#[repr(C)] +/// Specified keyword values for non-case transforms in the text-transform property. (Non-exclusive.) +pub struct TextTransformOther(u8); +bitflags! { + impl TextTransformOther: u8 { + /// full-width + const FULL_WIDTH = 1 << 0; + /// full-size-kana + const FULL_SIZE_KANA = 1 << 1; + } +} + +/// Specified and computed value of text-align-last. +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum TextAlignLast { + Auto, + Start, + End, + Left, + Right, + Center, + Justify, +} + +/// Specified value of text-align keyword value. +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + Hash, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +#[repr(u8)] +pub enum TextAlignKeyword { + Start, + Left, + Right, + Center, + #[cfg(any(feature = "gecko", feature = "servo-layout-2013"))] + Justify, + #[css(skip)] + #[cfg(feature = "gecko")] + Char, + End, + #[cfg(feature = "gecko")] + MozCenter, + #[cfg(feature = "gecko")] + MozLeft, + #[cfg(feature = "gecko")] + MozRight, + #[cfg(feature = "servo-layout-2013")] + ServoCenter, + #[cfg(feature = "servo-layout-2013")] + ServoLeft, + #[cfg(feature = "servo-layout-2013")] + ServoRight, +} + +/// Specified value of text-align property. +#[derive( + Clone, Copy, Debug, Eq, Hash, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem, +)] +pub enum TextAlign { + /// Keyword value of text-align property. + Keyword(TextAlignKeyword), + /// `match-parent` value of text-align property. It has a different handling + /// unlike other keywords. + #[cfg(feature = "gecko")] + MatchParent, + /// This is how we implement the following HTML behavior from + /// https://html.spec.whatwg.org/#tables-2: + /// + /// User agents are expected to have a rule in their user agent style sheet + /// that matches th elements that have a parent node whose computed value + /// for the 'text-align' property is its initial value, whose declaration + /// block consists of just a single declaration that sets the 'text-align' + /// property to the value 'center'. + /// + /// Since selectors can't depend on the ancestor styles, we implement it with a + /// magic value that computes to the right thing. Since this is an + /// implementation detail, it shouldn't be exposed to web content. + #[cfg(feature = "gecko")] + #[parse(condition = "ParserContext::chrome_rules_enabled")] + MozCenterOrInherit, +} + +impl ToComputedValue for TextAlign { + type ComputedValue = TextAlignKeyword; + + #[inline] + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + match *self { + TextAlign::Keyword(key) => key, + #[cfg(feature = "gecko")] + TextAlign::MatchParent => { + // on the root <html> element we should still respect the dir + // but the parent dir of that element is LTR even if it's <html dir=rtl> + // and will only be RTL if certain prefs have been set. + // In that case, the default behavior here will set it to left, + // but we want to set it to right -- instead set it to the default (`start`), + // which will do the right thing in this case (but not the general case) + if _context.builder.is_root_element { + return TextAlignKeyword::Start; + } + let parent = _context + .builder + .get_parent_inherited_text() + .clone_text_align(); + let ltr = _context.builder.inherited_writing_mode().is_bidi_ltr(); + match (parent, ltr) { + (TextAlignKeyword::Start, true) => TextAlignKeyword::Left, + (TextAlignKeyword::Start, false) => TextAlignKeyword::Right, + (TextAlignKeyword::End, true) => TextAlignKeyword::Right, + (TextAlignKeyword::End, false) => TextAlignKeyword::Left, + _ => parent, + } + }, + #[cfg(feature = "gecko")] + TextAlign::MozCenterOrInherit => { + let parent = _context + .builder + .get_parent_inherited_text() + .clone_text_align(); + if parent == TextAlignKeyword::Start { + TextAlignKeyword::Center + } else { + parent + } + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + TextAlign::Keyword(*computed) + } +} + +fn fill_mode_is_default_and_shape_exists( + fill: &TextEmphasisFillMode, + shape: &Option<TextEmphasisShapeKeyword>, +) -> bool { + shape.is_some() && fill.is_filled() +} + +/// Specified value of text-emphasis-style property. +/// +/// https://drafts.csswg.org/css-text-decor/#propdef-text-emphasis-style +#[derive(Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +#[allow(missing_docs)] +pub enum TextEmphasisStyle { + /// [ <fill> || <shape> ] + Keyword { + #[css(contextual_skip_if = "fill_mode_is_default_and_shape_exists")] + fill: TextEmphasisFillMode, + shape: Option<TextEmphasisShapeKeyword>, + }, + /// `none` + None, + /// `<string>` (of which only the first grapheme cluster will be used). + String(crate::OwnedStr), +} + +/// Fill mode for the text-emphasis-style property +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TextEmphasisFillMode { + /// `filled` + Filled, + /// `open` + Open, +} + +impl TextEmphasisFillMode { + /// Whether the value is `filled`. + #[inline] + pub fn is_filled(&self) -> bool { + matches!(*self, TextEmphasisFillMode::Filled) + } +} + +/// Shape keyword for the text-emphasis-style property +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TextEmphasisShapeKeyword { + /// `dot` + Dot, + /// `circle` + Circle, + /// `double-circle` + DoubleCircle, + /// `triangle` + Triangle, + /// `sesame` + Sesame, +} + +impl ToComputedValue for TextEmphasisStyle { + type ComputedValue = ComputedTextEmphasisStyle; + + #[inline] + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + TextEmphasisStyle::Keyword { fill, shape } => { + let shape = shape.unwrap_or_else(|| { + // FIXME(emilio, bug 1572958): This should set the + // rule_cache_conditions properly. + // + // Also should probably use WritingMode::is_vertical rather + // than the computed value of the `writing-mode` property. + if context.style().get_inherited_box().clone_writing_mode() == + SpecifiedWritingMode::HorizontalTb + { + TextEmphasisShapeKeyword::Circle + } else { + TextEmphasisShapeKeyword::Sesame + } + }); + ComputedTextEmphasisStyle::Keyword { fill, shape } + }, + TextEmphasisStyle::None => ComputedTextEmphasisStyle::None, + TextEmphasisStyle::String(ref s) => { + // FIXME(emilio): Doing this at computed value time seems wrong. + // The spec doesn't say that this should be a computed-value + // time operation. This is observable from getComputedStyle(). + // + // Note that the first grapheme cluster boundary should always be the start of the string. + let first_grapheme_end = GraphemeClusterSegmenter::new().segment_str(s).nth(1).unwrap_or(0); + ComputedTextEmphasisStyle::String(s[0..first_grapheme_end].to_string().into()) + }, + } + } + + #[inline] + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + match *computed { + ComputedTextEmphasisStyle::Keyword { fill, shape } => TextEmphasisStyle::Keyword { + fill, + shape: Some(shape), + }, + ComputedTextEmphasisStyle::None => TextEmphasisStyle::None, + ComputedTextEmphasisStyle::String(ref string) => { + TextEmphasisStyle::String(string.clone()) + }, + } + } +} + +impl Parse for TextEmphasisStyle { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(TextEmphasisStyle::None); + } + + if let Ok(s) = input.try_parse(|i| i.expect_string().map(|s| s.as_ref().to_owned())) { + // Handle <string> + return Ok(TextEmphasisStyle::String(s.into())); + } + + // Handle a pair of keywords + let mut shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); + let fill = input.try_parse(TextEmphasisFillMode::parse).ok(); + if shape.is_none() { + shape = input.try_parse(TextEmphasisShapeKeyword::parse).ok(); + } + + if shape.is_none() && fill.is_none() { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + // If a shape keyword is specified but neither filled nor open is + // specified, filled is assumed. + let fill = fill.unwrap_or(TextEmphasisFillMode::Filled); + + // We cannot do the same because the default `<shape>` depends on the + // computed writing-mode. + Ok(TextEmphasisStyle::Keyword { fill, shape }) + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + Parse, + Serialize, + SpecifiedValueInfo, + ToCss, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(C)] +#[css(bitflags( + mixed = "over,under,left,right", + validate_mixed = "Self::validate_and_simplify" +))] +/// Values for text-emphasis-position: +/// <https://drafts.csswg.org/css-text-decor/#text-emphasis-position-property> +pub struct TextEmphasisPosition(u8); +bitflags! { + impl TextEmphasisPosition: u8 { + /// Draws marks to the right of the text in vertical writing mode. + const OVER = 1 << 0; + /// Draw marks under the text in horizontal writing mode. + const UNDER = 1 << 1; + /// Draw marks to the left of the text in vertical writing mode. + const LEFT = 1 << 2; + /// Draws marks to the right of the text in vertical writing mode. + const RIGHT = 1 << 3; + } +} + +impl TextEmphasisPosition { + fn validate_and_simplify(&mut self) -> bool { + if self.intersects(Self::OVER) == self.intersects(Self::UNDER) { + return false; + } + + if self.intersects(Self::LEFT) { + return !self.intersects(Self::RIGHT); + } + + self.remove(Self::RIGHT); // Right is the default + true + } +} + +/// Values for the `word-break` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum WordBreak { + Normal, + BreakAll, + KeepAll, + /// The break-word value, needed for compat. + /// + /// Specifying `word-break: break-word` makes `overflow-wrap` behave as + /// `anywhere`, and `word-break` behave like `normal`. + #[cfg(feature = "gecko")] + BreakWord, +} + +/// Values for the `text-justify` CSS property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum TextJustify { + Auto, + None, + InterWord, + // See https://drafts.csswg.org/css-text-3/#valdef-text-justify-distribute + // and https://github.com/w3c/csswg-drafts/issues/6156 for the alias. + #[parse(aliases = "distribute")] + InterCharacter, +} + +/// Values for the `-moz-control-character-visibility` CSS property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum MozControlCharacterVisibility { + Hidden, + Visible, +} + +impl Default for MozControlCharacterVisibility { + fn default() -> Self { + if static_prefs::pref!("layout.css.control-characters.visible") { + Self::Visible + } else { + Self::Hidden + } + } +} + +/// Values for the `line-break` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum LineBreak { + Auto, + Loose, + Normal, + Strict, + Anywhere, +} + +/// Values for the `overflow-wrap` property. +#[repr(u8)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum OverflowWrap { + Normal, + BreakWord, + Anywhere, +} + +/// A specified value for the `text-indent` property +/// which takes the grammar of [<length-percentage>] && hanging? && each-line? +/// +/// https://drafts.csswg.org/css-text/#propdef-text-indent +pub type TextIndent = GenericTextIndent<LengthPercentage>; + +impl Parse for TextIndent { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut length = None; + let mut hanging = false; + let mut each_line = false; + + // The length-percentage and the two possible keywords can occur in any order. + while !input.is_exhausted() { + // If we haven't seen a length yet, try to parse one. + if length.is_none() { + if let Ok(len) = input + .try_parse(|i| LengthPercentage::parse_quirky(context, i, AllowQuirks::Yes)) + { + length = Some(len); + continue; + } + } + + if static_prefs::pref!("layout.css.text-indent-keywords.enabled") { + // Check for the keywords (boolean flags). + try_match_ident_ignore_ascii_case! { input, + "hanging" if !hanging => hanging = true, + "each-line" if !each_line => each_line = true, + } + continue; + } + + // If we reach here, there must be something that we failed to parse; + // just break and let the caller deal with it. + break; + } + + // The length-percentage value is required for the declaration to be valid. + if let Some(length) = length { + Ok(Self { + length, + hanging, + each_line, + }) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +/// Implements text-decoration-skip-ink which takes the keywords auto | none | all +/// +/// https://drafts.csswg.org/css-text-decor-4/#text-decoration-skip-ink-property +#[repr(u8)] +#[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[allow(missing_docs)] +pub enum TextDecorationSkipInk { + Auto, + None, + All, +} + +/// Implements type for `text-decoration-thickness` property +pub type TextDecorationLength = GenericTextDecorationLength<LengthPercentage>; + +impl TextDecorationLength { + /// `Auto` value. + #[inline] + pub fn auto() -> Self { + GenericTextDecorationLength::Auto + } + + /// Whether this is the `Auto` value. + #[inline] + pub fn is_auto(&self) -> bool { + matches!(*self, GenericTextDecorationLength::Auto) + } +} + +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[value_info(other_values = "auto,from-font,under,left,right")] +#[repr(C)] +/// Specified keyword values for the text-underline-position property. +/// (Non-exclusive, but not all combinations are allowed: the spec grammar gives +/// `auto | [ from-font | under ] || [ left | right ]`.) +/// https://drafts.csswg.org/css-text-decor-4/#text-underline-position-property +pub struct TextUnderlinePosition(u8); +bitflags! { + impl TextUnderlinePosition: u8 { + /// Use automatic positioning below the alphabetic baseline. + const AUTO = 0; + /// Use underline position from the first available font. + const FROM_FONT = 1 << 0; + /// Below the glyph box. + const UNDER = 1 << 1; + /// In vertical mode, place to the left of the text. + const LEFT = 1 << 2; + /// In vertical mode, place to the right of the text. + const RIGHT = 1 << 3; + } +} + +// TODO: This can be derived with some care. +impl Parse for TextUnderlinePosition { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<TextUnderlinePosition, ParseError<'i>> { + let mut result = TextUnderlinePosition::empty(); + + loop { + let location = input.current_source_location(); + let ident = match input.next() { + Ok(&Token::Ident(ref ident)) => ident, + Ok(other) => return Err(location.new_unexpected_token_error(other.clone())), + Err(..) => break, + }; + + match_ignore_ascii_case! { ident, + "auto" if result.is_empty() => { + return Ok(result); + }, + "from-font" if !result.intersects(TextUnderlinePosition::FROM_FONT | + TextUnderlinePosition::UNDER) => { + result.insert(TextUnderlinePosition::FROM_FONT); + }, + "under" if !result.intersects(TextUnderlinePosition::FROM_FONT | + TextUnderlinePosition::UNDER) => { + result.insert(TextUnderlinePosition::UNDER); + }, + "left" if !result.intersects(TextUnderlinePosition::LEFT | + TextUnderlinePosition::RIGHT) => { + result.insert(TextUnderlinePosition::LEFT); + }, + "right" if !result.intersects(TextUnderlinePosition::LEFT | + TextUnderlinePosition::RIGHT) => { + result.insert(TextUnderlinePosition::RIGHT); + }, + _ => return Err(location.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(ident.clone()) + )), + } + } + + if !result.is_empty() { + Ok(result) + } else { + Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + } + } +} + +impl ToCss for TextUnderlinePosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + if self.is_empty() { + return dest.write_str("auto"); + } + + let mut writer = SequenceWriter::new(dest, " "); + let mut any = false; + + macro_rules! maybe_write { + ($ident:ident => $str:expr) => { + if self.contains(TextUnderlinePosition::$ident) { + any = true; + writer.raw_item($str)?; + } + }; + } + + maybe_write!(FROM_FONT => "from-font"); + maybe_write!(UNDER => "under"); + maybe_write!(LEFT => "left"); + maybe_write!(RIGHT => "right"); + + debug_assert!(any); + + Ok(()) + } +} + +/// Values for `ruby-position` property +#[repr(u8)] +#[derive( + Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToComputedValue, ToResolvedValue, ToShmem, +)] +#[allow(missing_docs)] +pub enum RubyPosition { + AlternateOver, + AlternateUnder, + Over, + Under, +} + +impl Parse for RubyPosition { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<RubyPosition, ParseError<'i>> { + // Parse alternate before + let alternate = input + .try_parse(|i| i.expect_ident_matching("alternate")) + .is_ok(); + if alternate && input.is_exhausted() { + return Ok(RubyPosition::AlternateOver); + } + // Parse over / under + let over = try_match_ident_ignore_ascii_case! { input, + "over" => true, + "under" => false, + }; + // Parse alternate after + let alternate = alternate || + input + .try_parse(|i| i.expect_ident_matching("alternate")) + .is_ok(); + + Ok(match (over, alternate) { + (true, true) => RubyPosition::AlternateOver, + (false, true) => RubyPosition::AlternateUnder, + (true, false) => RubyPosition::Over, + (false, false) => RubyPosition::Under, + }) + } +} + +impl ToCss for RubyPosition { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(match self { + RubyPosition::AlternateOver => "alternate", + RubyPosition::AlternateUnder => "alternate under", + RubyPosition::Over => "over", + RubyPosition::Under => "under", + }) + } +} + +impl SpecifiedValueInfo for RubyPosition { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["alternate", "over", "under"]) + } +} diff --git a/servo/components/style/values/specified/time.rs b/servo/components/style/values/specified/time.rs new file mode 100644 index 0000000000..3061ebddcc --- /dev/null +++ b/servo/components/style/values/specified/time.rs @@ -0,0 +1,183 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified time values. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::time::Time as ComputedTime; +use crate::values::computed::{Context, ToComputedValue}; +use crate::values::specified::calc::CalcNode; +use crate::values::CSSFloat; +use crate::Zero; +use cssparser::{Parser, Token}; +use std::fmt::{self, Write}; +use style_traits::values::specified::AllowedNumericType; +use style_traits::{CssWriter, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss}; + +/// A time value according to CSS-VALUES § 6.2. +#[derive(Clone, Copy, Debug, MallocSizeOf, PartialEq, ToShmem)] +pub struct Time { + seconds: CSSFloat, + unit: TimeUnit, + calc_clamping_mode: Option<AllowedNumericType>, +} + +/// A time unit. +#[derive(Clone, Copy, Debug, Eq, MallocSizeOf, PartialEq, ToShmem)] +pub enum TimeUnit { + /// `s` + Second, + /// `ms` + Millisecond, +} + +impl Time { + /// Returns a time value that represents `seconds` seconds. + pub fn from_seconds_with_calc_clamping_mode( + seconds: CSSFloat, + calc_clamping_mode: Option<AllowedNumericType>, + ) -> Self { + Time { + seconds, + unit: TimeUnit::Second, + calc_clamping_mode, + } + } + + /// Returns a time value that represents `seconds` seconds. + pub fn from_seconds(seconds: CSSFloat) -> Self { + Self::from_seconds_with_calc_clamping_mode(seconds, None) + } + + /// Returns the time in fractional seconds. + pub fn seconds(self) -> CSSFloat { + self.seconds + } + + /// Returns the unit of the time. + #[inline] + pub fn unit(&self) -> &'static str { + match self.unit { + TimeUnit::Second => "s", + TimeUnit::Millisecond => "ms", + } + } + + #[inline] + fn unitless_value(&self) -> CSSFloat { + match self.unit { + TimeUnit::Second => self.seconds, + TimeUnit::Millisecond => self.seconds * 1000., + } + } + + /// Parses a time according to CSS-VALUES § 6.2. + pub fn parse_dimension(value: CSSFloat, unit: &str) -> Result<Time, ()> { + let (seconds, unit) = match_ignore_ascii_case! { unit, + "s" => (value, TimeUnit::Second), + "ms" => (value / 1000.0, TimeUnit::Millisecond), + _ => return Err(()) + }; + + Ok(Time { + seconds, + unit, + calc_clamping_mode: None, + }) + } + + fn parse_with_clamping_mode<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + clamping_mode: AllowedNumericType, + ) -> Result<Self, ParseError<'i>> { + use style_traits::ParsingMode; + + let location = input.current_source_location(); + match *input.next()? { + // Note that we generally pass ParserContext to is_ok() to check + // that the ParserMode of the ParserContext allows all numeric + // values for SMIL regardless of clamping_mode, but in this Time + // value case, the value does not animate for SMIL at all, so we use + // ParsingMode::DEFAULT directly. + Token::Dimension { + value, ref unit, .. + } if clamping_mode.is_ok(ParsingMode::DEFAULT, value) => { + Time::parse_dimension(value, unit) + .map_err(|()| location.new_custom_error(StyleParseErrorKind::UnspecifiedError)) + }, + Token::Function(ref name) => { + let function = CalcNode::math_function(context, name, location)?; + CalcNode::parse_time(context, input, clamping_mode, function) + }, + ref t => return Err(location.new_unexpected_token_error(t.clone())), + } + } + + /// Parses a non-negative time value. + pub fn parse_non_negative<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::NonNegative) + } +} + +impl Zero for Time { + #[inline] + fn zero() -> Self { + Self::from_seconds(0.0) + } + + #[inline] + fn is_zero(&self) -> bool { + // The unit doesn't matter, i.e. `s` and `ms` are the same for zero. + self.seconds == 0.0 && self.calc_clamping_mode.is_none() + } +} + +impl ToComputedValue for Time { + type ComputedValue = ComputedTime; + + fn to_computed_value(&self, _context: &Context) -> Self::ComputedValue { + let seconds = self + .calc_clamping_mode + .map_or(self.seconds(), |mode| mode.clamp(self.seconds())); + + ComputedTime::from_seconds(crate::values::normalize(seconds)) + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + Time { + seconds: computed.seconds(), + unit: TimeUnit::Second, + calc_clamping_mode: None, + } + } +} + +impl Parse for Time { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Self::parse_with_clamping_mode(context, input, AllowedNumericType::All) + } +} + +impl ToCss for Time { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + crate::values::serialize_specified_dimension( + self.unitless_value(), + self.unit(), + self.calc_clamping_mode.is_some(), + dest, + ) + } +} + +impl SpecifiedValueInfo for Time {} diff --git a/servo/components/style/values/specified/transform.rs b/servo/components/style/values/specified/transform.rs new file mode 100644 index 0000000000..ec5e286bc2 --- /dev/null +++ b/servo/components/style/values/specified/transform.rs @@ -0,0 +1,530 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for CSS values that are related to transformations. + +use crate::parser::{Parse, ParserContext}; +use crate::values::computed::{Context, LengthPercentage as ComputedLengthPercentage}; +use crate::values::computed::{Percentage as ComputedPercentage, ToComputedValue}; +use crate::values::generics::transform as generic; +use crate::values::generics::transform::{Matrix, Matrix3D}; +use crate::values::specified::position::{ + HorizontalPositionKeyword, Side, VerticalPositionKeyword, +}; +use crate::values::specified::{ + self, Angle, Integer, Length, LengthPercentage, Number, NumberOrPercentage, +}; +use crate::Zero; +use cssparser::Parser; +use style_traits::{ParseError, StyleParseErrorKind}; + +pub use crate::values::generics::transform::TransformStyle; + +/// A single operation in a specified CSS `transform` +pub type TransformOperation = + generic::TransformOperation<Angle, Number, Length, Integer, LengthPercentage>; + +/// A specified CSS `transform` +pub type Transform = generic::Transform<TransformOperation>; + +/// The specified value of a CSS `<transform-origin>` +pub type TransformOrigin = generic::TransformOrigin< + OriginComponent<HorizontalPositionKeyword>, + OriginComponent<VerticalPositionKeyword>, + Length, +>; + +#[cfg(feature = "gecko")] +fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool { + static_prefs::pref!("layout.css.transform-box-content-stroke.enabled") +} + +#[cfg(feature = "servo")] +fn all_transform_boxes_are_enabled(_context: &ParserContext) -> bool { + false +} + +/// The specified value of `transform-box`. +/// https://drafts.csswg.org/css-transforms-1/#transform-box +// Note: Once we ship everything, we can drop this and just use single_keyword for tranform-box. +#[allow(missing_docs)] +#[derive( + Animate, + Clone, + ComputeSquaredDistance, + Copy, + Debug, + Deserialize, + MallocSizeOf, + Parse, + PartialEq, + Serialize, + SpecifiedValueInfo, + ToAnimatedValue, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum TransformBox { + #[parse(condition = "all_transform_boxes_are_enabled")] + ContentBox, + BorderBox, + FillBox, + #[parse(condition = "all_transform_boxes_are_enabled")] + StrokeBox, + ViewBox, +} + +impl TransformOrigin { + /// Returns the initial specified value for `transform-origin`. + #[inline] + pub fn initial_value() -> Self { + Self::new( + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))), + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage(0.5))), + Length::zero(), + ) + } + + /// Returns the `0 0` value. + pub fn zero_zero() -> Self { + Self::new( + OriginComponent::Length(LengthPercentage::zero()), + OriginComponent::Length(LengthPercentage::zero()), + Length::zero(), + ) + } +} + +impl Transform { + /// Internal parse function for deciding if we wish to accept prefixed values or not + /// + /// `transform` allows unitless zero angles as an exception, see: + /// https://github.com/w3c/csswg-drafts/issues/1162 + fn parse_internal<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use style_traits::{Separator, Space}; + + if input + .try_parse(|input| input.expect_ident_matching("none")) + .is_ok() + { + return Ok(generic::Transform::none()); + } + + Ok(generic::Transform( + Space::parse(input, |input| { + let function = input.expect_function()?.clone(); + input.parse_nested_block(|input| { + let location = input.current_source_location(); + let result = match_ignore_ascii_case! { &function, + "matrix" => { + let a = Number::parse(context, input)?; + input.expect_comma()?; + let b = Number::parse(context, input)?; + input.expect_comma()?; + let c = Number::parse(context, input)?; + input.expect_comma()?; + let d = Number::parse(context, input)?; + input.expect_comma()?; + // Standard matrix parsing. + let e = Number::parse(context, input)?; + input.expect_comma()?; + let f = Number::parse(context, input)?; + Ok(generic::TransformOperation::Matrix(Matrix { a, b, c, d, e, f })) + }, + "matrix3d" => { + let m11 = Number::parse(context, input)?; + input.expect_comma()?; + let m12 = Number::parse(context, input)?; + input.expect_comma()?; + let m13 = Number::parse(context, input)?; + input.expect_comma()?; + let m14 = Number::parse(context, input)?; + input.expect_comma()?; + let m21 = Number::parse(context, input)?; + input.expect_comma()?; + let m22 = Number::parse(context, input)?; + input.expect_comma()?; + let m23 = Number::parse(context, input)?; + input.expect_comma()?; + let m24 = Number::parse(context, input)?; + input.expect_comma()?; + let m31 = Number::parse(context, input)?; + input.expect_comma()?; + let m32 = Number::parse(context, input)?; + input.expect_comma()?; + let m33 = Number::parse(context, input)?; + input.expect_comma()?; + let m34 = Number::parse(context, input)?; + input.expect_comma()?; + // Standard matrix3d parsing. + let m41 = Number::parse(context, input)?; + input.expect_comma()?; + let m42 = Number::parse(context, input)?; + input.expect_comma()?; + let m43 = Number::parse(context, input)?; + input.expect_comma()?; + let m44 = Number::parse(context, input)?; + Ok(generic::TransformOperation::Matrix3D(Matrix3D { + m11, m12, m13, m14, + m21, m22, m23, m24, + m31, m32, m33, m34, + m41, m42, m43, m44, + })) + }, + "translate" => { + let sx = specified::LengthPercentage::parse(context, input)?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + let sy = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::Translate(sx, sy)) + } else { + Ok(generic::TransformOperation::Translate(sx, Zero::zero())) + } + }, + "translatex" => { + let tx = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::TranslateX(tx)) + }, + "translatey" => { + let ty = specified::LengthPercentage::parse(context, input)?; + Ok(generic::TransformOperation::TranslateY(ty)) + }, + "translatez" => { + let tz = specified::Length::parse(context, input)?; + Ok(generic::TransformOperation::TranslateZ(tz)) + }, + "translate3d" => { + let tx = specified::LengthPercentage::parse(context, input)?; + input.expect_comma()?; + let ty = specified::LengthPercentage::parse(context, input)?; + input.expect_comma()?; + let tz = specified::Length::parse(context, input)?; + Ok(generic::TransformOperation::Translate3D(tx, ty, tz)) + }, + "scale" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + if input.try_parse(|input| input.expect_comma()).is_ok() { + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::Scale(sx, sy)) + } else { + Ok(generic::TransformOperation::Scale(sx, sx)) + } + }, + "scalex" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleX(sx)) + }, + "scaley" => { + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleY(sy)) + }, + "scalez" => { + let sz = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::ScaleZ(sz)) + }, + "scale3d" => { + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + input.expect_comma()?; + let sy = NumberOrPercentage::parse(context, input)?.to_number(); + input.expect_comma()?; + let sz = NumberOrPercentage::parse(context, input)?.to_number(); + Ok(generic::TransformOperation::Scale3D(sx, sy, sz)) + }, + "rotate" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::Rotate(theta)) + }, + "rotatex" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateX(theta)) + }, + "rotatey" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateY(theta)) + }, + "rotatez" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::RotateZ(theta)) + }, + "rotate3d" => { + let ax = Number::parse(context, input)?; + input.expect_comma()?; + let ay = Number::parse(context, input)?; + input.expect_comma()?; + let az = Number::parse(context, input)?; + input.expect_comma()?; + let theta = specified::Angle::parse_with_unitless(context, input)?; + // TODO(gw): Check that the axis can be normalized. + Ok(generic::TransformOperation::Rotate3D(ax, ay, az, theta)) + }, + "skew" => { + let ax = specified::Angle::parse_with_unitless(context, input)?; + if input.try_parse(|input| input.expect_comma()).is_ok() { + let ay = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::Skew(ax, ay)) + } else { + Ok(generic::TransformOperation::Skew(ax, Zero::zero())) + } + }, + "skewx" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::SkewX(theta)) + }, + "skewy" => { + let theta = specified::Angle::parse_with_unitless(context, input)?; + Ok(generic::TransformOperation::SkewY(theta)) + }, + "perspective" => { + let p = match input.try_parse(|input| specified::Length::parse_non_negative(context, input)) { + Ok(p) => generic::PerspectiveFunction::Length(p), + Err(..) => { + input.expect_ident_matching("none")?; + generic::PerspectiveFunction::None + } + }; + Ok(generic::TransformOperation::Perspective(p)) + }, + _ => Err(()), + }; + result.map_err(|()| { + location.new_custom_error(StyleParseErrorKind::UnexpectedFunction( + function.clone(), + )) + }) + }) + })? + .into(), + )) + } +} + +impl Parse for Transform { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + Transform::parse_internal(context, input) + } +} + +/// The specified value of a component of a CSS `<transform-origin>`. +#[derive(Clone, Debug, MallocSizeOf, Parse, PartialEq, SpecifiedValueInfo, ToCss, ToShmem)] +pub enum OriginComponent<S> { + /// `center` + Center, + /// `<length-percentage>` + Length(LengthPercentage), + /// `<side>` + Side(S), +} + +impl Parse for TransformOrigin { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let parse_depth = |input: &mut Parser| { + input + .try_parse(|i| Length::parse(context, i)) + .unwrap_or(Length::zero()) + }; + match input.try_parse(|i| OriginComponent::parse(context, i)) { + Ok(x_origin @ OriginComponent::Center) => { + if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) { + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let y_origin = OriginComponent::Center; + if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) { + let x_origin = OriginComponent::Side(x_keyword); + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let depth = Length::from_px(0.); + return Ok(Self::new(x_origin, y_origin, depth)); + }, + Ok(x_origin) => { + if let Ok(y_origin) = input.try_parse(|i| OriginComponent::parse(context, i)) { + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let y_origin = OriginComponent::Center; + let depth = Length::from_px(0.); + return Ok(Self::new(x_origin, y_origin, depth)); + }, + Err(_) => {}, + } + let y_keyword = VerticalPositionKeyword::parse(input)?; + let y_origin = OriginComponent::Side(y_keyword); + if let Ok(x_keyword) = input.try_parse(HorizontalPositionKeyword::parse) { + let x_origin = OriginComponent::Side(x_keyword); + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + if input + .try_parse(|i| i.expect_ident_matching("center")) + .is_ok() + { + let x_origin = OriginComponent::Center; + let depth = parse_depth(input); + return Ok(Self::new(x_origin, y_origin, depth)); + } + let x_origin = OriginComponent::Center; + let depth = Length::from_px(0.); + Ok(Self::new(x_origin, y_origin, depth)) + } +} + +impl<S> ToComputedValue for OriginComponent<S> +where + S: Side, +{ + type ComputedValue = ComputedLengthPercentage; + + fn to_computed_value(&self, context: &Context) -> Self::ComputedValue { + match *self { + OriginComponent::Center => { + ComputedLengthPercentage::new_percent(ComputedPercentage(0.5)) + }, + OriginComponent::Length(ref length) => length.to_computed_value(context), + OriginComponent::Side(ref keyword) => { + let p = ComputedPercentage(if keyword.is_start() { 0. } else { 1. }); + ComputedLengthPercentage::new_percent(p) + }, + } + } + + fn from_computed_value(computed: &Self::ComputedValue) -> Self { + OriginComponent::Length(ToComputedValue::from_computed_value(computed)) + } +} + +impl<S> OriginComponent<S> { + /// `0%` + pub fn zero() -> Self { + OriginComponent::Length(LengthPercentage::Percentage(ComputedPercentage::zero())) + } +} + +/// A specified CSS `rotate` +pub type Rotate = generic::Rotate<Number, Angle>; + +impl Parse for Rotate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Rotate::None); + } + + // Parse <angle> or [ x | y | z | <number>{3} ] && <angle>. + // + // The rotate axis and angle could be in any order, so we parse angle twice to cover + // two cases. i.e. `<number>{3} <angle>` or `<angle> <number>{3}` + let angle = input + .try_parse(|i| specified::Angle::parse(context, i)) + .ok(); + let axis = input + .try_parse(|i| { + Ok(try_match_ident_ignore_ascii_case! { i, + "x" => (Number::new(1.), Number::new(0.), Number::new(0.)), + "y" => (Number::new(0.), Number::new(1.), Number::new(0.)), + "z" => (Number::new(0.), Number::new(0.), Number::new(1.)), + }) + }) + .or_else(|_: ParseError| -> Result<_, ParseError> { + input.try_parse(|i| { + Ok(( + Number::parse(context, i)?, + Number::parse(context, i)?, + Number::parse(context, i)?, + )) + }) + }) + .ok(); + let angle = match angle { + Some(a) => a, + None => specified::Angle::parse(context, input)?, + }; + + Ok(match axis { + Some((x, y, z)) => generic::Rotate::Rotate3D(x, y, z, angle), + None => generic::Rotate::Rotate(angle), + }) + } +} + +/// A specified CSS `translate` +pub type Translate = generic::Translate<LengthPercentage, Length>; + +impl Parse for Translate { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Translate::None); + } + + let tx = specified::LengthPercentage::parse(context, input)?; + if let Ok(ty) = input.try_parse(|i| specified::LengthPercentage::parse(context, i)) { + if let Ok(tz) = input.try_parse(|i| specified::Length::parse(context, i)) { + // 'translate: <length-percentage> <length-percentage> <length>' + return Ok(generic::Translate::Translate(tx, ty, tz)); + } + + // translate: <length-percentage> <length-percentage>' + return Ok(generic::Translate::Translate( + tx, + ty, + specified::Length::zero(), + )); + } + + // 'translate: <length-percentage> ' + Ok(generic::Translate::Translate( + tx, + specified::LengthPercentage::zero(), + specified::Length::zero(), + )) + } +} + +/// A specified CSS `scale` +pub type Scale = generic::Scale<Number>; + +impl Parse for Scale { + /// Scale accepts <number> | <percentage>, so we parse it as NumberOrPercentage, + /// and then convert into an Number if it's a Percentage. + /// https://github.com/w3c/csswg-drafts/pull/4396 + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(generic::Scale::None); + } + + let sx = NumberOrPercentage::parse(context, input)?.to_number(); + if let Ok(sy) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) { + let sy = sy.to_number(); + if let Ok(sz) = input.try_parse(|i| NumberOrPercentage::parse(context, i)) { + // 'scale: <number> <number> <number>' + return Ok(generic::Scale::Scale(sx, sy, sz.to_number())); + } + + // 'scale: <number> <number>' + return Ok(generic::Scale::Scale(sx, sy, Number::new(1.0))); + } + + // 'scale: <number>' + Ok(generic::Scale::Scale(sx, sx, Number::new(1.0))) + } +} diff --git a/servo/components/style/values/specified/ui.rs b/servo/components/style/values/specified/ui.rs new file mode 100644 index 0000000000..2237335ec4 --- /dev/null +++ b/servo/components/style/values/specified/ui.rs @@ -0,0 +1,257 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Specified types for UI properties. + +use crate::parser::{Parse, ParserContext}; +use crate::values::generics::ui as generics; +use crate::values::specified::color::Color; +use crate::values::specified::image::Image; +use crate::values::specified::Number; +use cssparser::Parser; +use std::fmt::{self, Write}; +use style_traits::{ + CssWriter, KeywordsCollectFn, ParseError, SpecifiedValueInfo, StyleParseErrorKind, ToCss, +}; + +/// A specified value for the `cursor` property. +pub type Cursor = generics::GenericCursor<CursorImage>; + +/// A specified value for item of `image cursors`. +pub type CursorImage = generics::GenericCursorImage<Image, Number>; + +impl Parse for Cursor { + /// cursor: [<url> [<number> <number>]?]# [auto | default | ...] + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + let mut images = vec![]; + loop { + match input.try_parse(|input| CursorImage::parse(context, input)) { + Ok(image) => images.push(image), + Err(_) => break, + } + input.expect_comma()?; + } + Ok(Self { + images: images.into(), + keyword: CursorKind::parse(input)?, + }) + } +} + +impl Parse for CursorImage { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + use crate::Zero; + + let image = Image::parse_only_url(context, input)?; + let mut has_hotspot = false; + let mut hotspot_x = Number::zero(); + let mut hotspot_y = Number::zero(); + + if let Ok(x) = input.try_parse(|input| Number::parse(context, input)) { + has_hotspot = true; + hotspot_x = x; + hotspot_y = Number::parse(context, input)?; + } + + Ok(Self { + image, + has_hotspot, + hotspot_x, + hotspot_y, + }) + } +} + +// This trait is manually implemented because we don't support the whole <image> +// syntax for cursors +impl SpecifiedValueInfo for CursorImage { + fn collect_completion_keywords(f: KeywordsCollectFn) { + f(&["url", "image-set"]); + } +} +/// Specified value of `-moz-force-broken-image-icon` +#[derive( + Clone, + Copy, + Debug, + MallocSizeOf, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToResolvedValue, + ToShmem, +)] +#[repr(transparent)] +pub struct BoolInteger(pub bool); + +impl BoolInteger { + /// Returns 0 + #[inline] + pub fn zero() -> Self { + Self(false) + } +} + +impl Parse for BoolInteger { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + // We intentionally don't support calc values here. + match input.expect_integer()? { + 0 => Ok(Self(false)), + 1 => Ok(Self(true)), + _ => Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + } + } +} + +impl ToCss for BoolInteger { + fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result + where + W: Write, + { + dest.write_str(if self.0 { "1" } else { "0" }) + } +} + +/// A specified value for `scrollbar-color` property +pub type ScrollbarColor = generics::ScrollbarColor<Color>; + +impl Parse for ScrollbarColor { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result<Self, ParseError<'i>> { + if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { + return Ok(generics::ScrollbarColor::Auto); + } + Ok(generics::ScrollbarColor::Colors { + thumb: Color::parse(context, input)?, + track: Color::parse(context, input)?, + }) + } +} + +/// The specified value for the `user-select` property. +/// +/// https://drafts.csswg.org/css-ui-4/#propdef-user-select +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum UserSelect { + Auto, + Text, + #[parse(aliases = "-moz-none")] + None, + /// Force selection of all children. + All, +} + +/// The keywords allowed in the Cursor property. +/// +/// https://drafts.csswg.org/css-ui-4/#propdef-cursor +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum CursorKind { + None, + Default, + Pointer, + ContextMenu, + Help, + Progress, + Wait, + Cell, + Crosshair, + Text, + VerticalText, + Alias, + Copy, + Move, + NoDrop, + NotAllowed, + #[parse(aliases = "-moz-grab")] + Grab, + #[parse(aliases = "-moz-grabbing")] + Grabbing, + EResize, + NResize, + NeResize, + NwResize, + SResize, + SeResize, + SwResize, + WResize, + EwResize, + NsResize, + NeswResize, + NwseResize, + ColResize, + RowResize, + AllScroll, + #[parse(aliases = "-moz-zoom-in")] + ZoomIn, + #[parse(aliases = "-moz-zoom-out")] + ZoomOut, + Auto, +} + +/// The keywords allowed in the -moz-theme property. +#[allow(missing_docs)] +#[derive( + Clone, + Copy, + Debug, + Eq, + FromPrimitive, + MallocSizeOf, + Parse, + PartialEq, + SpecifiedValueInfo, + ToComputedValue, + ToCss, + ToResolvedValue, + ToShmem, +)] +#[repr(u8)] +pub enum MozTheme { + /// Choose the default (maybe native) rendering. + Auto, + /// Choose the non-native rendering. + NonNative, +} diff --git a/servo/components/style/values/specified/url.rs b/servo/components/style/values/specified/url.rs new file mode 100644 index 0000000000..17ecbe0d5e --- /dev/null +++ b/servo/components/style/values/specified/url.rs @@ -0,0 +1,15 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! Common handling for the specified value CSS url() values. + +use crate::values::generics::url::GenericUrlOrNone; + +#[cfg(feature = "gecko")] +pub use crate::gecko::url::{SpecifiedImageUrl, SpecifiedUrl}; +#[cfg(feature = "servo")] +pub use crate::servo::url::{SpecifiedImageUrl, SpecifiedUrl}; + +/// Specified <url> | <none> +pub type UrlOrNone = GenericUrlOrNone<SpecifiedUrl>; |