Skip to main content

guise/layout/
breakpoint.rs

1//! Responsive breakpoints: window width → a token, and per-breakpoint values.
2//!
3//! Desktop windows resize like browser windows; these helpers make layout
4//! decisions declarative. `Breakpoint::from_window(window)` during render,
5//! then either branch on it or resolve a [`Responsive`] value.
6//!
7//! ```ignore
8//! let bp = Breakpoint::from_window(window);
9//! let columns = Responsive::new(1).md(2).xl(4).resolve(bp);
10//! SimpleGrid::new(columns).children(cards)
11//! ```
12
13use gpui::Window;
14
15/// Width class thresholds (px), Tailwind-flavored: `Sm` 640, `Md` 768,
16/// `Lg` 1024, `Xl` 1280. Anything narrower is `Xs`.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum Breakpoint {
19    Xs,
20    Sm,
21    Md,
22    Lg,
23    Xl,
24}
25
26impl Breakpoint {
27    /// The breakpoint for a width in px.
28    pub fn of(width: f32) -> Breakpoint {
29        match width {
30            w if w >= 1280.0 => Breakpoint::Xl,
31            w if w >= 1024.0 => Breakpoint::Lg,
32            w if w >= 768.0 => Breakpoint::Md,
33            w if w >= 640.0 => Breakpoint::Sm,
34            _ => Breakpoint::Xs,
35        }
36    }
37
38    /// The breakpoint for the window's current viewport width.
39    pub fn from_window(window: &Window) -> Breakpoint {
40        Breakpoint::of(f32::from(window.viewport_size().width))
41    }
42
43    /// Mobile-first comparison: `bp.at_least(Breakpoint::Md)` reads like
44    /// CSS `min-width: 768px`.
45    pub fn at_least(self, other: Breakpoint) -> bool {
46        self >= other
47    }
48}
49
50/// A value that changes at breakpoints, mobile-first: the base applies from
51/// `Xs` up, and each override kicks in at its breakpoint and above.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub struct Responsive<T> {
54    base: T,
55    sm: Option<T>,
56    md: Option<T>,
57    lg: Option<T>,
58    xl: Option<T>,
59}
60
61impl<T: Clone> Responsive<T> {
62    /// The value below the first override.
63    pub fn new(base: T) -> Self {
64        Responsive {
65            base,
66            sm: None,
67            md: None,
68            lg: None,
69            xl: None,
70        }
71    }
72
73    pub fn sm(mut self, value: T) -> Self {
74        self.sm = Some(value);
75        self
76    }
77
78    pub fn md(mut self, value: T) -> Self {
79        self.md = Some(value);
80        self
81    }
82
83    pub fn lg(mut self, value: T) -> Self {
84        self.lg = Some(value);
85        self
86    }
87
88    pub fn xl(mut self, value: T) -> Self {
89        self.xl = Some(value);
90        self
91    }
92
93    /// The value in effect at `bp`: the largest override at or below it,
94    /// else the base.
95    pub fn resolve(&self, bp: Breakpoint) -> T {
96        let ladder = [
97            (Breakpoint::Xl, &self.xl),
98            (Breakpoint::Lg, &self.lg),
99            (Breakpoint::Md, &self.md),
100            (Breakpoint::Sm, &self.sm),
101        ];
102        for (level, value) in ladder {
103            if bp.at_least(level) {
104                if let Some(v) = value {
105                    return v.clone();
106                }
107            }
108        }
109        self.base.clone()
110    }
111
112    /// Shorthand: resolve against the window's current width.
113    pub fn for_window(&self, window: &Window) -> T {
114        self.resolve(Breakpoint::from_window(window))
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn widths_map_to_breakpoints() {
124        assert_eq!(Breakpoint::of(0.0), Breakpoint::Xs);
125        assert_eq!(Breakpoint::of(639.9), Breakpoint::Xs);
126        assert_eq!(Breakpoint::of(640.0), Breakpoint::Sm);
127        assert_eq!(Breakpoint::of(767.0), Breakpoint::Sm);
128        assert_eq!(Breakpoint::of(768.0), Breakpoint::Md);
129        assert_eq!(Breakpoint::of(1024.0), Breakpoint::Lg);
130        assert_eq!(Breakpoint::of(1280.0), Breakpoint::Xl);
131        assert_eq!(Breakpoint::of(3840.0), Breakpoint::Xl);
132    }
133
134    #[test]
135    fn at_least_is_mobile_first() {
136        assert!(Breakpoint::Lg.at_least(Breakpoint::Md));
137        assert!(Breakpoint::Md.at_least(Breakpoint::Md));
138        assert!(!Breakpoint::Sm.at_least(Breakpoint::Md));
139    }
140
141    #[test]
142    fn responsive_resolves_the_nearest_override_below() {
143        let cols = Responsive::new(1).md(2).xl(4);
144        assert_eq!(cols.resolve(Breakpoint::Xs), 1);
145        assert_eq!(cols.resolve(Breakpoint::Sm), 1);
146        assert_eq!(cols.resolve(Breakpoint::Md), 2);
147        assert_eq!(cols.resolve(Breakpoint::Lg), 2); // inherits md
148        assert_eq!(cols.resolve(Breakpoint::Xl), 4);
149    }
150
151    #[test]
152    fn base_only_is_constant() {
153        let pad = Responsive::new("tight");
154        assert_eq!(pad.resolve(Breakpoint::Xs), "tight");
155        assert_eq!(pad.resolve(Breakpoint::Xl), "tight");
156    }
157}