Skip to main content

ferroui_base/media/
box_shadows.rs

1use crate::media::string_splitter::{split_respecting_brackets, StringSplitOptions};
2use crate::media::BoxShadow;
3use crate::utilities::FormatError;
4use crate::Rect;
5use std::fmt;
6use std::rc::Rc;
7use std::str::FromStr;
8
9const SEPARATOR: char = ',';
10const OPENING_PARENTHESIS: char = '(';
11const CLOSING_PARENTHESIS: char = ')';
12
13/// Represents a collection of [`BoxShadow`]s.
14#[derive(Clone, Debug, Default)]
15pub struct BoxShadows {
16    first: BoxShadow,
17    list: Option<Rc<[BoxShadow]>>,
18    count: usize,
19}
20
21impl BoxShadows {
22    /// Creates a collection containing a single shadow. A default shadow
23    /// produces an empty collection.
24    pub fn new(shadow: BoxShadow) -> Self {
25        Self { first: shadow, list: None, count: if shadow == BoxShadow::default() { 0 } else { 1 } }
26    }
27
28    /// Creates a collection containing `first` followed by `rest`.
29    pub fn with_rest(first: BoxShadow, rest: &[BoxShadow]) -> Self {
30        Self { first, list: Some(Rc::from(rest)), count: 1 + rest.len() }
31    }
32
33    /// The number of shadows in the collection.
34    #[inline]
35    pub fn count(&self) -> usize {
36        self.count
37    }
38
39    /// The shadow at `index`. Panics if the index is out of range.
40    pub fn get(&self, index: usize) -> BoxShadow {
41        if index >= self.count {
42            panic!("index out of range");
43        }
44        if index == 0 {
45            return self.first;
46        }
47        self.list.as_ref().expect("list is present when count > 1")[index - 1]
48    }
49
50    /// Iterates over the shadows.
51    pub fn iter(&self) -> impl Iterator<Item = BoxShadow> + '_ {
52        (0..self.count).map(move |index| self.get(index))
53    }
54
55    /// Parses a comma separated list of box shadows.
56    pub fn parse(s: &str) -> Result<BoxShadows, FormatError> {
57        let sp = split_respecting_brackets(
58            s,
59            &[SEPARATOR],
60            OPENING_PARENTHESIS,
61            CLOSING_PARENTHESIS,
62            StringSplitOptions::REMOVE_EMPTY_ENTRIES,
63        )?;
64        if sp.is_empty() || (sp.len() == 1 && (sp[0].trim().is_empty() || sp[0] == "none")) {
65            return Ok(BoxShadows::default());
66        }
67
68        let first = BoxShadow::parse(sp[0])?;
69        if sp.len() == 1 {
70            return Ok(BoxShadows::new(first));
71        }
72
73        let mut rest = Vec::with_capacity(sp.len() - 1);
74        for part in &sp[1..] {
75            rest.push(BoxShadow::parse(part)?);
76        }
77        Ok(BoxShadows::with_rest(first, &rest))
78    }
79
80    /// Transforms the specified bounding rectangle to account for every
81    /// shadow in the collection.
82    pub fn transform_bounds(&self, rect: Rect) -> Rect {
83        let mut result = rect;
84        for shadow in self.iter() {
85            result = result.union(shadow.transform_bounds(rect));
86        }
87        result
88    }
89
90    /// Whether the collection contains an inset shadow.
91    pub fn has_inset_shadows(&self) -> bool {
92        self.iter().any(|shadow| shadow != BoxShadow::default() && shadow.is_inset)
93    }
94}
95
96impl PartialEq for BoxShadows {
97    fn eq(&self, other: &Self) -> bool {
98        if other.count != self.count {
99            return false;
100        }
101        (0..self.count).all(|index| self.get(index) == other.get(index))
102    }
103}
104
105impl FromStr for BoxShadows {
106    type Err = FormatError;
107    #[inline]
108    fn from_str(s: &str) -> Result<Self, FormatError> {
109        BoxShadows::parse(s)
110    }
111}
112
113impl fmt::Display for BoxShadows {
114    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115        if self.count == 0 {
116            return f.write_str("none");
117        }
118        for (index, shadow) in self.iter().enumerate() {
119            if index > 0 {
120                f.write_str(", ")?;
121            }
122            fmt::Display::fmt(&shadow, f)?;
123        }
124        Ok(())
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn parse_none_returns_empty() {
134        for input in ["none", " none "] {
135            let bs = BoxShadows::parse(input).unwrap();
136            assert_eq!(0, bs.count());
137            assert_eq!(BoxShadows::default(), bs);
138            assert_eq!("none", bs.to_string());
139        }
140    }
141
142    #[test]
143    fn parse_single_shadow_to_string_round_trip() {
144        for input in [
145            "0 0 5 0 #FF0000",
146            "10 20 30 5 rgba(0,0,0,0.5)",
147            "10 20 30 5 rgba(0, 0, 0, 0.5)",
148            "  10  20  30  5  rgba(0,  0,  0,  0.5)  ",
149        ] {
150            let bs = BoxShadows::parse(input).unwrap();
151            assert_eq!(1, bs.count());
152            let reparsed = BoxShadows::parse(&bs.to_string()).unwrap();
153            assert_eq!(bs, reparsed);
154        }
155    }
156
157    #[test]
158    fn transform_bounds_includes_shadow_expansion() {
159        for (input, min_expansion) in [("0 0 5 0 #FF0000", 10.0), ("0 0 10 0 rgba(0,0,0,0.5)", 20.0)] {
160            let bs = BoxShadows::parse(input).unwrap();
161            let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
162            let transformed = bs.transform_bounds(rect);
163            assert!(transformed.width >= rect.width + min_expansion);
164            assert!(transformed.height >= rect.height + min_expansion);
165        }
166    }
167
168    #[test]
169    fn parse_color_function_is_handled() {
170        for input in ["5 5 10 0 rgba(10,20,30,0.4)", "5 5 10 0 hsla(10,20%,30%,0.4)", "5 5 10 0 hsva(10,20%,30%,0.4)"] {
171            let bs = BoxShadows::parse(input).unwrap();
172            assert_eq!(1, bs.count());
173            let reparsed = BoxShadows::parse(&bs.to_string()).unwrap();
174            assert_eq!(bs, reparsed);
175        }
176    }
177
178    #[test]
179    fn parse_multiple_shadows() {
180        for (input, count) in [
181            ("1 2 3 0 #FF0000", 1),
182            ("10 20 30 5 rgba(0,0,0,0.5)", 1),
183            ("1 2 3 0 #FF0000, 1 2 3 0 #FF0000", 2),
184            ("10 20 30 5 rgba(0,0,0,0.5), 1 2 3 0 #FF0000", 2),
185            ("10 20 30 5 rgba(0,0,0,0.5), 10 20 30 5 rgba(0,0,0,0.5)", 2),
186            ("10 20 30 5 rgba(0,0,0,0.5), 10 20 30 5 rgba(0,0,0,0.5), 10 20 30 5 rgba(0,0,0,0.5)", 3),
187            ("10 20 30 5 rgba(0,0,0,0.5), 10 20 30 5 #ffffff, 10 20 30 5 Red", 3),
188            ("  10 20 30 5 rgba(0, 0, 0, 0.5), 10 20 30 5 rgba(0, 0, 0, 0.5), 10 20 30 5 rgba(0, 0, 0, 0.5)  ", 3),
189            ("  10 20 30 5 rgba(0, 0, 0, 0.5), 10 20 30 5 #ffffff, 10 20 30 5 Red  ", 3),
190        ] {
191            let bs = BoxShadows::parse(input).unwrap();
192            assert_eq!(count, bs.count());
193            let reparsed = BoxShadows::parse(&bs.to_string()).unwrap();
194            assert_eq!(bs, reparsed);
195        }
196    }
197}