Skip to main content

takumi_css/style/properties/
text_indent.rs

1use std::fmt;
2
3use cssparser::{Parser, Token, match_ignore_ascii_case};
4
5use crate::style::{
6  Animatable, Color, CssSyntaxKind, CssToken, FromCss, LengthDefaultsToZero, MakeComputed,
7  ParseResult, SizingContext, ToCss, unexpected_token,
8};
9
10/// Controls indentation of the first line, or hanging/each-line variants.
11#[derive(Debug, Clone, Copy, PartialEq, Default)]
12#[non_exhaustive]
13pub struct TextIndent {
14  /// The indent amount.
15  pub amount: LengthDefaultsToZero,
16  /// Apply the indent after every hard line break.
17  pub each_line: bool,
18  /// Indent continuation lines instead of the first line.
19  pub hanging: bool,
20}
21
22impl TextIndent {
23  /// Creates a text indent with the given amount and default keyword options.
24  pub const fn new(amount: LengthDefaultsToZero) -> Self {
25    Self {
26      amount,
27      each_line: false,
28      hanging: false,
29    }
30  }
31
32  /// Sets whether the indent also applies after explicit line breaks.
33  pub const fn with_each_line(mut self, each_line: bool) -> Self {
34    self.each_line = each_line;
35    self
36  }
37
38  /// Sets whether continuation lines are indented instead of the first line.
39  pub const fn with_hanging(mut self, hanging: bool) -> Self {
40    self.hanging = hanging;
41    self
42  }
43
44  pub fn resolve_px(self, sizing: &SizingContext, line_width: f32) -> f32 {
45    self.amount.to_px(sizing, line_width)
46  }
47}
48
49impl MakeComputed for TextIndent {}
50
51impl Animatable for TextIndent {
52  fn interpolate(
53    &mut self,
54    from: &Self,
55    to: &Self,
56    progress: f32,
57    sizing: &SizingContext,
58    current_color: Color,
59  ) {
60    self
61      .amount
62      .interpolate(&from.amount, &to.amount, progress, sizing, current_color);
63    self.each_line = if progress >= 0.5 {
64      to.each_line
65    } else {
66      from.each_line
67    };
68    self.hanging = if progress >= 0.5 {
69      to.hanging
70    } else {
71      from.hanging
72    };
73  }
74}
75
76impl<'i> FromCss<'i> for TextIndent {
77  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
78    let mut amount = None;
79    let mut each_line = false;
80    let mut hanging = false;
81
82    while !input.is_exhausted() {
83      if amount.is_none()
84        && let Ok(length) = input.try_parse(LengthDefaultsToZero::from_css)
85      {
86        amount = Some(length);
87        continue;
88      }
89
90      let location = input.current_source_location();
91      match input.next()? {
92        Token::Ident(keyword) => match_ignore_ascii_case! {keyword.as_ref(),
93          "each-line" if !each_line => each_line = true,
94          "hanging" if !hanging => hanging = true,
95          _ => return Err(unexpected_token!(location, &Token::Ident(keyword.clone()))),
96        },
97        token => return Err(unexpected_token!(location, token)),
98      }
99    }
100
101    Ok(Self {
102      amount: amount.unwrap_or_default(),
103      each_line,
104      hanging,
105    })
106  }
107
108  const VALID_TOKENS: &'static [CssToken] = &[
109    CssToken::Syntax(CssSyntaxKind::Length),
110    CssToken::Keyword("each-line"),
111    CssToken::Keyword("hanging"),
112  ];
113}
114
115impl ToCss for TextIndent {
116  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
117    self.amount.to_css(dest)?;
118    if self.each_line {
119      dest.write_str(" each-line")?;
120    }
121    if self.hanging {
122      dest.write_str(" hanging")?;
123    }
124    Ok(())
125  }
126}
127
128#[cfg(test)]
129mod tests {
130  use crate::style::{FromCss, LengthDefaultsToZero, TextIndent};
131
132  #[test]
133  fn parses_indent_keywords_in_any_order() {
134    assert_eq!(
135      TextIndent::from_str("hanging 2em each-line"),
136      Ok(TextIndent {
137        amount: LengthDefaultsToZero::Em(2.0),
138        each_line: true,
139        hanging: true,
140      })
141    );
142  }
143
144  #[test]
145  fn defaults_to_zero_indent() {
146    assert_eq!(TextIndent::default().amount, LengthDefaultsToZero::Px(0.0));
147  }
148}