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