cranpose-ui-graphics 0.1.162

Pure math/data for drawing & units in Cranpose
Documentation
//! Length units: [`Dp`], [`Sp`], [`Px`], and the [`Density`] that converts
//! between them.
//!
//! This mirrors Jetpack Compose's `Dp`/`TextUnit`/pixel split: a layout
//! author reasons in [`Dp`] (density-independent) and [`Sp`] (scale-
//! independent, for text), never in a raw device pixel count, and the only
//! way from either into [`Px`] is through an explicit [`Density`]. Neither
//! [`Dp`] nor [`Sp`] implements `From<Px>` (nor the reverse for [`Dp`]/
//! [`Sp`] into [`Px`] without a density), so a call site cannot hand a
//! pixel count to a `Dp`-typed parameter and have it silently compile:
//!
//! ```compile_fail
//! use cranpose_ui_graphics::{Dp, Px};
//!
//! fn takes_dp(_padding: impl Into<Dp>) {}
//!
//! let measured = Px(16.0);
//! takes_dp(measured); // Px has no `Into<Dp>` impl -- this does not compile.
//! ```
//!
//! Going the other way requires stating the grid:
//!
//! ```
//! use cranpose_ui_graphics::{Density, Dp};
//!
//! let padding = Dp(16.0);
//! let px = padding.to_px(Density::from_scale(2.0));
//! assert_eq!(px.0, 32.0);
//! ```

use std::ops::{Add, Div, Mul, Neg, Sub};

/// The device pixel grid a [`Dp`] or [`Sp`] value is measured against: how
/// many device pixels one [`Dp`] is worth, and how far the platform's text-
/// size setting scales an [`Sp`] beyond that.
///
/// This is the pure-data half of Compose's `Density`. The composition-aware
/// wrapper that reads the ambient grid from a running app and reacts to
/// [`CompositionLocalProvider`](https://docs.rs/cranpose-core)-style
/// overrides lives above this crate (`cranpose-ui`'s `Density`, which cannot
/// live here without an upward dependency on `cranpose-ui`) and converts
/// into this type at the boundary.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Density {
    /// Device pixels per [`Dp`].
    pub scale: f32,
    /// The user's text-size setting, applied on top of `scale` for [`Sp`].
    pub font_scale: f32,
}

impl Density {
    /// One layout point per device pixel, no extra text scaling -- the grid
    /// a test or a golden reaches for when the host's real grid does not
    /// matter to what it is checking.
    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 }
    }

    /// A grid whose text does not follow a separate font-scale setting --
    /// what a caller converting a length rather than a text size reaches
    /// for, since a length must not move when the user's text size does.
    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);

            /// The larger of two lengths in the same unit. Comparing across
            /// units (a `Dp` against an `Sp`) is exactly the mistake this
            /// module exists to make impossible, so this does not accept one.
            pub fn max(self, other: Self) -> Self {
                Self(self.0.max(other.0))
            }

            /// The smaller of two lengths in the same unit.
            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 {
    /// Converts to the actual device pixel count on `density`'s grid.
    pub fn to_px(self, density: Density) -> Px {
        Px(self.0 * density.scale)
    }

    /// Recovers the `Dp` that produced `px` on `density`'s grid.
    pub fn from_px(px: Px, density: Density) -> Self {
        Self(px.0 / density.scale)
    }
}

impl Sp {
    /// Converts to the actual device pixel count on `density`'s grid,
    /// scaled by the user's font-scale setting on top of pixel density.
    pub fn to_px(self, density: Density) -> Px {
        Px(self.0 * density.scale * density.font_scale)
    }

    /// Recovers the `Sp` that produced `px` on `density`'s grid.
    pub fn from_px(px: Px, density: Density) -> Self {
        Self(px.0 / (density.scale * density.font_scale))
    }
}

impl Px {
    /// Recovers the layout length that produced this pixel count on
    /// `density`'s grid.
    pub fn to_dp(self, density: Density) -> Dp {
        Dp(self.0 / density.scale)
    }
}

/// Reads `16.0.dp()` at a call site rather than `Dp(16.0)`, matching
/// Kotlin's `Float.dp`/`Int.dp` extension properties.
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)
    }
}

/// Reads `16.0.sp()` at a call site rather than `Sp(16.0)`.
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)
    }
}

/// Reads `16.0.px()` at a call site rather than `Px(16.0)`.
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)]
#[path = "tests/unit_tests.rs"]
mod tests;