Skip to main content

azul_css/props/style/
box_shadow.rs

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