Skip to main content

takumi_css/style/properties/
text_fit.rs

1use std::fmt;
2
3use cssparser::{Parser, Token};
4use typed_builder::TypedBuilder;
5
6use crate::style::{
7  Animatable, CssSyntaxKind, CssToken, FromCss, MakeComputed, ParseResult, ToCss,
8  declare_enum_from_css_impl, unexpected_token,
9};
10
11/// Controls whether inline contents are scaled to fit their line box.
12#[derive(Debug, Clone, Copy, PartialEq, Default, TypedBuilder)]
13#[builder(field_defaults(default))]
14#[non_exhaustive]
15pub struct TextFit {
16  /// Selects whether fitting grows, shrinks, or leaves text unchanged.
17  pub mode: TextFitMode,
18  /// Selects whether fitting uses one scale or per-line scales.
19  pub target: TextFitTarget,
20  /// Optional scale clamp as a multiplier, parsed from a CSS percentage.
21  pub limit: Option<f32>,
22}
23
24impl MakeComputed for TextFit {}
25impl Animatable for TextFit {}
26
27impl<'i> FromCss<'i> for TextFit {
28  // Syntax: [ none | grow | shrink ] [ consistent | per-line | per-line-all ]? <percentage>?
29  // The type keyword is mandatory and must appear first, matching the spec and Chromium.
30  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
31    let mode = TextFitMode::from_css(input)?;
32
33    let target = input.try_parse(TextFitTarget::from_css).unwrap_or_default();
34
35    let limit: Option<f32> = input
36      .try_parse(|input| -> ParseResult<'i, f32> {
37        let location = input.current_source_location();
38        match input.next()? {
39          Token::Percentage { unit_value, .. } => Ok(unit_value.max(0.0)),
40          token => Err(unexpected_token!(location, token)),
41        }
42      })
43      .ok();
44
45    // Reject trailing tokens (e.g. duplicate target or two percentages).
46    if !input.is_exhausted() {
47      return Err(input.new_error_for_next_token());
48    }
49
50    Ok(Self {
51      mode,
52      target,
53      limit,
54    })
55  }
56
57  const VALID_TOKENS: &'static [CssToken] = &[
58    CssToken::Keyword("none"),
59    CssToken::Keyword("grow"),
60    CssToken::Keyword("shrink"),
61    CssToken::Keyword("consistent"),
62    CssToken::Keyword("per-line"),
63    CssToken::Keyword("per-line-all"),
64    CssToken::Syntax(CssSyntaxKind::Percentage),
65  ];
66}
67
68/// Text fitting direction.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
70pub enum TextFitMode {
71  /// Do not scale inline contents.
72  #[default]
73  None,
74  /// Scale inline contents up to fit.
75  Grow,
76  /// Scale inline contents down to fit.
77  Shrink,
78}
79
80declare_enum_from_css_impl!(
81  TextFitMode,
82  "none" => TextFitMode::None,
83  "grow" => TextFitMode::Grow,
84  "shrink" => TextFitMode::Shrink,
85);
86
87/// Text fitting scale target.
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
89#[non_exhaustive]
90pub enum TextFitTarget {
91  /// Use one scale for all lines.
92  #[default]
93  Consistent,
94  /// Use a separate scale for each line except the last.
95  PerLine,
96  /// Use a separate scale for each line including the last.
97  PerLineAll,
98}
99
100declare_enum_from_css_impl!(
101  TextFitTarget,
102  "consistent" => TextFitTarget::Consistent,
103  "per-line" => TextFitTarget::PerLine,
104  "per-line-all" => TextFitTarget::PerLineAll,
105);
106
107impl ToCss for TextFit {
108  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
109    self.mode.to_css(dest)?;
110    if self.target != TextFitTarget::Consistent {
111      dest.write_char(' ')?;
112      self.target.to_css(dest)?;
113    }
114    if let Some(limit) = self.limit {
115      dest.write_char(' ')?;
116      write!(dest, "{}%", limit * 100.0)?;
117    }
118    Ok(())
119  }
120}