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    pub const fn zero() -> Self {
32        Self::new(LogicalPosition::zero(), LogicalSize::zero())
33    }
34    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(always)]
49    pub fn max_x(&self) -> f32 {
50        self.origin.x + self.size.width
51    }
52    /// Returns the minimum x coordinate (origin.x).
53    #[inline(always)]
54    pub fn min_x(&self) -> f32 {
55        self.origin.x
56    }
57    /// Returns the maximum y coordinate (origin.y + height).
58    #[inline(always)]
59    pub fn max_y(&self) -> f32 {
60        self.origin.y + self.size.height
61    }
62    /// Returns the minimum y coordinate (origin.y).
63    #[inline(always)]
64    pub fn min_y(&self) -> f32 {
65        self.origin.y
66    }
67
68    /// Returns whether this rectangle intersects with another rectangle
69    #[inline]
70    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    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    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 {
104            Some(LogicalPosition::new(dx_left_edge, dy_top_edge))
105        } else {
106            None
107        }
108    }
109
110}
111
112impl_vec!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor, LogicalRectVecDestructorType, LogicalRectVecSlice, OptionLogicalRect);
113impl_vec_clone!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor);
114impl_vec_debug!(LogicalRect, LogicalRectVec);
115impl_vec_partialeq!(LogicalRect, LogicalRectVec);
116impl_vec_partialord!(LogicalRect, LogicalRectVec);
117impl_vec_ord!(LogicalRect, LogicalRectVec);
118impl_vec_hash!(LogicalRect, LogicalRectVec);
119impl_vec_eq!(LogicalRect, LogicalRectVec);
120
121use core::{
122    cmp::Ordering,
123    hash::{Hash, Hasher},
124    ops::{self, AddAssign, SubAssign},
125};
126
127use azul_css::props::layout::LayoutWritingMode;
128
129/// A 2D position in logical (DPI-independent) coordinates.
130#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
131#[repr(C)]
132pub struct LogicalPosition {
133    pub x: f32,
134    pub y: f32,
135}
136
137impl LogicalPosition {
138    /// Scales the position in-place by the given DPI scale factor.
139    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
140        self.x *= scale_factor;
141        self.y *= scale_factor;
142    }
143}
144
145impl SubAssign<LogicalPosition> for LogicalPosition {
146    fn sub_assign(&mut self, other: LogicalPosition) {
147        self.x -= other.x;
148        self.y -= other.y;
149    }
150}
151
152impl AddAssign<LogicalPosition> for LogicalPosition {
153    fn add_assign(&mut self, other: LogicalPosition) {
154        self.x += other.x;
155        self.y += other.y;
156    }
157}
158
159impl core::fmt::Debug for LogicalPosition {
160    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
161        write!(f, "({}, {})", self.x, self.y)
162    }
163}
164
165impl core::fmt::Display for LogicalPosition {
166    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
167        write!(f, "({}, {})", self.x, self.y)
168    }
169}
170
171impl ops::Add for LogicalPosition {
172    type Output = Self;
173
174    #[inline]
175    fn add(self, other: Self) -> Self {
176        Self {
177            x: self.x + other.x,
178            y: self.y + other.y,
179        }
180    }
181}
182
183impl ops::Sub for LogicalPosition {
184    type Output = Self;
185
186    #[inline]
187    fn sub(self, other: Self) -> Self {
188        Self {
189            x: self.x - other.x,
190            y: self.y - other.y,
191        }
192    }
193}
194
195/// Multiplier for converting f32 coordinates to integers in Ord/Hash impls.
196/// Provides ~0.001 precision, sufficient for sub-pixel layout coordinates.
197const DECIMAL_MULTIPLIER: f32 = 1000.0;
198
199impl_option!(
200    LogicalPosition,
201    OptionLogicalPosition,
202    [Debug, Copy, Clone, PartialEq, PartialOrd]
203);
204
205impl Ord for LogicalPosition {
206    fn cmp(&self, other: &LogicalPosition) -> Ordering {
207        let self_x = (self.x * DECIMAL_MULTIPLIER) as isize;
208        let self_y = (self.y * DECIMAL_MULTIPLIER) as isize;
209        let other_x = (other.x * DECIMAL_MULTIPLIER) as isize;
210        let other_y = (other.y * DECIMAL_MULTIPLIER) as isize;
211        self_x.cmp(&other_x).then(self_y.cmp(&other_y))
212    }
213}
214
215impl Eq for LogicalPosition {}
216
217impl Hash for LogicalPosition {
218    fn hash<H>(&self, state: &mut H)
219    where
220        H: Hasher,
221    {
222        let self_x = (self.x * DECIMAL_MULTIPLIER) as isize;
223        let self_y = (self.y * DECIMAL_MULTIPLIER) as isize;
224        self_x.hash(state);
225        self_y.hash(state);
226    }
227}
228
229impl LogicalPosition {
230    /// Returns the main-axis component for the given writing mode.
231    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
232        match wm {
233            LayoutWritingMode::HorizontalTb => self.y,
234            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.x,
235        }
236    }
237
238    /// Returns the cross-axis component for the given writing mode.
239    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
240        match wm {
241            LayoutWritingMode::HorizontalTb => self.x,
242            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.y,
243        }
244    }
245
246    /// Creates a `LogicalPosition` from main and cross axis dimensions.
247    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
248        match wm {
249            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
250            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
251        }
252    }
253}
254
255/// A 2D size in logical (DPI-independent) coordinates.
256#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
257#[repr(C)]
258pub struct LogicalSize {
259    pub width: f32,
260    pub height: f32,
261}
262
263impl LogicalSize {
264    /// Scales the size in-place by the given DPI scale factor and returns self.
265    pub fn scale_for_dpi(&mut self, scale_factor: f32) -> Self {
266        self.width *= scale_factor;
267        self.height *= scale_factor;
268        *self
269    }
270
271    /// Creates a `LogicalSize` from main and cross axis dimensions.
272    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
273        match wm {
274            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
275            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
276        }
277    }
278}
279
280impl core::fmt::Debug for LogicalSize {
281    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
282        write!(f, "{}x{}", self.width, self.height)
283    }
284}
285
286impl core::fmt::Display for LogicalSize {
287    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
288        write!(f, "{}x{}", self.width, self.height)
289    }
290}
291
292impl_option!(
293    LogicalSize,
294    OptionLogicalSize,
295    [Debug, Copy, Clone, PartialEq, PartialOrd]
296);
297
298impl_option!(
299    LogicalRect,
300    OptionLogicalRect,
301    [Debug, Copy, Clone, PartialEq, PartialOrd]
302);
303
304impl Ord for LogicalSize {
305    fn cmp(&self, other: &LogicalSize) -> Ordering {
306        let self_width = (self.width * DECIMAL_MULTIPLIER) as isize;
307        let self_height = (self.height * DECIMAL_MULTIPLIER) as isize;
308        let other_width = (other.width * DECIMAL_MULTIPLIER) as isize;
309        let other_height = (other.height * DECIMAL_MULTIPLIER) as isize;
310        self_width
311            .cmp(&other_width)
312            .then(self_height.cmp(&other_height))
313    }
314}
315
316impl Eq for LogicalSize {}
317
318impl Hash for LogicalSize {
319    fn hash<H>(&self, state: &mut H)
320    where
321        H: Hasher,
322    {
323        let self_width = (self.width * DECIMAL_MULTIPLIER) as isize;
324        let self_height = (self.height * DECIMAL_MULTIPLIER) as isize;
325        self_width.hash(state);
326        self_height.hash(state);
327    }
328}
329
330impl LogicalSize {
331    /// Returns the main-axis dimension for the given writing mode.
332    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
333        match wm {
334            LayoutWritingMode::HorizontalTb => self.height,
335            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.width,
336        }
337    }
338
339    /// Returns the cross-axis dimension for the given writing mode.
340    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
341        match wm {
342            LayoutWritingMode::HorizontalTb => self.width,
343            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.height,
344        }
345    }
346
347    /// Returns a new `LogicalSize` with the main-axis dimension updated.
348    pub fn with_main(self, wm: LayoutWritingMode, value: f32) -> Self {
349        match wm {
350            LayoutWritingMode::HorizontalTb => Self {
351                height: value,
352                ..self
353            },
354            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
355                width: value,
356                ..self
357            },
358        }
359    }
360
361    /// Returns a new `LogicalSize` with the cross-axis dimension updated.
362    pub fn with_cross(self, wm: LayoutWritingMode, value: f32) -> Self {
363        match wm {
364            LayoutWritingMode::HorizontalTb => Self {
365                width: value,
366                ..self
367            },
368            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
369                height: value,
370                ..self
371            },
372        }
373    }
374}
375
376/// A 2D position in physical (pixel) coordinates.
377#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
378#[repr(C)]
379pub struct PhysicalPosition<T> {
380    pub x: T,
381    pub y: T,
382}
383
384impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalPosition<T> {
385    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
386        write!(f, "({}, {})", self.x, self.y)
387    }
388}
389
390pub type PhysicalPositionI32 = PhysicalPosition<i32>;
391impl_option!(
392    PhysicalPositionI32,
393    OptionPhysicalPositionI32,
394    [Debug, Copy, Clone, PartialEq, PartialOrd]
395);
396
397/// A 2D size in physical (pixel) coordinates.
398#[derive(Ord, Hash, Eq, Copy, Clone, PartialEq, PartialOrd)]
399#[repr(C)]
400pub struct PhysicalSize<T> {
401    pub width: T,
402    pub height: T,
403}
404
405impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalSize<T> {
406    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
407        write!(f, "{}x{}", self.width, self.height)
408    }
409}
410
411pub type PhysicalSizeU32 = PhysicalSize<u32>;
412impl_option!(
413    PhysicalSizeU32,
414    OptionPhysicalSizeU32,
415    [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
416);
417pub type PhysicalSizeF32 = PhysicalSize<f32>;
418impl_option!(
419    PhysicalSizeF32,
420    OptionPhysicalSizeF32,
421    [Debug, Copy, Clone, PartialEq, PartialOrd]
422);
423
424impl LogicalPosition {
425    #[inline(always)]
426    pub const fn new(x: f32, y: f32) -> Self {
427        Self { x, y }
428    }
429    #[inline(always)]
430    pub const fn zero() -> Self {
431        Self::new(0.0, 0.0)
432    }
433    /// Converts to physical pixel coordinates by multiplying by the DPI factor.
434    #[inline(always)]
435    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalPosition<u32> {
436        PhysicalPosition {
437            x: libm::roundf(self.x * hidpi_factor) as u32,
438            y: libm::roundf(self.y * hidpi_factor) as u32,
439        }
440    }
441}
442
443impl<T> PhysicalPosition<T> {
444    #[inline(always)]
445    pub const fn new(x: T, y: T) -> Self {
446        Self { x, y }
447    }
448}
449
450impl PhysicalPosition<i32> {
451    #[inline(always)]
452    pub const fn zero() -> Self {
453        Self::new(0, 0)
454    }
455    /// Converts to logical coordinates by dividing by the DPI factor.
456    #[inline(always)]
457    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
458        LogicalPosition {
459            x: self.x as f32 / hidpi_factor,
460            y: self.y as f32 / hidpi_factor,
461        }
462    }
463}
464
465impl PhysicalPosition<f64> {
466    #[inline(always)]
467    pub const fn zero() -> Self {
468        Self::new(0.0, 0.0)
469    }
470    /// Converts to logical coordinates by dividing by the DPI factor.
471    #[inline(always)]
472    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
473        LogicalPosition {
474            x: self.x as f32 / hidpi_factor,
475            y: self.y as f32 / hidpi_factor,
476        }
477    }
478}
479
480impl LogicalSize {
481    #[inline(always)]
482    pub const fn new(width: f32, height: f32) -> Self {
483        Self { width, height }
484    }
485    #[inline(always)]
486    pub const fn zero() -> Self {
487        Self::new(0.0, 0.0)
488    }
489    /// Converts to physical pixel size by multiplying by the DPI factor.
490    #[inline(always)]
491    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalSize<u32> {
492        PhysicalSize {
493            width: libm::roundf(self.width * hidpi_factor) as u32,
494            height: libm::roundf(self.height * hidpi_factor) as u32,
495        }
496    }
497}
498
499impl<T> PhysicalSize<T> {
500    #[inline(always)]
501    pub const fn new(width: T, height: T) -> Self {
502        Self { width, height }
503    }
504}
505
506impl PhysicalSize<u32> {
507    #[inline(always)]
508    pub const fn zero() -> Self {
509        Self::new(0, 0)
510    }
511    /// Converts to logical coordinates by dividing by the DPI factor.
512    #[inline(always)]
513    pub fn to_logical(self, hidpi_factor: f32) -> LogicalSize {
514        LogicalSize {
515            width: self.width as f32 / hidpi_factor,
516            height: self.height as f32 / hidpi_factor,
517        }
518    }
519}
520
521/// Marker enum documenting which coordinate space a geometric value is in.
522///
523/// This is for documentation and debugging purposes only — it does not enforce
524/// type safety at compile time. Use comments like `[CoordinateSpace::Window]`
525/// or `[CoordinateSpace::ScrollFrame]` in code to document coordinate contexts.
526///
527/// **Common bug pattern:** passing `Window`-space coordinates where
528/// `ScrollFrame`-space is expected (or vice versa). The scroll frame creates a
529/// new spatial node, so primitives must be offset by the frame origin.
530#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
531#[repr(C)]
532pub enum CoordinateSpace {
533    /// Absolute coordinates from window top-left (0,0).
534    /// Layout engine output is in this space.
535    Window,
536    
537    /// Relative to scroll frame content origin.
538    /// Transformation: scroll_pos = window_pos - scroll_frame_origin
539    ScrollFrame,
540    
541    /// Relative to parent node's content box origin.
542    Parent,
543    
544    /// Relative to a CSS transform reference frame origin.
545    ReferenceFrame,
546}
547
548
549// =============================================================================
550// Type-safe coordinate newtypes for API clarity
551// =============================================================================
552
553/// Position in screen coordinates (logical pixels, relative to primary monitor origin).
554/// On Wayland: falls back to window-local since global coords are unavailable.
555#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
556#[repr(C)]
557pub struct ScreenPosition {
558    pub x: f32,
559    pub y: f32,
560}
561
562impl ScreenPosition {
563    #[inline(always)]
564    pub const fn new(x: f32, y: f32) -> Self {
565        Self { x, y }
566    }
567    #[inline(always)]
568    pub const fn zero() -> Self {
569        Self::new(0.0, 0.0)
570    }
571    /// Convert to a raw LogicalPosition (for interop with existing code).
572    #[inline(always)]
573    pub const fn to_logical(self) -> LogicalPosition {
574        LogicalPosition { x: self.x, y: self.y }
575    }
576    /// Create from a raw LogicalPosition that is known to be in screen space.
577    #[inline(always)]
578    pub const fn from_logical(p: LogicalPosition) -> Self {
579        Self { x: p.x, y: p.y }
580    }
581}
582
583impl_option!(
584    ScreenPosition,
585    OptionScreenPosition,
586    [Debug, Copy, Clone, PartialEq, PartialOrd]
587);
588
589/// Position relative to a DOM node's border box origin (logical pixels).
590#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
591#[repr(C)]
592pub struct CursorNodePosition {
593    pub x: f32,
594    pub y: f32,
595}
596
597impl CursorNodePosition {
598    #[inline(always)]
599    pub const fn new(x: f32, y: f32) -> Self {
600        Self { x, y }
601    }
602    #[inline(always)]
603    pub const fn zero() -> Self {
604        Self::new(0.0, 0.0)
605    }
606    #[inline(always)]
607    pub const fn to_logical(self) -> LogicalPosition {
608        LogicalPosition { x: self.x, y: self.y }
609    }
610    #[inline(always)]
611    pub const fn from_logical(p: LogicalPosition) -> Self {
612        Self { x: p.x, y: p.y }
613    }
614}
615
616impl_option!(
617    CursorNodePosition,
618    OptionCursorNodePosition,
619    [Debug, Copy, Clone, PartialEq, PartialOrd]
620);