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