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!(
142    LogicalRect,
143    LogicalRectVec,
144    LogicalRectVecDestructor,
145    LogicalRectVecDestructorType
146);
147impl_vec_clone!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor);
148impl_vec_debug!(LogicalRect, LogicalRectVec);
149impl_vec_partialeq!(LogicalRect, LogicalRectVec);
150impl_vec_partialord!(LogicalRect, LogicalRectVec);
151impl_vec_ord!(LogicalRect, LogicalRectVec);
152impl_vec_hash!(LogicalRect, LogicalRectVec);
153impl_vec_eq!(LogicalRect, LogicalRectVec);
154
155use core::{
156    cmp::Ordering,
157    hash::{Hash, Hasher},
158    ops::{self, AddAssign, SubAssign},
159};
160
161use azul_css::props::{
162    basic::{LayoutPoint, LayoutRect, LayoutSize},
163    layout::LayoutWritingMode,
164};
165
166#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
167#[repr(C)]
168pub struct LogicalPosition {
169    pub x: f32,
170    pub y: f32,
171}
172
173impl LogicalPosition {
174    pub fn scale_for_dpi(&mut self, scale_factor: f32) {
175        self.x *= scale_factor;
176        self.y *= scale_factor;
177    }
178}
179
180impl SubAssign<LogicalPosition> for LogicalPosition {
181    fn sub_assign(&mut self, other: LogicalPosition) {
182        self.x -= other.x;
183        self.y -= other.y;
184    }
185}
186
187impl AddAssign<LogicalPosition> for LogicalPosition {
188    fn add_assign(&mut self, other: LogicalPosition) {
189        self.x += other.x;
190        self.y += other.y;
191    }
192}
193
194impl core::fmt::Debug for LogicalPosition {
195    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
196        write!(f, "({}, {})", self.x, self.y)
197    }
198}
199
200impl core::fmt::Display for LogicalPosition {
201    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
202        write!(f, "({}, {})", self.x, self.y)
203    }
204}
205
206impl ops::Add for LogicalPosition {
207    type Output = Self;
208
209    #[inline]
210    fn add(self, other: Self) -> Self {
211        Self {
212            x: self.x + other.x,
213            y: self.y + other.y,
214        }
215    }
216}
217
218impl ops::Sub for LogicalPosition {
219    type Output = Self;
220
221    #[inline]
222    fn sub(self, other: Self) -> Self {
223        Self {
224            x: self.x - other.x,
225            y: self.y - other.y,
226        }
227    }
228}
229
230const DECIMAL_MULTIPLIER: f32 = 1000.0;
231
232impl_option!(
233    LogicalPosition,
234    OptionLogicalPosition,
235    [Debug, Copy, Clone, PartialEq, PartialOrd]
236);
237
238impl Ord for LogicalPosition {
239    fn cmp(&self, other: &LogicalPosition) -> Ordering {
240        let self_x = (self.x * DECIMAL_MULTIPLIER) as usize;
241        let self_y = (self.y * DECIMAL_MULTIPLIER) as usize;
242        let other_x = (other.x * DECIMAL_MULTIPLIER) as usize;
243        let other_y = (other.y * DECIMAL_MULTIPLIER) as usize;
244        self_x.cmp(&other_x).then(self_y.cmp(&other_y))
245    }
246}
247
248impl Eq for LogicalPosition {}
249
250impl Hash for LogicalPosition {
251    fn hash<H>(&self, state: &mut H)
252    where
253        H: Hasher,
254    {
255        let self_x = (self.x * DECIMAL_MULTIPLIER) as usize;
256        let self_y = (self.y * DECIMAL_MULTIPLIER) as usize;
257        self_x.hash(state);
258        self_y.hash(state);
259    }
260}
261
262impl LogicalPosition {
263    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
264        match wm {
265            LayoutWritingMode::HorizontalTb => self.y,
266            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.x,
267        }
268    }
269
270    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
271        match wm {
272            LayoutWritingMode::HorizontalTb => self.x,
273            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.y,
274        }
275    }
276
277    // Creates a LogicalPosition from main and cross axis dimensions.
278    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
279        match wm {
280            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
281            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
282        }
283    }
284}
285
286#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
287#[repr(C)]
288pub struct LogicalSize {
289    pub width: f32,
290    pub height: f32,
291}
292
293impl LogicalSize {
294    pub fn scale_for_dpi(&mut self, scale_factor: f32) -> Self {
295        self.width *= scale_factor;
296        self.height *= scale_factor;
297        *self
298    }
299
300    // Creates a LogicalSize from main and cross axis dimensions.
301    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
302        match wm {
303            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
304            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
305        }
306    }
307}
308
309impl core::fmt::Debug for LogicalSize {
310    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
311        write!(f, "{}x{}", self.width, self.height)
312    }
313}
314
315impl core::fmt::Display for LogicalSize {
316    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
317        write!(f, "{}x{}", self.width, self.height)
318    }
319}
320
321impl_option!(
322    LogicalSize,
323    OptionLogicalSize,
324    [Debug, Copy, Clone, PartialEq, PartialOrd]
325);
326
327impl_option!(
328    LogicalRect,
329    OptionLogicalRect,
330    [Debug, Copy, Clone, PartialEq, PartialOrd]
331);
332
333impl Ord for LogicalSize {
334    fn cmp(&self, other: &LogicalSize) -> Ordering {
335        let self_width = (self.width * DECIMAL_MULTIPLIER) as usize;
336        let self_height = (self.height * DECIMAL_MULTIPLIER) as usize;
337        let other_width = (other.width * DECIMAL_MULTIPLIER) as usize;
338        let other_height = (other.height * DECIMAL_MULTIPLIER) as usize;
339        self_width
340            .cmp(&other_width)
341            .then(self_height.cmp(&other_height))
342    }
343}
344
345impl Eq for LogicalSize {}
346
347impl Hash for LogicalSize {
348    fn hash<H>(&self, state: &mut H)
349    where
350        H: Hasher,
351    {
352        let self_width = (self.width * DECIMAL_MULTIPLIER) as usize;
353        let self_height = (self.height * DECIMAL_MULTIPLIER) as usize;
354        self_width.hash(state);
355        self_height.hash(state);
356    }
357}
358
359impl LogicalSize {
360    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
361        match wm {
362            LayoutWritingMode::HorizontalTb => self.height,
363            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.width,
364        }
365    }
366
367    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
368        match wm {
369            LayoutWritingMode::HorizontalTb => self.width,
370            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.height,
371        }
372    }
373
374    // Returns a new LogicalSize with the main-axis dimension updated.
375    pub fn with_main(self, wm: LayoutWritingMode, value: f32) -> Self {
376        match wm {
377            LayoutWritingMode::HorizontalTb => Self {
378                height: value,
379                ..self
380            },
381            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
382                width: value,
383                ..self
384            },
385        }
386    }
387
388    pub fn with_cross(self, wm: LayoutWritingMode, value: f32) -> Self {
389        match wm {
390            LayoutWritingMode::HorizontalTb => Self {
391                width: value,
392                ..self
393            },
394            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
395                height: value,
396                ..self
397            },
398        }
399    }
400}
401
402#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
403#[repr(C)]
404pub struct PhysicalPosition<T> {
405    pub x: T,
406    pub y: T,
407}
408
409impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalPosition<T> {
410    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
411        write!(f, "({}, {})", self.x, self.y)
412    }
413}
414
415pub type PhysicalPositionI32 = PhysicalPosition<i32>;
416impl_option!(
417    PhysicalPositionI32,
418    OptionPhysicalPositionI32,
419    [Debug, Copy, Clone, PartialEq, PartialOrd]
420);
421
422#[derive(Ord, Hash, Eq, Copy, Clone, PartialEq, PartialOrd)]
423#[repr(C)]
424pub struct PhysicalSize<T> {
425    pub width: T,
426    pub height: T,
427}
428
429impl<T: ::core::fmt::Display> ::core::fmt::Debug for PhysicalSize<T> {
430    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
431        write!(f, "{}x{}", self.width, self.height)
432    }
433}
434
435pub type PhysicalSizeU32 = PhysicalSize<u32>;
436impl_option!(
437    PhysicalSizeU32,
438    OptionPhysicalSizeU32,
439    [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
440);
441pub type PhysicalSizeF32 = PhysicalSize<f32>;
442impl_option!(
443    PhysicalSizeF32,
444    OptionPhysicalSizeF32,
445    [Debug, Copy, Clone, PartialEq, PartialOrd]
446);
447
448impl LogicalPosition {
449    #[inline(always)]
450    pub const fn new(x: f32, y: f32) -> Self {
451        Self { x, y }
452    }
453    #[inline(always)]
454    pub const fn zero() -> Self {
455        Self::new(0.0, 0.0)
456    }
457    #[inline(always)]
458    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalPosition<u32> {
459        PhysicalPosition {
460            x: (self.x * hidpi_factor) as u32,
461            y: (self.y * hidpi_factor) as u32,
462        }
463    }
464}
465
466impl<T> PhysicalPosition<T> {
467    #[inline(always)]
468    pub const fn new(x: T, y: T) -> Self {
469        Self { x, y }
470    }
471}
472
473impl PhysicalPosition<i32> {
474    #[inline(always)]
475    pub const fn zero() -> Self {
476        Self::new(0, 0)
477    }
478    #[inline(always)]
479    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
480        LogicalPosition {
481            x: self.x as f32 / hidpi_factor,
482            y: self.y as f32 / hidpi_factor,
483        }
484    }
485}
486
487impl PhysicalPosition<f64> {
488    #[inline(always)]
489    pub const fn zero() -> Self {
490        Self::new(0.0, 0.0)
491    }
492    #[inline(always)]
493    pub fn to_logical(self, hidpi_factor: f32) -> LogicalPosition {
494        LogicalPosition {
495            x: self.x as f32 / hidpi_factor,
496            y: self.y as f32 / hidpi_factor,
497        }
498    }
499}
500
501impl LogicalSize {
502    #[inline(always)]
503    pub const fn new(width: f32, height: f32) -> Self {
504        Self { width, height }
505    }
506    #[inline(always)]
507    pub const fn zero() -> Self {
508        Self::new(0.0, 0.0)
509    }
510    #[inline(always)]
511    pub fn to_physical(self, hidpi_factor: f32) -> PhysicalSize<u32> {
512        PhysicalSize {
513            width: (self.width * hidpi_factor) as u32,
514            height: (self.height * hidpi_factor) as u32,
515        }
516    }
517}
518
519impl<T> PhysicalSize<T> {
520    #[inline(always)]
521    pub const fn new(width: T, height: T) -> Self {
522        Self { width, height }
523    }
524}
525
526impl PhysicalSize<u32> {
527    #[inline(always)]
528    pub const fn zero() -> Self {
529        Self::new(0, 0)
530    }
531    #[inline(always)]
532    pub fn to_logical(self, hidpi_factor: f32) -> LogicalSize {
533        LogicalSize {
534            width: self.width as f32 / hidpi_factor,
535            height: self.height as f32 / hidpi_factor,
536        }
537    }
538}
539
540// =============================================================================
541// CoordinateSpace - Debug marker for documenting coordinate system contexts
542// =============================================================================
543//
544// This enum serves as DOCUMENTATION for which coordinate space a value is in.
545// It does NOT enforce type-safety at compile time (no PhantomData generics).
546// The purpose is to help developers understand and debug coordinate transformations.
547//
548// COORDINATE SPACES IN AZUL:
549//
550// 1. Window (absolute coordinates from window top-left)
551//    - All layout primitives are initially computed in this space
552//    - Origin: (0, 0) = top-left corner of the window content area
553//    - Used by: Layout engine output, display list items before compositor
554//
555// 2. ScrollFrame (relative to scroll container origin)
556//    - Used for primitives inside a WebRender scroll frame
557//    - Origin: (0, 0) = top-left of scrollable content area
558//    - Transformation: scroll_pos = window_pos - scroll_frame_origin
559//    - The scroll_frame_origin is the Window-space position of the scroll frame
560//
561// 3. Parent (relative to parent node origin)  
562//    - Used for relative positioning within a parent container
563//    - Origin: (0, 0) = top-left of parent's content box
564//
565// 4. ReferenceFrame (relative to a CSS transform origin)
566//    - Used for primitives inside a WebRender reference frame (transforms)
567//    - Origin: Defined by the transform-origin property
568//
569// COMMON BUG PATTERN:
570//
571// The Y-offset bug in text areas was caused by passing Window-space coordinates
572// to WebRender when it expected ScrollFrame-space coordinates. The scroll frame
573// creates a new spatial node, so primitives must be offset by the frame origin.
574//
575// WRONG:  Push same offset for scroll frames (content appears at window position)
576// RIGHT:  Push frame_origin as new offset (content positioned relative to frame)
577
578/// Marker enum documenting which coordinate space a geometric value is in.
579/// 
580/// This is for documentation and debugging purposes only - it does not enforce
581/// type safety at compile time. Use comments like `[CoordinateSpace::Window]`
582/// or `[CoordinateSpace::ScrollFrame]` in code to document coordinate contexts.
583/// 
584/// See the module-level documentation above for details on each space.
585#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
586#[repr(C)]
587pub enum CoordinateSpace {
588    /// Absolute coordinates from window top-left (0,0).
589    /// Layout engine output is in this space.
590    Window,
591    
592    /// Relative to scroll frame content origin.
593    /// Transformation: scroll_pos = window_pos - scroll_frame_origin
594    ScrollFrame,
595    
596    /// Relative to parent node's content box origin.
597    Parent,
598    
599    /// Relative to a CSS transform reference frame origin.
600    ReferenceFrame,
601}
602
603impl CoordinateSpace {
604    /// Returns a human-readable description of this coordinate space.
605    pub const fn description(&self) -> &'static str {
606        match self {
607            CoordinateSpace::Window => "Absolute window coordinates (layout engine output)",
608            CoordinateSpace::ScrollFrame => "Relative to scroll frame origin (for WebRender scroll nodes)",
609            CoordinateSpace::Parent => "Relative to parent node origin",
610            CoordinateSpace::ReferenceFrame => "Relative to CSS transform origin",
611        }
612    }
613}