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#[derive(Clone, Debug, Default)]
15pub struct BoxShadows {
16 first: BoxShadow,
17 list: Option<Rc<[BoxShadow]>>,
18 count: usize,
19}
20
21impl BoxShadows {
22 pub fn new(shadow: BoxShadow) -> Self {
25 Self { first: shadow, list: None, count: if shadow == BoxShadow::default() { 0 } else { 1 } }
26 }
27
28 pub fn with_rest(first: BoxShadow, rest: &[BoxShadow]) -> Self {
30 Self { first, list: Some(Rc::from(rest)), count: 1 + rest.len() }
31 }
32
33 #[inline]
35 pub fn count(&self) -> usize {
36 self.count
37 }
38
39 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 pub fn iter(&self) -> impl Iterator<Item = BoxShadow> + '_ {
52 (0..self.count).map(move |index| self.get(index))
53 }
54
55 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 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 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}