Commit 9bf39f52 authored by Boris Chiou's avatar Boris Chiou
Browse files

Bug 1676782 - Part 1: Implement @scroll-timeline in style system. r=emilio

Define the data structure for @scroll-timeline rule, the parsing code,
and the serialization.

Differential Revision: https://phabricator.services.mozilla.com/D125764
parent 45237980
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -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;
+315 −0
Original line number Diff line number Diff line
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.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<Source>,
    /// The orientation of the current scroll timeline.
    pub orientation: Option<Orientation>,
    /// The scroll timeline's scrollOffsets.
    pub offsets: Option<ScrollOffsets>,
}

impl Parse for ScrollTimelineDescriptors {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        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<W>(&self, dest: &mut CssWriter<W>) -> 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 | <scroll-timeline-offset>#
///
/// 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<Self, ParseError<'i>> {
        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 <scroll-timeline-offset>.
/// value: auto | <length-percentage> | <element-offset>
///
/// 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 <element-offset-edge>.
///
/// 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 <element-offset>.
/// value: selector( <id-selector> ) [<element-offset-edge> || <number>]?
///
/// 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 <element-offset-edge>. If not provided, the default value is start.
    edge: Option<ElementOffsetEdge>,
    /// An optional value of threshold. If not provided, the default value is 0.
    threshold: Option<Number>,
}

impl Parse for ElementOffset {
    fn parse<'i, 't>(
        context: &ParserContext,
        input: &mut Parser<'i, 't>,
    ) -> Result<Self, ParseError<'i>> {
        let target = ScrollTimelineSelector::parse(context, input)?;

        // Parse `[<element-offset-edge> || <number>]?`
        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<Self, ParseError<'i>> {
        // Parse `selector(<id-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<W>(&self, dest: &mut CssWriter<W>) -> 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))
    }
}
+29 −17
Original line number Diff line number Diff line
@@ -464,29 +464,33 @@ impl ToCss for CustomIdent {
    }
}

/// The <timeline-name> or <keyframes-name>.
/// The definition of these two names are the same, so we use the same type for them.
///
/// <https://drafts.csswg.org/css-animations-2/#typedef-timeline-name>
/// <https://drafts.csswg.org/css-animations/#typedef-keyframes-name>
#[derive(
    Clone, Debug, MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem,
)]
pub enum KeyframesName {
pub enum TimelineOrKeyframesName {
    /// <custom-ident>
    Ident(CustomIdent),
    /// <string>
    QuotedString(Atom),
}

impl KeyframesName {
impl TimelineOrKeyframesName {
    /// <https://drafts.csswg.org/css-animations/#dom-csskeyframesrule-name>
    pub fn from_ident(value: &str) -> Self {
        let location = SourceLocation { line: 0, column: 0 };
        let custom_ident = CustomIdent::from_ident(location, &value.into(), &["none"]).ok();
        match custom_ident {
            Some(ident) => KeyframesName::Ident(ident),
            None => KeyframesName::QuotedString(value.into()),
            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 <string>, but currently Gecko
        // stores both of <custom-ident> and <string> 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<H>(&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<Self, ParseError<'i>> {
        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<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
    where
        W: Write,
    {
        match *self {
            KeyframesName::Ident(ref ident) => ident.to_css(dest),
            KeyframesName::QuotedString(ref atom) => atom.to_string().to_css(dest),
            Self::Ident(ref ident) => ident.to_css(dest),
            Self::QuotedString(ref atom) => atom.to_string().to_css(dest),
        }
    }
}

/// The typedef of <timeline-name>.
pub type TimelineName = TimelineOrKeyframesName;

/// The typedef of <keyframes-name>.
pub type KeyframesName = TimelineOrKeyframesName;