Skip to main content

azul_core/
geom.rs

1//! Logical and physical coordinate types for the GUI toolkit.
2//!
3//! Provides DPI-independent (`Logical*`) and pixel-level (`Physical*`) geometry
4//! types used throughout layout, rendering, windowing, and hit testing.
5//! Logical coordinates are scaled by a DPI factor to produce physical coordinates.
6
7// Re-export DragDelta from drag module (moved in code reorganization)
8pub use crate::drag::{DragDelta, OptionDragDelta};
9
10/// An axis-aligned rectangle in logical (DPI-independent) coordinates.
11#[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    /// Scales all coordinates in-place by the given DPI scale factor.
39    #[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    /// Returns the maximum x coordinate (origin.x + width).
48    #[inline]
49    #[must_use] pub fn max_x(&self) -> f32 {
50        self.origin.x + self.size.width
51    }
52    /// Returns the minimum x coordinate (origin.x).
53    #[inline]
54    #[must_use] pub const fn min_x(&self) -> f32 {
55        self.origin.x
56    }
57    /// Returns the maximum y coordinate (origin.y + height).
58    #[inline]
59    #[must_use] pub fn max_y(&self) -> f32 {
60        self.origin.y + self.size.height
61    }
62    /// Returns the minimum y coordinate (origin.y).
63    #[inline]
64    #[must_use] pub const fn min_y(&self) -> f32 {
65        self.origin.y
66    }
67
68    /// Returns whether this rectangle intersects with another rectangle
69    #[inline]
70    #[must_use] pub fn intersects(&self, other: Self) -> bool {
71        // Check if one rectangle is to the left of the other
72        if self.max_x() <= other.min_x() || other.max_x() <= self.min_x() {
73            return false;
74        }
75
76        // Check if one rectangle is above the other
77        if self.max_y() <= other.min_y() || other.max_y() <= self.min_y() {
78            return false;
79        }
80
81        // If we got here, the rectangles must intersect
82        true
83    }
84
85    /// Returns whether this rectangle contains the given point
86    #[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    /// Same as `contains()`, but returns the (x, y) offset of the hit point
95    ///
96    /// On a regular computer this function takes ~3.2ns to run
97    #[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        // Edge semantics must match `contains`: left/top inclusive (`>= min`),
104        // right/bottom exclusive (`< max`). Previously all four edges were
105        // exclusive, so a point exactly on the left/top edge hit-tested as a
106        // miss even though `contains` reported it inside — dropping/duplicating
107        // hits on shared edges between adjacent rects.
108        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/// A 2D position in logical (DPI-independent) coordinates.
135// PartialEq is hand-implemented over `quantize()` (see below) so that equality
136// agrees with the quantized `Ord`/`Hash`. A derived field-wise `PartialEq`
137// compared raw f32, so `a == b` could be false while `a.cmp(b) == Equal`,
138// breaking `BTreeMap`/`HashMap` lookups keyed on these types.
139#[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    /// Scales the position in-place by the given DPI scale factor.
154    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
210/// Multiplier for converting f32 coordinates to integers in Ord/Hash impls.
211/// Provides ~0.001 precision, sufficient for sub-pixel layout coordinates.
212const DECIMAL_MULTIPLIER: f32 = 1000.0;
213
214/// Quantizes an f32 coordinate to fixed-point for stable `Ord`/`Hash`/`PartialEq`
215/// (comparing raw f32 bit patterns would be unstable / non-total).
216// intentional fixed-point quantization: the truncation IS the rounding step.
217#[allow(clippy::cast_possible_truncation)]
218fn quantize(value: f32) -> i64 {
219    // NaN has no meaningful position in a total order. Map it to a single fixed
220    // sentinel (`i64::MIN`) so all NaNs compare equal to each other and sort
221    // below every real value — and, critically, do NOT collide with `0.0`
222    // (the old `NaN as isize == 0` behaviour aliased NaN onto the origin).
223    if value.is_nan() {
224        return i64::MIN;
225    }
226    // `f32 as i64` saturates on overflow (since Rust 1.45), so an out-of-range
227    // coordinate clamps to `i64::{MIN,MAX}` instead of wrapping. `isize` was
228    // only 32-bit on wasm32, so a large coordinate overflowed there — `i64` is
229    // wide enough on every target.
230    (value * DECIMAL_MULTIPLIER) as i64
231}
232
233impl_option!(
234    LogicalPosition,
235    OptionLogicalPosition,
236    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
237);
238
239// PartialOrd delegates to the quantized Ord (the derived field-wise PartialOrd
240// compared raw f32 and diverged from this quantized order — a latent bug).
241impl 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    /// Returns the main-axis component for the given writing mode.
272    #[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    /// Returns the cross-axis component for the given writing mode.
280    #[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    /// Creates a `LogicalPosition` from main and cross axis dimensions.
288    #[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/// A 2D size in logical (DPI-independent) coordinates.
297// PartialEq is hand-implemented over `quantize()` to agree with the quantized
298// `Ord`/`Hash` (see `LogicalPosition` for the rationale).
299#[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    /// Scales the size in-place by the given DPI scale factor and returns self.
315    // Mutates in place; the returned copy is only for optional chaining, so callers
316    // may legitimately discard it (e.g. ui_solver) — #[must_use] would be wrong here.
317    #[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    /// Creates a `LogicalSize` from main and cross axis dimensions.
325    #[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
357// PartialOrd delegates to the quantized Ord (the derived field-wise PartialOrd
358// compared raw f32 and diverged from this quantized order — a latent bug).
359impl 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    /// Returns the main-axis dimension for the given writing mode.
392    #[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    /// Returns the cross-axis dimension for the given writing mode.
400    #[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    /// Returns a new `LogicalSize` with the main-axis dimension updated.
408    #[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    /// Returns a new `LogicalSize` with the cross-axis dimension updated.
422    #[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/// A 2D position in physical (pixel) coordinates.
437#[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/// A 2D size in physical (pixel) coordinates.
458#[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    /// Converts to physical pixel coordinates by multiplying by the DPI factor.
494    #[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    /// Converts to logical coordinates by dividing by the DPI factor.
517    #[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    /// Converts to logical coordinates by dividing by the DPI factor.
533    #[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    /// Converts to physical pixel size by multiplying by the DPI factor.
553    #[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    /// Converts to logical coordinates by dividing by the DPI factor.
576    #[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/// Marker enum documenting which coordinate space a geometric value is in.
587///
588/// This is for documentation and debugging purposes only — it does not enforce
589/// type safety at compile time. Use comments like `[CoordinateSpace::Window]`
590/// or `[CoordinateSpace::ScrollFrame]` in code to document coordinate contexts.
591///
592/// **Common bug pattern:** passing `Window`-space coordinates where
593/// `ScrollFrame`-space is expected (or vice versa). The scroll frame creates a
594/// new spatial node, so primitives must be offset by the frame origin.
595#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
596#[repr(C)]
597pub enum CoordinateSpace {
598    /// Absolute coordinates from window top-left (0,0).
599    /// Layout engine output is in this space.
600    Window,
601    
602    /// Relative to scroll frame content origin.
603    /// Transformation: `scroll_pos` = `window_pos` - `scroll_frame_origin`
604    ScrollFrame,
605    
606    /// Relative to parent node's content box origin.
607    Parent,
608    
609    /// Relative to a CSS transform reference frame origin.
610    ReferenceFrame,
611}
612
613
614// =============================================================================
615// Type-safe coordinate newtypes for API clarity
616// =============================================================================
617
618/// Position in screen coordinates (logical pixels, relative to primary monitor origin).
619/// On Wayland: falls back to window-local since global coords are unavailable.
620#[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    /// Convert to a raw `LogicalPosition` (for interop with existing code).
637    #[inline]
638    #[must_use] pub const fn to_logical(self) -> LogicalPosition {
639        LogicalPosition { x: self.x, y: self.y }
640    }
641    /// Create from a raw `LogicalPosition` that is known to be in screen space.
642    #[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/// Position relative to a DOM node's border box origin (logical pixels).
655#[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        // left/top edge: inclusive in both
696        let tl = LogicalPosition::new(10.0, 20.0);
697        assert!(r.contains(tl));
698        assert!(r.hit_test(&tl).is_some());
699        // just inside
700        let inside = LogicalPosition::new(11.0, 21.0);
701        assert!(r.contains(inside));
702        assert!(r.hit_test(&inside).is_some());
703        // right/bottom edge: exclusive in both
704        let br = LogicalPosition::new(40.0, 60.0);
705        assert!(!r.contains(br));
706        assert!(r.hit_test(&br).is_none());
707        // outside left
708        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        // Two values within the same quantization bucket must be == AND cmp==Equal.
730        let a = LogicalPosition::new(1.00000, 2.00000);
731        let b = LogicalPosition::new(1.00004, 2.00004); // < 0.001 apart
732        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        // NaN equals NaN under the quantized PartialEq (i64::MIN bucket) — this
743        // is what stops the every-frame Resize loop upstream.
744        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        // Huge coordinate must saturate, not wrap to a small/negative bucket.
752        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    /// Hostile float grid: every class that can reach a coordinate field.
770    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    // ---------------------------------------------------------------------
794    // quantize: numeric / saturation / NaN
795    // ---------------------------------------------------------------------
796
797    #[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        // Sign of zero must not split the bucket, or (0,0) and (-0,-0) would be
802        // distinct HashMap keys for the same visual origin.
803        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        // Sub-millipixel deltas collapse into the same bucket (truncation IS the
817        // documented rounding step) — and truncation is toward zero, not floor.
818        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        // f32::MAX * 1000 overflows to +inf before the cast; the cast must clamp.
827        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        // Denormal-ish tiny values must land on 0, not on a garbage bucket.
832        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        // The historical bug: `NaN as isize == 0` put NaN on top of (0.0, 0.0).
839        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        // KNOWN, INTENTIONAL LOSSINESS: NaN, -inf and f32::MIN all collapse onto
847        // the i64::MIN bucket, so they compare Equal. This keeps Ord/Eq total and
848        // consistent (which is what the type contract needs), but callers cannot
849        // use == to distinguish "no value" (NaN) from a huge negative coordinate.
850        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    // ---------------------------------------------------------------------
879    // Ord / Eq / Hash total-order contract over the hostile grid
880    // ---------------------------------------------------------------------
881
882    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            // Eq requires reflexivity — raw f32 NaN would break it.
899            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                    // Hash/Eq contract: equal keys MUST hash equal, or HashMap
934                    // lookups silently miss.
935                    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        // LogicalRect's derived PartialEq/Hash must inherit the quantized field
957        // impls — a NaN-sized rect has to be a stable HashMap key.
958        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        // Sub-millipixel jitter must not create a new key.
967        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    // ---------------------------------------------------------------------
980    // Constructors / zero neutrality
981    // ---------------------------------------------------------------------
982
983    #[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        // A zero rect is degenerate: it contains no point at all, not even its
1026        // own origin, and it does not intersect itself.
1027        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    // ---------------------------------------------------------------------
1037    // LogicalRect getters
1038    // ---------------------------------------------------------------------
1039
1040    #[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                // Pure reads: must never panic, whatever the float class.
1061                let _ = r.min_x();
1062                let _ = r.max_x();
1063                let _ = r.min_y();
1064                let _ = r.max_y();
1065            }
1066        }
1067        // inf + (-inf) is NaN — max_x has no guard, so it propagates NaN rather
1068        // than panicking. Assert the defined (non-panicking) result.
1069        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    // ---------------------------------------------------------------------
1078    // contains / hit_test / intersects
1079    // ---------------------------------------------------------------------
1080
1081    #[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))); // top-left: in
1088        assert!(!r.contains(LogicalPosition::new(40.0, 59.0))); // right edge: out
1089        assert!(!r.contains(LogicalPosition::new(39.0, 60.0))); // bottom edge: out
1090        assert!(!r.contains(LogicalPosition::new(40.0, 60.0))); // bottom-right: out
1091        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        // The invariant the hit_test comment promises: identical edge semantics.
1097        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            // Negative extent: max < min, so it can never contain anything.
1102            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        // Every comparison against NaN is false, so a NaN point is never inside.
1133        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        // A negative width puts max_x below min_x: nothing can satisfy both bounds.
1148        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        // f32::MIN + f32::MAX == 0.0 exactly, so the rect spans [MIN, 0).
1164        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        // -inf + inf == NaN, so the "infinite" rect contains nothing. Surprising,
1173        // but defined and panic-free.
1174        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        // Right/bottom edges are exclusive.
1193        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        // Dyadic values only: `origin + offset` must reconstruct the point exactly,
1204        // so the assertion tests hit_test's arithmetic and not f32 rounding.
1205        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        // Zero-area rects never overlap anything, including themselves.
1258        assert!(!LogicalRect::zero().intersects(a));
1259    }
1260
1261    #[test]
1262    fn intersects_with_nan_rect_is_permissive_current_behavior() {
1263        // DOCUMENTS A REAL QUIRK (reported, not worked around): every `<=` guard
1264        // in `intersects` is false against NaN, so all four early-outs are skipped
1265        // and a fully-NaN rect reports that it intersects EVERYTHING — while
1266        // `contains` on the same rect correctly reports false for every point.
1267        // Locked down here so a future fix has to change this deliberately.
1268        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    // ---------------------------------------------------------------------
1282    // scale_for_dpi
1283    // ---------------------------------------------------------------------
1284
1285    #[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        // A mirrored rect has an inverted extent, so it contains nothing.
1330        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        // scale_for_dpi mutates in place AND returns a copy: they must match.
1340        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        // 0.0 * inf is the classic NaN trap: a zero-origin rect scaled by an
1359        // infinite DPI factor yields NaN coordinates, not zeros.
1360        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    // ---------------------------------------------------------------------
1367    // DPI conversion: to_physical / to_logical
1368    // ---------------------------------------------------------------------
1369
1370    #[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        // 2.5 -> 3 (round-half-away), NOT 2 (banker's rounding).
1377        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        // `as u32` saturates (Rust >= 1.45), so -1.0 must become 0, NOT u32::MAX.
1386        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        // x: 1e30 * 1e30 overflows f32 to +inf, then saturates at the cast.
1407        // y: 0.0 * 1e30 stays 0 — saturation must not smear across components.
1408        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        // `NaN as u32` == 0 by the saturating-cast rules. Defined, not UB.
1417        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        // 0.0 * inf == NaN -> 0
1426        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        // Float division by zero is defined: no divide-by-zero panic here.
1461        let s = PhysicalSize::new(100_u32, 100).to_logical(0.0);
1462        assert!(s.width.is_infinite() && s.width.is_sign_positive());
1463
1464        // 0 / 0 == NaN.
1465        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        // f64 -> f32 narrowing saturates to inf rather than wrapping.
1484        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    // ---------------------------------------------------------------------
1499    // Round-trips
1500    // ---------------------------------------------------------------------
1501
1502    #[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    // ---------------------------------------------------------------------
1561    // Writing-mode axis mapping
1562    // ---------------------------------------------------------------------
1563
1564    #[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        // In horizontal-tb the block (main) axis is vertical: main == y / height.
1593        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    // ---------------------------------------------------------------------
1642    // Display / Debug (serializers)
1643    // ---------------------------------------------------------------------
1644
1645    #[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}