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