1#![forbid(unsafe_code)]
2
3use super::{Breakpoint, Breakpoints, Flex, Rect, Responsive};
46
47#[derive(Debug, Clone, PartialEq)]
53pub struct ResponsiveSplit {
54 pub breakpoint: Breakpoint,
56 pub rects: Vec<Rect>,
58}
59
60#[derive(Debug, Clone)]
67pub struct ResponsiveLayout {
68 layouts: Responsive<Flex>,
70 breakpoints: Breakpoints,
72}
73
74impl ResponsiveLayout {
79 #[must_use]
83 pub fn new(base: Flex) -> Self {
84 Self {
85 layouts: Responsive::new(base),
86 breakpoints: Breakpoints::DEFAULT,
87 }
88 }
89
90 #[must_use]
92 pub fn at(mut self, bp: Breakpoint, layout: Flex) -> Self {
93 self.layouts.set(bp, layout);
94 self
95 }
96
97 #[must_use]
101 pub fn with_breakpoints(mut self, breakpoints: Breakpoints) -> Self {
102 self.breakpoints = breakpoints;
103 self
104 }
105
106 pub fn set(&mut self, bp: Breakpoint, layout: Flex) {
108 self.layouts.set(bp, layout);
109 }
110
111 pub fn clear(&mut self, bp: Breakpoint) {
115 self.layouts.clear(bp);
116 }
117}
118
119impl ResponsiveLayout {
124 #[must_use]
129 pub fn split(&self, area: Rect) -> ResponsiveSplit {
130 let bp = self.breakpoints.classify_width(area.width);
131 self.split_for(bp, area)
132 }
133
134 #[must_use]
139 pub fn split_for(&self, bp: Breakpoint, area: Rect) -> ResponsiveSplit {
140 let flex = self.layouts.resolve(bp);
141 ResponsiveSplit {
142 breakpoint: bp,
143 rects: flex.split(area),
144 }
145 }
146
147 #[must_use]
149 pub fn classify(&self, width: u16) -> Breakpoint {
150 self.breakpoints.classify_width(width)
151 }
152
153 #[must_use]
155 pub fn layout_for(&self, bp: Breakpoint) -> &Flex {
156 self.layouts.resolve(bp)
157 }
158
159 #[must_use]
161 pub fn has_explicit(&self, bp: Breakpoint) -> bool {
162 self.layouts.has_explicit(bp)
163 }
164
165 #[must_use]
167 pub fn breakpoints(&self) -> Breakpoints {
168 self.breakpoints
169 }
170
171 #[must_use]
176 pub fn constraint_count(&self, bp: Breakpoint) -> usize {
177 self.layouts.resolve(bp).constraint_count()
178 }
179
180 #[must_use]
184 pub fn detect_transition(
185 &self,
186 old_width: u16,
187 new_width: u16,
188 ) -> Option<(Breakpoint, Breakpoint)> {
189 let old_bp = self.breakpoints.classify_width(old_width);
190 let new_bp = self.breakpoints.classify_width(new_width);
191 if old_bp != new_bp {
192 Some((old_bp, new_bp))
193 } else {
194 None
195 }
196 }
197}
198
199#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::Constraint;
207
208 fn single_column() -> Flex {
209 Flex::vertical().constraints([Constraint::Fill])
210 }
211
212 fn two_column() -> Flex {
213 Flex::horizontal().constraints([Constraint::Fixed(30), Constraint::Fill])
214 }
215
216 fn three_column() -> Flex {
217 Flex::horizontal().constraints([
218 Constraint::Fixed(25),
219 Constraint::Fill,
220 Constraint::Fixed(25),
221 ])
222 }
223
224 fn area(w: u16, h: u16) -> Rect {
225 Rect::new(0, 0, w, h)
226 }
227
228 #[test]
229 fn base_layout_at_all_breakpoints() {
230 let layout = ResponsiveLayout::new(single_column());
231 for bp in Breakpoint::ALL {
232 let result = layout.split_for(bp, area(80, 24));
233 assert_eq!(result.rects.len(), 1);
234 }
235 }
236
237 #[test]
238 fn switches_at_breakpoint() {
239 let layout = ResponsiveLayout::new(single_column()).at(Breakpoint::Md, two_column());
240
241 let result = layout.split(area(50, 24));
243 assert_eq!(result.breakpoint, Breakpoint::Xs);
244 assert_eq!(result.rects.len(), 1);
245
246 let result = layout.split(area(100, 24));
248 assert_eq!(result.breakpoint, Breakpoint::Md);
249 assert_eq!(result.rects.len(), 2);
250 }
251
252 #[test]
253 fn inherits_from_smaller() {
254 let layout = ResponsiveLayout::new(single_column()).at(Breakpoint::Md, two_column());
255
256 let result = layout.split(area(130, 24));
258 assert_eq!(result.breakpoint, Breakpoint::Lg);
259 assert_eq!(result.rects.len(), 2);
260 }
261
262 #[test]
263 fn three_tier_layout() {
264 let layout = ResponsiveLayout::new(single_column())
265 .at(Breakpoint::Sm, two_column())
266 .at(Breakpoint::Lg, three_column());
267
268 assert_eq!(layout.split(area(40, 24)).rects.len(), 1); assert_eq!(layout.split(area(70, 24)).rects.len(), 2); assert_eq!(layout.split(area(100, 24)).rects.len(), 2); assert_eq!(layout.split(area(130, 24)).rects.len(), 3); assert_eq!(layout.split(area(170, 24)).rects.len(), 3); }
274
275 #[test]
276 fn split_for_ignores_width() {
277 let layout = ResponsiveLayout::new(single_column()).at(Breakpoint::Lg, two_column());
278
279 let result = layout.split_for(Breakpoint::Lg, area(40, 24));
281 assert_eq!(result.breakpoint, Breakpoint::Lg);
282 assert_eq!(result.rects.len(), 2);
283 }
284
285 #[test]
286 fn custom_breakpoints() {
287 let layout = ResponsiveLayout::new(single_column())
288 .at(Breakpoint::Sm, two_column())
289 .with_breakpoints(Breakpoints::new(40, 80, 120));
290
291 let result = layout.split(area(50, 24));
293 assert_eq!(result.breakpoint, Breakpoint::Sm);
294 assert_eq!(result.rects.len(), 2);
295 }
296
297 #[test]
298 fn detect_transition_some() {
299 let layout = ResponsiveLayout::new(single_column());
300
301 let transition = layout.detect_transition(50, 100);
303 assert!(transition.is_some());
304 let (old, new) = transition.unwrap();
305 assert_eq!(old, Breakpoint::Xs);
306 assert_eq!(new, Breakpoint::Md);
307 }
308
309 #[test]
310 fn detect_transition_none() {
311 let layout = ResponsiveLayout::new(single_column());
312
313 assert!(layout.detect_transition(70, 80).is_none());
315 }
316
317 #[test]
318 fn classify_width() {
319 let layout = ResponsiveLayout::new(single_column());
320 assert_eq!(layout.classify(40), Breakpoint::Xs);
321 assert_eq!(layout.classify(60), Breakpoint::Sm);
322 assert_eq!(layout.classify(90), Breakpoint::Md);
323 assert_eq!(layout.classify(120), Breakpoint::Lg);
324 assert_eq!(layout.classify(160), Breakpoint::Xl);
325 }
326
327 #[test]
328 fn constraint_count() {
329 let layout = ResponsiveLayout::new(single_column())
330 .at(Breakpoint::Md, two_column())
331 .at(Breakpoint::Lg, three_column());
332
333 assert_eq!(layout.constraint_count(Breakpoint::Xs), 1);
334 assert_eq!(layout.constraint_count(Breakpoint::Sm), 1); assert_eq!(layout.constraint_count(Breakpoint::Md), 2);
336 assert_eq!(layout.constraint_count(Breakpoint::Lg), 3);
337 }
338
339 #[test]
340 fn layout_for_access() {
341 let layout = ResponsiveLayout::new(single_column()).at(Breakpoint::Md, two_column());
342
343 let flex = layout.layout_for(Breakpoint::Md);
344 assert_eq!(flex.constraint_count(), 2);
345 }
346
347 #[test]
348 fn has_explicit_check() {
349 let layout = ResponsiveLayout::new(single_column()).at(Breakpoint::Lg, two_column());
350
351 assert!(layout.has_explicit(Breakpoint::Xs));
352 assert!(!layout.has_explicit(Breakpoint::Sm));
353 assert!(!layout.has_explicit(Breakpoint::Md));
354 assert!(layout.has_explicit(Breakpoint::Lg));
355 }
356
357 #[test]
358 fn set_mutating() {
359 let mut layout = ResponsiveLayout::new(single_column());
360 layout.set(Breakpoint::Xl, three_column());
361 assert_eq!(layout.constraint_count(Breakpoint::Xl), 3);
362 }
363
364 #[test]
365 fn clear_reverts_to_inheritance() {
366 let mut layout = ResponsiveLayout::new(single_column()).at(Breakpoint::Md, two_column());
367
368 assert_eq!(layout.constraint_count(Breakpoint::Md), 2);
369 layout.clear(Breakpoint::Md);
370 assert_eq!(layout.constraint_count(Breakpoint::Md), 1); }
372
373 #[test]
374 fn empty_area_returns_zero_rects() {
375 let layout = ResponsiveLayout::new(two_column());
376 let result = layout.split(area(0, 0));
377 assert_eq!(result.breakpoint, Breakpoint::Xs);
378 assert_eq!(result.rects.len(), 2);
380 assert!(result.rects.iter().all(|r| r.width == 0 && r.height == 0));
381 }
382
383 #[test]
384 fn rect_dimensions_correct() {
385 let layout = ResponsiveLayout::new(
386 Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]),
387 );
388
389 let result = layout.split(area(100, 30));
390 assert_eq!(result.rects[0].width, 20);
391 assert_eq!(result.rects[0].height, 30);
392 assert_eq!(result.rects[1].width, 80);
393 assert_eq!(result.rects[1].height, 30);
394 }
395
396 #[test]
397 fn breakpoints_accessor() {
398 let bps = Breakpoints::new(50, 80, 110);
399 let layout = ResponsiveLayout::new(single_column()).with_breakpoints(bps);
400 assert_eq!(layout.breakpoints(), bps);
401 }
402
403 #[test]
404 fn responsive_split_debug() {
405 let split = ResponsiveSplit {
406 breakpoint: Breakpoint::Md,
407 rects: vec![Rect::new(0, 0, 50, 24)],
408 };
409 let dbg = format!("{:?}", split);
410 assert!(dbg.contains("Md"));
411 }
412}