ailloli_ui_core/math/
scale.rs

1//! Device-pixel ratios and deterministic logical-to-physical snapping.
2
3use crate::{Point, Rect, Size};
4
5/// Device pixel ratio (DPR) for logical ↔ physical conversion.
6///
7/// # Examples
8///
9/// ```
10/// use ailloli_ui_core::Scale;
11/// assert_eq!(Scale::new(2.0).dpr, 2.0);
12/// ```
13#[derive(Debug, Clone, Copy, PartialEq)]
14pub struct Scale {
15    /// Pixels per logical unit (e.g. `2.0` on a 2× display).
16    pub dpr: f32,
17}
18
19impl Scale {
20    /// Creates a device-pixel ratio.
21    ///
22    /// In optimized builds, zero and negative finite values clamp to `0.0001`,
23    /// NaN follows floating-point `max` semantics and also becomes `0.0001`,
24    /// while positive infinity remains infinite.
25    ///
26    /// # Panics
27    ///
28    /// Debug builds assert that `dpr` is finite and strictly positive.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use ailloli_ui_core::Scale;
34    /// assert_eq!(Scale::new(1.5).dpr, 1.5);
35    /// ```
36    pub fn new(dpr: f32) -> Self {
37        debug_assert!(dpr.is_finite());
38        debug_assert!(dpr > 0.0);
39        Self {
40            dpr: dpr.max(0.0001),
41        }
42    }
43}
44
45/// Rectangle in physical pixel integers (snapped bounds).
46///
47/// # Examples
48///
49/// ```
50/// use ailloli_ui_core::PhysicalRectI32;
51/// assert_eq!(PhysicalRectI32 { x: 2, y: 4, w: 6, h: 8 }.w, 6);
52/// ```
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct PhysicalRectI32 {
55    /// Left edge in physical pixels.
56    pub x: i32,
57    /// Top edge in physical pixels.
58    pub y: i32,
59    /// Non-negative width in physical pixels.
60    pub w: i32,
61    /// Non-negative height in physical pixels.
62    pub h: i32,
63}
64
65/// Converts a logical length to physical pixels with `logical * dpr`.
66///
67/// This does not round, clamp, or reject non-finite values.
68///
69/// # Examples
70///
71/// ```
72/// use ailloli_ui_core::Scale;
73/// use ailloli_ui_core::math::to_physical_f32;
74/// assert_eq!(to_physical_f32(3.0, Scale::new(2.0)), 6.0);
75/// ```
76pub fn to_physical_f32(logical: f32, scale: Scale) -> f32 {
77    logical * scale.dpr
78}
79
80/// Converts physical pixels to logical units with `physical / dpr`.
81///
82/// This does not round, clamp, or reject non-finite values.
83///
84/// # Examples
85///
86/// ```
87/// use ailloli_ui_core::Scale;
88/// use ailloli_ui_core::math::to_logical_f32;
89/// assert_eq!(to_logical_f32(6.0, Scale::new(2.0)), 3.0);
90/// ```
91pub fn to_logical_f32(physical: f32, scale: Scale) -> f32 {
92    physical / scale.dpr
93}
94
95/// Rounds a logical coordinate to the nearest physical integer pixel.
96///
97/// The float-to-integer cast saturates values outside `i32` range and maps NaN
98/// to zero according to Rust cast semantics.
99///
100/// # Examples
101///
102/// ```
103/// use ailloli_ui_core::Scale;
104/// use ailloli_ui_core::math::snap_f32_to_physical_px;
105/// assert_eq!(snap_f32_to_physical_px(1.25, Scale::new(2.0)), 3);
106/// ```
107pub fn snap_f32_to_physical_px(x_logical: f32, scale: Scale) -> i32 {
108    to_physical_f32(x_logical, scale).round() as i32
109}
110
111/// Snaps both coordinates of a logical point to physical integer pixels.
112///
113/// # Examples
114///
115/// ```
116/// use ailloli_ui_core::{Point, Scale};
117/// use ailloli_ui_core::math::snap_point_to_physical;
118/// assert_eq!(snap_point_to_physical(Point::new(1.25, 2.0), Scale::new(2.0)), (3, 4));
119/// ```
120pub fn snap_point_to_physical(p: Point, scale: Scale) -> (i32, i32) {
121    (
122        snap_f32_to_physical_px(p.x, scale),
123        snap_f32_to_physical_px(p.y, scale),
124    )
125}
126
127/// Snaps logical width and height independently to physical integer pixels.
128///
129/// # Examples
130///
131/// ```
132/// use ailloli_ui_core::{Scale, Size};
133/// use ailloli_ui_core::math::snap_size_to_physical;
134/// assert_eq!(snap_size_to_physical(Size::new(1.25, 2.0), Scale::new(2.0)), (3, 4));
135/// ```
136pub fn snap_size_to_physical(s: Size, scale: Scale) -> (i32, i32) {
137    (
138        snap_f32_to_physical_px(s.w, scale),
139        snap_f32_to_physical_px(s.h, scale),
140    )
141}
142
143/// Snaps both logical rectangle edges and derives a non-negative physical size.
144///
145/// Snapping edges instead of width/height preserves their shared boundary. A
146/// reversed or collapsed result has zero physical width or height.
147///
148/// # Examples
149///
150/// ```
151/// use ailloli_ui_core::{PhysicalRectI32, Rect, Scale};
152/// use ailloli_ui_core::math::snap_rect_to_physical;
153/// assert_eq!(snap_rect_to_physical(Rect::new(1.0, 2.0, 3.0, 4.0), Scale::new(2.0)), PhysicalRectI32 { x: 2, y: 4, w: 6, h: 8 });
154/// ```
155pub fn snap_rect_to_physical(rect: Rect, scale: Scale) -> PhysicalRectI32 {
156    let x0 = snap_f32_to_physical_px(rect.x, scale);
157    let y0 = snap_f32_to_physical_px(rect.y, scale);
158    let x1 = snap_f32_to_physical_px(rect.x + rect.w, scale);
159    let y1 = snap_f32_to_physical_px(rect.y + rect.h, scale);
160
161    PhysicalRectI32 {
162        x: x0,
163        y: y0,
164        w: (x1 - x0).max(0),
165        h: (y1 - y0).max(0),
166    }
167}