1use alloc::string::{String, ToString};
4use core::fmt;
5use crate::corety::AzString;
6
7use crate::props::{
8 basic::{
9 color::{parse_css_color, ColorU, CssColorParseError, CssColorParseErrorOwned},
10 pixel::{
11 parse_pixel_value_no_percent, CssPixelValueParseError, CssPixelValueParseErrorOwned,
12 PixelValueNoPercent,
13 },
14 },
15 formatter::PrintAsCssValue,
16};
17
18#[derive(Debug, Default, Copy, Clone, PartialEq, Ord, PartialOrd, Eq, Hash)]
20#[repr(C)]
21pub enum BoxShadowClipMode {
22 #[default]
23 Outset,
24 Inset,
25}
26
27impl fmt::Display for BoxShadowClipMode {
28 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29 match self {
30 BoxShadowClipMode::Outset => Ok(()), BoxShadowClipMode::Inset => write!(f, "inset"),
32 }
33 }
34}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
38#[repr(C)]
39pub struct StyleBoxShadow {
40 pub offset_x: PixelValueNoPercent,
41 pub offset_y: PixelValueNoPercent,
42 pub blur_radius: PixelValueNoPercent,
43 pub spread_radius: PixelValueNoPercent,
44 pub clip_mode: BoxShadowClipMode,
45 pub color: ColorU,
46}
47
48impl Default for StyleBoxShadow {
49 fn default() -> Self {
50 Self {
51 offset_x: PixelValueNoPercent::default(),
52 offset_y: PixelValueNoPercent::default(),
53 blur_radius: PixelValueNoPercent::default(),
54 spread_radius: PixelValueNoPercent::default(),
55 clip_mode: BoxShadowClipMode::default(),
56 color: ColorU::BLACK,
57 }
58 }
59}
60
61impl StyleBoxShadow {
62 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
64 self.offset_x.scale_for_dpi(scale_factor);
65 self.offset_y.scale_for_dpi(scale_factor);
66 self.blur_radius.scale_for_dpi(scale_factor);
67 self.spread_radius.scale_for_dpi(scale_factor);
68 }
69}
70
71impl PrintAsCssValue for StyleBoxShadow {
72 fn print_as_css_value(&self) -> String {
73 let mut components = Vec::new();
74
75 if self.clip_mode == BoxShadowClipMode::Inset {
76 components.push("inset".to_string());
77 }
78 components.push(self.offset_x.to_string());
79 components.push(self.offset_y.to_string());
80
81 if self.blur_radius.inner.number.get() != 0.0
83 || self.spread_radius.inner.number.get() != 0.0
84 {
85 components.push(self.blur_radius.to_string());
86 }
87 if self.spread_radius.inner.number.get() != 0.0 {
88 components.push(self.spread_radius.to_string());
89 }
90 if self.color != ColorU::BLACK {
91 components.push(self.color.to_hash());
93 }
94
95 components.join(" ")
96 }
97}
98
99impl crate::format_rust_code::FormatAsRustCode for StyleBoxShadow {
101 fn format_as_rust_code(&self, tabs: usize) -> String {
102 let t = String::from(" ").repeat(tabs);
103 format!(
104 "StyleBoxShadow {{\r\n{} offset_x: {},\r\n{} offset_y: {},\r\n{} color: \
105 {},\r\n{} blur_radius: {},\r\n{} spread_radius: {},\r\n{} clip_mode: \
106 BoxShadowClipMode::{:?},\r\n{}}}",
107 t,
108 crate::format_rust_code::format_pixel_value_no_percent(&self.offset_x),
109 t,
110 crate::format_rust_code::format_pixel_value_no_percent(&self.offset_y),
111 t,
112 crate::format_rust_code::format_color_value(&self.color),
113 t,
114 crate::format_rust_code::format_pixel_value_no_percent(&self.blur_radius),
115 t,
116 crate::format_rust_code::format_pixel_value_no_percent(&self.spread_radius),
117 t,
118 self.clip_mode,
119 t
120 )
121 }
122}
123
124#[derive(Clone, PartialEq)]
128pub enum CssShadowParseError<'a> {
129 TooManyOrTooFewComponents(&'a str),
130 ValueParseErr(CssPixelValueParseError<'a>),
131 ColorParseError(CssColorParseError<'a>),
132}
133
134impl_debug_as_display!(CssShadowParseError<'a>);
135impl_display! { CssShadowParseError<'a>, {
136 TooManyOrTooFewComponents(e) => format!("Expected 2 to 4 length values for box-shadow, found an invalid number of components in: \"{}\"", e),
137 ValueParseErr(e) => format!("Invalid length value in box-shadow: {}", e),
138 ColorParseError(e) => format!("Invalid color value in box-shadow: {}", e),
139}}
140
141impl_from!(
142 CssPixelValueParseError<'a>,
143 CssShadowParseError::ValueParseErr
144);
145impl_from!(CssColorParseError<'a>, CssShadowParseError::ColorParseError);
146
147#[derive(Debug, Clone, PartialEq)]
149#[repr(C, u8)]
150pub enum CssShadowParseErrorOwned {
151 TooManyOrTooFewComponents(AzString),
152 ValueParseErr(CssPixelValueParseErrorOwned),
153 ColorParseError(CssColorParseErrorOwned),
154}
155
156impl<'a> CssShadowParseError<'a> {
157 pub fn to_contained(&self) -> CssShadowParseErrorOwned {
159 match self {
160 CssShadowParseError::TooManyOrTooFewComponents(s) => {
161 CssShadowParseErrorOwned::TooManyOrTooFewComponents(s.to_string().into())
162 }
163 CssShadowParseError::ValueParseErr(e) => {
164 CssShadowParseErrorOwned::ValueParseErr(e.to_contained())
165 }
166 CssShadowParseError::ColorParseError(e) => {
167 CssShadowParseErrorOwned::ColorParseError(e.to_contained())
168 }
169 }
170 }
171}
172
173impl CssShadowParseErrorOwned {
174 pub fn to_shared<'a>(&'a self) -> CssShadowParseError<'a> {
176 match self {
177 CssShadowParseErrorOwned::TooManyOrTooFewComponents(s) => {
178 CssShadowParseError::TooManyOrTooFewComponents(s.as_str())
179 }
180 CssShadowParseErrorOwned::ValueParseErr(e) => {
181 CssShadowParseError::ValueParseErr(e.to_shared())
182 }
183 CssShadowParseErrorOwned::ColorParseError(e) => {
184 CssShadowParseError::ColorParseError(e.to_shared())
185 }
186 }
187 }
188}
189
190#[cfg(feature = "parser")]
196pub fn parse_style_box_shadow<'a>(
197 input: &'a str,
198) -> Result<StyleBoxShadow, CssShadowParseError<'a>> {
199 let mut parts: Vec<&str> = input.split_whitespace().collect();
200 let mut shadow = StyleBoxShadow::default();
201
202 if let Some(pos) = parts.iter().position(|&p| p == "inset") {
204 shadow.clip_mode = BoxShadowClipMode::Inset;
205 parts.remove(pos);
206 }
207
208 if let Some((pos, color)) = parts
212 .iter()
213 .enumerate()
214 .rev()
215 .find_map(|(i, p)| parse_css_color(p).ok().map(|c| (i, c)))
216 {
217 shadow.color = color;
218 parts.remove(pos);
219 }
220
221 match parts.len() {
223 2..=4 => {
224 shadow.offset_x = parse_pixel_value_no_percent(parts[0])?;
225 shadow.offset_y = parse_pixel_value_no_percent(parts[1])?;
226 if parts.len() > 2 {
227 shadow.blur_radius = parse_pixel_value_no_percent(parts[2])?;
228 }
229 if parts.len() > 3 {
230 shadow.spread_radius = parse_pixel_value_no_percent(parts[3])?;
231 }
232 }
233 _ => return Err(CssShadowParseError::TooManyOrTooFewComponents(input)),
234 }
235
236 Ok(shadow)
237}
238
239#[cfg(all(test, feature = "parser"))]
240mod tests {
241 use super::*;
242 use crate::props::basic::pixel::PixelValue;
243
244 fn px_no_percent(val: f32) -> PixelValueNoPercent {
245 PixelValueNoPercent {
246 inner: PixelValue::px(val),
247 }
248 }
249
250 #[test]
251 fn test_parse_box_shadow_simple() {
252 let result = parse_style_box_shadow("10px 5px").unwrap();
253 assert_eq!(result.offset_x, px_no_percent(10.0));
254 assert_eq!(result.offset_y, px_no_percent(5.0));
255 assert_eq!(result.blur_radius, px_no_percent(0.0));
256 assert_eq!(result.spread_radius, px_no_percent(0.0));
257 assert_eq!(result.color, ColorU::BLACK);
258 assert_eq!(result.clip_mode, BoxShadowClipMode::Outset);
259 }
260
261 #[test]
262 fn test_parse_box_shadow_with_color() {
263 let result = parse_style_box_shadow("10px 5px #888").unwrap();
264 assert_eq!(result.offset_x, px_no_percent(10.0));
265 assert_eq!(result.offset_y, px_no_percent(5.0));
266 assert_eq!(result.color, ColorU::new_rgb(0x88, 0x88, 0x88));
267 }
268
269 #[test]
270 fn test_parse_box_shadow_with_blur() {
271 let result = parse_style_box_shadow("5px 10px 20px").unwrap();
272 assert_eq!(result.offset_x, px_no_percent(5.0));
273 assert_eq!(result.offset_y, px_no_percent(10.0));
274 assert_eq!(result.blur_radius, px_no_percent(20.0));
275 }
276
277 #[test]
278 fn test_parse_box_shadow_with_spread() {
279 let result = parse_style_box_shadow("2px 2px 2px 1px rgba(0,0,0,0.2)").unwrap();
280 assert_eq!(result.offset_x, px_no_percent(2.0));
281 assert_eq!(result.offset_y, px_no_percent(2.0));
282 assert_eq!(result.blur_radius, px_no_percent(2.0));
283 assert_eq!(result.spread_radius, px_no_percent(1.0));
284 assert_eq!(result.color, ColorU::new(0, 0, 0, 51));
285 }
286
287 #[test]
288 fn test_parse_box_shadow_inset() {
289 let result = parse_style_box_shadow("inset 0 0 10px #000").unwrap();
290 assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
291 assert_eq!(result.offset_x, px_no_percent(0.0));
292 assert_eq!(result.offset_y, px_no_percent(0.0));
293 assert_eq!(result.blur_radius, px_no_percent(10.0));
294 assert_eq!(result.color, ColorU::BLACK);
295 }
296
297 #[test]
298 fn test_parse_box_shadow_mixed_order() {
299 let result = parse_style_box_shadow("5px 1em red inset").unwrap();
300 assert_eq!(result.clip_mode, BoxShadowClipMode::Inset);
301 assert_eq!(result.offset_x, px_no_percent(5.0));
302 assert_eq!(
303 result.offset_y,
304 PixelValueNoPercent {
305 inner: PixelValue::em(1.0)
306 }
307 );
308 assert_eq!(result.color, ColorU::RED);
309 }
310
311 #[test]
312 fn test_parse_box_shadow_invalid() {
313 assert!(parse_style_box_shadow("10px").is_err());
314 assert!(parse_style_box_shadow("10px 5px 4px 3px 2px").is_err());
315 assert!(parse_style_box_shadow("10px 5px red blue").is_err());
318 assert!(parse_style_box_shadow("10% 5px").is_err()); }
320}