Skip to main content

azul_core/
geom.rs

1#[derive(Copy, Default, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
2#[repr(C)]
3pub struct LogicalRect {
4    pub origin: LogicalPosition,
5    pub size: LogicalSize,
6}
7
8impl core::fmt::Debug for LogicalRect {
9    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
10        write!(f, "{} @ {}", self.size, self.origin)
11    }
12}
13
14impl core::fmt::Display for LogicalRect {
15    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
16        write!(f, "{} @ {}", self.size, self.origin)
17    }
18}
19
20impl LogicalRect {
21    pub const fn zero() -> Self {
22        Self::new(LogicalPosition::zero(), LogicalSize::zero())
23    }
24    pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
25        Self { origin, size }
26    }
27
28    #[inline]
29    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
30        self.origin.x *= scale_factor;
31        self.origin.y *= scale_factor;
32        self.size.width *= scale_factor;
33        self.size.height *= scale_factor;
34    }
35
36    #[inline(always)]
37    pub fn max_x(&self) -> f32 {
38        self.origin.x + self.size.width
39    }
40    #[inline(always)]
41    pub fn min_x(&self) -> f32 {
42        self.origin.x
43    }
44    #[inline(always)]
45    pub fn max_y(&self) -> f32 {
46        self.origin.y + self.size.height
47    }
48    #[inline(always)]
49    pub fn min_y(&self) -> f32 {
50        self.origin.y
51    }
52
53    /// Returns whether this rectangle intersects with another rectangle
54    #[inline]
55    pub fn intersects(&self, other: Self) -> bool {
56        // Check if one rectangle is to the left of the other
57        if self.max_x() <= other.min_x() || other.max_x() <= self.min_x() {
58            return false;
59        }
60
61        // Check if one rectangle is above the other
62        if self.max_y() <= other.min_y() || other.max_y() <= self.min_y() {
63            return false;
64        }
65
66        // If we got here, the rectangles must intersect
67        true
68    }
69
70    /// Returns whether this rectangle contains the given point
71    #[inline]
72    pub fn contains(&self, point: LogicalPosition) -> bool {
73        point.x >= self.min_x()
74            && point.x < self.max_x()
75            && point.y >= self.min_y()
76            && point.y < self.max_y()
77    }
78
79    /// Faster union for a Vec<LayoutRect>
80    #[inline]
81    pub fn union<I: Iterator<Item = Self>>(mut rects: I) -> Option<Self> {
82        let first = rects.next()?;
83
84        let mut max_width = first.size.width;
85        let mut max_height = first.size.height;
86        let mut min_x = first.origin.x;
87        let mut min_y = first.origin.y;
88
89        while let Some(Self {
90            origin: LogicalPosition { x, y },
91            size: LogicalSize { width, height },
92        }) = rects.next()
93        {
94            let cur_lower_right_x = x + width;
95            let cur_lower_right_y = y + height;
96            max_width = max_width.max(cur_lower_right_x - min_x);
97            max_height = max_height.max(cur_lower_right_y - min_y);
98            min_x = min_x.min(x);
99            min_y = min_y.min(y);
100        }
101
102        Some(Self {
103            origin: LogicalPosition { x: min_x, y: min_y },
104            size: LogicalSize {
105                width: max_width,
106                height: max_height,
107            },
108        })
109    }
110
111    /// Same as `contains()`, but returns the (x, y) offset of the hit point
112    ///
113    /// On a regular computer this function takes ~3.2ns to run
114    #[inline]
115    pub fn hit_test(&self, other: &LogicalPosition) -> Option<LogicalPosition> {
116        let dx_left_edge = other.x - self.min_x();
117        let dx_right_edge = self.max_x() - other.x;
118        let dy_top_edge = other.y - self.min_y();
119        let dy_bottom_edge = self.max_y() - other.y;
120        if dx_left_edge > 0.0 && dx_right_edge > 0.0 && dy_top_edge > 0.0 && dy_bottom_edge > 0.0 {
121            Some(LogicalPosition::new(dx_left_edge, dy_top_edge))
122        } else {
123            None
124        }
125    }
126
127    pub fn to_layout_rect(&self) -> LayoutRect {
128        LayoutRect {
129            origin: LayoutPoint::new(
130                libm::roundf(self.origin.x) as isize,
131                libm::roundf(self.origin.y) as isize,
132            ),
133            size: LayoutSize::new(
134                libm::roundf(self.size.width) as isize,
135                libm::roundf(self.size.height) as isize,
136            ),
137        }
138    }
139}
140
141impl_vec!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor, LogicalRectVecDestructorType, LogicalRectVecSlice, OptionLogicalRect);
142impl_vec_clone!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor);
143impl_vec_debug!(LogicalRect, LogicalRectVec);
144impl_vec_partialeq!(LogicalRect, LogicalRectVec);
145impl_vec_partialord!(LogicalRect, LogicalRectVec);
146impl_vec_ord!(LogicalRect, LogicalRectVec);
147impl_vec_hash!(LogicalRect, LogicalRectVec);
148impl_vec_eq!(LogicalRect, LogicalRectVec);
149
150use core::{
151    cmp::Ordering,
152    hash::{Hash, Hasher},
153    ops::{self, AddAssign, SubAssign},
154};
155
156use azul_css::props::{
157    basic::{LayoutPoint, LayoutRect, LayoutSize},
158    layout::LayoutWritingMode,
159};
160
161#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
162#[repr(C)]
163pub struct LogicalPosition {
164    pub x: f32,
165    pub y: f32,
166}
167
168impl LogicalPosition {
169    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
170        self.x *= scale_factor;
171        self.y *= scale_factor;
172    }
173}
174
175impl SubAssign<LogicalPosition> for LogicalPosition {
176    fn sub_assign(&mut self, other: LogicalPosition) {
177        self.x -= other.x;
178        self.y -= other.y;
179    }
180}
181
182impl AddAssign<LogicalPosition> for LogicalPosition {
183    fn add_assign(&mut self, other: LogicalPosition) {
184        self.x += other.x;
185        self.y += other.y;
186    }
187}
188
189impl core::fmt::Debug for LogicalPosition {
190    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
191        write!(f, "({}, {})", self.x, self.y)
192    }
193}
194
195impl core::fmt::Display for LogicalPosition {
196    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
197        write!(f, "({}, {})", self.x, self.y)
198    }
199}
200
201impl ops::Add for LogicalPosition {
202    type Output = Self;
203
204    #[inline]
205    fn add(self, other: Self) -> Self {
206        Self {
207            x: self.x + other.x,
208            y: self.y + other.y,
209        }
210    }
211}
212
213impl ops::Sub for LogicalPosition {
214    type Output = Self;
215
216    #[inline]
217    fn sub(self, other: Self) -> Self {
218        Self {
219            x: self.x - other.x,
220            y: self.y - other.y,
221        }
222    }
223}
224
225const DECIMAL_MULTIPLIER: f32 = 1000.0;
226
227impl_option!(
228    LogicalPosition,
229    OptionLogicalPosition,
230    [Debug, Copy, Clone, PartialEq, PartialOrd]
231);
232
233impl Ord for LogicalPosition {
234    fn cmp(&self, other: &LogicalPosition) -> Ordering {
235        let self_x = (self.x * DECIMAL_MULTIPLIER) as usize;
236        let self_y = (self.y * DECIMAL_MULTIPLIER) as usize;
237        let other_x = (other.x * DECIMAL_MULTIPLIER) as usize;
238        let other_y = (other.y * DECIMAL_MULTIPLIER) as usize;
239        self_x.cmp(&other_x).then(self_y.cmp(&other_y))
240    }
241}
242
243impl Eq for LogicalPosition {}
244
245impl Hash for LogicalPosition {
246    fn hash<H>(&self, state: &mut H)
247    where
248        H: Hasher,
249    {
250        let self_x = (self.x * DECIMAL_MULTIPLIER) as usize;
251        let self_y = (self.y * DECIMAL_MULTIPLIER) as usize;
252        self_x.hash(state);
253        self_y.hash(state);
254    }
255}
256
257impl LogicalPosition {
258    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
259        match wm {
260            LayoutWritingMode::HorizontalTb => self.y,
261            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.x,
262        }
263    }
264
265    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
266        match wm {
267            LayoutWritingMode::HorizontalTb => self.x,
268            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.y,
269        }
270    }
271
272    // Creates a LogicalPosition from main and cross axis dimensions.
273    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
274        match wm {
275            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
276            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
277        }
278    }
279}
280
281#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
282#[repr(C)]
283pub struct LogicalSize {
284    pub width: f32,
285    pub height: f32,
286}
287
288impl LogicalSize {
289    pub fn scale_for_dpi(&mut self, scale_factor: f32) -> Self {
290        self.width *= scale_factor;
291        self.height *= scale_factor;
292        *self
293    }
294
295    // Creates a LogicalSize from main and cross axis dimensions.
296    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
297        match wm {
298            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
299            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
300        }
301    }
302}
303
304impl core::fmt::Debug for LogicalSize {
305    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
306        write!(f, "{}x{}", self.width, self.height)
307    }
308}
309
310impl core::fmt::Display for LogicalSize {
311    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
312        write!(f, "{}x{}", self.width, self.height)
313    }
314}
315
316impl_option!(
317    LogicalSize,
318    OptionLogicalSize,
319    [Debug, Copy, Clone, PartialEq, PartialOrd]
320);
321
322impl_option!(
323    LogicalRect,
324    OptionLogicalRect,
325    [Debug, Copy, Clone, PartialEq, PartialOrd]
326);
327
328impl Ord for LogicalSize {
329    fn cmp(&self, other: &LogicalSize) -> Ordering {
330        let self_width = (self.width * DECIMAL_MULTIPLIER) as usize;
331        let self_height = (self.height * DECIMAL_MULTIPLIER) as usize;
332        let other_width = (other.width * DECIMAL_MULTIPLIER) as usize;
333        let other_height = (other.height * DECIMAL_MULTIPLIER) as usize;
334        self_width
335            .cmp(&other_width)
336            .then(self_height.cmp(&other_height))
337    }
338}
339
340impl Eq for LogicalSize {}
341
342impl Hash for LogicalSize {
343    fn hash<H>(&self, state: &mut H)
344    where
345        H: Hasher,
346    {
347        let self_width = (self.width * DECIMAL_MULTIPLIER) as usize;
348        let self_height = (self.height * DECIMAL_MULTIPLIER) as usize;
349        self_width.hash(state);
350        self_height.hash(state);
351    }
352}
353
354impl LogicalSize {
355    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
356        match wm {
357            LayoutWritingMode::HorizontalTb => self.height,
358            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.width,
359        }
360    }
361
362    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
363        match wm {
364            LayoutWritingMode::HorizontalTb => self.width,
365            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.height,
366        }
367    }
368
369    // Returns a new LogicalSize with the main-axis dimension updated.
370    pub fn with_main(self, wm: LayoutWritingMode, value: f32) -> Self {
371        match wm {
372            LayoutWritingMode::HorizontalTb => Self {
373                height: value,
374                ..self
375            },
376            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
377                width: value,
378                ..self
379            },
380        }
381    }
382
383    pub fn with_cross(self, wm: LayoutWritingMode, value: f32) -> Self {
384        match wm {
385            LayoutWritingMode::HorizontalTb => Self {
386                width: value,
387                ..self
388            },
389            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
390                height: value,
391                ..self
392            },
393        }
394    }
395}
396
397#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
398#[repr(C)]
399pub struct PhysicalPosition<T> {
400    pub x: T,
401    pub y: T,
402}
403
404impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalPosition<T> {
405    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
406        write!(f, "({}, {})", self.x, self.y)
407    }
408}
409
410pub type PhysicalPositionI32 = PhysicalPosition<i32>;
411impl_option!(
412    PhysicalPositionI32,
413    OptionPhysicalPositionI32,
414    [Debug, Copy, Clone, PartialEq, PartialOrd]
415);
416
417#[derive(Ord, Hash, Eq, Copy, Clone, PartialEq, PartialOrd)]
418#[repr(C)]
419pub struct PhysicalSize<T> {
420    pub width: T,
421    pub height: T,
422}
423
424impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalSize<T> {
425    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
426        write!(f, "{}x{}", self.width, self.height)
427    }
428}
429
430pub type PhysicalSizeU32 = PhysicalSize<u32>;
431impl_option!(
432    PhysicalSizeU32,
433    OptionPhysicalSizeU32,
434    [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
435);
436pub type PhysicalSizeF32 = PhysicalSize<f32>;
437impl_option!(
438    PhysicalSizeF32,
439    OptionPhysicalSizeF32,
440    [Debug, Copy, Clone, PartialEq, PartialOrd]
441);
442
443impl LogicalPosition {
444    #[inline(always)]
445    pub const fn new(x: f32, y: f32) -> Self {
446        Self { x, y }
447    }
448    #[inline(always)]
449    pub const fn zero() -> Self {
450        Self::new(0.0, 0.0)
451    }
452    #[inline(always)]
453    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalPosition<u32> {
454        PhysicalPosition {
455            x: (self.x * hidpi_factor) as u32,
456            y: (self.y * hidpi_factor) as u32,
457        }
458    }
459}
460
461impl<T> PhysicalPosition<T> {
462    #[inline(always)]
463    pub const fn new(x: T, y: T) -> Self {
464        Self { x, y }
465    }
466}
467
468impl PhysicalPosition<i32> {
469    #[inline(always)]
470    pub const fn zero() -> Self {
471        Self::new(0, 0)
472    }
473    #[inline(always)]
474    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
475        LogicalPosition {
476            x: self.x as f32 / hidpi_factor,
477            y: self.y as f32 / hidpi_factor,
478        }
479    }
480}
481
482impl PhysicalPosition<f64> {
483    #[inline(always)]
484    pub const fn zero() -> Self {
485        Self::new(0.0, 0.0)
486    }
487    #[inline(always)]
488    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
489        LogicalPosition {
490            x: self.x as f32 / hidpi_factor,
491            y: self.y as f32 / hidpi_factor,
492        }
493    }
494}
495
496impl LogicalSize {
497    #[inline(always)]
498    pub const fn new(width: f32, height: f32) -> Self {
499        Self { width, height }
500    }
501    #[inline(always)]
502    pub const fn zero() -> Self {
503        Self::new(0.0, 0.0)
504    }
505    #[inline(always)]
506    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalSize<u32> {
507        PhysicalSize {
508            width: (self.width * hidpi_factor) as u32,
509            height: (self.height * hidpi_factor) as u32,
510        }
511    }
512}
513
514impl<T> PhysicalSize<T> {
515    #[inline(always)]
516    pub const fn new(width: T, height: T) -> Self {
517        Self { width, height }
518    }
519}
520
521impl PhysicalSize<u32> {
522    #[inline(always)]
523    pub const fn zero() -> Self {
524        Self::new(0, 0)
525    }
526    #[inline(always)]
527    pub fn to_logical(self, hidpi_factor: f32) -> LogicalSize {
528        LogicalSize {
529            width: self.width as f32 / hidpi_factor,
530            height: self.height as f32 / hidpi_factor,
531        }
532    }
533}
534
535// =============================================================================
536// CoordinateSpace - Debug marker for documenting coordinate system contexts
537// =============================================================================
538//
539// This enum serves as DOCUMENTATION for which coordinate space a value is in.
540// It does NOT enforce type-safety at compile time (no PhantomData generics).
541// The purpose is to help developers understand and debug coordinate transformations.
542//
543// COORDINATE SPACES IN AZUL:
544//
545// 1. Window (absolute coordinates from window top-left)
546//    - All layout primitives are initially computed in this space
547//    - Origin: (0, 0) = top-left corner of the window content area
548//    - Used by: Layout engine output, display list items before compositor
549//
550// 2. ScrollFrame (relative to scroll container origin)
551//    - Used for primitives inside a WebRender scroll frame
552//    - Origin: (0, 0) = top-left of scrollable content area
553//    - Transformation: scroll_pos = window_pos - scroll_frame_origin
554//    - The scroll_frame_origin is the Window-space position of the scroll frame
555//
556// 3. Parent (relative to parent node origin)  
557//    - Used for relative positioning within a parent container
558//    - Origin: (0, 0) = top-left of parent's content box
559//
560// 4. ReferenceFrame (relative to a CSS transform origin)
561//    - Used for primitives inside a WebRender reference frame (transforms)
562//    - Origin: Defined by the transform-origin property
563//
564// COMMON BUG PATTERN:
565//
566// The Y-offset bug in text areas was caused by passing Window-space coordinates
567// to WebRender when it expected ScrollFrame-space coordinates. The scroll frame
568// creates a new spatial node, so primitives must be offset by the frame origin.
569//
570// WRONG:  Push same offset for scroll frames (content appears at window position)
571// RIGHT:  Push frame_origin as new offset (content positioned relative to frame)
572
573/// Marker enum documenting which coordinate space a geometric value is in.
574/// 
575/// This is for documentation and debugging purposes only - it does not enforce
576/// type safety at compile time. Use comments like `[CoordinateSpace::Window]`
577/// or `[CoordinateSpace::ScrollFrame]` in code to document coordinate contexts.
578/// 
579/// See the module-level documentation above for details on each space.
580#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
581#[repr(C)]
582pub enum CoordinateSpace {
583    /// Absolute coordinates from window top-left (0,0).
584    /// Layout engine output is in this space.
585    Window,
586    
587    /// Relative to scroll frame content origin.
588    /// Transformation: scroll_pos = window_pos - scroll_frame_origin
589    ScrollFrame,
590    
591    /// Relative to parent node's content box origin.
592    Parent,
593    
594    /// Relative to a CSS transform reference frame origin.
595    ReferenceFrame,
596}
597
598impl CoordinateSpace {
599    /// Returns a human-readable description of this coordinate space.
600    pub const fn description(&self) -> &'static str {
601        match self {
602            CoordinateSpace::Window => "Absolute window coordinates (layout engine output)",
603            CoordinateSpace::ScrollFrame => "Relative to scroll frame origin (for WebRender scroll nodes)",
604            CoordinateSpace::Parent => "Relative to parent node origin",
605            CoordinateSpace::ReferenceFrame => "Relative to CSS transform origin",
606        }
607    }
608}
609
610// =============================================================================
611// Type-safe coordinate newtypes for API clarity
612// =============================================================================
613
614/// Position in screen coordinates (logical pixels, relative to primary monitor origin).
615/// On Wayland: falls back to window-local since global coords are unavailable.
616#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
617#[repr(C)]
618pub struct ScreenPosition {
619    pub x: f32,
620    pub y: f32,
621}
622
623impl ScreenPosition {
624    #[inline(always)]
625    pub const fn new(x: f32, y: f32) -> Self {
626        Self { x, y }
627    }
628    #[inline(always)]
629    pub const fn zero() -> Self {
630        Self::new(0.0, 0.0)
631    }
632    /// Convert to a raw LogicalPosition (for interop with existing code).
633    #[inline(always)]
634    pub const fn to_logical(self) -> LogicalPosition {
635        LogicalPosition { x: self.x, y: self.y }
636    }
637    /// Create from a raw LogicalPosition that is known to be in screen space.
638    #[inline(always)]
639    pub const fn from_logical(p: LogicalPosition) -> Self {
640        Self { x: p.x, y: p.y }
641    }
642}
643
644impl_option!(
645    ScreenPosition,
646    OptionScreenPosition,
647    [Debug, Copy, Clone, PartialEq, PartialOrd]
648);
649
650/// Position relative to a DOM node's border box origin (logical pixels).
651#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
652#[repr(C)]
653pub struct CursorNodePosition {
654    pub x: f32,
655    pub y: f32,
656}
657
658impl CursorNodePosition {
659    #[inline(always)]
660    pub const fn new(x: f32, y: f32) -> Self {
661        Self { x, y }
662    }
663    #[inline(always)]
664    pub const fn zero() -> Self {
665        Self::new(0.0, 0.0)
666    }
667    #[inline(always)]
668    pub const fn to_logical(self) -> LogicalPosition {
669        LogicalPosition { x: self.x, y: self.y }
670    }
671    #[inline(always)]
672    pub const fn from_logical(p: LogicalPosition) -> Self {
673        Self { x: p.x, y: p.y }
674    }
675}
676
677impl_option!(
678    CursorNodePosition,
679    OptionCursorNodePosition,
680    [Debug, Copy, Clone, PartialEq, PartialOrd]
681);
682
683/// Drag offset from the cursor position at drag start (logical pixels).
684/// `dx`/`dy` are the delta from drag start to current position.
685#[derive(Default, Debug, Copy, Clone, PartialEq, PartialOrd)]
686#[repr(C)]
687pub struct DragDelta {
688    pub dx: f32,
689    pub dy: f32,
690}
691
692impl DragDelta {
693    #[inline(always)]
694    pub const fn new(dx: f32, dy: f32) -> Self {
695        Self { dx, dy }
696    }
697    #[inline(always)]
698    pub const fn zero() -> Self {
699        Self::new(0.0, 0.0)
700    }
701}
702
703impl_option!(
704    DragDelta,
705    OptionDragDelta,
706    [Debug, Copy, Clone, PartialEq, PartialOrd]
707);