ferroui_base/media/
box_shadow.rs1use 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#[derive(Clone, Copy, Debug, Default, PartialEq)]
15pub struct BoxShadow {
16 pub offset_x: f64,
18 pub offset_y: f64,
20 pub blur: f64,
22 pub spread: f64,
25 pub color: Color,
27 pub is_inset: bool,
30}
31
32impl BoxShadow {
33 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 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}