Skip to main content

gpui_rhai/
responsive.rs

1use std::collections::BTreeMap;
2
3use serde::{Deserialize, Serialize};
4use thiserror::Error;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum ViewportClass {
9    Compact,
10    Regular,
11    Wide,
12}
13
14impl ViewportClass {
15    #[must_use]
16    pub const fn as_str(self) -> &'static str {
17        match self {
18            Self::Compact => "compact",
19            Self::Regular => "regular",
20            Self::Wide => "wide",
21        }
22    }
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
26pub struct ViewportBreakpoints {
27    compact_max: f64,
28    regular_max: f64,
29}
30
31impl ViewportBreakpoints {
32    /// Create increasing finite viewport boundaries in logical pixels.
33    ///
34    /// # Errors
35    ///
36    /// Returns [`ResponsiveError::InvalidBreakpoints`] for non-positive,
37    /// non-finite, or non-increasing values.
38    pub fn new(compact_max: f64, regular_max: f64) -> Result<Self, ResponsiveError> {
39        if compact_max.is_finite()
40            && regular_max.is_finite()
41            && compact_max > 0.0
42            && regular_max > compact_max
43        {
44            Ok(Self {
45                compact_max,
46                regular_max,
47            })
48        } else {
49            Err(ResponsiveError::InvalidBreakpoints {
50                compact_max,
51                regular_max,
52            })
53        }
54    }
55
56    /// Classify a finite non-negative viewport width.
57    ///
58    /// # Errors
59    ///
60    /// Returns [`ResponsiveError::InvalidWidth`] for invalid geometry.
61    pub fn classify(self, width: f64) -> Result<ViewportClass, ResponsiveError> {
62        if !width.is_finite() || width < 0.0 {
63            return Err(ResponsiveError::InvalidWidth(width));
64        }
65        Ok(if width <= self.compact_max {
66            ViewportClass::Compact
67        } else if width <= self.regular_max {
68            ViewportClass::Regular
69        } else {
70            ViewportClass::Wide
71        })
72    }
73
74    #[must_use]
75    pub const fn compact_max(self) -> f64 {
76        self.compact_max
77    }
78
79    #[must_use]
80    pub const fn regular_max(self) -> f64 {
81        self.regular_max
82    }
83}
84
85impl Default for ViewportBreakpoints {
86    fn default() -> Self {
87        Self {
88            compact_max: 600.0,
89            regular_max: 1000.0,
90        }
91    }
92}
93
94#[derive(Clone, Debug, Default)]
95pub struct ResponsiveRuntime {
96    breakpoints: ViewportBreakpoints,
97    windows: BTreeMap<String, ViewportClass>,
98}
99
100impl ResponsiveRuntime {
101    #[must_use]
102    pub fn new(breakpoints: ViewportBreakpoints) -> Self {
103        Self {
104            breakpoints,
105            windows: BTreeMap::new(),
106        }
107    }
108
109    /// Update one window and report whether its discrete class changed.
110    ///
111    /// # Errors
112    ///
113    /// Returns invalid-width errors.
114    pub fn update_window(&mut self, window: &str, width: f64) -> Result<bool, ResponsiveError> {
115        let class = self.breakpoints.classify(width)?;
116        Ok(self.windows.insert(window.to_owned(), class) != Some(class))
117    }
118
119    /// Check a width without mutating the committed viewport class.
120    ///
121    /// # Errors
122    ///
123    /// Returns [`ResponsiveError::InvalidWidth`] for invalid geometry.
124    pub fn would_update_window(&self, window: &str, width: f64) -> Result<bool, ResponsiveError> {
125        let class = self.breakpoints.classify(width)?;
126        Ok(self.windows.get(window).copied() != Some(class))
127    }
128
129    #[must_use]
130    pub fn class(&self, window: &str) -> ViewportClass {
131        self.windows
132            .get(window)
133            .copied()
134            .unwrap_or(ViewportClass::Regular)
135    }
136
137    pub fn remove_window(&mut self, window: &str) -> bool {
138        self.windows.remove(window).is_some()
139    }
140
141    #[must_use]
142    pub const fn breakpoints(&self) -> ViewportBreakpoints {
143        self.breakpoints
144    }
145}
146
147#[derive(Clone, Copy, Debug, Error, PartialEq)]
148pub enum ResponsiveError {
149    #[error(
150        "viewport breakpoints must be finite, positive, and increasing; got {compact_max}, {regular_max}"
151    )]
152    InvalidBreakpoints { compact_max: f64, regular_max: f64 },
153    #[error("viewport width must be finite and non-negative, got {0}")]
154    InvalidWidth(f64),
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn discrete_classes_change_only_at_configured_boundaries() {
163        let breakpoints = ViewportBreakpoints::new(500.0, 900.0).unwrap();
164        let mut runtime = ResponsiveRuntime::new(breakpoints);
165        assert!(runtime.update_window("main", 480.0).unwrap());
166        assert!(!runtime.update_window("main", 499.0).unwrap());
167        assert!(runtime.update_window("main", 700.0).unwrap());
168        assert_eq!(runtime.class("main"), ViewportClass::Regular);
169        assert!(runtime.update_window("main", 1200.0).unwrap());
170        assert_eq!(runtime.class("main"), ViewportClass::Wide);
171    }
172}