Skip to main content

ferroui_base/media/
box_shadow.rs

1use crate::media::string_splitter::{split_respecting_brackets, StringSplitOptions};
2use crate::media::Color;
3use crate::utilities::span_helpers::{parse_double, write_double};
4use crate::utilities::FormatError;
5use crate::{Rect, Vector};
6use std::fmt;
7use std::str::FromStr;
8
9const SEPARATORS: [char; 2] = [' ', '\t'];
10const OPENING_PARENTHESIS: char = '(';
11const CLOSING_PARENTHESIS: char = ')';
12
13/// Represents a box shadow which can be attached to an element or control.
14#[derive(Clone, Copy, Debug, Default, PartialEq)]
15pub struct BoxShadow {
16    /// The horizontal offset (distance) of the shadow.
17    pub offset_x: f64,
18    /// The vertical offset (distance) of the shadow.
19    pub offset_y: f64,
20    /// The blur radius. A higher value results in a more blurred shadow.
21    pub blur: f64,
22    /// The spread radius. Positive values make the shadow grow, negative
23    /// values make it shrink.
24    pub spread: f64,
25    /// The color of the shadow.
26    pub color: Color,
27    /// Whether the shadow is an inner shadow (inset) rather than an outer
28    /// shadow (outset).
29    pub is_inset: bool,
30}
31
32impl BoxShadow {
33    /// Parses a `BoxShadow` string: `[inset] offset-x offset-y [blur [spread]] color`,
34    /// or `none`.
35    pub fn parse(s: &str) -> Result<BoxShadow, FormatError> {
36        if s.is_empty() {
37            return Err(FormatError::default());
38        }
39
40        let p = split_respecting_brackets(
41            s,
42            &SEPARATORS,
43            OPENING_PARENTHESIS,
44            CLOSING_PARENTHESIS,
45            StringSplitOptions::REMOVE_EMPTY_ENTRIES,
46        )?;
47        if p.len() == 1 && p[0] == "none" {
48            return Ok(BoxShadow::default());
49        }
50
51        if p.len() < 3 || p.len() > 6 {
52            return Err(FormatError::default());
53        }
54
55        let mut inset = false;
56        let mut tokens = p.iter().copied();
57        let mut read_string = || tokens.next().ok_or_else(FormatError::default);
58
59        let mut first_token = read_string()?;
60        if first_token == "inset" {
61            inset = true;
62            first_token = read_string()?;
63        }
64
65        let parse = |token: &str| parse_double(token).ok_or_else(|| FormatError::invalid_input(token));
66
67        let offset_x = parse(first_token)?;
68        let offset_y = parse(read_string()?)?;
69        let mut blur = 0.0;
70        let mut spread = 0.0;
71
72        let token3 = read_string().ok();
73        let token4 = read_string().ok();
74        let token5 = read_string().ok();
75
76        if token4.is_some() {
77            blur = parse(token3.ok_or_else(FormatError::default)?)?;
78        }
79        if token5.is_some() {
80            spread = parse(token4.ok_or_else(FormatError::default)?)?;
81        }
82
83        let color = Color::parse(token5.or(token4).or(token3).ok_or_else(FormatError::default)?)?;
84        Ok(BoxShadow { is_inset: inset, offset_x, offset_y, blur, spread, color })
85    }
86
87    /// Transforms the specified bounding rectangle to account for the shadow's
88    /// offset, spread, and blur.
89    pub fn transform_bounds(&self, rect: Rect) -> Rect {
90        if self.is_inset {
91            rect
92        } else {
93            rect.translate(Vector::new(self.offset_x, self.offset_y)).inflate(self.spread + self.blur)
94        }
95    }
96}
97
98impl FromStr for BoxShadow {
99    type Err = FormatError;
100    #[inline]
101    fn from_str(s: &str) -> Result<Self, FormatError> {
102        BoxShadow::parse(s)
103    }
104}
105
106impl fmt::Display for BoxShadow {
107    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
108        if *self == BoxShadow::default() {
109            return f.write_str("none");
110        }
111
112        if self.is_inset {
113            f.write_str("inset ")?;
114        }
115
116        write_double(f, self.offset_x)?;
117        f.write_str(" ")?;
118        write_double(f, self.offset_y)?;
119        f.write_str(" ")?;
120
121        if self.blur != 0.0 || self.spread != 0.0 {
122            write_double(f, self.blur)?;
123            f.write_str(" ")?;
124        }
125
126        if self.spread != 0.0 {
127            write_double(f, self.spread)?;
128            f.write_str(" ")?;
129        }
130
131        fmt::Display::fmt(&self.color, f)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::media::{BoxShadows, Colors};
139
140    #[test]
141    fn box_shadow_should_parse() {
142        for extra_spaces in [false, true] {
143            for inset in [false, true] {
144                for (color, expected_color) in
145                    [("red", Colors::RED), ("#FF122403", Color::from_uint32(0xFF122403))]
146                {
147                    for component_count in 2..5 {
148                        let mut s = format!("{}10 20", if inset { "inset " } else { "" });
149                        if component_count > 2 {
150                            s += " 30";
151                        }
152                        if component_count > 3 {
153                            s += " 40";
154                        }
155                        s += " ";
156                        s += color;
157                        if extra_spaces {
158                            s = format!(" {}   ", s.replace(' ', "  "));
159                        }
160
161                        let parsed = BoxShadow::parse(&s).unwrap();
162                        assert_eq!(inset, parsed.is_inset);
163                        assert_eq!(10.0, parsed.offset_x);
164                        assert_eq!(20.0, parsed.offset_y);
165                        assert_eq!(if component_count > 2 { 30.0 } else { 0.0 }, parsed.blur);
166                        assert_eq!(if component_count > 3 { 40.0 } else { 0.0 }, parsed.spread);
167                        assert_eq!(expected_color, parsed.color);
168                    }
169                }
170            }
171        }
172    }
173
174    fn assert_to_string(source: BoxShadows, expected: &str) {
175        assert!(expected.eq_ignore_ascii_case(&source.to_string()), "{source} != {expected}");
176    }
177
178    #[test]
179    fn box_shadows_should_to_string() {
180        assert_to_string(
181            BoxShadows::new(BoxShadow { offset_x: -15.0, offset_y: 20.0, spread: 5.0, color: Colors::RED, ..Default::default() }),
182            "-15 20 0 5 red",
183        );
184        assert_to_string(
185            BoxShadows::new(BoxShadow {
186                is_inset: true,
187                offset_x: -15.0,
188                offset_y: 20.0,
189                spread: 5.0,
190                color: Colors::RED,
191                ..Default::default()
192            }),
193            "inset -15 20 0 5 red",
194        );
195        assert_to_string(
196            BoxShadows::new(BoxShadow { offset_x: -15.0, offset_y: 20.0, blur: 5.0, color: Colors::RED, ..Default::default() }),
197            "-15 20 5 red",
198        );
199        assert_to_string(
200            BoxShadows::with_rest(
201                BoxShadow {
202                    offset_x: -20.0,
203                    offset_y: -20.0,
204                    blur: 60.0,
205                    color: Color::parse("#CCFFFFFF").unwrap(),
206                    ..Default::default()
207                },
208                &[BoxShadow {
209                    offset_x: 20.0,
210                    offset_y: 20.0,
211                    blur: 60.0,
212                    color: Color::parse("#33000000").unwrap(),
213                    ..Default::default()
214                }],
215            ),
216            "-20 -20 60 #CCFFFFFF, 20 20 60 #33000000",
217        );
218    }
219
220    #[test]
221    fn parse_rejects_invalid_input() {
222        assert!(BoxShadow::parse("").is_err());
223        assert!(BoxShadow::parse("10 red").is_err());
224        assert!(BoxShadow::parse("a b red").is_err());
225        assert!(BoxShadow::parse("1 2 3 4 5 6 red").is_err());
226        assert!(BoxShadow::parse("1 2 rgba(0,0").is_err());
227    }
228}