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::{
14    parser::ParserContext,
15    values::{
16        animated::ToAnimatedValue,
17        generics::calc::{CalcUnits, GenericCalcNode},
18        specified::calc::{AllowParse, Leaf},
19    },
20};
21use cssparser::{color::OPAQUE, Parser, Token};
22use style_traits::{ParseError, ToCss};
23
24/// A single color component.
25#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToShmem)]
26#[repr(u8)]
27pub enum ColorComponent<ValueType> {
28    /// The "none" keyword.
29    None,
30    /// A absolute value.
31    Value(ValueType),
32    /// A channel keyword, e.g. `r`, `l`, `alpha`, etc.
33    ChannelKeyword(ChannelKeyword),
34    /// A calc() value.
35    Calc(Box<GenericCalcNode<Leaf>>),
36    /// Used when alpha components are not specified.
37    AlphaOmitted,
38}
39
40impl<ValueType> ColorComponent<ValueType> {
41    /// Return true if the component is "none".
42    #[inline]
43    pub fn is_none(&self) -> bool {
44        matches!(self, Self::None)
45    }
46}
47
48/// An utility trait that allows the construction of [ColorComponent]
49/// `ValueType`'s after parsing a color component.
50pub trait ColorComponentType: Sized + Clone {
51    // TODO(tlouw): This function should be named according to the rules in the spec
52    //              stating that all the values coming from color components are
53    //              numbers and that each has their own rules dependeing on types.
54    /// Construct a new component from a single value.
55    fn from_value(value: f32) -> Self;
56
57    /// Return the [CalcUnits] flags that the impl can handle.
58    fn units() -> CalcUnits;
59
60    /// Try to create a new component from the given token.
61    fn try_from_token(token: &Token) -> Result<Self, ()>;
62
63    /// Try to create a new component from the given [CalcNodeLeaf] that was
64    /// resolved from a [CalcNode].
65    fn try_from_leaf(leaf: &Leaf) -> Result<Self, ()>;
66}
67
68impl<ValueType: ColorComponentType> ColorComponent<ValueType> {
69    /// Parse a single [ColorComponent].
70    pub fn parse<'i, 't>(
71        context: &ParserContext,
72        input: &mut Parser<'i, 't>,
73        allow_none: bool,
74    ) -> Result<Self, ParseError<'i>> {
75        let location = input.current_source_location();
76
77        match *input.next()? {
78            Token::Ident(ref value) if allow_none && value.eq_ignore_ascii_case("none") => {
79                Ok(ColorComponent::None)
80            },
81            ref t @ Token::Ident(ref ident) => {
82                let Ok(channel_keyword) = ChannelKeyword::from_ident(ident) else {
83                    return Err(location.new_unexpected_token_error(t.clone()));
84                };
85                Ok(ColorComponent::ChannelKeyword(channel_keyword))
86            },
87            Token::Function(ref name) => {
88                let function = GenericCalcNode::math_function(context, name, location)?;
89                let allow = AllowParse::new(if rcs_enabled() {
90                    ValueType::units() | CalcUnits::COLOR_COMPONENT
91                } else {
92                    ValueType::units()
93                });
94                let mut node = GenericCalcNode::parse(context, input, function, allow)?;
95
96                // TODO(tlouw): We only have to simplify the node when we have to store it, but we
97                //              only know if we have to store it much later when the whole color
98                //              can't be resolved to absolute at which point the calc nodes are
99                //              burried deep in a [ColorFunction] struct.
100                node.simplify_and_sort();
101
102                Ok(Self::Calc(Box::new(node)))
103            },
104            ref t => ValueType::try_from_token(t)
105                .map(Self::Value)
106                .map_err(|_| location.new_unexpected_token_error(t.clone())),
107        }
108    }
109
110    /// Resolve a [ColorComponent] into a float.  None is "none".
111    pub fn resolve(&self, origin_color: Option<&AbsoluteColor>) -> Result<Option<ValueType>, ()> {
112        Ok(match self {
113            ColorComponent::None => None,
114            ColorComponent::Value(value) => Some(value.clone()),
115            ColorComponent::ChannelKeyword(channel_keyword) => match origin_color {
116                Some(origin_color) => {
117                    let value = origin_color.get_component_by_channel_keyword(*channel_keyword)?;
118                    Some(ValueType::from_value(value.unwrap_or(0.0)))
119                },
120                None => return Err(()),
121            },
122            ColorComponent::Calc(node) => {
123                let Ok(resolved_leaf) = node.resolve_map(|leaf| {
124                    Ok(match leaf {
125                        Leaf::ColorComponent(channel_keyword) => match origin_color {
126                            Some(origin_color) => {
127                                let value = origin_color
128                                    .get_component_by_channel_keyword(*channel_keyword)?;
129                                Leaf::Number(value.unwrap_or(0.0))
130                            },
131                            None => return Err(()),
132                        },
133                        l => l.clone(),
134                    })
135                }) else {
136                    return Err(());
137                };
138
139                Some(ValueType::try_from_leaf(&resolved_leaf)?)
140            },
141            ColorComponent::AlphaOmitted => {
142                if let Some(origin_color) = origin_color {
143                    // <https://drafts.csswg.org/css-color-5/#rcs-intro>
144                    // If the alpha value of the relative color is omitted, it defaults to that of
145                    // the origin color (rather than defaulting to 100%, as it does in the absolute
146                    // syntax).
147                    origin_color.alpha().map(ValueType::from_value)
148                } else {
149                    Some(ValueType::from_value(OPAQUE))
150                }
151            },
152        })
153    }
154}
155
156impl<ValueType: ToCss> ToCss for ColorComponent<ValueType> {
157    fn to_css<W>(&self, dest: &mut style_traits::CssWriter<W>) -> std::fmt::Result
158    where
159        W: Write,
160    {
161        match self {
162            ColorComponent::None => dest.write_str("none")?,
163            ColorComponent::Value(value) => value.to_css(dest)?,
164            ColorComponent::ChannelKeyword(channel_keyword) => channel_keyword.to_css(dest)?,
165            ColorComponent::Calc(node) => {
166                // When we only have a channel keyword in a leaf node, we should serialize it with
167                // calc(..), except when one of the rgb color space functions are used, e.g.
168                // rgb(..), hsl(..) or hwb(..) for historical reasons.
169                // <https://github.com/web-platform-tests/wpt/issues/47921>
170                node.to_css(dest)?;
171            },
172            ColorComponent::AlphaOmitted => {
173                debug_assert!(false, "can't serialize an omitted alpha component");
174            },
175        }
176
177        Ok(())
178    }
179}
180
181impl<ValueType> ToAnimatedValue for ColorComponent<ValueType> {
182    type AnimatedValue = Self;
183
184    fn to_animated_value(self, _context: &crate::values::animated::Context) -> Self::AnimatedValue {
185        self
186    }
187
188    fn from_animated_value(animated: Self::AnimatedValue) -> Self {
189        animated
190    }
191}