Skip to main content

sequoia_view/
responsive.rs

1use std::rc::Rc;
2
3use sequoia_core::{Length, Size};
4use sequoia_layout::{LayoutCtx, LayoutStyle};
5use sequoia_reactive::Signal;
6
7use crate::any::AnyView;
8use crate::runtime::{ElementId, Runtime};
9use crate::view::View;
10use crate::views::Unit;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13#[non_exhaustive]
14pub enum Breakpoint {
15    #[default]
16    Compact,
17
18    Medium,
19
20    Expanded,
21}
22
23impl Breakpoint {
24    pub const COMPACT_MAX: f32 = 600.0;
25
26    pub const MEDIUM_MAX: f32 = 840.0;
27
28    pub const LARGE: f32 = 1200.0;
29
30    pub fn of(width: f32) -> Self {
31        Breakpoints::MATERIAL.of(width)
32    }
33
34    pub fn of_size(size: Size) -> Self {
35        Self::of(size.width)
36    }
37
38    pub fn is_compact(self) -> bool {
39        matches!(self, Self::Compact)
40    }
41
42    pub fn is_medium(self) -> bool {
43        matches!(self, Self::Medium)
44    }
45
46    pub fn is_expanded(self) -> bool {
47        matches!(self, Self::Expanded)
48    }
49
50    pub fn is_wide(size: Size) -> bool {
51        size.width >= Self::LARGE
52    }
53}
54
55#[derive(Clone, Copy, Debug, PartialEq)]
56pub struct Breakpoints {
57    pub compact_max: f32,
58    pub medium_max: f32,
59}
60
61impl Breakpoints {
62    pub const MATERIAL: Self = Self {
63        compact_max: Breakpoint::COMPACT_MAX,
64        medium_max: Breakpoint::MEDIUM_MAX,
65    };
66
67    pub const fn new(compact_max: f32, medium_max: f32) -> Self {
68        Self {
69            compact_max,
70            medium_max,
71        }
72    }
73
74    pub fn of(self, width: f32) -> Breakpoint {
75        if width < self.compact_max {
76            Breakpoint::Compact
77        } else if width < self.medium_max {
78            Breakpoint::Medium
79        } else {
80            Breakpoint::Expanded
81        }
82    }
83
84    pub fn of_size(self, size: Size) -> Breakpoint {
85        self.of(size.width)
86    }
87}
88
89impl Default for Breakpoints {
90    fn default() -> Self {
91        Self::MATERIAL
92    }
93}
94
95#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
96#[non_exhaustive]
97pub enum ScreenOrientation {
98    Portrait,
99    Landscape,
100    #[default]
101    Square,
102}
103
104pub fn screen_orientation_of(size: Size) -> ScreenOrientation {
105    if size.width > size.height {
106        ScreenOrientation::Landscape
107    } else if size.height > size.width {
108        ScreenOrientation::Portrait
109    } else {
110        ScreenOrientation::Square
111    }
112}
113
114pub fn orientation_of(size: Size) -> ScreenOrientation {
115    screen_orientation_of(size)
116}
117
118pub struct Responsive {
119    window: Signal<Size>,
120    build: Rc<dyn Fn(Size) -> AnyView>,
121    style: LayoutStyle,
122}
123
124impl Responsive {
125    pub fn new(window: Signal<Size>, build: impl Fn(Size) -> AnyView + 'static) -> Self {
126        Self {
127            window,
128            build: Rc::new(build),
129            style: LayoutStyle::default(),
130        }
131    }
132
133    pub fn window(rt: &Runtime, build: impl Fn(Size) -> AnyView + 'static) -> Self {
134        Self::new(rt.window_size(), build)
135    }
136
137    pub fn width(mut self, width: impl Into<Length>) -> Self {
138        self.style = self.style.width(width.into());
139        self
140    }
141
142    pub fn height(mut self, height: impl Into<Length>) -> Self {
143        self.style = self.style.height(height.into());
144        self
145    }
146
147    pub fn fill_size(mut self) -> Self {
148        self.style = self
149            .style
150            .width(Length::Fill(1.0))
151            .height(Length::Fill(1.0));
152        self
153    }
154
155    pub fn fit(mut self) -> Self {
156        self.style = self.style.fit();
157        self
158    }
159}
160
161impl Clone for Responsive {
162    fn clone(&self) -> Self {
163        Self {
164            window: self.window.clone(),
165            build: Rc::clone(&self.build),
166            style: self.style,
167        }
168    }
169}
170
171impl View for Responsive {
172    fn body(&self) -> impl View + 'static {
173        Unit
174    }
175
176    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
177        let size = self.window.get();
178        Some(vec![(0, (self.build)(size))])
179    }
180
181    fn layout_style(&self) -> LayoutStyle {
182        self.style
183    }
184}
185
186pub struct WithSize {
187    size: Signal<Size>,
188    build: Rc<dyn Fn(Size) -> AnyView>,
189    style: LayoutStyle,
190}
191
192impl WithSize {
193    pub fn new(build: impl Fn(Size) -> AnyView + 'static) -> Self {
194        Self::with_signal(Signal::new(Size::ZERO), build)
195    }
196
197    pub fn with_initial(initial: Size, build: impl Fn(Size) -> AnyView + 'static) -> Self {
198        Self::with_signal(Signal::new(initial), build)
199    }
200
201    pub fn with_signal(size: Signal<Size>, build: impl Fn(Size) -> AnyView + 'static) -> Self {
202        Self {
203            size,
204            build: Rc::new(build),
205            style: LayoutStyle {
206                width: Length::Fill(1.0),
207                height: Length::Fit,
208                ..LayoutStyle::default()
209            },
210        }
211    }
212
213    pub fn size_signal(&self) -> Signal<Size> {
214        self.size.clone()
215    }
216
217    pub fn width(mut self, width: impl Into<Length>) -> Self {
218        self.style = self.style.width(width.into());
219        self
220    }
221
222    pub fn height(mut self, height: impl Into<Length>) -> Self {
223        self.style = self.style.height(height.into());
224        self
225    }
226
227    pub fn fill_size(mut self) -> Self {
228        self.style = self
229            .style
230            .width(Length::Fill(1.0))
231            .height(Length::Fill(1.0));
232        self
233    }
234
235    pub fn fit(mut self) -> Self {
236        self.style = self.style.fit();
237        self
238    }
239}
240
241impl Clone for WithSize {
242    fn clone(&self) -> Self {
243        Self {
244            size: self.size.clone(),
245            build: Rc::clone(&self.build),
246            style: self.style,
247        }
248    }
249}
250
251impl View for WithSize {
252    fn body(&self) -> impl View + 'static {
253        Unit
254    }
255
256    fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
257        let size = self.size.get();
258        Some(vec![(0, (self.build)(size))])
259    }
260
261    fn layout_style(&self) -> LayoutStyle {
262        self.style
263    }
264
265    fn arrange_override(
266        &self,
267        _rt: &Runtime,
268        _el: ElementId,
269        rect: RectAlias,
270        _ctx: &mut LayoutCtx,
271    ) -> bool {
272        self.size.set(rect.size);
273        false
274    }
275}
276
277type RectAlias = sequoia_core::Rect;
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282
283    #[test]
284    fn breakpoint_thresholds() {
285        assert_eq!(Breakpoint::of(0.0), Breakpoint::Compact);
286        assert_eq!(Breakpoint::of(599.9), Breakpoint::Compact);
287        assert_eq!(Breakpoint::of(600.0), Breakpoint::Medium);
288        assert_eq!(Breakpoint::of(839.9), Breakpoint::Medium);
289        assert_eq!(Breakpoint::of(840.0), Breakpoint::Expanded);
290        assert_eq!(Breakpoint::of(4000.0), Breakpoint::Expanded);
291        assert!(Breakpoint::is_wide(Size::new(1200.0, 800.0)));
292        assert!(!Breakpoint::is_wide(Size::new(1199.0, 800.0)));
293    }
294
295    #[test]
296    fn custom_breakpoints() {
297        let b = Breakpoints::new(500.0, 1000.0);
298        assert_eq!(b.of(499.0), Breakpoint::Compact);
299        assert_eq!(b.of(500.0), Breakpoint::Medium);
300        assert_eq!(b.of(1000.0), Breakpoint::Expanded);
301    }
302
303    #[test]
304    fn orientation_cases() {
305        assert_eq!(
306            screen_orientation_of(Size::new(800.0, 600.0)),
307            ScreenOrientation::Landscape
308        );
309        assert_eq!(
310            screen_orientation_of(Size::new(600.0, 800.0)),
311            ScreenOrientation::Portrait
312        );
313        assert_eq!(
314            screen_orientation_of(Size::new(600.0, 600.0)),
315            ScreenOrientation::Square
316        );
317    }
318}