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