diff --git a/components/style/stylesheets/mod.rs b/components/style/stylesheets/mod.rs index a2f4a705b11..d17bc3ffd05 100644 --- a/components/style/stylesheets/mod.rs +++ b/components/style/stylesheets/mod.rs @@ -20,6 +20,7 @@ mod page_rule; mod rule_list; mod rule_parser; mod rules_iterator; +pub mod scroll_timeline_rule; mod style_rule; mod stylesheet; pub mod supports_rule; diff --git a/components/style/stylesheets/scroll_timeline_rule.rs b/components/style/stylesheets/scroll_timeline_rule.rs new file mode 100644 index 00000000000..f11e469f958 --- /dev/null +++ b/components/style/stylesheets/scroll_timeline_rule.rs @@ -0,0 +1,315 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +//! scroll-timeline-at-rule: https://drafts.csswg.org/scroll-animations/#scroll-timeline-at-rule + +use crate::parser::{Parse, ParserContext}; +use crate::shared_lock::{SharedRwLockReadGuard, ToCssWithGuard}; +use crate::str::CssStringWriter; +use crate::values::specified::{LengthPercentage, Number}; +use crate::values::{AtomIdent, TimelineName}; +use cssparser::{AtRuleParser, CowRcStr, DeclarationParser, Parser, SourceLocation, Token}; +use selectors::parser::SelectorParseErrorKind; +use std::fmt::{self, Debug, Write}; +use style_traits::{CssWriter, ParseError, StyleParseErrorKind, ToCss}; + +/// A [`@scroll-timeline`][descriptors] rule. +/// +/// [descriptors] https://drafts.csswg.org/scroll-animations/#scroll-timeline-descriptors +#[derive(Clone, Debug, ToShmem)] +pub struct ScrollTimelineRule { + /// The name of the current scroll timeline. + pub name: TimelineName, + /// The descriptors. + pub descriptors: ScrollTimelineDescriptors, + /// The line and column of the rule's source code. + pub source_location: SourceLocation, +} + +impl ToCssWithGuard for ScrollTimelineRule { + fn to_css(&self, _guard: &SharedRwLockReadGuard, dest: &mut CssStringWriter) -> fmt::Result { + let mut dest = CssWriter::new(dest); + dest.write_str("@scroll-timeline ")?; + self.name.to_css(&mut dest)?; + dest.write_str(" { ")?; + self.descriptors.to_css(&mut dest)?; + dest.write_str("}") + } +} + +/// The descriptors of @scroll-timeline. +/// +/// https://drafts.csswg.org/scroll-animations/#scroll-timeline-descriptors +#[derive(Clone, Debug, Default, ToShmem)] +pub struct ScrollTimelineDescriptors { + /// The source of the current scroll timeline. + pub source: Option, + /// The orientation of the current scroll timeline. + pub orientation: Option, + /// The scroll timeline's scrollOffsets. + pub offsets: Option, +} + +impl Parse for ScrollTimelineDescriptors { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + use crate::cssparser::DeclarationListParser; + use crate::error_reporting::ContextualParseError; + + let mut descriptors = ScrollTimelineDescriptors::default(); + let parser = ScrollTimelineDescriptorsParser { + context, + descriptors: &mut descriptors, + }; + let mut iter = DeclarationListParser::new(input, parser); + while let Some(declaration) = iter.next() { + if let Err((error, slice)) = declaration { + let location = error.location; + let error = ContextualParseError::UnsupportedRule(slice, error); + context.log_css_error(location, error) + } + } + + Ok(descriptors) + } +} + +// Basically, this is used for the serialization of CSSScrollTimelineRule, so we follow the +// instructions in https://drafts.csswg.org/scroll-animations-1/#serialize-a-cssscrolltimelinerule. +impl ToCss for ScrollTimelineDescriptors { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + if let Some(ref value) = self.source { + dest.write_str("source: ")?; + value.to_css(dest)?; + dest.write_str("; ")?; + } + + if let Some(ref value) = self.orientation { + dest.write_str("orientation: ")?; + value.to_css(dest)?; + dest.write_str("; ")?; + } + + // https://github.com/w3c/csswg-drafts/issues/6617 + if let Some(ref value) = self.offsets { + dest.write_str("scroll-offsets: ")?; + value.to_css(dest)?; + dest.write_str("; ")?; + } + Ok(()) + } +} + +struct ScrollTimelineDescriptorsParser<'a, 'b: 'a> { + context: &'a ParserContext<'b>, + descriptors: &'a mut ScrollTimelineDescriptors, +} + +impl<'a, 'b, 'i> AtRuleParser<'i> for ScrollTimelineDescriptorsParser<'a, 'b> { + type Prelude = (); + type AtRule = (); + type Error = StyleParseErrorKind<'i>; +} + +impl<'a, 'b, 'i> DeclarationParser<'i> for ScrollTimelineDescriptorsParser<'a, 'b> { + type Declaration = (); + type Error = StyleParseErrorKind<'i>; + + fn parse_value<'t>( + &mut self, + name: CowRcStr<'i>, + input: &mut Parser<'i, 't>, + ) -> Result<(), ParseError<'i>> { + macro_rules! parse_descriptor { + ( + $( $name: tt / $ident: ident, )* + ) => { + match_ignore_ascii_case! { &*name, + $( + $name => { + let value = input.parse_entirely(|i| Parse::parse(self.context, i))?; + self.descriptors.$ident = Some(value) + }, + )* + _ => { + return Err(input.new_custom_error( + SelectorParseErrorKind::UnexpectedIdent(name.clone()), + )) + } + } + } + } + parse_descriptor! { + "source" / source, + "orientation" / orientation, + "scroll-offsets" / offsets, + }; + Ok(()) + } +} + +/// The scroll-timeline source. +/// +/// https://drafts.csswg.org/scroll-animations/#descdef-scroll-timeline-source +#[derive(Clone, Debug, Parse, PartialEq, ToCss, ToShmem)] +pub enum Source { + /// The scroll container. + Selector(ScrollTimelineSelector), + /// The initial value. The scrollingElement of the Document associated with the Window that is + /// the current global object. + Auto, + /// Null. However, it's not clear what is the expected behavior of this. See the spec issue: + /// https://drafts.csswg.org/scroll-animations/#issue-0d1e73bd + None, +} + +/// The scroll-timeline orientation. +/// https://drafts.csswg.org/scroll-animations/#descdef-scroll-timeline-orientation +/// +/// Note: the initial orientation is auto, and we will treat it as block, the same as the +/// definition of ScrollTimelineOptions (WebIDL API). +/// https://drafts.csswg.org/scroll-animations/#dom-scrolltimelineoptions-orientation +#[derive(Clone, Copy, Debug, MallocSizeOf, Eq, Parse, PartialEq, PartialOrd, ToCss, ToShmem)] +pub enum Orientation { + /// The initial value. + Auto, + /// The direction along the block axis. This is the default value. + Block, + /// The direction along the inline axis + Inline, + /// The physical horizontal direction. + Horizontal, + /// The physical vertical direction. + Vertical, +} + +/// Scroll-timeline offsets. We treat None as an empty vector. +/// value: none | # +/// +/// https://drafts.csswg.org/scroll-animations/#descdef-scroll-timeline-scroll-offsets +#[derive(Clone, Debug, ToCss, ToShmem)] +#[css(comma)] +pub struct ScrollOffsets(#[css(if_empty = "none", iterable)] Box<[ScrollTimelineOffset]>); + +impl Parse for ScrollOffsets { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() { + return Ok(ScrollOffsets(Box::new([]))); + } + + Ok(ScrollOffsets( + input + .parse_comma_separated(|i| ScrollTimelineOffset::parse(context, i))? + .into_boxed_slice(), + )) + } +} + +/// A . +/// value: auto | | +/// +/// https://drafts.csswg.org/scroll-animations/#typedef-scroll-timeline-offset +#[derive(Clone, Debug, Parse, PartialEq, ToCss, ToShmem)] +pub enum ScrollTimelineOffset { + /// The initial value. A container-based offset. + Auto, + /// A container-based offset with the distance indicated by the value along source's scroll + /// range in orientation. + LengthPercentage(LengthPercentage), + /// An element-based offset. + ElementOffset(ElementOffset), +} + +/// An . +/// +/// https://drafts.csswg.org/scroll-animations-1/#typedef-element-offset-edge +#[derive(Clone, Copy, Debug, MallocSizeOf, Eq, Parse, PartialEq, PartialOrd, ToCss, ToShmem)] +pub enum ElementOffsetEdge { + /// Start edge + Start, + /// End edge. + End, +} + +/// An . +/// value: selector( ) [ || ]? +/// +/// https://drafts.csswg.org/scroll-animations-1/#typedef-element-offset +#[derive(Clone, Debug, PartialEq, ToCss, ToShmem)] +pub struct ElementOffset { + /// The target whose intersection with source's scrolling box determines the concrete scroll + /// offset. + target: ScrollTimelineSelector, + /// An optional value of . If not provided, the default value is start. + edge: Option, + /// An optional value of threshold. If not provided, the default value is 0. + threshold: Option, +} + +impl Parse for ElementOffset { + fn parse<'i, 't>( + context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + let target = ScrollTimelineSelector::parse(context, input)?; + + // Parse `[ || ]?` + let mut edge = input.try_parse(ElementOffsetEdge::parse).ok(); + let threshold = input.try_parse(|i| Number::parse(context, i)).ok(); + if edge.is_none() { + edge = input.try_parse(ElementOffsetEdge::parse).ok(); + } + + Ok(ElementOffset { + target, + edge, + threshold, + }) + } +} + +/// The type of the selector ID. +#[derive(Clone, Eq, PartialEq, ToShmem)] +pub struct ScrollTimelineSelector(AtomIdent); + +impl Parse for ScrollTimelineSelector { + fn parse<'i, 't>( + _context: &ParserContext, + input: &mut Parser<'i, 't>, + ) -> Result> { + // Parse `selector()`. + input.expect_function_matching("selector")?; + input.parse_nested_block(|i| match i.next()? { + Token::IDHash(id) => Ok(ScrollTimelineSelector(id.as_ref().into())), + _ => Err(i.new_custom_error(StyleParseErrorKind::UnspecifiedError)), + }) + } +} + +impl ToCss for ScrollTimelineSelector { + fn to_css(&self, dest: &mut CssWriter) -> fmt::Result + where + W: Write, + { + use crate::cssparser::ToCss as CssparserToCss; + dest.write_str("selector(")?; + dest.write_char('#')?; + self.0.to_css(dest)?; + dest.write_char(')') + } +} + +impl Debug for ScrollTimelineSelector { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.to_css(&mut CssWriter::new(f)) + } +} diff --git a/components/style/values/mod.rs b/components/style/values/mod.rs index 79ec424ab3a..777b81e1da8 100644 --- a/components/style/values/mod.rs +++ b/components/style/values/mod.rs @@ -464,29 +464,33 @@ impl ToCss for CustomIdent { } } +/// The or . +/// The definition of these two names are the same, so we use the same type for them. +/// +/// /// #[derive( Clone, Debug, MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem, )] -pub enum KeyframesName { +pub enum TimelineOrKeyframesName { /// Ident(CustomIdent), /// QuotedString(Atom), } -impl KeyframesName { +impl TimelineOrKeyframesName { /// pub fn from_ident(value: &str) -> Self { let location = SourceLocation { line: 0, column: 0 }; let custom_ident = CustomIdent::from_ident(location, &value.into(), &["none"]).ok(); match custom_ident { - Some(ident) => KeyframesName::Ident(ident), - None => KeyframesName::QuotedString(value.into()), + Some(ident) => Self::Ident(ident), + None => Self::QuotedString(value.into()), } } - /// Create a new KeyframesName from Atom. + /// Create a new TimelineOrKeyframesName from Atom. #[cfg(feature = "gecko")] pub fn from_atom(atom: Atom) -> Self { debug_assert_ne!(atom, atom!("")); @@ -494,19 +498,19 @@ impl KeyframesName { // FIXME: We might want to preserve , but currently Gecko // stores both of and into nsAtom, so // we can't tell it. - KeyframesName::Ident(CustomIdent(atom)) + Self::Ident(CustomIdent(atom)) } /// The name as an Atom pub fn as_atom(&self) -> &Atom { match *self { - KeyframesName::Ident(ref ident) => &ident.0, - KeyframesName::QuotedString(ref atom) => atom, + Self::Ident(ref ident) => &ident.0, + Self::QuotedString(ref atom) => atom, } } } -impl Eq for KeyframesName {} +impl Eq for TimelineOrKeyframesName {} /// A trait that returns whether a given type is the `auto` value or not. So far /// only needed for background-size serialization, which special-cases `auto`. @@ -515,13 +519,13 @@ pub trait IsAuto { fn is_auto(&self) -> bool; } -impl PartialEq for KeyframesName { +impl PartialEq for TimelineOrKeyframesName { fn eq(&self, other: &Self) -> bool { self.as_atom() == other.as_atom() } } -impl hash::Hash for KeyframesName { +impl hash::Hash for TimelineOrKeyframesName { fn hash(&self, state: &mut H) where H: hash::Hasher, @@ -530,32 +534,40 @@ impl hash::Hash for KeyframesName { } } -impl Parse for KeyframesName { +impl Parse for TimelineOrKeyframesName { fn parse<'i, 't>( _context: &ParserContext, input: &mut Parser<'i, 't>, ) -> Result> { let location = input.current_source_location(); match *input.next()? { - Token::Ident(ref s) => Ok(KeyframesName::Ident(CustomIdent::from_ident( + Token::Ident(ref s) => Ok(Self::Ident(CustomIdent::from_ident( location, s, &["none"], )?)), - Token::QuotedString(ref s) => Ok(KeyframesName::QuotedString(Atom::from(s.as_ref()))), + Token::QuotedString(ref s) => { + Ok(Self::QuotedString(Atom::from(s.as_ref()))) + }, ref t => Err(location.new_unexpected_token_error(t.clone())), } } } -impl ToCss for KeyframesName { +impl ToCss for TimelineOrKeyframesName { fn to_css(&self, dest: &mut CssWriter) -> fmt::Result where W: Write, { match *self { - KeyframesName::Ident(ref ident) => ident.to_css(dest), - KeyframesName::QuotedString(ref atom) => atom.to_string().to_css(dest), + Self::Ident(ref ident) => ident.to_css(dest), + Self::QuotedString(ref atom) => atom.to_string().to_css(dest), } } } + +/// The typedef of . +pub type TimelineName = TimelineOrKeyframesName; + +/// The typedef of . +pub type KeyframesName = TimelineOrKeyframesName;