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}