Skip to main content

style/color/
component.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Parse/serialize and resolve a single color component.
6
7use std::fmt::Write;
8
9use super::{
10    parsing::{rcs_enabled, ChannelKeyword},
11    AbsoluteColor,
12};
13use crate::derives::*;
14use crate::{
15    parser::ParserContext,
16    values::{
17        animated::ToAnimatedValue,
18        computed,
19        generics::calc::CalcUnits,
20        specified::calc::{CalcNode, CalcParseFlags, Leaf},
21    },
22};
23use cssparser::{color::OPAQUE, Parser, Token};
24use style_traits::{ParseError, StyleParseErrorKind, ToCss};
25
26/// A single color component.
27#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
28#[repr(u8)]
29pub enum ColorComponent<ValueType> {
30    /// The "none" keyword.
31    None,
32    /// A absolute value.
33    Value(ValueType),
34    /// A channel keyword, e.g. `r`, `l`, `alpha`, etc.
35    ChannelKeyword(ChannelKeyword),
36    /// A calc() value.
37    Calc(Box<CalcNode>),
38    /// Used when alpha components are not specified.
39    AlphaOmitted,
40}
41
42impl<ValueType> ColorComponent<ValueType> {
43    /// Return true if the component is "none".
44    #[inline]
45    pub fn is_none(&self) -> bool {
46        matches!(self, Self::None)
47    }
48}
49
50/// An utility trait that allows the construction of [ColorComponent]
51/// `ValueType`'s after parsing a color component.
52pub trait ColorComponentType: Sized + Clone {
53    // TODO(tlouw): This function should be named according to the rules in the spec
54    //              stating that all the values coming from color components are
55    //              numbers and that each has their own rules dependeing on types.
56    /// Construct a new component from a single value.
57    fn from_value(value: f32) -> Self;
58
59    /// Return the [CalcUnits] flags that the impl can handle.
60    fn units() -> CalcUnits;
61
62    /// Try to create a new component from the given token.
63    fn try_from_token(token: &Token) -> Result<Self, ()>;
64
65    /// Try to create a new component from the given [CalcNodeLeaf] that was
66    /// resolved from a [CalcNode].
67    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()>;
68}
69
70impl<ValueType: ColorComponentType> ColorComponent<ValueType> {
71    /// Parse a single [ColorComponent].
72    pub fn parse<'i, 't>(
73        context: &ParserContext,
74        input: &mut Parser<'i, 't>,
75        allow_none: bool,
76        allowed_channel_keywords: ChannelKeyword,
77    ) -> Result<Self, ParseError<'i>> {
78        let location = input.current_source_location();
79
80        match *input.next()? {
81            Token::Ident(ref value) if allow_none && value.eq_ignore_ascii_case("none") => {
82                Ok(ColorComponent::None)
83            },
84            ref t @ Token::Ident(ref ident) => Ok(match ChannelKeyword::from_ident(ident) {
85                Ok(channel_keyword) if allowed_channel_keywords.contains(channel_keyword) => {
86                    ColorComponent::ChannelKeyword(channel_keyword)
87                },
88                _ => return Err(location.new_unexpected_token_error(t.clone())),
89            }),
90            Token::Function(ref name) => {
91                let function = CalcNode::math_function(context, name, location)?;
92                let mut flags = CalcParseFlags::new(ValueType::units());
93                flags.color_components = if rcs_enabled() {
94                    allowed_channel_keywords
95                } else {
96                    ChannelKeyword::empty()
97                };
98                let mut node = CalcNode::parse(context, input, function, flags)?;
99                node.simplify_and_sort();
100                if node.unit().is_err() {
101                    return Err(location.new_custom_error(StyleParseErrorKind::UnspecifiedError));
102                }
103                Ok(Self::Calc(Box::new(node)))
104            },
105            ref t => ValueType::try_from_token(t)
106                .map(Self::Value)
107                .map_err(|_| location.new_unexpected_token_error(t.clone())),
108        }
109    }
110
111    /// Compute the component's value against `context`, substituting color
112    /// channel references with the matching channel of `origin_color` when it is
113    /// provided (and already converted into this function's color space).
114    ///
115    /// If no (absolute) origin color is available, channel references are kept
116    /// intact, so the component can still be resolved later at use-value time.
117    pub fn to_computed_value(
118        &self,
119        context: Option<&computed::Context>,
120        origin_color: Option<&AbsoluteColor>,
121    ) -> Self {
122        match self {
123            Self::None => Self::None,
124            Self::Value(v) => Self::Value(v.clone()),
125            Self::ChannelKeyword(channel_keyword) => match origin_color {
126                Some(origin_color) => {
127                    match origin_color.get_component_by_channel_keyword(*channel_keyword) {
128                        Ok(value) => Self::Value(ValueType::from_value(value.unwrap_or(0.0))),
129                        Err(()) => Self::ChannelKeyword(*channel_keyword),
130                    }
131                },
132                None => Self::ChannelKeyword(*channel_keyword),
133            },
134            Self::Calc(node) => {
135                // Try to compute, substitute channels and fold the calc tree in a
136                // single pass. If it resolves to a concrete value, collapse to a
137                // value; otherwise keep the computed (still symbolic) calc tree.
138                if let Ok(value) = node
139                    .resolve_map(|leaf| Ok(leaf.to_computed_value(context, origin_color)))
140                    .and_then(|leaf| ValueType::try_from_leaf(&leaf))
141                {
142                    Self::Value(value)
143                } else {
144                    Self::Calc(Box::new(node.to_computed_value(context, origin_color)))
145                }
146            },
147            Self::AlphaOmitted => match origin_color {
148                // <https://drafts.csswg.org/css-color-5/#rcs-intro>
149                // If the alpha value of the relative color is omitted, it
150                // defaults to that of the origin color (rather than defaulting to
151                // 100%, as it does in the absolute syntax).
152                Some(origin_color) => match origin_color.alpha() {
153                    Some(alpha) => Self::Value(ValueType::from_value(alpha)),
154                    None => Self::None,
155                },
156                None => Self::AlphaOmitted,
157            },
158        }
159    }
160
161    /// Resolve an already-computed [ColorComponent] into a float. None is
162    /// "none". This assumes color channel references have already been
163    /// substituted by [`to_computed_value`], and so does not require an origin
164    /// color.
165    pub fn resolve(&self) -> Result<Option<ValueType>, ()> {
166        Ok(match self {
167            Self::None => None,
168            Self::Value(value) => Some(value.clone()),
169            // An unsubstituted channel reference can't be resolved without an
170            // origin color.
171            Self::ChannelKeyword(_) => return Err(()),
172            Self::Calc(node) => Some(ValueType::try_from_leaf(&node.resolve()?)?),
173            Self::AlphaOmitted => Some(ValueType::from_value(OPAQUE)),
174        })
175    }
176}
177
178impl<ValueType: ToCss> ToCss for ColorComponent<ValueType> {
179    fn to_css<W>(&self, dest: &mut style_traits::CssWriter<W>) -> std::fmt::Result
180    where
181        W: Write,
182    {
183        match self {
184            ColorComponent::None => dest.write_str("none")?,
185            ColorComponent::Value(value) => value.to_css(dest)?,
186            ColorComponent::ChannelKeyword(channel_keyword) => channel_keyword.to_css(dest)?,
187            ColorComponent::Calc(node) => {
188                // When we only have a channel keyword in a leaf node, we should serialize it with
189                // calc(..), except when one of the rgb color space functions are used, e.g.
190                // rgb(..), hsl(..) or hwb(..) for historical reasons.
191                // <https://github.com/web-platform-tests/wpt/issues/47921>
192                node.to_css(dest)?;
193            },
194            ColorComponent::AlphaOmitted => {
195                debug_assert!(false, "can't serialize an omitted alpha component");
196            },
197        }
198
199        Ok(())
200    }
201}
202
203impl<ValueType> ToAnimatedValue for ColorComponent<ValueType> {
204    type AnimatedValue = Self;
205
206    fn to_animated_value(self, _context: &crate::values::animated::Context) -> Self::AnimatedValue {
207        self
208    }
209
210    fn from_animated_value(animated: Self::AnimatedValue) -> Self {
211        animated
212    }
213}