takumi_css/style/properties/
text_shadow.rs1use 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#[derive(Debug, Clone, PartialEq, Copy, Default, TypedBuilder)]
14#[builder(field_defaults(default))]
15#[non_exhaustive]
16pub struct TextShadow {
17 pub offset_x: LengthDefaultsToZero,
19 pub offset_y: LengthDefaultsToZero,
21 pub blur_radius: LengthDefaultsToZero,
23 pub color: ColorInput,
25}
26
27pub 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 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}