cranpose_ui_graphics/
unit.rs1use std::ops::{Add, Div, Mul, Neg, Sub};
32
33#[derive(Clone, Copy, Debug, PartialEq)]
44pub struct Density {
45 pub scale: f32,
47 pub font_scale: f32,
49}
50
51impl Density {
52 pub const IDENTITY: Self = Self {
56 scale: 1.0,
57 font_scale: 1.0,
58 };
59
60 pub fn new(scale: f32, font_scale: f32) -> Self {
61 Self { scale, font_scale }
62 }
63
64 pub fn from_scale(scale: f32) -> Self {
68 Self::new(scale, 1.0)
69 }
70}
71
72impl Default for Density {
73 fn default() -> Self {
74 Self::IDENTITY
75 }
76}
77
78macro_rules! scalar_unit {
79 ($name:ident, $doc:literal) => {
80 #[doc = $doc]
81 #[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
82 pub struct $name(pub f32);
83
84 impl $name {
85 pub const ZERO: Self = Self(0.0);
86
87 pub fn max(self, other: Self) -> Self {
91 Self(self.0.max(other.0))
92 }
93
94 pub fn min(self, other: Self) -> Self {
96 Self(self.0.min(other.0))
97 }
98 }
99
100 impl From<f32> for $name {
101 fn from(value: f32) -> Self {
102 Self(value)
103 }
104 }
105
106 impl From<i32> for $name {
107 fn from(value: i32) -> Self {
108 Self(value as f32)
109 }
110 }
111
112 impl Add for $name {
113 type Output = Self;
114 fn add(self, rhs: Self) -> Self {
115 Self(self.0 + rhs.0)
116 }
117 }
118
119 impl Sub for $name {
120 type Output = Self;
121 fn sub(self, rhs: Self) -> Self {
122 Self(self.0 - rhs.0)
123 }
124 }
125
126 impl Neg for $name {
127 type Output = Self;
128 fn neg(self) -> Self {
129 Self(-self.0)
130 }
131 }
132
133 impl Mul<f32> for $name {
134 type Output = Self;
135 fn mul(self, rhs: f32) -> Self {
136 Self(self.0 * rhs)
137 }
138 }
139
140 impl Div<f32> for $name {
141 type Output = Self;
142 fn div(self, rhs: f32) -> Self {
143 Self(self.0 / rhs)
144 }
145 }
146 };
147}
148
149scalar_unit!(
150 Dp,
151 "Density-independent pixels: a layout length that reads the same on \
152 every device regardless of its pixel density."
153);
154scalar_unit!(
155 Sp,
156 "Scale-independent pixels: a text size that follows both the device's \
157 pixel density and the user's font-scale accessibility setting. \
158 Distinct from `Dp` because a length must not move when the user's \
159 text-size setting does, and a text size must."
160);
161scalar_unit!(
162 Px,
163 "A device pixel. Renderer-facing surfaces, hit-testing, and \
164 framebuffer coordinates read and write this; a layout author reaches \
165 for `Dp`/`Sp` instead, and can only get here through an explicit \
166 `Density`."
167);
168
169impl Dp {
170 pub fn to_px(self, density: Density) -> Px {
172 Px(self.0 * density.scale)
173 }
174
175 pub fn from_px(px: Px, density: Density) -> Self {
177 Self(px.0 / density.scale)
178 }
179}
180
181impl Sp {
182 pub fn to_px(self, density: Density) -> Px {
185 Px(self.0 * density.scale * density.font_scale)
186 }
187
188 pub fn from_px(px: Px, density: Density) -> Self {
190 Self(px.0 / (density.scale * density.font_scale))
191 }
192}
193
194impl Px {
195 pub fn to_dp(self, density: Density) -> Dp {
198 Dp(self.0 / density.scale)
199 }
200}
201
202pub trait DpExt {
205 fn dp(self) -> Dp;
206}
207
208impl DpExt for f32 {
209 fn dp(self) -> Dp {
210 Dp(self)
211 }
212}
213
214impl DpExt for i32 {
215 fn dp(self) -> Dp {
216 Dp(self as f32)
217 }
218}
219
220pub trait SpExt {
222 fn sp(self) -> Sp;
223}
224
225impl SpExt for f32 {
226 fn sp(self) -> Sp {
227 Sp(self)
228 }
229}
230
231impl SpExt for i32 {
232 fn sp(self) -> Sp {
233 Sp(self as f32)
234 }
235}
236
237pub trait PxExt {
239 fn px(self) -> Px;
240}
241
242impl PxExt for f32 {
243 fn px(self) -> Px {
244 Px(self)
245 }
246}
247
248impl PxExt for i32 {
249 fn px(self) -> Px {
250 Px(self as f32)
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
263 fn density_independent_pixels_round_trip_through_a_density() {
264 for scale in [1.0f32, 1.5, 2.0, 3.0] {
265 let density = Density::from_scale(scale);
266 let dp = Dp(24.0);
267 assert_eq!(dp.to_px(density), Px(24.0 * scale));
268 assert_eq!(Dp::from_px(dp.to_px(density), density), dp);
269 }
270 }
271
272 #[test]
277 fn a_dp_length_is_pinned_at_a_non_identity_density() {
278 let density = Density::from_scale(2.5);
279 assert_eq!(Dp(16.0).to_px(density), Px(40.0));
280 assert_eq!(Px(40.0).to_dp(density), Dp(16.0));
281 }
282
283 #[test]
286 fn scale_independent_pixels_round_trip_through_density_and_font_scale() {
287 for scale in [1.0f32, 2.0, 3.0] {
288 for font_scale in [0.85f32, 1.0, 1.3] {
289 let density = Density::new(scale, font_scale);
290 let sp = Sp(16.0);
291 assert_eq!(sp.to_px(density), Px(16.0 * scale * font_scale));
292 assert_eq!(Sp::from_px(sp.to_px(density), density), sp);
293 }
294 }
295 }
296
297 #[test]
301 fn an_sp_length_is_pinned_at_a_non_identity_density_and_font_scale() {
302 let density = Density::new(2.0, 1.25);
303 assert_eq!(Sp(16.0).to_px(density), Px(40.0));
304 }
305
306 #[test]
307 fn dp_arithmetic_matches_plain_float_arithmetic() {
308 assert_eq!(Dp(4.0) + Dp(2.0), Dp(6.0));
309 assert_eq!(Dp(4.0) - Dp(2.0), Dp(2.0));
310 assert_eq!(Dp(4.0) * 2.5, Dp(10.0));
311 assert_eq!(Dp(10.0) / 4.0, Dp(2.5));
312 assert_eq!(-Dp(4.0), Dp(-4.0));
313 assert_eq!(Dp(4.0).max(Dp(9.0)), Dp(9.0));
314 assert_eq!(Dp(4.0).min(Dp(9.0)), Dp(4.0));
315 }
316
317 #[test]
318 fn literal_and_extension_constructors_agree() {
319 assert_eq!(Dp::from(16.0f32), Dp(16.0));
320 assert_eq!(Dp::from(16), Dp(16.0));
321 assert_eq!(16.0.dp(), Dp(16.0));
322 assert_eq!(16.sp(), Sp(16.0));
323 assert_eq!(16.px(), Px(16.0));
324 }
325
326 #[test]
330 fn into_dp_accepts_a_bare_literal_and_an_explicit_conversion() {
331 fn padding(value: impl Into<Dp>) -> Dp {
332 value.into()
333 }
334
335 assert_eq!(padding(16.0), Dp(16.0));
336 assert_eq!(padding(16), Dp(16.0));
337 assert_eq!(padding(16.0.dp()), Dp(16.0));
338 }
339}