1use std::ops::Range;
6
7use crate::{
8 impl_default_coercion_rule,
9 math::{Transform2, Vector2},
10 Color, ColorChannel, Depth, Duration, Fill, GradientStop, LayoutRole, LightShape,
11 LinearGradient, Material, MaterialParams, MeshGradient, MeshPoint, Numeric, Opacity, Paint,
12 PathElement, PathSmoothing, PaxValue, Percent, Property, RadialGradient, Rotation, Size,
13 Stroke, StrokeCap, StrokeJoin, Transform2D, UnitValue, Vector3,
14};
15
16impl_default_coercion_rule!(bool, PaxValue::Bool, true);
19
20impl_default_coercion_rule!(u8, PaxValue::Numeric, true);
21impl_default_coercion_rule!(u16, PaxValue::Numeric, true);
22impl_default_coercion_rule!(u32, PaxValue::Numeric, true);
23impl_default_coercion_rule!(u64, PaxValue::Numeric, true);
24
25impl_default_coercion_rule!(i8, PaxValue::Numeric, true);
26impl_default_coercion_rule!(i16, PaxValue::Numeric, true);
27impl_default_coercion_rule!(i32, PaxValue::Numeric, true);
28impl_default_coercion_rule!(i64, PaxValue::Numeric, true);
29
30impl_default_coercion_rule!(f32, PaxValue::Numeric, true);
31impl_default_coercion_rule!(f64, PaxValue::Numeric, true);
32
33impl_default_coercion_rule!(isize, PaxValue::Numeric, true);
34impl_default_coercion_rule!(usize, PaxValue::Numeric, true);
35
36impl CoercionRules for LayoutRole {
37 fn try_coerce(value: PaxValue) -> Result<Self, String> {
38 match value {
39 PaxValue::Enum(contents) => {
40 let (_, variant, args) = *contents;
41 if !args.is_empty() {
42 return Err(format!(
43 "failed to coerce LayoutRole: expected no enum args, got {:?}",
44 args
45 ));
46 }
47 match variant.as_str() {
48 "Default" => Ok(LayoutRole::Default),
49 "Breakout" => Ok(LayoutRole::Breakout),
50 _ => Err(format!(
51 "failed to coerce LayoutRole: unknown enum variant {:?}",
52 variant
53 )),
54 }
55 }
56 PaxValue::Option(o) => {
57 if let Some(o) = *o {
58 LayoutRole::try_coerce(o)
59 } else {
60 Err("failed to coerce LayoutRole".to_string())
61 }
62 }
63 _ => Err("failed to coerce LayoutRole".to_string()),
64 }
65 }
66}
67
68pub trait CoercionRules
73where
74 Self: Sized + 'static,
75{
76 fn try_coerce(value: PaxValue) -> Result<Self, String>;
78
79 fn allows_singleton_stack() -> bool {
82 false
83 }
84
85 fn is_identity_roundtrip() -> bool {
94 false
95 }
96}
97
98pub fn is_typed_binding_safe<T: CoercionRules>() -> bool {
102 use std::{any::TypeId, cell::RefCell, collections::HashSet};
103 thread_local! {
104 static CHECKING: RefCell<HashSet<TypeId>> = RefCell::new(HashSet::new());
105 }
106 struct Check(TypeId);
107 impl Drop for Check {
108 fn drop(&mut self) {
109 CHECKING.with(|checking| checking.borrow_mut().remove(&self.0));
110 }
111 }
112 let id = TypeId::of::<T>();
113 if !CHECKING.with(|checking| checking.borrow_mut().insert(id)) {
114 return false;
115 }
116 let _check = Check(id);
117 T::is_identity_roundtrip()
118}
119
120impl CoercionRules for Color {
163 fn is_identity_roundtrip() -> bool {
164 true
165 }
166
167 fn try_coerce(value: PaxValue) -> Result<Self, String> {
168 match value {
169 PaxValue::Color(color) => Ok(*color),
170 PaxValue::Enum(contents) => {
171 let (name, variant, mut args) = *contents;
172 if name != "Color" {
173 return Err(format!(
174 "{:?} can't be coerced into a Color",
175 PaxValue::Enum(Box::new((name, variant, args)))
176 ));
177 }
178 match variant.as_str() {
179 "rgb" => Ok(Color::rgb(
180 ColorChannel::try_coerce(args.remove(0))?,
181 ColorChannel::try_coerce(args.remove(0))?,
182 ColorChannel::try_coerce(args.remove(0))?,
183 )),
184 "rgba" => Ok(Color::rgba(
185 ColorChannel::try_coerce(args.remove(0))?,
186 ColorChannel::try_coerce(args.remove(0))?,
187 ColorChannel::try_coerce(args.remove(0))?,
188 ColorChannel::try_coerce(args.remove(0))?,
189 )),
190 "hsl" => Ok(Color::hsl(
191 Rotation::try_coerce(args.remove(0))?,
192 ColorChannel::try_coerce(args.remove(0))?,
193 ColorChannel::try_coerce(args.remove(0))?,
194 )),
195 "hsla" => Ok(Color::hsla(
196 Rotation::try_coerce(args.remove(0))?,
197 ColorChannel::try_coerce(args.remove(0))?,
198 ColorChannel::try_coerce(args.remove(0))?,
199 ColorChannel::try_coerce(args.remove(0))?,
200 )),
201 "SLATE" => Ok(Color::SLATE),
202 "GRAY" => Ok(Color::GRAY),
203 "ZINC" => Ok(Color::ZINC),
204 "NEUTRAL" => Ok(Color::NEUTRAL),
205 "STONE" => Ok(Color::STONE),
206 "RED" => Ok(Color::RED),
207 "ORANGE" => Ok(Color::ORANGE),
208 "AMBER" => Ok(Color::AMBER),
209 "YELLOW" => Ok(Color::YELLOW),
210 "LIME" => Ok(Color::LIME),
211 "GREEN" => Ok(Color::GREEN),
212 "EMERALD" => Ok(Color::EMERALD),
213 "TEAL" => Ok(Color::TEAL),
214 "CYAN" => Ok(Color::CYAN),
215 "SKY" => Ok(Color::SKY),
216 "BLUE" => Ok(Color::BLUE),
217 "INDIGO" => Ok(Color::INDIGO),
218 "VIOLET" => Ok(Color::VIOLET),
219 "PURPLE" => Ok(Color::PURPLE),
220 "FUCHSIA" => Ok(Color::FUCHSIA),
221 "PINK" => Ok(Color::PINK),
222 "ROSE" => Ok(Color::ROSE),
223 "BLACK" => Ok(Color::BLACK),
224 "WHITE" => Ok(Color::WHITE),
225 "TRANSPARENT" => Ok(Color::TRANSPARENT),
226 "NONE" => Ok(Color::NONE),
227 _ => Err(format!(
228 "failed to convert to Color: unknown variant {:?}",
229 variant
230 )),
231 }
232 }
233 PaxValue::Option(o) => {
234 if let Some(o) = *o {
235 Color::try_coerce(o)
236 } else {
237 Err("failed to convert to Color".to_string())
238 }
239 }
240 _ => return Err(format!("{:?} can't be coerced into a Color", value)),
241 }
242 }
243}
244
245impl CoercionRules for PathElement {
246 fn is_identity_roundtrip() -> bool {
247 true
248 }
249
250 fn try_coerce(value: PaxValue) -> Result<Self, String> {
251 match value {
252 PaxValue::PathElement(path_elem) => Ok(*path_elem),
253 PaxValue::Enum(contents) => {
255 let (name, variant, values) = *contents;
256 if name == "PathElement" {
257 let mut values_itr = values.into_iter();
258 match variant.as_str() {
259 "Line" => Ok(PathElement::Line),
260 "Close" => Ok(PathElement::Close),
261 "Empty" => Ok(PathElement::Empty),
262 "Point" => Ok(PathElement::Point(
263 Size::try_coerce(values_itr.next().unwrap())?,
264 Size::try_coerce(values_itr.next().unwrap())?,
265 )),
266 "Quadratic" => Ok(PathElement::Quadratic(
267 Size::try_coerce(values_itr.next().unwrap())?,
268 Size::try_coerce(values_itr.next().unwrap())?,
269 )),
270 "Cubic" => Ok(PathElement::Cubic(
271 Size::try_coerce(values_itr.next().unwrap())?,
272 Size::try_coerce(values_itr.next().unwrap())?,
273 Size::try_coerce(values_itr.next().unwrap())?,
274 Size::try_coerce(values_itr.next().unwrap())?,
275 )),
276 _ => {
277 return Err(format!(
278 "failed to coerce PathElement: unknown enum variant: {:?}",
279 variant
280 ))
281 }
282 }
283 } else {
284 return Err(format!(
285 "failed to coerce PathElement: enum name doesn't match"
286 ));
287 }
288 }
289 _ => return Err(format!("failed to coerce PathElement: PaxValue not a path")),
290 }
291 }
292}
293
294impl CoercionRules for Paint {
298 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
299 Ok(match pax_value {
300 PaxValue::Color(color) => Paint::Solid(*color),
301 PaxValue::Enum(contents) => {
302 let (scope, variant, args) = *contents;
303 if scope == "Color" {
304 return Color::try_coerce(PaxValue::Enum(Box::new((scope, variant, args))))
305 .map(Paint::Solid);
306 }
307 if scope == "Fill" {
308 return Err("Fill paint constructors moved to Paint; Fill now describes an appearance layer".into());
309 }
310 if scope != "Paint" {
311 return Err(format!("expected Paint, received {scope}::{variant}"));
312 }
313 if matches!(
314 variant.as_str(),
315 "Solid" | "LinearGradient" | "RadialGradient" | "MeshGradient"
316 ) && args.len() != 1
317 {
318 return Err(format!("Paint::{variant} requires exactly one argument"));
319 }
320 match variant.as_str() {
321 "Blend" => {
322 if args.len() != 1 {
323 return Err("Paint::Blend requires one list of weighted paints".into());
324 }
325 Paint::blend(Vec::<(Paint, f64)>::try_coerce(
326 args.into_iter().next().unwrap(),
327 )?)
328 }
329 "Solid" => {
330 let color = Color::try_coerce(args.into_iter().next().unwrap())?;
331 Paint::Solid(color)
332 }
333 "MeshGradient" => Paint::MeshGradient(MeshGradient::try_coerce(
334 args.into_iter().next().unwrap(),
335 )?),
336 "LinearGradient" => {
337 let gradient =
338 LinearGradient::try_coerce(args.into_iter().next().unwrap())?;
339 Paint::LinearGradient(gradient)
340 }
341 "linearGradient" => Paint::LinearGradient(parse_linear_gradient_args(args)?),
342 "RadialGradient" => {
343 let gradient =
344 RadialGradient::try_coerce(args.into_iter().next().unwrap())?;
345 Paint::RadialGradient(gradient)
346 }
347 "radialGradient" => Paint::RadialGradient(parse_radial_gradient_args(args)?),
348 _ => {
349 return Err(format!(
350 "failed to coerce Paint: unknown enum variant {:?}",
351 variant
352 ))
353 }
354 }
355 }
356 PaxValue::Option(o) => {
357 if let Some(o) = *o {
358 Paint::try_coerce(o)?
359 } else {
360 return Err(format!("failed to coerce Paint: can't coerce None"));
361 }
362 }
363 _ => return Err(format!("{:?} can't be coerced into a Paint", pax_value)),
364 })
365 }
366}
367
368impl CoercionRules for MeshPoint {
369 fn try_coerce(value: PaxValue) -> Result<Self, String> {
370 match value {
371 PaxValue::Object(fields) => {
372 if fields
373 .iter()
374 .any(|(key, _)| key != "position" && key != "color")
375 {
376 return Err("mesh anchors accept only `position` and `color`".into());
377 }
378 let [position, color] = extract_options(["position", "color"], fields)?;
379 let coordinates = Vec::<Size>::try_coerce(position)?;
380 let [x, y]: [Size; 2] = coordinates
381 .try_into()
382 .map_err(|_| "mesh anchor position requires exactly two coordinates")?;
383 Ok(Self {
384 position: (x, y),
385 color: Color::try_coerce(color)?,
386 })
387 }
388 PaxValue::Option(value) => Self::try_coerce(value.ok_or("mesh anchor cannot be None")?),
389 _ => Err("mesh anchor requires {position: [x, y], color: ...}".into()),
390 }
391 }
392}
393
394impl CoercionRules for MeshGradient {
395 fn try_coerce(value: PaxValue) -> Result<Self, String> {
396 match value {
397 PaxValue::Object(fields) => {
398 if fields.iter().any(|(key, _)| key != "rows") {
399 return Err("mesh gradients accept only `rows`".into());
400 }
401 let [rows] = extract_options(["rows"], fields)?;
402 Ok(Self {
403 rows: Vec::<Vec<MeshPoint>>::try_coerce(rows)?,
404 })
405 }
406 PaxValue::Option(value) => {
407 Self::try_coerce(value.ok_or("mesh gradient cannot be None")?)
408 }
409 _ => Err("mesh gradient requires {rows: ...}".into()),
410 }
411 }
412}
413
414impl CoercionRules for LinearGradient {
415 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
416 Ok(match pax_value {
417 PaxValue::Object(map) => {
418 let [start, end, stops] = extract_options(["start", "end", "stops"], map)
419 .map_err(|e| format!("failed to convert to LinearGradient: {e}"))?;
420 let (s1, s2) = match start {
421 PaxValue::Vec(vec) => {
422 let mut itr = vec.into_iter();
423 let s1 = Size::try_coerce(itr.next().unwrap())?;
424 let s2 = Size::try_coerce(itr.next().unwrap())?;
425 (s1, s2)
426 }
427 _ => return Err(format!("failed to coerce LinearGradient")),
428 };
429
430 let (e1, e2) = match end {
431 PaxValue::Vec(vec) => {
432 let mut itr = vec.into_iter();
433 let e1 = Size::try_coerce(itr.next().unwrap())?;
434 let e2 = Size::try_coerce(itr.next().unwrap())?;
435 (e1, e2)
436 }
437 _ => return Err(format!("failed to coerce LinearGradient")),
438 };
439 let stops = Vec::<GradientStop>::try_coerce(stops)?;
440 LinearGradient {
441 start: (s1, s2),
442 end: (e1, e2),
443 stops,
444 }
445 }
446 PaxValue::Enum(contents) => {
447 let (_, variant, args) = *contents;
448 match variant.as_str() {
449 "linearGradient" => parse_linear_gradient_args(args)?,
450 _ => return Err(format!("failed to coerce LinearGradient")),
451 }
452 }
453 PaxValue::Option(o) => {
454 if let Some(o) = *o {
455 LinearGradient::try_coerce(o)?
456 } else {
457 return Err(format!("failed to coerce LinearGradient"));
458 }
459 }
460 _ => {
461 return Err(format!(
462 "{:?} can't be coerced into a LinearGradient",
463 pax_value
464 ))
465 }
466 })
467 }
468}
469
470impl CoercionRules for RadialGradient {
471 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
472 Ok(match pax_value {
473 PaxValue::Object(map) => {
474 let [start, end, stops, radius] =
475 extract_options(["start", "end", "stops", "radius"], map)
476 .map_err(|e| format!("failed to convert to RadialGradient: {e}"))?;
477 let (s1, s2) = match start {
478 PaxValue::Vec(vec) => {
479 let mut itr = vec.into_iter();
480 let s1 = Size::try_coerce(itr.next().unwrap())?;
481 let s2 = Size::try_coerce(itr.next().unwrap())?;
482 (s1, s2)
483 }
484 _ => {
485 return Err(format!("failed to coerce RadialGradient"));
486 }
487 };
488
489 let (e1, e2) = match end {
490 PaxValue::Vec(vec) => {
491 let mut itr = vec.into_iter();
492 let e1 = Size::try_coerce(itr.next().unwrap())?;
493 let e2 = Size::try_coerce(itr.next().unwrap())?;
494 (e1, e2)
495 }
496 _ => {
497 return Err(format!("failed to coerce RadialGradient"));
498 }
499 };
500 let radius = match radius {
501 PaxValue::Numeric(n) => n.to_float(),
502 _ => {
503 return Err(format!("failed to coerce RadialGradient"));
504 }
505 };
506 let stops = Vec::<GradientStop>::try_coerce(stops)?;
507 RadialGradient {
508 start: (s1, s2),
509 end: (e1, e2),
510 radius,
511 stops,
512 }
513 }
514 PaxValue::Enum(contents) => {
515 let (_, variant, args) = *contents;
516 match variant.as_str() {
517 "radialGradient" => parse_radial_gradient_args(args)?,
518 _ => return Err(format!("failed to coerce RadialGradient")),
519 }
520 }
521 PaxValue::Option(o) => {
522 if let Some(o) = *o {
523 RadialGradient::try_coerce(o)?
524 } else {
525 return Err(format!("failed to coerce RadialGradient"));
526 }
527 }
528 _ => {
529 return Err(format!("failed to coerce RadialGradient"));
530 }
531 })
532 }
533}
534
535impl CoercionRules for GradientStop {
536 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
537 Ok(match pax_value {
538 PaxValue::Object(map) => {
539 let [position, color] = extract_options(["position", "color"], map)
540 .map_err(|e| format!("failed to convert to GradientStop: {e}"))?;
541 let position = Size::try_coerce(position)?;
542 let color = Color::try_coerce(color)?;
543 GradientStop { position, color }
544 }
545 PaxValue::Enum(contents) => {
546 let (_, variant, args) = *contents;
547 match variant.as_str() {
548 "get" => {
549 let mut args = args.into_iter();
550 let color = Color::try_coerce(
551 args.next()
552 .ok_or_else(|| "failed to convert to GradientStop".to_string())?,
553 )?;
554 let position = Size::try_coerce(
555 args.next()
556 .ok_or_else(|| "failed to convert to GradientStop".to_string())?,
557 )?;
558 GradientStop { position, color }
559 }
560 _ => return Err(format!("failed to convert to GradientStop")),
561 }
562 }
563 PaxValue::Option(o) => {
564 if let Some(o) = *o {
565 GradientStop::try_coerce(o)?
566 } else {
567 return Err(format!("failed to convert to GradientStop"));
568 }
569 }
570 _ => {
571 return Err(format!("failed to convert to GradientStop"));
572 }
573 })
574 }
575}
576
577impl CoercionRules for Depth {
578 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
579 Ok(match pax_value {
580 PaxValue::Size(Size::Pixels(value)) => Depth(value),
581 PaxValue::Size(Size::Percent(_)) | PaxValue::Percent(_) => {
582 return Err("failed to coerce Depth: percent depth is not supported".to_string());
583 }
584 PaxValue::Size(Size::Combined(_, _)) => {
585 return Err("failed to coerce Depth: combined depth is not supported".to_string());
586 }
587 PaxValue::Numeric(value) => Depth(value),
588 PaxValue::Option(o) => {
589 if let Some(o) = *o {
590 Depth::try_coerce(o)?
591 } else {
592 return Err("failed to coerce Depth".to_string());
593 }
594 }
595 _ => return Err(format!("{:?} can't be coerced into a Depth", pax_value)),
596 })
597 }
598}
599
600impl CoercionRules for Vector3 {
601 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
602 Ok(match pax_value {
603 PaxValue::Object(map) => {
604 let [x, y, z] = extract_options(["x", "y", "z"], map)
605 .map_err(|e| format!("failed to convert to Vector3: {e}"))?;
606 Vector3 {
607 x: f64::try_coerce(x)?,
608 y: f64::try_coerce(y)?,
609 z: f64::try_coerce(z)?,
610 }
611 }
612 PaxValue::Vec(values) => {
613 if values.len() != 3 {
614 return Err("failed to coerce Vector3: expected three values".to_string());
615 }
616 let mut values = values.into_iter();
617 Vector3 {
618 x: f64::try_coerce(values.next().unwrap())?,
619 y: f64::try_coerce(values.next().unwrap())?,
620 z: f64::try_coerce(values.next().unwrap())?,
621 }
622 }
623 PaxValue::Enum(contents) => {
624 let (_, variant, values) = *contents;
625 match variant.as_str() {
626 "new" => {
627 if values.len() != 3 {
628 return Err("failed to coerce Vector3::new".to_string());
629 }
630 let mut values = values.into_iter();
631 Vector3 {
632 x: f64::try_coerce(values.next().unwrap())?,
633 y: f64::try_coerce(values.next().unwrap())?,
634 z: f64::try_coerce(values.next().unwrap())?,
635 }
636 }
637 _ => {
638 return Err(format!(
639 "failed to coerce Vector3: unknown enum variant {:?}",
640 variant
641 ))
642 }
643 }
644 }
645 PaxValue::Option(o) => {
646 if let Some(o) = *o {
647 Vector3::try_coerce(o)?
648 } else {
649 return Err("failed to coerce Vector3".to_string());
650 }
651 }
652 _ => return Err(format!("{:?} can't be coerced into a Vector3", pax_value)),
653 })
654 }
655}
656
657impl CoercionRules for LightShape {
658 fn try_coerce(value: PaxValue) -> Result<Self, String> {
659 match value {
660 PaxValue::Enum(contents) => {
661 let (_, variant, args) = *contents;
662 if !args.is_empty() {
663 return Err(format!(
664 "failed to coerce LightShape: expected no enum args, got {:?}",
665 args
666 ));
667 }
668 match variant.as_str() {
669 "Point" => Ok(LightShape::Point),
670 "Directional" => Ok(LightShape::Directional),
671 _ => Err(format!(
672 "failed to coerce LightShape: unknown enum variant {:?}",
673 variant
674 )),
675 }
676 }
677 PaxValue::Option(o) => {
678 if let Some(o) = *o {
679 LightShape::try_coerce(o)
680 } else {
681 Err("failed to coerce LightShape".to_string())
682 }
683 }
684 _ => Err("failed to coerce LightShape".to_string()),
685 }
686 }
687}
688
689impl CoercionRules for MaterialParams {
690 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
691 Ok(match pax_value {
692 PaxValue::Object(map) => {
693 let mut params = MaterialParams::default();
694 for (key, value) in map {
695 match key.as_str() {
696 "ambient" => params.ambient = Property::new(f64::try_coerce(value)?),
697 "diffuse" => params.diffuse = Property::new(f64::try_coerce(value)?),
698 "specular" => params.specular = Property::new(f64::try_coerce(value)?),
699 "roughness" => params.roughness = Property::new(f64::try_coerce(value)?),
700 "metallic" => params.metallic = Property::new(f64::try_coerce(value)?),
701 "emissive" => params.emissive = Property::new(Color::try_coerce(value)?),
702 "emissive_intensity" | "emissiveIntensity" => {
703 params.emissive_intensity = Property::new(f64::try_coerce(value)?)
704 }
705 _ => {}
706 }
707 }
708 params
709 }
710 PaxValue::Option(o) => {
711 if let Some(o) = *o {
712 MaterialParams::try_coerce(o)?
713 } else {
714 return Err("failed to coerce MaterialParams".to_string());
715 }
716 }
717 _ => {
718 return Err(format!(
719 "{:?} can't be coerced into MaterialParams",
720 pax_value
721 ))
722 }
723 })
724 }
725}
726
727impl CoercionRules for Material {
728 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
729 Ok(match pax_value {
730 PaxValue::Object(map) => {
731 Material::Lit(MaterialParams::try_coerce(PaxValue::Object(map))?)
732 }
733 PaxValue::Enum(contents) => {
734 let (_, variant, args) = *contents;
735 match variant.as_str() {
736 "Lit" | "custom" => {
737 let params = args
738 .into_iter()
739 .next()
740 .ok_or_else(|| "failed to coerce Material::custom".to_string())?;
741 Material::Lit(MaterialParams::try_coerce(params)?)
742 }
743 "Unlit" | "unlit" => Material::Unlit,
744 "Matte" | "matte" => Material::matte(),
745 "Glossy" | "glossy" => {
746 let specular = args
747 .into_iter()
748 .next()
749 .map(f64::try_coerce)
750 .transpose()?
751 .unwrap_or(0.28);
752 Material::glossy(specular)
753 }
754 "Metallic" | "metallic" => {
755 let metallic = args
756 .into_iter()
757 .next()
758 .map(f64::try_coerce)
759 .transpose()?
760 .unwrap_or(0.72);
761 Material::metallic(metallic)
762 }
763 "Emissive" | "emissive" => {
764 if args.len() != 2 {
765 return Err("failed to coerce Material::emissive".to_string());
766 }
767 let mut args = args.into_iter();
768 let color = Color::try_coerce(args.next().unwrap())?;
769 let intensity = f64::try_coerce(args.next().unwrap())?;
770 Material::emissive(color, intensity)
771 }
772 _ => {
773 return Err(format!(
774 "failed to coerce Material: unknown enum variant {:?}",
775 variant
776 ))
777 }
778 }
779 }
780 PaxValue::Option(o) => {
781 if let Some(o) = *o {
782 Material::try_coerce(o)?
783 } else {
784 return Err("failed to coerce Material".to_string());
785 }
786 }
787 _ => return Err(format!("{:?} can't be coerced into Material", pax_value)),
788 })
789 }
790}
791
792fn parse_gradient_point(value: PaxValue) -> Result<(Size, Size), String> {
793 match value {
794 PaxValue::Vec(vec) => {
795 let mut itr = vec.into_iter();
796 let x = Size::try_coerce(
797 itr.next()
798 .ok_or_else(|| "failed to coerce gradient point".to_string())?,
799 )?;
800 let y = Size::try_coerce(
801 itr.next()
802 .ok_or_else(|| "failed to coerce gradient point".to_string())?,
803 )?;
804 Ok((x, y))
805 }
806 _ => Err("failed to coerce gradient point".to_string()),
807 }
808}
809
810fn parse_linear_gradient_args(args: Vec<PaxValue>) -> Result<LinearGradient, String> {
811 if args.len() != 3 {
812 return Err("failed to coerce Paint::linearGradient".to_string());
813 }
814 let mut args = args.into_iter();
815 let start = parse_gradient_point(args.next().unwrap())?;
816 let end = parse_gradient_point(args.next().unwrap())?;
817 let stops = Vec::<GradientStop>::try_coerce(args.next().unwrap())?;
818 Ok(LinearGradient { start, end, stops })
819}
820
821fn parse_radial_gradient_args(args: Vec<PaxValue>) -> Result<RadialGradient, String> {
822 if args.len() != 4 {
823 return Err("failed to coerce Paint::radialGradient".to_string());
824 }
825 let mut args = args.into_iter();
826 let start = parse_gradient_point(args.next().unwrap())?;
827 let end = parse_gradient_point(args.next().unwrap())?;
828 let stops = Vec::<GradientStop>::try_coerce(args.next().unwrap())?;
829 let radius = Numeric::try_coerce(args.next().unwrap())?.to_float();
830 Ok(RadialGradient {
831 start,
832 end,
833 radius,
834 stops,
835 })
836}
837
838#[cfg(test)]
839mod tests {
840 use super::*;
841
842 fn pct(value: i64) -> PaxValue {
843 PaxValue::Percent(Percent(Numeric::I64(value)))
844 }
845
846 fn color(hex: &str) -> PaxValue {
847 PaxValue::Color(Box::new(Color::from_hex(hex)))
848 }
849
850 #[test]
851 fn coerces_color_helper_syntax() {
852 let pax_value = PaxValue::Enum(Box::new((
853 "Color".to_string(),
854 "rgba".to_string(),
855 vec![
856 PaxValue::Numeric(255.into()),
857 PaxValue::Numeric(224.into()),
858 PaxValue::Numeric(162.into()),
859 PaxValue::Numeric(255.into()),
860 ],
861 )));
862
863 let color = Color::try_coerce(pax_value).expect("rgba helper syntax should coerce");
864 assert_eq!(
865 color,
866 Color::rgba(
867 ColorChannel::Integer(255),
868 ColorChannel::Integer(224),
869 ColorChannel::Integer(162),
870 ColorChannel::Integer(255),
871 )
872 );
873 }
874
875 #[test]
876 fn coerces_fill_linear_gradient_helper_syntax() {
877 let pax_value = PaxValue::Enum(Box::new((
878 "Paint".to_string(),
879 "linearGradient".to_string(),
880 vec![
881 PaxValue::Vec(vec![pct(0), pct(50)]),
882 PaxValue::Vec(vec![pct(100), pct(50)]),
883 PaxValue::Vec(vec![
884 PaxValue::Enum(Box::new((
885 "GradientStop".to_string(),
886 "get".to_string(),
887 vec![color("ff00ffff"), pct(0)],
888 ))),
889 PaxValue::Enum(Box::new((
890 "GradientStop".to_string(),
891 "get".to_string(),
892 vec![color("ffffffff"), pct(100)],
893 ))),
894 ]),
895 ],
896 )));
897
898 let fill = Paint::try_coerce(pax_value).expect("helper syntax should coerce");
899 let Paint::LinearGradient(gradient) = fill else {
900 panic!("expected linear gradient");
901 };
902
903 assert_eq!(gradient.stops.len(), 2);
904 assert_eq!(gradient.start.0, Size::Percent(Numeric::I64(0)));
905 assert_eq!(gradient.end.0, Size::Percent(Numeric::I64(100)));
906 }
907
908 #[test]
909 fn coerces_gradient_stop_get_helper_syntax() {
910 let pax_value = PaxValue::Enum(Box::new((
911 "GradientStop".to_string(),
912 "get".to_string(),
913 vec![color("11223344"), pct(48)],
914 )));
915
916 let stop = GradientStop::try_coerce(pax_value).expect("helper syntax should coerce");
917 assert_eq!(stop.position, Size::Percent(Numeric::I64(48)));
918 assert_eq!(stop.color, Color::from_hex("11223344"));
919 }
920
921 #[test]
922 fn coerces_opacity_from_percent_literal() {
923 let opacity = Opacity::try_coerce(pct(50)).expect("percent opacity should coerce");
924 assert_eq!(opacity, Opacity::Percent(Numeric::I64(50)));
925 assert!((opacity.to_float_0_1() - 0.5).abs() < f64::EPSILON);
926 }
927
928 #[test]
929 fn coerces_opacity_from_numeric_literal() {
930 let opacity = Opacity::try_coerce(PaxValue::Numeric(Numeric::F64(0.5)))
931 .expect("numeric opacity should coerce");
932 assert_eq!(opacity, Opacity::Alpha(Numeric::F64(0.5)));
933 assert!((opacity.to_float_0_1() - 0.5).abs() < f64::EPSILON);
934 }
935
936 #[test]
937 fn coerces_depth_from_px_but_rejects_percent() {
938 let depth = Depth::try_coerce(PaxValue::Size(Size::Pixels(Numeric::F64(260.0))))
939 .expect("px depth should coerce");
940 assert_eq!(depth.to_float(), 260.0);
941
942 let err = Depth::try_coerce(pct(50)).expect_err("percent depth should fail");
943 assert!(err.contains("percent depth"));
944 }
945
946 #[test]
947 fn coerces_material_helper_syntax() {
948 let pax_value = PaxValue::Enum(Box::new((
949 "Material".to_string(),
950 "glossy".to_string(),
951 vec![PaxValue::Numeric(Numeric::F64(0.44))],
952 )));
953
954 let material = Material::try_coerce(pax_value).expect("material helper should coerce");
955 let Material::Lit(params) = material else {
956 panic!("expected lit material");
957 };
958 assert_eq!(params.specular.get(), 0.44);
959 assert_eq!(params.roughness.get(), 0.28);
960 }
961}
962
963fn layer_object(value: PaxValue, expected: &str) -> Result<PaxValue, String> {
964 match value {
965 PaxValue::Enum(contents) if contents.0 == "Fill" || contents.0 == "Stroke" => {
966 let (name, variant, mut args) = *contents;
967 if name != expected || variant != "__layer" || args.len() != 1 {
968 return Err(format!("expected {expected} layer, received {name}::{variant}; paint constructors now use Paint"));
969 }
970 Ok(args.remove(0))
971 }
972 PaxValue::Option(value) => match *value {
973 Some(value) => layer_object(value, expected),
974 None => Err(format!(
975 "expected {expected}, received None; use [] for no layers"
976 )),
977 },
978 value => Ok(value),
979 }
980}
981
982macro_rules! layer_coercion {
983 ($ty:ident, $($field:ident: $field_ty:ty),+) => {
984 impl CoercionRules for $ty {
985 fn allows_singleton_stack() -> bool { true }
986 fn try_coerce(value: PaxValue) -> Result<Self, String> {
987 match layer_object(value, stringify!($ty))? {
988 PaxValue::Object(fields) => {
989 let mut layer = Self::default();
990 for (name, value) in fields {
991 match name.as_str() {
992 $(stringify!($field) => {
993 layer.$field = Property::new(<$field_ty>::try_coerce(value)
994 .map_err(|e| format!("{}.{}: expected {}: {e}", stringify!($ty), name, stringify!($field_ty)))?);
995 },)+
996 "color" => return Err(format!("{}.color was renamed to paint", stringify!($ty))),
997 _ => return Err(format!("unknown {} field `{name}`", stringify!($ty))),
998 }
999 }
1000 layer.validate_layer()?;
1001 Ok(layer)
1002 }
1003 value => Paint::try_coerce(value).map(Self::from)
1004 .map_err(|e| format!("expected {} layer or Paint: {e}", stringify!($ty))),
1005 }
1006 }
1007 }
1008 }
1009}
1010layer_coercion!(Fill, paint: Paint, material: Material, opacity: Opacity);
1011layer_coercion!(Stroke, paint: Paint, material: Material, opacity: Opacity, width: Size, cap: StrokeCap, join: StrokeJoin);
1012
1013impl CoercionRules for StrokeCap {
1014 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1015 match value {
1016 PaxValue::Enum(contents) => {
1017 let (_, variant, args) = *contents;
1018 if !args.is_empty() {
1019 return Err(format!(
1020 "failed to coerce StrokeCap: expected no enum args, got {:?}",
1021 args
1022 ));
1023 }
1024 match variant.as_str() {
1025 "Butt" => Ok(StrokeCap::Butt),
1026 "Round" => Ok(StrokeCap::Round),
1027 "Square" => Ok(StrokeCap::Square),
1028 _ => Err(format!(
1029 "failed to coerce StrokeCap: unknown enum variant {:?}",
1030 variant
1031 )),
1032 }
1033 }
1034 PaxValue::Option(o) => {
1035 if let Some(o) = *o {
1036 StrokeCap::try_coerce(o)
1037 } else {
1038 Err("failed to coerce StrokeCap".to_string())
1039 }
1040 }
1041 _ => Err(format!("failed to coerce StrokeCap")),
1042 }
1043 }
1044}
1045
1046impl CoercionRules for PathSmoothing {
1047 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1048 match value {
1049 PaxValue::Enum(contents) => {
1050 let (_, variant, args) = *contents;
1051 if !args.is_empty() {
1052 return Err(format!(
1053 "failed to coerce PathSmoothing: expected no enum args, got {:?}",
1054 args
1055 ));
1056 }
1057 match variant.as_str() {
1058 "None" => Ok(PathSmoothing::None),
1059 "Light" => Ok(PathSmoothing::Light),
1060 "Strong" => Ok(PathSmoothing::Strong),
1061 _ => Err(format!(
1062 "failed to coerce PathSmoothing: unknown enum variant {:?}",
1063 variant
1064 )),
1065 }
1066 }
1067 PaxValue::Option(o) => {
1068 if let Some(o) = *o {
1069 PathSmoothing::try_coerce(o)
1070 } else {
1071 Err("failed to coerce PathSmoothing".to_string())
1072 }
1073 }
1074 _ => Err(format!("failed to coerce PathSmoothing")),
1075 }
1076 }
1077}
1078
1079impl CoercionRules for StrokeJoin {
1080 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1081 match value {
1082 PaxValue::Enum(contents) => {
1083 let (_, variant, args) = *contents;
1084 if !args.is_empty() {
1085 return Err(format!(
1086 "failed to coerce StrokeJoin: expected no enum args, got {:?}",
1087 args
1088 ));
1089 }
1090 match variant.as_str() {
1091 "Miter" => Ok(StrokeJoin::Miter),
1092 "Round" => Ok(StrokeJoin::Round),
1093 "Bevel" => Ok(StrokeJoin::Bevel),
1094 _ => Err(format!(
1095 "failed to coerce StrokeJoin: unknown enum variant {:?}",
1096 variant
1097 )),
1098 }
1099 }
1100 PaxValue::Option(o) => {
1101 if let Some(o) = *o {
1102 StrokeJoin::try_coerce(o)
1103 } else {
1104 Err("failed to coerce StrokeJoin".to_string())
1105 }
1106 }
1107 _ => Err(format!("failed to coerce StrokeJoin")),
1108 }
1109 }
1110}
1111
1112impl CoercionRules for ColorChannel {
1113 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1114 Ok(match value {
1115 PaxValue::Rotation(rot) => ColorChannel::Rotation(rot),
1116 PaxValue::Percent(perc) => ColorChannel::Percent(perc.0),
1117 PaxValue::Numeric(num) => ColorChannel::Integer(num.to_int().clamp(0, 255) as u8),
1118 PaxValue::Enum(contents) => {
1119 let (_, variant, args) = *contents;
1120 match variant.as_str() {
1121 "Rotation" => {
1122 let rot = Rotation::try_coerce(args.into_iter().next().unwrap())?;
1123 ColorChannel::Rotation(rot)
1124 }
1125 "Integer" => {
1126 let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1127 ColorChannel::Integer(num.to_int().clamp(0, 255) as u8)
1128 }
1129 "Percent" => {
1130 let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1131 ColorChannel::Percent(num)
1132 }
1133 _ => {
1134 return Err(format!(
1135 "failed to convert to ColorChannel: unknown variant {:?}",
1136 variant
1137 ))
1138 }
1139 }
1140 }
1141 PaxValue::Option(o) => {
1142 if let Some(o) = *o {
1143 ColorChannel::try_coerce(o)?
1144 } else {
1145 return Err(format!("failed to convert to ColorChannel"));
1146 }
1147 }
1148 _ => return Err(format!("failed to convert to ColorChannel")),
1149 })
1150 }
1151}
1152
1153impl CoercionRules for Opacity {
1154 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1155 Ok(match value {
1156 PaxValue::Percent(perc) => Opacity::Percent(perc.0),
1157 PaxValue::Numeric(num) => Opacity::Alpha(num),
1158 PaxValue::Enum(contents) => {
1159 let (_, variant, args) = *contents;
1160 match variant.as_str() {
1161 "Alpha" | "Numeric" => {
1162 let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1163 Opacity::Alpha(num)
1164 }
1165 "Percent" => {
1166 let num = Numeric::try_coerce(args.into_iter().next().unwrap())?;
1167 Opacity::Percent(num)
1168 }
1169 _ => {
1170 return Err(format!(
1171 "failed to convert to Opacity: unknown variant {:?}",
1172 variant
1173 ))
1174 }
1175 }
1176 }
1177 PaxValue::Option(o) => {
1178 if let Some(o) = *o {
1179 Opacity::try_coerce(o)?
1180 } else {
1181 return Err("failed to convert to Opacity".to_string());
1182 }
1183 }
1184 _ => return Err("failed to convert to Opacity".to_string()),
1185 })
1186 }
1187}
1188
1189impl CoercionRules for UnitValue {
1190 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1191 Ok(match value {
1192 PaxValue::Percent(percent) => UnitValue::Percent(percent.0),
1193 PaxValue::Numeric(num) => UnitValue::Unitless(num),
1194 PaxValue::Enum(contents) => {
1195 let (_, variant, args) = *contents;
1196 let mut args = args.into_iter();
1197 match variant.as_str() {
1198 "Unitless" | "Numeric" => {
1199 UnitValue::Unitless(Numeric::try_coerce(args.next().unwrap())?)
1200 }
1201 "Percent" => UnitValue::Percent(Numeric::try_coerce(args.next().unwrap())?),
1202 _ => {
1203 return Err(format!(
1204 "failed to convert to UnitValue: unknown variant {:?}",
1205 variant
1206 ))
1207 }
1208 }
1209 }
1210 PaxValue::Option(mut opt) => {
1211 if let Some(value) = opt.take() {
1212 UnitValue::try_coerce(value)?
1213 } else {
1214 return Err("failed to convert to UnitValue".to_string());
1215 }
1216 }
1217 _ => return Err("failed to convert to UnitValue".to_string()),
1218 })
1219 }
1220}
1221
1222impl CoercionRules for Percent {
1223 fn is_identity_roundtrip() -> bool {
1224 true
1225 }
1226
1227 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1228 Ok(match pax_value {
1229 PaxValue::Percent(p) => p,
1230 PaxValue::Numeric(v) => Percent(v),
1231 PaxValue::Option(mut opt) => {
1232 if let Some(p) = opt.take() {
1233 Percent::try_coerce(p)?
1234 } else {
1235 return Err(format!("None can't be coerced into a Percent"));
1236 }
1237 }
1238 _ => return Err(format!("{:?} can't be coerced into a Percent", pax_value)),
1239 })
1240 }
1241}
1242
1243impl CoercionRules for Size {
1244 fn is_identity_roundtrip() -> bool {
1245 true
1246 }
1247
1248 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1249 Ok(match pax_value {
1250 PaxValue::Size(size) => size,
1251 PaxValue::Percent(p) => Size::Percent(p.0),
1252 PaxValue::Numeric(v) => Size::Pixels(v),
1253 PaxValue::Option(mut opt) => {
1254 if let Some(p) = opt.take() {
1255 Size::try_coerce(p)?
1256 } else {
1257 return Err(format!("None can't be coerced into a Size"));
1258 }
1259 }
1260 _ => return Err(format!("{:?} can't be coerced into a Size", pax_value)),
1261 })
1262 }
1263}
1264
1265impl CoercionRules for String {
1266 fn is_identity_roundtrip() -> bool {
1267 true
1268 }
1269
1270 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1271 Ok(match pax_value {
1272 PaxValue::String(s) => s,
1273 PaxValue::Numeric(n) => {
1274 if n.is_float() {
1275 n.to_float().to_string()
1276 } else {
1277 n.to_int().to_string()
1278 }
1279 }
1280 PaxValue::Option(mut opt) => {
1281 if let Some(p) = opt.take() {
1282 String::try_coerce(p)?
1283 } else {
1284 return Err(format!("None can't be coerced into a String"));
1285 }
1286 }
1287 _ => return Err(format!("{:?} can't be coerced into a String", pax_value)),
1288 })
1289 }
1290}
1291
1292impl CoercionRules for Rotation {
1293 fn is_identity_roundtrip() -> bool {
1294 true
1295 }
1296
1297 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1298 Ok(match pax_value {
1299 PaxValue::Rotation(r) => r,
1300 PaxValue::Numeric(n) => Rotation::Degrees(n),
1301 PaxValue::Percent(p) => Rotation::Percent(p.0),
1302 PaxValue::Option(mut opt) => {
1303 if let Some(p) = opt.take() {
1304 Rotation::try_coerce(p)?
1305 } else {
1306 return Err(format!("None can't be coerced into a Rotation"));
1307 }
1308 }
1309 _ => return Err(format!("{:?} can't be coerced into a Rotation", pax_value)),
1310 })
1311 }
1312}
1313
1314impl CoercionRules for Duration {
1315 fn is_identity_roundtrip() -> bool {
1316 true
1317 }
1318
1319 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1320 Ok(match pax_value {
1321 PaxValue::Duration(duration) => duration,
1322 PaxValue::Numeric(value) => Duration::Frames(value),
1323 PaxValue::Option(mut opt) => {
1324 if let Some(p) = opt.take() {
1325 Duration::try_coerce(p)?
1326 } else {
1327 return Err(format!("None can't be coerced into a Duration"));
1328 }
1329 }
1330 _ => return Err(format!("{:?} can't be coerced into a Duration", pax_value)),
1331 })
1332 }
1333}
1334
1335impl CoercionRules for Numeric {
1336 fn is_identity_roundtrip() -> bool {
1337 true
1338 }
1339
1340 fn try_coerce(pax_value: PaxValue) -> Result<Self, String> {
1341 Ok(match pax_value {
1342 PaxValue::Bool(b) => (b as i32).into(),
1343 PaxValue::Numeric(n) => n.into(),
1344 PaxValue::Size(n) => n.into(),
1345 PaxValue::Option(mut opt) => {
1346 if let Some(p) = opt.take() {
1347 Numeric::try_coerce(p)?
1348 } else {
1349 return Err(format!("None can't be coerced into a Numeric"));
1350 }
1351 }
1352 _ => return Err(format!("{:?} can't be coerced into a Numeric", pax_value)),
1353 })
1354 }
1355}
1356
1357impl<T: CoercionRules> CoercionRules for Vec<T> {
1358 fn allows_singleton_stack() -> bool {
1359 T::allows_singleton_stack()
1360 }
1361 fn is_identity_roundtrip() -> bool {
1362 is_typed_binding_safe::<T>()
1363 }
1364
1365 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1366 match value {
1367 PaxValue::Vec(vec) => {
1368 let res: Result<Vec<T>, _> = vec
1369 .into_iter()
1370 .enumerate()
1371 .map(|(i, v)| T::try_coerce(v).map_err(|e| format!("entry [{i}]: {e}")))
1372 .collect();
1373 res.map_err(|e| format!("couldn't coerce vec, element {:?}", e))
1374 }
1375 PaxValue::Option(mut opt) => {
1376 if let Some(p) = opt.take() {
1377 Vec::<T>::try_coerce(p)
1378 } else {
1379 return Err(format!("None can't be coerced into a Vec"));
1380 }
1381 }
1382 v if T::allows_singleton_stack() => T::try_coerce(v).map(|layer| vec![layer]),
1383 v => Err(format!(
1384 "{:?} can't be coerced into {:?}",
1385 v,
1386 std::any::type_name::<Vec<T>>(),
1387 )),
1388 }
1389 }
1390}
1391
1392impl<T1: CoercionRules, T2: CoercionRules> CoercionRules for (T1, T2) {
1393 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1394 match value {
1395 PaxValue::Vec(vec) => {
1396 let mut itr = vec.into_iter();
1397 let res: Result<T1, _> = T1::try_coerce(itr.next().unwrap());
1398 let res2: Result<T2, _> = T2::try_coerce(itr.next().unwrap());
1399 res.and_then(|v1| res2.map(|v2| (v1, v2)))
1400 }
1401 PaxValue::Option(opt) => {
1402 if let Some(p) = *opt {
1403 <(T1, T2)>::try_coerce(p)
1404 } else {
1405 return Err(format!("None can't be coerced into a Vec"));
1406 }
1407 }
1408 v => Err(format!(
1409 "{:?} can't be coerced into {:?}",
1410 v,
1411 std::any::type_name::<(T1, T2)>(),
1412 )),
1413 }
1414 }
1415}
1416
1417impl<T: CoercionRules> CoercionRules for Option<T> {
1418 fn is_identity_roundtrip() -> bool {
1419 is_typed_binding_safe::<T>()
1420 }
1421
1422 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1423 match value {
1424 PaxValue::Option(opt) => {
1425 let res: Result<Option<T>, _> = opt.map(|v| T::try_coerce(v)).transpose();
1426 res.map_err(|e| format!("couldn't coerce option, element {:?}", e))
1427 }
1428 v => Some(T::try_coerce(v)).transpose(),
1429 }
1430 }
1431}
1432
1433impl<T: CoercionRules> CoercionRules for Range<T> {
1434 fn is_identity_roundtrip() -> bool {
1435 is_typed_binding_safe::<T>()
1436 }
1437
1438 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1439 match value {
1440 PaxValue::Range(start, end) => {
1441 let start = T::try_coerce(*start)?;
1442 let end = T::try_coerce(*end)?;
1443 Ok(start..end)
1444 }
1445 PaxValue::Option(mut opt) => {
1446 if let Some(p) = opt.take() {
1447 Range::<T>::try_coerce(p)
1448 } else {
1449 return Err(format!("None can't be coerced into a Range"));
1450 }
1451 }
1452 v => Err(format!(
1453 "{:?} can't be coerced into {:?}",
1454 v,
1455 std::any::type_name::<Range<T>>(),
1456 )),
1457 }
1458 }
1459}
1460
1461impl CoercionRules for PaxValue {
1462 fn is_identity_roundtrip() -> bool {
1463 true
1464 }
1465
1466 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1467 Ok(value)
1468 }
1469}
1470
1471impl<T: CoercionRules> CoercionRules for Box<T> {
1472 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1473 Ok(Box::new(T::try_coerce(value)?))
1474 }
1475}
1476
1477impl CoercionRules for Transform2D {
1478 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1479 Ok(match value {
1480 PaxValue::Option(opt) => {
1481 if let Some(t) = *opt {
1482 Transform2D::try_coerce(t)?
1483 } else {
1484 return Err(format!("None can't be coerced into a Transform2D"));
1485 }
1486 }
1487 PaxValue::Object(map) => {
1488 let [previous, rotate, translate, anchor, scale, skew] = extract_options(
1489 ["previous", "rotate", "translate", "anchor", "scale", "skew"],
1490 map,
1491 )
1492 .map_err(|e| format!("failed to convert to Transform2D: {e}"))?;
1493 let previous = Option::<Box<Transform2D>>::try_coerce(previous)?;
1494 let rotate = Option::<Rotation>::try_coerce(rotate)?;
1495 let translate = match translate {
1496 PaxValue::Option(opt) => {
1497 if let Some(t) = *opt {
1498 let t = Vec::<Size>::try_coerce(t)?;
1499 if t.len() != 2 {
1500 return Err(format!(
1501 "expected 2 elements in translate, got {:?}",
1502 t.len()
1503 ));
1504 }
1505 Some([t[0], t[1]])
1506 } else {
1507 None
1508 }
1509 }
1510 _ => return Err(format!("translate can't be coerced into a Transform2D",)),
1511 };
1512 let anchor = match anchor {
1513 PaxValue::Option(opt) => {
1514 if let Some(a) = *opt {
1515 let a = Vec::<Size>::try_coerce(a)?;
1516 if a.len() != 2 {
1517 return Err(format!(
1518 "expected 2 elements in anchor, got {:?}",
1519 a.len()
1520 ));
1521 }
1522 Some([a[0], a[1]])
1523 } else {
1524 None
1525 }
1526 }
1527 _ => return Err(format!("anchor can't be coerced into a Transform2D",)),
1528 };
1529 let scale = match scale {
1530 PaxValue::Option(opt) => {
1531 if let Some(s) = *opt {
1532 let s = Vec::<Size>::try_coerce(s)?;
1533 if s.len() != 2 {
1534 return Err(format!(
1535 "expected 2 elements in scale, got {:?}",
1536 s.len()
1537 ));
1538 }
1539 Some([s[0], s[1]])
1540 } else {
1541 None
1542 }
1543 }
1544 _ => return Err(format!("scale can't be coerced into a Transform2D",)),
1545 };
1546 let skew = match skew {
1547 PaxValue::Option(opt) => {
1548 if let Some(s) = *opt {
1549 let s = Vec::<Rotation>::try_coerce(s)?;
1550 if s.len() != 2 {
1551 return Err(format!(
1552 "expected 2 elements in skew, got {:?}",
1553 s.len()
1554 ));
1555 }
1556 Some([s[0], s[1]])
1557 } else {
1558 None
1559 }
1560 }
1561 _ => return Err(format!("skew can't be coerced into a Transform2D",)),
1562 };
1563 Transform2D {
1564 previous,
1565 rotate,
1566 translate,
1567 anchor,
1568 scale,
1569 skew,
1570 }
1571 }
1572 _ => return Err(format!("{:?} can't be coerced into a Transform2D", value)),
1573 })
1574 }
1575}
1576
1577impl CoercionRules for Transform2 {
1578 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1579 Ok(match value {
1580 PaxValue::Option(mut opt) => {
1581 if let Some(t) = opt.take() {
1582 Transform2::try_coerce(t)?
1583 } else {
1584 return Err(format!("None can't be coerced into a Transform2"));
1585 }
1586 }
1587 PaxValue::Object(map) => {
1588 let m = Vec::<f64>::try_coerce(
1589 map.into_iter()
1590 .find_map(|(n, v)| (n == "m").then_some(v))
1591 .unwrap(),
1592 )?;
1593 if m.len() != 6 {
1594 return Err(format!("expected 6 elements in coeffs, got {:?}", m.len()));
1595 }
1596 Transform2::new([m[0], m[1], m[2], m[3], m[4], m[5]])
1597 }
1598 _ => return Err(format!("{:?} can't be coerced into a Transform2", value)),
1599 })
1600 }
1601}
1602
1603impl CoercionRules for Vector2 {
1604 fn try_coerce(value: PaxValue) -> Result<Self, String> {
1605 Ok(match value {
1606 PaxValue::Option(mut opt) => {
1607 if let Some(t) = opt.take() {
1608 Vector2::try_coerce(t)?
1609 } else {
1610 return Err(format!("None can't be coerced into a Vector2"));
1611 }
1612 }
1613 PaxValue::Object(map) => {
1614 let [x, y] = extract_options(["x", "y"], map)
1615 .map_err(|e| format!("failed to convert to Vector2: {e}"))?;
1616 let x = f64::try_coerce(x)?;
1617 let y = f64::try_coerce(y)?;
1618 Vector2::new(x, y)
1619 }
1620 _ => return Err(format!("{:?} can't be coerced into a Vector2", value)),
1621 })
1622 }
1623}
1624
1625pub fn extract_options<T, const N: usize>(
1627 keys: [&'static str; N],
1628 vec: Vec<(String, T)>,
1629) -> Result<[T; N], String> {
1630 let mut intermediate: [Option<T>; N] = std::array::from_fn(|_| None);
1632
1633 for (k, v) in vec {
1635 if let Some(pos) = keys.iter().position(|&key| k == key) {
1636 intermediate[pos] = Some(v);
1637 }
1638 }
1639
1640 let result: Result<[T; N], String> = intermediate
1642 .into_iter()
1643 .enumerate()
1644 .map(|(i, opt)| opt.ok_or_else(|| format!("missing field: {}", keys[i])))
1645 .collect::<Result<Vec<_>, _>>()?
1646 .try_into()
1647 .map_err(|_| "Internal error converting vec to array".to_string());
1648
1649 result
1650}