use cranpose_core::{compositionLocalOf, CompositionLocal, CompositionLocalProvider};
use crate::{
font_scale::FontScaleCurve,
render_state::{current_density, current_font_scale_curve},
round_scaling_list::round_to_px,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Density {
density: f32,
font_scale: FontScaleCurve,
}
impl Density {
pub fn from_host() -> Self {
if crate::render_state::has_current_app_context() {
Self::with_curve(current_density(), current_font_scale_curve())
} else {
Self::default()
}
}
pub fn new(density: f32, font_scale: f32) -> Self {
let font_scale = if font_scale.is_finite() && font_scale > 0.0 {
font_scale
} else {
1.0
};
Self::with_curve(density, FontScaleCurve::linear(font_scale))
}
pub fn with_curve(density: f32, font_scale: FontScaleCurve) -> Self {
Self {
density: if density.is_finite() && density > 0.0 {
density
} else {
1.0
},
font_scale,
}
}
pub fn density(self) -> f32 {
self.density
}
pub fn font_scale(self) -> f32 {
self.font_scale.scale()
}
pub fn font_scale_curve(self) -> FontScaleCurve {
self.font_scale
}
pub fn dp(self, value: f32) -> f32 {
round_to_px(value, self.density)
}
pub fn sp(self, value: f32) -> f32 {
self.dp(self.font_scale.sp_to_dp(value))
}
pub fn round(self, value: f32) -> f32 {
round_to_px(value, self.density)
}
pub fn floor(self, value: f32) -> f32 {
if value.is_finite() {
(value * self.density).floor() / self.density
} else {
value
}
}
pub fn ceil(self, value: f32) -> f32 {
if value.is_finite() {
(value * self.density).ceil() / self.density
} else {
value
}
}
pub fn to_px(self, points: f32) -> f32 {
points * self.density
}
pub fn centre(self, available: f32, content: f32) -> f32 {
self.round((available - content) * 0.5)
}
}
impl Default for Density {
fn default() -> Self {
Self::new(1.0, 1.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DensityMeasureScope {
density: Density,
}
impl DensityMeasureScope {
pub fn new(density: Density) -> Self {
Self { density }
}
}
impl cranpose_ui_layout::MeasureScope for DensityMeasureScope {
fn density(&self) -> f32 {
self.density.density()
}
fn font_scale(&self) -> f32 {
self.density.font_scale()
}
}
pub fn local_density() -> CompositionLocal<Density> {
thread_local! {
static LOCAL: std::cell::RefCell<Option<CompositionLocal<Density>>> =
const { std::cell::RefCell::new(None) };
}
LOCAL.with(|cell| {
cell.borrow_mut()
.get_or_insert_with(|| compositionLocalOf(Density::from_host))
.clone()
})
}
pub fn density() -> Density {
local_density().current()
}
#[allow(non_snake_case)]
pub fn ProvideDensity(density: Density, content: impl FnOnce()) {
CompositionLocalProvider(vec![local_density().provides(density)], content);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_provided_density_reaches_the_content_and_ends_with_it() {
use std::{cell::Cell, rc::Rc};
use cranpose_core::{location_key, Composition, MemoryApplier};
let mut composition = Composition::new(MemoryApplier::new());
let outer = Rc::new(Cell::new(0.0_f32));
let inside = Rc::new(Cell::new(0.0_f32));
let after = Rc::new(Cell::new(0.0_f32));
let key = location_key(file!(), line!(), column!());
{
let (outer, inside, after) = (Rc::clone(&outer), Rc::clone(&inside), Rc::clone(&after));
let mut render = move || {
outer.set(density().density());
ProvideDensity(Density::new(3.0, 1.0), || inside.set(density().density()));
after.set(density().density());
};
composition.render(key, &mut render).expect("render");
}
assert_eq!(inside.get(), 3.0, "the subtree reads the grid it was given");
assert_ne!(
outer.get(),
3.0,
"the provision does not escape upwards out of its content"
);
assert_eq!(
after.get(),
outer.get(),
"nor does it leak into what follows it"
);
}
#[test]
fn a_dp_length_lands_on_a_whole_device_pixel() {
let density = Density::new(2.0, 1.0);
assert_eq!(density.dp(13.3), 13.5);
assert_eq!(density.to_px(density.dp(13.3)), 27.0);
assert_eq!(density.dp(0.25), 0.5);
}
#[test]
fn a_text_size_moves_with_the_font_scale_and_a_dp_length_does_not() {
let density = Density::new(2.0, 1.24);
assert_eq!(density.dp(15.0), 15.0);
assert_eq!(density.to_px(density.sp(15.0)), 37.0);
}
#[test]
fn centring_puts_the_leading_gap_on_a_pixel_boundary() {
let density = Density::new(2.0, 1.0);
let leading = density.centre(52.0, 46.5);
assert_eq!(density.to_px(leading), 6.0);
}
#[test]
fn floor_and_ceil_move_whole_pixels_only() {
let density = Density::new(2.0, 1.0);
assert_eq!(density.floor(13.3), 13.0);
assert_eq!(density.ceil(13.3), 13.5);
assert_eq!(density.ceil(13.5), 13.5);
}
#[test]
fn a_nonsense_grid_falls_back_to_one_rather_than_dividing_by_zero() {
let density = Density::new(0.0, f32::NAN);
assert_eq!(density.density(), 1.0);
assert_eq!(density.font_scale(), 1.0);
assert_eq!(density.dp(3.7), 4.0);
}
}