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() {
55 Self::with_curve(current_density(), current_font_scale_curve())
56 } else {
57 Self::default()
58 }
59 }
60
61 pub fn new(density: f32, font_scale: f32) -> Self {
65 let font_scale = if font_scale.is_finite() && font_scale > 0.0 {
66 font_scale
67 } else {
68 1.0
69 };
70 Self::with_curve(density, FontScaleCurve::linear(font_scale))
71 }
72
73 pub fn with_curve(density: f32, font_scale: FontScaleCurve) -> Self {
76 Self {
77 density: if density.is_finite() && density > 0.0 {
78 density
79 } else {
80 1.0
81 },
82 font_scale,
83 }
84 }
85
86 pub fn density(self) -> f32 {
88 self.density
89 }
90
91 pub fn font_scale(self) -> f32 {
93 self.font_scale.scale()
94 }
95
96 pub fn font_scale_curve(self) -> FontScaleCurve {
98 self.font_scale
99 }
100
101 pub fn dp(self, value: f32) -> f32 {
104 round_to_px(value, self.density)
105 }
106
107 pub fn sp(self, value: f32) -> f32 {
113 self.dp(self.font_scale.sp_to_dp(value))
114 }
115
116 pub fn round(self, value: f32) -> f32 {
118 round_to_px(value, self.density)
119 }
120
121 pub fn floor(self, value: f32) -> f32 {
123 if value.is_finite() {
124 (value * self.density).floor() / self.density
125 } else {
126 value
127 }
128 }
129
130 pub fn ceil(self, value: f32) -> f32 {
135 if value.is_finite() {
136 (value * self.density).ceil() / self.density
137 } else {
138 value
139 }
140 }
141
142 pub fn to_px(self, points: f32) -> f32 {
144 points * self.density
145 }
146
147 pub fn centre(self, available: f32, content: f32) -> f32 {
155 self.round((available - content) * 0.5)
156 }
157}
158
159impl Default for Density {
160 fn default() -> Self {
161 Self::new(1.0, 1.0)
162 }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq)]
175pub struct DensityMeasureScope {
176 density: Density,
177}
178
179impl DensityMeasureScope {
180 pub fn new(density: Density) -> Self {
182 Self { density }
183 }
184}
185
186impl cranpose_ui_layout::MeasureScope for DensityMeasureScope {
187 fn density(&self) -> f32 {
188 self.density.density()
189 }
190
191 fn font_scale(&self) -> f32 {
192 self.density.font_scale()
193 }
194}
195
196pub fn local_density() -> CompositionLocal<Density> {
203 thread_local! {
204 static LOCAL: std::cell::RefCell<Option<CompositionLocal<Density>>> =
205 const { std::cell::RefCell::new(None) };
206 }
207 LOCAL.with(|cell| {
208 cell.borrow_mut()
209 .get_or_insert_with(|| compositionLocalOf(Density::from_host))
210 .clone()
211 })
212}
213
214pub fn density() -> Density {
216 local_density().current()
217}
218
219#[allow(non_snake_case)]
225#[track_caller]
226pub fn ProvideDensity(density: Density, content: impl FnOnce()) {
227 CompositionLocalProvider(vec![local_density().provides(density)], content);
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn a_surviving_density_scope_keeps_its_entry_when_a_sibling_leaves() {
236 use std::{cell::Cell, rc::Rc};
237
238 use cranpose_core::{Composition, MemoryApplier, MutableState, location_key};
239
240 let mut composition = Composition::new(MemoryApplier::new());
241 let runtime = composition.runtime_handle();
242 let with_leading = MutableState::with_runtime(true, runtime.clone());
243 let survivor = MutableState::with_runtime(3.0_f32, runtime);
244 let seen = Rc::new(Cell::new(0.0_f32));
245
246 #[cranpose_macros::composable]
247 #[allow(non_snake_case)]
248 fn Reader(seen: Rc<Cell<f32>>) {
249 seen.set(density().density());
250 }
251
252 fn tree(with_leading: bool, survivor: f32, seen: Rc<Cell<f32>>) {
253 if with_leading {
254 ProvideDensity(Density::new(2.0, 1.0), || {});
255 }
256 ProvideDensity(Density::new(survivor, 1.0), move || Reader(seen));
257 }
258
259 #[cranpose_macros::composable]
260 #[allow(non_snake_case)]
261 fn Panel(
262 with_leading: MutableState<bool>,
263 survivor: MutableState<f32>,
264 seen: Rc<Cell<f32>>,
265 ) {
266 tree(with_leading.value(), survivor.value(), seen);
267 }
268
269 let key = location_key(file!(), line!(), column!());
270 {
271 let seen = Rc::clone(&seen);
272 composition
273 .render(key, move || Panel(with_leading, survivor, Rc::clone(&seen)))
274 .expect("initial composition");
275 }
276 assert_eq!(seen.get(), 3.0);
277
278 with_leading.set_value(false);
279 while composition
280 .process_invalid_scopes()
281 .expect("drop the leading density scope")
282 {}
283 survivor.set_value(4.0);
284 while composition
285 .process_invalid_scopes()
286 .expect("change the surviving density")
287 {}
288 assert_eq!(
289 seen.get(),
290 4.0,
291 "the surviving density scope must keep its entry and its reader's \
292 subscription when the sibling leaves"
293 );
294 }
295
296 #[test]
300 fn a_provided_density_reaches_the_content_and_ends_with_it() {
301 use std::{cell::Cell, rc::Rc};
302
303 use cranpose_core::{Composition, MemoryApplier, location_key};
304
305 let mut composition = Composition::new(MemoryApplier::new());
306 let outer = Rc::new(Cell::new(0.0_f32));
307 let inside = Rc::new(Cell::new(0.0_f32));
308 let after = Rc::new(Cell::new(0.0_f32));
309
310 let key = location_key(file!(), line!(), column!());
311 {
312 let (outer, inside, after) = (Rc::clone(&outer), Rc::clone(&inside), Rc::clone(&after));
313 let mut render = move || {
314 outer.set(density().density());
315 ProvideDensity(Density::new(3.0, 1.0), || inside.set(density().density()));
316 after.set(density().density());
317 };
318 composition.render(key, &mut render).expect("render");
319 }
320
321 assert_eq!(inside.get(), 3.0, "the subtree reads the grid it was given");
322 assert_ne!(
323 outer.get(),
324 3.0,
325 "the provision does not escape upwards out of its content"
326 );
327 assert_eq!(
328 after.get(),
329 outer.get(),
330 "nor does it leak into what follows it"
331 );
332 }
333
334 #[test]
335 fn a_dp_length_lands_on_a_whole_device_pixel() {
336 let density = Density::new(2.0, 1.0);
337 assert_eq!(density.dp(13.3), 13.5);
338 assert_eq!(density.to_px(density.dp(13.3)), 27.0);
339 assert_eq!(density.dp(0.25), 0.5);
341 }
342
343 #[test]
344 fn a_text_size_moves_with_the_font_scale_and_a_dp_length_does_not() {
345 let density = Density::new(2.0, 1.24);
346 assert_eq!(density.dp(15.0), 15.0);
347 assert_eq!(density.to_px(density.sp(15.0)), 37.0);
349 }
350
351 #[test]
352 fn centring_puts_the_leading_gap_on_a_pixel_boundary() {
353 let density = Density::new(2.0, 1.0);
354 let leading = density.centre(52.0, 46.5);
357 assert_eq!(density.to_px(leading), 6.0);
358 }
359
360 #[test]
361 fn floor_and_ceil_move_whole_pixels_only() {
362 let density = Density::new(2.0, 1.0);
363 assert_eq!(density.floor(13.3), 13.0);
364 assert_eq!(density.ceil(13.3), 13.5);
365 assert_eq!(density.ceil(13.5), 13.5);
366 }
367
368 #[test]
369 fn a_nonsense_grid_falls_back_to_one_rather_than_dividing_by_zero() {
370 let density = Density::new(0.0, f32::NAN);
371 assert_eq!(density.density(), 1.0);
372 assert_eq!(density.font_scale(), 1.0);
373 assert_eq!(density.dp(3.7), 4.0);
374 }
375}