use std::ops::Range;
use crate::{
impl_default_coercion_rule,
math::{Transform2, Vector2},
Color, ColorChannel, Depth, Duration, Fill, GradientStop, LayoutRole, LightShape,
LinearGradient, Material, MaterialParams, Numeric, Opacity, PathElement, PathSmoothing,
PaxValue, Percent, Property, RadialGradient, Rotation, Size, Stroke, StrokeCap, StrokeJoin,
Transform2D, UnitValue, Vector3,
};
impl_default_coercion_rule!(bool, PaxValue::Bool, true);
impl_default_coercion_rule!(u8, PaxValue::Numeric, true);
impl_default_coercion_rule!(u16, PaxValue::Numeric, true);
impl_default_coercion_rule!(u32, PaxValue::Numeric, true);
impl_default_coercion_rule!(u64, PaxValue::Numeric, true);
impl_default_coercion_rule!(i8, PaxValue::Numeric, true);
impl_default_coercion_rule!(i16, PaxValue::Numeric, true);
impl_default_coercion_rule!(i32, PaxValue::Numeric, true);
impl_default_coercion_rule!(i64, PaxValue::Numeric, true);
impl_default_coercion_rule!(f32, PaxValue::Numeric, true);
impl_default_coercion_rule!(f64, PaxValue::Numeric, true);
impl_default_coercion_rule!(isize, PaxValue::Numeric, true);
impl_default_coercion_rule!(usize, PaxValue::Numeric, true);
impl CoercionRules for LayoutRole {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
if !args.is_empty() {
return Err(format!(
"failed to coerce LayoutRole: expected no enum args, got {:?}",
args
));
}
match variant.as_str() {
"Default" => Ok(LayoutRole::Default),
"Breakout" => Ok(LayoutRole::Breakout),
_ => Err(format!(
"failed to coerce LayoutRole: unknown enum variant {:?}",
variant
)),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
LayoutRole::try_coerce(o)
} else {
Err("failed to coerce LayoutRole".to_string())
}
}
_ => Err("failed to coerce LayoutRole".to_string()),
}
}
}
pub trait CoercionRules
where
Self: Sized + 'static,
{
fn try_coerce(value: PaxValue) -> Result<Self, String>;
fn is_identity_roundtrip() -> bool {
false
}
}
pub fn is_typed_binding_safe<T: CoercionRules>() -> bool {
use std::{any::TypeId, cell::RefCell, collections::HashSet};
thread_local! {
static CHECKING: RefCell<HashSet<TypeId>> = RefCell::new(HashSet::new());
}
struct Check(TypeId);
impl Drop for Check {
fn drop(&mut self) {
CHECKING.with(|checking| checking.borrow_mut().remove(&self.0));
}
}
let id = TypeId::of::<T>();
if !CHECKING.with(|checking| checking.borrow_mut().insert(id)) {
return false;
}
let _check = Check(id);
T::is_identity_roundtrip()
}
impl CoercionRules for Color {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Color(color) => Ok(*color),
PaxValue::Enum(contents) => {
let (name, variant, mut args) = *contents;
if name != "Color" {
return Err(format!(
"{:?} can't be coerced into a Color",
PaxValue::Enum(Box::new((name, variant, args)))
));
}
match variant.as_str() {
"rgb" => Ok(Color::rgb(
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
)),
"rgba" => Ok(Color::rgba(
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
)),
"hsl" => Ok(Color::hsl(
Rotation::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
)),
"hsla" => Ok(Color::hsla(
Rotation::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
ColorChannel::try_coerce(args.remove(0))?,
)),
"SLATE" => Ok(Color::SLATE),
"GRAY" => Ok(Color::GRAY),
"ZINC" => Ok(Color::ZINC),
"NEUTRAL" => Ok(Color::NEUTRAL),
"STONE" => Ok(Color::STONE),
"RED" => Ok(Color::RED),
"ORANGE" => Ok(Color::ORANGE),
"AMBER" => Ok(Color::AMBER),
"YELLOW" => Ok(Color::YELLOW),
"LIME" => Ok(Color::LIME),
"GREEN" => Ok(Color::GREEN),
"EMERALD" => Ok(Color::EMERALD),
"TEAL" => Ok(Color::TEAL),
"CYAN" => Ok(Color::CYAN),
"SKY" => Ok(Color::SKY),
"BLUE" => Ok(Color::BLUE),
"INDIGO" => Ok(Color::INDIGO),
"VIOLET" => Ok(Color::VIOLET),
"PURPLE" => Ok(Color::PURPLE),
"FUCHSIA" => Ok(Color::FUCHSIA),
"PINK" => Ok(Color::PINK),
"ROSE" => Ok(Color::ROSE),
"BLACK" => Ok(Color::BLACK),
"WHITE" => Ok(Color::WHITE),
"TRANSPARENT" => Ok(Color::TRANSPARENT),
"NONE" => Ok(Color::NONE),
_ => Err(format!(
"failed to convert to Color: unknown variant {:?}",
variant
)),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
Color::try_coerce(o)
} else {
Err("failed to convert to Color".to_string())
}
}
_ => return Err(format!("{:?} can't be coerced into a Color", value)),
}
}
}
impl CoercionRules for PathElement {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::PathElement(path_elem) => Ok(*path_elem),
PaxValue::Enum(contents) => {
let (name, variant, values) = *contents;
if name == "PathElement" {
let mut values_itr = values.into_iter();
match variant.as_str() {
"Line" => Ok(PathElement::Line),
"Close" => Ok(PathElement::Close),
"Empty" => Ok(PathElement::Empty),
"Point" => Ok(PathElement::Point(
Size::try_coerce(values_itr.next().unwrap())?,
Size::try_coerce(values_itr.next().unwrap())?,
)),
"Quadratic" => Ok(PathElement::Quadratic(
Size::try_coerce(values_itr.next().unwrap())?,
Size::try_coerce(values_itr.next().unwrap())?,
)),
"Cubic" => Ok(PathElement::Cubic(
Size::try_coerce(values_itr.next().unwrap())?,
Size::try_coerce(values_itr.next().unwrap())?,
Size::try_coerce(values_itr.next().unwrap())?,
Size::try_coerce(values_itr.next().unwrap())?,
)),
_ => {
return Err(format!(
"failed to coerce PathElement: unknown enum variant: {:?}",
variant
))
}
}
} else {
return Err(format!(
"failed to coerce PathElement: enum name doesn't match"
));
}
}
_ => return Err(format!("failed to coerce PathElement: PaxValue not a path")),
}
}
}
impl CoercionRules for Fill {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Color(color) => Fill::Solid(*color),
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
match variant.as_str() {
"Solid" => {
let color = Color::try_coerce(args.into_iter().next().unwrap())?;
Fill::Solid(color)
}
"LinearGradient" => {
let gradient =
LinearGradient::try_coerce(args.into_iter().next().unwrap())?;
Fill::LinearGradient(gradient)
}
"linearGradient" => Fill::LinearGradient(parse_linear_gradient_args(args)?),
"RadialGradient" => {
let gradient =
RadialGradient::try_coerce(args.into_iter().next().unwrap())?;
Fill::RadialGradient(gradient)
}
"radialGradient" => Fill::RadialGradient(parse_radial_gradient_args(args)?),
_ => {
return Err(format!(
"failed to coerce Fill: unknown enum variant {:?}",
variant
))
}
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
Fill::try_coerce(o)?
} else {
return Err(format!("failed to coerce Fill: can't coerce None"));
}
}
_ => return Err(format!("{:?} can't be coerced into a Fill", pax_value)),
})
}
}
impl CoercionRules for LinearGradient {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Object(map) => {
let [start, end, stops] = extract_options(["start", "end", "stops"], map)
.map_err(|e| format!("failed to convert to LinearGradient: {e}"))?;
let (s1, s2) = match start {
PaxValue::Vec(vec) => {
let mut itr = vec.into_iter();
let s1 = Size::try_coerce(itr.next().unwrap())?;
let s2 = Size::try_coerce(itr.next().unwrap())?;
(s1, s2)
}
_ => return Err(format!("failed to coerce LinearGradient")),
};
let (e1, e2) = match end {
PaxValue::Vec(vec) => {
let mut itr = vec.into_iter();
let e1 = Size::try_coerce(itr.next().unwrap())?;
let e2 = Size::try_coerce(itr.next().unwrap())?;
(e1, e2)
}
_ => return Err(format!("failed to coerce LinearGradient")),
};
let stops = Vec::<GradientStop>::try_coerce(stops)?;
LinearGradient {
start: (s1, s2),
end: (e1, e2),
stops,
}
}
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
match variant.as_str() {
"linearGradient" => parse_linear_gradient_args(args)?,
_ => return Err(format!("failed to coerce LinearGradient")),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
LinearGradient::try_coerce(o)?
} else {
return Err(format!("failed to coerce LinearGradient"));
}
}
_ => {
return Err(format!(
"{:?} can't be coerced into a LinearGradient",
pax_value
))
}
})
}
}
impl CoercionRules for RadialGradient {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Object(map) => {
let [start, end, stops, radius] =
extract_options(["start", "end", "stops", "radius"], map)
.map_err(|e| format!("failed to convert to RadialGradient: {e}"))?;
let (s1, s2) = match start {
PaxValue::Vec(vec) => {
let mut itr = vec.into_iter();
let s1 = Size::try_coerce(itr.next().unwrap())?;
let s2 = Size::try_coerce(itr.next().unwrap())?;
(s1, s2)
}
_ => {
return Err(format!("failed to coerce RadialGradient"));
}
};
let (e1, e2) = match end {
PaxValue::Vec(vec) => {
let mut itr = vec.into_iter();
let e1 = Size::try_coerce(itr.next().unwrap())?;
let e2 = Size::try_coerce(itr.next().unwrap())?;
(e1, e2)
}
_ => {
return Err(format!("failed to coerce RadialGradient"));
}
};
let radius = match radius {
PaxValue::Numeric(n) => n.to_float(),
_ => {
return Err(format!("failed to coerce RadialGradient"));
}
};
let stops = Vec::<GradientStop>::try_coerce(stops)?;
RadialGradient {
start: (s1, s2),
end: (e1, e2),
radius,
stops,
}
}
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
match variant.as_str() {
"radialGradient" => parse_radial_gradient_args(args)?,
_ => return Err(format!("failed to coerce RadialGradient")),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
RadialGradient::try_coerce(o)?
} else {
return Err(format!("failed to coerce RadialGradient"));
}
}
_ => {
return Err(format!("failed to coerce RadialGradient"));
}
})
}
}
impl CoercionRules for GradientStop {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Object(map) => {
let [position, color] = extract_options(["position", "color"], map)
.map_err(|e| format!("failed to convert to GradientStop: {e}"))?;
let position = Size::try_coerce(position)?;
let color = Color::try_coerce(color)?;
GradientStop { position, color }
}
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
match variant.as_str() {
"get" => {
let mut args = args.into_iter();
let color = Color::try_coerce(
args.next()
.ok_or_else(|| "failed to convert to GradientStop".to_string())?,
)?;
let position = Size::try_coerce(
args.next()
.ok_or_else(|| "failed to convert to GradientStop".to_string())?,
)?;
GradientStop { position, color }
}
_ => return Err(format!("failed to convert to GradientStop")),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
GradientStop::try_coerce(o)?
} else {
return Err(format!("failed to convert to GradientStop"));
}
}
_ => {
return Err(format!("failed to convert to GradientStop"));
}
})
}
}
impl CoercionRules for Depth {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Size(Size::Pixels(value)) => Depth(value),
PaxValue::Size(Size::Percent(_)) | PaxValue::Percent(_) => {
return Err("failed to coerce Depth: percent depth is not supported".to_string());
}
PaxValue::Size(Size::Combined(_, _)) => {
return Err("failed to coerce Depth: combined depth is not supported".to_string());
}
PaxValue::Numeric(value) => Depth(value),
PaxValue::Option(o) => {
if let Some(o) = *o {
Depth::try_coerce(o)?
} else {
return Err("failed to coerce Depth".to_string());
}
}
_ => return Err(format!("{:?} can't be coerced into a Depth", pax_value)),
})
}
}
impl CoercionRules for Vector3 {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Object(map) => {
let [x, y, z] = extract_options(["x", "y", "z"], map)
.map_err(|e| format!("failed to convert to Vector3: {e}"))?;
Vector3 {
x: f64::try_coerce(x)?,
y: f64::try_coerce(y)?,
z: f64::try_coerce(z)?,
}
}
PaxValue::Vec(values) => {
if values.len() != 3 {
return Err("failed to coerce Vector3: expected three values".to_string());
}
let mut values = values.into_iter();
Vector3 {
x: f64::try_coerce(values.next().unwrap())?,
y: f64::try_coerce(values.next().unwrap())?,
z: f64::try_coerce(values.next().unwrap())?,
}
}
PaxValue::Enum(contents) => {
let (_, variant, values) = *contents;
match variant.as_str() {
"new" => {
if values.len() != 3 {
return Err("failed to coerce Vector3::new".to_string());
}
let mut values = values.into_iter();
Vector3 {
x: f64::try_coerce(values.next().unwrap())?,
y: f64::try_coerce(values.next().unwrap())?,
z: f64::try_coerce(values.next().unwrap())?,
}
}
_ => {
return Err(format!(
"failed to coerce Vector3: unknown enum variant {:?}",
variant
))
}
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
Vector3::try_coerce(o)?
} else {
return Err("failed to coerce Vector3".to_string());
}
}
_ => return Err(format!("{:?} can't be coerced into a Vector3", pax_value)),
})
}
}
impl CoercionRules for LightShape {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
if !args.is_empty() {
return Err(format!(
"failed to coerce LightShape: expected no enum args, got {:?}",
args
));
}
match variant.as_str() {
"Point" => Ok(LightShape::Point),
"Directional" => Ok(LightShape::Directional),
_ => Err(format!(
"failed to coerce LightShape: unknown enum variant {:?}",
variant
)),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
LightShape::try_coerce(o)
} else {
Err("failed to coerce LightShape".to_string())
}
}
_ => Err("failed to coerce LightShape".to_string()),
}
}
}
impl CoercionRules for MaterialParams {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Object(map) => {
let mut params = MaterialParams::default();
for (key, value) in map {
match key.as_str() {
"ambient" => params.ambient = Property::new(f64::try_coerce(value)?),
"diffuse" => params.diffuse = Property::new(f64::try_coerce(value)?),
"specular" => params.specular = Property::new(f64::try_coerce(value)?),
"roughness" => params.roughness = Property::new(f64::try_coerce(value)?),
"metallic" => params.metallic = Property::new(f64::try_coerce(value)?),
"emissive" => params.emissive = Property::new(Color::try_coerce(value)?),
"emissive_intensity" | "emissiveIntensity" => {
params.emissive_intensity = Property::new(f64::try_coerce(value)?)
}
_ => {}
}
}
params
}
PaxValue::Option(o) => {
if let Some(o) = *o {
MaterialParams::try_coerce(o)?
} else {
return Err("failed to coerce MaterialParams".to_string());
}
}
_ => {
return Err(format!(
"{:?} can't be coerced into MaterialParams",
pax_value
))
}
})
}
}
impl CoercionRules for Material {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Object(map) => {
Material::Lit(MaterialParams::try_coerce(PaxValue::Object(map))?)
}
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
match variant.as_str() {
"Lit" | "custom" => {
let params = args
.into_iter()
.next()
.ok_or_else(|| "failed to coerce Material::custom".to_string())?;
Material::Lit(MaterialParams::try_coerce(params)?)
}
"Unlit" | "unlit" => Material::Unlit,
"Matte" | "matte" => Material::matte(),
"Glossy" | "glossy" => {
let specular = args
.into_iter()
.next()
.map(f64::try_coerce)
.transpose()?
.unwrap_or(0.28);
Material::glossy(specular)
}
"Metallic" | "metallic" => {
let metallic = args
.into_iter()
.next()
.map(f64::try_coerce)
.transpose()?
.unwrap_or(0.72);
Material::metallic(metallic)
}
"Emissive" | "emissive" => {
if args.len() != 2 {
return Err("failed to coerce Material::emissive".to_string());
}
let mut args = args.into_iter();
let color = Color::try_coerce(args.next().unwrap())?;
let intensity = f64::try_coerce(args.next().unwrap())?;
Material::emissive(color, intensity)
}
_ => {
return Err(format!(
"failed to coerce Material: unknown enum variant {:?}",
variant
))
}
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
Material::try_coerce(o)?
} else {
return Err("failed to coerce Material".to_string());
}
}
_ => return Err(format!("{:?} can't be coerced into Material", pax_value)),
})
}
}
fn parse_gradient_point(value: PaxValue) -> Result<(Size, Size), String> {
match value {
PaxValue::Vec(vec) => {
let mut itr = vec.into_iter();
let x = Size::try_coerce(
itr.next()
.ok_or_else(|| "failed to coerce gradient point".to_string())?,
)?;
let y = Size::try_coerce(
itr.next()
.ok_or_else(|| "failed to coerce gradient point".to_string())?,
)?;
Ok((x, y))
}
_ => Err("failed to coerce gradient point".to_string()),
}
}
fn parse_linear_gradient_args(args: Vec<PaxValue>) -> Result<LinearGradient, String> {
if args.len() != 3 {
return Err("failed to coerce Fill::linearGradient".to_string());
}
let mut args = args.into_iter();
let start = parse_gradient_point(args.next().unwrap())?;
let end = parse_gradient_point(args.next().unwrap())?;
let stops = Vec::<GradientStop>::try_coerce(args.next().unwrap())?;
Ok(LinearGradient { start, end, stops })
}
fn parse_radial_gradient_args(args: Vec<PaxValue>) -> Result<RadialGradient, String> {
if args.len() != 4 {
return Err("failed to coerce Fill::radialGradient".to_string());
}
let mut args = args.into_iter();
let start = parse_gradient_point(args.next().unwrap())?;
let end = parse_gradient_point(args.next().unwrap())?;
let stops = Vec::<GradientStop>::try_coerce(args.next().unwrap())?;
let radius = Numeric::try_coerce(args.next().unwrap())?.to_float();
Ok(RadialGradient {
start,
end,
radius,
stops,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn pct(value: i64) -> PaxValue {
PaxValue::Percent(Percent(Numeric::I64(value)))
}
fn color(hex: &str) -> PaxValue {
PaxValue::Color(Box::new(Color::from_hex(hex)))
}
#[test]
fn coerces_color_helper_syntax() {
let pax_value = PaxValue::Enum(Box::new((
"Color".to_string(),
"rgba".to_string(),
vec![
PaxValue::Numeric(255.into()),
PaxValue::Numeric(224.into()),
PaxValue::Numeric(162.into()),
PaxValue::Numeric(255.into()),
],
)));
let color = Color::try_coerce(pax_value).expect("rgba helper syntax should coerce");
assert_eq!(
color,
Color::rgba(
ColorChannel::Integer(255),
ColorChannel::Integer(224),
ColorChannel::Integer(162),
ColorChannel::Integer(255),
)
);
}
#[test]
fn coerces_fill_linear_gradient_helper_syntax() {
let pax_value = PaxValue::Enum(Box::new((
"Fill".to_string(),
"linearGradient".to_string(),
vec![
PaxValue::Vec(vec![pct(0), pct(50)]),
PaxValue::Vec(vec![pct(100), pct(50)]),
PaxValue::Vec(vec![
PaxValue::Enum(Box::new((
"GradientStop".to_string(),
"get".to_string(),
vec![color("ff00ffff"), pct(0)],
))),
PaxValue::Enum(Box::new((
"GradientStop".to_string(),
"get".to_string(),
vec![color("ffffffff"), pct(100)],
))),
]),
],
)));
let fill = Fill::try_coerce(pax_value).expect("helper syntax should coerce");
let Fill::LinearGradient(gradient) = fill else {
panic!("expected linear gradient");
};
assert_eq!(gradient.stops.len(), 2);
assert_eq!(gradient.start.0, Size::Percent(Numeric::I64(0)));
assert_eq!(gradient.end.0, Size::Percent(Numeric::I64(100)));
}
#[test]
fn coerces_gradient_stop_get_helper_syntax() {
let pax_value = PaxValue::Enum(Box::new((
"GradientStop".to_string(),
"get".to_string(),
vec![color("11223344"), pct(48)],
)));
let stop = GradientStop::try_coerce(pax_value).expect("helper syntax should coerce");
assert_eq!(stop.position, Size::Percent(Numeric::I64(48)));
assert_eq!(stop.color, Color::from_hex("11223344"));
}
#[test]
fn coerces_opacity_from_percent_literal() {
let opacity = Opacity::try_coerce(pct(50)).expect("percent opacity should coerce");
assert_eq!(opacity, Opacity::Percent(Numeric::I64(50)));
assert!((opacity.to_float_0_1() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn coerces_opacity_from_numeric_literal() {
let opacity = Opacity::try_coerce(PaxValue::Numeric(Numeric::F64(0.5)))
.expect("numeric opacity should coerce");
assert_eq!(opacity, Opacity::Alpha(Numeric::F64(0.5)));
assert!((opacity.to_float_0_1() - 0.5).abs() < f64::EPSILON);
}
#[test]
fn coerces_depth_from_px_but_rejects_percent() {
let depth = Depth::try_coerce(PaxValue::Size(Size::Pixels(Numeric::F64(260.0))))
.expect("px depth should coerce");
assert_eq!(depth.to_float(), 260.0);
let err = Depth::try_coerce(pct(50)).expect_err("percent depth should fail");
assert!(err.contains("percent depth"));
}
#[test]
fn coerces_material_helper_syntax() {
let pax_value = PaxValue::Enum(Box::new((
"Material".to_string(),
"glossy".to_string(),
vec![PaxValue::Numeric(Numeric::F64(0.44))],
)));
let material = Material::try_coerce(pax_value).expect("material helper should coerce");
let Material::Lit(params) = material else {
panic!("expected lit material");
};
assert_eq!(params.specular.get(), 0.44);
assert_eq!(params.roughness.get(), 0.28);
}
}
impl CoercionRules for Stroke {
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Color(color) => Stroke {
color: Property::new(*color),
width: Property::new(Size::Pixels(1.into())),
cap: Property::new(StrokeCap::default()),
join: Property::new(StrokeJoin::default()),
},
PaxValue::Object(map) => {
let mut color = None;
let mut width = None;
let mut cap = None;
let mut join = None;
for (key, value) in map {
match key.as_str() {
"color" => color = Some(Color::try_coerce(value)?),
"width" => width = Some(Size::try_coerce(value)?),
"cap" => cap = Some(StrokeCap::try_coerce(value)?),
"join" => join = Some(StrokeJoin::try_coerce(value)?),
_ => {}
}
}
let color =
Property::new(color.ok_or_else(|| {
"failed to convert to Stroke: missing `color`".to_string()
})?);
let width =
Property::new(width.ok_or_else(|| {
"failed to convert to Stroke: missing `width`".to_string()
})?);
let cap = Property::new(cap.unwrap_or_default());
let join = Property::new(join.unwrap_or_default());
Stroke {
color,
width,
cap,
join,
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
Stroke::try_coerce(o)?
} else {
return Err(format!("failed to convert to Stroke"));
}
}
_ => return Err(format!("failed to convert to Stroke")),
})
}
}
impl CoercionRules for StrokeCap {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
if !args.is_empty() {
return Err(format!(
"failed to coerce StrokeCap: expected no enum args, got {:?}",
args
));
}
match variant.as_str() {
"Butt" => Ok(StrokeCap::Butt),
"Round" => Ok(StrokeCap::Round),
"Square" => Ok(StrokeCap::Square),
_ => Err(format!(
"failed to coerce StrokeCap: unknown enum variant {:?}",
variant
)),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
StrokeCap::try_coerce(o)
} else {
Err("failed to coerce StrokeCap".to_string())
}
}
_ => Err(format!("failed to coerce StrokeCap")),
}
}
}
impl CoercionRules for PathSmoothing {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
if !args.is_empty() {
return Err(format!(
"failed to coerce PathSmoothing: expected no enum args, got {:?}",
args
));
}
match variant.as_str() {
"None" => Ok(PathSmoothing::None),
"Light" => Ok(PathSmoothing::Light),
"Strong" => Ok(PathSmoothing::Strong),
_ => Err(format!(
"failed to coerce PathSmoothing: unknown enum variant {:?}",
variant
)),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
PathSmoothing::try_coerce(o)
} else {
Err("failed to coerce PathSmoothing".to_string())
}
}
_ => Err(format!("failed to coerce PathSmoothing")),
}
}
}
impl CoercionRules for StrokeJoin {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
if !args.is_empty() {
return Err(format!(
"failed to coerce StrokeJoin: expected no enum args, got {:?}",
args
));
}
match variant.as_str() {
"Miter" => Ok(StrokeJoin::Miter),
"Round" => Ok(StrokeJoin::Round),
"Bevel" => Ok(StrokeJoin::Bevel),
_ => Err(format!(
"failed to coerce StrokeJoin: unknown enum variant {:?}",
variant
)),
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
StrokeJoin::try_coerce(o)
} else {
Err("failed to coerce StrokeJoin".to_string())
}
}
_ => Err(format!("failed to coerce StrokeJoin")),
}
}
}
impl CoercionRules for ColorChannel {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(match value {
PaxValue::Rotation(rot) => ColorChannel::Rotation(rot),
PaxValue::Percent(perc) => ColorChannel::Percent(perc.0),
PaxValue::Numeric(num) => ColorChannel::Integer(num.to_int().clamp(0, 255) as u8),
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
match variant.as_str() {
"Rotation" => {
let rot = Rotation::try_coerce(args.into_iter().next().unwrap())?;
ColorChannel::Rotation(rot)
}
"Integer" => {
let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
ColorChannel::Integer(num.to_int().clamp(0, 255) as u8)
}
"Percent" => {
let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
ColorChannel::Percent(num)
}
_ => {
return Err(format!(
"failed to convert to ColorChannel: unknown variant {:?}",
variant
))
}
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
ColorChannel::try_coerce(o)?
} else {
return Err(format!("failed to convert to ColorChannel"));
}
}
_ => return Err(format!("failed to convert to ColorChannel")),
})
}
}
impl CoercionRules for Opacity {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(match value {
PaxValue::Percent(perc) => Opacity::Percent(perc.0),
PaxValue::Numeric(num) => Opacity::Alpha(num),
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
match variant.as_str() {
"Alpha" | "Numeric" => {
let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
Opacity::Alpha(num)
}
"Percent" => {
let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
Opacity::Percent(num)
}
_ => {
return Err(format!(
"failed to convert to Opacity: unknown variant {:?}",
variant
))
}
}
}
PaxValue::Option(o) => {
if let Some(o) = *o {
Opacity::try_coerce(o)?
} else {
return Err("failed to convert to Opacity".to_string());
}
}
_ => return Err("failed to convert to Opacity".to_string()),
})
}
}
impl CoercionRules for UnitValue {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(match value {
PaxValue::Percent(percent) => UnitValue::Percent(percent.0),
PaxValue::Numeric(num) => UnitValue::Unitless(num),
PaxValue::Enum(contents) => {
let (_, variant, args) = *contents;
let mut args = args.into_iter();
match variant.as_str() {
"Unitless" | "Numeric" => {
UnitValue::Unitless(Numeric::try_coerce(args.next().unwrap())?)
}
"Percent" => UnitValue::Percent(Numeric::try_coerce(args.next().unwrap())?),
_ => {
return Err(format!(
"failed to convert to UnitValue: unknown variant {:?}",
variant
))
}
}
}
PaxValue::Option(mut opt) => {
if let Some(value) = opt.take() {
UnitValue::try_coerce(value)?
} else {
return Err("failed to convert to UnitValue".to_string());
}
}
_ => return Err("failed to convert to UnitValue".to_string()),
})
}
}
impl CoercionRules for Percent {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Percent(p) => p,
PaxValue::Numeric(v) => Percent(v),
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
Percent::try_coerce(p)?
} else {
return Err(format!("None can't be coerced into a Percent"));
}
}
_ => return Err(format!("{:?} can't be coerced into a Percent", pax_value)),
})
}
}
impl CoercionRules for Size {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Size(size) => size,
PaxValue::Percent(p) => Size::Percent(p.0),
PaxValue::Numeric(v) => Size::Pixels(v),
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
Size::try_coerce(p)?
} else {
return Err(format!("None can't be coerced into a Size"));
}
}
_ => return Err(format!("{:?} can't be coerced into a Size", pax_value)),
})
}
}
impl CoercionRules for String {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::String(s) => s,
PaxValue::Numeric(n) => {
if n.is_float() {
n.to_float().to_string()
} else {
n.to_int().to_string()
}
}
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
String::try_coerce(p)?
} else {
return Err(format!("None can't be coerced into a String"));
}
}
_ => return Err(format!("{:?} can't be coerced into a String", pax_value)),
})
}
}
impl CoercionRules for Rotation {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Rotation(r) => r,
PaxValue::Numeric(n) => Rotation::Degrees(n),
PaxValue::Percent(p) => Rotation::Percent(p.0),
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
Rotation::try_coerce(p)?
} else {
return Err(format!("None can't be coerced into a Rotation"));
}
}
_ => return Err(format!("{:?} can't be coerced into a Rotation", pax_value)),
})
}
}
impl CoercionRules for Duration {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Duration(duration) => duration,
PaxValue::Numeric(value) => Duration::Frames(value),
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
Duration::try_coerce(p)?
} else {
return Err(format!("None can't be coerced into a Duration"));
}
}
_ => return Err(format!("{:?} can't be coerced into a Duration", pax_value)),
})
}
}
impl CoercionRules for Numeric {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
Ok(match pax_value {
PaxValue::Bool(b) => (b as i32).into(),
PaxValue::Numeric(n) => n.into(),
PaxValue::Size(n) => n.into(),
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
Numeric::try_coerce(p)?
} else {
return Err(format!("None can't be coerced into a Numeric"));
}
}
_ => return Err(format!("{:?} can't be coerced into a Numeric", pax_value)),
})
}
}
impl<T: CoercionRules> CoercionRules for Vec<T> {
fn is_identity_roundtrip() -> bool {
is_typed_binding_safe::<T>()
}
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Vec(vec) => {
let res: Result<Vec<T>, _> = vec.into_iter().map(|v| T::try_coerce(v)).collect();
res.map_err(|e| format!("couldn't coerce vec, element {:?}", e))
}
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
Vec::<T>::try_coerce(p)
} else {
return Err(format!("None can't be coerced into a Vec"));
}
}
v => Err(format!(
"{:?} can't be coerced into {:?}",
v,
std::any::type_name::<Vec<T>>(),
)),
}
}
}
impl<T1: CoercionRules, T2: CoercionRules> CoercionRules for (T1, T2) {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Vec(vec) => {
let mut itr = vec.into_iter();
let res: Result<T1, _> = T1::try_coerce(itr.next().unwrap());
let res2: Result<T2, _> = T2::try_coerce(itr.next().unwrap());
res.and_then(|v1| res2.map(|v2| (v1, v2)))
}
PaxValue::Option(opt) => {
if let Some(p) = *opt {
<(T1, T2)>::try_coerce(p)
} else {
return Err(format!("None can't be coerced into a Vec"));
}
}
v => Err(format!(
"{:?} can't be coerced into {:?}",
v,
std::any::type_name::<(T1, T2)>(),
)),
}
}
}
impl<T: CoercionRules> CoercionRules for Option<T> {
fn is_identity_roundtrip() -> bool {
is_typed_binding_safe::<T>()
}
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Option(opt) => {
let res: Result<Option<T>, _> = opt.map(|v| T::try_coerce(v)).transpose();
res.map_err(|e| format!("couldn't coerce option, element {:?}", e))
}
v => Some(T::try_coerce(v)).transpose(),
}
}
}
impl<T: CoercionRules> CoercionRules for Range<T> {
fn is_identity_roundtrip() -> bool {
is_typed_binding_safe::<T>()
}
fn try_coerce(value: PaxValue) -> Result<Self, String> {
match value {
PaxValue::Range(start, end) => {
let start = T::try_coerce(*start)?;
let end = T::try_coerce(*end)?;
Ok(start..end)
}
PaxValue::Option(mut opt) => {
if let Some(p) = opt.take() {
Range::<T>::try_coerce(p)
} else {
return Err(format!("None can't be coerced into a Range"));
}
}
v => Err(format!(
"{:?} can't be coerced into {:?}",
v,
std::any::type_name::<Range<T>>(),
)),
}
}
}
impl CoercionRules for PaxValue {
fn is_identity_roundtrip() -> bool {
true
}
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(value)
}
}
impl<T: CoercionRules> CoercionRules for Box<T> {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(Box::new(T::try_coerce(value)?))
}
}
impl CoercionRules for Transform2D {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(match value {
PaxValue::Option(opt) => {
if let Some(t) = *opt {
Transform2D::try_coerce(t)?
} else {
return Err(format!("None can't be coerced into a Transform2D"));
}
}
PaxValue::Object(map) => {
let [previous, rotate, translate, anchor, scale, skew] = extract_options(
["previous", "rotate", "translate", "anchor", "scale", "skew"],
map,
)
.map_err(|e| format!("failed to convert to Transform2D: {e}"))?;
let previous = Option::<Box<Transform2D>>::try_coerce(previous)?;
let rotate = Option::<Rotation>::try_coerce(rotate)?;
let translate = match translate {
PaxValue::Option(opt) => {
if let Some(t) = *opt {
let t = Vec::<Size>::try_coerce(t)?;
if t.len() != 2 {
return Err(format!(
"expected 2 elements in translate, got {:?}",
t.len()
));
}
Some([t[0], t[1]])
} else {
None
}
}
_ => return Err(format!("translate can't be coerced into a Transform2D",)),
};
let anchor = match anchor {
PaxValue::Option(opt) => {
if let Some(a) = *opt {
let a = Vec::<Size>::try_coerce(a)?;
if a.len() != 2 {
return Err(format!(
"expected 2 elements in anchor, got {:?}",
a.len()
));
}
Some([a[0], a[1]])
} else {
None
}
}
_ => return Err(format!("anchor can't be coerced into a Transform2D",)),
};
let scale = match scale {
PaxValue::Option(opt) => {
if let Some(s) = *opt {
let s = Vec::<Size>::try_coerce(s)?;
if s.len() != 2 {
return Err(format!(
"expected 2 elements in scale, got {:?}",
s.len()
));
}
Some([s[0], s[1]])
} else {
None
}
}
_ => return Err(format!("scale can't be coerced into a Transform2D",)),
};
let skew = match skew {
PaxValue::Option(opt) => {
if let Some(s) = *opt {
let s = Vec::<Rotation>::try_coerce(s)?;
if s.len() != 2 {
return Err(format!(
"expected 2 elements in skew, got {:?}",
s.len()
));
}
Some([s[0], s[1]])
} else {
None
}
}
_ => return Err(format!("skew can't be coerced into a Transform2D",)),
};
Transform2D {
previous,
rotate,
translate,
anchor,
scale,
skew,
}
}
_ => return Err(format!("{:?} can't be coerced into a Transform2D", value)),
})
}
}
impl CoercionRules for Transform2 {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(match value {
PaxValue::Option(mut opt) => {
if let Some(t) = opt.take() {
Transform2::try_coerce(t)?
} else {
return Err(format!("None can't be coerced into a Transform2"));
}
}
PaxValue::Object(map) => {
let m = Vec::<f64>::try_coerce(
map.into_iter()
.find_map(|(n, v)| (n == "m").then_some(v))
.unwrap(),
)?;
if m.len() != 6 {
return Err(format!("expected 6 elements in coeffs, got {:?}", m.len()));
}
Transform2::new([m[0], m[1], m[2], m[3], m[4], m[5]])
}
_ => return Err(format!("{:?} can't be coerced into a Transform2", value)),
})
}
}
impl CoercionRules for Vector2 {
fn try_coerce(value: PaxValue) -> Result<Self, String> {
Ok(match value {
PaxValue::Option(mut opt) => {
if let Some(t) = opt.take() {
Vector2::try_coerce(t)?
} else {
return Err(format!("None can't be coerced into a Vector2"));
}
}
PaxValue::Object(map) => {
let [x, y] = extract_options(["x", "y"], map)
.map_err(|e| format!("failed to convert to Vector2: {e}"))?;
let x = f64::try_coerce(x)?;
let y = f64::try_coerce(y)?;
Vector2::new(x, y)
}
_ => return Err(format!("{:?} can't be coerced into a Vector2", value)),
})
}
}
pub fn extract_options<T, const N: usize>(
keys: [&'static str; N],
vec: Vec<(String, T)>,
) -> Result<[T; N], String> {
let mut intermediate: [Option<T>; N] = std::array::from_fn(|_| None);
for (k, v) in vec {
if let Some(pos) = keys.iter().position(|&key| k == key) {
intermediate[pos] = Some(v);
}
}
let result: Result<[T; N], String> = intermediate
.into_iter()
.enumerate()
.map(|(i, opt)| opt.ok_or_else(|| format!("missing field: {}", keys[i])))
.collect::<Result<Vec<_>, _>>()?
.try_into()
.map_err(|_| "Internal error converting vec to array".to_string());
result
}