Skip to main content

takumi_css/style/properties/
text_shadow.rs

1use std::{fmt, fmt::Debug};
2
3use cssparser::{BasicParseErrorKind, Parser};
4use typed_builder::TypedBuilder;
5
6use super::box_shadow::parse_offsets_blur;
7use crate::style::{
8  Animatable, Color, ColorInput, CssSyntaxKind, CssToken, FromCss, Length, LengthDefaultsToZero,
9  ListInterpolationStrategy, MakeComputed, ParseResult, SizingContext, ToCss, next_is_comma,
10};
11
12/// Represents a text shadow with all its properties.
13#[derive(Debug, Clone, PartialEq, Copy, Default, TypedBuilder)]
14#[builder(field_defaults(default))]
15#[non_exhaustive]
16pub struct TextShadow {
17  /// Horizontal offset of the shadow.
18  pub offset_x: LengthDefaultsToZero,
19  /// Vertical offset of the shadow.
20  pub offset_y: LengthDefaultsToZero,
21  /// Blur radius of the shadow. Higher values create a more blurred shadow.
22  pub blur_radius: LengthDefaultsToZero,
23  /// Color of the shadow.
24  pub color: ColorInput,
25}
26
27/// Represents a collection of text shadows; has custom `FromCss` implementation for comma-separated values.
28pub type TextShadows = Box<[TextShadow]>;
29
30impl<'i> FromCss<'i> for TextShadows {
31  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
32    Ok(
33      input
34        .parse_comma_separated(TextShadow::from_css)?
35        .into_boxed_slice(),
36    )
37  }
38
39  const VALID_TOKENS: &'static [CssToken] = TextShadow::VALID_TOKENS;
40}
41
42impl<'i> FromCss<'i> for TextShadow {
43  /// Parses a text-shadow value from CSS input.
44  ///
45  /// The text-shadow syntax supports the following components (in that order):
46  /// - Two length values for horizontal and vertical offsets (required)
47  /// - An optional length value for blur radius
48  /// - An optional color value
49  ///
50  /// Examples:
51  /// - `text-shadow: 2px 4px;`
52  /// - `text-shadow: 2px 4px 6px;`
53  /// - `text-shadow: 2px 4px red;`
54  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, TextShadow> {
55    let mut color = None;
56    let mut lengths = None;
57
58    while !input.is_exhausted() && !next_is_comma(input) {
59      if lengths.is_none() {
60        let value = input.try_parse(parse_offsets_blur);
61
62        if let Ok(value) = value {
63          lengths = Some(value);
64          continue;
65        }
66      }
67
68      if color.is_none()
69        && let Ok(value) = input.try_parse(ColorInput::from_css)
70      {
71        color = Some(value);
72        continue;
73      }
74
75      break;
76    }
77
78    let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
79
80    Ok(TextShadow {
81      color: color.unwrap_or(ColorInput::CurrentColor),
82      offset_x: lengths.0,
83      offset_y: lengths.1,
84      blur_radius: lengths.2,
85    })
86  }
87
88  const VALID_TOKENS: &'static [CssToken] = &[
89    CssToken::Syntax(CssSyntaxKind::Length),
90    CssToken::Syntax(CssSyntaxKind::Color),
91  ];
92}
93
94impl crate::style::tw::TailwindPropertyParser for TextShadow {
95  fn parse_tw(token: &str) -> Option<Self> {
96    Self::from_str(token).ok()
97  }
98}
99
100impl MakeComputed for TextShadow {
101  fn make_computed(&mut self, sizing: &SizingContext) {
102    self.offset_x.make_computed(sizing);
103    self.offset_y.make_computed(sizing);
104    self.blur_radius.make_computed(sizing);
105  }
106}
107
108impl Animatable for TextShadow {
109  fn list_interpolation_strategy() -> ListInterpolationStrategy {
110    ListInterpolationStrategy::PadToLongestWithNeutral
111  }
112
113  fn neutral_value_like(_other: &Self) -> Option<Self> {
114    Some(Self {
115      offset_x: Length::zero(),
116      offset_y: Length::zero(),
117      blur_radius: Length::zero(),
118      color: Color::transparent().into(),
119    })
120  }
121
122  fn interpolate(
123    &mut self,
124    from: &Self,
125    to: &Self,
126    progress: f32,
127    sizing: &SizingContext,
128    current_color: Color,
129  ) {
130    self.offset_x.interpolate(
131      &from.offset_x,
132      &to.offset_x,
133      progress,
134      sizing,
135      current_color,
136    );
137    self.offset_y.interpolate(
138      &from.offset_y,
139      &to.offset_y,
140      progress,
141      sizing,
142      current_color,
143    );
144    self.blur_radius.interpolate(
145      &from.blur_radius,
146      &to.blur_radius,
147      progress,
148      sizing,
149      current_color,
150    );
151    self
152      .color
153      .interpolate(&from.color, &to.color, progress, sizing, current_color);
154  }
155}
156
157impl ToCss for TextShadow {
158  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
159    self.offset_x.to_css(dest)?;
160    dest.write_char(' ')?;
161    self.offset_y.to_css(dest)?;
162    if self.blur_radius != Length::zero() {
163      dest.write_char(' ')?;
164      self.blur_radius.to_css(dest)?;
165    }
166    if self.color != ColorInput::CurrentColor {
167      dest.write_char(' ')?;
168      self.color.to_css(dest)?;
169    }
170    Ok(())
171  }
172}
173
174#[cfg(test)]
175mod tests {
176  use crate::style::{Color, Length::Px};
177
178  use super::*;
179
180  #[test]
181  fn test_parse_text_shadow_no_blur_radius() {
182    assert_eq!(
183      TextShadows::from_str("5px 5px #558abb"),
184      Ok(
185        [TextShadow {
186          offset_x: Px(5.0),
187          offset_y: Px(5.0),
188          blur_radius: Px(0.0),
189          color: Color([85, 138, 187, 255]).into(),
190        }]
191        .into()
192      )
193    );
194  }
195
196  #[test]
197  fn test_parse_text_shadow_multiple_values() {
198    assert_eq!(
199      TextShadows::from_str("5px 5px #558abb, 10px 10px #558abb"),
200      Ok(
201        [
202          TextShadow {
203            offset_x: Px(5.0),
204            offset_y: Px(5.0),
205            blur_radius: Px(0.0),
206            color: Color([85, 138, 187, 255]).into(),
207          },
208          TextShadow {
209            offset_x: Px(10.0),
210            offset_y: Px(10.0),
211            blur_radius: Px(0.0),
212            color: Color([85, 138, 187, 255]).into(),
213          }
214        ]
215        .into()
216      )
217    );
218  }
219
220  #[test]
221  fn test_parse_text_shadow_multiple_rgba_values() {
222    assert_eq!(
223      TextShadows::from_str("5px 5px rgba(0, 0, 0, 0.5), 10px 10px rgba(255, 0, 0, 0.25)"),
224      Ok(
225        [
226          TextShadow {
227            offset_x: Px(5.0),
228            offset_y: Px(5.0),
229            blur_radius: Px(0.0),
230            color: Color([0, 0, 0, 128]).into(),
231          },
232          TextShadow {
233            offset_x: Px(10.0),
234            offset_y: Px(10.0),
235            blur_radius: Px(0.0),
236            color: Color([255, 0, 0, 64]).into(),
237          }
238        ]
239        .into()
240      )
241    );
242  }
243}