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