Skip to main content

repose_core/
units.rs

1//! Density-independent (`Dp`), physical pixel (`Px`), and font-scaled (`Sp`) units.
2//!
3//! - `Dp` is a transparent value-class wrapper around `f32`, constructed via
4//!   the [`UnitExt::dp`] extension (`16.dp()`), like Compose's `Float.dp`.
5//!   `Dp(16.0)` is equivalent; prefer `.dp()` in runtime code and `Dp(..)`
6//!   in `const` contexts (`.dp()` is not `const`).
7//! - `Sp` mirrors Compose's `TextUnit` Sp type (`14.sp()`). Em is omitted:
8//!   repose has no relative-em usage (future addition if needed).
9//! - There is **no `Px` scalar type in Compose** (pixels are bare `Float`);
10//!   `Px` is added here per repo decision so paint/input boundaries are
11//!   explicit instead of unitless `f32`. App code should not take `Px`
12//!   params (no `padding_px` etc.); `Px` is for scene/canvas/paint/input.
13//! - Conversions are `Density`-scoped in Compose (`Density.Dp.toPx()`).
14//!   The ambient `to_px()` helpers below use the thread-local
15//!   `Density * UiScale` (and `* TextScale` for `Sp`), matching repose's
16//!   existing `effective_density_scale()` behavior; explicit
17//!   [`Density`](crate::locals::Density) overloads take an exact scale.
18//! - Compound geometry is split like Compose: dp-space
19//!   [`DpOffset`]/[`DpSize`]/[`DpRect`] vs px-space `Vec2`/`Size`/`Rect`
20//!   (Compose `Offset`/`Size`/`Rect` are px floats).
21//! - `Duration` is intentionally **not** wrapped: like Compose (stdlib
22//!   `kotlin.time.Duration`), repose uses `std`/`web_time::Duration`
23//!   (see `MotionScheme`/`AnimationSpec`).
24//! - `Velocity` is intentionally **not** wrapped either: like Compose's
25//!   px-per-second `Velocity`, repose expresses velocities as `f32` px/s
26//!   (see `gesture`/`scroll` physics, documented at each site). A newtype
27//!   can be added if velocity/dp confusion ever arises; today the `/s`
28//!   dimension makes misuse obvious.
29
30use std::fmt;
31use std::ops::{Add, Div, Mul, Neg, Sub};
32
33use crate::geometry::{Rect, Size, Vec2};
34
35/// Density-independent pixels. Authoring APIs (Modifier, layout) take this.
36///
37/// Compose parity (`Dp.kt`): value class, `Hairline`/`Infinity`/`Unspecified`,
38/// `isSpecified`, arithmetic, `coerce*`, `lerp`.
39#[derive(Clone, Copy, Default, PartialEq)]
40#[repr(transparent)]
41pub struct Dp(pub f32);
42
43impl Dp {
44    pub const ZERO: Dp = Dp(0.0);
45    /// Compose `Dp.Hairline` (0.dp: takes no space, draws 1 px).
46    pub const HAIRLINE: Dp = Dp(0.0);
47    pub const INFINITY: Dp = Dp(f32::INFINITY);
48    /// Compose `Dp.Unspecified` (NaN sentinel).
49    pub const UNSPECIFIED: Dp = Dp(f32::NAN);
50
51    #[inline]
52    pub const fn from_px_raw(v: f32) -> Dp {
53        Dp(v)
54    }
55
56    /// Raw value in dp.
57    #[inline]
58    pub const fn value(self) -> f32 {
59        self.0
60    }
61
62    /// `false` for [`Dp::UNSPECIFIED`].
63    #[inline]
64    pub fn is_specified(self) -> bool {
65        !self.0.is_nan()
66    }
67
68    /// `true` for [`Dp::UNSPECIFIED`].
69    #[inline]
70    pub fn is_unspecified(self) -> bool {
71        self.0.is_nan()
72    }
73
74    /// Return self if specified, else `block()`. (Compose `takeOrElse`.)
75    #[inline]
76    pub fn take_or_else(self, block: impl FnOnce() -> Dp) -> Dp {
77        if self.is_specified() { self } else { block() }
78    }
79
80    #[inline]
81    pub fn is_finite(self) -> bool {
82        self.0.is_finite()
83    }
84
85    /// Absolute value (mirrors `f32::abs`).
86    #[inline]
87    pub fn abs(self) -> Dp {
88        Dp(self.0.abs())
89    }
90
91    /// Minimum of two `Dp`s (mirrors `f32::min`).
92    #[inline]
93    pub fn min(self, other: Dp) -> Dp {
94        Dp(self.0.min(other.0))
95    }
96
97    /// Maximum of two `Dp`s (mirrors `f32::max`).
98    #[inline]
99    pub fn max(self, other: Dp) -> Dp {
100        Dp(self.0.max(other.0))
101    }
102
103    /// Clamp within `[min, max]` (mirrors `f32::clamp`).
104    #[inline]
105    pub fn clamp(self, min: Dp, max: Dp) -> Dp {
106        Dp(self.0.clamp(min.0, max.0))
107    }
108
109    #[inline]
110    pub fn coerce_in(self, min: Dp, max: Dp) -> Dp {
111        Dp(self.0.clamp(min.0, max.0))
112    }
113
114    #[inline]
115    pub fn coerce_at_least(self, min: Dp) -> Dp {
116        Dp(self.0.max(min.0))
117    }
118
119    #[inline]
120    pub fn coerce_at_most(self, max: Dp) -> Dp {
121        Dp(self.0.min(max.0))
122    }
123
124    /// Convert to physical pixels with the ambient `Density * UiScale`.
125    #[inline]
126    pub fn to_px(self) -> Px {
127        Px(self.0 * crate::locals::effective_density_scale())
128    }
129
130    /// Convert to physical pixels with an explicit scale.
131    #[inline]
132    pub fn to_px_with_scale(self, scale: f32) -> Px {
133        Px(self.0 * scale)
134    }
135
136    /// Round to whole pixels with the ambient scale (Compose `roundToPx`).
137    #[inline]
138    pub fn round_to_px(self) -> i32 {
139        self.to_px().0.round() as i32
140    }
141
142    /// Compose `FontScalingLinear.Dp.toSp`: `value / fontScale`.
143    #[inline]
144    pub fn to_sp(self) -> Sp {
145        let fs = crate::locals::text_scale().0.max(0.0001);
146        Sp(self.0 / fs)
147    }
148
149    /// Normalized bits for hashing / `scope!` inputs
150    /// (like `f32::to_bits`, with `-0.0`/`NaN` normalized).
151    #[inline]
152    pub fn to_bits(self) -> u32 {
153        hash_f32_bits(self.0)
154    }
155}
156
157impl fmt::Debug for Dp {
158    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159        if self.is_unspecified() {
160            write!(f, "Dp.Unspecified")
161        } else {
162            write!(f, "{}dp", self.0)
163        }
164    }
165}
166
167impl fmt::Display for Dp {
168    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
169        if self.is_unspecified() {
170            write!(f, "Dp.Unspecified")
171        } else {
172            write!(f, "{}dp", self.0)
173        }
174    }
175}
176
177impl std::hash::Hash for Dp {
178    #[inline]
179    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
180        // `Eq` is intentionally NOT implemented (NaN `Unspecified` never
181        // equals itself, like Compose); hashing still works via normalized bits.
182        hash_f32_bits(self.0).hash(state);
183    }
184}
185
186/// Bit hash of an `f32` with `-0.0` and NaN normalized
187/// (same normalization as `repose-tree` content hashing).
188#[inline]
189fn hash_f32_bits(v: f32) -> u32 {
190    let mut bits = v.to_bits();
191    if bits == 0x8000_0000 {
192        bits = 0;
193    }
194    if v.is_nan() {
195        bits = 0x7FC0_0000;
196    }
197    bits
198}
199
200impl Add for Dp {
201    type Output = Dp;
202    #[inline]
203    fn add(self, other: Dp) -> Dp {
204        Dp(self.0 + other.0)
205    }
206}
207
208impl Sub for Dp {
209    type Output = Dp;
210    #[inline]
211    fn sub(self, other: Dp) -> Dp {
212        Dp(self.0 - other.0)
213    }
214}
215
216impl Neg for Dp {
217    type Output = Dp;
218    #[inline]
219    fn neg(self) -> Dp {
220        Dp(-self.0)
221    }
222}
223
224impl Mul<f32> for Dp {
225    type Output = Dp;
226    #[inline]
227    fn mul(self, other: f32) -> Dp {
228        Dp(self.0 * other)
229    }
230}
231
232impl Mul<Dp> for f32 {
233    type Output = Dp;
234    #[inline]
235    fn mul(self, other: Dp) -> Dp {
236        Dp(self * other.0)
237    }
238}
239
240impl Div<f32> for Dp {
241    type Output = Dp;
242    #[inline]
243    fn div(self, other: f32) -> Dp {
244        Dp(self.0 / other)
245    }
246}
247
248/// Divide by another `Dp` to get a scalar (Compose `Dp.div(Dp): Float`).
249impl Div<Dp> for Dp {
250    type Output = f32;
251    #[inline]
252    fn div(self, other: Dp) -> f32 {
253        self.0 / other.0
254    }
255}
256
257impl PartialOrd for Dp {
258    #[inline]
259    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
260        // Compose: unspecified compares == 0 but never equals.
261        if self.0.is_nan() || other.0.is_nan() {
262            Some(std::cmp::Ordering::Equal)
263        } else {
264            self.0.partial_cmp(&other.0)
265        }
266    }
267}
268
269/// Linear interpolation between two `Dp`s (Compose `lerp`).
270#[inline]
271pub fn lerp_dp(start: Dp, stop: Dp, fraction: f32) -> Dp {
272    Dp(start.0 + (stop.0 - start.0) * fraction)
273}
274
275#[inline]
276pub fn min_dp(a: Dp, b: Dp) -> Dp {
277    Dp(a.0.min(b.0))
278}
279
280#[inline]
281pub fn max_dp(a: Dp, b: Dp) -> Dp {
282    Dp(a.0.max(b.0))
283}
284
285/// Physical pixels. Paint/input/raster boundaries take this.
286///
287/// NOTE: Compose has no `Px` scalar type (px are bare `Float`); this newtype
288/// exists so `f32` return/param positions are unambiguous in repose.
289#[derive(Clone, Copy, Default, PartialEq)]
290#[repr(transparent)]
291pub struct Px(pub f32);
292
293impl Px {
294    pub const ZERO: Px = Px(0.0);
295    pub const INFINITY: Px = Px(f32::INFINITY);
296    pub const UNSPECIFIED: Px = Px(f32::NAN);
297
298    #[inline]
299    pub const fn value(self) -> f32 {
300        self.0
301    }
302
303    #[inline]
304    pub fn is_specified(self) -> bool {
305        !self.0.is_nan()
306    }
307
308    #[inline]
309    pub fn is_finite(self) -> bool {
310        self.0.is_finite()
311    }
312
313    /// Absolute value (mirrors `f32::abs`).
314    #[inline]
315    pub fn abs(self) -> Px {
316        Px(self.0.abs())
317    }
318
319    /// Minimum of two `Px`s (mirrors `f32::min`).
320    #[inline]
321    pub fn min(self, other: Px) -> Px {
322        Px(self.0.min(other.0))
323    }
324
325    /// Maximum of two `Px`s (mirrors `f32::max`).
326    #[inline]
327    pub fn max(self, other: Px) -> Px {
328        Px(self.0.max(other.0))
329    }
330
331    /// Convert to dp with the ambient `Density * UiScale`.
332    #[inline]
333    pub fn to_dp(self) -> Dp {
334        let scale = crate::locals::effective_density_scale();
335        if scale <= 0.0001 {
336            Dp(0.0)
337        } else {
338            Dp(self.0 / scale)
339        }
340    }
341
342    /// Convert to dp with an explicit scale.
343    #[inline]
344    pub fn to_dp_with_scale(self, scale: f32) -> Dp {
345        if scale <= 0.0001 {
346            Dp(0.0)
347        } else {
348            Dp(self.0 / scale)
349        }
350    }
351
352    /// Convert a px value to `Sp` (px → dp → sp).
353    #[inline]
354    pub fn to_sp(self) -> Sp {
355        self.to_dp().to_sp()
356    }
357}
358
359impl fmt::Debug for Px {
360    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361        write!(f, "{}px", self.0)
362    }
363}
364
365impl fmt::Display for Px {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        write!(f, "{}px", self.0)
368    }
369}
370
371impl std::hash::Hash for Px {
372    #[inline]
373    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
374        hash_f32_bits(self.0).hash(state);
375    }
376}
377
378impl Add for Px {
379    type Output = Px;
380    #[inline]
381    fn add(self, other: Px) -> Px {
382        Px(self.0 + other.0)
383    }
384}
385
386impl Sub for Px {
387    type Output = Px;
388    #[inline]
389    fn sub(self, other: Px) -> Px {
390        Px(self.0 - other.0)
391    }
392}
393
394impl Neg for Px {
395    type Output = Px;
396    #[inline]
397    fn neg(self) -> Px {
398        Px(-self.0)
399    }
400}
401
402impl Mul<f32> for Px {
403    type Output = Px;
404    #[inline]
405    fn mul(self, other: f32) -> Px {
406        Px(self.0 * other)
407    }
408}
409
410impl Mul<Px> for f32 {
411    type Output = Px;
412    #[inline]
413    fn mul(self, other: Px) -> Px {
414        Px(self * other.0)
415    }
416}
417
418impl Div<f32> for Px {
419    type Output = Px;
420    #[inline]
421    fn div(self, other: f32) -> Px {
422        Px(self.0 / other)
423    }
424}
425
426impl Div<Px> for Px {
427    type Output = f32;
428    #[inline]
429    fn div(self, other: Px) -> f32 {
430        self.0 / other.0
431    }
432}
433
434impl PartialOrd for Px {
435    #[inline]
436    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
437        self.0.partial_cmp(&other.0)
438    }
439}
440
441#[inline]
442pub fn lerp_px(start: Px, stop: Px, fraction: f32) -> Px {
443    Px(start.0 + (stop.0 - start.0) * fraction)
444}
445
446/// Scaled pixels for text. Mirrors Compose `TextUnit` Sp type.
447///
448/// Converted to px with `Density * UiScale * TextScale`
449/// (Compose `Density.TextUnit.toPx` via `toDp().toPx()`).
450#[derive(Clone, Copy, Default, PartialEq)]
451#[repr(transparent)]
452pub struct Sp(pub f32);
453
454impl Sp {
455    pub const ZERO: Sp = Sp(0.0);
456    pub const UNSPECIFIED: Sp = Sp(f32::NAN);
457
458    #[inline]
459    pub const fn value(self) -> f32 {
460        self.0
461    }
462
463    #[inline]
464    pub fn is_specified(self) -> bool {
465        !self.0.is_nan()
466    }
467
468    /// Absolute value (mirrors `f32::abs`).
469    #[inline]
470    pub fn abs(self) -> Sp {
471        Sp(self.0.abs())
472    }
473
474    /// Minimum of two `Sp`s (mirrors `f32::min`).
475    #[inline]
476    pub fn min(self, other: Sp) -> Sp {
477        Sp(self.0.min(other.0))
478    }
479
480    /// Maximum of two `Sp`s (mirrors `f32::max`).
481    #[inline]
482    pub fn max(self, other: Sp) -> Sp {
483        Sp(self.0.max(other.0))
484    }
485
486    /// Pixels with ambient `Density * UiScale * TextScale`.
487    #[inline]
488    pub fn to_px(self) -> Px {
489        Px(self.0
490            * crate::locals::effective_density_scale()
491            * crate::locals::text_scale().0.max(0.0))
492    }
493
494    /// Compose `TextUnit.toDp`: `value * fontScale`.
495    #[inline]
496    pub fn to_dp(self) -> Dp {
497        Dp(self.0 * crate::locals::text_scale().0.max(0.0))
498    }
499
500    pub fn take_or_else(self, block: impl FnOnce() -> Sp) -> Sp {
501        if self.is_specified() { self } else { block() }
502    }
503}
504
505impl fmt::Debug for Sp {
506    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
507        if self.is_specified() {
508            write!(f, "{}sp", self.0)
509        } else {
510            write!(f, "Sp.Unspecified")
511        }
512    }
513}
514
515impl fmt::Display for Sp {
516    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517        if self.is_specified() {
518            write!(f, "{}sp", self.0)
519        } else {
520            write!(f, "Sp.Unspecified")
521        }
522    }
523}
524
525impl std::hash::Hash for Sp {
526    #[inline]
527    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
528        hash_f32_bits(self.0).hash(state);
529    }
530}
531
532impl Add for Sp {
533    type Output = Sp;
534    #[inline]
535    fn add(self, other: Sp) -> Sp {
536        Sp(self.0 + other.0)
537    }
538}
539
540impl Sub for Sp {
541    type Output = Sp;
542    #[inline]
543    fn sub(self, other: Sp) -> Sp {
544        Sp(self.0 - other.0)
545    }
546}
547
548impl Neg for Sp {
549    type Output = Sp;
550    #[inline]
551    fn neg(self) -> Sp {
552        Sp(-self.0)
553    }
554}
555
556impl Mul<f32> for Sp {
557    type Output = Sp;
558    #[inline]
559    fn mul(self, other: f32) -> Sp {
560        Sp(self.0 * other)
561    }
562}
563
564impl Div<f32> for Sp {
565    type Output = Sp;
566    #[inline]
567    fn div(self, other: f32) -> Sp {
568        Sp(self.0 / other)
569    }
570}
571
572impl PartialOrd for Sp {
573    #[inline]
574    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
575        self.0.partial_cmp(&other.0)
576    }
577}
578
579#[inline]
580pub fn lerp_sp(start: Sp, stop: Sp, fraction: f32) -> Sp {
581    Sp(start.0 + (stop.0 - start.0) * fraction)
582}
583
584/// Compose-style unit constructors: `16.0.dp()`, `10.px()`, `14.0.sp()`.
585pub trait UnitExt {
586    fn dp(self) -> Dp;
587    fn px(self) -> Px;
588    fn sp(self) -> Sp;
589}
590
591impl UnitExt for f32 {
592    #[inline]
593    fn dp(self) -> Dp {
594        Dp(self)
595    }
596    #[inline]
597    fn px(self) -> Px {
598        Px(self)
599    }
600    #[inline]
601    fn sp(self) -> Sp {
602        Sp(self)
603    }
604}
605
606impl UnitExt for f64 {
607    #[inline]
608    fn dp(self) -> Dp {
609        Dp(self as f32)
610    }
611    #[inline]
612    fn px(self) -> Px {
613        Px(self as f32)
614    }
615    #[inline]
616    fn sp(self) -> Sp {
617        Sp(self as f32)
618    }
619}
620
621impl UnitExt for i32 {
622    #[inline]
623    fn dp(self) -> Dp {
624        Dp(self as f32)
625    }
626    #[inline]
627    fn px(self) -> Px {
628        Px(self as f32)
629    }
630    #[inline]
631    fn sp(self) -> Sp {
632        Sp(self as f32)
633    }
634}
635
636impl UnitExt for u32 {
637    #[inline]
638    fn dp(self) -> Dp {
639        Dp(self as f32)
640    }
641    #[inline]
642    fn px(self) -> Px {
643        Px(self as f32)
644    }
645    #[inline]
646    fn sp(self) -> Sp {
647        Sp(self as f32)
648    }
649}
650
651/// Standard conversions so generic code can use `.into()`.
652/// Setters intentionally keep taking `Dp`/`Sp` directly (no `impl Into<Dp>`
653/// params) so call sites stay explicit: `16.dp()` or `Dp(16.0)`.
654impl From<f32> for Dp {
655    #[inline]
656    fn from(v: f32) -> Self {
657        Dp(v)
658    }
659}
660impl From<f64> for Dp {
661    #[inline]
662    fn from(v: f64) -> Self {
663        Dp(v as f32)
664    }
665}
666impl From<i32> for Dp {
667    #[inline]
668    fn from(v: i32) -> Self {
669        Dp(v as f32)
670    }
671}
672impl From<u32> for Dp {
673    #[inline]
674    fn from(v: u32) -> Self {
675        Dp(v as f32)
676    }
677}
678impl From<Dp> for f32 {
679    #[inline]
680    fn from(v: Dp) -> Self {
681        v.0
682    }
683}
684
685impl From<f32> for Sp {
686    #[inline]
687    fn from(v: f32) -> Self {
688        Sp(v)
689    }
690}
691impl From<f64> for Sp {
692    #[inline]
693    fn from(v: f64) -> Self {
694        Sp(v as f32)
695    }
696}
697impl From<i32> for Sp {
698    #[inline]
699    fn from(v: i32) -> Self {
700        Sp(v as f32)
701    }
702}
703impl From<u32> for Sp {
704    #[inline]
705    fn from(v: u32) -> Self {
706        Sp(v as f32)
707    }
708}
709impl From<Sp> for f32 {
710    #[inline]
711    fn from(v: Sp) -> Self {
712        v.0
713    }
714}
715
716impl From<f32> for Px {
717    #[inline]
718    fn from(v: f32) -> Self {
719        Px(v)
720    }
721}
722impl From<f64> for Px {
723    #[inline]
724    fn from(v: f64) -> Self {
725        Px(v as f32)
726    }
727}
728impl From<i32> for Px {
729    #[inline]
730    fn from(v: i32) -> Self {
731        Px(v as f32)
732    }
733}
734impl From<u32> for Px {
735    #[inline]
736    fn from(v: u32) -> Self {
737        Px(v as f32)
738    }
739}
740impl From<Px> for f32 {
741    #[inline]
742    fn from(v: Px) -> Self {
743        v.0
744    }
745}
746
747/// Dp-space 2D offset (Compose `DpOffset`).
748#[derive(Clone, Copy, Debug, Default, PartialEq)]
749pub struct DpOffset {
750    pub x: Dp,
751    pub y: Dp,
752}
753
754impl DpOffset {
755    pub const ZERO: DpOffset = DpOffset {
756        x: Dp::ZERO,
757        y: Dp::ZERO,
758    };
759    pub const UNSPECIFIED: DpOffset = DpOffset {
760        x: Dp::UNSPECIFIED,
761        y: Dp::UNSPECIFIED,
762    };
763
764    #[inline]
765    pub fn new(x: Dp, y: Dp) -> Self {
766        Self { x, y }
767    }
768
769    #[inline]
770    pub fn is_specified(self) -> bool {
771        self.x.is_specified() && self.y.is_specified()
772    }
773
774    /// To px-space offset with the ambient scale.
775    #[inline]
776    pub fn to_px(self) -> Vec2 {
777        Vec2 {
778            x: self.x.to_px().0,
779            y: self.y.to_px().0,
780        }
781    }
782}
783
784impl Add for DpOffset {
785    type Output = DpOffset;
786    #[inline]
787    fn add(self, other: DpOffset) -> DpOffset {
788        DpOffset::new(self.x + other.x, self.y + other.y)
789    }
790}
791
792impl Sub for DpOffset {
793    type Output = DpOffset;
794    #[inline]
795    fn sub(self, other: DpOffset) -> DpOffset {
796        DpOffset::new(self.x - other.x, self.y - other.y)
797    }
798}
799
800/// Dp-space size (Compose `DpSize`).
801#[derive(Clone, Copy, Debug, Default, PartialEq)]
802pub struct DpSize {
803    pub width: Dp,
804    pub height: Dp,
805}
806
807impl DpSize {
808    pub const ZERO: DpSize = DpSize {
809        width: Dp::ZERO,
810        height: Dp::ZERO,
811    };
812
813    #[inline]
814    pub fn new(width: Dp, height: Dp) -> Self {
815        Self { width, height }
816    }
817
818    /// To px-space size with the ambient scale (Compose `DpSize.toSize`).
819    #[inline]
820    pub fn to_px(self) -> Size {
821        Size {
822            width: self.width.to_px().0,
823            height: self.height.to_px().0,
824        }
825    }
826}
827
828/// Dp-space bounds (Compose `DpRect`).
829#[derive(Clone, Copy, Debug, Default, PartialEq)]
830pub struct DpRect {
831    pub left: Dp,
832    pub top: Dp,
833    pub right: Dp,
834    pub bottom: Dp,
835}
836
837impl DpRect {
838    #[inline]
839    pub fn new(left: Dp, top: Dp, right: Dp, bottom: Dp) -> Self {
840        Self {
841            left,
842            top,
843            right,
844            bottom,
845        }
846    }
847
848    #[inline]
849    pub fn width(self) -> Dp {
850        self.right - self.left
851    }
852
853    #[inline]
854    pub fn height(self) -> Dp {
855        self.bottom - self.top
856    }
857
858    /// To px-space rect with the ambient scale (Compose `DpRect.toRect`).
859    #[inline]
860    pub fn to_px(self) -> Rect {
861        Rect {
862            x: self.left.to_px().0,
863            y: self.top.to_px().0,
864            w: (self.right - self.left).to_px().0,
865            h: (self.bottom - self.top).to_px().0,
866        }
867    }
868}
869
870/// Px → dp-geometry helpers (Compose `Size.toDpSize`).
871#[inline]
872pub fn size_px_to_dp(size: Size) -> DpSize {
873    DpSize::new(Px(size.width).to_dp(), Px(size.height).to_dp())
874}
875
876#[cfg(test)]
877mod tests {
878    use super::*;
879
880    #[test]
881    fn dp_arithmetic_matches_compose() {
882        assert_eq!(Dp(2.0) + Dp(3.0), Dp(5.0));
883        assert_eq!(Dp(5.0) - Dp(3.0), Dp(2.0));
884        assert_eq!(-Dp(2.0), Dp(-2.0));
885        assert_eq!(Dp(2.0) * 3.0, Dp(6.0));
886        assert_eq!(Dp(6.0) / 3.0, Dp(2.0));
887        assert_eq!(Dp(6.0) / Dp(3.0), 2.0);
888    }
889
890    #[test]
891    fn dp_unspecified_semantics() {
892        assert!(!Dp::UNSPECIFIED.is_specified());
893        assert!(Dp(1.0).is_specified());
894        assert_eq!(Dp::UNSPECIFIED.take_or_else(|| Dp(4.0)), Dp(4.0));
895        // Compose: unspecified compares Equal but never equals.
896        assert!(Dp::UNSPECIFIED != Dp::UNSPECIFIED);
897        assert_eq!(
898            Dp::UNSPECIFIED.partial_cmp(&Dp(1.0)),
899            Some(std::cmp::Ordering::Equal)
900        );
901    }
902
903    #[test]
904    fn unit_ext_constructors() {
905        assert_eq!(16.0f32.dp(), Dp(16.0));
906        assert_eq!(10i32.dp(), Dp(10.0));
907        assert_eq!(14.0f32.sp(), Sp(14.0));
908        assert_eq!(2.0f32.px(), Px(2.0));
909    }
910
911    #[test]
912    fn std_from_conversions() {
913        assert_eq!(Dp::from(16.0f32), Dp(16.0));
914        assert_eq!(Dp::from(10i32), Dp(10.0));
915        assert_eq!(Sp::from(14.0f32), Sp(14.0));
916        assert_eq!(Px::from(2.0f32), Px(2.0));
917        assert_eq!(f32::from(Dp(3.0)), 3.0);
918        let v: Dp = 8.0f32.into();
919        assert_eq!(v, Dp(8.0));
920    }
921
922    #[test]
923    fn dp_rect_dimensions() {
924        let r = DpRect::new(Dp(0.0), Dp(0.0), Dp(10.0), Dp(20.0));
925        assert_eq!(r.width(), Dp(10.0));
926        assert_eq!(r.height(), Dp(20.0));
927    }
928
929    #[test]
930    fn lerp_midpoint() {
931        assert_eq!(lerp_dp(Dp(0.0), Dp(10.0), 0.5), Dp(5.0));
932        assert_eq!(lerp_sp(Sp(0.0), Sp(10.0), 0.5), Sp(10.0 * 0.5));
933    }
934}