takumi_css/style/properties/
text_decoration.rs1use std::fmt;
2
3use crate::style::{ToCss, unexpected_token};
4use bitflags::bitflags;
5use cssparser::{Parser, Token, match_ignore_ascii_case};
6use typed_builder::TypedBuilder;
7
8use crate::style::{
9 Animatable, Color, CssSyntaxKind, CssToken, FromCss, Length, MakeComputed, ParseResult,
10 SizingContext, declare_enum_from_css_impl, properties::ColorInput, tw::TailwindPropertyParser,
11};
12
13bitflags! {
14 #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
16 #[non_exhaustive]
17 pub struct TextDecorationLines: u8 {
18 const UNDERLINE = 0b001;
20 const LINE_THROUGH = 0b010;
22 const OVERLINE = 0b100;
24 }
25}
26
27impl<'i> FromCss<'i> for TextDecorationLines {
28 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
29 let mut lines = TextDecorationLines::empty();
30
31 let first_location = input.current_source_location();
33 let first_ident = input.expect_ident()?;
34 match_ignore_ascii_case! {first_ident,
35 "underline" => lines |= TextDecorationLines::UNDERLINE,
36 "line-through" => lines |= TextDecorationLines::LINE_THROUGH,
37 "overline" => lines |= TextDecorationLines::OVERLINE,
38 _ => return Err(unexpected_token!(first_location, &Token::Ident(first_ident.clone()))),
39 }
40
41 while !input.is_exhausted() {
43 let state = input.state();
44 if let Ok(ident) = input.expect_ident() {
45 match_ignore_ascii_case! {ident,
46 "underline" => lines |= TextDecorationLines::UNDERLINE,
47 "line-through" => lines |= TextDecorationLines::LINE_THROUGH,
48 "overline" => lines |= TextDecorationLines::OVERLINE,
49 _ => {
50 input.reset(&state);
51 break;
52 }
53 }
54 } else {
55 break;
56 }
57 }
58
59 Ok(lines)
60 }
61
62 const VALID_TOKENS: &'static [CssToken] = &[
63 CssToken::Keyword("underline"),
64 CssToken::Keyword("line-through"),
65 CssToken::Keyword("overline"),
66 ];
67}
68
69impl MakeComputed for TextDecorationLines {}
70
71#[derive(Debug, Clone, Copy, PartialEq)]
73#[non_exhaustive]
74pub enum TextDecorationThickness {
75 FromFont,
77 Length(Length),
79}
80
81impl Default for TextDecorationThickness {
82 fn default() -> Self {
83 Self::Length(Length::Auto)
84 }
85}
86
87impl MakeComputed for TextDecorationThickness {
88 fn make_computed(&mut self, sizing: &SizingContext) {
89 if let Self::Length(length) = self {
90 length.make_computed(sizing);
91 }
92 }
93}
94
95impl Animatable for TextDecorationThickness {
96 fn interpolate(
97 &mut self,
98 from: &Self,
99 to: &Self,
100 progress: f32,
101 sizing: &SizingContext,
102 current_color: Color,
103 ) {
104 *self = match (*from, *to) {
105 (TextDecorationThickness::Length(from), TextDecorationThickness::Length(to)) => {
106 let mut value = from;
107 value.interpolate(&from, &to, progress, sizing, current_color);
108 TextDecorationThickness::Length(value)
109 }
110 _ => {
111 if progress >= 0.5 {
112 *to
113 } else {
114 *from
115 }
116 }
117 };
118 }
119}
120
121#[derive(Debug, Clone, Copy, PartialEq)]
122pub enum SizedTextDecorationThickness {
123 FromFont,
124 Value(f32),
125}
126
127impl<'i> FromCss<'i> for TextDecorationThickness {
128 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
129 if input
130 .try_parse(|input| input.expect_ident_matching("from-font"))
131 .is_ok()
132 {
133 return Ok(Self::FromFont);
134 }
135
136 Ok(Self::Length(Length::from_css(input)?))
137 }
138
139 const VALID_TOKENS: &'static [CssToken] = &[
140 CssToken::Keyword("from-font"),
141 CssToken::Syntax(CssSyntaxKind::Length),
142 ];
143}
144
145impl TailwindPropertyParser for TextDecorationThickness {
146 fn parse_tw(token: &str) -> Option<Self> {
147 if let Ok(number) = token.parse::<f32>() {
148 return Some(Self::Length(Length::Px(number)));
149 }
150
151 Self::from_str(token).ok()
152 }
153}
154
155impl ToCss for TextDecorationLines {
156 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
157 if self.is_empty() {
158 return dest.write_str("none");
159 }
160 let mut first = true;
161 if self.contains(TextDecorationLines::UNDERLINE) {
162 dest.write_str("underline")?;
163 first = false;
164 }
165 if self.contains(TextDecorationLines::LINE_THROUGH) {
166 if !first {
167 dest.write_char(' ')?;
168 }
169 dest.write_str("line-through")?;
170 first = false;
171 }
172 if self.contains(TextDecorationLines::OVERLINE) {
173 if !first {
174 dest.write_char(' ')?;
175 }
176 dest.write_str("overline")?;
177 }
178 Ok(())
179 }
180}
181
182impl ToCss for TextDecorationThickness {
183 fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
184 match self {
185 Self::FromFont => dest.write_str("from-font"),
186 Self::Length(l) => l.to_css(dest),
187 }
188 }
189}
190
191#[derive(Debug, Clone, Copy, PartialEq, Default)]
193#[non_exhaustive]
194pub enum TextDecorationStyle {
195 #[default]
197 Solid,
198}
199
200declare_enum_from_css_impl!(
201 TextDecorationStyle,
202 "solid" => Self::Solid
203);
204
205#[derive(Debug, Default, Clone, PartialEq, TypedBuilder)]
207#[builder(field_defaults(default))]
208#[non_exhaustive]
209pub struct TextDecoration {
210 pub line: TextDecorationLines,
212 pub style: TextDecorationStyle,
214 pub color: ColorInput,
216 pub thickness: TextDecorationThickness,
218}
219
220impl MakeComputed for TextDecoration {
221 fn make_computed(&mut self, sizing: &SizingContext) {
222 self.color.make_computed(sizing);
223 self.thickness.make_computed(sizing);
224 }
225}
226
227impl<'i> FromCss<'i> for TextDecoration {
228 fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
229 let mut line = TextDecorationLines::empty();
230 let mut style = None;
231 let mut color = None;
232 let mut thickness = None;
233
234 loop {
235 if let Ok(value) = input.try_parse(TextDecorationLines::from_css) {
236 line |= value;
237 continue;
238 }
239
240 if let Ok(value) = input.try_parse(TextDecorationStyle::from_css) {
241 style = Some(value);
242 continue;
243 }
244
245 if let Ok(value) = input.try_parse(ColorInput::from_css) {
246 color = Some(value);
247 continue;
248 }
249
250 if let Ok(value) = input.try_parse(TextDecorationThickness::from_css) {
251 thickness = Some(value);
252 continue;
253 }
254
255 if input.is_exhausted() {
256 break;
257 }
258
259 return Err(unexpected_token!(
260 input.current_source_location(),
261 input.next()?,
262 ));
263 }
264
265 Ok(TextDecoration {
266 line,
267 style: style.unwrap_or_default(),
268 color: color.unwrap_or_default(),
269 thickness: thickness.unwrap_or_default(),
270 })
271 }
272
273 const VALID_TOKENS: &'static [CssToken] = &[
274 CssToken::Keyword("underline"),
275 CssToken::Keyword("line-through"),
276 CssToken::Keyword("overline"),
277 CssToken::Keyword("solid"),
278 CssToken::Syntax(CssSyntaxKind::Color),
279 ];
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::style::properties::Color;
286
287 #[test]
288 fn test_parse_text_decoration_underline() {
289 assert_eq!(
290 TextDecoration::from_str("underline"),
291 Ok(
292 TextDecoration::builder()
293 .line(TextDecorationLines::UNDERLINE)
294 .build()
295 )
296 );
297 }
298
299 #[test]
300 fn test_parse_text_decoration_line_through() {
301 assert_eq!(
302 TextDecoration::from_str("line-through"),
303 Ok(
304 TextDecoration::builder()
305 .line(TextDecorationLines::LINE_THROUGH)
306 .build()
307 )
308 );
309 }
310
311 #[test]
312 fn test_parse_text_decoration_underline_solid() {
313 assert_eq!(
314 TextDecoration::from_str("underline solid"),
315 Ok(
316 TextDecoration::builder()
317 .line(TextDecorationLines::UNDERLINE)
318 .style(TextDecorationStyle::Solid)
319 .build()
320 )
321 );
322 }
323
324 #[test]
325 fn test_parse_text_decoration_line_through_solid_red() {
326 assert_eq!(
327 TextDecoration::from_str("line-through solid red"),
328 Ok(
329 TextDecoration::builder()
330 .line(TextDecorationLines::LINE_THROUGH)
331 .style(TextDecorationStyle::Solid)
332 .color(ColorInput::Value(Color([255, 0, 0, 255])))
333 .build()
334 )
335 );
336 }
337
338 #[test]
339 fn test_parse_text_decoration_multiple_lines() {
340 assert_eq!(
341 TextDecoration::from_str("underline line-through solid red"),
342 Ok(
343 TextDecoration::builder()
344 .line(TextDecorationLines::UNDERLINE | TextDecorationLines::LINE_THROUGH)
345 .style(TextDecorationStyle::Solid)
346 .color(ColorInput::Value(Color([255, 0, 0, 255])))
347 .build()
348 )
349 );
350 }
351
352 #[test]
353 fn test_parse_text_decoration_invalid() {
354 let result = TextDecoration::from_str("invalid");
355 assert!(result.is_err());
356 }
357}