use std::ops::{Add, Div, Mul, Neg, Sub};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Density {
pub scale: f32,
pub font_scale: f32,
}
impl Density {
pub const IDENTITY: Self = Self {
scale: 1.0,
font_scale: 1.0,
};
pub fn new(scale: f32, font_scale: f32) -> Self {
Self { scale, font_scale }
}
pub fn from_scale(scale: f32) -> Self {
Self::new(scale, 1.0)
}
}
impl Default for Density {
fn default() -> Self {
Self::IDENTITY
}
}
macro_rules! scalar_unit {
($name:ident, $doc:literal) => {
#[doc = $doc]
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
pub struct $name(pub f32);
impl $name {
pub const ZERO: Self = Self(0.0);
pub fn max(self, other: Self) -> Self {
Self(self.0.max(other.0))
}
pub fn min(self, other: Self) -> Self {
Self(self.0.min(other.0))
}
}
impl From<f32> for $name {
fn from(value: f32) -> Self {
Self(value)
}
}
impl From<i32> for $name {
fn from(value: i32) -> Self {
Self(value as f32)
}
}
impl Add for $name {
type Output = Self;
fn add(self, rhs: Self) -> Self {
Self(self.0 + rhs.0)
}
}
impl Sub for $name {
type Output = Self;
fn sub(self, rhs: Self) -> Self {
Self(self.0 - rhs.0)
}
}
impl Neg for $name {
type Output = Self;
fn neg(self) -> Self {
Self(-self.0)
}
}
impl Mul<f32> for $name {
type Output = Self;
fn mul(self, rhs: f32) -> Self {
Self(self.0 * rhs)
}
}
impl Div<f32> for $name {
type Output = Self;
fn div(self, rhs: f32) -> Self {
Self(self.0 / rhs)
}
}
};
}
scalar_unit!(
Dp,
"Density-independent pixels: a layout length that reads the same on \
every device regardless of its pixel density."
);
scalar_unit!(
Sp,
"Scale-independent pixels: a text size that follows both the device's \
pixel density and the user's font-scale accessibility setting. \
Distinct from `Dp` because a length must not move when the user's \
text-size setting does, and a text size must."
);
scalar_unit!(
Px,
"A device pixel. Renderer-facing surfaces, hit-testing, and \
framebuffer coordinates read and write this; a layout author reaches \
for `Dp`/`Sp` instead, and can only get here through an explicit \
`Density`."
);
impl Dp {
pub fn to_px(self, density: Density) -> Px {
Px(self.0 * density.scale)
}
pub fn from_px(px: Px, density: Density) -> Self {
Self(px.0 / density.scale)
}
}
impl Sp {
pub fn to_px(self, density: Density) -> Px {
Px(self.0 * density.scale * density.font_scale)
}
pub fn from_px(px: Px, density: Density) -> Self {
Self(px.0 / (density.scale * density.font_scale))
}
}
impl Px {
pub fn to_dp(self, density: Density) -> Dp {
Dp(self.0 / density.scale)
}
}
pub trait DpExt {
fn dp(self) -> Dp;
}
impl DpExt for f32 {
fn dp(self) -> Dp {
Dp(self)
}
}
impl DpExt for i32 {
fn dp(self) -> Dp {
Dp(self as f32)
}
}
pub trait SpExt {
fn sp(self) -> Sp;
}
impl SpExt for f32 {
fn sp(self) -> Sp {
Sp(self)
}
}
impl SpExt for i32 {
fn sp(self) -> Sp {
Sp(self as f32)
}
}
pub trait PxExt {
fn px(self) -> Px;
}
impl PxExt for f32 {
fn px(self) -> Px {
Px(self)
}
}
impl PxExt for i32 {
fn px(self) -> Px {
Px(self as f32)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn density_independent_pixels_round_trip_through_a_density() {
for scale in [1.0f32, 1.5, 2.0, 3.0] {
let density = Density::from_scale(scale);
let dp = Dp(24.0);
assert_eq!(dp.to_px(density), Px(24.0 * scale));
assert_eq!(Dp::from_px(dp.to_px(density), density), dp);
}
}
#[test]
fn a_dp_length_is_pinned_at_a_non_identity_density() {
let density = Density::from_scale(2.5);
assert_eq!(Dp(16.0).to_px(density), Px(40.0));
assert_eq!(Px(40.0).to_dp(density), Dp(16.0));
}
#[test]
fn scale_independent_pixels_round_trip_through_density_and_font_scale() {
for scale in [1.0f32, 2.0, 3.0] {
for font_scale in [0.85f32, 1.0, 1.3] {
let density = Density::new(scale, font_scale);
let sp = Sp(16.0);
assert_eq!(sp.to_px(density), Px(16.0 * scale * font_scale));
assert_eq!(Sp::from_px(sp.to_px(density), density), sp);
}
}
}
#[test]
fn an_sp_length_is_pinned_at_a_non_identity_density_and_font_scale() {
let density = Density::new(2.0, 1.25);
assert_eq!(Sp(16.0).to_px(density), Px(40.0));
}
#[test]
fn dp_arithmetic_matches_plain_float_arithmetic() {
assert_eq!(Dp(4.0) + Dp(2.0), Dp(6.0));
assert_eq!(Dp(4.0) - Dp(2.0), Dp(2.0));
assert_eq!(Dp(4.0) * 2.5, Dp(10.0));
assert_eq!(Dp(10.0) / 4.0, Dp(2.5));
assert_eq!(-Dp(4.0), Dp(-4.0));
assert_eq!(Dp(4.0).max(Dp(9.0)), Dp(9.0));
assert_eq!(Dp(4.0).min(Dp(9.0)), Dp(4.0));
}
#[test]
fn literal_and_extension_constructors_agree() {
assert_eq!(Dp::from(16.0f32), Dp(16.0));
assert_eq!(Dp::from(16), Dp(16.0));
assert_eq!(16.0.dp(), Dp(16.0));
assert_eq!(16.sp(), Sp(16.0));
assert_eq!(16.px(), Px(16.0));
}
#[test]
fn into_dp_accepts_a_bare_literal_and_an_explicit_conversion() {
fn padding(value: impl Into<Dp>) -> Dp {
value.into()
}
assert_eq!(padding(16.0), Dp(16.0));
assert_eq!(padding(16), Dp(16.0));
assert_eq!(padding(16.0.dp()), Dp(16.0));
}
}