use-breakpoint 0.1.0

Viewport and container breakpoint primitives for RustUse UI
Documentation
#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]

/// Standard breakpoint labels.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub enum BreakpointName {
    Xs,
    Sm,
    Md,
    Lg,
    Xl,
    Xxl,
}

impl BreakpointName {
    pub fn as_str(self) -> &'static str {
        match self {
            Self::Xs => "xs",
            Self::Sm => "sm",
            Self::Md => "md",
            Self::Lg => "lg",
            Self::Xl => "xl",
            Self::Xxl => "xxl",
        }
    }
}

/// A named breakpoint threshold.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
pub struct Breakpoint {
    name: BreakpointName,
    min_width: u32,
}

impl Breakpoint {
    pub fn new(name: BreakpointName, min_width: u32) -> Self {
        Self { name, min_width }
    }

    pub fn name(self) -> BreakpointName {
        self.name
    }

    pub fn min_width(self) -> u32 {
        self.min_width
    }
}

/// Inclusive lower and exclusive upper range for a breakpoint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BreakpointRange {
    min_width: Option<u32>,
    max_width: Option<u32>,
}

impl BreakpointRange {
    pub fn new(min_width: Option<u32>, max_width: Option<u32>) -> Self {
        Self {
            min_width,
            max_width,
        }
    }

    pub fn contains(self, width: u32) -> bool {
        self.min_width.is_none_or(|minimum| width >= minimum)
            && self.max_width.is_none_or(|maximum| width < maximum)
    }
}

/// A collection of breakpoints ordered by threshold.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BreakpointSet {
    breakpoints: Vec<Breakpoint>,
}

impl BreakpointSet {
    pub fn new(breakpoints: Vec<Breakpoint>) -> Self {
        let mut set = Self { breakpoints };
        set.breakpoints
            .sort_by_key(|breakpoint| breakpoint.min_width);
        set
    }

    pub fn defaults() -> Self {
        Self::new(vec![
            Breakpoint::new(BreakpointName::Xs, 0),
            Breakpoint::new(BreakpointName::Sm, 480),
            Breakpoint::new(BreakpointName::Md, 768),
            Breakpoint::new(BreakpointName::Lg, 1024),
            Breakpoint::new(BreakpointName::Xl, 1280),
            Breakpoint::new(BreakpointName::Xxl, 1536),
        ])
    }

    pub fn breakpoints(&self) -> &[Breakpoint] {
        &self.breakpoints
    }

    pub fn matching(&self, width: u32) -> Option<Breakpoint> {
        self.breakpoints
            .iter()
            .copied()
            .filter(|breakpoint| width >= breakpoint.min_width)
            .max_by_key(|breakpoint| breakpoint.min_width)
    }
}

#[cfg(test)]
mod tests {
    use super::{Breakpoint, BreakpointName, BreakpointRange, BreakpointSet};

    #[test]
    fn matches_default_breakpoints() {
        let set = BreakpointSet::defaults();

        assert_eq!(
            set.matching(320).map(Breakpoint::name),
            Some(BreakpointName::Xs)
        );
        assert_eq!(
            set.matching(800).map(Breakpoint::name),
            Some(BreakpointName::Md)
        );
        assert_eq!(
            set.matching(1600).map(Breakpoint::name),
            Some(BreakpointName::Xxl)
        );
    }

    #[test]
    fn checks_breakpoint_ranges() {
        let range = BreakpointRange::new(Some(480), Some(768));

        assert!(range.contains(480));
        assert!(range.contains(767));
        assert!(!range.contains(768));
        assert_eq!(BreakpointName::Lg.as_str(), "lg");
    }
}