Skip to main content

takumi_css/style/properties/
box_shadow.rs

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/// Represents a box shadow with all its properties.
12/// Construct with [`BoxShadow::builder`].
13#[derive(Debug, Clone, PartialEq, Copy, Default, TypedBuilder)]
14#[builder(field_defaults(default))]
15#[non_exhaustive]
16pub struct BoxShadow {
17  /// Whether the shadow is inset (inside the element) or outset (outside the element).
18  #[builder(default = false)]
19  pub inset: bool,
20  /// Horizontal offset of the shadow.
21  pub offset_x: LengthDefaultsToZero,
22  /// Vertical offset of the shadow.
23  pub offset_y: LengthDefaultsToZero,
24  /// Blur radius of the shadow. Higher values create a more blurred shadow.
25  pub blur_radius: LengthDefaultsToZero,
26  /// Spread radius of the shadow. Positive values expand the shadow, negative values shrink it.
27  pub spread_radius: LengthDefaultsToZero,
28  /// Color of the shadow.
29  pub color: ColorInput,
30}
31
32/// Represents a collection of box shadows, have custom `FromCss` implementation for comma-separated values.
33pub 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  /// Parses a box-shadow value from CSS input.
65  ///
66  /// The box-shadow syntax allows for the following components in any order:
67  /// - inset keyword (optional)
68  /// - Two length values for horizontal and vertical offsets (required)
69  /// - Two optional length values for blur radius and spread radius
70  /// - A color value (optional)
71  ///
72  /// Examples:
73  /// - `box-shadow: 2px 4px;`
74  /// - `box-shadow: 2px 4px 6px;`
75  /// - `box-shadow: 2px 4px 6px 8px;`
76  /// - `box-shadow: 2px 4px red;`
77  /// - `box-shadow: inset 2px 4px 6px red;`
78  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}