use std::fmt;
use serde::Deserialize;
use serde::de::{self, Deserializer, MapAccess, Visitor};
use super::animatable::Animatable;
use super::decode_warn;
use super::units::{Angle, Length, Rect};
#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OutlineSpec {
#[serde(default)]
pub width: Option<Length>,
#[serde(default)]
pub offset: Option<Length>,
#[serde(default)]
pub color: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BoxShadowSpec {
#[serde(default)]
pub color: Option<String>,
#[serde(default)]
pub x_offset: Option<Length>,
#[serde(default)]
pub y_offset: Option<Length>,
#[serde(default)]
pub spread_radius: Option<Length>,
#[serde(default)]
pub blur_radius: Option<Length>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum BoxShadowList {
One(BoxShadowSpec),
Many(Vec<BoxShadowSpec>),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum LineHeightSpec {
Relative(f32),
Px { px: f32 },
Str(String),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum LetterSpacingSpec {
Px(f32),
Rem { rem: f32 },
Str(String),
}
#[derive(Debug, Clone, PartialEq, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextShadowSpec {
#[serde(default)]
pub color: Option<String>,
#[serde(default)]
pub offset_x: Option<f32>,
#[serde(default)]
pub offset_y: Option<f32>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GradientStop {
pub color: Animatable<String>,
#[serde(default)]
pub position: Option<Animatable<Length>>,
#[serde(default)]
pub hint: Option<Animatable<f32>>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AngularStop {
pub color: Animatable<String>,
#[serde(default)]
pub angle: Option<Animatable<Angle>>,
#[serde(default)]
pub hint: Option<Animatable<f32>>,
}
pub type GradientPosition = String;
pub type ColorSpace = String;
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RadialShapeSpec {
Keyword(String),
Circle { circle: Animatable<Length> },
Ellipse { ellipse: [Animatable<Length>; 2] },
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LinearGradientSpec {
#[serde(default)]
pub angle: Option<Animatable<Angle>>,
#[serde(default)]
pub stops: Vec<GradientStop>,
#[serde(default)]
pub color_space: Option<ColorSpace>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RadialGradientSpec {
#[serde(default)]
pub position: Option<GradientPosition>,
#[serde(default)]
pub shape: Option<RadialShapeSpec>,
#[serde(default)]
pub stops: Vec<GradientStop>,
#[serde(default)]
pub color_space: Option<ColorSpace>,
}
#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ConicGradientSpec {
#[serde(default)]
pub start: Option<Animatable<Angle>>,
#[serde(default)]
pub position: Option<GradientPosition>,
#[serde(default)]
pub stops: Vec<AngularStop>,
#[serde(default)]
pub color_space: Option<ColorSpace>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum GradientSpec {
Linear(LinearGradientSpec),
Radial(RadialGradientSpec),
Conic(ConicGradientSpec),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum GradientList {
One(GradientSpec),
Many(Vec<GradientSpec>),
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct BorderColorSpec {
pub top: Option<String>,
pub right: Option<String>,
pub bottom: Option<String>,
pub left: Option<String>,
}
impl BorderColorSpec {
fn uniform(s: String) -> Self {
BorderColorSpec {
top: Some(s.clone()),
right: Some(s.clone()),
bottom: Some(s.clone()),
left: Some(s),
}
}
}
impl<'de> Deserialize<'de> for BorderColorSpec {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
struct BorderColorVisitor;
impl<'de> Visitor<'de> for BorderColorVisitor {
type Value = BorderColorSpec;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a CSS color string or a {top,right,bottom,left} object of colors")
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<BorderColorSpec, E> {
Ok(BorderColorSpec::uniform(s.to_owned()))
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<BorderColorSpec, A::Error> {
let mut spec = BorderColorSpec::default();
while let Some(key) = map.next_key::<String>()? {
let v = map.next_value::<String>()?;
match key.as_str() {
"top" => spec.top = Some(v),
"right" => spec.right = Some(v),
"bottom" => spec.bottom = Some(v),
"left" => spec.left = Some(v),
_ => decode_warn(
"borderColor",
&key,
&format!(
"unknown borderColor side {key:?}; ignoring (expected top/right/bottom/left)"
),
),
}
}
Ok(spec)
}
}
d.deserialize_any(BorderColorVisitor)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct CornerRadii {
pub x: Rect,
pub y: Option<Rect>,
}
impl From<Rect> for CornerRadii {
fn from(x: Rect) -> Self {
Self { x, y: None }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum BorderRadiusSpec {
Circular(Rect),
Elliptical(Box<[Rect; 2]>),
}
impl BorderRadiusSpec {
pub fn radii(&self) -> CornerRadii {
match self {
Self::Circular(x) => (*x).into(),
Self::Elliptical(xy) => CornerRadii {
x: xy[0],
y: Some(xy[1]),
},
}
}
}
impl From<CornerRadii> for BorderRadiusSpec {
fn from(radii: CornerRadii) -> Self {
match radii.y {
None => Self::Circular(radii.x),
Some(y) => Self::Elliptical(Box::new([radii.x, y])),
}
}
}
impl From<Rect> for BorderRadiusSpec {
fn from(rect: Rect) -> Self {
Self::Circular(rect)
}
}
impl<'de> Deserialize<'de> for BorderRadiusSpec {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
use de::IntoDeserializer;
use de::value::MapAccessDeserializer;
struct RadiiVisitor;
impl<'de> Visitor<'de> for RadiiVisitor {
type Value = BorderRadiusSpec;
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("a rect, or a \"<horizontal radii> / <vertical radii>\" string")
}
fn visit_f64<E: de::Error>(self, v: f64) -> Result<BorderRadiusSpec, E> {
Rect::deserialize(v.into_deserializer()).map(Into::into)
}
fn visit_i64<E: de::Error>(self, v: i64) -> Result<BorderRadiusSpec, E> {
Rect::deserialize(v.into_deserializer()).map(Into::into)
}
fn visit_u64<E: de::Error>(self, v: u64) -> Result<BorderRadiusSpec, E> {
Rect::deserialize(v.into_deserializer()).map(Into::into)
}
fn visit_str<E: de::Error>(self, s: &str) -> Result<BorderRadiusSpec, E> {
let side =
|s: &str| -> Result<Rect, E> { Rect::deserialize(s.into_deserializer()) };
Ok(match s.split_once('/') {
Some((x, y)) => BorderRadiusSpec::Elliptical(Box::new([side(x)?, side(y)?])),
None => side(s)?.into(),
})
}
fn visit_map<A: MapAccess<'de>>(self, map: A) -> Result<BorderRadiusSpec, A::Error> {
Rect::deserialize(MapAccessDeserializer::new(map)).map(Into::into)
}
}
d.deserialize_any(RadiiVisitor)
}
}
#[cfg(test)]
mod tests {
use crate::protocol::animatable::AnimatableField;
use crate::style::Style;
use crate::style::props::{BORDER_COLOR, BORDER_RADIUS, WIDTH};
#[test]
fn border_radius_decodes_the_slash_form() {
use super::{BorderRadiusSpec, CornerRadii};
use crate::protocol::units::{Length::*, Rect};
let radii = |v: serde_json::Value| {
serde_json::from_value::<BorderRadiusSpec>(v)
.expect("decodes")
.radii()
};
let rect = |v: serde_json::Value| serde_json::from_value::<Rect>(v).expect("decodes");
let r = radii(serde_json::json!("10px 20px / 5px"));
assert_eq!(r.x, rect(serde_json::json!("10px 20px")));
assert_eq!(r.y, Some(rect(serde_json::json!(5))));
assert_eq!((r.x.top, r.x.right), (Px(10.0), Px(20.0)));
assert_eq!(
radii(serde_json::json!("50%/25%")),
radii(serde_json::json!("50% / 25%"))
);
assert_eq!(
radii(serde_json::json!("50%/25%")).y.map(|y| y.top),
Some(Percent(25.0))
);
for v in [
serde_json::json!(8),
serde_json::json!(2.5),
serde_json::json!("8px 16px"),
serde_json::json!({ "top": 4 }),
serde_json::json!({ "horizontal": 4 }),
] {
assert_eq!(radii(v.clone()), CornerRadii::from(rect(v)));
}
}
#[test]
fn border_radius_slash_decodes_through_the_style() {
let s: Style = serde_json::from_str(r#"{ "borderRadius": "40px / 10px" }"#).unwrap();
let radii = s.get(&BORDER_RADIUS).static_ref().expect("static").radii();
assert!(radii.y.is_some());
let s: Style = serde_json::from_value(serde_json::json!({
"borderRadius": { "animated": { "id": 3 }, "seed": "40px / 10px" }
}))
.unwrap();
let value = s.get(&BORDER_RADIUS).expect("present");
assert!(value.binding().is_some() && value.value().is_none());
assert_eq!(value.seed().map(|seed| seed.radii()), Some(radii));
}
#[cfg(all(feature = "devtools", debug_assertions))]
#[test]
fn bad_border_radius_slash_values_warn_and_fall_back() {
use crate::protocol::units::{Length, Rect};
let decode = |radius: &str| {
crate::diag::decode_batch_start();
let s: Style =
serde_json::from_value(serde_json::json!({ "borderRadius": radius, "width": 40 }))
.expect("a bad radius must not abort deserialization");
assert_eq!(s.get(&WIDTH).static_val(), Some(Length::Px(40.0)));
let radii = s.get(&BORDER_RADIUS).static_ref().expect("static").radii();
let warned: Vec<String> = crate::diag::take_decode_warnings()
.into_iter()
.filter(|w| w.kind == "rect")
.map(|w| w.value)
.collect();
(radii, warned)
};
let (radii, warned) = decode("10px / ");
assert_eq!(radii.y, Some(Rect::default()), "an empty half is 0");
assert_eq!(warned.len(), 1, "{warned:?}");
let (radii, warned) = decode("10px / bogus");
assert_eq!(radii.y, Some(Rect::default()));
assert_eq!(warned, ["bogus"]);
let (_, warned) = decode("1px / 2px / 3px");
assert_eq!(warned, ["/"]);
}
#[test]
fn gradient_leaves_decode_animatable_and_compare() {
use super::GradientSpec;
use crate::protocol::animatable::Animatable;
let g: GradientSpec = serde_json::from_value(serde_json::json!({
"type": "linear",
"angle": { "animated": { "id": 1 }, "seed": 45 },
"stops": [
{ "color": { "animated": { "type": "interpolateColor", "id": 2,
"input": [0, 1], "output": [[0,0,0,1],[1,1,1,1]] } },
"position": { "animated": { "id": 3 }, "seed": "20px" } },
{ "color": "#ff0000", "hint": 0.25 },
],
}))
.unwrap();
let GradientSpec::Linear(l) = &g else {
panic!()
};
assert!(l.angle.binding().is_some());
assert!(matches!(&l.stops[0].color, Animatable::Animated(_)));
assert_eq!(l.stops[1].color, Animatable::Static("#ff0000".to_string()));
assert_eq!(g.clone(), g, "PartialEq derived");
}
#[test]
fn radial_shape_wire_forms_preserved() {
use super::RadialShapeSpec;
use crate::protocol::animatable::Animatable;
use crate::protocol::units::Length;
let k: RadialShapeSpec =
serde_json::from_value(serde_json::json!({ "keyword": "closestSide" })).unwrap();
assert!(matches!(k, RadialShapeSpec::Keyword(ref s) if s == "closestSide"));
let c: RadialShapeSpec =
serde_json::from_value(serde_json::json!({ "circle": { "circle": 40 } })).unwrap();
let RadialShapeSpec::Circle { circle } = c else {
panic!("expected Circle, got {c:?}");
};
assert_eq!(circle, Animatable::Static(Length::Px(40.0)));
let e: RadialShapeSpec =
serde_json::from_value(serde_json::json!({ "ellipse": { "ellipse": [10, "50%"] } }))
.unwrap();
let RadialShapeSpec::Ellipse { ellipse } = e else {
panic!("expected Ellipse, got {e:?}");
};
assert_eq!(ellipse[0], Animatable::Static(Length::Px(10.0)));
assert_eq!(ellipse[1], Animatable::Static(Length::Percent(50.0)));
for v in [
serde_json::json!("closestSide"),
serde_json::json!({ "circle": 40 }),
serde_json::json!({ "ellipse": [10, 20] }),
] {
assert!(
serde_json::from_value::<RadialShapeSpec>(v.clone()).is_err(),
"{v} unexpectedly decoded"
);
}
}
#[test]
fn border_color_scalar_and_per_side() {
let uniform: Style =
serde_json::from_str(r#"{ "borderColor": "white" }"#).expect("scalar decodes");
let bc = uniform
.get(&BORDER_COLOR)
.static_ref()
.expect("border_color present");
assert_eq!(bc.top.as_deref(), Some("white"));
assert_eq!(bc.right.as_deref(), Some("white"));
assert_eq!(bc.bottom.as_deref(), Some("white"));
assert_eq!(bc.left.as_deref(), Some("white"));
let sided: Style =
serde_json::from_str(r##"{ "borderColor": { "top": "#f00", "left": "blue" } }"##)
.expect("object decodes");
let bc = sided
.get(&BORDER_COLOR)
.static_ref()
.expect("border_color present");
assert_eq!(bc.top.as_deref(), Some("#f00"));
assert_eq!(bc.left.as_deref(), Some("blue"));
assert_eq!(bc.right, None);
assert_eq!(bc.bottom, None);
let bogus: Style =
serde_json::from_str(r#"{ "borderColor": { "middle": "red", "top": "blue" } }"#)
.expect("unknown side key must not abort deserialization");
let bc = bogus
.get(&BORDER_COLOR)
.static_ref()
.expect("border_color present");
assert_eq!(bc.top.as_deref(), Some("blue"));
assert_eq!(bc.right, None);
assert_eq!(bc.bottom, None);
assert_eq!(bc.left, None);
}
}