tessera_ui/
dp.rs

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