Skip to main content

tessera_ui/
px.rs

1//! Physical pixel geometry.
2//!
3//! ## Usage
4//!
5//! Use Px for measured dimensions and PxPosition for fractional placement.
6
7use std::ops::{AddAssign, Neg, SubAssign};
8
9use crate::{ComputedData, dp::Dp};
10
11/// A signed physical pixel coordinate, independent of display density.
12#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
13pub struct Px(pub i32);
14
15impl Px {
16    /// A constant representing zero pixels.
17    pub const ZERO: Self = Self(0);
18
19    /// A constant representing the maximum possible pixel value.
20    pub const MAX: Self = Self(i32::MAX);
21
22    /// Returns the raw i32 value.
23    pub fn raw(self) -> i32 {
24        self.0
25    }
26
27    /// Creates a new `Px` instance from an i32 value.
28    pub const fn new(value: i32) -> Self {
29        Px(value)
30    }
31
32    /// Scales density-independent units to whole physical pixels.
33    pub fn from_dp(dp: Dp) -> Self {
34        Px(dp.to_pixels_f64() as i32)
35    }
36
37    /// Converts to density-independent units using the active session's density.
38    pub fn to_dp(self) -> Dp {
39        Dp::from_pixels_f64(self.0 as f64)
40    }
41
42    /// Returns the absolute value as a u32
43    pub fn abs(self) -> u32 {
44        self.0.unsigned_abs()
45    }
46
47    /// Returns only the positive value, or zero if negative.
48    pub fn positive(self) -> u32 {
49        if self.0 < 0 { 0 } else { self.0 as u32 }
50    }
51
52    /// Returns the negative value, or zero if positive.
53    pub fn negative(self) -> i32 {
54        if self.0 > 0 { 0 } else { self.0 }
55    }
56
57    /// Converts the pixel value to f32.
58    pub fn to_f32(self) -> f32 {
59        self.0 as f32
60    }
61
62    /// Truncates a fractional pixel value toward zero, saturating at `i32` bounds.
63    pub fn from_f32(value: f32) -> Self {
64        Px(value as i32)
65    }
66
67    /// Truncates a fractional pixel value toward zero, saturating at `i32` bounds.
68    pub fn saturating_from_f32(value: f32) -> Self {
69        Self::from_f32(value)
70    }
71
72    /// Adds with saturation at `i32` bounds.
73    ///
74    /// ```
75    /// use tessera_ui::Px;
76    /// assert_eq!(Px(i32::MAX).saturating_add(Px(1)), Px(i32::MAX));
77    /// ```
78    pub fn saturating_add(self, rhs: Self) -> Self {
79        Px(self.0.saturating_add(rhs.0))
80    }
81
82    /// Subtracts with saturation at `i32` bounds.
83    pub fn saturating_sub(self, rhs: Self) -> Self {
84        Px(self.0.saturating_sub(rhs.0))
85    }
86
87    /// Multiplies by a scalar, truncating to whole physical pixels.
88    pub fn mul_f32(self, rhs: f32) -> Self {
89        Px((self.0 as f32 * rhs) as i32)
90    }
91
92    /// Divides by a scalar, truncating to whole physical pixels.
93    pub fn div_f32(self, rhs: f32) -> Self {
94        Px::from_f32(self.to_f32() / rhs)
95    }
96}
97
98/// A fractional physical-pixel position; x increases rightward and y downward.
99#[derive(Debug, Clone, Copy, PartialEq, Default)]
100pub struct PxPosition {
101    /// The x-coordinate in physical pixels. May be fractional.
102    pub x: f32,
103    /// The y-coordinate in physical pixels. May be fractional.
104    pub y: f32,
105}
106
107impl PxPosition {
108    /// A constant representing the zero position (0, 0).
109    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
110
111    /// Creates a new position from whole-pixel coordinates.
112    pub const fn new(x: Px, y: Px) -> Self {
113        Self {
114            x: x.0 as f32,
115            y: y.0 as f32,
116        }
117    }
118
119    /// Creates a position from fractional pixel coordinates.
120    pub const fn from_f32_xy(x: f32, y: f32) -> Self {
121        Self { x, y }
122    }
123
124    /// Returns the x-coordinate rounded to the nearest whole pixel.
125    pub fn x_px(self) -> Px {
126        Px(self.x.round() as i32)
127    }
128
129    /// Returns the y-coordinate rounded to the nearest whole pixel.
130    pub fn y_px(self) -> Px {
131        Px(self.y.round() as i32)
132    }
133
134    /// Offsets the position by the given whole-pixel deltas.
135    pub fn offset(self, dx: Px, dy: Px) -> Self {
136        Self {
137            x: self.x + dx.0 as f32,
138            y: self.y + dy.0 as f32,
139        }
140    }
141
142    /// Offsets the position by fractional pixel deltas.
143    pub fn offset_f32(self, dx: f32, dy: f32) -> Self {
144        Self {
145            x: self.x + dx,
146            y: self.y + dy,
147        }
148    }
149
150    /// Offsets the position with saturating arithmetic.
151    pub fn saturating_offset(self, dx: Px, dy: Px) -> Self {
152        const MIN: f32 = i32::MIN as f32;
153        const MAX: f32 = i32::MAX as f32;
154        Self {
155            x: (self.x + dx.0 as f32).clamp(MIN, MAX),
156            y: (self.y + dy.0 as f32).clamp(MIN, MAX),
157        }
158    }
159
160    /// Calculates the Euclidean distance to another position.
161    pub fn distance_to(self, other: Self) -> f32 {
162        let dx = self.x - other.x;
163        let dy = self.y - other.y;
164        (dx * dx + dy * dy).sqrt()
165    }
166
167    /// Converts the position to a 2D f32 array.
168    pub fn to_f32_arr2(self) -> [f32; 2] {
169        [self.x, self.y]
170    }
171
172    /// Converts the position to a 3D f32 array with z=0.
173    pub fn to_f32_arr3(self) -> [f32; 3] {
174        [self.x, self.y, 0.0]
175    }
176
177    /// Creates a position from a 2D f32 array.
178    pub fn from_f32_arr2(arr: [f32; 2]) -> Self {
179        Self {
180            x: arr[0],
181            y: arr[1],
182        }
183    }
184
185    /// Creates a position from a 3D f32 array, ignoring z.
186    pub fn from_f32_arr3(arr: [f32; 3]) -> Self {
187        Self {
188            x: arr[0],
189            y: arr[1],
190        }
191    }
192
193    /// Converts the position to a 2D f64 array.
194    pub fn to_f64_arr2(self) -> [f64; 2] {
195        [self.x as f64, self.y as f64]
196    }
197
198    /// Converts the position to a 3D f64 array with z=0.
199    pub fn to_f64_arr3(self) -> [f64; 3] {
200        [self.x as f64, self.y as f64, 0.0]
201    }
202
203    /// Creates a position from a 2D f64 array.
204    pub fn from_f64_arr2(arr: [f64; 2]) -> Self {
205        Self {
206            x: arr[0] as f32,
207            y: arr[1] as f32,
208        }
209    }
210
211    /// Creates a position from a 3D f64 array, ignoring the z component.
212    pub fn from_f64_arr3(arr: [f64; 3]) -> Self {
213        Self {
214            x: arr[0] as f32,
215            y: arr[1] as f32,
216        }
217    }
218}
219
220/// A width and height in whole physical pixels.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
222pub struct PxSize {
223    /// The width in physical pixels
224    pub width: Px,
225    /// The height in physical pixels
226    pub height: Px,
227}
228
229impl PxSize {
230    /// A constant representing zero size (0×0).
231    pub const ZERO: Self = Self {
232        width: Px(0),
233        height: Px(0),
234    };
235
236    /// Creates a new size from width and height.
237    pub const fn new(width: Px, height: Px) -> Self {
238        Self { width, height }
239    }
240
241    /// Converts the size to a 2D f32 array.
242    pub fn to_f32_arr2(self) -> [f32; 2] {
243        [self.width.0 as f32, self.height.0 as f32]
244    }
245}
246
247/// A 2D rectangle in physical pixel space.
248#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
249pub struct PxRect {
250    /// The x-coordinate of the top-left corner
251    pub x: Px,
252    /// The y-coordinate of the top-left corner
253    pub y: Px,
254    /// The width of the rectangle
255    pub width: Px,
256    /// The height of the rectangle
257    pub height: Px,
258}
259
260impl PxRect {
261    /// A constant representing a zero rectangle (0×0 at position (0, 0)).
262    pub const ZERO: Self = Self {
263        x: Px::ZERO,
264        y: Px::ZERO,
265        width: Px::ZERO,
266        height: Px::ZERO,
267    };
268
269    /// Creates a new rectangle from position and size.
270    pub const fn new(x: Px, y: Px, width: Px, height: Px) -> Self {
271        Self {
272            x,
273            y,
274            width,
275            height,
276        }
277    }
278
279    /// Creates a new rectangle from a position and size.
280    pub fn from_position_size(position: PxPosition, size: PxSize) -> Self {
281        Self {
282            x: position.x_px(),
283            y: position.y_px(),
284            width: size.width,
285            height: size.height,
286        }
287    }
288
289    pub(crate) fn enclosing(position: PxPosition, size: PxSize) -> Self {
290        Self::enclosing_f32(position, size.to_f32_arr2())
291    }
292
293    pub(crate) fn enclosing_f32(position: PxPosition, size: [f32; 2]) -> Self {
294        let x = position.x.floor() as i32;
295        let y = position.y.floor() as i32;
296        Self::new(
297            Px(x),
298            Px(y),
299            Px(((position.x + size[0]).ceil() as i32).saturating_sub(x)),
300            Px(((position.y + size[1]).ceil() as i32).saturating_sub(y)),
301        )
302    }
303
304    /// Returns whether the rectangles do not overlap; touching edges do not overlap.
305    pub fn is_orthogonal(&self, other: &Self) -> bool {
306        let x_overlap = self.x.0 < other.x.0 + other.width.0 && other.x.0 < self.x.0 + self.width.0;
307
308        let y_overlap =
309            self.y.0 < other.y.0 + other.height.0 && other.y.0 < self.y.0 + self.height.0;
310
311        !x_overlap || !y_overlap
312    }
313
314    /// Returns the smallest rectangle containing both rectangles.
315    ///
316    /// # Examples
317    ///
318    /// ```
319    /// use tessera_ui::px::{Px, PxRect};
320    ///
321    /// let rect1 = PxRect::new(Px::new(0), Px::new(0), Px::new(100), Px::new(100));
322    /// let rect2 = PxRect::new(Px::new(50), Px::new(50), Px::new(100), Px::new(100));
323    /// let union_rect = rect1.union(&rect2);
324    /// assert_eq!(
325    ///     union_rect,
326    ///     PxRect::new(Px::new(0), Px::new(0), Px::new(150), Px::new(150))
327    /// );
328    /// ```
329    pub fn union(&self, other: &Self) -> Self {
330        let x = self.x.0.min(other.x.0);
331        let y = self.y.0.min(other.y.0);
332        let width = (self.x.0 + self.width.0).max(other.x.0 + other.width.0) - x;
333        let height = (self.y.0 + self.height.0).max(other.y.0 + other.height.0) - y;
334
335        Self {
336            x: Px(x),
337            y: Px(y),
338            width: Px(width),
339            height: Px(height),
340        }
341    }
342
343    /// Returns the area, treating negative width or height as zero.
344    pub fn area(&self) -> u32 {
345        let width = self.width.0.max(0) as u32;
346        let height = self.height.0.max(0) as u32;
347        width * height
348    }
349
350    /// Returns the overlapping rectangle, or None when edges only touch.
351    ///
352    /// # Examples
353    ///
354    /// ```
355    /// use tessera_ui::px::{Px, PxRect};
356    ///
357    /// let rect1 = PxRect::new(Px::new(0), Px::new(0), Px::new(100), Px::new(100));
358    /// let rect2 = PxRect::new(Px::new(50), Px::new(50), Px::new(100), Px::new(100));
359    /// let intersection = rect1.intersection(&rect2);
360    /// assert_eq!(
361    ///     intersection,
362    ///     Some(PxRect::new(
363    ///         Px::new(50),
364    ///         Px::new(50),
365    ///         Px::new(50),
366    ///         Px::new(50)
367    ///     ))
368    /// );
369    /// ```
370    pub fn intersection(&self, other: &Self) -> Option<Self> {
371        let x1 = self.x.0.max(other.x.0);
372        let y1 = self.y.0.max(other.y.0);
373        let x2 = (self.x.0 + self.width.0).min(other.x.0 + other.width.0);
374        let y2 = (self.y.0 + self.height.0).min(other.y.0 + other.height.0);
375
376        if x1 < x2 && y1 < y2 {
377            Some(Self {
378                x: Px(x1),
379                y: Px(y1),
380                width: Px(x2 - x1),
381                height: Px(y2 - y1),
382            })
383        } else {
384            None
385        }
386    }
387
388    /// Includes the top and left edges and excludes the bottom and right edges.
389    pub fn contains(&self, point: PxPosition) -> bool {
390        point.x >= self.x.0 as f32
391            && point.x < (self.x.0 + self.width.0) as f32
392            && point.y >= self.y.0 as f32
393            && point.y < (self.y.0 + self.height.0) as f32
394    }
395}
396
397impl From<[Px; 2]> for PxSize {
398    fn from(size: [Px; 2]) -> Self {
399        Self {
400            width: size[0],
401            height: size[1],
402        }
403    }
404}
405
406impl From<PxSize> for winit::dpi::PhysicalSize<i32> {
407    fn from(size: PxSize) -> Self {
408        winit::dpi::PhysicalSize {
409            width: size.width.raw(),
410            height: size.height.raw(),
411        }
412    }
413}
414
415impl From<winit::dpi::PhysicalSize<u32>> for PxSize {
416    fn from(size: winit::dpi::PhysicalSize<u32>) -> Self {
417        Self {
418            width: Px(size.width as i32),
419            height: Px(size.height as i32),
420        }
421    }
422}
423
424impl From<ComputedData> for PxSize {
425    fn from(data: ComputedData) -> Self {
426        Self {
427            width: data.width,
428            height: data.height,
429        }
430    }
431}
432
433impl From<PxSize> for winit::dpi::Size {
434    fn from(size: PxSize) -> Self {
435        winit::dpi::PhysicalSize::from(size).into()
436    }
437}
438
439impl std::ops::Add for Px {
440    type Output = Self;
441
442    fn add(self, rhs: Self) -> Self::Output {
443        Px(self.0 + rhs.0)
444    }
445}
446
447impl Neg for Px {
448    type Output = Self;
449
450    fn neg(self) -> Self::Output {
451        Px::new(-self.0)
452    }
453}
454
455impl std::ops::Sub for Px {
456    type Output = Self;
457
458    fn sub(self, rhs: Self) -> Self::Output {
459        Px(self.0 - rhs.0)
460    }
461}
462
463impl std::ops::Mul for Px {
464    type Output = Self;
465
466    fn mul(self, rhs: Self) -> Self::Output {
467        Px(self.0 * rhs.0)
468    }
469}
470
471impl std::ops::Div for Px {
472    type Output = Self;
473
474    fn div(self, rhs: Self) -> Self::Output {
475        Px(self.0 / rhs.0)
476    }
477}
478
479impl std::ops::Mul<i32> for Px {
480    type Output = Self;
481
482    fn mul(self, rhs: i32) -> Self::Output {
483        Px(self.0 * rhs)
484    }
485}
486
487impl std::ops::Div<i32> for Px {
488    type Output = Self;
489
490    fn div(self, rhs: i32) -> Self::Output {
491        Px(self.0 / rhs)
492    }
493}
494
495impl From<i32> for Px {
496    fn from(value: i32) -> Self {
497        Px(value)
498    }
499}
500
501impl From<u32> for Px {
502    fn from(value: u32) -> Self {
503        Px(value as i32)
504    }
505}
506
507impl From<Dp> for Px {
508    fn from(dp: Dp) -> Self {
509        Px::from_dp(dp)
510    }
511}
512
513impl From<PxPosition> for winit::dpi::PhysicalPosition<i32> {
514    fn from(pos: PxPosition) -> Self {
515        winit::dpi::PhysicalPosition {
516            x: pos.x_px().0,
517            y: pos.y_px().0,
518        }
519    }
520}
521
522impl From<PxPosition> for winit::dpi::Position {
523    fn from(pos: PxPosition) -> Self {
524        winit::dpi::PhysicalPosition::from(pos).into()
525    }
526}
527
528impl AddAssign for Px {
529    fn add_assign(&mut self, rhs: Self) {
530        self.0 += rhs.0;
531    }
532}
533
534impl SubAssign for Px {
535    fn sub_assign(&mut self, rhs: Self) {
536        self.0 -= rhs.0;
537    }
538}
539
540impl std::ops::Add for PxPosition {
541    type Output = Self;
542
543    fn add(self, rhs: Self) -> Self::Output {
544        PxPosition {
545            x: self.x + rhs.x,
546            y: self.y + rhs.y,
547        }
548    }
549}
550
551impl std::ops::Sub for PxPosition {
552    type Output = Self;
553
554    fn sub(self, rhs: Self) -> Self::Output {
555        PxPosition {
556            x: self.x - rhs.x,
557            y: self.y - rhs.y,
558        }
559    }
560}
561
562impl From<[i32; 2]> for PxPosition {
563    fn from(pos: [i32; 2]) -> Self {
564        PxPosition::from_f32_xy(pos[0] as f32, pos[1] as f32)
565    }
566}
567
568impl From<PxPosition> for [i32; 2] {
569    fn from(pos: PxPosition) -> Self {
570        [pos.x_px().0, pos.y_px().0]
571    }
572}
573
574impl From<[u32; 2]> for PxPosition {
575    fn from(pos: [u32; 2]) -> Self {
576        PxPosition::from_f32_xy(pos[0] as f32, pos[1] as f32)
577    }
578}
579
580impl From<PxPosition> for [u32; 2] {
581    fn from(pos: PxPosition) -> Self {
582        [pos.x_px().positive(), pos.y_px().positive()]
583    }
584}
585
586impl From<[Px; 2]> for PxPosition {
587    fn from(pos: [Px; 2]) -> Self {
588        PxPosition::from_f32_xy(pos[0].0 as f32, pos[1].0 as f32)
589    }
590}
591
592impl From<PxPosition> for [Px; 2] {
593    fn from(pos: PxPosition) -> Self {
594        [pos.x_px(), pos.y_px()]
595    }
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601
602    #[test]
603    fn test_saturating_from_f32() {
604        assert_eq!(Px::saturating_from_f32(f32::MAX), Px(i32::MAX));
605        assert_eq!(Px::saturating_from_f32(f32::MIN), Px(i32::MIN));
606        assert_eq!(Px::saturating_from_f32(100.5), Px(100));
607        assert_eq!(Px::saturating_from_f32(-100.5), Px(-100));
608    }
609}