Skip to main content

use_breakpoint/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// Standard breakpoint labels.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
6pub enum BreakpointName {
7    Xs,
8    Sm,
9    Md,
10    Lg,
11    Xl,
12    Xxl,
13}
14
15impl BreakpointName {
16    pub fn as_str(self) -> &'static str {
17        match self {
18            Self::Xs => "xs",
19            Self::Sm => "sm",
20            Self::Md => "md",
21            Self::Lg => "lg",
22            Self::Xl => "xl",
23            Self::Xxl => "xxl",
24        }
25    }
26}
27
28/// A named breakpoint threshold.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
30pub struct Breakpoint {
31    name: BreakpointName,
32    min_width: u32,
33}
34
35impl Breakpoint {
36    pub fn new(name: BreakpointName, min_width: u32) -> Self {
37        Self { name, min_width }
38    }
39
40    pub fn name(self) -> BreakpointName {
41        self.name
42    }
43
44    pub fn min_width(self) -> u32 {
45        self.min_width
46    }
47}
48
49/// Inclusive lower and exclusive upper range for a breakpoint.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
51pub struct BreakpointRange {
52    min_width: Option<u32>,
53    max_width: Option<u32>,
54}
55
56impl BreakpointRange {
57    pub fn new(min_width: Option<u32>, max_width: Option<u32>) -> Self {
58        Self {
59            min_width,
60            max_width,
61        }
62    }
63
64    pub fn contains(self, width: u32) -> bool {
65        self.min_width.is_none_or(|minimum| width >= minimum)
66            && self.max_width.is_none_or(|maximum| width < maximum)
67    }
68}
69
70/// A collection of breakpoints ordered by threshold.
71#[derive(Debug, Clone, Default, PartialEq, Eq)]
72pub struct BreakpointSet {
73    breakpoints: Vec<Breakpoint>,
74}
75
76impl BreakpointSet {
77    pub fn new(breakpoints: Vec<Breakpoint>) -> Self {
78        let mut set = Self { breakpoints };
79        set.breakpoints
80            .sort_by_key(|breakpoint| breakpoint.min_width);
81        set
82    }
83
84    pub fn defaults() -> Self {
85        Self::new(vec![
86            Breakpoint::new(BreakpointName::Xs, 0),
87            Breakpoint::new(BreakpointName::Sm, 480),
88            Breakpoint::new(BreakpointName::Md, 768),
89            Breakpoint::new(BreakpointName::Lg, 1024),
90            Breakpoint::new(BreakpointName::Xl, 1280),
91            Breakpoint::new(BreakpointName::Xxl, 1536),
92        ])
93    }
94
95    pub fn breakpoints(&self) -> &[Breakpoint] {
96        &self.breakpoints
97    }
98
99    pub fn matching(&self, width: u32) -> Option<Breakpoint> {
100        self.breakpoints
101            .iter()
102            .copied()
103            .filter(|breakpoint| width >= breakpoint.min_width)
104            .max_by_key(|breakpoint| breakpoint.min_width)
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::{Breakpoint, BreakpointName, BreakpointRange, BreakpointSet};
111
112    #[test]
113    fn matches_default_breakpoints() {
114        let set = BreakpointSet::defaults();
115
116        assert_eq!(
117            set.matching(320).map(Breakpoint::name),
118            Some(BreakpointName::Xs)
119        );
120        assert_eq!(
121            set.matching(800).map(Breakpoint::name),
122            Some(BreakpointName::Md)
123        );
124        assert_eq!(
125            set.matching(1600).map(Breakpoint::name),
126            Some(BreakpointName::Xxl)
127        );
128    }
129
130    #[test]
131    fn checks_breakpoint_ranges() {
132        let range = BreakpointRange::new(Some(480), Some(768));
133
134        assert!(range.contains(480));
135        assert!(range.contains(767));
136        assert!(!range.contains(768));
137        assert_eq!(BreakpointName::Lg.as_str(), "lg");
138    }
139}