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)
8
pub 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)]
13
pub struct LogicalRect {
14
    pub origin: LogicalPosition,
15
    pub size: LogicalSize,
16
}
17

            
18
impl core::fmt::Debug for LogicalRect {
19
31710
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
20
31710
        write!(f, "{} @ {}", self.size, self.origin)
21
31710
    }
22
}
23

            
24
impl 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

            
30
impl LogicalRect {
31
89104
    pub const fn zero() -> Self {
32
89104
        Self::new(LogicalPosition::zero(), LogicalSize::zero())
33
89104
    }
34
242102
    pub const fn new(origin: LogicalPosition, size: LogicalSize) -> Self {
35
242102
        Self { origin, size }
36
242102
    }
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

            
112
impl_vec!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor, LogicalRectVecDestructorType, LogicalRectVecSlice, OptionLogicalRect);
113
impl_vec_clone!(LogicalRect, LogicalRectVec, LogicalRectVecDestructor);
114
impl_vec_debug!(LogicalRect, LogicalRectVec);
115
impl_vec_partialeq!(LogicalRect, LogicalRectVec);
116
impl_vec_partialord!(LogicalRect, LogicalRectVec);
117
impl_vec_ord!(LogicalRect, LogicalRectVec);
118
impl_vec_hash!(LogicalRect, LogicalRectVec);
119
impl_vec_eq!(LogicalRect, LogicalRectVec);
120

            
121
use core::{
122
    cmp::Ordering,
123
    hash::{Hash, Hasher},
124
    ops::{self, AddAssign, SubAssign},
125
};
126

            
127
use 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)]
132
pub struct LogicalPosition {
133
    pub x: f32,
134
    pub y: f32,
135
}
136

            
137
impl 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

            
145
impl 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

            
152
impl 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

            
159
impl core::fmt::Debug for LogicalPosition {
160
36204
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
161
36204
        write!(f, "({}, {})", self.x, self.y)
162
36204
    }
163
}
164

            
165
impl core::fmt::Display for LogicalPosition {
166
31710
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
167
31710
        write!(f, "({}, {})", self.x, self.y)
168
31710
    }
169
}
170

            
171
impl 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

            
183
impl 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.
197
const DECIMAL_MULTIPLIER: f32 = 1000.0;
198

            
199
impl_option!(
200
    LogicalPosition,
201
    OptionLogicalPosition,
202
    [Debug, Copy, Clone, PartialEq, PartialOrd]
203
);
204

            
205
impl 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

            
215
impl Eq for LogicalPosition {}
216

            
217
impl 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

            
229
impl LogicalPosition {
230
    /// Returns the main-axis component for the given writing mode.
231
4410
    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
232
4410
        match wm {
233
4410
            LayoutWritingMode::HorizontalTb => self.y,
234
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.x,
235
        }
236
4410
    }
237

            
238
    /// Returns the cross-axis component for the given writing mode.
239
3675
    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
240
3675
        match wm {
241
3675
            LayoutWritingMode::HorizontalTb => self.x,
242
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.y,
243
        }
244
3675
    }
245

            
246
    /// Creates a `LogicalPosition` from main and cross axis dimensions.
247
21665
    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
248
21665
        match wm {
249
21665
            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
250
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
251
        }
252
21665
    }
253
}
254

            
255
/// A 2D size in logical (DPI-independent) coordinates.
256
#[derive(Default, Copy, Clone, PartialEq, PartialOrd)]
257
#[repr(C)]
258
pub struct LogicalSize {
259
    pub width: f32,
260
    pub height: f32,
261
}
262

            
263
impl 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
172340
    pub fn from_main_cross(main: f32, cross: f32, wm: LayoutWritingMode) -> Self {
273
172340
        match wm {
274
172340
            LayoutWritingMode::HorizontalTb => Self::new(cross, main),
275
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self::new(main, cross),
276
        }
277
172340
    }
278
}
279

            
280
impl core::fmt::Debug for LogicalSize {
281
99036
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
282
99036
        write!(f, "{}x{}", self.width, self.height)
283
99036
    }
284
}
285

            
286
impl core::fmt::Display for LogicalSize {
287
31710
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
288
31710
        write!(f, "{}x{}", self.width, self.height)
289
31710
    }
