1use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
22
23use crate::{
24 font_scale::FontScaleCurve,
25 render_state::{current_density, current_font_scale_curve},
26 round_scaling_list::round_to_px,
27};
28
29#[derive(Clone, Copy, Debug, PartialEq)]
39pub struct Density {
40 density: f32,
41 font_scale: FontScaleCurve,
42}
43
44impl Density {
45 pub fn from_host() -> Self {
51 if crate::render_state::has_current_app_context() {
52 Self::with_curve(current_density(), current_font_scale_curve())
53 } else {
54 Self::default()
55 }
56 }
57
58 pub fn new(density: f32, font_scale: f32) -> Self {
62 let font_scale = if font_scale.is_finite() && font_scale > 0.0 {
63 font_scale
64 } else {
65 1.0
66 };
67 Self::with_curve(density, FontScaleCurve::linear(font_scale))
68 }
69
70 pub fn with_curve(density: f32, font_scale: FontScaleCurve) -> Self {
73 Self {
74 density: if density.is_finite() && density > 0.0 {
75 density
76 } else {
77 1.0
78 },
79 font_scale,
80 }
81 }
82
83 pub fn density(self) -> f32 {
85 self.density
86 }
87
88 pub fn font_scale(self) -> f32 {
90 self.font_scale.scale()
91 }
92
93 pub fn font_scale_curve(self) -> FontScaleCurve {
95 self.font_scale
96 }
97
98 pub fn dp(self, value: f32) -> f32 {
101 round_to_px(value, self.density)
102 }
103
104 pub fn sp(self, value: f32) -> f32 {
110 self.dp(self.font_scale.sp_to_dp(value))
111 }
112
113 pub fn round(self, value: f32) -> f32 {
115 round_to_px(value, self.density)
116 }
117
118 pub fn floor(self, value: f32) -> f32 {
120 if value.is_finite() {
121 (value * self.density).floor() / self.density
122 } else {
123 value
124 }
125 }
126
127 pub fn ceil(self, value: f32) -> f32 {
132 if value.is_finite() {
133 (value * self.density).ceil() / self.density
134 } else {
135 value
136 }
137 }
138
139 pub fn to_px(self, points: f32) -> f32 {
141 points * self.density
142 }
143
144 pub fn centre(self, available: f32, content: f32) -> f32 {
152 self.round((available - content) * 0.5)
153 }
154}
155
156impl Default for Density {
157 fn default() -> Self {
158 Self::new(1.0, 1.0)
159 }
160}
161
162#[derive(Clone, Copy, Debug, PartialEq)]
172pub struct DensityMeasureScope {
173 density: Density,
174}
175
176impl DensityMeasureScope {
177 pub fn new(density: Density) -> Self {
179 Self { density }
180 }
181}
182
183impl cranpose_ui_layout::MeasureScope for DensityMeasureScope {
184 fn density(&self) -> f32 {
185 self.density.density()
186 }
187
188 fn font_scale(&self) -> f32 {
189 self.density.font_scale()
190 }
191}
192
193pub fn local_density() -> CompositionLocal<Density> {
200 thread_local! {
201 static LOCAL: std::cell::RefCell<Option<CompositionLocal<Density>>> =
202 const { std::cell::RefCell::new(None) };
203 }
204 LOCAL.with(|cell| {
205 cell.borrow_mut()
206 .get_or_insert_with(|| compositionLocalOf(Density::from_host))
207 .clone()
208 })
209}
210
211pub fn density() -> Density {
213 local_density().current()
214}
215
216#[allow(non_snake_case)]
222#[track_caller]
223pub fn ProvideDensity(density: Density, content: impl FnOnce()) {
224 CompositionLocalProvider(vec![local_density().provides(density)], content);
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn a_surviving_density_scope_keeps_its_entry_when_a_sibling_leaves() {
233 use std::{cell::Cell, rc::Rc};
234
235 use cranpose_core::{Composition, MemoryApplier, MutableState, location_key};
236
237 let mut composition = Composition::new(MemoryApplier::new());
238 let runtime = composition.runtime_handle();
239 let with_leading = MutableState::with_runtime(true, runtime.clone());
240 let survivor = MutableState::with_runtime(3.0_f32, runtime);
241 let seen = Rc::new(Cell::new(0.0_f32));
242
243 #[cranpose_macros::composable]
244 #[allow(non_snake_case)]
245 fn Reader(seen: Rc<Cell<f32>>) {
246 seen.set(density().density());
247 }
248
249 fn tree(with_leading: bool, survivor: f32, seen: Rc<Cell<f32>>) {
250 if with_leading {
251 ProvideDensity(Density::new(2.0, 1.0), || {});
252 }
253 ProvideDensity(Density::new(survivor, 1.0), move || Reader(seen));
254 }
255
256 #[cranpose_macros::composable]
257 #[allow(non_snake_case)]
258 fn Panel(
259 with_leading: MutableState<bool>,
260 survivor: MutableState<f32>,
261 seen: Rc<Cell<f32>>,
262 ) {
263 tree(with_leading.value(), survivor.value(), seen);
264 }
265
266 let key = location_key(file!(), line!(), column!());
267 {
268 let seen = Rc::clone(&seen);
269 composition
270 .render(key, move || Panel(with_leading, survivor, Rc::clone(&seen)))
271 .expect("initial composition");
272 }
273 assert_eq!(seen.get(), 3.0);
274
275 with_leading.set_value(false);
276 while composition
277 .process_invalid_scopes()
278 .expect("drop the leading density scope")
279 {}
280 survivor.set_value(4.0);
281 while composition
282 .process_invalid_scopes()
283 .expect("change the surviving density")
284 {}
285 assert_eq!(
286 seen.get(),
287 4.0,
288 "the surviving density scope must keep its entry and its reader's \
289 subscription when the sibling leaves"
290 );
291 }
292
293 #[test]
294 fn a_provided_density_reaches_the_content_and_ends_with_it() {
295 use std::{cell::Cell, rc::Rc};
296
297 use cranpose_core::{Composition, MemoryApplier, location_key};
298
299 let mut composition = Composition::new(MemoryApplier::new());
300 let outer = Rc::new(Cell::new(0.0_f32));
301 let inside = Rc::new(Cell::new(0.0_f32));
302 let after = Rc::new(Cell::new(0.0_f32));
303
304 let key = location_key(file!(), line!(), column!());
305 {
306 let (outer, inside, after) = (Rc::clone(&outer), Rc::clone(&inside), Rc::clone(&after));
307 let mut render = move || {
308 outer.set(density().density());
309 ProvideDensity(Density::new(3.0, 1.0), || inside.set(density().density()));
310 after.set(density().density());
311 };
312 composition.render(key, &mut render).expect("render");
313 }
314
315 assert_eq!(inside.get(), 3.0, "the subtree reads the grid it was given");
316 assert_ne!(
317 outer.get(),
318 3.0,
319 "the provision does not escape upwards out of its content"
320 );
321 assert_eq!(
322 after.get(),
323 outer.get(),
324 "nor does it leak into what follows it"
325 );
326 }
327
328 #[test]
329 fn a_dp_length_lands_on_a_whole_device_pixel() {
330 let density = Density::new(2.0, 1.0);
331 assert_eq!(density.dp(13.3), 13.5);
332 assert_eq!(density.to_px(density.dp(13.3)), 27.0);
333 assert_eq!(density.dp(0.25), 0.5);
334 }
335
336 #[test]
337 fn a_text_size_moves_with_the_font_scale_and_a_dp_length_does_not() {
338 let density = Density::new(2.0, 1.24);
339 assert_eq!(density.dp(15.0), 15.0);
340 assert_eq!(density.to_px(density.sp(15.0)), 37.0);
341 }
342
343 #[test]
344 fn centring_puts_the_leading_gap_on_a_pixel_boundary() {
345 let density = Density::new(2.0, 1.0);
346 let leading = density.centre(52.0, 46.5);
347 assert_eq!(density.to_px(leading), 6.0);
348 }
349
350 #[test]
351 fn floor_and_ceil_move_whole_pixels_only() {
352 let density = Density::new(2.0, 1.0);
353 assert_eq!(density.floor(13.3), 13.0);
354 assert_eq!(density.ceil(13.3), 13.5);
355 assert_eq!(density.ceil(13.5), 13.5);
356 }
357
358 #[test]
359 fn a_nonsense_grid_falls_back_to_one_rather_than_dividing_by_zero() {
360 let density = Density::new(0.0, f32::NAN);
361 assert_eq!(density.density(), 1.0);
362 assert_eq!(density.font_scale(), 1.0);
363 assert_eq!(density.dp(3.7), 4.0);
364 }
365}