Skip to main content

frus_core/
responsive.rs

1//! Size classes (breakpoints) — the foundation of responsive behaviour.
2//!
3//! A `SizeClass` sorts a width (in **logical** pixels) into one of three bands, in
4//! the Material 3 style. Applications and responsive widgets use it to adapt their
5//! layout without hand-coding thresholds.
6
7/// A display-width band.
8///
9/// Thresholds (logical px): `Compact` < 600, `Medium` 600–840, `Expanded` ≥ 840.
10#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
11pub enum SizeClass {
12    /// Phone / narrow window (< 600).
13    Compact,
14    /// Portrait tablet / medium window (600–840).
15    Medium,
16    /// Desktop / wide window (≥ 840).
17    Expanded,
18}
19
20impl SizeClass {
21    /// Lower bound (inclusive) of the `Medium` band, in logical px.
22    pub const MEDIUM: f32 = 600.0;
23    /// Lower bound (inclusive) of the `Expanded` band, in logical px.
24    pub const EXPANDED: f32 = 840.0;
25
26    /// The class matching a width (logical px).
27    pub fn from_width(width: f32) -> SizeClass {
28        if width >= Self::EXPANDED {
29            SizeClass::Expanded
30        } else if width >= Self::MEDIUM {
31            SizeClass::Medium
32        } else {
33            SizeClass::Compact
34        }
35    }
36
37    /// The class matching a **height** (logical px), using the same thresholds.
38    ///
39    /// Useful on the vertical axis: a short window (< 600) is `Compact` in height,
40    /// which is a cue to hide labels, shrink margins, and so on.
41    pub fn from_height(height: f32) -> SizeClass {
42        Self::from_width(height)
43    }
44
45    /// Ordinal rank (0 = Compact … 2 = Expanded), handy for comparing bands.
46    pub fn rank(self) -> u8 {
47        match self {
48            SizeClass::Compact => 0,
49            SizeClass::Medium => 1,
50            SizeClass::Expanded => 2,
51        }
52    }
53}
54
55/// Display orientation, derived from the width-to-height ratio.
56///
57/// A second **axis** of responsiveness, independent of the size class: the same
58/// width can be portrait (phone held upright) or landscape (phone on its side),
59/// which sometimes calls for a different layout.
60#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum Orientation {
62    /// Taller than wide, or square: `height >= width`.
63    Portrait,
64    /// Wider than tall: `width > height`.
65    Landscape,
66}
67
68impl Orientation {
69    /// The orientation of a window of the given dimensions (logical px).
70    pub fn from_size(width: f32, height: f32) -> Orientation {
71        if width > height {
72            Orientation::Landscape
73        } else {
74            Orientation::Portrait
75        }
76    }
77
78    /// `true` when portrait (taller than wide).
79    pub fn is_portrait(self) -> bool {
80        self == Orientation::Portrait
81    }
82
83    /// `true` when landscape (wider than tall).
84    pub fn is_landscape(self) -> bool {
85        self == Orientation::Landscape
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn thresholds() {
95        assert_eq!(SizeClass::from_width(0.0), SizeClass::Compact);
96        assert_eq!(SizeClass::from_width(599.0), SizeClass::Compact);
97        assert_eq!(SizeClass::from_width(600.0), SizeClass::Medium);
98        assert_eq!(SizeClass::from_width(839.0), SizeClass::Medium);
99        assert_eq!(SizeClass::from_width(840.0), SizeClass::Expanded);
100        assert_eq!(SizeClass::from_width(1920.0), SizeClass::Expanded);
101    }
102
103    #[test]
104    fn ordering_by_rank() {
105        assert!(SizeClass::Compact < SizeClass::Medium);
106        assert!(SizeClass::Medium < SizeClass::Expanded);
107        assert_eq!(SizeClass::Expanded.rank(), 2);
108    }
109
110    #[test]
111    fn height_uses_same_thresholds() {
112        assert_eq!(SizeClass::from_height(500.0), SizeClass::Compact);
113        assert_eq!(SizeClass::from_height(700.0), SizeClass::Medium);
114        assert_eq!(SizeClass::from_height(900.0), SizeClass::Expanded);
115    }
116
117    #[test]
118    fn orientation_from_size() {
119        assert_eq!(Orientation::from_size(400.0, 800.0), Orientation::Portrait);
120        assert_eq!(Orientation::from_size(800.0, 400.0), Orientation::Landscape);
121        // Square → portrait (the `height >= width` convention).
122        assert_eq!(Orientation::from_size(500.0, 500.0), Orientation::Portrait);
123        assert!(Orientation::from_size(400.0, 800.0).is_portrait());
124        assert!(Orientation::from_size(800.0, 400.0).is_landscape());
125    }
126}