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}