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