290
}
291

            
292
impl_option!(
293
    LogicalSize,
294
    OptionLogicalSize,
295
    [Debug, Copy, Clone, PartialEq, PartialOrd]
296
);
297

            
298
impl_option!(
299
    LogicalRect,
300
    OptionLogicalRect,
301
    [Debug, Copy, Clone, PartialEq, PartialOrd]
302
);
303

            
304
impl 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

            
316
impl Eq for LogicalSize {}
317

            
318
impl 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

            
330
impl LogicalSize {
331
    /// Returns the main-axis dimension for the given writing mode.
332
182875
    pub fn main(&self, wm: LayoutWritingMode) -> f32 {
333
182875
        match wm {
334
182875
            LayoutWritingMode::HorizontalTb => self.height,
335
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.width,
336
        }
337
182875
    }
338

            
339
    /// Returns the cross-axis dimension for the given writing mode.
340
183400
    pub fn cross(&self, wm: LayoutWritingMode) -> f32 {
341
183400
        match wm {
342
183400
            LayoutWritingMode::HorizontalTb => self.width,
343
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => self.height,
344
        }
345
183400
    }
346

            
347
    /// Returns a new `LogicalSize` with the main-axis dimension updated.
348
21840
    pub fn with_main(self, wm: LayoutWritingMode, value: f32) -> Self {
349
21840
        match wm {
350
21840
            LayoutWritingMode::HorizontalTb => Self {
351
21840
                height: value,
352
21840
                ..self
353
21840
            },
354
            LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr => Self {
355
                width: value,
356
                ..self
357
            },
358
        }
359
21840
    }
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)]
379
pub struct PhysicalPosition<T> {
380
    pub x: T,
381
    pub y: T,
382
}
383

            
384
impl<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

            
390
pub type PhysicalPositionI32 = PhysicalPosition<i32>;
391
impl_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)]
400
pub struct PhysicalSize<T> {
401
    pub width: T,
402
    pub height: T,
403
}
404

            
405
impl<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

            
411
pub type PhysicalSizeU32 = PhysicalSize<u32>;
412
impl_option!(
413
    PhysicalSizeU32,
414
    OptionPhysicalSizeU32,
415
    [Debug, Copy, Clone, PartialEq, PartialOrd, Eq, Ord, Hash]
416
);
417
pub type PhysicalSizeF32 = PhysicalSize<f32>;
418
impl_option!(
419
    PhysicalSizeF32,
420
    OptionPhysicalSizeF32,
421
    [Debug, Copy, Clone, PartialEq, PartialOrd]
422
);
423

            
424
impl LogicalPosition {
425
    #[inline(always)]
426
378313
    pub const fn new(x: f32, y: f32) -> Self {
427
378313
        Self { x, y }
428
378313
    }
429
    #[inline(always)]
430
139000
    pub const fn zero() -> Self {
431
139000
        Self::new(0.0, 0.0)
432
139000
    }
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

            
443
impl<T> PhysicalPosition<T> {
444
    #[inline(always)]
445
    pub const fn new(x: T, y: T) -> Self {
446
        Self { x, y }
447
    }
448
}
449

            
450
impl 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

            
465
impl 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

            
480
impl LogicalSize {
481
    #[inline(always)]
482
444290
    pub const fn new(width: f32, height: f32) -> Self {
483
444290
        Self { width, height }
484
444290
    }
485
    #[inline(always)]
486
91414
    pub const fn zero() -> Self {
487
91414
        Self::new(0.0, 0.0)
488
91414
    }
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

            
499
impl<T> PhysicalSize<T> {
500
    #[inline(always)]
501
    pub const fn new(width: T, height: T) -> Self {
502
        Self { width, height }
503
    }
504
}
505

            
506
impl 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)]
532
pub 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)]
557
pub struct ScreenPosition {
558
    pub x: f32,
559
    pub y: f32,
560
}
561

            
562
impl 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

            
583
impl_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)]
592
pub struct CursorNodePosition {
593
    pub x: f32,
594
    pub y: f32,
595
}
596

            
597
impl 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

            
616
impl_option!(
617
    CursorNodePosition,
618
    OptionCursorNodePosition,
619
    [Debug, Copy, Clone, PartialEq, PartialOrd]
620
);