Skip to main content

style/color/
to_css.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//! Write colors into CSS strings.
6
7use super::{
8    AbsoluteColor, ColorFlags, ColorSpace,
9    parsing::{NumberOrAngleComponent, NumberOrPercentageComponent},
10};
11use crate::values::normalize;
12use cssparser::color::{OPAQUE, clamp_unit_f32, serialize_color_alpha};
13use std::fmt::{self, Write};
14use style_traits::{CssWriter, ToCss};
15
16/// A [`ModernComponent`] can serialize to `none`, `nan`, `infinity` and
17/// floating point values.
18struct ModernComponent<'a>(&'a Option<f32>);
19
20impl<'a> ToCss for ModernComponent<'a> {
21    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
22    where
23        W: fmt::Write,
24    {
25        if let Some(value) = self.0 {
26            if value.is_finite() {
27                value.to_css(dest)
28            } else if value.is_nan() {
29                dest.write_str("calc(NaN)")
30            } else {
31                debug_assert!(value.is_infinite());
32                if value.is_sign_negative() {
33                    dest.write_str("calc(-infinity)")
34                } else {
35                    dest.write_str("calc(infinity)")
36                }
37            }
38        } else {
39            dest.write_str("none")
40        }
41    }
42}
43
44impl ToCss for NumberOrPercentageComponent {
45    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
46    where
47        W: Write,
48    {
49        use crate::values::computed::Percentage;
50
51        match self {
52            Self::Number(number) => number.to_css(dest)?,
53            Self::Percentage(percentage) => Percentage(*percentage).to_css(dest)?,
54        }
55        Ok(())
56    }
57}
58
59impl ToCss for NumberOrAngleComponent {
60    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
61    where
62        W: Write,
63    {
64        use crate::values::computed::Angle;
65
66        match self {
67            Self::Number(number) => number.to_css(dest)?,
68            Self::Angle(degrees) => Angle::from_degrees(*degrees).to_css(dest)?,
69        }
70        Ok(())
71    }
72}
73
74impl ToCss for AbsoluteColor {
75    fn to_css<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
76    where
77        W: Write,
78    {
79        match self.color_space {
80            ColorSpace::Srgb if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) => {
81                // The "none" keyword is not supported in the rgb/rgba legacy syntax.
82                let has_alpha = self.alpha != OPAQUE;
83
84                dest.write_str(if has_alpha { "rgba(" } else { "rgb(" })?;
85                clamp_unit_f32(self.components.0).to_css(dest)?;
86                dest.write_str(", ")?;
87                clamp_unit_f32(self.components.1).to_css(dest)?;
88                dest.write_str(", ")?;
89                clamp_unit_f32(self.components.2).to_css(dest)?;
90
91                // Legacy syntax does not allow none components.
92                serialize_color_alpha(dest, Some(self.alpha), true)?;
93
94                dest.write_char(')')
95            },
96            ColorSpace::Hsl | ColorSpace::Hwb => {
97                if self.flags.intersects(ColorFlags::NONE_FLAGS) {
98                    dest.write_str(if self.color_space == ColorSpace::Hsl {
99                        "hsl("
100                    } else {
101                        "hwb("
102                    })?;
103                    ModernComponent(&self.c0()).to_css(dest)?;
104                    for component in [self.c1(), self.c2()] {
105                        dest.write_char(' ')?;
106                        if let Some(value) = component {
107                            value.to_css(dest)?;
108                            dest.write_char('%')?;
109                        } else {
110                            dest.write_str("none")?;
111                        }
112                    }
113                    serialize_color_alpha(dest, self.alpha(), false)?;
114                    dest.write_char(')')
115                } else if self.flags.contains(ColorFlags::IS_LEGACY_SRGB) {
116                    self.into_srgb_legacy().to_css(dest)
117                } else {
118                    self.to_color_space(ColorSpace::Srgb).to_css(dest)
119                }
120            },
121            ColorSpace::Oklab | ColorSpace::Lab | ColorSpace::Oklch | ColorSpace::Lch => {
122                if let ColorSpace::Oklab | ColorSpace::Oklch = self.color_space {
123                    dest.write_str("ok")?;
124                }
125                if let ColorSpace::Oklab | ColorSpace::Lab = self.color_space {
126                    dest.write_str("lab(")?;
127                } else {
128                    dest.write_str("lch(")?;
129                }
130                ModernComponent(&self.c0()).to_css(dest)?;
131                dest.write_char(' ')?;
132                ModernComponent(&self.c1()).to_css(dest)?;
133                dest.write_char(' ')?;
134                ModernComponent(&self.c2()).to_css(dest)?;
135                serialize_color_alpha(dest, self.alpha(), false)?;
136                dest.write_char(')')
137            },
138            _ => {
139                #[cfg(debug_assertions)]
140                match self.color_space {
141                    ColorSpace::Srgb => {
142                        debug_assert!(
143                            !self.flags.contains(ColorFlags::IS_LEGACY_SRGB),
144                            "legacy srgb is not a color function"
145                        );
146                    },
147                    ColorSpace::SrgbLinear
148                    | ColorSpace::DisplayP3
149                    | ColorSpace::DisplayP3Linear
150                    | ColorSpace::A98Rgb
151                    | ColorSpace::ProphotoRgb
152                    | ColorSpace::Rec2020
153                    | ColorSpace::XyzD50
154                    | ColorSpace::XyzD65 => {
155                        // These color spaces are allowed.
156                    },
157                    ColorSpace::Hsl
158                    | ColorSpace::Hwb
159                    | ColorSpace::Lab
160                    | ColorSpace::Oklab
161                    | ColorSpace::Lch
162                    | ColorSpace::Oklch => {
163                        unreachable!("other color spaces do not support color() syntax")
164                    },
165                };
166
167                dest.write_str("color(")?;
168                self.color_space.to_css(dest)?;
169                dest.write_char(' ')?;
170                ModernComponent(&self.c0()).to_css(dest)?;
171                dest.write_char(' ')?;
172                ModernComponent(&self.c1()).to_css(dest)?;
173                dest.write_char(' ')?;
174                ModernComponent(&self.c2()).to_css(dest)?;
175
176                serialize_color_alpha(dest, self.alpha(), false)?;
177
178                dest.write_char(')')
179            },
180        }
181    }
182}
183
184impl AbsoluteColor {
185    /// Write a string to `dest` that represents a color as an author would
186    /// enter it.
187    /// NOTE: The format of the output is NOT according to any specification,
188    /// but makes assumptions about the best ways that authors would want to
189    /// enter color values in style sheets, devtools, etc.
190    pub fn write_author_preferred_value<W>(&self, dest: &mut CssWriter<W>) -> fmt::Result
191    where
192        W: Write,
193    {
194        macro_rules! precision {
195            ($v:expr) => {{ ($v * 100.0).round() / 100.0 }};
196        }
197        macro_rules! number {
198            ($c:expr) => {{
199                if let Some(v) = $c.map(normalize) {
200                    precision!(v).to_css(dest)?;
201                } else {
202                    write!(dest, "none")?;
203                }
204            }};
205        }
206        macro_rules! percentage {
207            ($c:expr) => {{
208                if let Some(v) = $c.map(normalize) {
209                    precision!(v).to_css(dest)?;
210                    dest.write_char('%')?;
211                } else {
212                    write!(dest, "none")?;
213                }
214            }};
215        }
216        macro_rules! unit_percentage {
217            ($c:expr) => {{
218                if let Some(v) = $c.map(normalize) {
219                    precision!(v * 100.0).to_css(dest)?;
220                    dest.write_char('%')?;
221                } else {
222                    write!(dest, "none")?;
223                }
224            }};
225        }
226        macro_rules! angle {
227            ($c:expr) => {{
228                if let Some(v) = $c.map(normalize) {
229                    precision!(v).to_css(dest)?;
230                    dest.write_str("deg")?;
231                } else {
232                    write!(dest, "none")?;
233                }
234            }};
235        }
236
237        match self.color_space {
238            ColorSpace::Srgb => {
239                write!(dest, "rgb(")?;
240                unit_percentage!(self.c0());
241                dest.write_char(' ')?;
242                unit_percentage!(self.c1());
243                dest.write_char(' ')?;
244                unit_percentage!(self.c2());
245                serialize_color_alpha(dest, self.alpha(), false)?;
246                dest.write_char(')')
247            },
248            ColorSpace::Hsl | ColorSpace::Hwb => {
249                dest.write_str(if self.color_space == ColorSpace::Hsl {
250                    "hsl("
251                } else {
252                    "hwb("
253                })?;
254                angle!(self.c0());
255                dest.write_char(' ')?;
256                percentage!(self.c1());
257                dest.write_char(' ')?;
258                percentage!(self.c2());
259                serialize_color_alpha(dest, self.alpha(), false)?;
260                dest.write_char(')')
261            },
262            ColorSpace::Lab | ColorSpace::Oklab => {
263                if self.color_space == ColorSpace::Oklab {
264                    dest.write_str("ok")?;
265                }
266                dest.write_str("lab(")?;
267                if self.color_space == ColorSpace::Lab {
268                    percentage!(self.c0())
269                } else {
270                    unit_percentage!(self.c0())
271                }
272                dest.write_char(' ')?;
273                number!(self.c1());
274                dest.write_char(' ')?;
275                number!(self.c2());
276                serialize_color_alpha(dest, self.alpha(), false)?;
277                dest.write_char(')')
278            },
279            ColorSpace::Lch | ColorSpace::Oklch => {
280                if self.color_space == ColorSpace::Oklch {
281                    dest.write_str("ok")?;
282                }
283                dest.write_str("lch(")?;
284                number!(self.c0());
285                dest.write_char(' ')?;
286                number!(self.c1());
287                dest.write_char(' ')?;
288                angle!(self.c2());
289                serialize_color_alpha(dest, self.alpha(), false)?;
290                dest.write_char(')')
291            },
292            ColorSpace::SrgbLinear
293            | ColorSpace::DisplayP3
294            | ColorSpace::DisplayP3Linear
295            | ColorSpace::A98Rgb
296            | ColorSpace::ProphotoRgb
297            | ColorSpace::Rec2020
298            | ColorSpace::XyzD50
299            | ColorSpace::XyzD65 => {
300                dest.write_str("color(")?;
301                self.color_space.to_css(dest)?;
302                dest.write_char(' ')?;
303                number!(self.c0());
304                dest.write_char(' ')?;
305                number!(self.c1());
306                dest.write_char(' ')?;
307                number!(self.c2());
308                serialize_color_alpha(dest, self.alpha(), false)?;
309                dest.write_char(')')
310            },
311        }
312    }
313}