Skip to main content

takumi_css/style/properties/
text_decoration.rs

1use 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  /// Represents a collection of text decoration lines.
15  #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
16  #[non_exhaustive]
17  pub struct TextDecorationLines: u8 {
18    /// Underline text decoration.
19    const UNDERLINE = 0b001;
20    /// Line-through text decoration.
21    const LINE_THROUGH = 0b010;
22    /// Overline text decoration.
23    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    // Parse at least one line decoration
32    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    // Parse additional decorations if present
42    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/// Represents text decoration thickness options.
72#[derive(Debug, Clone, Copy, PartialEq)]
73#[non_exhaustive]
74pub enum TextDecorationThickness {
75  /// Use the font's default thickness, fallback to `auto` if not available.
76  FromFont,
77  /// Use a specific length.
78  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/// Represents text decoration style options (currently only solid is supported).
192#[derive(Debug, Clone, Copy, PartialEq, Default)]
193#[non_exhaustive]
194pub enum TextDecorationStyle {
195  /// Solid text decoration style.
196  #[default]
197  Solid,
198}
199
200declare_enum_from_css_impl!(
201  TextDecorationStyle,
202  "solid" => Self::Solid
203);
204
205/// Parsed `text-decoration` value.
206#[derive(Debug, Default, Clone, PartialEq, TypedBuilder)]
207#[builder(field_defaults(default))]
208#[non_exhaustive]
209pub struct TextDecoration {
210  /// Text decoration line style.
211  pub line: TextDecorationLines,
212  /// Text decoration style (currently only solid is supported).
213  pub style: TextDecorationStyle,
214  /// Optional text decoration color.
215  pub color: ColorInput,
216  /// Optional text decoration thickness.
217  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}