takumi_css/style/properties/
text_indent.rs1use 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#[derive(Debug, Clone, Copy, PartialEq, Default)]
12#[non_exhaustive]
13pub struct TextIndent {
14 pub amount: LengthDefaultsToZero,
16 pub each_line: bool,
18 pub hanging: bool,
20}
21
22impl TextIndent {
23 pub const fn new(amount: LengthDefaultsToZero) -> Self {
25 Self {
26 amount,
27 each_line: false,
28 hanging: false,
29 }
30 }
31
32 pub const fn with_each_line(mut self, each_line: bool) -> Self {
34 self.each_line = each_line;
35 self
36 }
37
38 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}