tessera_ui/px.rs
1//! Physical pixel coordinate system for Tessera UI framework.
2//!
3//! This module provides types and operations for working with physical pixel coordinates,
4//! positions, and sizes. Physical pixels represent actual screen pixels and are used
5//! internally by the rendering system.
6//!
7//! # Key Types
8//!
9//! - [`Px`] - A single physical pixel coordinate value that supports negative values for scrolling
10//! - [`PxPosition`] - A 2D position in physical pixel space (x, y coordinates)
11//! - [`PxSize`] - A 2D size in physical pixel space (width, height dimensions)
12//!
13//! # Coordinate System
14//!
15//! The coordinate system uses:
16//! - Origin (0, 0) at the top-left corner
17//! - X-axis increases to the right
18//! - Y-axis increases downward
19//! - Negative coordinates are supported for scrolling and off-screen positioning
20//!
21//! # Conversion
22//!
23//! Physical pixels can be converted to and from density-independent pixels ([`Dp`]):
24//! - Use [`Px::from_dp`] to convert from Dp to Px
25//! - Use [`Px::to_dp`] to convert from Px to Dp
26//!
27//! # Example
28//!
29//! ```
30//! use tessera_ui::px::{Px, PxPosition, PxSize};
31//! use tessera_ui::dp::Dp;
32//!
33//! // Create pixel values
34//! let x = Px::new(100);
35//! let y = Px::new(200);
36//!
37//! // Create a position
38//! let position = PxPosition::new(x, y);
39//!
40//! // Create a size
41//! let size = PxSize::new(Px::new(300), Px::new(400));
42//!
43//! // Arithmetic operations
44//! let offset_position = position.offset(Px::new(10), Px::new(-5));
45//!
46//! // Convert between Dp and Px
47//! let dp_value = Dp(16.0);
48//! let px_value = Px::from_dp(dp_value);
49//! ```
50
51use std::ops::{AddAssign, Neg};
52
53use crate::dp::{Dp, SCALE_FACTOR};
54
55/// A physical pixel coordinate value.
56///
57/// This type represents a single coordinate value in physical pixel space. Physical pixels
58/// correspond directly to screen pixels and are used internally by the rendering system.
59/// Unlike density-independent pixels ([`Dp`]), physical pixels are not scaled based on
60/// screen density.
61///
62/// # Features
63///
64/// - Supports negative values for scrolling and off-screen positioning
65/// - Provides arithmetic operations (addition, subtraction, multiplication, division)
66/// - Includes saturating arithmetic to prevent overflow
67/// - Converts to/from density-independent pixels ([`Dp`])
68/// - Converts to/from floating-point values with overflow protection
69///
70/// # Examples
71///
72/// ```
73/// use tessera_ui::px::Px;
74///
75/// // Create pixel values
76/// let px1 = Px::new(100);
77/// let px2 = Px::new(-50); // Negative values supported
78///
79/// // Arithmetic operations
80/// let sum = px1 + px2; // Px(50)
81/// let doubled = px1 * 2; // Px(200)
82///
83/// // Saturating arithmetic prevents overflow
84/// let max_px = Px::new(i32::MAX);
85/// let safe_add = max_px.saturating_add(Px::new(1)); // Still Px(i32::MAX)
86///
87/// // Convert to absolute value for rendering
88/// let abs_value = Px::new(-10).abs(); // 0 (negative becomes 0)
89/// ```
90#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)]
91pub struct Px(pub i32);
92
93impl Px {
94 /// A constant representing zero pixels.
95 pub const ZERO: Self = Self(0);
96
97 /// A constant representing the maximum possible pixel value.
98 pub const MAX: Self = Self(i32::MAX);
99
100 /// Returns the raw i32 value.
101 ///
102 /// This provides direct access to the underlying integer value.
103 ///
104 /// # Examples
105 ///
106 /// ```
107 /// use tessera_ui::px::Px;
108 ///
109 /// let px = Px::new(42);
110 /// assert_eq!(px.raw(), 42);
111 /// ```
112 pub fn raw(self) -> i32 {
113 self.0
114 }
115
116 /// Creates a new `Px` instance from an i32 value.
117 ///
118 /// # Arguments
119 ///
120 /// * `value` - The pixel value as an i32. Negative values are allowed.
121 ///
122 /// # Examples
123 ///
124 /// ```
125 /// use tessera_ui::px::Px;
126 ///
127 /// let positive = Px::new(100);
128 /// let negative = Px::new(-50);
129 /// let zero = Px::new(0);
130 /// ```
131 pub const fn new(value: i32) -> Self {
132 Px(value)
133 }
134
135 /// Converts from density-independent pixels ([`Dp`]) to physical pixels.
136 ///
137 /// This conversion uses the current scale factor to determine how many physical
138 /// pixels correspond to the given Dp value. The scale factor is typically
139 /// determined by the screen's pixel density.
140 ///
141 /// # Arguments
142 ///
143 /// * `dp` - The density-independent pixel value to convert
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use tessera_ui::px::Px;
149 /// use tessera_ui::dp::Dp;
150 ///
151 /// let dp_value = Dp(16.0);
152 /// let px_value = Px::from_dp(dp_value);
153 /// ```
154 pub fn from_dp(dp: Dp) -> Self {
155 Px(dp.to_pixels_f64() as i32)
156 }
157
158 /// Converts from physical pixels to density-independent pixels ([`Dp`]).
159 ///
160 /// This conversion uses the current scale factor to determine the Dp value
161 /// that corresponds to this physical pixel value.
162 ///
163 /// # Examples
164 ///
165 /// ```
166 /// use tessera_ui::px::Px;
167 ///
168 /// let px_value = Px::new(32);
169 /// let dp_value = px_value.to_dp();
170 /// ```
171 pub fn to_dp(self) -> Dp {
172 let scale_factor = SCALE_FACTOR.get().map(|lock| *lock.read()).unwrap_or(1.0);
173 Dp((self.0 as f64) / scale_factor)
174 }
175
176 /// Returns the absolute value as a u32
177 ///
178 /// This method is primarily used for coordinate conversion during rendering,
179 /// where negative coordinates need to be handled appropriately.
180 ///
181 /// # Examples
182 ///
183 /// ```
184 /// use tessera_ui::px::Px;
185 ///
186 /// assert_eq!(Px::new(10).abs(), 10);
187 /// assert_eq!(Px::new(-5).abs(), 5);
188 /// assert_eq!(Px::new(0).abs(), 0);
189 /// ```
190 pub fn abs(self) -> u32 {
191 self.0.unsigned_abs()
192 }
193
194 /// Returns only the positive value, or zero if negative.
195 ///
196 /// This is useful for ensuring that pixel values are always non-negative,
197 /// especially when dealing with rendering or layout calculations.
198 ///
199 /// # Examples
200 ///
201 /// ```
202 /// use tessera_ui::px::Px;
203 ///
204 /// assert_eq!(Px::new(10).positive(), 10);
205 /// assert_eq!(Px::new(-5).positive(), 0);
206 /// assert_eq!(Px::new(0).positive(), 0);
207 /// ```
208 pub fn positive(self) -> u32 {
209 if self.0 < 0 { 0 } else { self.0 as u32 }
210 }
211
212 /// Returns the negative value, or zero if positive.
213 ///
214 /// This is useful for ensuring that pixel values are always non-positive,
215 /// especially when dealing with rendering or layout calculations.
216 ///
217 /// # Examples
218 ///
219 /// ```
220 /// use tessera_ui::px::Px;
221 ///
222 /// assert_eq!(Px::new(10).negative(), 0);
223 /// assert_eq!(Px::new(-5).negative(), -5);
224 /// assert_eq!(Px::new(0).negative(), 0);
225 pub fn negative(self) -> i32 {
226 if self.0 > 0 { 0 } else { self.0 }
227 }
228
229 /// Converts the pixel value to f32.
230 ///
231 /// # Examples
232 ///
233 /// ```
234 /// use tessera_ui::px::Px;
235 ///
236 /// let px = Px::new(42);
237 /// assert_eq!(px.to_f32(), 42.0);
238 /// ```
239 pub fn to_f32(self) -> f32 {
240 self.0 as f32
241 }
242
243 /// Creates a `Px` from an f32 value.
244 ///
245 /// # Panics
246 ///
247 /// This function may panic on overflow in debug builds when the f32 value
248 /// cannot be represented as an i32.
249 ///
250 /// # Examples
251 ///
252 /// ```
253 /// use tessera_ui::px::Px;
254 ///
255 /// let px = Px::from_f32(42.7);
256 /// assert_eq!(px.raw(), 42);
257 /// ```
258 pub fn from_f32(value: f32) -> Self {
259 Px(value as i32)
260 }
261
262 /// Creates a `Px` from an f32 value, saturating at the numeric bounds instead of overflowing.
263 ///
264 /// This is the safe alternative to [`from_f32`](Self::from_f32) that handles overflow
265 /// by clamping the value to the valid i32 range.
266 ///
267 /// # Examples
268 ///
269 /// ```
270 /// use tessera_ui::px::Px;
271 ///
272 /// let normal = Px::saturating_from_f32(42.7);
273 /// assert_eq!(normal.raw(), 42);
274 ///
275 /// let max_val = Px::saturating_from_f32(f32::MAX);
276 /// assert_eq!(max_val.raw(), i32::MAX);
277 ///
278 /// let min_val = Px::saturating_from_f32(f32::MIN);
279 /// assert_eq!(min_val.raw(), i32::MIN);
280 /// ```
281 pub fn saturating_from_f32(value: f32) -> Self {
282 let clamped_value = value.clamp(i32::MIN as f32, i32::MAX as f32);
283 Px(clamped_value as i32)
284 }
285
286 /// Saturating integer addition.
287 ///
288 /// Computes `self + rhs`, saturating at the numeric bounds instead of overflowing.
289 /// This prevents integer overflow by clamping the result to the valid i32 range.
290 ///
291 /// # Arguments
292 ///
293 /// * `rhs` - The right-hand side value to add
294 ///
295 /// # Examples
296 ///
297 /// ```
298 /// use tessera_ui::px::Px;
299 ///
300 /// let a = Px::new(10);
301 /// let b = Px::new(5);
302 /// assert_eq!(a.saturating_add(b), Px::new(15));
303 ///
304 /// // Prevents overflow
305 /// let max = Px::new(i32::MAX);
306 /// assert_eq!(max.saturating_add(Px::new(1)), max);
307 /// ```
308 pub fn saturating_add(self, rhs: Self) -> Self {
309 Px(self.0.saturating_add(rhs.0))
310 }
311
312 /// Saturating integer subtraction.
313 ///
314 /// Computes `self - rhs`, saturating at the numeric bounds instead of overflowing.
315 /// This prevents integer underflow by clamping the result to the valid i32 range.
316 ///
317 /// # Arguments
318 ///
319 /// * `rhs` - The right-hand side value to subtract
320 ///
321 /// # Examples
322 ///
323 /// ```
324 /// use tessera_ui::px::Px;
325 ///
326 /// let a = Px::new(10);
327 /// let b = Px::new(5);
328 /// assert_eq!(a.saturating_sub(b), Px::new(5));
329 ///
330 /// // Prevents underflow
331 /// let min = Px::new(i32::MIN);
332 /// assert_eq!(min.saturating_sub(Px::new(1)), min);
333 /// ```
334 pub fn saturating_sub(self, rhs: Self) -> Self {
335 Px(self.0.saturating_sub(rhs.0))
336 }
337
338 /// Multiplies the pixel value by a scalar f32.
339 ///
340 /// # Arguments
341 ///
342 /// * `rhs` - The scalar value to multiply by
343 ///
344 /// # Returns
345 ///
346 /// A new `Px` instance with the result of the multiplication.
347 ///
348 /// # Examples
349 ///
350 /// ```
351 /// use tessera_ui::px::Px;
352 ///
353 /// let px = Px::new(10);
354 /// let result = px.mul_f32(2.0);
355 /// assert_eq!(result, Px::new(20));
356 /// ```
357 pub fn mul_f32(self, rhs: f32) -> Self {
358 Px((self.0 as f32 * rhs) as i32)
359 }
360
361 /// Divides the pixel value by a scalar f32.
362 ///
363 /// # Arguments
364 ///
365 /// * `rhs` - The scalar value to divide by
366 ///
367 /// # Returns
368 ///
369 /// A new `Px` instance with the result of the division.
370 ///
371 /// # Panics
372 ///
373 /// This function may panic if `rhs` is zero, as division by zero is undefined.
374 ///
375 /// # Examples
376 ///
377 /// ```
378 /// use tessera_ui::px::Px;
379 ///
380 /// let px = Px::new(20);
381 /// let result = px.div_f32(2.0);
382 /// assert_eq!(result, Px::new(10));
383 /// ```
384 pub fn div_f32(self, rhs: f32) -> Self {
385 Px::from_f32(self.to_f32() / rhs)
386 }
387}
388
389/// A 2D position in physical pixel space.
390///
391/// This type represents a position with x and y coordinates in physical pixel space.
392/// Physical pixels correspond directly to screen pixels and are used internally
393/// by the rendering system.
394///
395/// # Coordinate System
396///
397/// - Origin (0, 0) is at the top-left corner
398/// - X-axis increases to the right
399/// - Y-axis increases downward
400/// - Negative coordinates are supported for scrolling and off-screen positioning
401///
402/// # Examples
403///
404/// ```
405/// use tessera_ui::px::{Px, PxPosition};
406///
407/// // Create a position
408/// let position = PxPosition::new(Px::new(100), Px::new(200));
409///
410/// // Offset the position
411/// let offset_position = position.offset(Px::new(10), Px::new(-5));
412///
413/// // Calculate distance between positions
414/// let other_position = PxPosition::new(Px::new(103), Px::new(196));
415/// let distance = position.distance_to(other_position);
416///
417/// // Arithmetic operations
418/// let sum = position + other_position;
419/// let diff = position - other_position;
420/// ```
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
422pub struct PxPosition {
423 /// The x-coordinate in physical pixels
424 pub x: Px,
425 /// The y-coordinate in physical pixels
426 pub y: Px,
427}
428
429impl PxPosition {
430 /// A constant representing the zero position (0, 0).
431 pub const ZERO: Self = Self { x: Px(0), y: Px(0) };
432
433 /// Creates a new position from x and y coordinates.
434 ///
435 /// # Arguments
436 ///
437 /// * `x` - The x-coordinate in physical pixels
438 /// * `y` - The y-coordinate in physical pixels
439 ///
440 /// # Examples
441 ///
442 /// ```
443 /// use tessera_ui::px::{Px, PxPosition};
444 ///
445 /// let position = PxPosition::new(Px::new(100), Px::new(200));
446 /// assert_eq!(position.x, Px::new(100));
447 /// assert_eq!(position.y, Px::new(200));
448 /// ```
449 pub const fn new(x: Px, y: Px) -> Self {
450 Self { x, y }
451 }
452
453 /// Offsets the position by the given deltas.
454 ///
455 /// # Panics
456 ///
457 /// This function may panic on overflow in debug builds.
458 ///
459 /// # Arguments
460 ///
461 /// * `dx` - The x-axis offset in physical pixels
462 /// * `dy` - The y-axis offset in physical pixels
463 ///
464 /// # Examples
465 ///
466 /// ```
467 /// use tessera_ui::px::{Px, PxPosition};
468 ///
469 /// let position = PxPosition::new(Px::new(10), Px::new(20));
470 /// let offset_position = position.offset(Px::new(5), Px::new(-3));
471 /// assert_eq!(offset_position, PxPosition::new(Px::new(15), Px::new(17)));
472 /// ```
473 pub fn offset(self, dx: Px, dy: Px) -> Self {
474 Self {
475 x: self.x + dx,
476 y: self.y + dy,
477 }
478 }
479
480 /// Offsets the position with saturating arithmetic.
481 ///
482 /// This prevents overflow by clamping the result to the valid coordinate range.
483 ///
484 /// # Arguments
485 ///
486 /// * `dx` - The x-axis offset in physical pixels
487 /// * `dy` - The y-axis offset in physical pixels
488 ///
489 /// # Examples
490 ///
491 /// ```
492 /// use tessera_ui::px::{Px, PxPosition};
493 ///
494 /// let position = PxPosition::new(Px::new(10), Px::new(20));
495 /// let offset_position = position.saturating_offset(Px::new(5), Px::new(-3));
496 /// assert_eq!(offset_position, PxPosition::new(Px::new(15), Px::new(17)));
497 ///
498 /// // Prevents overflow
499 /// let max_position = PxPosition::new(Px::new(i32::MAX), Px::new(i32::MAX));
500 /// let safe_offset = max_position.saturating_offset(Px::new(1), Px::new(1));
501 /// assert_eq!(safe_offset, max_position);
502 /// ```
503 pub fn saturating_offset(self, dx: Px, dy: Px) -> Self {
504 Self {
505 x: self.x.saturating_add(dx),
506 y: self.y.saturating_add(dy),
507 }
508 }
509
510 /// Calculates the Euclidean distance to another position.
511 ///
512 /// # Arguments
513 ///
514 /// * `other` - The other position to calculate distance to
515 ///
516 /// # Returns
517 ///
518 /// The distance as a floating-point value
519 ///
520 /// # Examples
521 ///
522 /// ```
523 /// use tessera_ui::px::{Px, PxPosition};
524 ///
525 /// let pos1 = PxPosition::new(Px::new(0), Px::new(0));
526 /// let pos2 = PxPosition::new(Px::new(3), Px::new(4));
527 /// assert_eq!(pos1.distance_to(pos2), 5.0);
528 /// ```
529 pub fn distance_to(self, other: Self) -> f32 {
530 let dx = (self.x.0 - other.x.0) as f32;
531 let dy = (self.y.0 - other.y.0) as f32;
532 (dx * dx + dy * dy).sqrt()
533 }
534
535 /// Converts the position to a 2D f32 array.
536 ///
537 /// # Returns
538 ///
539 /// An array `[x, y]` where both coordinates are converted to f32
540 ///
541 /// # Examples
542 ///
543 /// ```
544 /// use tessera_ui::px::{Px, PxPosition};
545 ///
546 /// let position = PxPosition::new(Px::new(10), Px::new(20));
547 /// assert_eq!(position.to_f32_arr2(), [10.0, 20.0]);
548 /// ```
549 pub fn to_f32_arr2(self) -> [f32; 2] {
550 [self.x.0 as f32, self.y.0 as f32]
551 }
552
553 /// Converts the position to a 3D f32 array with z=0.
554 ///
555 /// # Returns
556 ///
557 /// An array `[x, y, 0.0]` where x and y are converted to f32 and z is 0.0
558 ///
559 /// # Examples
560 ///
561 /// ```
562 /// use tessera_ui::px::{Px, PxPosition};
563 ///
564 /// let position = PxPosition::new(Px::new(10), Px::new(20));
565 /// assert_eq!(position.to_f32_arr3(), [10.0, 20.0, 0.0]);
566 /// ```
567 pub fn to_f32_arr3(self) -> [f32; 3] {
568 [self.x.0 as f32, self.y.0 as f32, 0.0]
569 }
570
571 /// Creates a position from a 2D f32 array.
572 ///
573 /// # Arguments
574 ///
575 /// * `arr` - An array `[x, y]` where both values will be converted to i32
576 ///
577 /// # Examples
578 ///
579 /// ```
580 /// use tessera_ui::px::{Px, PxPosition};
581 ///
582 /// let position = PxPosition::from_f32_arr2([10.5, 20.7]);
583 /// assert_eq!(position, PxPosition::new(Px::new(10), Px::new(20)));
584 /// ```
585 pub fn from_f32_arr2(arr: [f32; 2]) -> Self {
586 Self {
587 x: Px::new(arr[0] as i32),
588 y: Px::new(arr[1] as i32),
589 }
590 }
591
592 /// Creates a position from a 3D f32 array, ignoring the z component.
593 ///
594 /// # Arguments
595 ///
596 /// * `arr` - An array `[x, y, z]` where only x and y are used
597 ///
598 /// # Examples
599 ///
600 /// ```
601 /// use tessera_ui::px::{Px, PxPosition};
602 ///
603 /// let position = PxPosition::from_f32_arr3([10.5, 20.7, 30.9]);
604 /// assert_eq!(position, PxPosition::new(Px::new(10), Px::new(20)));
605 /// ```
606 pub fn from_f32_arr3(arr: [f32; 3]) -> Self {
607 Self {
608 x: Px::new(arr[0] as i32),
609 y: Px::new(arr[1] as i32),
610 }
611 }
612
613 /// Converts the position to a 2D f64 array.
614 ///
615 /// # Returns
616 ///
617 /// An array `[x, y]` where both coordinates are converted to f64
618 ///
619 /// # Examples
620 ///
621 /// ```
622 /// use tessera_ui::px::{Px, PxPosition};
623 ///
624 /// let position = PxPosition::new(Px::new(10), Px::new(20));
625 /// assert_eq!(position.to_f64_arr2(), [10.0, 20.0]);
626 /// ```
627 pub fn to_f64_arr2(self) -> [f64; 2] {
628 [self.x.0 as f64, self.y.0 as f64]
629 }
630
631 /// Converts the position to a 3D f64 array with z=0.
632 ///
633 /// # Returns
634 ///
635 /// An array `[x, y, 0.0]` where x and y are converted to f64 and z is 0.0
636 ///
637 /// # Examples
638 ///
639 /// ```
640 /// use tessera_ui::px::{Px, PxPosition};
641 ///
642 /// let position = PxPosition::new(Px::new(10), Px::new(20));
643 /// assert_eq!(position.to_f64_arr3(), [10.0, 20.0, 0.0]);
644 /// ```
645 pub fn to_f64_arr3(self) -> [f64; 3] {
646 [self.x.0 as f64, self.y.0 as f64, 0.0]
647 }
648
649 /// Creates a position from a 2D f64 array.
650 ///
651 /// # Arguments
652 ///
653 /// * `arr` - An array `[x, y]` where both values will be converted to i32
654 ///
655 /// # Examples
656 ///
657 /// ```
658 /// use tessera_ui::px::{Px, PxPosition};
659 ///
660 /// let position = PxPosition::from_f64_arr2([10.5, 20.7]);
661 /// assert_eq!(position, PxPosition::new(Px::new(10), Px::new(20)));
662 /// ```
663 pub fn from_f64_arr2(arr: [f64; 2]) -> Self {
664 Self {
665 x: Px::new(arr[0] as i32),
666 y: Px::new(arr[1] as i32),
667 }
668 }
669
670 /// Creates a position from a 3D f64 array, ignoring the z component.
671 ///
672 /// # Arguments
673 ///
674 /// * `arr` - An array `[x, y, z]` where only x and y are used
675 ///
676 /// # Examples
677 ///
678 /// ```
679 /// use tessera_ui::px::{Px, PxPosition};
680 ///
681 /// let position = PxPosition::from_f64_arr3([10.5, 20.7, 30.9]);
682 /// assert_eq!(position, PxPosition::new(Px::new(10), Px::new(20)));
683 /// ```
684 pub fn from_f64_arr3(arr: [f64; 3]) -> Self {
685 Self {
686 x: Px::new(arr[0] as i32),
687 y: Px::new(arr[1] as i32),
688 }
689 }
690}
691
692/// A 2D size in physical pixel space.
693///
694/// This type represents dimensions (width and height) in physical pixel space.
695/// Physical pixels correspond directly to screen pixels and are used internally
696/// by the rendering system.
697///
698/// # Examples
699///
700/// ```
701/// use tessera_ui::px::{Px, PxSize};
702///
703/// // Create a size
704/// let size = PxSize::new(Px::new(300), Px::new(200));
705///
706/// // Convert to array formats for graphics APIs
707/// let f32_array = size.to_f32_arr2();
708/// assert_eq!(f32_array, [300.0, 200.0]);
709///
710/// // Create from array
711/// let from_array = PxSize::from([Px::new(400), Px::new(300)]);
712/// ```
713#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
714pub struct PxSize {
715 /// The width in physical pixels
716 pub width: Px,
717 /// The height in physical pixels
718 pub height: Px,
719}
720
721impl PxSize {
722 /// A constant representing zero size (0×0).
723 pub const ZERO: Self = Self {
724 width: Px(0),
725 height: Px(0),
726 };
727
728 /// Creates a new size from width and height.
729 ///
730 /// # Arguments
731 ///
732 /// * `width` - The width in physical pixels
733 /// * `height` - The height in physical pixels
734 ///
735 /// # Examples
736 ///
737 /// ```
738 /// use tessera_ui::px::{Px, PxSize};
739 ///
740 /// let size = PxSize::new(Px::new(300), Px::new(200));
741 /// assert_eq!(size.width, Px::new(300));
742 /// assert_eq!(size.height, Px::new(200));
743 /// ```
744 pub const fn new(width: Px, height: Px) -> Self {
745 Self { width, height }
746 }
747
748 /// Converts the size to a 2D f32 array.
749 ///
750 /// This is useful for interfacing with graphics APIs that expect
751 /// floating-point size values.
752 ///
753 /// # Returns
754 ///
755 /// An array `[width, height]` where both dimensions are converted to f32
756 ///
757 /// # Examples
758 ///
759 /// ```
760 /// use tessera_ui::px::{Px, PxSize};
761 ///
762 /// let size = PxSize::new(Px::new(300), Px::new(200));
763 /// assert_eq!(size.to_f32_arr2(), [300.0, 200.0]);
764 /// ```
765 pub fn to_f32_arr2(self) -> [f32; 2] {
766 [self.width.0 as f32, self.height.0 as f32]
767 }
768}
769
770/// A 2D rectangle in physical pixel space.
771///
772/// This type represents a rectangle with a position (top-left corner) and dimensions
773/// in physical pixel space.
774#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
775pub struct PxRect {
776 /// The x-coordinate of the top-left corner
777 pub x: Px,
778 /// The y-coordinate of the top-left corner
779 pub y: Px,
780 /// The width of the rectangle
781 pub width: Px,
782 /// The height of the rectangle
783 pub height: Px,
784}
785
786impl PxRect {
787 /// A constant representing a zero rectangle (0×0 at position (0, 0)).
788 pub const ZERO: Self = Self {
789 x: Px::ZERO,
790 y: Px::ZERO,
791 width: Px::ZERO,
792 height: Px::ZERO,
793 };
794
795 /// Creates a new rectangle from position and size.
796 pub const fn new(x: Px, y: Px, width: Px, height: Px) -> Self {
797 Self {
798 x,
799 y,
800 width,
801 height,
802 }
803 }
804
805 /// Checks if this rectangle is orthogonal (non-overlapping) with another rectangle.
806 ///
807 /// Two rectangles are orthogonal if they do not overlap in either the x or y axis.
808 /// This is useful for barrier batching optimization where non-overlapping rectangles
809 /// can be processed together without requiring barriers.
810 ///
811 /// # Arguments
812 ///
813 /// * `other` - The other rectangle to check orthogonality against
814 ///
815 /// # Returns
816 ///
817 /// `true` if the rectangles are orthogonal (non-overlapping), `false` otherwise
818 ///
819 /// # Examples
820 ///
821 /// ```
822 /// use tessera_ui::px::{Px, PxRect};
823 ///
824 /// let rect1 = PxRect::new(Px::new(0), Px::new(0), Px::new(100), Px::new(100));
825 /// let rect2 = PxRect::new(Px::new(150), Px::new(0), Px::new(100), Px::new(100));
826 /// assert!(rect1.is_orthogonal(&rect2));
827 ///
828 /// let rect3 = PxRect::new(Px::new(50), Px::new(50), Px::new(100), Px::new(100));
829 /// assert!(!rect1.is_orthogonal(&rect3));
830 /// ```
831 pub fn is_orthogonal(&self, other: &Self) -> bool {
832 // Check if rectangles overlap on x-axis
833 let x_overlap = self.x.0 < other.x.0 + other.width.0 && other.x.0 < self.x.0 + self.width.0;
834
835 // Check if rectangles overlap on y-axis
836 let y_overlap =
837 self.y.0 < other.y.0 + other.height.0 && other.y.0 < self.y.0 + self.height.0;
838
839 // Rectangles are orthogonal if they don't overlap on either axis
840 !x_overlap || !y_overlap
841 }
842
843 /// Creates a new rectangle that is the union of this rectangle and another rectangle.
844 /// Which is the smallest rectangle that contains both rectangles.
845 ///
846 /// # Arguments
847 ///
848 /// * `other` - The other rectangle to union with
849 ///
850 /// # Returns
851 ///
852 /// A new `PxRect` that is the union of this rectangle and the other rectangle
853 ///
854 /// # Examples
855 ///
856 /// ```
857 /// use tessera_ui::px::{Px, PxRect};
858 ///
859 /// let rect1 = PxRect::new(Px::new(0), Px::new(0), Px::new(100), Px::new(100));
860 /// let rect2 = PxRect::new(Px::new(50), Px::new(50), Px::new(100), Px::new(100));
861 /// let union_rect = rect1.union(&rect2);
862 /// assert_eq!(union_rect, PxRect::new(Px::new(0), Px::new(0), Px::new(150), Px::new(150)));
863 /// ```
864 pub fn union(&self, other: &Self) -> Self {
865 let x = self.x.0.min(other.x.0);
866 let y = self.y.0.min(other.y.0);
867 let width = (self.x.0 + self.width.0).max(other.x.0 + other.width.0) - x;
868 let height = (self.y.0 + self.height.0).max(other.y.0 + other.height.0) - y;
869
870 Self {
871 x: Px(x),
872 y: Px(y),
873 width: Px(width),
874 height: Px(height),
875 }
876 }
877
878 /// Returns the area of this rectangle.
879 ///
880 /// # Returns
881 ///
882 /// The area as a positive integer, or 0 if width or height is negative
883 pub fn area(&self) -> u32 {
884 let width = self.width.0.max(0) as u32;
885 let height = self.height.0.max(0) as u32;
886 width * height
887 }
888
889 /// Gets the intersection of this rectangle with another rectangle.
890 ///
891 /// If the rectangles do not intersect, returns `None`.
892 ///
893 /// # Arguments
894 ///
895 /// * `other` - The other rectangle to intersect with
896 ///
897 /// # Returns
898 ///
899 /// An `Option<PxRect>` that is `Some` if the rectangles intersect,
900 /// or `None` if they do not.
901 ///
902 /// # Examples
903 ///
904 /// ```
905 /// use tessera_ui::px::{Px, PxRect};
906 ///
907 /// let rect1 = PxRect::new(Px::new(0), Px::new(0), Px::new(100), Px::new(100));
908 /// let rect2 = PxRect::new(Px::new(50), Px::new(50), Px::new(100), Px::new(100));
909 /// let intersection = rect1.intersection(&rect2);
910 /// assert_eq!(intersection, Some(PxRect::new(Px::new(50), Px::new(50), Px::new(50), Px::new(50))));
911 /// ```
912 pub fn intersection(&self, other: &Self) -> Option<Self> {
913 let x1 = self.x.0.max(other.x.0);
914 let y1 = self.y.0.max(other.y.0);
915 let x2 = (self.x.0 + self.width.0).min(other.x.0 + other.width.0);
916 let y2 = (self.y.0 + self.height.0).min(other.y.0 + other.height.0);
917
918 if x1 < x2 && y1 < y2 {
919 Some(Self {
920 x: Px(x1),
921 y: Px(y1),
922 width: Px(x2 - x1),
923 height: Px(y2 - y1),
924 })
925 } else {
926 None
927 }
928 }
929
930 /// Check if a point is inside the rectangle.
931 ///
932 /// # Arguments
933 ///
934 /// * `point` - The point to check
935 ///
936 /// # Returns
937 ///
938 /// An bool shows that whether the point is inside rectangle.
939 pub fn contains(&self, point: PxPosition) -> bool {
940 point.x.0 >= self.x.0
941 && point.x.0 < self.x.0 + self.width.0
942 && point.y.0 >= self.y.0
943 && point.y.0 < self.y.0 + self.height.0
944 }
945}
946
947impl From<[Px; 2]> for PxSize {
948 fn from(size: [Px; 2]) -> Self {
949 Self {
950 width: size[0],
951 height: size[1],
952 }
953 }
954}
955
956impl From<PxSize> for winit::dpi::PhysicalSize<i32> {
957 fn from(size: PxSize) -> Self {
958 winit::dpi::PhysicalSize {
959 width: size.width.raw(),
960 height: size.height.raw(),
961 }
962 }
963}
964
965impl From<winit::dpi::PhysicalSize<u32>> for PxSize {
966 fn from(size: winit::dpi::PhysicalSize<u32>) -> Self {
967 Self {
968 width: Px(size.width as i32),
969 height: Px(size.height as i32),
970 }
971 }
972}
973
974impl From<crate::component_tree::ComputedData> for PxSize {
975 fn from(data: crate::component_tree::ComputedData) -> Self {
976 Self {
977 width: data.width,
978 height: data.height,
979 }
980 }
981}
982
983impl From<PxSize> for winit::dpi::Size {
984 fn from(size: PxSize) -> Self {
985 winit::dpi::PhysicalSize::from(size).into()
986 }
987}
988
989impl std::ops::Add for Px {
990 type Output = Self;
991
992 fn add(self, rhs: Self) -> Self::Output {
993 Px(self.0 + rhs.0)
994 }
995}
996
997impl Neg for Px {
998 type Output = Self;
999
1000 fn neg(self) -> Self::Output {
1001 Px::new(-self.0)
1002 }
1003}
1004
1005impl std::ops::Sub for Px {
1006 type Output = Self;
1007
1008 fn sub(self, rhs: Self) -> Self::Output {
1009 Px(self.0 - rhs.0)
1010 }
1011}
1012
1013impl std::ops::Mul for Px {
1014 type Output = Self;
1015
1016 fn mul(self, rhs: Self) -> Self::Output {
1017 Px(self.0 * rhs.0)
1018 }
1019}
1020
1021impl std::ops::Div for Px {
1022 type Output = Self;
1023
1024 fn div(self, rhs: Self) -> Self::Output {
1025 Px(self.0 / rhs.0)
1026 }
1027}
1028
1029impl std::ops::Mul<i32> for Px {
1030 type Output = Self;
1031
1032 fn mul(self, rhs: i32) -> Self::Output {
1033 Px(self.0 * rhs)
1034 }
1035}
1036
1037impl std::ops::Div<i32> for Px {
1038 type Output = Self;
1039
1040 fn div(self, rhs: i32) -> Self::Output {
1041 Px(self.0 / rhs)
1042 }
1043}
1044
1045impl From<i32> for Px {
1046 fn from(value: i32) -> Self {
1047 Px(value)
1048 }
1049}
1050
1051impl From<u32> for Px {
1052 fn from(value: u32) -> Self {
1053 Px(value as i32)
1054 }
1055}
1056
1057impl From<Dp> for Px {
1058 fn from(dp: Dp) -> Self {
1059 Px::from_dp(dp)
1060 }
1061}
1062
1063impl From<PxPosition> for winit::dpi::PhysicalPosition<i32> {
1064 fn from(pos: PxPosition) -> Self {
1065 winit::dpi::PhysicalPosition {
1066 x: pos.x.0,
1067 y: pos.y.0,
1068 }
1069 }
1070}
1071
1072impl From<PxPosition> for winit::dpi::Position {
1073 fn from(pos: PxPosition) -> Self {
1074 winit::dpi::PhysicalPosition::from(pos).into()
1075 }
1076}
1077
1078impl AddAssign for Px {
1079 fn add_assign(&mut self, rhs: Self) {
1080 self.0 += rhs.0;
1081 }
1082}
1083
1084// Arithmetic operations support - PxPosition
1085impl std::ops::Add for PxPosition {
1086 type Output = Self;
1087
1088 fn add(self, rhs: Self) -> Self::Output {
1089 PxPosition {
1090 x: self.x + rhs.x,
1091 y: self.y + rhs.y,
1092 }
1093 }
1094}
1095
1096impl std::ops::Sub for PxPosition {
1097 type Output = Self;
1098
1099 fn sub(self, rhs: Self) -> Self::Output {
1100 PxPosition {
1101 x: self.x - rhs.x,
1102 y: self.y - rhs.y,
1103 }
1104 }
1105}
1106
1107// Type conversion implementations
1108impl From<[i32; 2]> for PxPosition {
1109 fn from(pos: [i32; 2]) -> Self {
1110 PxPosition {
1111 x: Px(pos[0]),
1112 y: Px(pos[1]),
1113 }
1114 }
1115}
1116
1117impl From<PxPosition> for [i32; 2] {
1118 fn from(pos: PxPosition) -> Self {
1119 [pos.x.0, pos.y.0]
1120 }
1121}
1122
1123impl From<[u32; 2]> for PxPosition {
1124 fn from(pos: [u32; 2]) -> Self {
1125 PxPosition {
1126 x: Px(pos[0] as i32),
1127 y: Px(pos[1] as i32),
1128 }
1129 }
1130}
1131
1132impl From<PxPosition> for [u32; 2] {
1133 fn from(pos: PxPosition) -> Self {
1134 [pos.x.positive(), pos.y.positive()]
1135 }
1136}
1137
1138impl From<[Px; 2]> for PxPosition {
1139 fn from(pos: [Px; 2]) -> Self {
1140 PxPosition {
1141 x: pos[0],
1142 y: pos[1],
1143 }
1144 }
1145}
1146
1147impl From<PxPosition> for [Px; 2] {
1148 fn from(pos: PxPosition) -> Self {
1149 [pos.x, pos.y]
1150 }
1151}
1152
1153#[cfg(test)]
1154mod tests {
1155 use super::*;
1156
1157 #[test]
1158 fn test_px_creation() {
1159 let px = Px::new(42);
1160 assert_eq!(px.0, 42);
1161
1162 let px_neg = Px::new(-10);
1163 assert_eq!(px_neg.0, -10);
1164 }
1165
1166 #[test]
1167 fn test_px_arithmetic() {
1168 let a = Px(10);
1169 let b = Px(5);
1170
1171 assert_eq!(a + b, Px(15));
1172 assert_eq!(a - b, Px(5));
1173 assert_eq!(a * 2, Px(20));
1174 assert_eq!(a / 2, Px(5));
1175 assert_eq!(a * b, Px(50));
1176 assert_eq!(a / b, Px(2));
1177 }
1178
1179 #[test]
1180 fn test_px_saturating_arithmetic() {
1181 let max = Px(i32::MAX);
1182 let min = Px(i32::MIN);
1183 assert_eq!(max.saturating_add(Px(1)), max);
1184 assert_eq!(min.saturating_sub(Px(1)), min);
1185 }
1186
1187 #[test]
1188 fn test_saturating_from_f32() {
1189 assert_eq!(Px::saturating_from_f32(f32::MAX), Px(i32::MAX));
1190 assert_eq!(Px::saturating_from_f32(f32::MIN), Px(i32::MIN));
1191 assert_eq!(Px::saturating_from_f32(100.5), Px(100));
1192 assert_eq!(Px::saturating_from_f32(-100.5), Px(-100));
1193 }
1194
1195 #[test]
1196 fn test_px_abs() {
1197 assert_eq!(Px(10).abs(), 10);
1198 assert_eq!(Px(-5).abs(), 5);
1199 assert_eq!(Px(0).abs(), 0);
1200 }
1201
1202 #[test]
1203 fn test_px_position() {
1204 let pos = PxPosition::new(Px(10), Px(-5));
1205 assert_eq!(pos.x, Px(10));
1206 assert_eq!(pos.y, Px(-5));
1207
1208 let offset_pos = pos.offset(Px(2), Px(3));
1209 assert_eq!(offset_pos, PxPosition::new(Px(12), Px(-2)));
1210 }
1211
1212 #[test]
1213 fn test_px_position_arithmetic() {
1214 let pos1 = PxPosition::new(Px(10), Px(20));
1215 let pos2 = PxPosition::new(Px(5), Px(15));
1216
1217 let sum = pos1 + pos2;
1218 assert_eq!(sum, PxPosition::new(Px(15), Px(35)));
1219
1220 let diff = pos1 - pos2;
1221 assert_eq!(diff, PxPosition::new(Px(5), Px(5)));
1222 }
1223
1224 #[test]
1225 fn test_px_position_conversions() {
1226 let i32_pos: [i32; 2] = [10, -5];
1227 let px_pos: PxPosition = i32_pos.into();
1228 let back_to_i32: [i32; 2] = px_pos.into();
1229 assert_eq!(i32_pos, back_to_i32);
1230
1231 let u32_pos: [u32; 2] = [10, 5];
1232 let px_from_u32: PxPosition = u32_pos.into();
1233 let back_to_u32: [u32; 2] = px_from_u32.into();
1234 assert_eq!(u32_pos, back_to_u32);
1235 }
1236
1237 #[test]
1238 fn test_distance() {
1239 let pos1 = PxPosition::new(Px(0), Px(0));
1240 let pos2 = PxPosition::new(Px(3), Px(4));
1241 assert_eq!(pos1.distance_to(pos2), 5.0);
1242 }
1243}