1#![forbid(unsafe_code)]
2
3use crate::{Constraint, Sizes};
41use ftui_core::geometry::Rect;
42use std::collections::HashMap;
43
44#[derive(Debug, Clone, Default)]
46pub struct Grid {
47 row_constraints: Vec<Constraint>,
49 col_constraints: Vec<Constraint>,
51 row_gap: u16,
53 col_gap: u16,
55 named_areas: HashMap<String, GridArea>,
57 overflow: crate::OverflowBehavior,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub struct GridArea {
64 pub row: usize,
66 pub col: usize,
68 pub rowspan: usize,
70 pub colspan: usize,
72}
73
74impl GridArea {
75 #[inline]
77 #[must_use]
78 pub fn cell(row: usize, col: usize) -> Self {
79 Self {
80 row,
81 col,
82 rowspan: 1,
83 colspan: 1,
84 }
85 }
86
87 #[inline]
89 #[must_use]
90 pub fn span(row: usize, col: usize, rowspan: usize, colspan: usize) -> Self {
91 Self {
92 row,
93 col,
94 rowspan: rowspan.max(1),
95 colspan: colspan.max(1),
96 }
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct GridLayout {
103 row_heights: Sizes,
105 col_widths: Sizes,
107 row_positions: Sizes,
109 col_positions: Sizes,
111 named_areas: HashMap<String, GridArea>,
113 row_gap: u16,
115 col_gap: u16,
117 bounds: Rect,
119}
120
121impl Grid {
122 #[inline]
124 #[must_use]
125 pub fn new() -> Self {
126 Self::default()
127 }
128
129 #[must_use]
131 pub fn rows(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
132 self.row_constraints = constraints.into_iter().collect();
133 self
134 }
135
136 #[must_use]
138 pub fn columns(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
139 self.col_constraints = constraints.into_iter().collect();
140 self
141 }
142
143 #[must_use]
145 pub fn row_gap(mut self, gap: u16) -> Self {
146 self.row_gap = gap;
147 self
148 }
149
150 #[must_use]
152 pub fn col_gap(mut self, gap: u16) -> Self {
153 self.col_gap = gap;
154 self
155 }
156
157 #[must_use]
159 pub fn gap(self, gap: u16) -> Self {
160 self.row_gap(gap).col_gap(gap)
161 }
162
163 #[must_use]
175 pub fn area(mut self, name: impl Into<String>, area: GridArea) -> Self {
176 self.named_areas.insert(name.into(), area);
177 self
178 }
179
180 #[must_use]
182 pub fn overflow(mut self, overflow: crate::OverflowBehavior) -> Self {
183 self.overflow = overflow;
184 self
185 }
186
187 #[must_use]
189 pub fn overflow_behavior(&self) -> crate::OverflowBehavior {
190 self.overflow
191 }
192
193 #[inline]
195 pub fn num_rows(&self) -> usize {
196 self.row_constraints.len()
197 }
198
199 #[inline]
201 pub fn num_cols(&self) -> usize {
202 self.col_constraints.len()
203 }
204
205 pub fn split(&self, area: Rect) -> GridLayout {
207 let num_rows = self.row_constraints.len();
208 let num_cols = self.col_constraints.len();
209
210 if num_rows == 0 || num_cols == 0 || area.is_empty() {
211 return GridLayout {
212 row_heights: smallvec::smallvec![0; num_rows],
213 col_widths: smallvec::smallvec![0; num_cols],
214 row_positions: smallvec::smallvec![area.y; num_rows],
215 col_positions: smallvec::smallvec![area.x; num_cols],
216 named_areas: self.named_areas.clone(),
217 row_gap: 0,
218 col_gap: 0,
219 bounds: area,
220 };
221 }
222
223 let total_row_gap = if num_rows > 1 {
225 let gaps = (num_rows - 1) as u64;
226 (gaps * self.row_gap as u64).min(u16::MAX as u64) as u16
227 } else {
228 0
229 };
230 let total_col_gap = if num_cols > 1 {
231 let gaps = (num_cols - 1) as u64;
232 (gaps * self.col_gap as u64).min(u16::MAX as u64) as u16
233 } else {
234 0
235 };
236
237 let available_height = area.height.saturating_sub(total_row_gap);
239 let available_width = area.width.saturating_sub(total_col_gap);
240
241 let row_heights = crate::solve_constraints(&self.row_constraints, available_height);
243 let col_widths = crate::solve_constraints(&self.col_constraints, available_width);
244
245 let row_positions = self.calculate_positions(&row_heights, area.y, self.row_gap);
247 let col_positions = self.calculate_positions(&col_widths, area.x, self.col_gap);
248
249 GridLayout {
250 row_heights,
251 col_widths,
252 row_positions,
253 col_positions,
254 named_areas: self.named_areas.clone(),
255 row_gap: self.row_gap,
256 col_gap: self.col_gap,
257 bounds: area,
258 }
259 }
260
261 fn calculate_positions(&self, sizes: &[u16], start: u16, gap: u16) -> Sizes {
263 let mut positions = Sizes::with_capacity(sizes.len());
264 let mut pos = start;
265
266 for (i, &size) in sizes.iter().enumerate() {
267 positions.push(pos);
268 pos = pos.saturating_add(size);
269 if i < sizes.len() - 1 {
270 pos = pos.saturating_add(gap);
271 }
272 }
273
274 positions
275 }
276}
277
278impl GridLayout {
279 #[inline]
283 pub fn cell(&self, row: usize, col: usize) -> Rect {
284 self.span(row, col, 1, 1)
285 }
286
287 pub fn span(&self, row: usize, col: usize, rowspan: usize, colspan: usize) -> Rect {
291 let rowspan = rowspan.max(1);
292 let colspan = colspan.max(1);
293
294 if row >= self.row_heights.len() || col >= self.col_widths.len() {
296 return Rect::default();
297 }
298
299 let end_row = (row + rowspan).min(self.row_heights.len());
300 let end_col = (col + colspan).min(self.col_widths.len());
301
302 let x = self.col_positions[col];
304 let y = self.row_positions[row];
305
306 let mut width: u16 = 0;
308 for c in col..end_col {
309 width = width.saturating_add(self.col_widths[c]);
310 }
311 if end_col > col + 1 {
313 let gap_count = (end_col - col - 1) as u16;
314 width = width.saturating_add(self.col_gap.saturating_mul(gap_count));
315 }
316
317 let mut height: u16 = 0;
319 for r in row..end_row {
320 height = height.saturating_add(self.row_heights[r]);
321 }
322 if end_row > row + 1 {
323 let gap_count = (end_row - row - 1) as u16;
324 height = height.saturating_add(self.row_gap.saturating_mul(gap_count));
325 }
326
327 Rect::new(x, y, width, height).intersection(&self.bounds)
328 }
329
330 pub fn area(&self, name: &str) -> Option<Rect> {
334 self.named_areas
335 .get(name)
336 .map(|a| self.span(a.row, a.col, a.rowspan, a.colspan))
337 }
338
339 #[inline]
341 pub fn num_rows(&self) -> usize {
342 self.row_heights.len()
343 }
344
345 #[inline]
347 pub fn num_cols(&self) -> usize {
348 self.col_widths.len()
349 }
350
351 #[inline]
353 pub fn row_height(&self, row: usize) -> u16 {
354 self.row_heights.get(row).copied().unwrap_or(0)
355 }
356
357 #[inline]
359 pub fn col_width(&self, col: usize) -> u16 {
360 self.col_widths.get(col).copied().unwrap_or(0)
361 }
362
363 pub fn iter_cells(&self) -> impl Iterator<Item = (usize, usize, Rect)> + '_ {
365 let num_rows = self.num_rows();
366 let num_cols = self.num_cols();
367 (0..num_rows)
368 .flat_map(move |row| (0..num_cols).map(move |col| (row, col, self.cell(row, col))))
369 }
370}
371
372#[cfg(test)]
373mod tests {
374 use super::*;
375
376 #[test]
377 fn empty_grid() {
378 let grid = Grid::new();
379 let layout = grid.split(Rect::new(0, 0, 100, 50));
380 assert_eq!(layout.num_rows(), 0);
381 assert_eq!(layout.num_cols(), 0);
382 }
383
384 #[test]
385 fn simple_2x2_grid() {
386 let grid = Grid::new()
387 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
388 .columns([Constraint::Fixed(20), Constraint::Fixed(20)]);
389
390 let layout = grid.split(Rect::new(0, 0, 100, 50));
391
392 assert_eq!(layout.num_rows(), 2);
393 assert_eq!(layout.num_cols(), 2);
394
395 assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 20, 10));
397 assert_eq!(layout.cell(0, 1), Rect::new(20, 0, 20, 10));
398 assert_eq!(layout.cell(1, 0), Rect::new(0, 10, 20, 10));
399 assert_eq!(layout.cell(1, 1), Rect::new(20, 10, 20, 10));
400 }
401
402 #[test]
403 fn grid_with_gaps() {
404 let grid = Grid::new()
405 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
406 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
407 .row_gap(2)
408 .col_gap(5);
409
410 let layout = grid.split(Rect::new(0, 0, 100, 50));
411
412 assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 20, 10));
414 assert_eq!(layout.cell(0, 1), Rect::new(25, 0, 20, 10));
416 assert_eq!(layout.cell(1, 0), Rect::new(0, 12, 20, 10));
418 assert_eq!(layout.cell(1, 1), Rect::new(25, 12, 20, 10));
420 }
421
422 #[test]
423 fn percentage_constraints() {
424 let grid = Grid::new()
425 .rows([Constraint::Percentage(50.0), Constraint::Percentage(50.0)])
426 .columns([Constraint::Percentage(30.0), Constraint::Percentage(70.0)]);
427
428 let layout = grid.split(Rect::new(0, 0, 100, 50));
429
430 assert_eq!(layout.row_height(0), 25);
431 assert_eq!(layout.row_height(1), 25);
432 assert_eq!(layout.col_width(0), 30);
433 assert_eq!(layout.col_width(1), 70);
434 }
435
436 #[test]
437 fn min_constraints_fill_space() {
438 let grid = Grid::new()
439 .rows([Constraint::Fixed(10), Constraint::Min(5)])
440 .columns([Constraint::Fixed(20), Constraint::Min(10)]);
441
442 let layout = grid.split(Rect::new(0, 0, 100, 50));
443
444 assert_eq!(layout.row_height(0), 10);
446 assert_eq!(layout.row_height(1), 40); assert_eq!(layout.col_width(0), 20);
448 assert_eq!(layout.col_width(1), 80); }
450
451 #[test]
452 fn grid_span_clamps_out_of_bounds() {
453 let grid = Grid::new()
454 .rows([Constraint::Fixed(4), Constraint::Fixed(6)])
455 .columns([Constraint::Fixed(8), Constraint::Fixed(12)]);
456
457 let layout = grid.split(Rect::new(0, 0, 40, 20));
458 let span = layout.span(1, 1, 5, 5);
459
460 assert_eq!(span, Rect::new(8, 4, 12, 6));
461 }
462
463 #[test]
464 fn grid_span_includes_gaps_between_tracks() {
465 let grid = Grid::new()
466 .rows([Constraint::Fixed(3)])
467 .columns([
468 Constraint::Fixed(2),
469 Constraint::Fixed(2),
470 Constraint::Fixed(2),
471 ])
472 .col_gap(1);
473
474 let layout = grid.split(Rect::new(0, 0, 20, 10));
475 let span = layout.span(0, 0, 1, 3);
476
477 assert_eq!(span.width, 8); assert_eq!(span.height, 3);
479 }
480
481 #[test]
482 fn grid_tiny_area_with_gaps_produces_zero_tracks() {
483 let grid = Grid::new()
484 .rows([Constraint::Fixed(1), Constraint::Fixed(1)])
485 .columns([Constraint::Fixed(1), Constraint::Fixed(1)])
486 .row_gap(2)
487 .col_gap(2);
488
489 let layout = grid.split(Rect::new(0, 0, 1, 1));
490 assert_eq!(layout.row_height(0), 0);
491 assert_eq!(layout.row_height(1), 0);
492 assert_eq!(layout.col_width(0), 0);
493 assert_eq!(layout.col_width(1), 0);
494 }
495
496 #[test]
497 fn cell_spanning() {
498 let grid = Grid::new()
499 .rows([
500 Constraint::Fixed(10),
501 Constraint::Fixed(10),
502 Constraint::Fixed(10),
503 ])
504 .columns([
505 Constraint::Fixed(20),
506 Constraint::Fixed(20),
507 Constraint::Fixed(20),
508 ]);
509
510 let layout = grid.split(Rect::new(0, 0, 100, 50));
511
512 assert_eq!(layout.span(0, 0, 1, 1), Rect::new(0, 0, 20, 10));
514
515 assert_eq!(layout.span(0, 0, 1, 2), Rect::new(0, 0, 40, 10));
517
518 assert_eq!(layout.span(0, 0, 2, 1), Rect::new(0, 0, 20, 20));
520
521 assert_eq!(layout.span(0, 0, 2, 2), Rect::new(0, 0, 40, 20));
523 }
524
525 #[test]
526 fn cell_spanning_with_gaps() {
527 let grid = Grid::new()
528 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
529 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
530 .row_gap(2)
531 .col_gap(5);
532
533 let layout = grid.split(Rect::new(0, 0, 100, 50));
534
535 let full = layout.span(0, 0, 2, 2);
537 assert_eq!(full.width, 45);
540 assert_eq!(full.height, 22);
541 }
542
543 #[test]
544 fn named_areas() {
545 let grid = Grid::new()
546 .rows([
547 Constraint::Fixed(5),
548 Constraint::Min(10),
549 Constraint::Fixed(3),
550 ])
551 .columns([Constraint::Fixed(20), Constraint::Min(30)])
552 .area("header", GridArea::span(0, 0, 1, 2))
553 .area("sidebar", GridArea::span(1, 0, 2, 1))
554 .area("content", GridArea::cell(1, 1))
555 .area("footer", GridArea::cell(2, 1));
556
557 let layout = grid.split(Rect::new(0, 0, 80, 30));
558
559 let header = layout.area("header").unwrap();
561 assert_eq!(header.y, 0);
562 assert_eq!(header.height, 5);
563
564 let sidebar = layout.area("sidebar").unwrap();
566 assert_eq!(sidebar.x, 0);
567 assert_eq!(sidebar.width, 20);
568
569 let content = layout.area("content").unwrap();
571 assert_eq!(content.x, 20);
572 assert_eq!(content.y, 5);
573
574 let footer = layout.area("footer").unwrap();
576 assert_eq!(
577 footer.y,
578 layout.area("content").unwrap().y + layout.area("content").unwrap().height
579 );
580 }
581
582 #[test]
583 fn out_of_bounds_returns_empty() {
584 let grid = Grid::new()
585 .rows([Constraint::Fixed(10)])
586 .columns([Constraint::Fixed(20)]);
587
588 let layout = grid.split(Rect::new(0, 0, 100, 50));
589
590 assert_eq!(layout.cell(5, 5), Rect::default());
592 assert_eq!(layout.cell(0, 5), Rect::default());
593 assert_eq!(layout.cell(5, 0), Rect::default());
594 }
595
596 #[test]
597 fn iter_cells() {
598 let grid = Grid::new()
599 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
600 .columns([Constraint::Fixed(20), Constraint::Fixed(20)]);
601
602 let layout = grid.split(Rect::new(0, 0, 100, 50));
603
604 let cells: Vec<_> = layout.iter_cells().collect();
605 assert_eq!(cells.len(), 4);
606 assert_eq!(cells[0], (0, 0, Rect::new(0, 0, 20, 10)));
607 assert_eq!(cells[1], (0, 1, Rect::new(20, 0, 20, 10)));
608 assert_eq!(cells[2], (1, 0, Rect::new(0, 10, 20, 10)));
609 assert_eq!(cells[3], (1, 1, Rect::new(20, 10, 20, 10)));
610 }
611
612 #[test]
613 fn undefined_area_returns_none() {
614 let grid = Grid::new()
615 .rows([Constraint::Fixed(10)])
616 .columns([Constraint::Fixed(20)]);
617
618 let layout = grid.split(Rect::new(0, 0, 100, 50));
619
620 assert!(layout.area("nonexistent").is_none());
621 }
622
623 #[test]
624 fn empty_area_produces_empty_cells() {
625 let grid = Grid::new()
626 .rows([Constraint::Fixed(10)])
627 .columns([Constraint::Fixed(20)]);
628
629 let layout = grid.split(Rect::new(0, 0, 0, 0));
630
631 assert_eq!(layout.cell(0, 0), Rect::new(0, 0, 0, 0));
632 }
633
634 #[test]
635 fn offset_area() {
636 let grid = Grid::new()
637 .rows([Constraint::Fixed(10)])
638 .columns([Constraint::Fixed(20)]);
639
640 let layout = grid.split(Rect::new(10, 5, 100, 50));
641
642 assert_eq!(layout.cell(0, 0), Rect::new(10, 5, 20, 10));
644 }
645
646 #[test]
647 fn ratio_constraints() {
648 let grid = Grid::new()
649 .rows([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
650 .columns([Constraint::Fixed(30)]);
651
652 let layout = grid.split(Rect::new(0, 0, 30, 30));
653
654 assert_eq!(layout.row_height(0), 10);
656 assert_eq!(layout.row_height(1), 20);
657 }
658
659 #[test]
660 fn max_constraints() {
661 let grid = Grid::new()
663 .rows([Constraint::Max(5), Constraint::Fixed(20)])
664 .columns([Constraint::Fixed(30)]);
665
666 let layout = grid.split(Rect::new(0, 0, 30, 30));
667
668 assert!(layout.row_height(0) <= 5);
671 assert_eq!(layout.row_height(1), 20);
673 }
674
675 #[test]
676 fn fixed_constraints_exceed_available_clamped() {
677 let grid = Grid::new()
678 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
679 .columns([Constraint::Fixed(7), Constraint::Fixed(7)]);
680
681 let layout = grid.split(Rect::new(0, 0, 10, 15));
682
683 assert_eq!(layout.row_height(0), 10);
684 assert_eq!(layout.row_height(1), 5);
685 assert_eq!(layout.col_width(0), 7);
686 assert_eq!(layout.col_width(1), 3);
687 }
688
689 #[test]
690 fn ratio_constraints_calculate_strictly() {
691 let grid = Grid::new()
692 .rows([Constraint::Fixed(1)])
693 .columns([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
694
695 let layout = grid.split(Rect::new(0, 0, 5, 1));
696
697 assert_eq!(layout.col_width(0), 1);
702 assert_eq!(layout.col_width(1), 3);
703 }
704
705 #[test]
708 fn uniform_gap_sets_both() {
709 let grid = Grid::new()
710 .rows([Constraint::Fixed(10), Constraint::Fixed(10)])
711 .columns([Constraint::Fixed(20), Constraint::Fixed(20)])
712 .gap(3);
713
714 let layout = grid.split(Rect::new(0, 0, 100, 50));
715
716 assert_eq!(layout.cell(0, 1).x, 23); assert_eq!(layout.cell(1, 0).y, 13); }
720
721 #[test]
722 fn grid_area_cell_is_1x1_span() {
723 let a = GridArea::cell(2, 3);
724 assert_eq!(a.row, 2);
725 assert_eq!(a.col, 3);
726 assert_eq!(a.rowspan, 1);
727 assert_eq!(a.colspan, 1);
728 }
729
730 #[test]
731 fn grid_area_span_clamps_zero() {
732 let a = GridArea::span(0, 0, 0, 0);
734 assert_eq!(a.rowspan, 1);
735 assert_eq!(a.colspan, 1);
736 }
737
738 #[test]
739 fn grid_num_rows_cols() {
740 let grid = Grid::new()
741 .rows([
742 Constraint::Fixed(5),
743 Constraint::Fixed(5),
744 Constraint::Fixed(5),
745 ])
746 .columns([Constraint::Fixed(10), Constraint::Fixed(10)]);
747 assert_eq!(grid.num_rows(), 3);
748 assert_eq!(grid.num_cols(), 2);
749 }
750
751 #[test]
752 fn grid_row_height_col_width_out_of_bounds() {
753 let grid = Grid::new()
754 .rows([Constraint::Fixed(10)])
755 .columns([Constraint::Fixed(20)]);
756 let layout = grid.split(Rect::new(0, 0, 100, 50));
757 assert_eq!(layout.row_height(0), 10);
758 assert_eq!(layout.row_height(99), 0); assert_eq!(layout.col_width(0), 20);
760 assert_eq!(layout.col_width(99), 0); }
762
763 #[test]
764 fn grid_span_clamped_to_bounds() {
765 let grid = Grid::new()
766 .rows([Constraint::Fixed(10)])
767 .columns([Constraint::Fixed(20)]);
768 let layout = grid.split(Rect::new(0, 0, 100, 50));
769
770 let r = layout.span(0, 0, 5, 5);
772 assert_eq!(r, Rect::new(0, 0, 20, 10));
774 }
775
776 #[test]
777 fn grid_with_all_constraint_types() {
778 let grid = Grid::new()
779 .rows([
780 Constraint::Fixed(5),
781 Constraint::Percentage(20.0),
782 Constraint::Min(3),
783 Constraint::Max(10),
784 Constraint::Ratio(1, 4),
785 ])
786 .columns([Constraint::Fixed(30)]);
787
788 let layout = grid.split(Rect::new(0, 0, 30, 50));
789
790 let total: u16 = (0..layout.num_rows()).map(|r| layout.row_height(r)).sum();
792 assert!(total <= 50);
793 }
794
795 #[test]
797 fn invariant_total_size_within_bounds() {
798 for (width, height) in [(50, 30), (100, 50), (80, 24)] {
799 let grid = Grid::new()
800 .rows([
801 Constraint::Fixed(10),
802 Constraint::Min(5),
803 Constraint::Percentage(20.0),
804 ])
805 .columns([
806 Constraint::Fixed(15),
807 Constraint::Min(10),
808 Constraint::Ratio(1, 2),
809 ]);
810
811 let layout = grid.split(Rect::new(0, 0, width, height));
812
813 let total_height: u16 = (0..layout.num_rows()).map(|r| layout.row_height(r)).sum();
814 let total_width: u16 = (0..layout.num_cols()).map(|c| layout.col_width(c)).sum();
815
816 assert!(
817 total_height <= height,
818 "Total height {} exceeds available {}",
819 total_height,
820 height
821 );
822 assert!(
823 total_width <= width,
824 "Total width {} exceeds available {}",
825 total_width,
826 width
827 );
828 }
829 }
830
831 #[test]
832 fn invariant_cells_within_area() {
833 let area = Rect::new(10, 20, 80, 60);
834 let grid = Grid::new()
835 .rows([
836 Constraint::Fixed(15),
837 Constraint::Min(10),
838 Constraint::Fixed(15),
839 ])
840 .columns([
841 Constraint::Fixed(20),
842 Constraint::Min(20),
843 Constraint::Fixed(20),
844 ])
845 .row_gap(2)
846 .col_gap(3);
847
848 let layout = grid.split(area);
849
850 for (row, col, cell) in layout.iter_cells() {
851 assert!(
852 cell.x >= area.x,
853 "Cell ({},{}) x {} < area x {}",
854 row,
855 col,
856 cell.x,
857 area.x
858 );
859 assert!(
860 cell.y >= area.y,
861 "Cell ({},{}) y {} < area y {}",
862 row,
863 col,
864 cell.y,
865 area.y
866 );
867 assert!(
868 cell.right() <= area.right(),
869 "Cell ({},{}) right {} > area right {}",
870 row,
871 col,
872 cell.right(),
873 area.right()
874 );
875 assert!(
876 cell.bottom() <= area.bottom(),
877 "Cell ({},{}) bottom {} > area bottom {}",
878 row,
879 col,
880 cell.bottom(),
881 area.bottom()
882 );
883 }
884 }
885
886 #[test]
887 fn grid_layout_span_clamps_zero() {
888 let grid = Grid::new()
889 .rows([Constraint::Fixed(10)])
890 .columns([Constraint::Fixed(20)]);
891 let layout = grid.split(Rect::new(0, 0, 100, 50));
892
893 let r = layout.span(0, 0, 0, 0);
895 assert_eq!(r, layout.cell(0, 0));
896 assert_eq!(r.width, 20);
897 assert_eq!(r.height, 10);
898 }
899}