Skip to main content

azul_css/props/style/
box_shadow.rs

1//! Shared types for CSS shadow properties (used by both `box-shadow` and `text-shadow`).
2
3use alloc::string::{String, ToString};
4use core::fmt;
5use crate::corety::AzString;
6
7use crate::props::{
8    basic::{
9        color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
10        pixel::{
11            parse_pixel_value_no_percent, CssPixelValueParseError, CssPixelValueParseErrorOwned,
12            PixelValueNoPercent,
13        },
14    },
15    formatter::PrintAsCssValue,
16};
17
18/// What direction should a `box-shadow` be clipped in (inset or outset).
19#[derive(Debug, Default, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
20#[repr(C)]
21pub enum BoxShadowClipMode {
22    #[default]
23    Outset,
24    Inset,
25}
26
27impl fmt::Display for BoxShadowClipMode {
28    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29        match self {
30            BoxShadowClipMode::Outset => Ok(()), // Outset is the default, not written
31            BoxShadowClipMode::Inset => write!(f, "inset"),
32        }
33    }
34}
35
36/// Represents a single CSS shadow value, shared by both `box-shadow` and `text-shadow`.
37#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[repr(C)]
39pub struct StyleBoxShadow {
40    pub offset_x: PixelValueNoPercent,
41    pub offset_y: PixelValueNoPercent,
42    pub blur_radius: PixelValueNoPercent,
43    pub spread_radius: PixelValueNoPercent,
44    pub clip_mode: BoxShadowClipMode,
45    pub color: ColorU,
46}
47
48impl Default for StyleBoxShadow {
49    fn default() -> Self {
50        Self {
51            offset_x: PixelValueNoPercent::default(),
52            offset_y: PixelValueNoPercent::default(),
53            blur_radius: PixelValueNoPercent::default(),
54            spread_radius: PixelValueNoPercent::default(),
55            clip_mode: BoxShadowClipMode::default(),
56            color: ColorU::BLACK,
57        }
58    }
59}
60
61impl StyleBoxShadow {
62    /// Scales the pixel values of the shadow for a given DPI factor.
63    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
64        self.offset_x.scale_for_dpi(scale_factor);
65        self.offset_y.scale_for_dpi(scale_factor);
66        self.blur_radius.scale_for_dpi(scale_factor);
67        self.spread_radius.scale_for_dpi(scale_factor);
68    }
69}
70
71impl PrintAsCssValue for StyleBoxShadow {
72    fn print_as_css_value(&self) -> String {
73        let mut components = Vec::new();
74
75        if self.clip_mode == BoxShadowClipMode::Inset {
76            components.push("inset".to_string());
77        }
78        components.push(self.offset_x.to_string());
79        components.push(self.offset_y.to_string());
80
81        // Only print blur, spread, and color if they are not default, for brevity
82        if self.blur_radius.inner.number.get() != 0.0
83            || self.spread_radius.inner.number.get() != 0.0
84        {
85            components.push(self.blur_radius.to_string());
86        }
87        if self.spread_radius.inner.number.get() != 0.0 {
88            components.push(self.spread_radius.to_string());
89        }
90        if self.color != ColorU::BLACK {
91            // Assuming black is the default
92            components.push(self.color.to_hash());
93        }
94
95        components.join(" ")
96    }
97}
98
99// Formatting to Rust code for StyleBoxShadow
100impl crate::format_rust_code::FormatAsRustCode for StyleBoxShadow {
101    fn format_as_rust_code(&self, tabs: usize) -> String {
102        let t = String::from("    ").repeat(tabs);
103        format!(
104            "StyleBoxShadow {{\r\n{}    offset_x: {},\r\n{}    offset_y: {},\r\n{}    color: \
105             {},\r\n{}    blur_radius: {},\r\n{}    spread_radius: {},\r\n{}    clip_mode: \
106             BoxShadowClipMode::{:?},\r\n{}}}",
107            t,
108            crate::format_rust_code::format_pixel_value_no_percent(&self.offset_x),
109            t,
110            crate::format_rust_code::format_pixel_value_no_percent(&self.offset_y),
111            t,
112            crate::format_rust_code::format_color_value(&self.color),
113            t,
114            crate::format_rust_code::format_pixel_value_no_percent(&self.blur_radius),
115            t,
116            crate::format_rust_code::format_pixel_value_no_percent(&self.spread_radius),
117            t,
118            self.clip_mode,
119            t
120        )
121    }
122}
123
124// --- PARSER ---
125
126/// Error returned when parsing a CSS shadow value fails.
127#[derive(Clone, PartialEq)]
128pub enum CssShadowParseError<'a> {
129    TooManyOrTooFewComponents(&'a str),
130    ValueParseErr(CssPixelValueParseError<'a>),
131    ColorParseError(CssColorParseError<'a>),
132}
133
134impl_debug_as_display!(CssShadowParseError<'a>);
135impl_display! { CssShadowParseError<'a>, {
136    TooManyOrTooFewComponents(e) => format!("Expected 2 to 4 length values for box-shadow, found an invalid number of components in: \"{}\"", e),
137    ValueParseErr(e) => format!("Invalid length value in box-shadow: {}", e),
138    ColorParseError(e) => format!("Invalid color value in box-shadow: {}", e),
139}}
140
141impl_from!(
142    CssPixelValueParseError<'a>,
143    CssShadowParseError::ValueParseErr
144);
145impl_from!(CssColorParseError<'a>, CssShadowParseError::ColorParseError);
146
147/// Owned version of `CssShadowParseError`.
148#[derive(Debug, Clone, PartialEq)]
149#[repr(C, u8)]
150pub enum CssShadowParseErrorOwned {
151    TooManyOrTooFewComponents(AzString),
152    ValueParseErr(CssPixelValueParseErrorOwned),
153    ColorParseError(CssColorParseErrorOwned),
154}
155
156impl<'a> CssShadowParseError<'a> {
157    /// Converts the borrowed error into an owned version for storage.
158    pub fn to_contained(&self) -> CssShadowParseErrorOwned {
159        match self {
160            CssShadowParseError::TooManyOrTooFewComponents(s) => {
161                CssShadowParseErrorOwned::TooManyOrTooFewComponents(s.to_string().into())
162            }
163            CssShadowParseError::ValueParseErr(e) => {
164                CssShadowParseErrorOwned::ValueParseErr(e.to_contained())
165            }
166            CssShadowParseError::ColorParseError(e) => {
167                CssShadowParseErrorOwned::ColorParseError(e.to_contained())
168            }
169        }
170    }
171}
172
173impl CssShadowParseErrorOwned {
174    /// Converts the owned error back into a borrowed version.
175    pub fn to_shared<'a>(&'a self) -> CssShadowParseError<'a> {
176        match self {
177            CssShadowParseErrorOwned::TooManyOrTooFewComponents(s) => {
178                CssShadowParseError::TooManyOrTooFewComponents(s.as_str())
179            }
180            CssShadowParseErrorOwned::ValueParseErr(e) => {
181                CssShadowParseError::ValueParseErr(e.to_shared())
182            }
183            CssShadowParseErrorOwned::ColorParseError(e) => {
184                CssShadowParseError::ColorParseError(e.to_shared())
185            }
186        }
187    }
188}
189
190/// Parses a CSS box-shadow, such as `"5px 10px #888 inset"`.
191///
192/// Note: This parser does not handle the `none` keyword, as that is handled by the
193/// `CssPropertyValue` enum wrapper. It also does not handle comma-separated lists
194/// of multiple shadows; it only parses a single shadow value.
195#[cfg(feature = "parser")]
196pub fn parse_style_box_shadow<'a>(
197    input: &'a str,
198) -> Result<StyleBoxShadow, CssShadowParseError<'a>> {
199    let mut parts: Vec<&str> = input.split_whitespace().collect();
200    let mut shadow = StyleBoxShadow::default();
201
202    // The `inset` keyword can appear anywhere. Find it, set the flag, and remove it.
203    if let Some(pos) = parts.iter().position(|&p| p == "inset") {
204        shadow.clip_mode = BoxShadowClipMode::Inset;
205        parts.remove(pos);
206    }
207
208    // The color can also be anywhere. Find it, set the color, and remove it.
209    // It's the only part that isn't a length. We iterate from the back because
210    // it's slightly more common for the color to be last.
211    if let Some((pos, color)) = parts
212        .iter()
213        .enumerate()
214        .rev()
215        .find_map(|(i, p)| parse_css_color(p).ok().map(|c| (i, c)))
216    {
217        shadow.color = color;
218        parts.remove(pos);
219    }
220
221    // The remaining parts must be 2, 3, or 4 length values.
222    match parts.len() {
223        2..=4 => {
224            shadow.offset_x = parse_pixel_value_no_percent(parts[0])?;
225            shadow.offset_y = parse_pixel_value_no_percent(parts[1])?;
226            if parts.len() > 2 {
227                shadow.blur_radius = parse_pixel_value_no_percent(parts[2])?;
228            }
229            if parts.len() > 3 {
230                shadow.spread_radius = parse_pixel_value_no_percent(parts[3])?;
231            }
232        }
233        _ => return Err(CssShadowParseError::TooManyOrTooFewComponents(input)),
234    }
235
236    Ok(shadow)
237}
238
239#[cfg(all(test, feature = "parser"))]
240mod tests {
241    use super::*;
242    use crate::props::basic::pixel::PixelValue;
243
244    fn px_no_percent(val: f32) -> PixelValueNoPercent {
245        PixelValueNoPercent {
246            inner: PixelValue::px(val),
247        }
248    }
249
250    #[test]
251    fn test_parse_box_shadow_simple() {
252        let result = parse_style_box_shadow("10px 5px").unwrap();
253        assert_eq!(result.offset_x, px_no_percent(10.0));
254        assert_eq!(result.offset_y, px_no_percent(5.0));
255        assert_eq!(result.blur_radius, px_no_percent(0.0));
256        assert_eq!(result.spread_radius, px_no_percent(0.0));
257        assert_eq!(result.color, ColorU::BLACK);
258        assert_eq!(result.clip_mode, BoxShadowClipMode::Outset);
259    }
260
261    #[test]
262    fn test_parse_box_shadow_with_color() {
263        let result = parse_style_box_shadow("10px 5px #888").unwrap();
264        assert_eq!(result.offset_x, px_no_percent(10.0));
265        assert_eq!(result.offset_y, px_no_percent(5.0));
266        assert_eq!(result.color, ColorU::new_rgb(0x88, 0x88, 0x88));
267    }
268
269    #[test]
270    fn test_parse_box_shadow_with_blur() {
271        let result = parse_style_box_shadow("5px 10px 20px").unwrap();
272        assert_eq!(result.offset_x, px_no_percent(5.0));
273        assert_eq!(result.offset_y, px_no_percent(10.0));
274        assert_eq!(result.blur_radius, px_no_percent(20.0));
275    }
276
277    #[test]
278    fn test_parse_box_shadow_with_spread() {
279        let result = parse_style_box_shadow("2px 2px 2px 1px rgba(0,0,0,0.2)").unwrap();
280        assert_eq!(result.offset_x, px_no_percent(2.0));
281        assert_eq!(result.offset_y, px_no_percent(2.0));
282        assert_eq!(result.blur_radius, px_no_percent(2.0));
283        assert_eq!(result.spread_radius, px_no_percent(1.0));
284        assert_eq!(result.color, ColorU::new(0, 0, 0, 51));
285    }
286
287    #[test]
288    fn test_parse_box_shadow_inset() {
289        let result = parse_style_box_shadow("inset 0 0 10px #000").unwrap();
290        assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
291        assert_eq!(result.offset_x, px_no_percent(0.0));
292        assert_eq!(result.offset_y, px_no_percent(0.0));
293        assert_eq!(result.blur_radius, px_no_percent(10.0));
294        assert_eq!(result.color, ColorU::BLACK);
295    }
296
297    #[test]
298    fn test_parse_box_shadow_mixed_order() {
299        let result = parse_style_box_shadow("5px 1em red inset").unwrap();
300        assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
301        assert_eq!(result.offset_x, px_no_percent(5.0));
302        assert_eq!(
303            result.offset_y,
304            PixelValueNoPercent {
305                inner: PixelValue::em(1.0)
306            }
307        );
308        assert_eq!(result.color, ColorU::RED);
309    }
310
311    #[test]
312    fn test_parse_box_shadow_invalid() {
313        assert!(parse_style_box_shadow("10px").is_err());
314        assert!(parse_style_box_shadow("10px 5px 4px 3px 2px").is_err());
315        // Two colors: rposition picks "blue" as the color, leaving "red" which
316        // fails to parse as a pixel value.
317        assert!(parse_style_box_shadow("10px 5px red blue").is_err());
318        assert!(parse_style_box_shadow("10% 5px").is_err()); // No percent allowed
319    }
320}