tessera_ui/
dp.rs

1//! # Density-Independent Pixels (Dp)
2//!
3//! This module provides the [`Dp`] type for representing density-independent pixels,
4//! a fundamental unit for UI scaling in the Tessera framework.
5//!
6//! ## Overview
7//!
8//! Density-independent pixels (dp) are a virtual pixel unit that provides consistent
9//! visual sizing across different screen densities. Unlike physical pixels, dp units
10//! automatically scale based on the device's screen density, ensuring that UI elements
11//! appear at the same physical size regardless of the display's pixel density.
12//!
13//! ## Scale Factor
14//!
15//! The conversion between dp and physical pixels is controlled by a global scale factor
16//! stored in [`SCALE_FACTOR`]. This factor is typically set based on the device's DPI
17//! (dots per inch) and user preferences.
18//!
19//! ## Usage
20//!
21//! ```
22//! use tessera_ui::Dp;
23//!
24//! // Create a dp value
25//! let padding = Dp(16.0);
26//!
27//! // Convert to pixels for rendering
28//! let pixels = padding.to_pixels_f32();
29//!
30//! // Create from pixel values
31//! let dp_from_pixels = Dp::from_pixels_f32(48.0);
32//! ```
33//!
34//! ## Relationship with Px
35//!
36//! The [`Dp`] type works closely with the [`Px`] type (physical pixels). You can
37//! convert between them using the provided methods, with the conversion automatically
38//! applying the current scale factor.
39
40use std::{
41    fmt::Display,
42    ops::{Div, Mul},
43    sync::OnceLock,
44};
45
46use parking_lot::RwLock;
47
48use crate::Px;
49
50/// Global scale factor for converting between density-independent pixels and physical pixels.
51///
52/// This static variable holds the current scale factor used for dp-to-pixel conversions.
53/// It's typically initialized once during application startup based on the device's
54/// screen density and user scaling preferences.
55///
56/// The scale factor represents how many physical pixels correspond to one dp unit.
57/// For example:
58/// - Scale factor of 1.0: 1 dp = 1 pixel (standard density)
59/// - Scale factor of 2.0: 1 dp = 2 pixels (high density)
60/// - Scale factor of 0.75: 1 dp = 0.75 pixels (low density)
61///
62/// # Thread Safety
63///
64/// This variable uses `OnceLock<RwLock<f64>>` to ensure thread-safe access while
65/// allowing the scale factor to be updated during runtime if needed.
66pub static SCALE_FACTOR: OnceLock<RwLock<f64>> = OnceLock::new();
67
68/// Density-independent pixels (dp) for UI scaling.
69///
70/// `Dp` represents a length measurement that remains visually consistent across
71/// different screen densities. This is essential for creating UIs that look the
72/// same physical size on devices with varying pixel densities.
73///
74/// ## Design Philosophy
75///
76/// The dp unit is inspired by Android's density-independent pixel system and
77/// provides a device-agnostic way to specify UI dimensions. When you specify
78/// a button height of `Dp(48.0)`, it will appear roughly the same physical
79/// size on a low-DPI laptop screen and a high-DPI mobile device.
80///
81/// ## Internal Representation
82///
83/// The `Dp` struct wraps a single `f64` value representing the dp measurement.
84/// This value is converted to physical pixels using the global [`SCALE_FACTOR`]
85/// when rendering operations require pixel-precise measurements.
86///
87/// ## Examples
88///
89/// ```
90/// use tessera_ui::Dp;
91///
92/// // Common UI measurements in dp
93/// let small_padding = Dp(8.0);
94/// let medium_padding = Dp(16.0);
95/// let button_height = Dp(48.0);
96/// let large_spacing = Dp(32.0);
97///
98/// // Convert to pixels for rendering
99/// let pixels = button_height.to_pixels_f32();
100/// // Result depends on the current scale factor
101/// ```
102///
103/// ## Arithmetic Operations
104///
105/// While `Dp` doesn't implement arithmetic operators directly, you can perform
106/// operations on the inner value:
107///
108/// ```
109/// use tessera_ui::Dp;
110///
111/// let base_size = Dp(16.0);
112/// let double_size = Dp(base_size.0 * 2.0);
113/// let half_size = Dp(base_size.0 / 2.0);
114/// ```
115#[derive(Debug, Default, Clone, Copy, PartialEq, PartialOrd)]
116pub struct Dp(pub f64);
117
118impl Display for Dp {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        write!(f, "{:.2}dp", self.0)
121    }
122}
123
124impl Dp {
125    /// A constant representing zero density-independent pixels.
126    ///
127    /// # Examples
128    ///
129    /// ```
130    /// use tessera_ui::Dp;
131    ///
132    /// let zero_dp = Dp::ZERO;
133    /// assert_eq!(zero_dp, Dp(0.0));
134    /// ```
135    pub const ZERO: Dp = Dp(0.0);
136
137    /// Creates a new `Dp` instance with the specified value.
138    ///
139    /// This is a const function, allowing `Dp` values to be created at compile time.
140    ///
141    /// # Arguments
142    ///
143    /// * `value` - The dp value as a floating-point number
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use tessera_ui::Dp;
149    ///
150    /// const BUTTON_HEIGHT: Dp = Dp(48.0);
151    /// let padding = Dp(16.0);
152    /// ```
153    pub const fn new(value: f64) -> Self {
154        Dp(value)
155    }
156
157    /// Converts this dp value to physical pixels as an `f64`.
158    ///
159    /// This method applies the current global scale factor to convert density-independent
160    /// pixels to physical pixels. The scale factor is read from [`SCALE_FACTOR`].
161    ///
162    /// # Returns
163    ///
164    /// The equivalent value in physical pixels as a 64-bit floating-point number.
165    /// If the scale factor hasn't been initialized, defaults to 1.0 (no scaling).
166    ///
167    /// # Examples
168    ///
169    /// ```
170    /// use tessera_ui::Dp;
171    ///
172    /// let dp_value = Dp(24.0);
173    /// let pixels = dp_value.to_pixels_f64();
174    /// // Result depends on the current scale factor
175    /// ```
176    pub fn to_pixels_f64(&self) -> f64 {
177        let scale_factor = SCALE_FACTOR.get().map(|lock| *lock.read()).unwrap_or(1.0);
178        self.0 * scale_factor
179    }
180
181    /// Creates a `Dp` value from physical pixels specified as an `f64`.
182    ///
183    /// This method performs the inverse conversion of [`to_pixels_f64`](Self::to_pixels_f64),
184    /// converting physical pixels back to density-independent pixels using the current
185    /// global scale factor.
186    ///
187    /// # Arguments
188    ///
189    /// * `value` - The pixel value as a 64-bit floating-point number
190    ///
191    /// # Returns
192    ///
193    /// A new `Dp` instance representing the equivalent dp value.
194    /// If the scale factor hasn't been initialized, defaults to 1.0 (no scaling).
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use tessera_ui::Dp;
200    ///
201    /// // Convert 96 pixels to dp (assuming 2.0 scale factor = 48 dp)
202    /// let dp_value = Dp::from_pixels_f64(96.0);
203    /// ```
204    pub fn from_pixels_f64(value: f64) -> Self {
205        let scale_factor = SCALE_FACTOR.get().map(|lock| *lock.read()).unwrap_or(1.0);
206        Dp(value / scale_factor)
207    }
208
209    /// Converts this dp value to physical pixels as a `u32`.
210    ///
211    /// This method applies the current global scale factor and truncates the result
212    /// to an unsigned 32-bit integer. This is commonly used for rendering operations
213    /// that require integer pixel coordinates.
214    ///
215    /// # Returns
216    ///
217    /// The equivalent value in physical pixels as an unsigned 32-bit integer.
218    /// The result is truncated (not rounded) from the floating-point calculation.
219    /// If the scale factor hasn't been initialized, defaults to 1.0 (no scaling).
220    ///
221    /// # Examples
222    ///
223    /// ```
224    /// use tessera_ui::Dp;
225    ///
226    /// let dp_value = Dp(24.5);
227    /// let pixels = dp_value.to_pixels_u32();
228    /// // With scale factor 2.0: 24.5 * 2.0 = 49.0 -> 49u32
229    /// ```
230    ///
231    /// # Note
232    ///
233    /// Values are truncated, not rounded. For more precise control over rounding
234    /// behavior, use [`to_pixels_f64`](Self::to_pixels_f64) and apply your preferred
235    /// rounding method.
236    pub fn to_pixels_u32(&self) -> u32 {
237        let scale_factor = SCALE_FACTOR.get().map(|lock| *lock.read()).unwrap_or(1.0);
238        (self.0 * scale_factor) as u32
239    }
240
241    /// Creates a `Dp` value from physical pixels specified as a `u32`.
242    ///
243    /// This method converts an unsigned 32-bit integer pixel value to density-independent
244    /// pixels using the current global scale factor. The integer is first converted to
245    /// `f64` for the calculation.
246    ///
247    /// # Arguments
248    ///
249    /// * `value` - The pixel value as an unsigned 32-bit integer
250    ///
251    /// # Returns
252    ///
253    /// A new `Dp` instance representing the equivalent dp value.
254    /// If the scale factor hasn't been initialized, defaults to 1.0 (no scaling).
255    ///
256    /// # Examples
257    ///
258    /// ```
259    /// use tessera_ui::Dp;
260    ///
261    /// // Convert 96 pixels to dp (assuming 2.0 scale factor = 48.0 dp)
262    /// let dp_value = Dp::from_pixels_u32(96);
263    /// ```
264    pub fn from_pixels_u32(value: u32) -> Self {
265        let scale_factor = SCALE_FACTOR.get().map(|lock| *lock.read()).unwrap_or(1.0);
266        Dp((value as f64) / scale_factor)
267    }
268
269    /// Converts this dp value to physical pixels as an `f32`.
270    ///
271    /// This method applies the current global scale factor and converts the result
272    /// to a 32-bit floating-point number. This is commonly used for graphics APIs
273    /// that work with `f32` coordinates.
274    ///
275    /// # Returns
276    ///
277    /// The equivalent value in physical pixels as a 32-bit floating-point number.
278    /// If the scale factor hasn't been initialized, defaults to 1.0 (no scaling).
279    ///
280    /// # Examples
281    ///
282    /// ```
283    /// use tessera_ui::Dp;
284    ///
285    /// let dp_value = Dp(24.0);
286    /// let pixels = dp_value.to_pixels_f32();
287    /// // With scale factor 1.5: 24.0 * 1.5 = 36.0f32
288    /// ```
289    ///
290    /// # Precision Note
291    ///
292    /// Converting from `f64` to `f32` may result in precision loss for very large
293    /// or very precise values. For maximum precision, use [`to_pixels_f64`](Self::to_pixels_f64).
294    pub fn to_pixels_f32(&self) -> f32 {
295        let scale_factor = SCALE_FACTOR.get().map(|lock| *lock.read()).unwrap_or(1.0);
296        (self.0 * scale_factor) as f32
297    }
298
299    /// Creates a `Dp` value from physical pixels specified as an `f32`.
300    ///
301    /// This method converts a 32-bit floating-point pixel value to density-independent
302    /// pixels using the current global scale factor. The `f32` value is first converted
303    /// to `f64` for internal calculations.
304    ///
305    /// # Arguments
306    ///
307    /// * `value` - The pixel value as a 32-bit floating-point number
308    ///
309    /// # Returns
310    ///
311    /// A new `Dp` instance representing the equivalent dp value.
312    /// If the scale factor hasn't been initialized, defaults to 1.0 (no scaling).
313    ///
314    /// # Examples
315    ///
316    /// ```
317    /// use tessera_ui::Dp;
318    ///
319    /// // Convert 36.0 pixels to dp (assuming 1.5 scale factor = 24.0 dp)
320    /// let dp_value = Dp::from_pixels_f32(36.0);
321    /// ```
322    pub fn from_pixels_f32(value: f32) -> Self {
323        let scale_factor = SCALE_FACTOR.get().map(|lock| *lock.read()).unwrap_or(1.0);
324        Dp((value as f64) / scale_factor)
325    }
326
327    /// Converts this `Dp` value to a `Px` (physical pixels) value.
328    ///
329    /// This method provides a convenient way to convert between the two pixel
330    /// types used in the Tessera framework. It applies the current scale factor
331    /// and creates a `Px` instance from the result.
332    ///
333    /// # Returns
334    ///
335    /// A new `Px` instance representing the equivalent physical pixel value.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use tessera_ui::Dp;
341    ///
342    /// let dp_value = Dp(24.0);
343    /// let px_value = dp_value.to_px();
344    /// // px_value now contains the scaled pixel equivalent
345    /// ```
346    ///
347    /// # See Also
348    ///
349    /// * [`Px::to_dp`] - For the inverse conversion
350    /// * [`to_pixels_f32`](Self::to_pixels_f32) - For direct `f32` pixel conversion
351    pub fn to_px(&self) -> Px {
352        Px::from_f32(self.to_pixels_f32())
353    }
354}
355
356impl From<f64> for Dp {
357    /// Creates a `Dp` instance from an `f64` value.
358    ///
359    /// This implementation allows for convenient conversion from floating-point
360    /// numbers to `Dp` values using the `into()` method or direct assignment
361    /// in contexts where type coercion occurs.
362    ///
363    /// # Arguments
364    ///
365    /// * `value` - The dp value as a 64-bit floating-point number
366    ///
367    /// # Examples
368    ///
369    /// ```
370    /// use tessera_ui::Dp;
371    ///
372    /// let dp1: Dp = 24.0.into();
373    /// let dp2 = Dp::from(16.0);
374    ///
375    /// // In function calls that expect Dp
376    /// fn set_padding(padding: Dp) { /* ... */ }
377    /// set_padding(8.0.into());
378    /// ```
379    fn from(value: f64) -> Self {
380        Dp(value)
381    }
382}
383
384impl From<Px> for Dp {
385    /// Creates a `Dp` instance from a `Px` (physical pixels) value.
386    ///
387    /// This implementation enables seamless conversion between the two pixel
388    /// types used in the Tessera framework. The conversion applies the inverse
389    /// of the current scale factor to convert physical pixels back to
390    /// density-independent pixels.
391    ///
392    /// # Arguments
393    ///
394    /// * `px` - A `Px` instance representing physical pixels
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use tessera_ui::{Dp, Px};
400    ///
401    /// let px_value = Px::from_f32(48.0);
402    /// let dp_value: Dp = px_value.into();
403    ///
404    /// // Or using From::from
405    /// let dp_value2 = Dp::from(px_value);
406    /// ```
407    ///
408    /// # See Also
409    ///
410    /// * [`to_px`](Self::to_px) - For the inverse conversion
411    /// * [`from_pixels_f64`](Self::from_pixels_f64) - For direct pixel-to-dp conversion
412    fn from(px: Px) -> Self {
413        Dp::from_pixels_f64(px.to_dp().0)
414    }
415}
416
417impl Mul<f32> for Dp {
418    type Output = Dp;
419
420    fn mul(self, rhs: f32) -> Self::Output {
421        Dp(self.0 * (rhs as f64))
422    }
423}
424
425impl Div<f32> for Dp {
426    type Output = Dp;
427
428    fn div(self, rhs: f32) -> Self::Output {
429        Dp(self.0 / (rhs as f64))
430    }
431}
432
433impl Mul<f64> for Dp {
434    type Output = Dp;
435
436    fn mul(self, rhs: f64) -> Self::Output {
437        Dp(self.0 * rhs)
438    }
439}
440
441impl Div<f64> for Dp {
442    type Output = Dp;
443
444    fn div(self, rhs: f64) -> Self::Output {
445        Dp(self.0 / rhs)
446    }
447}