1use std::{borrow::Cow, fmt, fmt::Debug};
2
3use cssparser::{BasicParseErrorKind, ParseError, Parser};
4use typed_builder::TypedBuilder;
5
6use crate::style::{
7 Animatable, Color, ColorInput, CssSyntaxKind, CssToken, FromCss, Length, LengthDefaultsToZero,
8 ListInterpolationStrategy, MakeComputed, ParseResult, SizingContext, ToCss, next_is_comma,
9};
10
11#[derive(Debug, Clone, PartialEq, Copy, Default, TypedBuilder)]
14#[builder(field_defaults(default))]
15#[non_exhaustive]
16pub struct BoxShadow {
17 #[builder(default = false)]
19 pub inset: bool,
20 pub offset_x: LengthDefaultsToZero,
22 pub offset_y: LengthDefaultsToZero,
24 pub blur_radius: LengthDefaultsToZero,
26 pub spread_radius: LengthDefaultsToZero,
28 pub color: ColorInput,
30}
31
32pub type BoxShadows = Box<[BoxShadow]>;
34
35impl<'i> FromCss<'i> for BoxShadows {
36 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
37 Ok(
38 input
39 .parse_comma_separated(BoxShadow::from_css)?
40 .into_boxed_slice(),
41 )
42 }
43
44 const VALID_TOKENS: &'static [CssToken] = BoxShadow::VALID_TOKENS;
45}
46
47pub(super) fn parse_offsets_blur<'i>(
48 input: &mut Parser<'i, '_>,
49) -> ParseResult<
50 'i,
51 (
52 LengthDefaultsToZero,
53 LengthDefaultsToZero,
54 LengthDefaultsToZero,
55 ),
56> {
57 let horizontal = Length::from_css(input)?;
58 let vertical = Length::from_css(input)?;
59 let blur = input.try_parse(Length::from_css).unwrap_or(Length::zero());
60 Ok((horizontal, vertical, blur))
61}
62
63impl<'i> FromCss<'i> for BoxShadow {
64 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, BoxShadow> {
79 let mut color = None;
80 let mut lengths = None;
81 let mut inset = false;
82
83 while !input.is_exhausted() && !next_is_comma(input) {
84 if !inset
85 && input
86 .try_parse(|input| input.expect_ident_matching("inset"))
87 .is_ok()
88 {
89 inset = true;
90 continue;
91 }
92
93 if lengths.is_none() {
94 let value = input.try_parse::<_, _, ParseError<Cow<'i, str>>>(|input| {
95 let (horizontal, vertical, blur) = parse_offsets_blur(input)?;
96 let spread = input.try_parse(Length::from_css).unwrap_or(Length::zero());
97 Ok((horizontal, vertical, blur, spread))
98 });
99
100 if let Ok(value) = value {
101 lengths = Some(value);
102 continue;
103 }
104 }
105
106 if color.is_none()
107 && let Ok(value) = input.try_parse(ColorInput::from_css)
108 {
109 color = Some(value);
110 continue;
111 }
112
113 break;
114 }
115
116 let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
117
118 Ok(BoxShadow {
119 color: color.unwrap_or(ColorInput::Value(Color::transparent())),
120 offset_x: lengths.0,
121 offset_y: lengths.1,
122 blur_radius: lengths.2,
123 spread_radius: lengths.3,
124 inset,
125 })
126 }
127
128 const VALID_TOKENS: &'static [CssToken] = &[
129 CssToken::Keyword("inset"),
130 CssToken::Syntax(CssSyntaxKind::Length),
131 CssToken::Syntax(CssSyntaxKind::Color),
132 ];
133}
134
135impl crate::style::tw::TailwindPropertyParser for BoxShadow {
136 fn parse_tw(token: &str) -> Option<Self> {
137 Self::from_str(token).ok()
138 }
139}
140
141impl MakeComputed for BoxShadow {
142 fn make_computed(&mut self, sizing: &SizingContext) {
143 self.offset_x.make_computed(sizing);
144 self.offset_y.make_computed(sizing);
145 self.blur_radius.make_computed(sizing);
146 self.spread_radius.make_computed(sizing);
147 }
148}
149
150impl Animatable for BoxShadow {
151 fn list_interpolation_strategy() -> ListInterpolationStrategy {
152 ListInterpolationStrategy::PadToLongestWithNeutral
153 }
154
155 fn neutral_value_like(other: &Self) -> Option<Self> {
156 Some(Self {
157 inset: other.inset,
158 offset_x: Length::zero(),
159 offset_y: Length::zero(),
160 blur_radius: Length::zero(),
161 spread_radius: Length::zero(),
162 color: Color::transparent().into(),
163 })
164 }
165
166 fn interpolate(
167 &mut self,
168 from: &Self,
169 to: &Self,
170 progress: f32,
171 sizing: &SizingContext,
172 current_color: Color,
173 ) {
174 if from.inset != to.inset {
175 *self = if progress >= 0.5 { *to } else { *from };
176 return;
177 }
178
179 self.inset = from.inset;
180 self.offset_x.interpolate(
181 &from.offset_x,
182 &to.offset_x,
183 progress,
184 sizing,
185 current_color,
186 );
187 self.offset_y.interpolate(
188 &from.offset_y,
189 &to.offset_y,
190 progress,
191 sizing,
192 current_color,
193 );
194 self.blur_radius.interpolate(
195 &from.blur_radius,
196 &to.blur_radius,
197 progress,
198 sizing,
199 current_color,
200 );
201 self.spread_radius.interpolate(
202 &from.spread_radius,
203 &to.spread_radius,
204 progress,
205 sizing,
206 current_color,
207 );
208 self
209 .color
210 .interpolate(&from.color, &to.color, progress, sizing, current_color);
211 }
212}
213
214impl ToCss for BoxShadow {
215 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
216 if self.inset {
217 dest.write_str("inset ")?;
218 }
219 self.offset_x.to_css(dest)?;
220 dest.write_char(' ')?;
221 self.offset_y.to_css(dest)?;
222
223 let blur_zero = self.blur_radius == Length::zero();
224 let spread_zero = self.spread_radius == Length::zero();
225 if !spread_zero {
226 dest.write_char(' ')?;
227 self.blur_radius.to_css(dest)?;
228 dest.write_char(' ')?;
229 self.spread_radius.to_css(dest)?;
230 } else if !blur_zero {
231 dest.write_char(' ')?;
232 self.blur_radius.to_css(dest)?;
233 }
234
235 dest.write_char(' ')?;
236 self.color.to_css(dest)
237 }
238}
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::style::{
243 Color,
244 Length::{self, Px},
245 };
246
247 fn red() -> ColorInput {
248 ColorInput::Value(Color([255, 0, 0, 255]))
249 }
250
251 fn transparent() -> ColorInput {
252 ColorInput::Value(Color::transparent())
253 }
254
255 #[test]
256 fn test_parse_box_shadow() {
257 let cases: &[(&str, BoxShadow)] = &[
258 (
259 "2px 4px",
260 BoxShadow {
261 offset_x: Px(2.0),
262 offset_y: Px(4.0),
263 color: transparent(),
264 ..Default::default()
265 },
266 ),
267 (
268 "2px 4px 6px",
269 BoxShadow {
270 offset_x: Px(2.0),
271 offset_y: Px(4.0),
272 blur_radius: Px(6.0),
273 color: transparent(),
274 ..Default::default()
275 },
276 ),
277 (
278 "2px 4px 6px 8px",
279 BoxShadow {
280 offset_x: Px(2.0),
281 offset_y: Px(4.0),
282 blur_radius: Px(6.0),
283 spread_radius: Px(8.0),
284 color: transparent(),
285 ..Default::default()
286 },
287 ),
288 (
289 "2px 4px red",
290 BoxShadow {
291 offset_x: Px(2.0),
292 offset_y: Px(4.0),
293 color: red(),
294 ..Default::default()
295 },
296 ),
297 (
298 "inset 2px 4px",
299 BoxShadow {
300 offset_x: Px(2.0),
301 offset_y: Px(4.0),
302 color: transparent(),
303 inset: true,
304 ..Default::default()
305 },
306 ),
307 (
308 "red 2px 4px",
309 BoxShadow {
310 offset_x: Px(2.0),
311 offset_y: Px(4.0),
312 color: red(),
313 ..Default::default()
314 },
315 ),
316 (
317 "2px 4px inset red",
318 BoxShadow {
319 offset_x: Px(2.0),
320 offset_y: Px(4.0),
321 color: red(),
322 inset: true,
323 ..Default::default()
324 },
325 ),
326 (
327 "2px 4px #ff0000",
328 BoxShadow {
329 offset_x: Px(2.0),
330 offset_y: Px(4.0),
331 color: red(),
332 ..Default::default()
333 },
334 ),
335 (
336 "2px 4px rgba(255, 0, 0, 0.5)",
337 BoxShadow {
338 offset_x: Px(2.0),
339 offset_y: Px(4.0),
340 color: ColorInput::Value(Color([255, 0, 0, 128])),
341 ..Default::default()
342 },
343 ),
344 ];
345
346 for (css, expected) in cases {
347 assert_eq!(BoxShadow::from_str(css), Ok(*expected), "css: {css}");
348 }
349 }
350
351 #[test]
352 fn test_parse_box_shadow_invalid() {
353 assert!(BoxShadow::from_str("2px").is_err());
354 assert!(BoxShadow::from_str("").is_err());
355 }
356
357 #[test]
358 fn test_parse_multiple_box_shadows_with_rgba() {
359 assert_eq!(
360 BoxShadows::from_str("2px 4px rgba(0, 0, 0, 0.5), 1px 2px 3px rgba(255, 0, 0, 0.25)"),
361 Ok(
362 [
363 BoxShadow {
364 offset_x: Px(2.0),
365 offset_y: Px(4.0),
366 blur_radius: Length::zero(),
367 spread_radius: Length::zero(),
368 color: ColorInput::Value(Color([0, 0, 0, 128])),
369 inset: false,
370 },
371 BoxShadow {
372 offset_x: Px(1.0),
373 offset_y: Px(2.0),
374 blur_radius: Px(3.0),
375 spread_radius: Length::zero(),
376 color: ColorInput::Value(Color([255, 0, 0, 64])),
377 inset: false,
378 }
379 ]
380 .into()
381 )
382 );
383 }
384}