Skip to main content

style/values/generics/
color.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//! Generic types for color properties.
6
7use crate::color::ColorMixItemList;
8use crate::color::{mix::ColorInterpolationMethod, AbsoluteColor, ColorFunction};
9use crate::derives::*;
10use crate::values::generics::Optional;
11use crate::values::{
12    computed::ToComputedValue, specified::percentage::ToPercentage, ParseError, Parser,
13};
14use std::fmt::{self, Write};
15use style_traits::{owned_slice::OwnedSlice, CssWriter, ToCss};
16
17/// This struct represents a combined color from a numeric color and
18/// the current foreground color (currentcolor keyword).
19#[derive(Clone, Debug, MallocSizeOf, PartialEq, ToAnimatedValue, ToShmem)]
20#[repr(C)]
21pub enum GenericColor<Percentage> {
22    /// The actual numeric color.
23    Absolute(AbsoluteColor),
24    /// A unresolvable color.
25    ColorFunction(Box<ColorFunction<Self>>),
26    /// The `CurrentColor` keyword.
27    CurrentColor,
28    /// The color-mix() function.
29    ColorMix(Box<GenericColorMix<Self, Percentage>>),
30    /// The contrast-color() function.
31    ContrastColor(Box<Self>),
32}
33
34/// Flags used to modify the calculation of a color mix result.
35#[derive(Clone, Copy, Debug, Default, MallocSizeOf, PartialEq, ToShmem)]
36#[repr(C)]
37pub struct ColorMixFlags(u8);
38bitflags! {
39    impl ColorMixFlags : u8 {
40        /// Normalize the weights of the mix.
41        const NORMALIZE_WEIGHTS = 1 << 0;
42        /// The result should always be converted to the modern color syntax.
43        const RESULT_IN_MODERN_SYNTAX = 1 << 1;
44    }
45}
46
47/// One `(color, percentage)` component of a `color-mix()` expression.
48#[derive(
49    Clone,
50    Debug,
51    MallocSizeOf,
52    PartialEq,
53    ToAnimatedValue,
54    ToComputedValue,
55    ToResolvedValue,
56    ToShmem,
57)]
58#[allow(missing_docs)]
59#[repr(C)]
60pub struct GenericColorMixItem<Color, Percentage> {
61    pub color: Color,
62    /// [Optional::None] when the percentage was omitted next to a calc() sibling.
63    pub percentage: Optional<Percentage>,
64}
65
66/// A restricted version of the css `color-mix()` function, which only supports
67/// percentages.
68///
69/// https://drafts.csswg.org/css-color-5/#color-mix
70#[derive(
71    Clone,
72    Debug,
73    MallocSizeOf,
74    PartialEq,
75    ToAnimatedValue,
76    ToComputedValue,
77    ToResolvedValue,
78    ToShmem,
79)]
80#[allow(missing_docs)]
81#[repr(C)]
82pub struct GenericColorMix<Color, Percentage> {
83    pub interpolation: ColorInterpolationMethod,
84    pub items: OwnedSlice<GenericColorMixItem<Color, Percentage>>,
85    pub flags: ColorMixFlags,
86}
87
88pub use self::GenericColorMix as ColorMix;
89
90impl<Color: ToCss, Percentage: ToCss + ToPercentage> ToCss for ColorMix<Color, Percentage> {
91    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
92    where
93        W: Write,
94    {
95        dest.write_str("color-mix(")?;
96
97        // If the color interpolation method is oklab (which is now the default),
98        // it can be omitted.
99        // See: https://github.com/web-platform-tests/interop/issues/1166
100        if !self.interpolation.is_default() {
101            self.interpolation.to_css(dest)?;
102            dest.write_str(", ")?;
103        }
104
105        let uniform_value = 1.0 / self.items.len() as f32;
106
107        // Per https://drafts.csswg.org/css-color-5/#serial-color-mix: omit every
108        // percentage iff all are known (specified, non-calc) and equal to 100%/N.
109        let omit_all = self.items.iter().all(|item| {
110            item.percentage
111                .as_ref()
112                .is_some_and(|p| !p.is_calc() && p.to_percentage() == Some(uniform_value))
113        });
114
115        for (index, item) in self.items.iter().enumerate() {
116            if index != 0 {
117                dest.write_str(", ")?;
118            }
119
120            item.color.to_css(dest)?;
121
122            if omit_all {
123                continue;
124            }
125
126            // An omitted percentage next to a calc() sibling serializes to nothing.
127            if let Some(percentage) = item.percentage.as_ref() {
128                dest.write_char(' ')?;
129                percentage.to_css(dest)?;
130            }
131        }
132
133        dest.write_char(')')
134    }
135}
136
137impl<Color, Percentage> ColorMix<Color, Percentage> {
138    /// The weight used for percentages that were omitted next to a calc()
139    /// sibling: an equal share of the weight not taken by specified percentages.
140    /// Returns `None` if a specified percentage can't be resolved to a number.
141    pub(crate) fn omitted_weight(&self) -> Option<f32>
142    where
143        Percentage: ToPercentage,
144    {
145        let mut specified_sum = 0.0;
146        let mut omitted = 0;
147        for item in self.items.iter() {
148            match item.percentage.as_ref() {
149                Some(p) => specified_sum += p.to_percentage()?,
150                None => omitted += 1,
151            }
152        }
153        Some(if omitted > 0 {
154            (1.0 - specified_sum) / omitted as f32
155        } else {
156            0.0
157        })
158    }
159}
160
161impl<Percentage> ColorMix<GenericColor<Percentage>, Percentage> {
162    /// Mix the colors so that we get a single color. If any of the 2 colors are
163    /// not mixable (perhaps not absolute?), then return None.
164    pub fn mix_to_absolute(&self) -> Option<AbsoluteColor>
165    where
166        Percentage: ToPercentage,
167    {
168        use crate::color::mix;
169
170        let fill = self.omitted_weight()?;
171
172        let mut items = ColorMixItemList::with_capacity(self.items.len());
173        for item in self.items.iter() {
174            let weight = match item.percentage.as_ref() {
175                Some(percentage) => percentage.to_percentage()?,
176                None => fill,
177            };
178            items.push(mix::ColorMixItem::new(*item.color.as_absolute()?, weight))
179        }
180
181        Some(mix::mix_many(self.interpolation, items, self.flags))
182    }
183}
184
185pub use self::GenericColor as Color;
186
187impl<Percentage> Color<Percentage> {
188    /// If this color is absolute return it's value, otherwise return None.
189    pub fn as_absolute(&self) -> Option<&AbsoluteColor> {
190        match *self {
191            Self::Absolute(ref absolute) => Some(absolute),
192            _ => None,
193        }
194    }
195
196    /// Returns a color value representing currentcolor.
197    pub fn currentcolor() -> Self {
198        Self::CurrentColor
199    }
200
201    /// Whether it is a currentcolor value (no numeric color component).
202    pub fn is_currentcolor(&self) -> bool {
203        matches!(*self, Self::CurrentColor)
204    }
205
206    /// Whether this color is an absolute color.
207    pub fn is_absolute(&self) -> bool {
208        matches!(*self, Self::Absolute(..))
209    }
210}
211
212/// Either `<color>` or `auto`.
213#[derive(
214    Animate,
215    Clone,
216    ComputeSquaredDistance,
217    Copy,
218    Debug,
219    MallocSizeOf,
220    PartialEq,
221    Parse,
222    SpecifiedValueInfo,
223    ToAnimatedValue,
224    ToAnimatedZero,
225    ToComputedValue,
226    ToResolvedValue,
227    ToCss,
228    ToShmem,
229    ToTyped,
230)]
231#[repr(C, u8)]
232pub enum GenericColorOrAuto<C> {
233    /// A `<color>`.
234    Color(C),
235    /// `auto`
236    Auto,
237}
238
239pub use self::GenericColorOrAuto as ColorOrAuto;
240
241/// Caret color is effectively a ColorOrAuto, but resolves `auto` to
242/// currentColor.
243#[derive(
244    Animate,
245    Clone,
246    ComputeSquaredDistance,
247    Copy,
248    Debug,
249    MallocSizeOf,
250    PartialEq,
251    SpecifiedValueInfo,
252    ToAnimatedValue,
253    ToAnimatedZero,
254    ToComputedValue,
255    ToCss,
256    ToShmem,
257    ToTyped,
258)]
259#[repr(transparent)]
260pub struct GenericCaretColor<C>(pub GenericColorOrAuto<C>);
261
262impl<C> GenericCaretColor<C> {
263    /// Returns the `auto` value.
264    pub fn auto() -> Self {
265        GenericCaretColor(GenericColorOrAuto::Auto)
266    }
267}
268
269pub use self::GenericCaretColor as CaretColor;
270
271/// A light-dark(<light>, <dark>) function.
272#[derive(
273    Clone, Debug, MallocSizeOf, PartialEq, SpecifiedValueInfo, ToShmem, ToCss, ToResolvedValue,
274)]
275#[css(function = "light-dark", comma)]
276#[repr(C)]
277pub struct GenericLightDark<T> {
278    /// The value returned when using a light theme.
279    pub light: T,
280    /// The value returned when using a dark theme.
281    pub dark: T,
282}
283
284impl<T> GenericLightDark<T> {
285    /// Parse the arguments of the light-dark() function.
286    pub fn parse_args_with(
287        input: &mut Parser,
288        mut parse_one: impl FnMut(&mut Parser) -> Result<T, ParseError>,
289    ) -> Result<Self, ParseError> {
290        let light = parse_one(input)?;
291        input.expect_comma()?;
292        let dark = parse_one(input)?;
293        Ok(Self { light, dark })
294    }
295
296    /// Parse the light-dark() function.
297    pub fn parse_with(
298        input: &mut Parser,
299        parse_one: impl FnMut(&mut Parser) -> Result<T, ParseError>,
300    ) -> Result<Self, ParseError> {
301        input.expect_function_matching("light-dark")?;
302        input.parse_nested_block(|input| Self::parse_args_with(input, parse_one))
303    }
304}
305
306impl<T: ToComputedValue> GenericLightDark<T> {
307    /// Choose the light or dark version of this value for computation purposes, and compute it.
308    pub fn compute(&self, cx: &crate::values::computed::Context) -> T::ComputedValue {
309        let dark = cx.device().is_dark_color_scheme(cx.builder.color_scheme);
310        if cx.for_non_inherited_property {
311            cx.rule_cache_conditions
312                .borrow_mut()
313                .set_color_scheme_dependency(cx.builder.color_scheme);
314        }
315        let chosen = if dark { &self.dark } else { &self.light };
316        chosen.to_computed_value(cx)
317    }
318}