Skip to main content

takumi_css/style/properties/
traits.rs

1use cssparser::{ParseError, Parser, ParserInput};
2use std::borrow::Cow;
3use std::fmt;
4use std::sync::Arc;
5
6use crate::style::{Color, SizingContext, math::lcm};
7
8/// Parser result type alias for CSS property parsers.
9pub type ParseResult<'i, T> = Result<T, ParseError<'i, Cow<'i, str>>>;
10
11/// Compact identifiers for frequently reused CSS syntax tokens.
12#[derive(Clone, Copy)]
13#[non_exhaustive]
14pub enum CssSyntaxKind {
15  /// `<angle>`
16  Angle,
17  /// `<border-style>`
18  BorderStyle,
19  /// `<clip>`
20  Clip,
21  /// `<color>`
22  Color,
23  /// `<custom-ident>`
24  CustomIdent,
25  /// `<easing-function>`
26  EasingFunction,
27  /// `<family-name>`
28  FamilyName,
29  /// `<generic-name>`
30  GenericName,
31  /// `<ident>`
32  Ident,
33  /// `<image>`
34  Image,
35  /// `<integer>`
36  Integer,
37  /// `<length>`
38  Length,
39  /// `<line-names>`
40  LineNames,
41  /// `<number>`
42  Number,
43  /// `<percentage>`
44  Percentage,
45  /// `<position>`
46  Position,
47  /// `<repeat>`
48  Repeat,
49  /// `<string>`
50  String,
51  /// `<time>`
52  Time,
53  /// `<track-size>`
54  TrackSize,
55  /// `<transform-function>`
56  TransformFunction,
57}
58
59impl CssSyntaxKind {
60  const fn as_str(self) -> &'static str {
61    match self {
62      Self::Angle => "angle",
63      Self::BorderStyle => "border-style",
64      Self::Clip => "clip",
65      Self::Color => "color",
66      Self::CustomIdent => "custom-ident",
67      Self::EasingFunction => "easing-function",
68      Self::FamilyName => "family-name",
69      Self::GenericName => "generic-name",
70      Self::Ident => "ident",
71      Self::Image => "image",
72      Self::Integer => "integer",
73      Self::Length => "length",
74      Self::LineNames => "line-names",
75      Self::Number => "number",
76      Self::Percentage => "percentage",
77      Self::Position => "position",
78      Self::Repeat => "repeat",
79      Self::String => "string",
80      Self::Time => "time",
81      Self::TrackSize => "track-size",
82      Self::TransformFunction => "transform-function",
83    }
84  }
85}
86
87/// Compact identifiers for reusable CSS descriptor and function labels.
88#[derive(Clone, Copy)]
89pub enum CssDescriptorKind {
90  /// `<blur()>`
91  BlurFn,
92  /// `<blend-mode>`
93  BlendMode,
94  /// `<brightness()>`
95  BrightnessFn,
96  /// `<circle()>`
97  CircleFn,
98  /// `<color and percentage>`
99  ColorAndPercentage,
100  /// `<color-mix()>`
101  ColorMixFn,
102  /// `<conic-gradient()>`
103  ConicGradientFn,
104  /// `<repeating-conic-gradient()>`
105  RepeatingConicGradientFn,
106  /// `<contrast()>`
107  ContrastFn,
108  /// `<cubic-bezier()>`
109  CubicBezierFn,
110  /// `<drop-shadow()>`
111  DropShadowFn,
112  /// `<ellipse()>`
113  EllipseFn,
114  /// `<grayscale()>`
115  GrayscaleFn,
116  /// `<hue-rotate()>`
117  HueRotateFn,
118  /// `<in <color-space>>`
119  InColorSpace,
120  /// `<inset()>`
121  InsetFn,
122  /// `<invert()>`
123  InvertFn,
124  /// `<linear-gradient()>`
125  LinearGradientFn,
126  /// `<repeating-linear-gradient()>`
127  RepeatingLinearGradientFn,
128  /// `<minmax()>`
129  MinmaxFn,
130  /// `<opacity()>`
131  OpacityFn,
132  /// `<path()>`
133  PathFn,
134  /// `<polygon()>`
135  PolygonFn,
136  /// `<radial-gradient()>`
137  RadialGradientFn,
138  /// `<repeating-radial-gradient()>`
139  RepeatingRadialGradientFn,
140  /// `<repeat()>`
141  RepeatFn,
142  /// `<saturate()>`
143  SaturateFn,
144  /// `<sepia()>`
145  SepiaFn,
146  /// `<steps()>`
147  StepsFn,
148  /// `<text-wrap-mode>`
149  TextWrapMode,
150  /// `<text-wrap-style>`
151  TextWrapStyle,
152  /// `<url()>`
153  UrlFn,
154  /// `<white-space-collapse>`
155  WhiteSpaceCollapse,
156}
157
158impl CssDescriptorKind {
159  const fn as_str(self) -> &'static str {
160    match self {
161      Self::BlurFn => "blur()",
162      Self::BlendMode => "blend-mode",
163      Self::BrightnessFn => "brightness()",
164      Self::CircleFn => "circle()",
165      Self::ColorAndPercentage => "color and percentage",
166      Self::ColorMixFn => "color-mix()",
167      Self::ConicGradientFn => "conic-gradient()",
168      Self::RepeatingConicGradientFn => "repeating-conic-gradient()",
169      Self::ContrastFn => "contrast()",
170      Self::CubicBezierFn => "cubic-bezier()",
171      Self::DropShadowFn => "drop-shadow()",
172      Self::EllipseFn => "ellipse()",
173      Self::GrayscaleFn => "grayscale()",
174      Self::HueRotateFn => "hue-rotate()",
175      Self::InColorSpace => "in <color-space>",
176      Self::InsetFn => "inset()",
177      Self::InvertFn => "invert()",
178      Self::LinearGradientFn => "linear-gradient()",
179      Self::RepeatingLinearGradientFn => "repeating-linear-gradient()",
180      Self::MinmaxFn => "minmax()",
181      Self::OpacityFn => "opacity()",
182      Self::PathFn => "path()",
183      Self::PolygonFn => "polygon()",
184      Self::RadialGradientFn => "radial-gradient()",
185      Self::RepeatingRadialGradientFn => "repeating-radial-gradient()",
186      Self::RepeatFn => "repeat()",
187      Self::SaturateFn => "saturate()",
188      Self::SepiaFn => "sepia()",
189      Self::StepsFn => "steps()",
190      Self::TextWrapMode => "text-wrap-mode",
191      Self::TextWrapStyle => "text-wrap-style",
192      Self::UrlFn => "url()",
193      Self::WhiteSpaceCollapse => "white-space-collapse",
194    }
195  }
196}
197
198/// Enum representing CSS tokens.
199#[non_exhaustive]
200pub enum CssToken {
201  /// A CSS keyword.
202  Keyword(&'static str),
203  /// A common CSS syntax token backed by a compact enum table.
204  Syntax(CssSyntaxKind),
205  /// A reusable CSS descriptor backed by a compact enum table.
206  Descriptor(CssDescriptorKind),
207}
208
209impl std::fmt::Display for CssToken {
210  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211    match self {
212      CssToken::Keyword(keyword) => write!(f, "'{}'", keyword),
213      CssToken::Syntax(token) => write!(f, "<{}>", token.as_str()),
214      CssToken::Descriptor(token) => write!(f, "<{}>", token.as_str()),
215    }
216  }
217}
218
219/// Defines reusable message templates for CSS parse errors.
220#[non_exhaustive]
221pub enum CssExpectedMessage {
222  /// Expects a value or the `none` keyword.
223  ValueOrNone,
224  /// Expects exactly one value.
225  OneValue,
226  /// Expects one or two values.
227  OneOrTwoValues,
228  /// Expects one to four values.
229  OneToFourValues,
230  /// Expects the border-radius shorthand grammar.
231  BorderRadius,
232}
233
234impl CssExpectedMessage {
235  pub fn build_message(&self, token: &str, valid_tokens: String) -> String {
236    match self {
237      Self::ValueOrNone => {
238        format!("Unexpected token: {token}, expected a value of {valid_tokens} or 'none'")
239      }
240      Self::OneValue => format!("Unexpected token: {token}, expected a value of {valid_tokens}"),
241      Self::OneOrTwoValues => {
242        format!("Unexpected token: {token}, expected 1 ~ 2 values of {valid_tokens}")
243      }
244      Self::OneToFourValues => {
245        format!("Unexpected token: {token}, expected 1 ~ 4 values of {valid_tokens}")
246      }
247      Self::BorderRadius => format!(
248        "Unexpected token: {token}, expected 1 to 4 length values for width, optionally followed by '/' and 1 to 4 length values for height"
249      ),
250    }
251  }
252}
253
254/// Trait for types that can be parsed from CSS.
255pub trait FromCss<'i> {
256  /// Parses the type from a [`Parser`] instance.
257  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self>
258  where
259    Self: Sized;
260
261  /// Helper function to parse the type from a string.
262  fn from_str(source: &'i str) -> ParseResult<'i, Self>
263  where
264    Self: Sized,
265  {
266    let mut input = ParserInput::new(source);
267    let mut parser = Parser::new(&mut input);
268
269    Self::from_css(&mut parser)
270  }
271
272  /// Returns the list of valid CSS tokens for this type.
273  const VALID_TOKENS: &'static [CssToken];
274
275  /// Message template used when building parse errors for this type.
276  const EXPECT_MESSAGE: CssExpectedMessage = CssExpectedMessage::OneValue;
277}
278
279impl<'i, T: FromCss<'i>> FromCss<'i> for Option<T> {
280  // 'none' is intentionally omitted and applied in `expect_message`
281  const VALID_TOKENS: &'static [CssToken] = T::VALID_TOKENS;
282
283  const EXPECT_MESSAGE: CssExpectedMessage = CssExpectedMessage::ValueOrNone;
284
285  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
286    if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
287      return Ok(None);
288    }
289
290    T::from_css(input).map(Some)
291  }
292}
293
294impl<'i> FromCss<'i> for String {
295  const VALID_TOKENS: &'static [CssToken] = &[
296    CssToken::Syntax(CssSyntaxKind::String),
297    CssToken::Syntax(CssSyntaxKind::CustomIdent),
298  ];
299
300  fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
301    Ok(input.expect_ident_or_string()?.to_string())
302  }
303}
304
305/// Converts a parsed/inherited value into a computed value for the current node context.
306pub(crate) trait MakeComputed {
307  /// Default no-op for types that do not need computed-value normalization.
308  fn make_computed(&mut self, _sizing: &SizingContext) {}
309}
310
311pub(crate) trait Animatable: Sized + Clone {
312  fn interpolate(
313    &mut self,
314    from: &Self,
315    to: &Self,
316    progress: f32,
317    _sizing: &SizingContext,
318    _current_color: Color,
319  ) {
320    *self = if progress >= 0.5 {
321      to.clone()
322    } else {
323      from.clone()
324    };
325  }
326
327  fn list_interpolation_strategy() -> ListInterpolationStrategy {
328    ListInterpolationStrategy::Discrete
329  }
330
331  fn neutral_value_like(_other: &Self) -> Option<Self> {
332    None
333  }
334
335  fn missing_value() -> Option<Self> {
336    None
337  }
338}
339
340pub(crate) enum ListInterpolationStrategy {
341  Discrete,
342  RepeatToLcm,
343  PadToLongestWithNeutral,
344}
345
346impl<T: MakeComputed> MakeComputed for Option<T> {
347  fn make_computed(&mut self, sizing: &SizingContext) {
348    if let Some(value) = self.as_mut() {
349      value.make_computed(sizing);
350    }
351  }
352}
353
354impl<T: MakeComputed> MakeComputed for Box<[T]> {
355  fn make_computed(&mut self, sizing: &SizingContext) {
356    for value in self.iter_mut() {
357      value.make_computed(sizing);
358    }
359  }
360}
361
362impl<T: MakeComputed> MakeComputed for Vec<T> {
363  fn make_computed(&mut self, sizing: &SizingContext) {
364    for value in self.iter_mut() {
365      value.make_computed(sizing);
366    }
367  }
368}
369
370impl<T: Animatable + Clone> Animatable for Option<T> {
371  fn interpolate(
372    &mut self,
373    from: &Self,
374    to: &Self,
375    progress: f32,
376    sizing: &SizingContext,
377    current_color: Color,
378  ) {
379    *self = match (from, to) {
380      (Some(from), Some(to)) => {
381        let mut value = from.clone();
382        value.interpolate(from, to, progress, sizing, current_color);
383        Some(value)
384      }
385      (Some(from), None) => T::missing_value().map_or_else(
386        || {
387          if progress >= 0.5 {
388            None
389          } else {
390            Some(from.clone())
391          }
392        },
393        |missing| {
394          let mut value = from.clone();
395          value.interpolate(from, &missing, progress, sizing, current_color);
396          Some(value)
397        },
398      ),
399      (None, Some(to)) => T::missing_value().map_or_else(
400        || {
401          if progress >= 0.5 {
402            Some(to.clone())
403          } else {
404            None
405          }
406        },
407        |missing| {
408          let mut value = missing.clone();
409          value.interpolate(&missing, to, progress, sizing, current_color);
410          Some(value)
411        },
412      ),
413      (None, None) => None,
414    };
415  }
416}
417
418impl<T: Animatable + Clone> Animatable for Box<[T]> {
419  fn missing_value() -> Option<Self> {
420    match T::list_interpolation_strategy() {
421      ListInterpolationStrategy::Discrete => None,
422      ListInterpolationStrategy::RepeatToLcm
423      | ListInterpolationStrategy::PadToLongestWithNeutral => Some(Box::default()),
424    }
425  }
426
427  fn interpolate(
428    &mut self,
429    from: &Self,
430    to: &Self,
431    progress: f32,
432    sizing: &SizingContext,
433    current_color: Color,
434  ) {
435    *self = interpolate_list(
436      from,
437      to,
438      progress,
439      sizing,
440      current_color,
441      Vec::into_boxed_slice,
442    )
443    .unwrap_or_else(|| {
444      if progress >= 0.5 {
445        to.clone()
446      } else {
447        from.clone()
448      }
449    });
450  }
451}
452
453impl<T: Animatable + Clone> Animatable for Vec<T> {
454  fn missing_value() -> Option<Self> {
455    match T::list_interpolation_strategy() {
456      ListInterpolationStrategy::Discrete => None,
457      ListInterpolationStrategy::RepeatToLcm
458      | ListInterpolationStrategy::PadToLongestWithNeutral => Some(Vec::new()),
459    }
460  }
461
462  fn interpolate(
463    &mut self,
464    from: &Self,
465    to: &Self,
466    progress: f32,
467    sizing: &SizingContext,
468    current_color: Color,
469  ) {
470    *self = interpolate_list(from, to, progress, sizing, current_color, |values| values)
471      .unwrap_or_else(|| {
472        if progress >= 0.5 {
473          to.clone()
474        } else {
475          from.clone()
476        }
477      });
478  }
479}
480
481fn interpolate_list<T: Animatable + Clone, C: AsRef<[T]>, O>(
482  from: &C,
483  to: &C,
484  progress: f32,
485  sizing: &SizingContext,
486  current_color: Color,
487  build: impl FnOnce(Vec<T>) -> O,
488) -> Option<O> {
489  let from = from.as_ref();
490  let to = to.as_ref();
491
492  let values = match T::list_interpolation_strategy() {
493    ListInterpolationStrategy::Discrete => {
494      if from.len() != to.len() {
495        return None;
496      }
497      interpolate_pairwise_list(from, to, from.len(), progress, sizing, current_color)
498    }
499    ListInterpolationStrategy::RepeatToLcm => {
500      if from.is_empty() || to.is_empty() {
501        return None;
502      }
503      interpolate_pairwise_list(
504        from,
505        to,
506        lcm(from.len(), to.len()),
507        progress,
508        sizing,
509        current_color,
510      )
511    }
512    ListInterpolationStrategy::PadToLongestWithNeutral => {
513      interpolate_neutral_padded_list(from, to, progress, sizing, current_color)?
514    }
515  };
516
517  Some(build(values))
518}
519
520fn interpolate_pairwise_list<T: Animatable + Clone>(
521  from: &[T],
522  to: &[T],
523  output_len: usize,
524  progress: f32,
525  sizing: &SizingContext,
526  current_color: Color,
527) -> Vec<T> {
528  (0..output_len)
529    .map(|index| {
530      let from_value = &from[index % from.len()];
531      let to_value = &to[index % to.len()];
532      let mut value = from_value.clone();
533      value.interpolate(from_value, to_value, progress, sizing, current_color);
534      value
535    })
536    .collect()
537}
538
539fn interpolate_neutral_padded_list<T: Animatable + Clone>(
540  from: &[T],
541  to: &[T],
542  progress: f32,
543  sizing: &SizingContext,
544  current_color: Color,
545) -> Option<Vec<T>> {
546  let output_len = from.len().max(to.len());
547
548  (0..output_len)
549    .map(|index| {
550      let from_value = if index < from.len() {
551        from.get(index).cloned()
552      } else {
553        to.get(index).and_then(T::neutral_value_like)
554      }?;
555      let to_value = if index < to.len() {
556        to.get(index).cloned()
557      } else {
558        from.get(index).and_then(T::neutral_value_like)
559      }?;
560
561      let mut value = from_value.clone();
562      value.interpolate(&from_value, &to_value, progress, sizing, current_color);
563      Some(value)
564    })
565    .collect()
566}
567
568/// Serialize a style value to its CSS string representation.
569pub trait ToCss {
570  /// Write the CSS representation of this value into `dest`.
571  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result;
572}
573
574impl<T: ToCss + ?Sized> ToCss for &T {
575  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
576    (*self).to_css(dest)
577  }
578}
579
580impl<T: ToCss> ToCss for Option<T> {
581  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
582    match self {
583      Some(v) => v.to_css(dest),
584      None => dest.write_str("none"),
585    }
586  }
587}
588
589impl<T: ToCss> ToCss for Box<[T]> {
590  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
591    for (i, item) in self.iter().enumerate() {
592      if i > 0 {
593        dest.write_str(", ")?;
594      }
595      item.to_css(dest)?;
596    }
597    Ok(())
598  }
599}
600
601impl<T: ToCss> ToCss for Vec<T> {
602  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
603    for (i, item) in self.iter().enumerate() {
604      if i > 0 {
605        dest.write_str(", ")?;
606      }
607      item.to_css(dest)?;
608    }
609    Ok(())
610  }
611}
612
613impl ToCss for f32 {
614  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
615    write!(dest, "{}", self)
616  }
617}
618
619impl ToCss for u32 {
620  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
621    write!(dest, "{}", self)
622  }
623}
624
625impl ToCss for i32 {
626  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
627    write!(dest, "{}", self)
628  }
629}
630
631impl ToCss for String {
632  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
633    dest.write_str(self)
634  }
635}
636
637impl ToCss for Arc<str> {
638  fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
639    dest.write_str(self)
640  }
641}
642
643/// Macro to implement From trait for Taffy enum conversions.
644macro_rules! impl_from_taffy_enum {
645  ($from_ty:ty, $to_ty:ty, $($variant:ident),*) => {
646    impl From<$from_ty> for $to_ty {
647      fn from(value: $from_ty) -> Self {
648        match value {
649          $(<$from_ty>::$variant => <$to_ty>::$variant,)*
650        }
651      }
652    }
653  };
654}
655
656pub(crate) use impl_from_taffy_enum;
657
658/// Declares a CSS enum parser with automatic value list generation.
659macro_rules! declare_enum_from_css_impl {
660  (
661    $enum_type:ty,
662    $($css_value:expr => $variant:path),* $(,)?
663  ) => {
664    impl crate::style::MakeComputed for $enum_type {}
665
666    impl<'i> crate::style::FromCss<'i> for $enum_type {
667      const VALID_TOKENS: &'static [crate::style::CssToken] =
668        &[$(crate::style::CssToken::Keyword($css_value)),*];
669
670      fn from_css(input: &mut cssparser::Parser<'i, '_>) -> crate::style::ParseResult<'i, Self> {
671        let location = input.current_source_location();
672        let token = input.next()?;
673
674        let cssparser::Token::Ident(ident) = token else {
675          return Err($crate::style::unexpected_token!(location, token));
676        };
677
678        cssparser::match_ignore_ascii_case! {&ident,
679          $(
680            $css_value => Ok($variant),
681          )*
682          _ => Err($crate::style::unexpected_token!(location, token)),
683        }
684      }
685    }
686
687    impl crate::style::properties::ToCss for $enum_type {
688      fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
689        match self {
690          $(
691            $variant => dest.write_str($css_value),
692          )*
693        }
694      }
695    }
696  };
697}
698
699pub(crate) use declare_enum_from_css_impl;
700
701/// Declares a box-alignment enum parser that accepts the optional `safe`/`unsafe`
702/// overflow-position prefix on its positional keywords.
703macro_rules! declare_box_alignment_enum_impl {
704  (
705    $enum_type:ty,
706    safe { $($safe_css:literal => $base_variant:ident / $safe_variant:ident),+ $(,)? },
707    plain { $($plain_css:literal => $plain_variant:ident),* $(,)? }
708  ) => {
709    impl crate::style::MakeComputed for $enum_type {}
710
711    impl<'i> crate::style::FromCss<'i> for $enum_type {
712      const VALID_TOKENS: &'static [crate::style::CssToken] = &[
713        $(crate::style::CssToken::Keyword($plain_css),)*
714        $(crate::style::CssToken::Keyword($safe_css),)*
715        crate::style::CssToken::Keyword("safe"),
716        crate::style::CssToken::Keyword("unsafe"),
717      ];
718
719      fn from_css(input: &mut cssparser::Parser<'i, '_>) -> crate::style::ParseResult<'i, Self> {
720        let mut safe = false;
721
722        loop {
723          let location = input.current_source_location();
724          let token = input.next()?;
725
726          let cssparser::Token::Ident(ident) = token else {
727            return Err($crate::style::unexpected_token!(location, token));
728          };
729
730          cssparser::match_ignore_ascii_case! {&ident,
731            "safe" => safe = true,
732            "unsafe" => safe = false,
733            $($safe_css => return Ok(if safe { Self::$safe_variant } else { Self::$base_variant }),)*
734            $($plain_css => return if safe {
735              Err($crate::style::unexpected_token!(location, token))
736            } else {
737              Ok(Self::$plain_variant)
738            },)*
739            _ => return Err($crate::style::unexpected_token!(location, token)),
740          }
741        }
742      }
743    }
744
745    impl crate::style::properties::ToCss for $enum_type {
746      fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
747        match self {
748          $(Self::$plain_variant => dest.write_str($plain_css),)*
749          $(Self::$base_variant => dest.write_str($safe_css),)*
750          $(Self::$safe_variant => {
751            dest.write_str("safe ")?;
752            dest.write_str($safe_css)
753          })*
754        }
755      }
756    }
757  };
758}
759
760pub(crate) use declare_box_alignment_enum_impl;