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