1use crate::corety::{AzString, OptionF32};
6use alloc::string::String;
7
8fn cmp_f32(a: f32, b: f32) -> core::cmp::Ordering {
10 a.partial_cmp(&b).unwrap_or(core::cmp::Ordering::Equal)
11}
12
13#[derive(Debug, Copy, Clone, PartialEq)]
15#[repr(C)]
16pub struct ShapePoint {
17 pub x: f32,
18 pub y: f32,
19}
20
21impl_option!(
22 ShapePoint,
23 OptionShapePoint,
24 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
25);
26
27impl ShapePoint {
28 #[must_use]
29 pub const fn new(x: f32, y: f32) -> Self {
30 Self { x, y }
31 }
32
33 #[must_use]
34 pub const fn zero() -> Self {
35 Self { x: 0.0, y: 0.0 }
36 }
37}
38
39impl Eq for ShapePoint {}
40
41impl PartialOrd for ShapePoint {
43 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
44 Some(self.cmp(other))
45 }
46}
47
48impl Ord for ShapePoint {
49 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
50 match self.x.partial_cmp(&other.x) {
51 Some(core::cmp::Ordering::Equal) => self
52 .y
53 .partial_cmp(&other.y)
54 .unwrap_or(core::cmp::Ordering::Equal),
55 other => other.unwrap_or(core::cmp::Ordering::Equal),
56 }
57 }
58}
59
60impl core::hash::Hash for ShapePoint {
61 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
62 self.x.to_bits().hash(state);
63 self.y.to_bits().hash(state);
64 }
65}
66
67impl_vec!(
68 ShapePoint,
69 ShapePointVec,
70 ShapePointVecDestructor,
71 ShapePointVecDestructorType,
72 ShapePointVecSlice,
73 OptionShapePoint
74);
75impl_vec_debug!(ShapePoint, ShapePointVec);
76impl_vec_partialord!(ShapePoint, ShapePointVec);
77impl_vec_ord!(ShapePoint, ShapePointVec);
78impl_vec_clone!(ShapePoint, ShapePointVec, ShapePointVecDestructor);
79impl_vec_partialeq!(ShapePoint, ShapePointVec);
80impl_vec_eq!(ShapePoint, ShapePointVec);
81impl_vec_hash!(ShapePoint, ShapePointVec);
82
83#[derive(Debug, Copy, Clone)]
85#[repr(C)]
86pub struct ShapeCircle {
87 pub center: ShapePoint,
88 pub radius: f32,
89}
90
91impl PartialEq for ShapeCircle {
94 fn eq(&self, other: &Self) -> bool {
95 self.cmp(other) == core::cmp::Ordering::Equal
96 }
97}
98impl Eq for ShapeCircle {}
99impl core::hash::Hash for ShapeCircle {
100 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
101 self.center.hash(state);
102 self.radius.to_bits().hash(state);
103 }
104}
105impl PartialOrd for ShapeCircle {
106 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
107 Some(self.cmp(other))
108 }
109}
110impl Ord for ShapeCircle {
111 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
112 match self.center.cmp(&other.center) {
113 core::cmp::Ordering::Equal => self
114 .radius
115 .partial_cmp(&other.radius)
116 .unwrap_or(core::cmp::Ordering::Equal),
117 other => other,
118 }
119 }
120}
121
122#[derive(Debug, Copy, Clone)]
124#[repr(C)]
125pub struct ShapeEllipse {
126 pub center: ShapePoint,
127 pub radius_x: f32,
128 pub radius_y: f32,
129}
130
131impl PartialEq for ShapeEllipse {
133 fn eq(&self, other: &Self) -> bool {
134 self.cmp(other) == core::cmp::Ordering::Equal
135 }
136}
137impl Eq for ShapeEllipse {}
138impl core::hash::Hash for ShapeEllipse {
139 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
140 self.center.hash(state);
141 self.radius_x.to_bits().hash(state);
142 self.radius_y.to_bits().hash(state);
143 }
144}
145impl PartialOrd for ShapeEllipse {
146 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
147 Some(self.cmp(other))
148 }
149}
150impl Ord for ShapeEllipse {
151 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
152 match self.center.cmp(&other.center) {
153 core::cmp::Ordering::Equal => match self.radius_x.partial_cmp(&other.radius_x) {
154 Some(core::cmp::Ordering::Equal) | None => self
155 .radius_y
156 .partial_cmp(&other.radius_y)
157 .unwrap_or(core::cmp::Ordering::Equal),
158 Some(other) => other,
159 },
160 other => other,
161 }
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
167#[repr(C)]
168pub struct ShapePolygon {
169 pub points: ShapePointVec,
170}
171
172#[derive(Debug, Copy, Clone)]
175#[repr(C)]
176pub struct ShapeInset {
177 pub inset_top: f32,
178 pub inset_right: f32,
179 pub inset_bottom: f32,
180 pub inset_left: f32,
181 pub border_radius: OptionF32,
182}
183
184impl PartialEq for ShapeInset {
186 fn eq(&self, other: &Self) -> bool {
187 self.cmp(other) == core::cmp::Ordering::Equal
188 }
189}
190impl Eq for ShapeInset {}
191impl core::hash::Hash for ShapeInset {
192 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
193 self.inset_top.to_bits().hash(state);
194 self.inset_right.to_bits().hash(state);
195 self.inset_bottom.to_bits().hash(state);
196 self.inset_left.to_bits().hash(state);
197 self.border_radius.hash(state);
198 }
199}
200impl PartialOrd for ShapeInset {
201 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
202 Some(self.cmp(other))
203 }
204}
205impl Ord for ShapeInset {
206 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
207 cmp_f32(self.inset_top, other.inset_top)
208 .then_with(|| cmp_f32(self.inset_right, other.inset_right))
209 .then_with(|| cmp_f32(self.inset_bottom, other.inset_bottom))
210 .then_with(|| cmp_f32(self.inset_left, other.inset_left))
211 .then_with(|| self.border_radius.cmp(&other.border_radius))
212 }
213}
214
215#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
218#[repr(C)]
219pub struct ShapePath {
220 pub data: AzString,
221}
222
223#[derive(Debug, Clone)]
226#[repr(C, u8)]
227pub enum CssShape {
228 Circle(ShapeCircle),
229 Ellipse(ShapeEllipse),
230 Polygon(ShapePolygon),
231 Inset(ShapeInset),
232 Path(ShapePath),
233}
234
235impl PartialEq for CssShape {
238 fn eq(&self, other: &Self) -> bool {
239 self.cmp(other) == core::cmp::Ordering::Equal
240 }
241}
242impl Eq for CssShape {}
243
244impl core::hash::Hash for CssShape {
245 fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
246 core::mem::discriminant(self).hash(state);
247 match self {
248 Self::Circle(c) => c.hash(state),
249 Self::Ellipse(e) => e.hash(state),
250 Self::Polygon(p) => p.hash(state),
251 Self::Inset(i) => i.hash(state),
252 Self::Path(p) => p.hash(state),
253 }
254 }
255}
256
257impl PartialOrd for CssShape {
258 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
259 Some(self.cmp(other))
260 }
261}
262
263impl Ord for CssShape {
264 #[allow(clippy::match_same_arms)]
268 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
269 match (self, other) {
270 (Self::Circle(a), Self::Circle(b)) => a.cmp(b),
271 (Self::Ellipse(a), Self::Ellipse(b)) => a.cmp(b),
272 (Self::Polygon(a), Self::Polygon(b)) => a.cmp(b),
273 (Self::Inset(a), Self::Inset(b)) => a.cmp(b),
274 (Self::Path(a), Self::Path(b)) => a.cmp(b),
275 (Self::Circle(_), _) => core::cmp::Ordering::Less,
277 (_, Self::Circle(_)) => core::cmp::Ordering::Greater,
278 (Self::Ellipse(_), _) => core::cmp::Ordering::Less,
279 (_, Self::Ellipse(_)) => core::cmp::Ordering::Greater,
280 (Self::Polygon(_), _) => core::cmp::Ordering::Less,
281 (_, Self::Polygon(_)) => core::cmp::Ordering::Greater,
282 (Self::Inset(_), Self::Path(_)) => core::cmp::Ordering::Less,
283 (Self::Path(_), Self::Inset(_)) => core::cmp::Ordering::Greater,
284 }
285 }
286}
287
288impl CssShape {
289 #[must_use]
291 pub const fn circle(center: ShapePoint, radius: f32) -> Self {
292 Self::Circle(ShapeCircle { center, radius })
293 }
294
295 #[must_use]
297 pub const fn ellipse(center: ShapePoint, radius_x: f32, radius_y: f32) -> Self {
298 Self::Ellipse(ShapeEllipse {
299 center,
300 radius_x,
301 radius_y,
302 })
303 }
304
305 #[must_use]
307 pub const fn polygon(points: ShapePointVec) -> Self {
308 Self::Polygon(ShapePolygon { points })
309 }
310
311 #[must_use]
313 pub const fn inset(top: f32, right: f32, bottom: f32, left: f32) -> Self {
314 Self::Inset(ShapeInset {
315 inset_top: top,
316 inset_right: right,
317 inset_bottom: bottom,
318 inset_left: left,
319 border_radius: OptionF32::None,
320 })
321 }
322
323 #[must_use]
325 pub const fn inset_rounded(top: f32, right: f32, bottom: f32, left: f32, radius: f32) -> Self {
326 Self::Inset(ShapeInset {
327 inset_top: top,
328 inset_right: right,
329 inset_bottom: bottom,
330 inset_left: left,
331 border_radius: OptionF32::Some(radius),
332 })
333 }
334
335 #[must_use]
336 pub fn print_as_css_value(&self) -> String {
337 use alloc::format;
338 match self {
339 Self::Circle(ShapeCircle { center, radius }) => {
340 format!("circle({}px at {}px {}px)", radius, center.x, center.y)
341 }
342 Self::Ellipse(ShapeEllipse {
343 center,
344 radius_x,
345 radius_y,
346 }) => {
347 format!(
348 "ellipse({}px {}px at {}px {}px)",
349 radius_x, radius_y, center.x, center.y
350 )
351 }
352 Self::Polygon(ShapePolygon { points }) => {
353 let pts: Vec<String> = points
354 .as_ref()
355 .iter()
356 .map(|p| format!("{}px {}px", p.x, p.y))
357 .collect();
358 format!("polygon({})", pts.join(", "))
359 }
360 Self::Inset(ShapeInset {
361 inset_top,
362 inset_right,
363 inset_bottom,
364 inset_left,
365 border_radius,
366 }) => {
367 let base =
368 format!("inset({inset_top}px {inset_right}px {inset_bottom}px {inset_left}px");
369 match border_radius {
370 OptionF32::Some(r) => format!("{base} round {r}px)"),
371 OptionF32::None => format!("{base})"),
372 }
373 }
374 Self::Path(ShapePath { data }) => {
375 format!("path(\"{}\")", data.as_str())
376 }
377 }
378 }
379
380 #[must_use]
381 pub fn format_as_rust_code(&self) -> String {
382 use alloc::format;
383 match self {
384 Self::Circle(ShapeCircle { center, radius }) => {
385 format!(
386 "CssShape::Circle(ShapeCircle {{ center: ShapePoint::new({}_f32, {}_f32), radius: {}_f32 }})",
387 center.x, center.y, radius
388 )
389 }
390 Self::Ellipse(ShapeEllipse {
391 center,
392 radius_x,
393 radius_y,
394 }) => {
395 format!(
396 "CssShape::Ellipse(ShapeEllipse {{ center: ShapePoint::new({}_f32, {}_f32), radius_x: {}_f32, radius_y: {}_f32 }})",
397 center.x, center.y, radius_x, radius_y
398 )
399 }
400 Self::Polygon(ShapePolygon { points }) => {
401 let pts: Vec<String> = points
402 .as_ref()
403 .iter()
404 .map(|p| format!("ShapePoint::new({}_f32, {}_f32)", p.x, p.y))
405 .collect();
406 format!(
407 "CssShape::Polygon(ShapePolygon {{ points: vec![{}].into() }})",
408 pts.join(", ")
409 )
410 }
411 Self::Inset(ShapeInset {
412 inset_top,
413 inset_right,
414 inset_bottom,
415 inset_left,
416 border_radius,
417 }) => {
418 let br = match border_radius {
419 OptionF32::Some(r) => format!("OptionF32::Some({r}_f32)"),
420 OptionF32::None => String::from("OptionF32::None"),
421 };
422 format!(
423 "CssShape::Inset(ShapeInset {{ inset_top: {inset_top}_f32, inset_right: {inset_right}_f32, inset_bottom: {inset_bottom}_f32, inset_left: {inset_left}_f32, border_radius: {br} }})"
424 )
425 }
426 Self::Path(ShapePath { data }) => {
427 format!(
428 "CssShape::Path(ShapePath {{ data: AzString::from_const_str(\"{}\") }})",
429 data.as_str()
430 )
431 }
432 }
433 }
434}
435
436impl_option!(
437 CssShape,
438 OptionCssShape,
439 copy = false,
440 [Debug, Clone, PartialEq, Eq]
441);
442
443#[cfg(test)]
444mod autotest_generated {
445 #![allow(
449 clippy::float_cmp,
450 clippy::unreadable_literal,
451 clippy::cast_precision_loss
452 )]
453
454 use core::{
455 cmp::Ordering,
456 hash::{Hash, Hasher},
457 };
458 use std::collections::hash_map::DefaultHasher;
459
460 use super::*;
461 use crate::shape_parser::{parse_shape, ShapeParseError};
462
463 const EDGE_F32: &[f32] = &[
465 0.0,
466 -0.0,
467 1.0,
468 -1.0,
469 f32::MIN,
470 f32::MAX,
471 f32::MIN_POSITIVE,
472 f32::EPSILON,
473 f32::INFINITY,
474 f32::NEG_INFINITY,
475 f32::NAN,
476 ];
477
478 fn hash_of<T: Hash>(t: &T) -> u64 {
479 let mut h = DefaultHasher::new();
480 t.hash(&mut h);
481 h.finish()
482 }
483
484 fn roundtrip(shape: &CssShape) -> CssShape {
487 let css = shape.print_as_css_value();
488 parse_shape(&css).unwrap_or_else(|e| panic!("round-trip failed for {css:?}: {e:?}"))
489 }
490
491 fn poly(coords: &[(f32, f32)]) -> CssShape {
492 let pts: Vec<ShapePoint> = coords
493 .iter()
494 .map(|(x, y)| ShapePoint::new(*x, *y))
495 .collect();
496 CssShape::polygon(ShapePointVec::from_vec(pts))
497 }
498
499 fn path(data: &str) -> CssShape {
500 CssShape::Path(ShapePath {
501 data: AzString::from(data),
502 })
503 }
504
505 #[test]
510 fn cmp_f32_orders_ordinary_values() {
511 assert_eq!(cmp_f32(1.0, 2.0), Ordering::Less);
512 assert_eq!(cmp_f32(2.0, 1.0), Ordering::Greater);
513 assert_eq!(cmp_f32(2.0, 2.0), Ordering::Equal);
514 assert_eq!(cmp_f32(-1.0, 1.0), Ordering::Less);
515 }
516
517 #[test]
518 fn cmp_f32_treats_both_zeroes_as_equal() {
519 assert_eq!(cmp_f32(0.0, -0.0), Ordering::Equal);
520 assert_eq!(cmp_f32(-0.0, 0.0), Ordering::Equal);
521 }
522
523 #[test]
524 fn cmp_f32_nan_is_equal_to_everything_and_never_panics() {
525 assert_eq!(cmp_f32(f32::NAN, f32::NAN), Ordering::Equal);
527 for &v in EDGE_F32 {
528 assert_eq!(cmp_f32(f32::NAN, v), Ordering::Equal);
529 assert_eq!(cmp_f32(v, f32::NAN), Ordering::Equal);
530 }
531 }
532
533 #[test]
534 fn cmp_f32_handles_infinities_and_limits() {
535 assert_eq!(cmp_f32(f32::INFINITY, f32::MAX), Ordering::Greater);
536 assert_eq!(cmp_f32(f32::NEG_INFINITY, f32::MIN), Ordering::Less);
537 assert_eq!(cmp_f32(f32::INFINITY, f32::INFINITY), Ordering::Equal);
538 assert_eq!(cmp_f32(f32::NEG_INFINITY, f32::INFINITY), Ordering::Less);
539 assert_eq!(cmp_f32(f32::MIN_POSITIVE, 0.0), Ordering::Greater);
540 assert_eq!(cmp_f32(f32::from_bits(1), 0.0), Ordering::Greater);
542 assert_eq!(cmp_f32(f32::MIN, f32::MAX), Ordering::Less);
543 }
544
545 #[test]
546 fn cmp_f32_is_antisymmetric_over_all_edge_values() {
547 for &a in EDGE_F32 {
548 for &b in EDGE_F32 {
549 assert_eq!(
550 cmp_f32(a, b),
551 cmp_f32(b, a).reverse(),
552 "antisymmetry broken for ({a}, {b})"
553 );
554 }
555 }
556 }
557
558 #[test]
559 fn cmp_f32_nan_equality_is_not_transitive() {
560 assert_eq!(cmp_f32(f32::NAN, 1.0), Ordering::Equal);
565 assert_eq!(cmp_f32(f32::NAN, 2.0), Ordering::Equal);
566 assert_eq!(cmp_f32(1.0, 2.0), Ordering::Less);
567 }
568
569 #[test]
574 fn shapepoint_new_stores_fields_verbatim() {
575 let p = ShapePoint::new(3.5, -7.25);
576 assert_eq!(p.x, 3.5);
577 assert_eq!(p.y, -7.25);
578 }
579
580 #[test]
581 fn shapepoint_new_does_not_normalize_edge_values() {
582 for &x in EDGE_F32 {
583 for &y in EDGE_F32 {
584 let p = ShapePoint::new(x, y);
585 assert_eq!(p.x.to_bits(), x.to_bits());
587 assert_eq!(p.y.to_bits(), y.to_bits());
588 }
589 }
590 }
591
592 #[test]
593 fn shapepoint_new_preserves_negative_zero_sign() {
594 let p = ShapePoint::new(-0.0, 0.0);
595 assert_eq!(p.x.to_bits(), (-0.0f32).to_bits());
596 assert_eq!(p.y.to_bits(), (0.0f32).to_bits());
597 assert!(p.x.is_sign_negative());
598 }
599
600 #[test]
601 fn shapepoint_zero_is_the_neutral_positive_zero() {
602 let z = ShapePoint::zero();
603 assert_eq!(z.x.to_bits(), 0);
604 assert_eq!(z.y.to_bits(), 0);
605 assert_eq!(z, ShapePoint::new(0.0, 0.0));
606 assert_eq!(z.cmp(&ShapePoint::new(0.0, 0.0)), Ordering::Equal);
607 }
608
609 #[test]
610 fn shapepoint_constructors_are_usable_in_const_context() {
611 const P: ShapePoint = ShapePoint::new(1.0, 2.0);
612 const Z: ShapePoint = ShapePoint::zero();
613 assert_eq!(P.x, 1.0);
614 assert_eq!(Z, ShapePoint::zero());
615 assert_eq!(size_of::<ShapePoint>(), 8);
617 }
618
619 #[test]
624 fn shapepoint_ord_sorts_by_x_then_y() {
625 let mut v = vec![
626 ShapePoint::new(1.0, 5.0),
627 ShapePoint::new(-3.0, 0.0),
628 ShapePoint::new(1.0, -5.0),
629 ShapePoint::new(0.0, 0.0),
630 ];
631 v.sort(); assert_eq!(
633 v,
634 vec![
635 ShapePoint::new(-3.0, 0.0),
636 ShapePoint::new(0.0, 0.0),
637 ShapePoint::new(1.0, -5.0),
638 ShapePoint::new(1.0, 5.0),
639 ]
640 );
641 }
642
643 #[test]
644 fn shapepoint_cmp_is_antisymmetric_even_with_nan() {
645 for &ax in EDGE_F32 {
646 for &ay in EDGE_F32 {
647 let a = ShapePoint::new(ax, ay);
648 for &bx in EDGE_F32 {
649 let b = ShapePoint::new(bx, 1.0);
650 assert_eq!(
651 a.cmp(&b),
652 b.cmp(&a).reverse(),
653 "antisymmetry broken for {a:?} vs {b:?}"
654 );
655 assert_eq!(a.partial_cmp(&b), Some(a.cmp(&b)));
657 }
658 }
659 }
660 }
661
662 #[test]
663 fn shapepoint_cmp_with_nan_x_ignores_y_entirely() {
664 let a = ShapePoint::new(f32::NAN, 1.0);
669 let b = ShapePoint::new(f32::NAN, 2.0);
670 assert_eq!(a.cmp(&b), Ordering::Equal);
671
672 let e1 = ShapeEllipse {
673 center: ShapePoint::zero(),
674 radius_x: f32::NAN,
675 radius_y: 1.0,
676 };
677 let e2 = ShapeEllipse {
678 center: ShapePoint::zero(),
679 radius_x: f32::NAN,
680 radius_y: 2.0,
681 };
682 assert_eq!(e1.cmp(&e2), Ordering::Less);
683 }
684
685 #[test]
686 fn shapepoint_nan_is_ord_equal_but_partialeq_unequal() {
687 let a = ShapePoint::new(f32::NAN, 0.0);
690 let b = ShapePoint::new(f32::NAN, 0.0);
691 assert_ne!(a, b, "derived PartialEq: NaN != NaN");
692 assert_eq!(a.cmp(&b), Ordering::Equal, "Ord: NaN treated as equal");
693 }
694
695 #[test]
696 fn shapepoint_hash_is_deterministic_and_bitwise() {
697 let a = ShapePoint::new(1.5, -2.5);
698 let b = ShapePoint::new(1.5, -2.5);
699 assert_eq!(hash_of(&a), hash_of(&b));
700 assert_ne!(hash_of(&a), hash_of(&ShapePoint::new(-2.5, 1.5)));
701 let n1 = ShapePoint::new(f32::NAN, 0.0);
704 let n2 = ShapePoint::new(f32::NAN, 0.0);
705 assert_eq!(hash_of(&n1), hash_of(&n2));
706 }
707
708 #[test]
713 fn circle_stores_center_and_radius_including_zero_and_negative() {
714 match CssShape::circle(ShapePoint::zero(), 0.0) {
715 CssShape::Circle(c) => {
716 assert_eq!(c.radius, 0.0);
717 assert_eq!(c.center, ShapePoint::zero());
718 }
719 other => panic!("expected Circle, got {other:?}"),
720 }
721 match CssShape::circle(ShapePoint::new(-1.0, -2.0), -50.0) {
723 CssShape::Circle(c) => assert_eq!(c.radius, -50.0),
724 other => panic!("expected Circle, got {other:?}"),
725 }
726 }
727
728 #[test]
729 fn circle_accepts_every_f32_edge_value_without_panicking() {
730 for &r in EDGE_F32 {
731 for &c in EDGE_F32 {
732 let shape = CssShape::circle(ShapePoint::new(c, c), r);
733 match shape {
734 CssShape::Circle(circle) => {
735 assert_eq!(circle.radius.to_bits(), r.to_bits());
736 }
737 other => panic!("expected Circle, got {other:?}"),
738 }
739 }
740 }
741 }
742
743 #[test]
744 fn ellipse_stores_both_radii_verbatim() {
745 match CssShape::ellipse(ShapePoint::new(1.0, 2.0), f32::INFINITY, f32::NEG_INFINITY) {
746 CssShape::Ellipse(e) => {
747 assert!(e.radius_x.is_infinite() && e.radius_x.is_sign_positive());
748 assert!(e.radius_y.is_infinite() && e.radius_y.is_sign_negative());
749 assert_eq!(e.center, ShapePoint::new(1.0, 2.0));
750 }
751 other => panic!("expected Ellipse, got {other:?}"),
752 }
753 match CssShape::ellipse(ShapePoint::zero(), f32::NAN, f32::MAX) {
754 CssShape::Ellipse(e) => {
755 assert!(e.radius_x.is_nan());
756 assert_eq!(e.radius_y, f32::MAX);
757 }
758 other => panic!("expected Ellipse, got {other:?}"),
759 }
760 }
761
762 #[test]
763 fn polygon_accepts_empty_and_huge_point_lists() {
764 match CssShape::polygon(ShapePointVec::from_vec(Vec::new())) {
765 CssShape::Polygon(p) => {
766 assert!(p.points.is_empty());
767 assert_eq!(p.points.len(), 0);
768 }
769 other => panic!("expected Polygon, got {other:?}"),
770 }
771
772 let big: Vec<ShapePoint> = (0..10_000)
773 .map(|i| ShapePoint::new(i as f32, -(i as f32)))
774 .collect();
775 match CssShape::polygon(ShapePointVec::from_vec(big)) {
776 CssShape::Polygon(p) => {
777 assert_eq!(p.points.len(), 10_000);
778 assert_eq!(p.points.as_ref()[9_999], ShapePoint::new(9999.0, -9999.0));
779 }
780 other => panic!("expected Polygon, got {other:?}"),
781 }
782 }
783
784 #[test]
785 fn inset_has_no_border_radius_and_keeps_side_order() {
786 match CssShape::inset(1.0, 2.0, 3.0, 4.0) {
787 CssShape::Inset(i) => {
788 assert_eq!(i.inset_top, 1.0);
789 assert_eq!(i.inset_right, 2.0);
790 assert_eq!(i.inset_bottom, 3.0);
791 assert_eq!(i.inset_left, 4.0);
792 assert_eq!(i.border_radius, OptionF32::None);
793 assert_eq!(i.border_radius.into_option(), None);
794 }
795 other => panic!("expected Inset, got {other:?}"),
796 }
797 }
798
799 #[test]
800 fn inset_accepts_min_max_and_nan_without_panicking() {
801 match CssShape::inset(f32::MIN, f32::MAX, f32::NAN, f32::NEG_INFINITY) {
802 CssShape::Inset(i) => {
803 assert_eq!(i.inset_top, f32::MIN);
804 assert_eq!(i.inset_right, f32::MAX);
805 assert!(i.inset_bottom.is_nan());
806 assert!(i.inset_left.is_infinite());
807 }
808 other => panic!("expected Inset, got {other:?}"),
809 }
810 }
811
812 #[test]
813 fn inset_rounded_keeps_even_a_css_invalid_negative_radius() {
814 match CssShape::inset_rounded(0.0, 0.0, 0.0, 0.0, -5.0) {
815 CssShape::Inset(i) => {
816 assert_eq!(i.border_radius, OptionF32::Some(-5.0));
817 assert_eq!(i.border_radius.into_option(), Some(-5.0));
818 }
819 other => panic!("expected Inset, got {other:?}"),
820 }
821 match CssShape::inset_rounded(0.0, 0.0, 0.0, 0.0, f32::NAN) {
822 CssShape::Inset(i) => match i.border_radius {
823 OptionF32::Some(r) => assert!(r.is_nan()),
824 OptionF32::None => panic!("radius was dropped"),
825 },
826 other => panic!("expected Inset, got {other:?}"),
827 }
828 }
829
830 #[test]
831 fn css_shape_constructors_are_usable_in_const_context() {
832 const CIRCLE: CssShape = CssShape::circle(ShapePoint::zero(), 5.0);
833 const ELLIPSE: CssShape = CssShape::ellipse(ShapePoint::zero(), 1.0, 2.0);
834 const INSET: CssShape = CssShape::inset(0.0, 0.0, 0.0, 0.0);
835 const ROUNDED: CssShape = CssShape::inset_rounded(0.0, 0.0, 0.0, 0.0, 1.0);
836 assert!(matches!(CIRCLE, CssShape::Circle(_)));
837 assert!(matches!(ELLIPSE, CssShape::Ellipse(_)));
838 assert!(matches!(INSET, CssShape::Inset(_)));
839 assert!(matches!(ROUNDED, CssShape::Inset(_)));
840 }
841
842 #[test]
847 fn css_shape_variant_order_is_circle_ellipse_polygon_inset_path() {
848 let shapes = vec![
849 path("M 0 0"),
850 CssShape::inset(1.0, 1.0, 1.0, 1.0),
851 poly(&[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0)]),
852 CssShape::ellipse(ShapePoint::zero(), 1.0, 2.0),
853 CssShape::circle(ShapePoint::zero(), 1.0),
854 ];
855 let mut sorted = shapes;
856 sorted.sort(); let discriminants: Vec<&str> = sorted
858 .iter()
859 .map(|s| match s {
860 CssShape::Circle(_) => "circle",
861 CssShape::Ellipse(_) => "ellipse",
862 CssShape::Polygon(_) => "polygon",
863 CssShape::Inset(_) => "inset",
864 CssShape::Path(_) => "path",
865 })
866 .collect();
867 assert_eq!(
868 discriminants,
869 vec!["circle", "ellipse", "polygon", "inset", "path"]
870 );
871 }
872
873 #[test]
874 fn css_shape_cmp_is_antisymmetric_across_variants_and_nan() {
875 let shapes = vec![
876 CssShape::circle(ShapePoint::zero(), f32::NAN),
877 CssShape::circle(ShapePoint::new(f32::NAN, 0.0), 1.0),
878 CssShape::ellipse(ShapePoint::zero(), f32::NAN, f32::INFINITY),
879 poly(&[]),
880 poly(&[(f32::NAN, f32::NEG_INFINITY)]),
881 CssShape::inset(f32::NAN, f32::MAX, f32::MIN, -0.0),
882 CssShape::inset_rounded(0.0, 0.0, 0.0, 0.0, f32::NAN),
883 path(""),
884 path("\u{1F600}"),
885 ];
886 for a in &shapes {
887 for b in &shapes {
888 assert_eq!(
889 a.cmp(b),
890 b.cmp(a).reverse(),
891 "antisymmetry broken for {a:?} vs {b:?}"
892 );
893 assert_eq!(a.partial_cmp(b), Some(a.cmp(b)));
894 }
895 }
896 }
897
898 #[test]
899 fn css_shape_hash_separates_variants_with_identical_payloads() {
900 let circle = CssShape::circle(ShapePoint::zero(), 1.0);
901 let ellipse = CssShape::ellipse(ShapePoint::zero(), 1.0, 1.0);
902 assert_ne!(hash_of(&circle), hash_of(&ellipse));
903 assert_eq!(
904 hash_of(&circle),
905 hash_of(&CssShape::circle(ShapePoint::zero(), 1.0))
906 );
907 let p1 = poly(&[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]);
909 let p2 = poly(&[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]);
910 assert_eq!(p1, p2);
911 assert_eq!(hash_of(&p1), hash_of(&p2));
912 }
913
914 #[test]
919 fn print_as_css_value_known_constructions() {
920 assert_eq!(
921 CssShape::circle(ShapePoint::new(100.0, 100.0), 50.0).print_as_css_value(),
922 "circle(50px at 100px 100px)"
923 );
924 assert_eq!(
925 CssShape::ellipse(ShapePoint::new(1.0, 2.0), 3.0, 4.5).print_as_css_value(),
926 "ellipse(3px 4.5px at 1px 2px)"
927 );
928 assert_eq!(
929 poly(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0)]).print_as_css_value(),
930 "polygon(0px 0px, 100px 0px, 100px 100px)"
931 );
932 assert_eq!(
933 CssShape::inset(1.0, 2.0, 3.0, 4.0).print_as_css_value(),
934 "inset(1px 2px 3px 4px)"
935 );
936 assert_eq!(
937 CssShape::inset_rounded(1.0, 2.0, 3.0, 4.0, 5.0).print_as_css_value(),
938 "inset(1px 2px 3px 4px round 5px)"
939 );
940 assert_eq!(
941 path("M 0 0 L 1 1 Z").print_as_css_value(),
942 "path(\"M 0 0 L 1 1 Z\")"
943 );
944 }
945
946 #[test]
947 fn print_as_css_value_empty_polygon_emits_empty_parens() {
948 assert_eq!(poly(&[]).print_as_css_value(), "polygon()");
950 }
951
952 #[test]
953 fn print_as_css_value_emits_non_css_tokens_for_nan_and_inf() {
954 let s = CssShape::circle(ShapePoint::new(f32::INFINITY, f32::NEG_INFINITY), f32::NAN)
957 .print_as_css_value();
958 assert_eq!(s, "circle(NaNpx at infpx -infpx)");
959 }
960
961 #[test]
962 fn print_as_css_value_handles_extreme_finite_values() {
963 let s = CssShape::inset(f32::MIN, f32::MAX, f32::MIN_POSITIVE, 0.0).print_as_css_value();
964 assert!(s.starts_with("inset(-"), "got {s}");
965 assert!(s.ends_with("px)"), "got {s}");
966 assert!(!s.contains("inf"), "MIN/MAX must not print as inf: {s}");
967 }
968
969 #[test]
970 fn print_as_css_value_survives_a_huge_polygon() {
971 let coords: Vec<(f32, f32)> = (0..5_000).map(|i| (i as f32, i as f32)).collect();
972 let s = poly(&coords).print_as_css_value();
973 assert!(s.starts_with("polygon(0px 0px, "));
974 assert!(s.ends_with("4999px 4999px)"));
975 assert_eq!(s.matches(", ").count(), 4_999);
976 }
977
978 #[test]
979 fn print_as_css_value_does_not_escape_path_data() {
980 let s = path("a\"b").print_as_css_value();
983 assert_eq!(s, "path(\"a\"b\")");
984 let uni = path("M 0 0 \u{2192} \u{1F600}").print_as_css_value();
986 assert!(uni.contains('\u{1F600}'));
987 }
988
989 #[test]
994 fn format_as_rust_code_known_constructions() {
995 assert_eq!(
996 CssShape::circle(ShapePoint::new(1.0, 2.0), 3.0).format_as_rust_code(),
997 "CssShape::Circle(ShapeCircle { center: ShapePoint::new(1_f32, 2_f32), radius: 3_f32 \
998 })"
999 );
1000 assert_eq!(
1001 CssShape::ellipse(ShapePoint::new(1.0, 2.0), 3.0, 4.0).format_as_rust_code(),
1002 "CssShape::Ellipse(ShapeEllipse { center: ShapePoint::new(1_f32, 2_f32), radius_x: \
1003 3_f32, radius_y: 4_f32 })"
1004 );
1005 assert_eq!(
1006 CssShape::inset(1.0, 2.0, 3.0, 4.0).format_as_rust_code(),
1007 "CssShape::Inset(ShapeInset { inset_top: 1_f32, inset_right: 2_f32, inset_bottom: \
1008 3_f32, inset_left: 4_f32, border_radius: OptionF32::None })"
1009 );
1010 assert!(CssShape::inset_rounded(0.0, 0.0, 0.0, 0.0, 5.0)
1011 .format_as_rust_code()
1012 .contains("border_radius: OptionF32::Some(5_f32)"));
1013 assert_eq!(
1014 path("M 0 0").format_as_rust_code(),
1015 "CssShape::Path(ShapePath { data: AzString::from_const_str(\"M 0 0\") })"
1016 );
1017 }
1018
1019 #[test]
1020 fn format_as_rust_code_is_non_empty_for_every_variant() {
1021 let shapes = vec![
1022 CssShape::circle(ShapePoint::zero(), 0.0),
1023 CssShape::ellipse(ShapePoint::zero(), 0.0, 0.0),
1024 poly(&[]),
1025 CssShape::inset(0.0, 0.0, 0.0, 0.0),
1026 path(""),
1027 ];
1028 for s in &shapes {
1029 let code = s.format_as_rust_code();
1030 assert!(code.starts_with("CssShape::"), "got {code}");
1031 assert!(code.ends_with(')'), "got {code}");
1032 assert!(!code.is_empty());
1033 }
1034 }
1035
1036 #[test]
1037 fn format_as_rust_code_empty_polygon_emits_empty_vec() {
1038 assert_eq!(
1039 poly(&[]).format_as_rust_code(),
1040 "CssShape::Polygon(ShapePolygon { points: vec![].into() })"
1041 );
1042 assert_eq!(
1043 poly(&[(0.0, 0.0), (1.0, 1.0)]).format_as_rust_code(),
1044 "CssShape::Polygon(ShapePolygon { points: vec![ShapePoint::new(0_f32, 0_f32), \
1045 ShapePoint::new(1_f32, 1_f32)].into() })"
1046 );
1047 }
1048
1049 #[test]
1050 fn format_as_rust_code_emits_uncompilable_tokens_for_nan_and_inf() {
1051 let code = CssShape::circle(ShapePoint::new(f32::INFINITY, f32::NEG_INFINITY), f32::NAN)
1055 .format_as_rust_code();
1056 assert!(code.contains("NaN_f32"), "got {code}");
1057 assert!(code.contains("inf_f32"), "got {code}");
1058 assert!(code.contains("-inf_f32"), "got {code}");
1059 }
1060
1061 #[test]
1062 fn format_as_rust_code_does_not_escape_path_data() {
1063 let code = path("a\"b\\c").format_as_rust_code();
1066 assert!(code.contains("from_const_str(\"a\"b\\c\")"), "got {code}");
1067 }
1068
1069 #[test]
1070 fn format_as_rust_code_survives_extreme_values() {
1071 for &v in EDGE_F32 {
1072 let code = CssShape::inset_rounded(v, v, v, v, v).format_as_rust_code();
1073 assert!(code.starts_with("CssShape::Inset("), "got {code}");
1074 }
1075 }
1076
1077 #[test]
1082 fn roundtrip_circle_ellipse_inset_and_path() {
1083 for shape in [
1084 CssShape::circle(ShapePoint::new(100.0, -25.5), 50.0),
1085 CssShape::circle(ShapePoint::zero(), 0.0),
1086 CssShape::ellipse(ShapePoint::new(-1.0, 2.0), 3.0, 4.5),
1087 CssShape::inset(1.0, 2.0, 3.0, 4.0),
1088 CssShape::inset(-1.0, -2.0, -3.0, -4.0),
1089 CssShape::inset_rounded(1.0, 2.0, 3.0, 4.0, 5.0),
1090 path("M 0 0 L 100 0 L 100 100 Z"),
1091 path(""),
1092 ] {
1093 assert_eq!(roundtrip(&shape), shape);
1094 }
1095 }
1096
1097 #[test]
1098 fn roundtrip_polygon_needs_at_least_three_points() {
1099 let ok = poly(&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]);
1100 assert_eq!(roundtrip(&ok), ok);
1101
1102 let two = poly(&[(0.0, 0.0), (1.0, 1.0)]).print_as_css_value();
1105 assert!(matches!(
1106 parse_shape(&two),
1107 Err(ShapeParseError::InvalidSyntax(_))
1108 ));
1109 let empty = poly(&[]).print_as_css_value();
1110 assert!(matches!(
1111 parse_shape(&empty),
1112 Err(ShapeParseError::InvalidSyntax(_))
1113 ));
1114 }
1115
1116 #[test]
1117 fn roundtrip_preserves_extreme_finite_floats_bit_for_bit() {
1118 for &v in &[
1119 f32::MIN,
1120 f32::MAX,
1121 f32::MIN_POSITIVE,
1122 f32::EPSILON,
1123 f32::from_bits(1), 1e-7,
1125 123_456.79,
1126 ] {
1127 let shape = CssShape::circle(ShapePoint::new(v, -v), v);
1128 match roundtrip(&shape) {
1129 CssShape::Circle(c) => {
1130 assert_eq!(c.radius.to_bits(), v.to_bits(), "radius lost for {v:e}");
1131 assert_eq!(c.center.x.to_bits(), v.to_bits(), "x lost for {v:e}");
1132 assert_eq!(c.center.y.to_bits(), (-v).to_bits(), "y lost for {v:e}");
1133 }
1134 other => panic!("expected Circle, got {other:?}"),
1135 }
1136 }
1137 }
1138
1139 #[test]
1140 fn roundtrip_of_non_finite_values_survives_but_is_not_valid_css() {
1141 match roundtrip(&CssShape::circle(ShapePoint::zero(), f32::INFINITY)) {
1144 CssShape::Circle(c) => assert!(c.radius.is_infinite() && c.radius > 0.0),
1145 other => panic!("expected Circle, got {other:?}"),
1146 }
1147 match roundtrip(&CssShape::circle(ShapePoint::zero(), f32::NAN)) {
1148 CssShape::Circle(c) => assert!(c.radius.is_nan()),
1149 other => panic!("expected Circle, got {other:?}"),
1150 }
1151 match roundtrip(&CssShape::inset(
1152 f32::NEG_INFINITY,
1153 f32::INFINITY,
1154 f32::NAN,
1155 0.0,
1156 )) {
1157 CssShape::Inset(i) => {
1158 assert!(i.inset_top.is_infinite() && i.inset_top < 0.0);
1159 assert!(i.inset_right.is_infinite() && i.inset_right > 0.0);
1160 assert!(i.inset_bottom.is_nan());
1161 assert_eq!(i.inset_left, 0.0);
1162 }
1163 other => panic!("expected Inset, got {other:?}"),
1164 }
1165 }
1166
1167 #[test]
1168 fn roundtrip_path_with_hostile_and_unicode_data() {
1169 for data in [
1172 "M 0 0)",
1173 "M 0 0 \u{2192} \u{1F600}",
1174 "M\n0\t0",
1175 "a\"b",
1176 "\u{0}",
1177 ] {
1178 let shape = path(data);
1179 assert_eq!(
1180 roundtrip(&shape),
1181 shape,
1182 "path data {data:?} did not survive"
1183 );
1184 }
1185 }
1186
1187 #[test]
1188 fn roundtrip_huge_polygon() {
1189 let coords: Vec<(f32, f32)> = (0..2_000).map(|i| (i as f32, -(i as f32))).collect();
1190 let shape = poly(&coords);
1191 assert_eq!(roundtrip(&shape), shape);
1192 }
1193}