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]
32    pub const fn zero() -> Self {
33        Self::new(LogicalPosition::zero(), LogicalSize::zero())
34    }
35    #[must_use]
36    pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
37        Self { origin, size }
38    }
39
40    /// Scales all coordinates in-place by the given DPI scale factor.
41    #[inline]
42    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
43        self.origin.x *= scale_factor;
44        self.origin.y *= scale_factor;
45        self.size.width *= scale_factor;
46        self.size.height *= scale_factor;
47    }
48
49    /// Returns the maximum x coordinate (origin.x + width).
50    #[inline]
51    #[must_use]
52    pub fn max_x(&self) -> f32 {
53        self.origin.x + self.size.width
54    }
55    /// Returns the minimum x coordinate (origin.x).
56    #[inline]
57    #[must_use]
58    pub const fn min_x(&self) -> f32 {
59        self.origin.x
60    }
61    /// Returns the maximum y coordinate (origin.y + height).
62    #[inline]
63    #[must_use]
64    pub fn max_y(&self) -> f32 {
65        self.origin.y + self.size.height
66    }
67    /// Returns the minimum y coordinate (origin.y).
68    #[inline]
69    #[must_use]
70    pub const fn min_y(&self) -> f32 {
71        self.origin.y
72    }
73
74    /// Returns whether this rectangle intersects with another rectangle
75    #[inline]
76    #[must_use]
77    pub fn intersects(&self, other: Self) -> bool {
78        // Check if one rectangle is to the left of the other
79        if self.max_x() <= other.min_x() || other.max_x() <= self.min_x() {
80            return false;
81        }
82
83        // Check if one rectangle is above the other
84        if self.max_y() <= other.min_y() || other.max_y() <= self.min_y() {
85            return false;
86        }
87
88        // If we got here, the rectangles must intersect
89        true
90    }
91
92    /// Returns whether this rectangle contains the given point
93    #[inline]
94    #[must_use]
95    pub fn contains(&self, point: LogicalPosition) -> bool {
96        point.x >= self.min_x()
97            && point.x < self.max_x()
98            && point.y >= self.min_y()
99            && point.y < self.max_y()
100    }
101
102    /// Same as `contains()`, but returns the (x, y) offset of the hit point
103    ///
104    /// On a regular computer this function takes ~3.2ns to run
105    #[inline]
106    #[must_use]
107    pub fn hit_test(&self, other: &LogicalPosition) -> Option<LogicalPosition> {
108        let dx_left_edge = other.x - self.min_x();
109        let dx_right_edge = self.max_x() - other.x;
110        let dy_top_edge = other.y - self.min_y();
111        let dy_bottom_edge = self.max_y() - other.y;
112        // Edge semantics must match `contains`: left/top inclusive (`>= min`),
113        // right/bottom exclusive (`< max`). Previously all four edges were
114        // exclusive, so a point exactly on the left/top edge hit-tested as a
115        // miss even though `contains` reported it inside — dropping/duplicating
116        // hits on shared edges between adjacent rects.
117        if dx_left_edge >= 0.0 && dx_right_edge > 0.0 && dy_top_edge >= 0.0 && dy_bottom_edge > 0.0
118        {
119            Some(LogicalPosition::new(dx_left_edge, dy_top_edge))
120        } else {
121            None
122        }
123    }
124}
125
126impl_vec!(
127    LogicalRect,
128    LogicalRectVec,
129    LogicalRectVecDestructor,
130    LogicalRectVecDestructorType,
131    LogicalRectVecSlice,
132    OptionLogicalRect
133);
134impl_vec_clone!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor);
135impl_vec_debug!(LogicalRect, LogicalRectVec);
136impl_vec_partialeq!(LogicalRect, LogicalRectVec);
137impl_vec_partialord!(LogicalRect, LogicalRectVec);
138impl_vec_ord!(LogicalRect, LogicalRectVec);
139impl_vec_hash!(LogicalRect, LogicalRectVec);
140impl_vec_eq!(LogicalRect, LogicalRectVec);
141
142use core::{
143    cmp::Ordering,
144    hash::{Hash, Hasher},
145    ops::{self, AddAssign, SubAssign},
146};
147
148use azul_css::props::layout::LayoutWritingMode;
149
150/// A 2D position in logical (DPI-independent) coordinates.
151// PartialEq is hand-implemented over `quantize()` (see below) so that equality
152// agrees with the quantized `Ord`/`Hash`. A derived field-wise `PartialEq`
153// compared raw f32, so `a == b` could be false while `a.cmp(b) == Equal`,
154// breaking `BTreeMap`/`HashMap` lookups keyed on these types.
155#[derive(Default, Copy, Clone)]
156#[repr(C)]
157pub struct LogicalPosition {
158    pub x: f32,
159    pub y: f32,
160}
161
162impl PartialEq for LogicalPosition {
163    fn eq(&self, other: &Self) -> bool {
164        quantize(self.x) == quantize(other.x) && quantize(self.y) == quantize(other.y)
165    }
166}
167
168impl LogicalPosition {
169    /// Scales the position in-place by the given DPI scale factor.
170    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
171        self.x *= scale_factor;
172        self.y *= scale_factor;
173    }
174}
175
176impl SubAssign<Self> for LogicalPosition {
177    fn sub_assign(&mut self, other: Self) {
178        self.x -= other.x;
179        self.y -= other.y;
180    }
181}
182
183impl AddAssign<Self> for LogicalPosition {
184    fn add_assign(&mut self, other: Self) {
185        self.x += other.x;
186        self.y += other.y;
187    }
188}
189
190impl core::fmt::Debug for LogicalPosition {
191    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
192        write!(f, "({}, {})", self.x, self.y)
193    }
194}
195
196impl core::fmt::Display for LogicalPosition {
197    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
198        write!(f, "({}, {})", self.x, self.y)
199    }
200}
201
202impl ops::Add for LogicalPosition {
203    type Output = Self;
204
205    #[inline]
206    fn add(self, other: Self) -> Self {
207        Self {
208            x: self.x + other.x,
209            y: self.y + other.y,
210        }
211    }
212}
213
214impl ops::Sub for LogicalPosition {
215    type Output = Self;
216
217    #[inline]
218    fn sub(self, other: Self) -> Self {
219        Self {
220            x: self.x - other.x,
221            y: self.y - other.y,
222        }
223    }
224}
225
226/// Multiplier for converting f32 coordinates to integers in Ord/Hash impls.
227/// Provides ~0.001 precision, sufficient for sub-pixel layout coordinates.
228const DECIMAL_MULTIPLIER: f32 = 1000.0;
229
230/// Quantizes an f32 coordinate to fixed-point for stable `Ord`/`Hash`/`PartialEq`
231/// (comparing raw f32 bit patterns would be unstable / non-total).
232// intentional fixed-point quantization: the truncation IS the rounding step.
233#[allow(clippy::cast_possible_truncation)]
234fn quantize(value: f32) -> i64 {
235    // NaN has no meaningful position in a total order. Map it to a single fixed
236    // sentinel (`i64::MIN`) so all NaNs compare equal to each other and sort
237    // below every real value — and, critically, do NOT collide with `0.0`
238    // (the old `NaN as isize == 0` behaviour aliased NaN onto the origin).
239    if value.is_nan() {
240        return i64::MIN;
241    }
242    // `f32 as i64` saturates on overflow (since Rust 1.45), so an out-of-range
243    // coordinate clamps to `i64::{MIN,MAX}` instead of wrapping. `isize` was
244    // only 32-bit on wasm32, so a large coordinate overflowed there — `i64` is
245    // wide enough on every target.
246    (value * DECIMAL_MULTIPLIER) as i64
247}
248
249impl_option!(
250    LogicalPosition,
251    OptionLogicalPosition,
252    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
253);
254
255// PartialOrd delegates to the quantized Ord (the derived field-wise PartialOrd
256// compared raw f32 and diverged from this quantized order — a latent bug).
257impl PartialOrd for LogicalPosition {
258    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
259        Some(self.cmp(other))
260    }
261}
262impl Ord for LogicalPosition {
263    fn cmp(&self, other: &Self) -> Ordering {
264        let self_x = quantize(self.x);
265        let self_y = quantize(self.y);
266        let other_x = quantize(other.x);
267        let other_y = quantize(other.y);
268        self_x.cmp(&other_x).then(self_y.cmp(&other_y))
269    }
270}
271
272impl Eq for LogicalPosition {}
273
274impl Hash for LogicalPosition {
275    fn hash<H>(&self, state: &mut H)
276    where
277        H: Hasher,
278    {
279        let self_x = quantize(self.x);
280        let self_y = quantize(self.y);
281        self_x.hash(state);
282        self_y.hash(state);
283    }
284}
285
286impl LogicalPosition {
287    /// Returns the main-axis component for the given writing mode.
288    #[must_use]
289    pub const fn main(&self, wm: LayoutWritingMode) -> f32 {
290        match wm {
291            LayoutWritingMode::HorizontalTb => self.y,
292            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.x,
293        }
294    }
295
296    /// Returns the cross-axis component for the given writing mode.
297    #[must_use]
298    pub const fn cross(&self, wm: LayoutWritingMode) -> f32 {
299        match wm {
300            LayoutWritingMode::HorizontalTb => self.x,
301            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.y,
302        }
303    }
304
305    /// Creates a `LogicalPosition` from main and cross axis dimensions.
306    #[must_use]
307    pub const fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
308        match wm {
309            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
310            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
311        }
312    }
313}
314
315/// A 2D size in logical (DPI-independent) coordinates.
316// PartialEq is hand-implemented over `quantize()` to agree with the quantized
317// `Ord`/`Hash` (see `LogicalPosition` for the rationale).
318#[derive(Default, Copy, Clone)]
319#[repr(C)]
320pub struct LogicalSize {
321    pub width: f32,
322    pub height: f32,
323}
324
325impl PartialEq for LogicalSize {
326    fn eq(&self, other: &Self) -> bool {
327        quantize(self.width) == quantize(other.width)
328            && quantize(self.height) == quantize(other.height)
329    }
330}
331
332impl LogicalSize {
333    /// Scales the size in-place by the given DPI scale factor and returns self.
334    // Mutates in place; the returned copy is only for optional chaining, so callers
335    // may legitimately discard it (e.g. ui_solver) — #[must_use] would be wrong here.
336    #[allow(clippy::return_self_not_must_use)]
337    pub fn scale_for_dpi(&mut self, scale_factor: f32) -> Self {
338        self.width *= scale_factor;
339        self.height *= scale_factor;
340        *self
341    }
342
343    /// Creates a `LogicalSize` from main and cross axis dimensions.
344    #[must_use]
345    pub const fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
346        match wm {
347            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
348            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
349        }
350    }
351}
352
353impl core::fmt::Debug for LogicalSize {
354    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
355        write!(f, "{}x{}", self.width, self.height)
356    }
357}
358
359impl core::fmt::Display for LogicalSize {
360    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
361        write!(f, "{}x{}", self.width, self.height)
362    }
363}
364
365impl_option!(
366    LogicalSize,
367    OptionLogicalSize,
368    // Ord + Hash so it can ride inside NodeType (TransientWindowConfig::size);
369    // LogicalSize already implements both by bit pattern.
370    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
371);
372
373impl_option!(
374    LogicalRect,
375    OptionLogicalRect,
376    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
377);
378
379// PartialOrd delegates to the quantized Ord (the derived field-wise PartialOrd
380// compared raw f32 and diverged from this quantized order — a latent bug).
381impl PartialOrd for LogicalSize {
382    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
383        Some(self.cmp(other))
384    }
385}
386impl Ord for LogicalSize {
387    fn cmp(&self, other: &Self) -> Ordering {
388        let self_width = quantize(self.width);
389        let self_height = quantize(self.height);
390        let other_width = quantize(other.width);
391        let other_height = quantize(other.height);
392        self_width
393            .cmp(&other_width)
394            .then(self_height.cmp(&other_height))
395    }
396}
397
398impl Eq for LogicalSize {}
399
400impl Hash for LogicalSize {
401    fn hash<H>(&self, state: &mut H)
402    where
403        H: Hasher,
404    {
405        let self_width = quantize(self.width);
406        let self_height = quantize(self.height);
407        self_width.hash(state);
408        self_height.hash(state);
409    }
410}
411
412impl LogicalSize {
413    /// Returns the main-axis dimension for the given writing mode.
414    #[must_use]
415    pub const fn main(&self, wm: LayoutWritingMode) -> f32 {
416        match wm {
417            LayoutWritingMode::HorizontalTb => self.height,
418            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.width,
419        }
420    }
421
422    /// Returns the cross-axis dimension for the given writing mode.
423    #[must_use]
424    pub const fn cross(&self, wm: LayoutWritingMode) -> f32 {
425        match wm {
426            LayoutWritingMode::HorizontalTb => self.width,
427            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.height,
428        }
429    }
430
431    /// Returns a new `LogicalSize` with the main-axis dimension updated.
432    #[must_use]
433    pub const fn with_main(self, wm: LayoutWritingMode, value: f32) -> Self {
434        match wm {
435            LayoutWritingMode::HorizontalTb => Self {
436                height: value,
437                ..self
438            },
439            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
440                width: value,
441                ..self
442            },
443        }
444    }
445
446    /// Returns a new `LogicalSize` with the cross-axis dimension updated.
447    #[must_use]
448    pub const fn with_cross(self, wm: LayoutWritingMode, value: f32) -> Self {
449        match wm {
450            LayoutWritingMode::HorizontalTb => Self {
451                width: value,
452                ..self
453            },
454            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
455                height: value,
456                ..self
457            },
458        }
459    }
460}
461
462/// A 2D position in physical (pixel) coordinates.
463#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
464#[repr(C)]
465pub struct PhysicalPosition<T> {
466    pub x: T,
467    pub y: T,
468}
469
470impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalPosition<T> {
471    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
472        write!(f, "({}, {})", self.x, self.y)
473    }
474}
475
476pub type PhysicalPositionI32 = PhysicalPosition<i32>;
477impl_option!(
478    PhysicalPositionI32,
479    OptionPhysicalPositionI32,
480    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd]
481);
482
483/// A 2D size in physical (pixel) coordinates.
484#[derive(Ord, Hash, Eq, Copy, Clone, PartialEq, PartialOrd)]
485#[repr(C)]
486pub struct PhysicalSize<T> {
487    pub width: T,
488    pub height: T,
489}
490
491impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalSize<T> {
492    fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
493        write!(f, "{}x{}", self.width, self.height)
494    }
495}
496
497pub type PhysicalSizeU32 = PhysicalSize<u32>;
498impl_option!(
499    PhysicalSizeU32,
500    OptionPhysicalSizeU32,
501    [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
502);
503pub type PhysicalSizeF32 = PhysicalSize<f32>;
504impl_option!(
505    PhysicalSizeF32,
506    OptionPhysicalSizeF32,
507    [Debug, Copy, Clone, PartialEq, PartialOrd]
508);
509
510impl LogicalPosition {
511    #[inline]
512    #[must_use]
513    pub const fn new(x: f32, y: f32) -> Self {
514        Self { x, y }
515    }
516    #[inline]
517    #[must_use]
518    pub const fn zero() -> Self {
519        Self::new(0.0, 0.0)
520    }
521    /// Converts to physical pixel coordinates by multiplying by the DPI factor.
522    #[inline]
523    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
524    #[must_use]
525    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalPosition<u32> {
526        PhysicalPosition {
527            x: libm::roundf(self.x * hidpi_factor) as u32,
528            y: libm::roundf(self.y * hidpi_factor) as u32,
529        }
530    }
531}
532
533impl<T> PhysicalPosition<T> {
534    #[inline]
535    pub const fn new(x: T, y: T) -> Self {
536        Self { x, y }
537    }
538}
539
540impl PhysicalPosition<i32> {
541    #[inline]
542    #[must_use]
543    pub const fn zero() -> Self {
544        Self::new(0, 0)
545    }
546    /// Converts to logical coordinates by dividing by the DPI factor.
547    #[inline]
548    #[allow(clippy::cast_precision_loss)]
549    #[must_use]
550    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
551        LogicalPosition {
552            x: self.x as f32 / hidpi_factor,
553            y: self.y as f32 / hidpi_factor,
554        }
555    }
556}
557
558impl PhysicalPosition<f64> {
559    #[inline]
560    #[must_use]
561    pub const fn zero() -> Self {
562        Self::new(0.0, 0.0)
563    }
564    /// Converts to logical coordinates by dividing by the DPI factor.
565    #[inline]
566    #[allow(clippy::cast_possible_truncation)]
567    #[must_use]
568    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
569        LogicalPosition {
570            x: self.x as f32 / hidpi_factor,
571            y: self.y as f32 / hidpi_factor,
572        }
573    }
574}
575
576impl LogicalSize {
577    #[inline]
578    #[must_use]
579    pub const fn new(width: f32, height: f32) -> Self {
580        Self { width, height }
581    }
582    #[inline]
583    #[must_use]
584    pub const fn zero() -> Self {
585        Self::new(0.0, 0.0)
586    }
587    /// Converts to physical pixel size by multiplying by the DPI factor.
588    #[inline]
589    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
590    #[must_use]
591    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalSize<u32> {
592        PhysicalSize {
593            width: libm::roundf(self.width * hidpi_factor) as u32,
594            height: libm::roundf(self.height * hidpi_factor) as u32,
595        }
596    }
597}
598
599impl<T> PhysicalSize<T> {
600    #[inline]
601    pub const fn new(width: T, height: T) -> Self {
602        Self { width, height }
603    }
604}
605
606impl PhysicalSize<u32> {
607    #[inline]
608    #[must_use]
609    pub const fn zero() -> Self {
610        Self::new(0, 0)
611    }
612    /// Converts to logical coordinates by dividing by the DPI factor.
613    #[inline]
614    #[allow(clippy::cast_precision_loss)]
615    #[must_use]
616    pub fn to_logical(self, hidpi_factor: f32) -> LogicalSize {
617        LogicalSize {
618            width: self.width as f32 / hidpi_factor,
619            height: self.height as f32 / hidpi_factor,
620        }
621    }
622}
623
624/// Marker enum documenting which coordinate space a geometric value is in.
625///
626/// This is for documentation and debugging purposes only — it does not enforce
627/// type safety at compile time. Use comments like `[CoordinateSpace::Window]`
628/// or `[CoordinateSpace::ScrollFrame]` in code to document coordinate contexts.
629///
630/// **Common bug pattern:** passing `Window`-space coordinates where
631/// `ScrollFrame`-space is expected (or vice versa). The scroll frame creates a
632/// new spatial node, so primitives must be offset by the frame origin.
633#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
634#[repr(C)]
635pub enum CoordinateSpace {
636    /// Absolute coordinates from window top-left (0,0).
637    /// Layout engine output is in this space.
638    Window,
639
640    /// Relative to scroll frame content origin.
641    /// Transformation: `scroll_pos` = `window_pos` - `scroll_frame_origin`
642    ScrollFrame,
643
644    /// Relative to parent node's content box origin.
645    Parent,
646
647    /// Relative to a CSS transform reference frame origin.
648    ReferenceFrame,
649}
650
651// =============================================================================
652// Type-safe coordinate newtypes for API clarity
653// =============================================================================
654
655/// Position in screen coordinates (logical pixels, relative to primary monitor origin).
656/// On Wayland: falls back to window-local since global coords are unavailable.
657#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
658#[repr(C)]
659pub struct ScreenPosition {
660    pub x: f32,
661    pub y: f32,
662}
663
664impl ScreenPosition {
665    #[inline]
666    #[must_use]
667    pub const fn new(x: f32, y: f32) -> Self {
668        Self { x, y }
669    }
670    #[inline]
671    #[must_use]
672    pub const fn zero() -> Self {
673        Self::new(0.0, 0.0)
674    }
675    /// Convert to a raw `LogicalPosition` (for interop with existing code).
676    #[inline]
677    #[must_use]
678    pub const fn to_logical(self) -> LogicalPosition {
679        LogicalPosition {
680            x: self.x,
681            y: self.y,
682        }
683    }
684    /// Create from a raw `LogicalPosition` that is known to be in screen space.
685    #[inline]
686    #[must_use]
687    pub const fn from_logical(p: LogicalPosition) -> Self {
688        Self { x: p.x, y: p.y }
689    }
690}
691
692impl_option!(
693    ScreenPosition,
694    OptionScreenPosition,
695    [Debug, Copy, Clone, PartialEq, PartialOrd]
696);
697
698/// Position relative to a DOM node's border box origin (logical pixels).
699#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
700#[repr(C)]
701pub struct CursorNodePosition {
702    pub x: f32,
703    pub y: f32,
704}
705
706impl CursorNodePosition {
707    #[inline]
708    #[must_use]
709    pub const fn new(x: f32, y: f32) -> Self {
710        Self { x, y }
711    }
712    #[inline]
713    #[must_use]
714    pub const fn zero() -> Self {
715        Self::new(0.0, 0.0)
716    }
717    #[inline]
718    #[must_use]
719    pub const fn to_logical(self) -> LogicalPosition {
720        LogicalPosition {
721            x: self.x,
722            y: self.y,
723        }
724    }
725    #[inline]
726    #[must_use]
727    pub const fn from_logical(p: LogicalPosition) -> Self {
728        Self { x: p.x, y: p.y }
729    }
730}
731
732impl_option!(
733    CursorNodePosition,
734    OptionCursorNodePosition,
735    [Debug, Copy, Clone, PartialEq, PartialOrd]
736);
737
738#[cfg(test)]
739#[path = "geom_test.rs"]
740mod geom_test;