use std::{borrow::Cow, fmt, fmt::Debug};
use cssparser::{BasicParseErrorKind, ParseError, Parser};
use typed_builder::TypedBuilder;
use crate::{
layout::style::{
Animatable, Color, ColorInput, CssSyntaxKind, CssToken, FromCss, Length, LengthDefaultsToZero,
ListInterpolationStrategy, MakeComputed, ParseResult, ToCss, next_is_comma,
},
rendering::Sizing,
};
#[derive(Debug, Clone, PartialEq, Copy, Default, TypedBuilder)]
#[non_exhaustive]
#[builder(field_defaults(default))]
pub struct BoxShadow {
#[builder(default = false)]
pub inset: bool,
pub offset_x: LengthDefaultsToZero,
pub offset_y: LengthDefaultsToZero,
pub blur_radius: LengthDefaultsToZero,
pub spread_radius: LengthDefaultsToZero,
pub color: ColorInput,
}
pub type BoxShadows = Box<[BoxShadow]>;
impl<'i> FromCss<'i> for BoxShadows {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, Self> {
Ok(
input
.parse_comma_separated(BoxShadow::from_css)?
.into_boxed_slice(),
)
}
const VALID_TOKENS: &'static [CssToken] = BoxShadow::VALID_TOKENS;
}
impl<'i> FromCss<'i> for BoxShadow {
fn from_css(input: &mut Parser<'i, '_>) -> ParseResult<'i, BoxShadow> {
let mut color = None;
let mut lengths = None;
let mut inset = false;
while !input.is_exhausted() && !next_is_comma(input) {
if !inset
&& input
.try_parse(|input| input.expect_ident_matching("inset"))
.is_ok()
{
inset = true;
continue;
}
if lengths.is_none() {
let value = input.try_parse::<_, _, ParseError<Cow<'i, str>>>(|input| {
let horizontal = Length::from_css(input)?;
let vertical = Length::from_css(input)?;
let blur = input.try_parse(Length::from_css).unwrap_or(Length::zero());
let spread = input.try_parse(Length::from_css).unwrap_or(Length::zero());
Ok((horizontal, vertical, blur, spread))
});
if let Ok(value) = value {
lengths = Some(value);
continue;
}
}
if color.is_none()
&& let Ok(value) = input.try_parse(ColorInput::from_css)
{
color = Some(value);
continue;
}
break;
}
let lengths = lengths.ok_or(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid))?;
Ok(BoxShadow {
color: color.unwrap_or(ColorInput::Value(Color::transparent())),
offset_x: lengths.0,
offset_y: lengths.1,
blur_radius: lengths.2,
spread_radius: lengths.3,
inset,
})
}
const VALID_TOKENS: &'static [CssToken] = &[
CssToken::Keyword("inset"),
CssToken::Syntax(CssSyntaxKind::Length),
CssToken::Syntax(CssSyntaxKind::Color),
];
}
impl crate::layout::style::tw::TailwindPropertyParser for BoxShadow {
fn parse_tw(token: &str) -> Option<Self> {
Self::from_str(token).ok()
}
}
impl MakeComputed for BoxShadow {
fn make_computed(&mut self, sizing: &Sizing) {
self.offset_x.make_computed(sizing);
self.offset_y.make_computed(sizing);
self.blur_radius.make_computed(sizing);
self.spread_radius.make_computed(sizing);
}
}
impl Animatable for BoxShadow {
fn list_interpolation_strategy() -> ListInterpolationStrategy {
ListInterpolationStrategy::PadToLongestWithNeutral
}
fn neutral_value_like(other: &Self) -> Option<Self> {
Some(Self {
inset: other.inset,
offset_x: Length::zero(),
offset_y: Length::zero(),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: Color::transparent().into(),
})
}
fn interpolate(
&mut self,
from: &Self,
to: &Self,
progress: f32,
sizing: &Sizing,
current_color: Color,
) {
if from.inset != to.inset {
*self = if progress >= 0.5 { *to } else { *from };
return;
}
self.inset = from.inset;
self.offset_x.interpolate(
&from.offset_x,
&to.offset_x,
progress,
sizing,
current_color,
);
self.offset_y.interpolate(
&from.offset_y,
&to.offset_y,
progress,
sizing,
current_color,
);
self.blur_radius.interpolate(
&from.blur_radius,
&to.blur_radius,
progress,
sizing,
current_color,
);
self.spread_radius.interpolate(
&from.spread_radius,
&to.spread_radius,
progress,
sizing,
current_color,
);
self
.color
.interpolate(&from.color, &to.color, progress, sizing, current_color);
}
}
impl ToCss for BoxShadow {
fn to_css<W: fmt::Write>(&self, dest: &mut W) -> fmt::Result {
if self.inset {
dest.write_str("inset ")?;
}
self.offset_x.to_css(dest)?;
dest.write_char(' ')?;
self.offset_y.to_css(dest)?;
let blur_zero = self.blur_radius == Length::zero();
let spread_zero = self.spread_radius == Length::zero();
if !spread_zero {
dest.write_char(' ')?;
self.blur_radius.to_css(dest)?;
dest.write_char(' ')?;
self.spread_radius.to_css(dest)?;
} else if !blur_zero {
dest.write_char(' ')?;
self.blur_radius.to_css(dest)?;
}
dest.write_char(' ')?;
self.color.to_css(dest)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::style::{
Color,
Length::{self, Px},
};
#[test]
fn test_parse_simple_box_shadow() {
assert_eq!(
BoxShadow::from_str("2px 4px"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color::transparent()),
inset: false,
})
);
}
#[test]
fn test_parse_box_shadow_with_blur() {
assert_eq!(
BoxShadow::from_str("2px 4px 6px"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Px(6.0),
spread_radius: Length::zero(),
color: ColorInput::Value(Color::transparent()),
inset: false,
})
);
}
#[test]
fn test_parse_box_shadow_with_spread() {
assert_eq!(
BoxShadow::from_str("2px 4px 6px 8px"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Px(6.0),
spread_radius: Px(8.0),
color: ColorInput::Value(Color::transparent()),
inset: false,
})
);
}
#[test]
fn test_parse_box_shadow_with_color() {
assert_eq!(
BoxShadow::from_str("2px 4px red"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color([255, 0, 0, 255])),
inset: false,
})
);
}
#[test]
fn test_parse_inset_box_shadow() {
assert_eq!(
BoxShadow::from_str("inset 2px 4px"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color::transparent()),
inset: true,
})
);
}
#[test]
fn test_parse_box_shadow_color_first() {
assert_eq!(
BoxShadow::from_str("red 2px 4px"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color([255, 0, 0, 255])),
inset: false,
})
);
}
#[test]
fn test_parse_box_shadow_inset_after_offsets() {
assert_eq!(
BoxShadow::from_str("2px 4px inset red"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color([255, 0, 0, 255])),
inset: true,
})
);
}
#[test]
fn test_parse_box_shadow_hex_color() {
assert_eq!(
BoxShadow::from_str("2px 4px #ff0000"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color([255, 0, 0, 255])),
inset: false,
})
);
}
#[test]
fn test_parse_box_shadow_rgba_color() {
assert_eq!(
BoxShadow::from_str("2px 4px rgba(255, 0, 0, 0.5)"),
Ok(BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color([255, 0, 0, 128])), inset: false,
})
);
}
#[test]
fn test_parse_box_shadow_invalid() {
assert!(BoxShadow::from_str("2px").is_err());
assert!(BoxShadow::from_str("").is_err());
}
#[test]
fn test_parse_multiple_box_shadows_with_rgba() {
assert_eq!(
BoxShadows::from_str("2px 4px rgba(0, 0, 0, 0.5), 1px 2px 3px rgba(255, 0, 0, 0.25)"),
Ok(
[
BoxShadow {
offset_x: Px(2.0),
offset_y: Px(4.0),
blur_radius: Length::zero(),
spread_radius: Length::zero(),
color: ColorInput::Value(Color([0, 0, 0, 128])),
inset: false,
},
BoxShadow {
offset_x: Px(1.0),
offset_y: Px(2.0),
blur_radius: Px(3.0),
spread_radius: Length::zero(),
color: ColorInput::Value(Color([255, 0, 0, 64])),
inset: false,
}
]
.into()
)
);
}
}