1pub use crate::drag::{DragDelta, OptionDragDelta};
9
10#[derive(Copy, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
12#[repr(C)]
13pub struct LogicalRect {
14 pub origin: LogicalPosition,
15 pub size: LogicalSize,
16}
17
18impl core::fmt::Debug for LogicalRect {
19 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20 write!(f, "{} @ {}", self.size, self.origin)
21 }
22}
23
24impl core::fmt::Display for LogicalRect {
25 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
26 write!(f, "{} @ {}", self.size, self.origin)
27 }
28}
29
30impl LogicalRect {
31 #[must_use] pub const fn zero() -> Self {
32 Self::new(LogicalPosition::zero(), LogicalSize::zero())
33 }
34 #[must_use] pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
35 Self { origin, size }
36 }
37
38 #[inline]
40 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
41 self.origin.x *= scale_factor;
42 self.origin.y *= scale_factor;
43 self.size.width *= scale_factor;
44 self.size.height *= scale_factor;
45 }
46
47 #[inline]
49 #[must_use] pub fn max_x(&self) -> f32 {
50 self.origin.x + self.size.width
51 }
52 #[inline]
54 #[must_use] pub const fn min_x(&self) -> f32 {
55 self.origin.x
56 }
57 #[inline]
59 #[must_use] pub fn max_y(&self) -> f32 {
60 self.origin.y + self.size.height
61 }
62 #[inline]
64 #[must_use] pub const fn min_y(&self) -> f32 {
65 self.origin.y
66 }
67
68 #[inline]
70 #[must_use] pub fn intersects(&self, other: Self) -> bool {
71 if self.max_x() <= other.min_x() || other.max_x() <= self.min_x() {
73 return false;
74 }
75
76 if self.max_y() <= other.min_y() || other.max_y() <= self.min_y() {
78 return false;
79 }
80
81 true
83 }
84
85 #[inline]
87 #[must_use] pub fn contains(&self, point: LogicalPosition) -> bool {
88 point.x >= self.min_x()
89 && point.x < self.max_x()
90 && point.y >= self.min_y()
91 && point.y < self.max_y()
92 }
93
94 #[inline]
98 #[must_use] pub fn hit_test(&self, other: &LogicalPosition) -> Option<LogicalPosition> {
99 let dx_left_edge = other.x - self.min_x();
100 let dx_right_edge = self.max_x() - other.x;
101 let dy_top_edge = other.y - self.min_y();
102 let dy_bottom_edge = self.max_y() - other.y;
103 if dx_left_edge >= 0.0 && dx_right_edge > 0.0 && dy_top_edge >= 0.0 && dy_bottom_edge > 0.0 {
109 Some(LogicalPosition::new(dx_left_edge, dy_top_edge))
110 } else {
111 None
112 }
113 }
114
115}
116
117impl_vec!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor, LogicalRectVecDestructorType, LogicalRectVecSlice, OptionLogicalRect);
118impl_vec_clone!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor);
119impl_vec_debug!(LogicalRect, LogicalRectVec);
120impl_vec_partialeq!(LogicalRect, LogicalRectVec);
121impl_vec_partialord!(LogicalRect, LogicalRectVec);
122impl_vec_ord!(LogicalRect, LogicalRectVec);
123impl_vec_hash!(LogicalRect, LogicalRectVec);
124impl_vec_eq!(LogicalRect, LogicalRectVec);
125
126use core::{
127 cmp::Ordering,
128 hash::{Hash, Hasher},
129 ops::{self, AddAssign, SubAssign},
130};
131
132use azul_css::props::layout::LayoutWritingMode;
133
134#[derive(Default, Copy, Clone)]
140#[repr(C)]
141pub struct LogicalPosition {
142 pub x: f32,
143 pub y: f32,
144}
145
146impl PartialEq for LogicalPosition {
147 fn eq(&self, other: &Self) -> bool {
148 quantize(self.x) == quantize(other.x) && quantize(self.y) == quantize(other.y)
149 }
150}
151
152impl LogicalPosition {
153 pub fn scale_for_dpi(&mut self, scale_factor: f32) {
155 self.x *= scale_factor;
156 self.y *= scale_factor;
157 }
158}
159
160impl SubAssign<Self> for LogicalPosition {
161 fn sub_assign(&mut self, other: Self) {
162 self.x -= other.x;
163 self.y -= other.y;
164 }
165}
166
167impl AddAssign<Self> for LogicalPosition {
168 fn add_assign(&mut self, other: Self) {
169 self.x += other.x;
170 self.y += other.y;
171 }
172}
173
174impl core::fmt::Debug for LogicalPosition {
175 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
176 write!(f, "({}, {})", self.x, self.y)
177 }
178}
179
180impl core::fmt::Display for LogicalPosition {
181 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
182 write!(f, "({}, {})", self.x, self.y)
183 }
184}
185
186impl ops::Add for LogicalPosition {
187 type Output = Self;
188
189 #[inline]
190 fn add(self, other: Self) -> Self {
191 Self {
192 x: self.x + other.x,
193 y: self.y + other.y,
194 }
195 }
196}
197
198impl ops::Sub for LogicalPosition {
199 type Output = Self;
200
201 #[inline]
202 fn sub(self, other: Self) -> Self {
203 Self {
204 x: self.x - other.x,
205 y: self.y - other.y,
206 }
207 }
208}
209
210const DECIMAL_MULTIPLIER: f32 = 1000.0;
213
214#[allow(clippy::cast_possible_truncation)]
218fn quantize(value: f32) -> i64 {
219 if value.is_nan() {
224 return i64::MIN;
225 }
226 (value * DECIMAL_MULTIPLIER) as i64
231}
232
233impl_option!(
234 LogicalPosition,
235 OptionLogicalPosition,
236 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
237);
238
239impl PartialOrd for LogicalPosition {
242 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
243 Some(self.cmp(other))
244 }
245}
246impl Ord for LogicalPosition {
247 fn cmp(&self, other: &Self) -> Ordering {
248 let self_x = quantize(self.x);
249 let self_y = quantize(self.y);
250 let other_x = quantize(other.x);
251 let other_y = quantize(other.y);
252 self_x.cmp(&other_x).then(self_y.cmp(&other_y))
253 }
254}
255
256impl Eq for LogicalPosition {}
257
258impl Hash for LogicalPosition {
259 fn hash<H>(&self, state: &mut H)
260 where
261 H: Hasher,
262 {
263 let self_x = quantize(self.x);
264 let self_y = quantize(self.y);
265 self_x.hash(state);
266 self_y.hash(state);
267 }
268}
269
270impl LogicalPosition {
271 #[must_use] pub const fn main(&self, wm: LayoutWritingMode) -> f32 {
273 match wm {
274 LayoutWritingMode::HorizontalTb => self.y,
275 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.x,
276 }
277 }
278
279 #[must_use] pub const fn cross(&self, wm: LayoutWritingMode) -> f32 {
281 match wm {
282 LayoutWritingMode::HorizontalTb => self.x,
283 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.y,
284 }
285 }
286
287 #[must_use] pub const fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
289 match wm {
290 LayoutWritingMode::HorizontalTb => Self::new(cross, main),
291 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
292 }
293 }
294}
295
296#[derive(Default, Copy, Clone)]
300#[repr(C)]
301pub struct LogicalSize {
302 pub width: f32,
303 pub height: f32,
304}
305
306impl PartialEq for LogicalSize {
307 fn eq(&self, other: &Self) -> bool {
308 quantize(self.width) == quantize(other.width)
309 && quantize(self.height) == quantize(other.height)
310 }
311}
312
313impl LogicalSize {
314 #[allow(clippy::return_self_not_must_use)]
318 pub fn scale_for_dpi(&mut self, scale_factor: f32) -> Self {
319 self.width *= scale_factor;
320 self.height *= scale_factor;
321 *self
322 }
323
324 #[must_use] pub const fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
326 match wm {
327 LayoutWritingMode::HorizontalTb => Self::new(cross, main),
328 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
329 }
330 }
331}
332
333impl core::fmt::Debug for LogicalSize {
334 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
335 write!(f, "{}x{}", self.width, self.height)
336 }
337}
338
339impl core::fmt::Display for LogicalSize {
340 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
341 write!(f, "{}x{}", self.width, self.height)
342 }
343}
344
345impl_option!(
346 LogicalSize,
347 OptionLogicalSize,
348 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
349);
350
351impl_option!(
352 LogicalRect,
353 OptionLogicalRect,
354 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
355);
356
357impl PartialOrd for LogicalSize {
360 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
361 Some(self.cmp(other))
362 }
363}
364impl Ord for LogicalSize {
365 fn cmp(&self, other: &Self) -> Ordering {
366 let self_width = quantize(self.width);
367 let self_height = quantize(self.height);
368 let other_width = quantize(other.width);
369 let other_height = quantize(other.height);
370 self_width
371 .cmp(&other_width)
372 .then(self_height.cmp(&other_height))
373 }
374}
375
376impl Eq for LogicalSize {}
377
378impl Hash for LogicalSize {
379 fn hash<H>(&self, state: &mut H)
380 where
381 H: Hasher,
382 {
383 let self_width = quantize(self.width);
384 let self_height = quantize(self.height);
385 self_width.hash(state);
386 self_height.hash(state);
387 }
388}
389
390impl LogicalSize {
391 #[must_use] pub const fn main(&self, wm: LayoutWritingMode) -> f32 {
393 match wm {
394 LayoutWritingMode::HorizontalTb => self.height,
395 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.width,
396 }
397 }
398
399 #[must_use] pub const fn cross(&self, wm: LayoutWritingMode) -> f32 {
401 match wm {
402 LayoutWritingMode::HorizontalTb => self.width,
403 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.height,
404 }
405 }
406
407 #[must_use] pub const fn with_main(self, wm: LayoutWritingMode, value: f32) -> Self {
409 match wm {
410 LayoutWritingMode::HorizontalTb => Self {
411 height: value,
412 ..self
413 },
414 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
415 width: value,
416 ..self
417 },
418 }
419 }
420
421 #[must_use] pub const fn with_cross(self, wm: LayoutWritingMode, value: f32) -> Self {
423 match wm {
424 LayoutWritingMode::HorizontalTb => Self {
425 width: value,
426 ..self
427 },
428 LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
429 height: value,
430 ..self
431 },
432 }
433 }
434}
435
436#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
438#[repr(C)]
439pub struct PhysicalPosition<T> {
440 pub x: T,
441 pub y: T,
442}
443
444impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalPosition<T> {
445 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
446 write!(f, "({}, {})", self.x, self.y)
447 }
448}
449
450pub type PhysicalPositionI32 = PhysicalPosition<i32>;
451impl_option!(
452 PhysicalPositionI32,
453 OptionPhysicalPositionI32,
454 [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
455);
456
457#[derive(Ord, Hash, Eq, Copy, Clone, PartialEq, PartialOrd)]
459#[repr(C)]
460pub struct PhysicalSize<T> {
461 pub width: T,
462 pub height: T,
463}
464
465impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalSize<T> {
466 fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
467 write!(f, "{}x{}", self.width, self.height)
468 }
469}
470
471pub type PhysicalSizeU32 = PhysicalSize<u32>;
472impl_option!(
473 PhysicalSizeU32,
474 OptionPhysicalSizeU32,
475 [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
476);
477pub type PhysicalSizeF32 = PhysicalSize<f32>;
478impl_option!(
479 PhysicalSizeF32,
480 OptionPhysicalSizeF32,
481 [Debug, Copy, Clone, PartialEq, PartialOrd]
482);
483
484impl LogicalPosition {
485 #[inline]
486 #[must_use] pub const fn new(x: f32, y: f32) -> Self {
487 Self { x, y }
488 }
489 #[inline]
490 #[must_use] pub const fn zero() -> Self {
491 Self::new(0.0, 0.0)
492 }
493 #[inline]
495 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
496 #[must_use] pub fn to_physical(self, hidpi_factor: f32) -> PhysicalPosition<u32> {
497 PhysicalPosition {
498 x: libm::roundf(self.x * hidpi_factor) as u32,
499 y: libm::roundf(self.y * hidpi_factor) as u32,
500 }
501 }
502}
503
504impl<T> PhysicalPosition<T> {
505 #[inline]
506 pub const fn new(x: T, y: T) -> Self {
507 Self { x, y }
508 }
509}
510
511impl PhysicalPosition<i32> {
512 #[inline]
513 #[must_use] pub const fn zero() -> Self {
514 Self::new(0, 0)
515 }
516 #[inline]
518 #[allow(clippy::cast_precision_loss)]
519 #[must_use] pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
520 LogicalPosition {
521 x: self.x as f32 / hidpi_factor,
522 y: self.y as f32 / hidpi_factor,
523 }
524 }
525}
526
527impl PhysicalPosition<f64> {
528 #[inline]
529 #[must_use] pub const fn zero() -> Self {
530 Self::new(0.0, 0.0)
531 }
532 #[inline]
534 #[allow(clippy::cast_possible_truncation)]
535 #[must_use] pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
536 LogicalPosition {
537 x: self.x as f32 / hidpi_factor,
538 y: self.y as f32 / hidpi_factor,
539 }
540 }
541}
542
543impl LogicalSize {
544 #[inline]
545 #[must_use] pub const fn new(width: f32, height: f32) -> Self {
546 Self { width, height }
547 }
548 #[inline]
549 #[must_use] pub const fn zero() -> Self {
550 Self::new(0.0, 0.0)
551 }
552 #[inline]
554 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
555 #[must_use] pub fn to_physical(self, hidpi_factor: f32) -> PhysicalSize<u32> {
556 PhysicalSize {
557 width: libm::roundf(self.width * hidpi_factor) as u32,
558 height: libm::roundf(self.height * hidpi_factor) as u32,
559 }
560 }
561}
562
563impl<T> PhysicalSize<T> {
564 #[inline]
565 pub const fn new(width: T, height: T) -> Self {
566 Self { width, height }
567 }
568}
569
570impl PhysicalSize<u32> {
571 #[inline]
572 #[must_use] pub const fn zero() -> Self {
573 Self::new(0, 0)
574 }
575 #[inline]
577 #[allow(clippy::cast_precision_loss)]
578 #[must_use] pub fn to_logical(self, hidpi_factor: f32) -> LogicalSize {
579 LogicalSize {
580 width: self.width as f32 / hidpi_factor,
581 height: self.height as f32 / hidpi_factor,
582 }
583 }
584}
585
586#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
596#[repr(C)]
597pub enum CoordinateSpace {
598 Window,
601
602 ScrollFrame,
605
606 Parent,
608
609 ReferenceFrame,
611}
612
613
614#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
621#[repr(C)]
622pub struct ScreenPosition {
623 pub x: f32,
624 pub y: f32,
625}
626
627impl ScreenPosition {
628 #[inline]
629 #[must_use] pub const fn new(x: f32, y: f32) -> Self {
630 Self { x, y }
631 }
632 #[inline]
633 #[must_use] pub const fn zero() -> Self {
634 Self::new(0.0, 0.0)
635 }
636 #[inline]
638 #[must_use] pub const fn to_logical(self) -> LogicalPosition {
639 LogicalPosition { x: self.x, y: self.y }
640 }
641 #[inline]
643 #[must_use] pub const fn from_logical(p: LogicalPosition) -> Self {
644 Self { x: p.x, y: p.y }
645 }
646}
647
648impl_option!(
649 ScreenPosition,
650 OptionScreenPosition,
651 [Debug, Copy, Clone, PartialEq, PartialOrd]
652);
653
654#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
656#[repr(C)]
657pub struct CursorNodePosition {
658 pub x: f32,
659 pub y: f32,
660}
661
662impl CursorNodePosition {
663 #[inline]
664 #[must_use] pub const fn new(x: f32, y: f32) -> Self {
665 Self { x, y }
666 }
667 #[inline]
668 #[must_use] pub const fn zero() -> Self {
669 Self::new(0.0, 0.0)
670 }
671 #[inline]
672 #[must_use] pub const fn to_logical(self) -> LogicalPosition {
673 LogicalPosition { x: self.x, y: self.y }
674 }
675 #[inline]
676 #[must_use] pub const fn from_logical(p: LogicalPosition) -> Self {
677 Self { x: p.x, y: p.y }
678 }
679}
680
681impl_option!(
682 CursorNodePosition,
683 OptionCursorNodePosition,
684 [Debug, Copy, Clone, PartialEq, PartialOrd]
685);
686
687#[cfg(test)]
688mod tests {
689 use super::*;
690 use core::cmp::Ordering;
691
692 #[test]
693 fn hit_test_edges_match_contains() {
694 let r = LogicalRect::new(LogicalPosition::new(10.0, 20.0), LogicalSize::new(30.0, 40.0));
695 let tl = LogicalPosition::new(10.0, 20.0);
697 assert!(r.contains(tl));
698 assert!(r.hit_test(&tl).is_some());
699 let inside = LogicalPosition::new(11.0, 21.0);
701 assert!(r.contains(inside));
702 assert!(r.hit_test(&inside).is_some());
703 let br = LogicalPosition::new(40.0, 60.0);
705 assert!(!r.contains(br));
706 assert!(r.hit_test(&br).is_none());
707 let out = LogicalPosition::new(9.0, 20.0);
709 assert!(!r.contains(out));
710 assert!(r.hit_test(&out).is_none());
711 }
712
713 #[test]
714 fn hit_test_offset_is_from_top_left() {
715 let r = LogicalRect::new(LogicalPosition::new(10.0, 20.0), LogicalSize::new(30.0, 40.0));
716 let hit = r.hit_test(&LogicalPosition::new(15.0, 25.0)).unwrap();
717 assert_eq!(hit, LogicalPosition::new(5.0, 5.0));
718 }
719
720 #[test]
721 fn quantize_nan_is_distinct_from_zero() {
722 assert_eq!(quantize(f32::NAN), i64::MIN);
723 assert_ne!(quantize(f32::NAN), quantize(0.0));
724 }
725
726 #[test]
727 fn partial_eq_agrees_with_ord_and_hash() {
728 use core::hash::{Hash, Hasher};
729 let a = LogicalPosition::new(1.00000, 2.00000);
731 let b = LogicalPosition::new(1.00004, 2.00004); assert_eq!(a, b);
733 assert_eq!(a.cmp(&b), Ordering::Equal);
734
735 let hash_of = |p: &LogicalPosition| {
736 let mut h = std::collections::hash_map::DefaultHasher::new();
737 p.hash(&mut h);
738 h.finish()
739 };
740 assert_eq!(hash_of(&a), hash_of(&b));
741
742 let n1 = LogicalSize::new(f32::NAN, 1.0);
745 let n2 = LogicalSize::new(f32::NAN, 1.0);
746 assert_eq!(n1, n2);
747 }
748
749 #[test]
750 fn quantize_saturates_instead_of_wrapping() {
751 assert_eq!(quantize(f32::INFINITY), i64::MAX);
753 assert_eq!(quantize(f32::NEG_INFINITY), i64::MIN);
754 }
755}
756
757#[cfg(test)]
758#[allow(clippy::float_cmp)]
759mod autotest_generated {
760 use core::{
761 cmp::Ordering,
762 hash::{Hash, Hasher},
763 };
764
765 use azul_css::props::layout::LayoutWritingMode;
766
767 use super::*;
768
769 const HOSTILE: [f32; 8] = [
771 f32::NAN,
772 f32::NEG_INFINITY,
773 f32::MIN,
774 -1.0,
775 0.0,
776 1.0,
777 f32::MAX,
778 f32::INFINITY,
779 ];
780
781 const WMS: [LayoutWritingMode; 3] = [
782 LayoutWritingMode::HorizontalTb,
783 LayoutWritingMode::VerticalRl,
784 LayoutWritingMode::VerticalLr,
785 ];
786
787 fn hash_of<T: Hash>(v: &T) -> u64 {
788 let mut h = std::collections::hash_map::DefaultHasher::new();
789 v.hash(&mut h);
790 h.finish()
791 }
792
793 #[test]
798 fn quantize_zero_and_negative_zero_share_a_bucket() {
799 assert_eq!(quantize(0.0), 0);
800 assert_eq!(quantize(-0.0), 0);
801 assert_eq!(quantize(0.0), quantize(-0.0));
804 }
805
806 #[test]
807 fn quantize_applies_the_decimal_multiplier() {
808 assert_eq!(quantize(1.0), DECIMAL_MULTIPLIER as i64);
809 assert_eq!(quantize(-1.0), -(DECIMAL_MULTIPLIER as i64));
810 assert_eq!(quantize(1.5), 1500);
811 assert_eq!(quantize(-1.5), -1500);
812 }
813
814 #[test]
815 fn quantize_truncates_toward_zero_below_precision() {
816 assert_eq!(quantize(0.0004), 0);
819 assert_eq!(quantize(-0.0004), 0);
820 assert_eq!(quantize(1.0004), 1000);
821 assert_eq!(quantize(-1.0004), -1000);
822 }
823
824 #[test]
825 fn quantize_extremes_saturate_and_never_wrap() {
826 assert_eq!(quantize(f32::MAX), i64::MAX);
828 assert_eq!(quantize(f32::MIN), i64::MIN);
829 assert_eq!(quantize(f32::INFINITY), i64::MAX);
830 assert_eq!(quantize(f32::NEG_INFINITY), i64::MIN);
831 assert_eq!(quantize(f32::MIN_POSITIVE), 0);
833 assert_eq!(quantize(-f32::MIN_POSITIVE), 0);
834 }
835
836 #[test]
837 fn quantize_nan_never_aliases_the_origin() {
838 assert_eq!(quantize(f32::NAN), i64::MIN);
840 assert_eq!(quantize(-f32::NAN), i64::MIN);
841 assert_ne!(quantize(f32::NAN), quantize(0.0));
842 }
843
844 #[test]
845 fn quantize_saturation_aliases_nan_with_the_bottom_of_the_range() {
846 assert_eq!(quantize(f32::NAN), quantize(f32::NEG_INFINITY));
851 assert_eq!(quantize(f32::NAN), quantize(f32::MIN));
852 assert_eq!(
853 LogicalPosition::new(f32::NAN, 0.0),
854 LogicalPosition::new(f32::NEG_INFINITY, 0.0)
855 );
856 }
857
858 #[test]
859 fn quantize_is_monotonic_over_finite_inputs() {
860 let ascending = [-1.0e6_f32, -1.0, -0.001, 0.0, 0.001, 1.0, 1.0e6];
861 for w in ascending.windows(2) {
862 assert!(
863 quantize(w[0]) <= quantize(w[1]),
864 "quantize inverted the order of {} and {}",
865 w[0],
866 w[1]
867 );
868 }
869 }
870
871 #[test]
872 fn quantize_is_deterministic_across_calls() {
873 for v in HOSTILE {
874 assert_eq!(quantize(v), quantize(v));
875 }
876 }
877
878 fn hostile_positions() -> [LogicalPosition; 64] {
883 let mut out = [LogicalPosition::zero(); 64];
884 let mut i = 0;
885 for x in HOSTILE {
886 for y in HOSTILE {
887 out[i] = LogicalPosition::new(x, y);
888 i += 1;
889 }
890 }
891 out
892 }
893
894 #[test]
895 fn ord_is_reflexive_and_antisymmetric_even_with_nan() {
896 let grid = hostile_positions();
897 for a in grid {
898 assert_eq!(a.cmp(&a), Ordering::Equal);
900 assert_eq!(a, a);
901 for b in grid {
902 assert_eq!(a.cmp(&b), b.cmp(&a).reverse());
903 }
904 }
905 }
906
907 #[test]
908 fn ord_is_transitive_over_the_hostile_grid() {
909 let grid = hostile_positions();
910 for a in grid {
911 for b in grid {
912 if a.cmp(&b) != Ordering::Less {
913 continue;
914 }
915 for c in grid {
916 if b.cmp(&c) == Ordering::Less {
917 assert_eq!(a.cmp(&c), Ordering::Less);
918 }
919 }
920 }
921 }
922 }
923
924 #[test]
925 fn partial_eq_ord_and_hash_agree_over_the_hostile_grid() {
926 let grid = hostile_positions();
927 for a in grid {
928 for b in grid {
929 let eq = a == b;
930 assert_eq!(eq, a.cmp(&b) == Ordering::Equal);
931 assert_eq!(Some(a.cmp(&b)), a.partial_cmp(&b));
932 if eq {
933 assert_eq!(hash_of(&a), hash_of(&b));
936 }
937 }
938 }
939 }
940
941 #[test]
942 fn logical_size_eq_and_hash_agree_including_nan() {
943 for w in HOSTILE {
944 for h in HOSTILE {
945 let a = LogicalSize::new(w, h);
946 let b = LogicalSize::new(w, h);
947 assert_eq!(a, b);
948 assert_eq!(a.cmp(&b), Ordering::Equal);
949 assert_eq!(hash_of(&a), hash_of(&b));
950 }
951 }
952 }
953
954 #[test]
955 fn logical_rect_eq_and_hash_are_quantized_through_its_fields() {
956 let a = LogicalRect::new(
959 LogicalPosition::new(f32::NAN, 1.0),
960 LogicalSize::new(f32::NAN, 2.0),
961 );
962 let b = a;
963 assert_eq!(a, b);
964 assert_eq!(hash_of(&a), hash_of(&b));
965
966 let c = LogicalRect::new(
968 LogicalPosition::new(1.0, 2.0),
969 LogicalSize::new(3.0, 4.0),
970 );
971 let d = LogicalRect::new(
972 LogicalPosition::new(1.00004, 2.00004),
973 LogicalSize::new(3.00004, 4.00004),
974 );
975 assert_eq!(c, d);
976 assert_eq!(hash_of(&c), hash_of(&d));
977 }
978
979 #[test]
984 fn constructors_preserve_fields_for_extreme_arguments() {
985 for x in HOSTILE {
986 for y in HOSTILE {
987 let p = LogicalPosition::new(x, y);
988 assert_eq!(p.x.to_bits(), x.to_bits());
989 assert_eq!(p.y.to_bits(), y.to_bits());
990
991 let s = LogicalSize::new(x, y);
992 assert_eq!(s.width.to_bits(), x.to_bits());
993 assert_eq!(s.height.to_bits(), y.to_bits());
994
995 let r = LogicalRect::new(p, s);
996 assert_eq!(r.origin.x.to_bits(), x.to_bits());
997 assert_eq!(r.size.height.to_bits(), y.to_bits());
998
999 assert_eq!(ScreenPosition::new(x, y).x.to_bits(), x.to_bits());
1000 assert_eq!(CursorNodePosition::new(x, y).y.to_bits(), y.to_bits());
1001 assert_eq!(PhysicalPosition::new(x, y).x.to_bits(), x.to_bits());
1002 assert_eq!(PhysicalSize::new(x, y).height.to_bits(), y.to_bits());
1003 }
1004 }
1005 }
1006
1007 #[test]
1008 fn zero_constructors_are_neutral_and_match_default() {
1009 assert_eq!(LogicalPosition::zero(), LogicalPosition::default());
1010 assert_eq!(LogicalSize::zero(), LogicalSize::default());
1011 assert_eq!(LogicalRect::zero(), LogicalRect::default());
1012 assert_eq!(LogicalRect::zero().origin, LogicalPosition::zero());
1013 assert_eq!(LogicalRect::zero().size, LogicalSize::zero());
1014
1015 assert_eq!(ScreenPosition::zero(), ScreenPosition::default());
1016 assert_eq!(CursorNodePosition::zero(), CursorNodePosition::default());
1017
1018 assert_eq!(PhysicalPosition::<i32>::zero(), PhysicalPosition::new(0, 0));
1019 assert_eq!(
1020 PhysicalPosition::<f64>::zero(),
1021 PhysicalPosition::new(0.0_f64, 0.0_f64)
1022 );
1023 assert_eq!(PhysicalSize::<u32>::zero(), PhysicalSize::new(0, 0));
1024
1025 let z = LogicalRect::zero();
1028 assert!(!z.contains(LogicalPosition::zero()));
1029 assert!(!z.intersects(z));
1030 assert_eq!(z.min_x(), 0.0);
1031 assert_eq!(z.max_x(), 0.0);
1032 assert_eq!(z.min_y(), 0.0);
1033 assert_eq!(z.max_y(), 0.0);
1034 }
1035
1036 #[test]
1041 fn rect_getters_return_the_constructed_edges() {
1042 let r = LogicalRect::new(
1043 LogicalPosition::new(10.0, 20.0),
1044 LogicalSize::new(30.0, 40.0),
1045 );
1046 assert_eq!(r.min_x(), 10.0);
1047 assert_eq!(r.max_x(), 40.0);
1048 assert_eq!(r.min_y(), 20.0);
1049 assert_eq!(r.max_y(), 60.0);
1050 }
1051
1052 #[test]
1053 fn rect_getters_do_not_panic_on_extreme_geometry() {
1054 for x in HOSTILE {
1055 for w in HOSTILE {
1056 let r = LogicalRect::new(
1057 LogicalPosition::new(x, x),
1058 LogicalSize::new(w, w),
1059 );
1060 let _ = r.min_x();
1062 let _ = r.max_x();
1063 let _ = r.min_y();
1064 let _ = r.max_y();
1065 }
1066 }
1067 let r = LogicalRect::new(
1070 LogicalPosition::new(f32::INFINITY, f32::INFINITY),
1071 LogicalSize::new(f32::NEG_INFINITY, f32::NEG_INFINITY),
1072 );
1073 assert!(r.max_x().is_nan());
1074 assert!(r.max_y().is_nan());
1075 }
1076
1077 #[test]
1082 fn contains_is_half_open_left_top_inclusive_right_bottom_exclusive() {
1083 let r = LogicalRect::new(
1084 LogicalPosition::new(10.0, 20.0),
1085 LogicalSize::new(30.0, 40.0),
1086 );
1087 assert!(r.contains(LogicalPosition::new(10.0, 20.0))); assert!(!r.contains(LogicalPosition::new(40.0, 59.0))); assert!(!r.contains(LogicalPosition::new(39.0, 60.0))); assert!(!r.contains(LogicalPosition::new(40.0, 60.0))); assert!(r.contains(LogicalPosition::new(39.999, 59.999)));
1092 }
1093
1094 #[test]
1095 fn contains_and_hit_test_agree_on_the_hostile_grid() {
1096 let rects = [
1098 LogicalRect::zero(),
1099 LogicalRect::new(LogicalPosition::new(10.0, 20.0), LogicalSize::new(30.0, 40.0)),
1100 LogicalRect::new(LogicalPosition::new(-5.0, -5.0), LogicalSize::new(10.0, 10.0)),
1101 LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(-10.0, -10.0)),
1103 LogicalRect::new(
1104 LogicalPosition::new(f32::NAN, f32::NAN),
1105 LogicalSize::new(f32::NAN, f32::NAN),
1106 ),
1107 LogicalRect::new(
1108 LogicalPosition::zero(),
1109 LogicalSize::new(f32::INFINITY, f32::INFINITY),
1110 ),
1111 ];
1112 for r in rects {
1113 for x in HOSTILE {
1114 for y in HOSTILE {
1115 let p = LogicalPosition::new(x, y);
1116 assert_eq!(
1117 r.contains(p),
1118 r.hit_test(&p).is_some(),
1119 "contains/hit_test disagree for {r:?} at {p:?}"
1120 );
1121 }
1122 }
1123 }
1124 }
1125
1126 #[test]
1127 fn contains_rejects_nan_points_and_nan_rects() {
1128 let r = LogicalRect::new(
1129 LogicalPosition::new(0.0, 0.0),
1130 LogicalSize::new(100.0, 100.0),
1131 );
1132 assert!(!r.contains(LogicalPosition::new(f32::NAN, 50.0)));
1134 assert!(!r.contains(LogicalPosition::new(50.0, f32::NAN)));
1135 assert!(!r.contains(LogicalPosition::new(f32::NAN, f32::NAN)));
1136
1137 let nan_rect = LogicalRect::new(
1138 LogicalPosition::new(f32::NAN, f32::NAN),
1139 LogicalSize::new(f32::NAN, f32::NAN),
1140 );
1141 assert!(!nan_rect.contains(LogicalPosition::zero()));
1142 assert!(nan_rect.hit_test(&LogicalPosition::zero()).is_none());
1143 }
1144
1145 #[test]
1146 fn contains_handles_negative_extent_rects_without_panicking() {
1147 let r = LogicalRect::new(
1149 LogicalPosition::new(0.0, 0.0),
1150 LogicalSize::new(-10.0, -10.0),
1151 );
1152 assert!(!r.contains(LogicalPosition::zero()));
1153 assert!(!r.contains(LogicalPosition::new(-5.0, -5.0)));
1154 assert!(r.hit_test(&LogicalPosition::new(-5.0, -5.0)).is_none());
1155 }
1156
1157 #[test]
1158 fn contains_at_the_coordinate_extremes() {
1159 let huge = LogicalRect::new(
1160 LogicalPosition::new(f32::MIN, f32::MIN),
1161 LogicalSize::new(f32::MAX, f32::MAX),
1162 );
1163 assert_eq!(huge.max_x(), 0.0);
1165 assert!(huge.contains(LogicalPosition::new(-1.0, -1.0)));
1166 assert!(!huge.contains(LogicalPosition::zero()));
1167
1168 let unbounded = LogicalRect::new(
1169 LogicalPosition::new(f32::NEG_INFINITY, f32::NEG_INFINITY),
1170 LogicalSize::new(f32::INFINITY, f32::INFINITY),
1171 );
1172 assert!(unbounded.max_x().is_nan());
1175 assert!(!unbounded.contains(LogicalPosition::zero()));
1176 }
1177
1178 #[test]
1179 fn hit_test_returns_the_offset_from_the_top_left_corner() {
1180 let r = LogicalRect::new(
1181 LogicalPosition::new(10.0, 20.0),
1182 LogicalSize::new(30.0, 40.0),
1183 );
1184 assert_eq!(
1185 r.hit_test(&LogicalPosition::new(10.0, 20.0)),
1186 Some(LogicalPosition::new(0.0, 0.0))
1187 );
1188 assert_eq!(
1189 r.hit_test(&LogicalPosition::new(25.0, 45.0)),
1190 Some(LogicalPosition::new(15.0, 25.0))
1191 );
1192 assert_eq!(r.hit_test(&LogicalPosition::new(40.0, 30.0)), None);
1194 assert_eq!(r.hit_test(&LogicalPosition::new(30.0, 60.0)), None);
1195 }
1196
1197 #[test]
1198 fn hit_test_offset_is_always_non_negative_when_it_hits() {
1199 let r = LogicalRect::new(
1200 LogicalPosition::new(-100.0, -100.0),
1201 LogicalSize::new(200.0, 200.0),
1202 );
1203 for x in [-100.0_f32, -50.0, 0.0, 50.0, 99.5] {
1206 for y in [-100.0_f32, -50.0, 0.0, 50.0, 99.5] {
1207 let hit = r.hit_test(&LogicalPosition::new(x, y)).expect("inside");
1208 assert!(hit.x >= 0.0 && hit.y >= 0.0, "negative offset {hit:?}");
1209 assert_eq!(r.origin.x + hit.x, x);
1210 assert_eq!(r.origin.y + hit.y, y);
1211 }
1212 }
1213 }
1214
1215 #[test]
1216 fn intersects_is_symmetric_even_for_degenerate_and_nan_rects() {
1217 let rects = [
1218 LogicalRect::zero(),
1219 LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(10.0, 10.0)),
1220 LogicalRect::new(LogicalPosition::new(5.0, 5.0), LogicalSize::new(10.0, 10.0)),
1221 LogicalRect::new(LogicalPosition::new(10.0, 0.0), LogicalSize::new(10.0, 10.0)),
1222 LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(-10.0, -10.0)),
1223 LogicalRect::new(
1224 LogicalPosition::new(f32::NAN, f32::NAN),
1225 LogicalSize::new(f32::NAN, f32::NAN),
1226 ),
1227 LogicalRect::new(
1228 LogicalPosition::new(f32::MIN, f32::MIN),
1229 LogicalSize::new(f32::MAX, f32::MAX),
1230 ),
1231 ];
1232 for a in rects {
1233 for b in rects {
1234 assert_eq!(
1235 a.intersects(b),
1236 b.intersects(a),
1237 "intersects is asymmetric for {a:?} / {b:?}"
1238 );
1239 }
1240 }
1241 }
1242
1243 #[test]
1244 fn intersects_touching_edges_do_not_count_as_overlap() {
1245 let a = LogicalRect::new(LogicalPosition::new(0.0, 0.0), LogicalSize::new(10.0, 10.0));
1246 let touching = LogicalRect::new(
1247 LogicalPosition::new(10.0, 0.0),
1248 LogicalSize::new(10.0, 10.0),
1249 );
1250 let overlapping = LogicalRect::new(
1251 LogicalPosition::new(9.99, 0.0),
1252 LogicalSize::new(10.0, 10.0),
1253 );
1254 assert!(!a.intersects(touching));
1255 assert!(a.intersects(overlapping));
1256 assert!(a.intersects(a));
1257 assert!(!LogicalRect::zero().intersects(a));
1259 }
1260
1261 #[test]
1262 fn intersects_with_nan_rect_is_permissive_current_behavior() {
1263 let nan_rect = LogicalRect::new(
1269 LogicalPosition::new(f32::NAN, f32::NAN),
1270 LogicalSize::new(f32::NAN, f32::NAN),
1271 );
1272 let normal = LogicalRect::new(
1273 LogicalPosition::new(0.0, 0.0),
1274 LogicalSize::new(10.0, 10.0),
1275 );
1276 assert!(nan_rect.intersects(normal));
1277 assert!(normal.intersects(nan_rect));
1278 assert!(!nan_rect.contains(LogicalPosition::zero()));
1279 }
1280
1281 #[test]
1286 fn scale_for_dpi_by_one_is_the_identity() {
1287 let mut p = LogicalPosition::new(1.5, -2.5);
1288 p.scale_for_dpi(1.0);
1289 assert_eq!(p, LogicalPosition::new(1.5, -2.5));
1290
1291 let mut s = LogicalSize::new(3.5, 4.5);
1292 assert_eq!(s.scale_for_dpi(1.0), LogicalSize::new(3.5, 4.5));
1293
1294 let mut r = LogicalRect::new(
1295 LogicalPosition::new(1.0, 2.0),
1296 LogicalSize::new(3.0, 4.0),
1297 );
1298 r.scale_for_dpi(1.0);
1299 assert_eq!(
1300 r,
1301 LogicalRect::new(LogicalPosition::new(1.0, 2.0), LogicalSize::new(3.0, 4.0))
1302 );
1303 }
1304
1305 #[test]
1306 fn scale_for_dpi_by_zero_collapses_to_the_origin() {
1307 let mut r = LogicalRect::new(
1308 LogicalPosition::new(10.0, 20.0),
1309 LogicalSize::new(30.0, 40.0),
1310 );
1311 r.scale_for_dpi(0.0);
1312 assert_eq!(r, LogicalRect::zero());
1313 }
1314
1315 #[test]
1316 fn scale_for_dpi_by_negative_factor_mirrors_deterministically() {
1317 let mut r = LogicalRect::new(
1318 LogicalPosition::new(10.0, 20.0),
1319 LogicalSize::new(30.0, 40.0),
1320 );
1321 r.scale_for_dpi(-2.0);
1322 assert_eq!(
1323 r,
1324 LogicalRect::new(
1325 LogicalPosition::new(-20.0, -40.0),
1326 LogicalSize::new(-60.0, -80.0)
1327 )
1328 );
1329 assert!(!r.contains(LogicalPosition::new(-30.0, -50.0)));
1331 }
1332
1333 #[test]
1334 fn scale_for_dpi_overflows_to_infinity_rather_than_panicking() {
1335 let mut s = LogicalSize::new(f32::MAX, f32::MAX);
1336 let out = s.scale_for_dpi(2.0);
1337 assert!(out.width.is_infinite() && out.width.is_sign_positive());
1338 assert!(out.height.is_infinite());
1339 assert_eq!(out, s);
1341 }
1342
1343 #[test]
1344 fn scale_for_dpi_with_nan_or_inf_does_not_panic() {
1345 for factor in HOSTILE {
1346 let mut p = LogicalPosition::new(1.0, -1.0);
1347 p.scale_for_dpi(factor);
1348
1349 let mut s = LogicalSize::new(1.0, -1.0);
1350 let _ = s.scale_for_dpi(factor);
1351
1352 let mut r = LogicalRect::new(
1353 LogicalPosition::new(1.0, -1.0),
1354 LogicalSize::new(2.0, -2.0),
1355 );
1356 r.scale_for_dpi(factor);
1357 }
1358 let mut r = LogicalRect::new(LogicalPosition::zero(), LogicalSize::new(1.0, 1.0));
1361 r.scale_for_dpi(f32::INFINITY);
1362 assert!(r.origin.x.is_nan());
1363 assert!(r.size.width.is_infinite());
1364 }
1365
1366 #[test]
1371 fn to_physical_rounds_half_away_from_zero() {
1372 assert_eq!(
1373 LogicalPosition::new(0.5, 1.5).to_physical(1.0),
1374 PhysicalPosition::new(1, 2)
1375 );
1376 assert_eq!(
1378 LogicalSize::new(2.5, 3.5).to_physical(1.0),
1379 PhysicalSize::new(3, 4)
1380 );
1381 }
1382
1383 #[test]
1384 fn to_physical_clamps_negatives_to_zero_instead_of_wrapping() {
1385 assert_eq!(
1387 LogicalPosition::new(-1.0, -1000.0).to_physical(1.0),
1388 PhysicalPosition::new(0, 0)
1389 );
1390 assert_eq!(
1391 LogicalSize::new(-0.6, -1.0).to_physical(2.0),
1392 PhysicalSize::new(0, 0)
1393 );
1394 assert_eq!(
1395 LogicalPosition::new(1.0, 1.0).to_physical(-1.0),
1396 PhysicalPosition::new(0, 0)
1397 );
1398 }
1399
1400 #[test]
1401 fn to_physical_saturates_at_u32_max_on_overflow() {
1402 assert_eq!(
1403 LogicalSize::new(f32::MAX, f32::INFINITY).to_physical(1.0),
1404 PhysicalSize::new(u32::MAX, u32::MAX)
1405 );
1406 assert_eq!(
1409 LogicalPosition::new(1.0e30, 0.0).to_physical(1.0e30),
1410 PhysicalPosition::new(u32::MAX, 0)
1411 );
1412 }
1413
1414 #[test]
1415 fn to_physical_maps_nan_to_zero() {
1416 assert_eq!(
1418 LogicalPosition::new(f32::NAN, f32::NAN).to_physical(1.0),
1419 PhysicalPosition::new(0, 0)
1420 );
1421 assert_eq!(
1422 LogicalSize::new(f32::NAN, 5.0).to_physical(f32::NAN),
1423 PhysicalSize::new(0, 0)
1424 );
1425 assert_eq!(
1427 LogicalSize::new(0.0, 0.0).to_physical(f32::INFINITY),
1428 PhysicalSize::new(0, 0)
1429 );
1430 }
1431
1432 #[test]
1433 fn to_physical_never_panics_on_the_hostile_grid() {
1434 for v in HOSTILE {
1435 for f in HOSTILE {
1436 let _ = LogicalPosition::new(v, v).to_physical(f);
1437 let _ = LogicalSize::new(v, v).to_physical(f);
1438 }
1439 }
1440 }
1441
1442 #[test]
1443 fn to_logical_divides_by_the_dpi_factor() {
1444 assert_eq!(
1445 PhysicalSize::new(200_u32, 100).to_logical(2.0),
1446 LogicalSize::new(100.0, 50.0)
1447 );
1448 assert_eq!(
1449 PhysicalPosition::new(-10_i32, 20).to_logical(2.0),
1450 LogicalPosition::new(-5.0, 10.0)
1451 );
1452 assert_eq!(
1453 PhysicalPosition::new(-10.0_f64, 20.0).to_logical(2.0),
1454 LogicalPosition::new(-5.0, 10.0)
1455 );
1456 }
1457
1458 #[test]
1459 fn to_logical_with_zero_dpi_yields_infinity_not_a_panic() {
1460 let s = PhysicalSize::new(100_u32, 100).to_logical(0.0);
1462 assert!(s.width.is_infinite() && s.width.is_sign_positive());
1463
1464 let z = PhysicalSize::<u32>::zero().to_logical(0.0);
1466 assert!(z.width.is_nan() && z.height.is_nan());
1467
1468 let p = PhysicalPosition::new(-5_i32, 5).to_logical(0.0);
1469 assert!(p.x.is_infinite() && p.x.is_sign_negative());
1470 assert!(p.y.is_infinite() && p.y.is_sign_positive());
1471 }
1472
1473 #[test]
1474 fn to_logical_at_the_integer_limits() {
1475 let p = PhysicalPosition::new(i32::MIN, i32::MAX).to_logical(1.0);
1476 assert_eq!(p.x, i32::MIN as f32);
1477 assert_eq!(p.y, i32::MAX as f32);
1478
1479 let s = PhysicalSize::new(u32::MAX, 0_u32).to_logical(1.0);
1480 assert_eq!(s.width, u32::MAX as f32);
1481 assert_eq!(s.height, 0.0);
1482
1483 let big = PhysicalPosition::new(f64::MAX, f64::MIN).to_logical(1.0);
1485 assert!(big.x.is_infinite() && big.x.is_sign_positive());
1486 assert!(big.y.is_infinite() && big.y.is_sign_negative());
1487 }
1488
1489 #[test]
1490 fn to_logical_never_panics_for_hostile_dpi_factors() {
1491 for f in HOSTILE {
1492 let _ = PhysicalPosition::new(i32::MIN, i32::MAX).to_logical(f);
1493 let _ = PhysicalPosition::new(f64::MAX, f64::MIN).to_logical(f);
1494 let _ = PhysicalSize::new(u32::MAX, 0_u32).to_logical(f);
1495 }
1496 }
1497
1498 #[test]
1503 fn logical_size_physical_round_trip_is_lossless_for_integral_pixels() {
1504 for factor in [1.0_f32, 2.0, 4.0] {
1505 for (w, h) in [(0.0_f32, 0.0_f32), (1.0, 1.0), (100.0, 50.0), (1920.0, 1080.0)] {
1506 let original = LogicalSize::new(w, h);
1507 let round_tripped = original.to_physical(factor).to_logical(factor);
1508 assert_eq!(
1509 original, round_tripped,
1510 "round-trip lost {original:?} at dpi {factor}"
1511 );
1512 }
1513 }
1514 }
1515
1516 #[test]
1517 fn physical_size_logical_round_trip_preserves_the_pixel_count() {
1518 for factor in [1.0_f32, 1.5, 2.0, 3.0] {
1519 for (w, h) in [(0_u32, 0_u32), (1, 1), (1920, 1080), (3840, 2160)] {
1520 let original = PhysicalSize::new(w, h);
1521 let round_tripped = original.to_logical(factor).to_physical(factor);
1522 assert_eq!(
1523 original, round_tripped,
1524 "round-trip lost {original:?} at dpi {factor}"
1525 );
1526 }
1527 }
1528 }
1529
1530 #[test]
1531 fn screen_and_cursor_position_logical_round_trip_bit_for_bit() {
1532 for x in HOSTILE {
1533 for y in HOSTILE {
1534 let p = LogicalPosition::new(x, y);
1535
1536 let screen = ScreenPosition::from_logical(p).to_logical();
1537 assert_eq!(screen.x.to_bits(), x.to_bits());
1538 assert_eq!(screen.y.to_bits(), y.to_bits());
1539
1540 let cursor = CursorNodePosition::from_logical(p).to_logical();
1541 assert_eq!(cursor.x.to_bits(), x.to_bits());
1542 assert_eq!(cursor.y.to_bits(), y.to_bits());
1543 }
1544 }
1545 }
1546
1547 #[test]
1548 fn add_sub_are_inverse_for_finite_positions() {
1549 let a = LogicalPosition::new(10.0, -20.0);
1550 let b = LogicalPosition::new(2.5, 7.5);
1551 assert_eq!((a + b) - b, a);
1552
1553 let mut c = a;
1554 c += b;
1555 assert_eq!(c, a + b);
1556 c -= b;
1557 assert_eq!(c, a);
1558 }
1559
1560 #[test]
1565 fn position_main_cross_round_trip_for_every_writing_mode() {
1566 for wm in WMS {
1567 for main in HOSTILE {
1568 for cross in HOSTILE {
1569 let p = LogicalPosition::from_main_cross(main, cross, wm);
1570 assert_eq!(p.main(wm).to_bits(), main.to_bits());
1571 assert_eq!(p.cross(wm).to_bits(), cross.to_bits());
1572 }
1573 }
1574 }
1575 }
1576
1577 #[test]
1578 fn size_main_cross_round_trip_for_every_writing_mode() {
1579 for wm in WMS {
1580 for main in HOSTILE {
1581 for cross in HOSTILE {
1582 let s = LogicalSize::from_main_cross(main, cross, wm);
1583 assert_eq!(s.main(wm).to_bits(), main.to_bits());
1584 assert_eq!(s.cross(wm).to_bits(), cross.to_bits());
1585 }
1586 }
1587 }
1588 }
1589
1590 #[test]
1591 fn horizontal_tb_maps_main_to_the_block_axis() {
1592 let wm = LayoutWritingMode::HorizontalTb;
1594 let p = LogicalPosition::new(3.0, 7.0);
1595 assert_eq!(p.main(wm), 7.0);
1596 assert_eq!(p.cross(wm), 3.0);
1597
1598 let s = LogicalSize::new(30.0, 70.0);
1599 assert_eq!(s.main(wm), 70.0);
1600 assert_eq!(s.cross(wm), 30.0);
1601 }
1602
1603 #[test]
1604 fn vertical_modes_map_main_to_the_horizontal_axis() {
1605 for wm in [LayoutWritingMode::VerticalRl, LayoutWritingMode::VerticalLr] {
1606 let p = LogicalPosition::new(3.0, 7.0);
1607 assert_eq!(p.main(wm), 3.0);
1608 assert_eq!(p.cross(wm), 7.0);
1609
1610 let s = LogicalSize::new(30.0, 70.0);
1611 assert_eq!(s.main(wm), 30.0);
1612 assert_eq!(s.cross(wm), 70.0);
1613 }
1614 }
1615
1616 #[test]
1617 fn with_main_and_with_cross_only_touch_their_own_axis() {
1618 for wm in WMS {
1619 for v in HOSTILE {
1620 let s = LogicalSize::new(10.0, 20.0);
1621
1622 let m = s.with_main(wm, v);
1623 assert_eq!(m.main(wm).to_bits(), v.to_bits());
1624 assert_eq!(m.cross(wm), s.cross(wm), "with_main clobbered the cross axis");
1625
1626 let c = s.with_cross(wm, v);
1627 assert_eq!(c.cross(wm).to_bits(), v.to_bits());
1628 assert_eq!(c.main(wm), s.main(wm), "with_cross clobbered the main axis");
1629 }
1630 }
1631 }
1632
1633 #[test]
1634 fn with_main_then_with_cross_reconstructs_from_main_cross() {
1635 for wm in WMS {
1636 let built = LogicalSize::zero().with_main(wm, 5.0).with_cross(wm, 9.0);
1637 assert_eq!(built, LogicalSize::from_main_cross(5.0, 9.0, wm));
1638 }
1639 }
1640
1641 #[test]
1646 fn display_formats_are_well_formed_for_representative_values() {
1647 let p = LogicalPosition::new(1.5, -2.5);
1648 assert_eq!(format!("{p}"), "(1.5, -2.5)");
1649 assert_eq!(format!("{p:?}"), "(1.5, -2.5)");
1650
1651 let s = LogicalSize::new(30.0, 40.0);
1652 assert_eq!(format!("{s}"), "30x40");
1653 assert_eq!(format!("{s:?}"), "30x40");
1654
1655 let r = LogicalRect::new(p, s);
1656 assert_eq!(format!("{r}"), "30x40 @ (1.5, -2.5)");
1657 assert_eq!(format!("{r:?}"), "30x40 @ (1.5, -2.5)");
1658
1659 assert_eq!(format!("{:?}", PhysicalPosition::new(1_i32, 2)), "(1, 2)");
1660 assert_eq!(format!("{:?}", PhysicalSize::new(1_u32, 2)), "1x2");
1661 }
1662
1663 #[test]
1664 fn display_of_zero_values_is_non_empty() {
1665 assert!(!format!("{}", LogicalPosition::zero()).is_empty());
1666 assert!(!format!("{}", LogicalSize::zero()).is_empty());
1667 assert!(!format!("{}", LogicalRect::zero()).is_empty());
1668 assert_eq!(format!("{}", LogicalRect::zero()), "0x0 @ (0, 0)");
1669 }
1670
1671 #[test]
1672 fn display_does_not_panic_on_nan_or_infinite_coordinates() {
1673 for x in HOSTILE {
1674 for y in HOSTILE {
1675 let r = LogicalRect::new(
1676 LogicalPosition::new(x, y),
1677 LogicalSize::new(x, y),
1678 );
1679 let shown = format!("{r}");
1680 assert!(!shown.is_empty());
1681 assert_eq!(shown, format!("{r:?}"));
1682 }
1683 }
1684 let nan = LogicalRect::new(
1685 LogicalPosition::new(f32::NAN, f32::INFINITY),
1686 LogicalSize::new(f32::NEG_INFINITY, f32::NAN),
1687 );
1688 assert_eq!(format!("{nan}"), "-infxNaN @ (NaN, inf)");
1689 }
1690}