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