use cssparser::{ParseError, Parser, ParserInput};
use std::{borrow::Cow, fmt, sync::Arc};
use crate::style::{Color, SizingContext, math::lcm};
pub type ParseResult<'i, T> = Result<T, ParseError<'i, Cow<'i, str>>>;
#[derive(Clone, Copy)]
#[non_exhaustive]
pub enum CssSyntaxKind {
Angle,
BorderStyle,
Clip,
Color,
CustomIdent,
EasingFunction,
FamilyName,
GenericName,
Ident,
Image,
Integer,
Length,
LineNames,
Number,
Percentage,
Position,
Repeat,
String,
Time,
TrackSize,
TransformFunction,
}
impl CssSyntaxKind {
const fn as_str(self) -> &'static str {
match self {
Self::Angle => "angle",
Self::BorderStyle => "border-style",
Self::Clip => "clip",
Self::Color => "color",
Self::CustomIdent => "custom-ident",
Self::EasingFunction => "easing-function",
Self::FamilyName => "family-name",
Self::GenericName => "generic-name",
Self::Ident => "ident",
Self::Image => "image",
Self::Integer => "integer",
Self::Length => "length",
Self::LineNames => "line-names",
Self::Number => "number",
Self::Percentage => "percentage",
Self::Position => "position",
Self::Repeat => "repeat",
Self::String => "string",
Self::Time => "time",
Self::TrackSize => "track-size",
Self::TransformFunction => "transform-function",
}
}
}
#[derive(Clone, Copy)]
pub enum CssDescriptorKind {
BlurFn,
BlendMode,
BrightnessFn,
CircleFn,
ColorAndPercentage,
ColorMixFn,
ConicGradientFn,
RepeatingConicGradientFn,
ContrastFn,
CubicBezierFn,
DropShadowFn,
EllipseFn,
GrayscaleFn,
HueRotateFn,
InColorSpace,
InsetFn,
InvertFn,
LinearGradientFn,
RepeatingLinearGradientFn,
MinmaxFn,
OpacityFn,
PathFn,
PolygonFn,
RadialGradientFn,
RepeatingRadialGradientFn,
RepeatFn,
SaturateFn,
SepiaFn,
StepsFn,
TextWrapMode,
TextWrapStyle,
UrlFn,
WhiteSpaceCollapse,
}
impl CssDescriptorKind {
const fn as_str(self) -> &'static str {
match self {
Self::BlurFn => "blur()",
Self::BlendMode => "blend-mode",
Self::BrightnessFn => "brightness()",
Self::CircleFn => "circle()",
Self::ColorAndPercentage => "color and percentage",
Self::ColorMixFn => "color-mix()",
Self::ConicGradientFn => "conic-gradient()",
Self::RepeatingConicGradientFn => "repeating-conic-gradient()",
Self::ContrastFn => "contrast()",
Self::CubicBezierFn => "cubic-bezier()",
Self::DropShadowFn => "drop-shadow()",
Self::EllipseFn => "ellipse()",
Self::GrayscaleFn => "grayscale()",
Self::HueRotateFn => "hue-rotate()",
Self::InColorSpace => "in <color-space>",
Self::InsetFn => "inset()",
Self::InvertFn => "invert()",
Self::LinearGradientFn => "linear-gradient()",
Self::RepeatingLinearGradientFn => "repeating-linear-gradient()",
Self::MinmaxFn => "minmax()",
Self::OpacityFn => "opacity()",
Self::PathFn => "path()",
Self::PolygonFn => "polygon()",
Self::RadialGradientFn => "radial-gradient()",
Self::RepeatingRadialGradientFn => "repeating-radial-gradient()",
Self::RepeatFn => "repeat()",
Self::SaturateFn => "saturate()",
Self::SepiaFn => "sepia()",
Self::StepsFn => "steps()",
Self::TextWrapMode => "text-wrap-mode",
Self::TextWrapStyle => "text-wrap-style",
Self::UrlFn => "url()",
Self::WhiteSpaceCollapse => "white-space-collapse",
}
}
}
#[non_exhaustive]
pub enum CssToken {
Keyword(&'static str),
Syntax(CssSyntaxKind),
Descriptor(CssDescriptorKind),
}
impl std::fmt::Display for CssToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CssToken::Keyword(keyword) => write!(f, "'{}'", keyword),
CssToken::Syntax(token) => write!(f, "<{}>", token.as_str()),
CssToken::Descriptor(token) => write!(f, "<{}>", token.as_str()),
}
}
}
#[non_exhaustive]
pub enum CssExpectedMessage {
ValueOrNone,
OneValue,
OneOrTwoValues,
OneToFourValues,
BorderRadius,
}
impl CssExpectedMessage {
pub fn build_message(&self, token: &str, valid_tokens: String) -> String {
match self {
Self::ValueOrNone => {
format!("Unexpected token: {token}, expected a value of {valid_tokens} or 'none'")
}
Self::OneValue => format!("Unexpected token: {token}, expected a value of {valid_tokens}"),
Self::OneOrTwoValues => {
format!("Unexpected token: {token}, expected 1 ~ 2 values of {valid_tokens}")
}
Self::OneToFourValues => {
format!("Unexpected token: {token}, expected 1 ~ 4 values of {valid_tokens}")
}
Self::BorderRadius => format!(
"Unexpected token: {token}, expected 1 to 4 length values for width, optionally followed by '/' and 1 to 4 length values for height"
),
}
}
}
pub trait FromCss<'i> {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self>
where
Self: Sized;
fn from_str(source: &'i str) -> ParseResult<'i, Self>
where
Self: Sized,
{
let mut input = ParserInput::new(source);
let mut parser = Parser::new(&mut input);
Self::from_css(&mut parser)
}
const VALID_TOKENS: &'static [CssToken];
const EXPECT_MESSAGE: CssExpectedMessage = CssExpectedMessage::OneValue;
}
impl<'i, T: FromCss<'i>> FromCss<'i> for Option<T> {
const VALID_TOKENS: &'static [CssToken] = T::VALID_TOKENS;
const EXPECT_MESSAGE: CssExpectedMessage = CssExpectedMessage::ValueOrNone;
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
if input.try_parse(|i| i.expect_ident_matching("none")).is_ok() {
return Ok(None);
}
T::from_css(input).map(Some)
}
}
impl<'i> FromCss<'i> for String {
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Syntax(CssSyntaxKind::String),
CssToken::Syntax(CssSyntaxKind::CustomIdent),
];
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
Ok(input.expect_ident_or_string()?.to_string())
}
}
pub(crate) trait MakeComputed {
fn make_computed(&mut self, _sizing: &SizingContext) {}
}
pub(crate) trait Animatable: Sized + Clone {
fn interpolate(
&mut self,
from: &Self,
to: &Self,
progress: f32,
_sizing: &SizingContext,
_current_color: Color,
) {
*self = if progress >= 0.5 {
to.clone()
} else {
from.clone()
};
}
fn list_interpolation_strategy() -> ListInterpolationStrategy {
ListInterpolationStrategy::Discrete
}
fn neutral_value_like(_other: &Self) -> Option<Self> {
None
}
fn missing_value() -> Option<Self> {
None
}
}
pub(crate) enum ListInterpolationStrategy {
Discrete,
RepeatToLcm,
PadToLongestWithNeutral,
}
impl<T: MakeComputed> MakeComputed for Option<T> {
fn make_computed(&mut self, sizing: &SizingContext) {
if let Some(value) = self.as_mut() {
value.make_computed(sizing);
}
}
}
impl<T: MakeComputed> MakeComputed for Box<[T]> {
fn make_computed(&mut self, sizing: &SizingContext) {
for value in self.iter_mut() {
value.make_computed(sizing);
}
}
}
impl<T: MakeComputed> MakeComputed for Vec<T> {
fn make_computed(&mut self, sizing: &SizingContext) {
for value in self.iter_mut() {
value.make_computed(sizing);
}
}
}
impl<T: Animatable + Clone> Animatable for Option<T> {
fn interpolate(
&mut self,
from: &Self,
to: &Self,
progress: f32,
sizing: &SizingContext,
current_color: Color,
) {
*self = match (from, to) {
(Some(from), Some(to)) => {
let mut value = from.clone();
value.interpolate(from, to, progress, sizing, current_color);
Some(value)
}
(Some(from), None) => T::missing_value().map_or_else(
|| {
if progress >= 0.5 {
None
} else {
Some(from.clone())
}
},
|missing| {
let mut value = from.clone();
value.interpolate(from, &missing, progress, sizing, current_color);
Some(value)
},
),
(None, Some(to)) => T::missing_value().map_or_else(
|| {
if progress >= 0.5 {
Some(to.clone())
} else {
None
}
},
|missing| {
let mut value = missing.clone();
value.interpolate(&missing, to, progress, sizing, current_color);
Some(value)
},
),
(None, None) => None,
};
}
}
impl<T: Animatable + Clone> Animatable for Box<[T]> {
fn missing_value() -> Option<Self> {
match T::list_interpolation_strategy() {
ListInterpolationStrategy::Discrete => None,
ListInterpolationStrategy::RepeatToLcm
| ListInterpolationStrategy::PadToLongestWithNeutral => Some(Box::default()),
}
}
fn interpolate(
&mut self,
from: &Self,
to: &Self,
progress: f32,
sizing: &SizingContext,
current_color: Color,
) {
*self = interpolate_list(
from,
to,
progress,
sizing,
current_color,
Vec::into_boxed_slice,
)
.unwrap_or_else(|| {
if progress >= 0.5 {
to.clone()
} else {
from.clone()
}
});
}
}
impl<T: Animatable + Clone> Animatable for Vec<T> {
fn missing_value() -> Option<Self> {
match T::list_interpolation_strategy() {
ListInterpolationStrategy::Discrete => None,
ListInterpolationStrategy::RepeatToLcm
| ListInterpolationStrategy::PadToLongestWithNeutral => Some(Vec::new()),
}
}
fn interpolate(
&mut self,
from: &Self,
to: &Self,
progress: f32,
sizing: &SizingContext,
current_color: Color,
) {
*self = interpolate_list(from, to, progress, sizing, current_color, |values| values)
.unwrap_or_else(|| {
if progress >= 0.5 {
to.clone()
} else {
from.clone()
}
});
}
}
fn interpolate_list<T: Animatable + Clone, C: AsRef<[T]>, O>(
from: &C,
to: &C,
progress: f32,
sizing: &SizingContext,
current_color: Color,
build: impl FnOnce(Vec<T>) -> O,
) -> Option<O> {
let from = from.as_ref();
let to = to.as_ref();
let values = match T::list_interpolation_strategy() {
ListInterpolationStrategy::Discrete => {
if from.len() != to.len() {
return None;
}
interpolate_pairwise_list(from, to, from.len(), progress, sizing, current_color)
}
ListInterpolationStrategy::RepeatToLcm => {
if from.is_empty() || to.is_empty() {
return None;
}
interpolate_pairwise_list(
from,
to,
lcm(from.len(), to.len()),
progress,
sizing,
current_color,
)
}
ListInterpolationStrategy::PadToLongestWithNeutral => {
interpolate_neutral_padded_list(from, to, progress, sizing, current_color)?
}
};
Some(build(values))
}
fn interpolate_pairwise_list<T: Animatable + Clone>(
from: &[T],
to: &[T],
output_len: usize,
progress: f32,
sizing: &SizingContext,
current_color: Color,
) -> Vec<T> {
(0..output_len)
.map(|index| {
let from_value = &from[index % from.len()];
let to_value = &to[index % to.len()];
let mut value = from_value.clone();
value.interpolate(from_value, to_value, progress, sizing, current_color);
value
})
.collect()
}
fn interpolate_neutral_padded_list<T: Animatable + Clone>(
from: &[T],
to: &[T],
progress: f32,
sizing: &SizingContext,
current_color: Color,
) -> Option<Vec<T>> {
let output_len = from.len().max(to.len());
(0..output_len)
.map(|index| {
let from_value = if index < from.len() {
from.get(index).cloned()
} else {
to.get(index).and_then(T::neutral_value_like)
}?;
let to_value = if index < to.len() {
to.get(index).cloned()
} else {
from.get(index).and_then(T::neutral_value_like)
}?;
let mut value = from_value.clone();
value.interpolate(&from_value, &to_value, progress, sizing, current_color);
Some(value)
})
.collect()
}
pub trait ToCss {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result;
fn to_css_string(&self) -> String {
let mut css = String::new();
let _ = self.to_css(&mut css);
css
}
}
impl<T: ToCss + ?Sized> ToCss for &T {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
(*self).to_css(dest)
}
}
impl<T: ToCss> ToCss for Option<T> {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
match self {
Some(v) => v.to_css(dest),
None => dest.write_str("none"),
}
}
}
impl<T: ToCss> ToCss for Box<[T]> {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
for (i, item) in self.iter().enumerate() {
if i > 0 {
dest.write_str(", ")?;
}
item.to_css(dest)?;
}
Ok(())
}
}
impl<T: ToCss> ToCss for Vec<T> {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
for (i, item) in self.iter().enumerate() {
if i > 0 {
dest.write_str(", ")?;
}
item.to_css(dest)?;
}
Ok(())
}
}
impl ToCss for f32 {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
write!(dest, "{}", self)
}
}
impl ToCss for u32 {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
write!(dest, "{}", self)
}
}
impl MakeComputed for u32 {}
impl Animatable for u32 {}
impl<'i> FromCss<'i> for u32 {
const VALID_TOKENS: &'static [CssToken] = &[CssToken::Syntax(CssSyntaxKind::Integer)];
fn from_css(input: &mut cssparser::Parser<'i, '_>) -> ParseResult<'i, Self> {
let value = input.expect_integer()?;
if value < 0 {
return Err(input.new_error(cssparser::BasicParseErrorKind::QualifiedRuleInvalid));
}
Ok(value as u32)
}
}
impl ToCss for i32 {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
write!(dest, "{}", self)
}
}
impl ToCss for String {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(self)
}
}
impl ToCss for Arc<str> {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
dest.write_str(self)
}
}
macro_rules! impl_from_taffy_enum {
($from_ty:ty, $to_ty:ty, $($variant:ident),*) => {
impl From<$from_ty> for $to_ty {
fn from(value: $from_ty) -> Self {
match value {
$(<$from_ty>::$variant => <$to_ty>::$variant,)*
}
}
}
};
}
pub(crate) use impl_from_taffy_enum;
macro_rules! declare_enum_from_css_impl {
(
$enum_type:ty,
$($css_value:expr => $variant:path),* $(,)?
) => {
impl crate::style::MakeComputed for $enum_type {}
impl<'i> crate::style::FromCss<'i> for $enum_type {
const VALID_TOKENS: &'static [crate::style::CssToken] =
&[$(crate::style::CssToken::Keyword($css_value)),*];
fn from_css(input: &mut cssparser::Parser<'i, '_>) -> crate::style::ParseResult<'i, Self> {
let location = input.current_source_location();
let token = input.next()?;
let cssparser::Token::Ident(ident) = token else {
return Err($crate::style::unexpected_token!(location, token));
};
cssparser::match_ignore_ascii_case! {&ident,
$(
$css_value => Ok($variant),
)*
_ => Err($crate::style::unexpected_token!(location, token)),
}
}
}
impl crate::style::properties::ToCss for $enum_type {
fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
match self {
$(
$variant => dest.write_str($css_value),
)*
}
}
}
};
}
pub(crate) use declare_enum_from_css_impl;
macro_rules! declare_box_alignment_enum_impl {
(
$enum_type:ty,
safe { $($safe_css:literal => $base_variant:ident / $safe_variant:ident),+ $(,)? },
plain { $($plain_css:literal => $plain_variant:ident),* $(,)? }
) => {
impl crate::style::MakeComputed for $enum_type {}
impl<'i> crate::style::FromCss<'i> for $enum_type {
const VALID_TOKENS: &'static [crate::style::CssToken] = &[
$(crate::style::CssToken::Keyword($plain_css),)*
$(crate::style::CssToken::Keyword($safe_css),)*
crate::style::CssToken::Keyword("safe"),
crate::style::CssToken::Keyword("unsafe"),
];
fn from_css(input: &mut cssparser::Parser<'i, '_>) -> crate::style::ParseResult<'i, Self> {
let mut safe = false;
loop {
let location = input.current_source_location();
let token = input.next()?;
let cssparser::Token::Ident(ident) = token else {
return Err($crate::style::unexpected_token!(location, token));
};
cssparser::match_ignore_ascii_case! {&ident,
"safe" => safe = true,
"unsafe" => safe = false,
$($safe_css => return Ok(if safe { Self::$safe_variant } else { Self::$base_variant }),)*
$($plain_css => return if safe {
Err($crate::style::unexpected_token!(location, token))
} else {
Ok(Self::$plain_variant)
},)*
_ => return Err($crate::style::unexpected_token!(location, token)),
}
}
}
}
impl crate::style::properties::ToCss for $enum_type {
fn to_css<W: std::fmt::Write>(&self, dest: &mut W) -> std::fmt::Result {
match self {
$(Self::$plain_variant => dest.write_str($plain_css),)*
$(Self::$base_variant => dest.write_str($safe_css),)*
$(Self::$safe_variant => {
dest.write_str("safe ")?;
dest.write_str($safe_css)
})*
}
}
}
};
}
pub(crate) use declare_box_alignment_enum_impl;