1#![forbid(unsafe_code)]
2
3pub mod cache;
43pub mod debug;
44pub mod direction;
45pub mod grid;
46#[cfg(test)]
47mod repro_max_constraint;
48#[cfg(test)]
49mod repro_space_around;
50pub mod responsive;
51pub mod responsive_layout;
52pub mod visibility;
53
54pub use cache::{CoherenceCache, CoherenceId, LayoutCache, LayoutCacheKey, LayoutCacheStats};
55pub use direction::{FlowDirection, LogicalAlignment, LogicalSides, mirror_rects_horizontal};
56pub use ftui_core::geometry::{Rect, Sides, Size};
57pub use grid::{Grid, GridArea, GridLayout};
58pub use responsive::Responsive;
59pub use responsive_layout::{ResponsiveLayout, ResponsiveSplit};
60use std::cmp::min;
61pub use visibility::Visibility;
62
63#[derive(Debug, Clone, Copy, PartialEq)]
65pub enum Constraint {
66 Fixed(u16),
68 Percentage(f32),
70 Min(u16),
72 Max(u16),
74 Ratio(u32, u32),
76 Fill,
78 FitContent,
83 FitContentBounded {
88 min: u16,
90 max: u16,
92 },
93 FitMin,
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub struct LayoutSizeHint {
123 pub min: u16,
125 pub preferred: u16,
127 pub max: Option<u16>,
129}
130
131impl LayoutSizeHint {
132 pub const ZERO: Self = Self {
134 min: 0,
135 preferred: 0,
136 max: None,
137 };
138
139 #[inline]
141 pub const fn exact(size: u16) -> Self {
142 Self {
143 min: size,
144 preferred: size,
145 max: Some(size),
146 }
147 }
148
149 #[inline]
151 pub const fn at_least(min: u16, preferred: u16) -> Self {
152 Self {
153 min,
154 preferred,
155 max: None,
156 }
157 }
158
159 #[inline]
161 pub fn clamp(&self, value: u16) -> u16 {
162 let max = self.max.unwrap_or(u16::MAX);
163 value.max(self.min).min(max)
164 }
165}
166
167#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
169pub enum Direction {
170 #[default]
172 Vertical,
173 Horizontal,
175}
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
179pub enum Alignment {
180 #[default]
182 Start,
183 Center,
185 End,
187 SpaceAround,
189 SpaceBetween,
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
206pub enum Breakpoint {
207 Xs,
209 Sm,
211 Md,
213 Lg,
215 Xl,
217}
218
219impl Breakpoint {
220 pub const ALL: [Breakpoint; 5] = [
222 Breakpoint::Xs,
223 Breakpoint::Sm,
224 Breakpoint::Md,
225 Breakpoint::Lg,
226 Breakpoint::Xl,
227 ];
228
229 #[inline]
231 const fn index(self) -> u8 {
232 match self {
233 Breakpoint::Xs => 0,
234 Breakpoint::Sm => 1,
235 Breakpoint::Md => 2,
236 Breakpoint::Lg => 3,
237 Breakpoint::Xl => 4,
238 }
239 }
240
241 #[must_use]
243 pub const fn label(self) -> &'static str {
244 match self {
245 Breakpoint::Xs => "xs",
246 Breakpoint::Sm => "sm",
247 Breakpoint::Md => "md",
248 Breakpoint::Lg => "lg",
249 Breakpoint::Xl => "xl",
250 }
251 }
252}
253
254impl std::fmt::Display for Breakpoint {
255 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256 f.write_str(self.label())
257 }
258}
259
260#[derive(Debug, Clone, Copy, PartialEq, Eq)]
265pub struct Breakpoints {
266 pub sm: u16,
268 pub md: u16,
270 pub lg: u16,
272 pub xl: u16,
274}
275
276impl Breakpoints {
277 pub const DEFAULT: Self = Self {
279 sm: 60,
280 md: 90,
281 lg: 120,
282 xl: 160,
283 };
284
285 pub const fn new(sm: u16, md: u16, lg: u16) -> Self {
289 let md = if md < sm { sm } else { md };
290 let lg = if lg < md { md } else { lg };
291 let xl = if lg + 40 > lg { lg + 40 } else { u16::MAX };
293 Self { sm, md, lg, xl }
294 }
295
296 pub const fn new_with_xl(sm: u16, md: u16, lg: u16, xl: u16) -> Self {
300 let md = if md < sm { sm } else { md };
301 let lg = if lg < md { md } else { lg };
302 let xl = if xl < lg { lg } else { xl };
303 Self { sm, md, lg, xl }
304 }
305
306 #[inline]
308 pub const fn classify_width(self, width: u16) -> Breakpoint {
309 if width >= self.xl {
310 Breakpoint::Xl
311 } else if width >= self.lg {
312 Breakpoint::Lg
313 } else if width >= self.md {
314 Breakpoint::Md
315 } else if width >= self.sm {
316 Breakpoint::Sm
317 } else {
318 Breakpoint::Xs
319 }
320 }
321
322 #[inline]
324 pub const fn classify_size(self, size: Size) -> Breakpoint {
325 self.classify_width(size.width)
326 }
327
328 #[inline]
330 pub const fn at_least(self, width: u16, min: Breakpoint) -> bool {
331 self.classify_width(width).index() >= min.index()
332 }
333
334 #[inline]
336 pub const fn between(self, width: u16, min: Breakpoint, max: Breakpoint) -> bool {
337 let idx = self.classify_width(width).index();
338 idx >= min.index() && idx <= max.index()
339 }
340
341 #[must_use]
343 pub const fn threshold(self, bp: Breakpoint) -> u16 {
344 match bp {
345 Breakpoint::Xs => 0,
346 Breakpoint::Sm => self.sm,
347 Breakpoint::Md => self.md,
348 Breakpoint::Lg => self.lg,
349 Breakpoint::Xl => self.xl,
350 }
351 }
352
353 #[must_use]
355 pub const fn thresholds(self) -> [(Breakpoint, u16); 5] {
356 [
357 (Breakpoint::Xs, 0),
358 (Breakpoint::Sm, self.sm),
359 (Breakpoint::Md, self.md),
360 (Breakpoint::Lg, self.lg),
361 (Breakpoint::Xl, self.xl),
362 ]
363 }
364}
365
366#[derive(Debug, Clone, Copy, Default)]
368pub struct Measurement {
369 pub min_width: u16,
371 pub min_height: u16,
373 pub max_width: Option<u16>,
375 pub max_height: Option<u16>,
377}
378
379impl Measurement {
380 pub fn fixed(width: u16, height: u16) -> Self {
382 Self {
383 min_width: width,
384 min_height: height,
385 max_width: Some(width),
386 max_height: Some(height),
387 }
388 }
389
390 pub fn flexible(min_width: u16, min_height: u16) -> Self {
392 Self {
393 min_width,
394 min_height,
395 max_width: None,
396 max_height: None,
397 }
398 }
399}
400
401#[derive(Debug, Clone, Default)]
403pub struct Flex {
404 direction: Direction,
405 constraints: Vec<Constraint>,
406 margin: Sides,
407 gap: u16,
408 alignment: Alignment,
409 flow_direction: direction::FlowDirection,
410}
411
412impl Flex {
413 pub fn vertical() -> Self {
415 Self {
416 direction: Direction::Vertical,
417 ..Default::default()
418 }
419 }
420
421 pub fn horizontal() -> Self {
423 Self {
424 direction: Direction::Horizontal,
425 ..Default::default()
426 }
427 }
428
429 pub fn direction(mut self, direction: Direction) -> Self {
431 self.direction = direction;
432 self
433 }
434
435 pub fn constraints(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
437 self.constraints = constraints.into_iter().collect();
438 self
439 }
440
441 pub fn margin(mut self, margin: Sides) -> Self {
443 self.margin = margin;
444 self
445 }
446
447 pub fn gap(mut self, gap: u16) -> Self {
449 self.gap = gap;
450 self
451 }
452
453 pub fn alignment(mut self, alignment: Alignment) -> Self {
455 self.alignment = alignment;
456 self
457 }
458
459 pub fn flow_direction(mut self, flow: direction::FlowDirection) -> Self {
465 self.flow_direction = flow;
466 self
467 }
468
469 #[must_use]
471 pub fn constraint_count(&self) -> usize {
472 self.constraints.len()
473 }
474
475 pub fn split(&self, area: Rect) -> Vec<Rect> {
477 let inner = area.inner(self.margin);
479 if inner.is_empty() {
480 return self.constraints.iter().map(|_| Rect::default()).collect();
481 }
482
483 let total_size = match self.direction {
484 Direction::Horizontal => inner.width,
485 Direction::Vertical => inner.height,
486 };
487
488 let count = self.constraints.len();
489 if count == 0 {
490 return Vec::new();
491 }
492
493 let gap_count = count - 1;
495 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
496 let available_size = total_size.saturating_sub(total_gap);
497
498 let sizes = solve_constraints(&self.constraints, available_size);
500
501 let mut rects = self.sizes_to_rects(inner, &sizes);
503
504 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
506 direction::mirror_rects_horizontal(&mut rects, inner);
507 }
508
509 rects
510 }
511
512 fn sizes_to_rects(&self, area: Rect, sizes: &[u16]) -> Vec<Rect> {
513 let mut rects = Vec::with_capacity(sizes.len());
514
515 let total_gaps = if sizes.len() > 1 {
517 let gap_count = sizes.len() - 1;
518 (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16
519 } else {
520 0
521 };
522 let total_used: u16 = sizes.iter().sum::<u16>().saturating_add(total_gaps);
523 let total_available = match self.direction {
524 Direction::Horizontal => area.width,
525 Direction::Vertical => area.height,
526 };
527 let leftover = total_available.saturating_sub(total_used);
528
529 let (start_offset, extra_gap) = match self.alignment {
531 Alignment::Start => (0, 0),
532 Alignment::End => (leftover, 0),
533 Alignment::Center => (leftover / 2, 0),
534 Alignment::SpaceBetween => (0, 0),
535 Alignment::SpaceAround => {
536 if sizes.is_empty() {
537 (0, 0)
538 } else {
539 let slots = sizes.len() * 2;
542 let unit = (leftover as usize / slots) as u16;
543 let rem = (leftover as usize % slots) as u16;
544 (unit + rem / 2, 0)
545 }
546 }
547 };
548
549 let mut current_pos = match self.direction {
550 Direction::Horizontal => area.x.saturating_add(start_offset),
551 Direction::Vertical => area.y.saturating_add(start_offset),
552 };
553
554 for (i, &size) in sizes.iter().enumerate() {
555 let rect = match self.direction {
556 Direction::Horizontal => Rect {
557 x: current_pos,
558 y: area.y,
559 width: size,
560 height: area.height,
561 },
562 Direction::Vertical => Rect {
563 x: area.x,
564 y: current_pos,
565 width: area.width,
566 height: size,
567 },
568 };
569 rects.push(rect);
570
571 current_pos = current_pos
573 .saturating_add(size)
574 .saturating_add(self.gap)
575 .saturating_add(extra_gap);
576
577 match self.alignment {
579 Alignment::SpaceBetween => {
580 if sizes.len() > 1 && i < sizes.len() - 1 {
581 let count = sizes.len() - 1; let base = (leftover as usize / count) as u16;
584 let rem = (leftover as usize % count) as u16;
585 let extra = base + if (i as u16) < rem { 1 } else { 0 };
586 current_pos = current_pos.saturating_add(extra);
587 }
588 }
589 Alignment::SpaceAround => {
590 if !sizes.is_empty() {
591 let slots = sizes.len() * 2; let unit = (leftover as usize / slots) as u16;
593 current_pos = current_pos.saturating_add(unit.saturating_mul(2));
594 }
595 }
596 _ => {}
597 }
598 }
599
600 rects
601 }
602
603 pub fn split_with_measurer<F>(&self, area: Rect, measurer: F) -> Vec<Rect>
627 where
628 F: Fn(usize, u16) -> LayoutSizeHint,
629 {
630 let inner = area.inner(self.margin);
632 if inner.is_empty() {
633 return self.constraints.iter().map(|_| Rect::default()).collect();
634 }
635
636 let total_size = match self.direction {
637 Direction::Horizontal => inner.width,
638 Direction::Vertical => inner.height,
639 };
640
641 let count = self.constraints.len();
642 if count == 0 {
643 return Vec::new();
644 }
645
646 let gap_count = count - 1;
648 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
649 let available_size = total_size.saturating_sub(total_gap);
650
651 let sizes = solve_constraints_with_hints(&self.constraints, available_size, &measurer);
653
654 let mut rects = self.sizes_to_rects(inner, &sizes);
656
657 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
659 direction::mirror_rects_horizontal(&mut rects, inner);
660 }
661
662 rects
663 }
664}
665
666pub(crate) fn solve_constraints(constraints: &[Constraint], available_size: u16) -> Vec<u16> {
671 solve_constraints_with_hints(constraints, available_size, &|_, _| LayoutSizeHint::ZERO)
673}
674
675pub(crate) fn solve_constraints_with_hints<F>(
680 constraints: &[Constraint],
681 available_size: u16,
682 measurer: &F,
683) -> Vec<u16>
684where
685 F: Fn(usize, u16) -> LayoutSizeHint,
686{
687 let mut sizes = vec![0u16; constraints.len()];
688 let mut remaining = available_size;
689 let mut grow_indices = Vec::new();
690
691 for (i, &constraint) in constraints.iter().enumerate() {
693 match constraint {
694 Constraint::Fixed(size) => {
695 let size = min(size, remaining);
696 sizes[i] = size;
697 remaining = remaining.saturating_sub(size);
698 }
699 Constraint::Percentage(p) => {
700 let size = (available_size as f32 * p / 100.0)
701 .round()
702 .min(u16::MAX as f32) as u16;
703 let size = min(size, remaining);
704 sizes[i] = size;
705 remaining = remaining.saturating_sub(size);
706 }
707 Constraint::Min(min_size) => {
708 let size = min(min_size, remaining);
709 sizes[i] = size;
710 remaining = remaining.saturating_sub(size);
711 grow_indices.push(i);
712 }
713 Constraint::Max(_) => {
714 grow_indices.push(i);
716 }
717 Constraint::Ratio(_, _) => {
718 grow_indices.push(i);
720 }
721 Constraint::Fill => {
722 grow_indices.push(i);
724 }
725 Constraint::FitContent => {
726 let hint = measurer(i, remaining);
728 let size = min(hint.preferred, remaining);
729 sizes[i] = size;
730 remaining = remaining.saturating_sub(size);
731 }
733 Constraint::FitContentBounded {
734 min: min_bound,
735 max: max_bound,
736 } => {
737 let hint = measurer(i, remaining);
739 let preferred = hint.preferred.max(min_bound).min(max_bound);
740 let size = min(preferred, remaining);
741 sizes[i] = size;
742 remaining = remaining.saturating_sub(size);
743 }
744 Constraint::FitMin => {
745 let hint = measurer(i, remaining);
747 let size = min(hint.min, remaining);
748 sizes[i] = size;
749 remaining = remaining.saturating_sub(size);
750 grow_indices.push(i);
752 }
753 }
754 }
755
756 loop {
758 if remaining == 0 || grow_indices.is_empty() {
759 break;
760 }
761
762 let mut total_weight = 0u64;
763 const WEIGHT_SCALE: u64 = 10_000;
764
765 for &i in &grow_indices {
766 match constraints[i] {
767 Constraint::Ratio(n, d) => {
768 let w = n as u64 * WEIGHT_SCALE / d.max(1) as u64;
769 total_weight += w;
771 }
772 _ => total_weight += WEIGHT_SCALE,
773 }
774 }
775
776 if total_weight == 0 {
777 break;
782 }
783
784 let space_to_distribute = remaining;
785 let mut allocated = 0;
786 let mut shares = vec![0u16; constraints.len()];
787
788 for (idx, &i) in grow_indices.iter().enumerate() {
789 let weight = match constraints[i] {
790 Constraint::Ratio(n, d) => n as u64 * WEIGHT_SCALE / d.max(1) as u64,
791 _ => WEIGHT_SCALE,
792 };
793
794 let size = if idx == grow_indices.len() - 1 {
797 if weight == 0 {
799 0
800 } else {
801 space_to_distribute - allocated
802 }
803 } else {
804 let s = (space_to_distribute as u64 * weight / total_weight) as u16;
805 min(s, space_to_distribute - allocated)
806 };
807
808 shares[i] = size;
809 allocated += size;
810 }
811
812 let mut violations = Vec::new();
814 for &i in &grow_indices {
815 if let Constraint::Max(max_val) = constraints[i]
816 && sizes[i].saturating_add(shares[i]) > max_val
817 {
818 violations.push(i);
819 }
820 }
821
822 if violations.is_empty() {
823 for &i in &grow_indices {
825 sizes[i] = sizes[i].saturating_add(shares[i]);
826 }
827 break;
828 }
829
830 for i in violations {
832 if let Constraint::Max(max_val) = constraints[i] {
833 let consumed = max_val.saturating_sub(sizes[i]);
836 sizes[i] = max_val;
837 remaining = remaining.saturating_sub(consumed);
838
839 if let Some(pos) = grow_indices.iter().position(|&x| x == i) {
841 grow_indices.remove(pos);
842 }
843 }
844 }
845 }
846
847 sizes
848}
849
850pub type PreviousAllocation = Option<Vec<u16>>;
860
861pub fn round_layout_stable(targets: &[f64], total: u16, prev: PreviousAllocation) -> Vec<u16> {
923 let n = targets.len();
924 if n == 0 {
925 return Vec::new();
926 }
927
928 let floors: Vec<u16> = targets
930 .iter()
931 .map(|&r| (r.max(0.0).floor() as u64).min(u16::MAX as u64) as u16)
932 .collect();
933
934 let floor_sum: u16 = floors.iter().copied().sum();
935
936 let deficit = total.saturating_sub(floor_sum);
938
939 if deficit == 0 {
940 if floor_sum > total {
943 return redistribute_overflow(&floors, total);
944 }
945 return floors;
946 }
947
948 let mut priority: Vec<(usize, f64, bool)> = targets
950 .iter()
951 .enumerate()
952 .map(|(i, &r)| {
953 let remainder = r - (floors[i] as f64);
954 let ceil_val = floors[i].saturating_add(1);
955 let prev_used_ceil = prev
957 .as_ref()
958 .is_some_and(|p| p.get(i).copied() == Some(ceil_val));
959 (i, remainder, prev_used_ceil)
960 })
961 .collect();
962
963 priority.sort_by(|a, b| {
965 b.1.partial_cmp(&a.1)
966 .unwrap_or(std::cmp::Ordering::Equal)
967 .then_with(|| {
968 b.2.cmp(&a.2)
970 })
971 .then_with(|| {
972 a.0.cmp(&b.0)
974 })
975 });
976
977 let mut result = floors;
979 let distribute = (deficit as usize).min(n);
980 for &(i, _, _) in priority.iter().take(distribute) {
981 result[i] = result[i].saturating_add(1);
982 }
983
984 result
985}
986
987fn redistribute_overflow(floors: &[u16], total: u16) -> Vec<u16> {
992 let mut result = floors.to_vec();
993 let mut current_sum: u16 = result.iter().copied().sum();
994
995 while current_sum > total {
997 if let Some((idx, _)) = result
999 .iter()
1000 .enumerate()
1001 .filter(|item| *item.1 > 0)
1002 .max_by_key(|item| *item.1)
1003 {
1004 result[idx] = result[idx].saturating_sub(1);
1005 current_sum = current_sum.saturating_sub(1);
1006 } else {
1007 break;
1008 }
1009 }
1010
1011 result
1012}
1013
1014#[cfg(test)]
1015mod tests {
1016 use super::*;
1017
1018 #[test]
1019 fn fixed_split() {
1020 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(20)]);
1021 let rects = flex.split(Rect::new(0, 0, 100, 10));
1022 assert_eq!(rects.len(), 2);
1023 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1024 assert_eq!(rects[1], Rect::new(10, 0, 20, 10)); }
1026
1027 #[test]
1028 fn percentage_split() {
1029 let flex = Flex::horizontal()
1030 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1031 let rects = flex.split(Rect::new(0, 0, 100, 10));
1032 assert_eq!(rects[0].width, 50);
1033 assert_eq!(rects[1].width, 50);
1034 }
1035
1036 #[test]
1037 fn gap_handling() {
1038 let flex = Flex::horizontal()
1039 .gap(5)
1040 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1041 let rects = flex.split(Rect::new(0, 0, 100, 10));
1042 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1046 assert_eq!(rects[1], Rect::new(15, 0, 10, 10));
1047 }
1048
1049 #[test]
1050 fn mixed_constraints() {
1051 let flex = Flex::horizontal().constraints([
1052 Constraint::Fixed(10),
1053 Constraint::Min(10), Constraint::Percentage(10.0), ]);
1056
1057 let rects = flex.split(Rect::new(0, 0, 100, 1));
1065 assert_eq!(rects[0].width, 10); assert_eq!(rects[2].width, 10); assert_eq!(rects[1].width, 80); }
1069
1070 #[test]
1071 fn measurement_fixed_constraints() {
1072 let fixed = Measurement::fixed(5, 7);
1073 assert_eq!(fixed.min_width, 5);
1074 assert_eq!(fixed.min_height, 7);
1075 assert_eq!(fixed.max_width, Some(5));
1076 assert_eq!(fixed.max_height, Some(7));
1077 }
1078
1079 #[test]
1080 fn measurement_flexible_constraints() {
1081 let flexible = Measurement::flexible(2, 3);
1082 assert_eq!(flexible.min_width, 2);
1083 assert_eq!(flexible.min_height, 3);
1084 assert_eq!(flexible.max_width, None);
1085 assert_eq!(flexible.max_height, None);
1086 }
1087
1088 #[test]
1089 fn breakpoints_classify_defaults() {
1090 let bp = Breakpoints::DEFAULT;
1091 assert_eq!(bp.classify_width(20), Breakpoint::Xs);
1092 assert_eq!(bp.classify_width(60), Breakpoint::Sm);
1093 assert_eq!(bp.classify_width(90), Breakpoint::Md);
1094 assert_eq!(bp.classify_width(120), Breakpoint::Lg);
1095 }
1096
1097 #[test]
1098 fn breakpoints_at_least_and_between() {
1099 let bp = Breakpoints::new(50, 80, 110);
1100 assert!(bp.at_least(85, Breakpoint::Sm));
1101 assert!(bp.between(85, Breakpoint::Sm, Breakpoint::Md));
1102 assert!(!bp.between(85, Breakpoint::Lg, Breakpoint::Lg));
1103 }
1104
1105 #[test]
1106 fn alignment_end() {
1107 let flex = Flex::horizontal()
1108 .alignment(Alignment::End)
1109 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1110 let rects = flex.split(Rect::new(0, 0, 100, 10));
1111 assert_eq!(rects[0], Rect::new(80, 0, 10, 10));
1113 assert_eq!(rects[1], Rect::new(90, 0, 10, 10));
1114 }
1115
1116 #[test]
1117 fn alignment_center() {
1118 let flex = Flex::horizontal()
1119 .alignment(Alignment::Center)
1120 .constraints([Constraint::Fixed(20), Constraint::Fixed(20)]);
1121 let rects = flex.split(Rect::new(0, 0, 100, 10));
1122 assert_eq!(rects[0], Rect::new(30, 0, 20, 10));
1124 assert_eq!(rects[1], Rect::new(50, 0, 20, 10));
1125 }
1126
1127 #[test]
1128 fn alignment_space_between() {
1129 let flex = Flex::horizontal()
1130 .alignment(Alignment::SpaceBetween)
1131 .constraints([
1132 Constraint::Fixed(10),
1133 Constraint::Fixed(10),
1134 Constraint::Fixed(10),
1135 ]);
1136 let rects = flex.split(Rect::new(0, 0, 100, 10));
1137 assert_eq!(rects[0].x, 0);
1139 assert_eq!(rects[1].x, 45); assert_eq!(rects[2].x, 90); }
1142
1143 #[test]
1144 fn vertical_alignment() {
1145 let flex = Flex::vertical()
1146 .alignment(Alignment::End)
1147 .constraints([Constraint::Fixed(5), Constraint::Fixed(5)]);
1148 let rects = flex.split(Rect::new(0, 0, 10, 100));
1149 assert_eq!(rects[0], Rect::new(0, 90, 10, 5));
1151 assert_eq!(rects[1], Rect::new(0, 95, 10, 5));
1152 }
1153
1154 #[test]
1155 fn nested_flex_support() {
1156 let outer = Flex::horizontal()
1158 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1159 let outer_rects = outer.split(Rect::new(0, 0, 100, 100));
1160
1161 let inner = Flex::vertical().constraints([Constraint::Fixed(30), Constraint::Min(10)]);
1163 let inner_rects = inner.split(outer_rects[0]);
1164
1165 assert_eq!(inner_rects[0], Rect::new(0, 0, 50, 30));
1166 assert_eq!(inner_rects[1], Rect::new(0, 30, 50, 70));
1167 }
1168
1169 #[test]
1171 fn invariant_total_size_does_not_exceed_available() {
1172 for total in [10u16, 50, 100, 255] {
1174 let flex = Flex::horizontal().constraints([
1175 Constraint::Fixed(30),
1176 Constraint::Percentage(50.0),
1177 Constraint::Min(20),
1178 ]);
1179 let rects = flex.split(Rect::new(0, 0, total, 10));
1180 let total_width: u16 = rects.iter().map(|r| r.width).sum();
1181 assert!(
1182 total_width <= total,
1183 "Total width {} exceeded available {} for constraints",
1184 total_width,
1185 total
1186 );
1187 }
1188 }
1189
1190 #[test]
1191 fn invariant_empty_area_produces_empty_rects() {
1192 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1193 let rects = flex.split(Rect::new(0, 0, 0, 0));
1194 assert!(rects.iter().all(|r| r.is_empty()));
1195 }
1196
1197 #[test]
1198 fn invariant_no_constraints_produces_empty_vec() {
1199 let flex = Flex::horizontal().constraints([]);
1200 let rects = flex.split(Rect::new(0, 0, 100, 100));
1201 assert!(rects.is_empty());
1202 }
1203
1204 #[test]
1207 fn ratio_constraint_splits_proportionally() {
1208 let flex =
1209 Flex::horizontal().constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
1210 let rects = flex.split(Rect::new(0, 0, 90, 10));
1211 assert_eq!(rects[0].width, 30);
1212 assert_eq!(rects[1].width, 60);
1213 }
1214
1215 #[test]
1216 fn ratio_constraint_with_zero_denominator() {
1217 let flex = Flex::horizontal().constraints([Constraint::Ratio(1, 0)]);
1219 let rects = flex.split(Rect::new(0, 0, 100, 10));
1220 assert_eq!(rects.len(), 1);
1221 }
1222
1223 #[test]
1224 fn ratio_zero_numerator_should_be_zero() {
1225 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
1228 let rects = flex.split(Rect::new(0, 0, 100, 1));
1229
1230 assert_eq!(rects[0].width, 100, "Fill should take all space");
1232 assert_eq!(rects[1].width, 0, "Ratio(0, 1) should be width 0");
1233 }
1234
1235 #[test]
1238 fn max_constraint_clamps_size() {
1239 let flex = Flex::horizontal().constraints([Constraint::Max(20), Constraint::Fixed(30)]);
1240 let rects = flex.split(Rect::new(0, 0, 100, 10));
1241 assert!(rects[0].width <= 20);
1242 assert_eq!(rects[1].width, 30);
1243 }
1244
1245 #[test]
1248 fn alignment_space_around() {
1249 let flex = Flex::horizontal()
1250 .alignment(Alignment::SpaceAround)
1251 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1252 let rects = flex.split(Rect::new(0, 0, 100, 10));
1253
1254 assert_eq!(rects[0].x, 20);
1257 assert_eq!(rects[1].x, 70);
1258 }
1259
1260 #[test]
1263 fn vertical_gap() {
1264 let flex = Flex::vertical()
1265 .gap(5)
1266 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1267 let rects = flex.split(Rect::new(0, 0, 50, 100));
1268 assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
1269 assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
1270 }
1271
1272 #[test]
1275 fn vertical_center() {
1276 let flex = Flex::vertical()
1277 .alignment(Alignment::Center)
1278 .constraints([Constraint::Fixed(10)]);
1279 let rects = flex.split(Rect::new(0, 0, 50, 100));
1280 assert_eq!(rects[0].y, 45);
1282 assert_eq!(rects[0].height, 10);
1283 }
1284
1285 #[test]
1288 fn single_min_takes_all() {
1289 let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
1290 let rects = flex.split(Rect::new(0, 0, 80, 24));
1291 assert_eq!(rects[0].width, 80);
1292 }
1293
1294 #[test]
1297 fn fixed_exceeds_available_clamped() {
1298 let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
1299 let rects = flex.split(Rect::new(0, 0, 100, 10));
1300 assert_eq!(rects[0].width, 60);
1302 assert_eq!(rects[1].width, 40);
1303 }
1304
1305 #[test]
1308 fn percentage_overflow_clamped() {
1309 let flex = Flex::horizontal()
1310 .constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
1311 let rects = flex.split(Rect::new(0, 0, 100, 10));
1312 assert_eq!(rects[0].width, 80);
1313 assert_eq!(rects[1].width, 20); }
1315
1316 #[test]
1319 fn margin_reduces_split_area() {
1320 let flex = Flex::horizontal()
1321 .margin(Sides::all(10))
1322 .constraints([Constraint::Fixed(20), Constraint::Min(0)]);
1323 let rects = flex.split(Rect::new(0, 0, 100, 100));
1324 assert_eq!(rects[0].x, 10);
1326 assert_eq!(rects[0].y, 10);
1327 assert_eq!(rects[0].width, 20);
1328 assert_eq!(rects[0].height, 80);
1329 }
1330
1331 #[test]
1334 fn builder_methods_chain() {
1335 let flex = Flex::vertical()
1336 .direction(Direction::Horizontal)
1337 .gap(3)
1338 .margin(Sides::all(1))
1339 .alignment(Alignment::End)
1340 .constraints([Constraint::Fixed(10)]);
1341 let rects = flex.split(Rect::new(0, 0, 50, 50));
1342 assert_eq!(rects.len(), 1);
1343 }
1344
1345 #[test]
1348 fn space_between_single_item() {
1349 let flex = Flex::horizontal()
1350 .alignment(Alignment::SpaceBetween)
1351 .constraints([Constraint::Fixed(10)]);
1352 let rects = flex.split(Rect::new(0, 0, 100, 10));
1353 assert_eq!(rects[0].x, 0);
1355 assert_eq!(rects[0].width, 10);
1356 }
1357
1358 #[test]
1359 fn invariant_rects_within_bounds() {
1360 let area = Rect::new(10, 20, 80, 60);
1361 let flex = Flex::horizontal()
1362 .margin(Sides::all(5))
1363 .gap(2)
1364 .constraints([
1365 Constraint::Fixed(15),
1366 Constraint::Percentage(30.0),
1367 Constraint::Min(10),
1368 ]);
1369 let rects = flex.split(area);
1370
1371 let inner = area.inner(Sides::all(5));
1373 for rect in &rects {
1374 assert!(
1375 rect.x >= inner.x && rect.right() <= inner.right(),
1376 "Rect {:?} exceeds horizontal bounds of {:?}",
1377 rect,
1378 inner
1379 );
1380 assert!(
1381 rect.y >= inner.y && rect.bottom() <= inner.bottom(),
1382 "Rect {:?} exceeds vertical bounds of {:?}",
1383 rect,
1384 inner
1385 );
1386 }
1387 }
1388
1389 #[test]
1392 fn fill_takes_remaining_space() {
1393 let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
1394 let rects = flex.split(Rect::new(0, 0, 100, 10));
1395 assert_eq!(rects[0].width, 20);
1396 assert_eq!(rects[1].width, 80); }
1398
1399 #[test]
1400 fn multiple_fills_share_space() {
1401 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
1402 let rects = flex.split(Rect::new(0, 0, 100, 10));
1403 assert_eq!(rects[0].width, 50);
1404 assert_eq!(rects[1].width, 50);
1405 }
1406
1407 #[test]
1410 fn fit_content_uses_preferred_size() {
1411 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1412 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1413 if idx == 0 {
1414 LayoutSizeHint {
1415 min: 5,
1416 preferred: 30,
1417 max: None,
1418 }
1419 } else {
1420 LayoutSizeHint::ZERO
1421 }
1422 });
1423 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
1426
1427 #[test]
1428 fn fit_content_clamps_to_available() {
1429 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
1430 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1431 min: 10,
1432 preferred: 80,
1433 max: None,
1434 });
1435 assert_eq!(rects[0].width, 80);
1437 assert_eq!(rects[1].width, 20);
1438 }
1439
1440 #[test]
1441 fn fit_content_without_measurer_gets_zero() {
1442 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1444 let rects = flex.split(Rect::new(0, 0, 100, 10));
1445 assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
1448
1449 #[test]
1450 fn fit_content_zero_area_returns_empty_rects() {
1451 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1452 let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
1453 min: 5,
1454 preferred: 10,
1455 max: None,
1456 });
1457 assert_eq!(rects.len(), 2);
1458 assert_eq!(rects[0].width, 0);
1459 assert_eq!(rects[0].height, 0);
1460 assert_eq!(rects[1].width, 0);
1461 assert_eq!(rects[1].height, 0);
1462 }
1463
1464 #[test]
1465 fn fit_content_tiny_available_clamps_to_remaining() {
1466 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1467 let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
1468 min: 5,
1469 preferred: 10,
1470 max: None,
1471 });
1472 assert_eq!(rects[0].width, 1);
1473 assert_eq!(rects[1].width, 0);
1474 }
1475
1476 #[test]
1479 fn fit_content_bounded_clamps_to_min() {
1480 let flex = Flex::horizontal().constraints([
1481 Constraint::FitContentBounded { min: 20, max: 50 },
1482 Constraint::Fill,
1483 ]);
1484 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1485 min: 5,
1486 preferred: 10, max: None,
1488 });
1489 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
1491 }
1492
1493 #[test]
1494 fn fit_content_bounded_respects_small_available() {
1495 let flex = Flex::horizontal().constraints([
1496 Constraint::FitContentBounded { min: 20, max: 50 },
1497 Constraint::Fill,
1498 ]);
1499 let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
1500 min: 5,
1501 preferred: 10,
1502 max: None,
1503 });
1504 assert_eq!(rects[0].width, 5);
1506 assert_eq!(rects[1].width, 0);
1507 }
1508
1509 #[test]
1510 fn fit_content_vertical_uses_preferred_height() {
1511 let flex = Flex::vertical().constraints([Constraint::FitContent, Constraint::Fill]);
1512 let rects = flex.split_with_measurer(Rect::new(0, 0, 10, 10), |idx, _| {
1513 if idx == 0 {
1514 LayoutSizeHint {
1515 min: 1,
1516 preferred: 4,
1517 max: None,
1518 }
1519 } else {
1520 LayoutSizeHint::ZERO
1521 }
1522 });
1523 assert_eq!(rects[0].height, 4);
1524 assert_eq!(rects[1].height, 6);
1525 }
1526
1527 #[test]
1528 fn fit_content_bounded_clamps_to_max() {
1529 let flex = Flex::horizontal().constraints([
1530 Constraint::FitContentBounded { min: 10, max: 30 },
1531 Constraint::Fill,
1532 ]);
1533 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1534 min: 5,
1535 preferred: 50, max: None,
1537 });
1538 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
1540 }
1541
1542 #[test]
1543 fn fit_content_bounded_uses_preferred_when_in_range() {
1544 let flex = Flex::horizontal().constraints([
1545 Constraint::FitContentBounded { min: 10, max: 50 },
1546 Constraint::Fill,
1547 ]);
1548 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1549 min: 5,
1550 preferred: 35, max: None,
1552 });
1553 assert_eq!(rects[0].width, 35);
1554 assert_eq!(rects[1].width, 65);
1555 }
1556
1557 #[test]
1560 fn fit_min_uses_minimum_size() {
1561 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1562 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1563 if idx == 0 {
1564 LayoutSizeHint {
1565 min: 15,
1566 preferred: 40,
1567 max: None,
1568 }
1569 } else {
1570 LayoutSizeHint::ZERO
1571 }
1572 });
1573 let total: u16 = rects.iter().map(|r| r.width).sum();
1587 assert_eq!(total, 100);
1588 assert!(rects[0].width >= 15, "FitMin should get at least minimum");
1589 }
1590
1591 #[test]
1592 fn fit_min_without_measurer_gets_zero() {
1593 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1594 let rects = flex.split(Rect::new(0, 0, 100, 10));
1595 assert_eq!(rects[0].width, 50);
1598 assert_eq!(rects[1].width, 50);
1599 }
1600
1601 #[test]
1604 fn layout_size_hint_zero_is_default() {
1605 assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
1606 }
1607
1608 #[test]
1609 fn layout_size_hint_exact() {
1610 let h = LayoutSizeHint::exact(25);
1611 assert_eq!(h.min, 25);
1612 assert_eq!(h.preferred, 25);
1613 assert_eq!(h.max, Some(25));
1614 }
1615
1616 #[test]
1617 fn layout_size_hint_at_least() {
1618 let h = LayoutSizeHint::at_least(10, 30);
1619 assert_eq!(h.min, 10);
1620 assert_eq!(h.preferred, 30);
1621 assert_eq!(h.max, None);
1622 }
1623
1624 #[test]
1625 fn layout_size_hint_clamp() {
1626 let h = LayoutSizeHint {
1627 min: 10,
1628 preferred: 20,
1629 max: Some(30),
1630 };
1631 assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
1635
1636 #[test]
1637 fn layout_size_hint_clamp_unbounded() {
1638 let h = LayoutSizeHint::at_least(5, 10);
1639 assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
1642
1643 #[test]
1646 fn fit_content_with_fixed_and_fill() {
1647 let flex = Flex::horizontal().constraints([
1648 Constraint::Fixed(20),
1649 Constraint::FitContent,
1650 Constraint::Fill,
1651 ]);
1652 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1653 if idx == 1 {
1654 LayoutSizeHint {
1655 min: 5,
1656 preferred: 25,
1657 max: None,
1658 }
1659 } else {
1660 LayoutSizeHint::ZERO
1661 }
1662 });
1663 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
1667
1668 #[test]
1669 fn total_allocation_never_exceeds_available_with_fit_content() {
1670 for available in [10u16, 50, 100, 255] {
1671 let flex = Flex::horizontal().constraints([
1672 Constraint::FitContent,
1673 Constraint::FitContent,
1674 Constraint::Fill,
1675 ]);
1676 let rects =
1677 flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
1678 min: 10,
1679 preferred: 40,
1680 max: None,
1681 });
1682 let total: u16 = rects.iter().map(|r| r.width).sum();
1683 assert!(
1684 total <= available,
1685 "Total {} exceeded available {} with FitContent",
1686 total,
1687 available
1688 );
1689 }
1690 }
1691
1692 mod rounding_tests {
1697 use super::super::*;
1698
1699 #[test]
1702 fn rounding_conserves_sum_exact() {
1703 let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
1704 assert_eq!(result.iter().copied().sum::<u16>(), 40);
1705 assert_eq!(result, vec![10, 20, 10]);
1706 }
1707
1708 #[test]
1709 fn rounding_conserves_sum_fractional() {
1710 let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
1711 assert_eq!(
1712 result.iter().copied().sum::<u16>(),
1713 40,
1714 "Sum must equal total: {:?}",
1715 result
1716 );
1717 }
1718
1719 #[test]
1720 fn rounding_conserves_sum_many_fractions() {
1721 let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
1722 let result = round_layout_stable(&targets, 100, None);
1723 assert_eq!(
1724 result.iter().copied().sum::<u16>(),
1725 100,
1726 "Sum must be exactly 100: {:?}",
1727 result
1728 );
1729 }
1730
1731 #[test]
1732 fn rounding_conserves_sum_all_half() {
1733 let targets = vec![10.5, 10.5, 10.5, 10.5];
1734 let result = round_layout_stable(&targets, 42, None);
1735 assert_eq!(
1736 result.iter().copied().sum::<u16>(),
1737 42,
1738 "Sum must be exactly 42: {:?}",
1739 result
1740 );
1741 }
1742
1743 #[test]
1746 fn rounding_displacement_bounded() {
1747 let targets = vec![33.33, 33.33, 33.34];
1748 let result = round_layout_stable(&targets, 100, None);
1749 assert_eq!(result.iter().copied().sum::<u16>(), 100);
1750
1751 for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
1752 let floor = r.floor() as u16;
1753 let ceil = floor + 1;
1754 assert!(
1755 x == floor || x == ceil,
1756 "Element {} = {} not in {{floor={}, ceil={}}} of target {}",
1757 i,
1758 x,
1759 floor,
1760 ceil,
1761 r
1762 );
1763 }
1764 }
1765
1766 #[test]
1769 fn temporal_tiebreak_stable_when_unchanged() {
1770 let targets = vec![10.5, 10.5, 10.5, 10.5];
1771 let first = round_layout_stable(&targets, 42, None);
1772 let second = round_layout_stable(&targets, 42, Some(first.clone()));
1773 assert_eq!(
1774 first, second,
1775 "Identical targets should produce identical results"
1776 );
1777 }
1778
1779 #[test]
1780 fn temporal_tiebreak_prefers_previous_direction() {
1781 let targets = vec![10.5, 10.5];
1782 let total = 21;
1783 let first = round_layout_stable(&targets, total, None);
1784 assert_eq!(first.iter().copied().sum::<u16>(), total);
1785 let second = round_layout_stable(&targets, total, Some(first.clone()));
1786 assert_eq!(first, second, "Should maintain rounding direction");
1787 }
1788
1789 #[test]
1790 fn temporal_tiebreak_adapts_to_changed_targets() {
1791 let targets_a = vec![10.5, 10.5];
1792 let result_a = round_layout_stable(&targets_a, 21, None);
1793 let targets_b = vec![15.7, 5.3];
1794 let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
1795 assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
1796 assert!(result_b[0] > result_b[1], "Should follow larger target");
1797 }
1798
1799 #[test]
1802 fn property_min_displacement_brute_force_small() {
1803 let targets = vec![3.3, 3.3, 3.4];
1804 let total: u16 = 10;
1805 let result = round_layout_stable(&targets, total, None);
1806 let our_displacement: f64 = result
1807 .iter()
1808 .zip(targets.iter())
1809 .map(|(&x, &r)| (x as f64 - r).abs())
1810 .sum();
1811
1812 let mut min_displacement = f64::MAX;
1813 let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
1814 let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
1815
1816 for a in floors[0]..=ceils[0] {
1817 for b in floors[1]..=ceils[1] {
1818 for c in floors[2]..=ceils[2] {
1819 if a + b + c == total {
1820 let disp = (a as f64 - targets[0]).abs()
1821 + (b as f64 - targets[1]).abs()
1822 + (c as f64 - targets[2]).abs();
1823 if disp < min_displacement {
1824 min_displacement = disp;
1825 }
1826 }
1827 }
1828 }
1829 }
1830
1831 assert!(
1832 (our_displacement - min_displacement).abs() < 1e-10,
1833 "Our displacement {} should match optimal {}: {:?}",
1834 our_displacement,
1835 min_displacement,
1836 result
1837 );
1838 }
1839
1840 #[test]
1843 fn rounding_deterministic() {
1844 let targets = vec![7.7, 8.3, 14.0];
1845 let a = round_layout_stable(&targets, 30, None);
1846 let b = round_layout_stable(&targets, 30, None);
1847 assert_eq!(a, b, "Same inputs must produce identical outputs");
1848 }
1849
1850 #[test]
1853 fn rounding_empty_targets() {
1854 let result = round_layout_stable(&[], 0, None);
1855 assert!(result.is_empty());
1856 }
1857
1858 #[test]
1859 fn rounding_single_element() {
1860 let result = round_layout_stable(&[10.7], 11, None);
1861 assert_eq!(result, vec![11]);
1862 }
1863
1864 #[test]
1865 fn rounding_zero_total() {
1866 let result = round_layout_stable(&[5.0, 5.0], 0, None);
1867 assert_eq!(result.iter().copied().sum::<u16>(), 0);
1868 }
1869
1870 #[test]
1871 fn rounding_all_zeros() {
1872 let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
1873 assert_eq!(result, vec![0, 0, 0]);
1874 }
1875
1876 #[test]
1877 fn rounding_integer_targets() {
1878 let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
1879 assert_eq!(result, vec![10, 20, 30]);
1880 }
1881
1882 #[test]
1883 fn rounding_large_deficit() {
1884 let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
1885 assert_eq!(result.iter().copied().sum::<u16>(), 3);
1886 assert_eq!(result, vec![1, 1, 1]);
1887 }
1888
1889 #[test]
1890 fn rounding_with_prev_different_length() {
1891 let result = round_layout_stable(&[10.5, 10.5], 21, Some(vec![11, 10, 5]));
1892 assert_eq!(result.iter().copied().sum::<u16>(), 21);
1893 }
1894
1895 #[test]
1896 fn rounding_very_small_fractions() {
1897 let targets = vec![10.001, 20.001, 9.998];
1898 let result = round_layout_stable(&targets, 40, None);
1899 assert_eq!(result.iter().copied().sum::<u16>(), 40);
1900 }
1901
1902 #[test]
1903 fn rounding_conserves_sum_stress() {
1904 let n = 50;
1905 let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
1906 let total = 120u16;
1907 let result = round_layout_stable(&targets, total, None);
1908 assert_eq!(
1909 result.iter().copied().sum::<u16>(),
1910 total,
1911 "Sum must be exactly {} for {} items: {:?}",
1912 total,
1913 n,
1914 result
1915 );
1916 }
1917 }
1918
1919 mod property_constraint_tests {
1924 use super::super::*;
1925
1926 struct Lcg(u64);
1928
1929 impl Lcg {
1930 fn new(seed: u64) -> Self {
1931 Self(seed)
1932 }
1933 fn next_u32(&mut self) -> u32 {
1934 self.0 = self
1935 .0
1936 .wrapping_mul(6_364_136_223_846_793_005)
1937 .wrapping_add(1);
1938 (self.0 >> 33) as u32
1939 }
1940 fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
1941 if lo >= hi {
1942 return lo;
1943 }
1944 lo + (self.next_u32() % (hi - lo) as u32) as u16
1945 }
1946 fn next_f32(&mut self) -> f32 {
1947 (self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
1948 }
1949 }
1950
1951 fn random_constraint(rng: &mut Lcg) -> Constraint {
1953 match rng.next_u32() % 7 {
1954 0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
1955 1 => Constraint::Percentage(rng.next_f32() * 100.0),
1956 2 => Constraint::Min(rng.next_u16_range(0, 40)),
1957 3 => Constraint::Max(rng.next_u16_range(5, 120)),
1958 4 => {
1959 let n = rng.next_u32() % 5 + 1;
1960 let d = rng.next_u32() % 5 + 1;
1961 Constraint::Ratio(n, d)
1962 }
1963 5 => Constraint::Fill,
1964 _ => Constraint::FitContent,
1965 }
1966 }
1967
1968 #[test]
1969 fn property_constraints_respected_fixed() {
1970 let mut rng = Lcg::new(0xDEAD_BEEF);
1971 for _ in 0..200 {
1972 let fixed_val = rng.next_u16_range(1, 60);
1973 let avail = rng.next_u16_range(10, 200);
1974 let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
1975 let rects = flex.split(Rect::new(0, 0, avail, 10));
1976 assert!(
1977 rects[0].width <= fixed_val.min(avail),
1978 "Fixed({}) in avail {} -> width {}",
1979 fixed_val,
1980 avail,
1981 rects[0].width
1982 );
1983 }
1984 }
1985
1986 #[test]
1987 fn property_constraints_respected_max() {
1988 let mut rng = Lcg::new(0xCAFE_BABE);
1989 for _ in 0..200 {
1990 let max_val = rng.next_u16_range(5, 80);
1991 let avail = rng.next_u16_range(10, 200);
1992 let flex =
1993 Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
1994 let rects = flex.split(Rect::new(0, 0, avail, 10));
1995 assert!(
1996 rects[0].width <= max_val,
1997 "Max({}) in avail {} -> width {}",
1998 max_val,
1999 avail,
2000 rects[0].width
2001 );
2002 }
2003 }
2004
2005 #[test]
2006 fn property_constraints_respected_min() {
2007 let mut rng = Lcg::new(0xBAAD_F00D);
2008 for _ in 0..200 {
2009 let min_val = rng.next_u16_range(0, 40);
2010 let avail = rng.next_u16_range(min_val.max(1), 200);
2011 let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
2012 let rects = flex.split(Rect::new(0, 0, avail, 10));
2013 assert!(
2014 rects[0].width >= min_val,
2015 "Min({}) in avail {} -> width {}",
2016 min_val,
2017 avail,
2018 rects[0].width
2019 );
2020 }
2021 }
2022
2023 #[test]
2024 fn property_constraints_respected_ratio_proportional() {
2025 let mut rng = Lcg::new(0x1234_5678);
2026 for _ in 0..200 {
2027 let n1 = rng.next_u32() % 5 + 1;
2028 let n2 = rng.next_u32() % 5 + 1;
2029 let d = rng.next_u32() % 5 + 1;
2030 let avail = rng.next_u16_range(20, 200);
2031 let flex = Flex::horizontal()
2032 .constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
2033 let rects = flex.split(Rect::new(0, 0, avail, 10));
2034 let w1 = rects[0].width as f64;
2035 let w2 = rects[1].width as f64;
2036 let total = w1 + w2;
2037 if total > 0.0 {
2038 let expected_ratio = n1 as f64 / (n1 + n2) as f64;
2039 let actual_ratio = w1 / total;
2040 assert!(
2041 (actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
2042 "Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
2043 n1,
2044 d,
2045 n1,
2046 n2,
2047 avail,
2048 expected_ratio,
2049 actual_ratio,
2050 w1,
2051 w2
2052 );
2053 }
2054 }
2055 }
2056
2057 #[test]
2058 fn property_total_allocation_never_exceeds_available() {
2059 let mut rng = Lcg::new(0xFACE_FEED);
2060 for _ in 0..500 {
2061 let n = (rng.next_u32() % 6 + 1) as usize;
2062 let constraints: Vec<Constraint> =
2063 (0..n).map(|_| random_constraint(&mut rng)).collect();
2064 let avail = rng.next_u16_range(5, 200);
2065 let dir = if rng.next_u32().is_multiple_of(2) {
2066 Direction::Horizontal
2067 } else {
2068 Direction::Vertical
2069 };
2070 let flex = Flex::default().direction(dir).constraints(constraints);
2071 let area = Rect::new(0, 0, avail, avail);
2072 let rects = flex.split(area);
2073 let total: u16 = rects
2074 .iter()
2075 .map(|r| match dir {
2076 Direction::Horizontal => r.width,
2077 Direction::Vertical => r.height,
2078 })
2079 .sum();
2080 assert!(
2081 total <= avail,
2082 "Total {} exceeded available {} with {} constraints",
2083 total,
2084 avail,
2085 n
2086 );
2087 }
2088 }
2089
2090 #[test]
2091 fn property_no_overlap_horizontal() {
2092 let mut rng = Lcg::new(0xABCD_1234);
2093 for _ in 0..300 {
2094 let n = (rng.next_u32() % 5 + 2) as usize;
2095 let constraints: Vec<Constraint> =
2096 (0..n).map(|_| random_constraint(&mut rng)).collect();
2097 let avail = rng.next_u16_range(20, 200);
2098 let flex = Flex::horizontal().constraints(constraints);
2099 let rects = flex.split(Rect::new(0, 0, avail, 10));
2100
2101 for i in 1..rects.len() {
2102 let prev_end = rects[i - 1].x + rects[i - 1].width;
2103 assert!(
2104 rects[i].x >= prev_end,
2105 "Overlap at {}: prev ends {}, next starts {}",
2106 i,
2107 prev_end,
2108 rects[i].x
2109 );
2110 }
2111 }
2112 }
2113
2114 #[test]
2115 fn property_deterministic_across_runs() {
2116 let mut rng = Lcg::new(0x9999_8888);
2117 for _ in 0..100 {
2118 let n = (rng.next_u32() % 5 + 1) as usize;
2119 let constraints: Vec<Constraint> =
2120 (0..n).map(|_| random_constraint(&mut rng)).collect();
2121 let avail = rng.next_u16_range(10, 200);
2122 let r1 = Flex::horizontal()
2123 .constraints(constraints.clone())
2124 .split(Rect::new(0, 0, avail, 10));
2125 let r2 = Flex::horizontal()
2126 .constraints(constraints)
2127 .split(Rect::new(0, 0, avail, 10));
2128 assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
2129 }
2130 }
2131 }
2132
2133 mod property_temporal_tests {
2138 use super::super::*;
2139 use crate::cache::{CoherenceCache, CoherenceId};
2140
2141 struct Lcg(u64);
2143
2144 impl Lcg {
2145 fn new(seed: u64) -> Self {
2146 Self(seed)
2147 }
2148 fn next_u32(&mut self) -> u32 {
2149 self.0 = self
2150 .0
2151 .wrapping_mul(6_364_136_223_846_793_005)
2152 .wrapping_add(1);
2153 (self.0 >> 33) as u32
2154 }
2155 }
2156
2157 #[test]
2158 fn property_temporal_stability_small_resize() {
2159 let constraints = [
2160 Constraint::Percentage(33.3),
2161 Constraint::Percentage(33.3),
2162 Constraint::Fill,
2163 ];
2164 let mut coherence = CoherenceCache::new(64);
2165 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2166
2167 for total in [80u16, 100, 120] {
2168 let flex = Flex::horizontal().constraints(constraints);
2169 let rects = flex.split(Rect::new(0, 0, total, 10));
2170 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2171
2172 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2173 let prev = coherence.get(&id);
2174 let rounded = round_layout_stable(&targets, total, prev);
2175
2176 if let Some(old) = coherence.get(&id) {
2177 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2178 assert!(
2179 max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
2180 "max_disp={} too large for size change {} -> {}",
2181 max_disp,
2182 old.iter().copied().sum::<u16>(),
2183 total
2184 );
2185 let _ = sum_disp;
2186 }
2187 coherence.store(id, rounded);
2188 }
2189 }
2190
2191 #[test]
2192 fn property_temporal_stability_random_walk() {
2193 let constraints = [
2194 Constraint::Ratio(1, 3),
2195 Constraint::Ratio(1, 3),
2196 Constraint::Ratio(1, 3),
2197 ];
2198 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2199 let mut coherence = CoherenceCache::new(64);
2200 let mut rng = Lcg::new(0x5555_AAAA);
2201 let mut total: u16 = 90;
2202
2203 for step in 0..200 {
2204 let prev_total = total;
2205 let delta = (rng.next_u32() % 7) as i32 - 3;
2206 total = (total as i32 + delta).clamp(10, 250) as u16;
2207
2208 let flex = Flex::horizontal().constraints(constraints);
2209 let rects = flex.split(Rect::new(0, 0, total, 10));
2210 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2211
2212 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2213 let prev = coherence.get(&id);
2214 let rounded = round_layout_stable(&targets, total, prev);
2215
2216 if coherence.get(&id).is_some() {
2217 let (_, max_disp) = coherence.displacement(&id, &rounded);
2218 let size_change = total.abs_diff(prev_total);
2219 assert!(
2220 max_disp <= size_change as u32 + 2,
2221 "step {}: max_disp={} exceeds size_change={} + 2",
2222 step,
2223 max_disp,
2224 size_change
2225 );
2226 }
2227 coherence.store(id, rounded);
2228 }
2229 }
2230
2231 #[test]
2232 fn property_temporal_stability_identical_frames() {
2233 let constraints = [
2234 Constraint::Fixed(20),
2235 Constraint::Fill,
2236 Constraint::Fixed(15),
2237 ];
2238 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2239 let mut coherence = CoherenceCache::new(64);
2240
2241 let flex = Flex::horizontal().constraints(constraints);
2242 let rects = flex.split(Rect::new(0, 0, 100, 10));
2243 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2244 coherence.store(id, widths.clone());
2245
2246 for _ in 0..10 {
2247 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2248 let prev = coherence.get(&id);
2249 let rounded = round_layout_stable(&targets, 100, prev);
2250 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2251 assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
2252 assert_eq!(max_disp, 0);
2253 coherence.store(id, rounded);
2254 }
2255 }
2256
2257 #[test]
2258 fn property_temporal_coherence_sweep() {
2259 let constraints = [
2260 Constraint::Percentage(25.0),
2261 Constraint::Percentage(50.0),
2262 Constraint::Fill,
2263 ];
2264 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2265 let mut coherence = CoherenceCache::new(64);
2266 let mut total_displacement: u64 = 0;
2267
2268 for total in 60u16..=140 {
2269 let flex = Flex::horizontal().constraints(constraints);
2270 let rects = flex.split(Rect::new(0, 0, total, 10));
2271 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2272
2273 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2274 let prev = coherence.get(&id);
2275 let rounded = round_layout_stable(&targets, total, prev);
2276
2277 if coherence.get(&id).is_some() {
2278 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2279 total_displacement += sum_disp;
2280 }
2281 coherence.store(id, rounded);
2282 }
2283
2284 assert!(
2285 total_displacement <= 80 * 3,
2286 "Total displacement {} exceeds bound for 80-step sweep",
2287 total_displacement
2288 );
2289 }
2290 }
2291
2292 mod snapshot_layout_tests {
2297 use super::super::*;
2298 use crate::grid::{Grid, GridArea};
2299
2300 fn snapshot_flex(
2301 constraints: &[Constraint],
2302 dir: Direction,
2303 width: u16,
2304 height: u16,
2305 ) -> String {
2306 let flex = Flex::default()
2307 .direction(dir)
2308 .constraints(constraints.iter().copied());
2309 let rects = flex.split(Rect::new(0, 0, width, height));
2310 let mut out = format!(
2311 "Flex {:?} {}x{} ({} constraints)\n",
2312 dir,
2313 width,
2314 height,
2315 constraints.len()
2316 );
2317 for (i, r) in rects.iter().enumerate() {
2318 out.push_str(&format!(
2319 " [{}] x={} y={} w={} h={}\n",
2320 i, r.x, r.y, r.width, r.height
2321 ));
2322 }
2323 let total: u16 = rects
2324 .iter()
2325 .map(|r| match dir {
2326 Direction::Horizontal => r.width,
2327 Direction::Vertical => r.height,
2328 })
2329 .sum();
2330 out.push_str(&format!(" total={}\n", total));
2331 out
2332 }
2333
2334 fn snapshot_grid(
2335 rows: &[Constraint],
2336 cols: &[Constraint],
2337 areas: &[(&str, GridArea)],
2338 width: u16,
2339 height: u16,
2340 ) -> String {
2341 let mut grid = Grid::new()
2342 .rows(rows.iter().copied())
2343 .columns(cols.iter().copied());
2344 for &(name, area) in areas {
2345 grid = grid.area(name, area);
2346 }
2347 let layout = grid.split(Rect::new(0, 0, width, height));
2348
2349 let mut out = format!(
2350 "Grid {}x{} ({}r x {}c)\n",
2351 width,
2352 height,
2353 rows.len(),
2354 cols.len()
2355 );
2356 for r in 0..rows.len() {
2357 for c in 0..cols.len() {
2358 let rect = layout.cell(r, c);
2359 out.push_str(&format!(
2360 " [{},{}] x={} y={} w={} h={}\n",
2361 r, c, rect.x, rect.y, rect.width, rect.height
2362 ));
2363 }
2364 }
2365 for &(name, _) in areas {
2366 if let Some(rect) = layout.area(name) {
2367 out.push_str(&format!(
2368 " area({}) x={} y={} w={} h={}\n",
2369 name, rect.x, rect.y, rect.width, rect.height
2370 ));
2371 }
2372 }
2373 out
2374 }
2375
2376 #[test]
2379 fn snapshot_flex_thirds_80x24() {
2380 let snap = snapshot_flex(
2381 &[
2382 Constraint::Ratio(1, 3),
2383 Constraint::Ratio(1, 3),
2384 Constraint::Ratio(1, 3),
2385 ],
2386 Direction::Horizontal,
2387 80,
2388 24,
2389 );
2390 assert_eq!(
2391 snap,
2392 "\
2393Flex Horizontal 80x24 (3 constraints)
2394 [0] x=0 y=0 w=26 h=24
2395 [1] x=26 y=0 w=26 h=24
2396 [2] x=52 y=0 w=28 h=24
2397 total=80
2398"
2399 );
2400 }
2401
2402 #[test]
2403 fn snapshot_flex_sidebar_content_80x24() {
2404 let snap = snapshot_flex(
2405 &[Constraint::Fixed(20), Constraint::Fill],
2406 Direction::Horizontal,
2407 80,
2408 24,
2409 );
2410 assert_eq!(
2411 snap,
2412 "\
2413Flex Horizontal 80x24 (2 constraints)
2414 [0] x=0 y=0 w=20 h=24
2415 [1] x=20 y=0 w=60 h=24
2416 total=80
2417"
2418 );
2419 }
2420
2421 #[test]
2422 fn snapshot_flex_header_body_footer_80x24() {
2423 let snap = snapshot_flex(
2424 &[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
2425 Direction::Vertical,
2426 80,
2427 24,
2428 );
2429 assert_eq!(
2430 snap,
2431 "\
2432Flex Vertical 80x24 (3 constraints)
2433 [0] x=0 y=0 w=80 h=3
2434 [1] x=0 y=3 w=80 h=20
2435 [2] x=0 y=23 w=80 h=1
2436 total=24
2437"
2438 );
2439 }
2440
2441 #[test]
2444 fn snapshot_flex_thirds_120x40() {
2445 let snap = snapshot_flex(
2446 &[
2447 Constraint::Ratio(1, 3),
2448 Constraint::Ratio(1, 3),
2449 Constraint::Ratio(1, 3),
2450 ],
2451 Direction::Horizontal,
2452 120,
2453 40,
2454 );
2455 assert_eq!(
2456 snap,
2457 "\
2458Flex Horizontal 120x40 (3 constraints)
2459 [0] x=0 y=0 w=40 h=40
2460 [1] x=40 y=0 w=40 h=40
2461 [2] x=80 y=0 w=40 h=40
2462 total=120
2463"
2464 );
2465 }
2466
2467 #[test]
2468 fn snapshot_flex_sidebar_content_120x40() {
2469 let snap = snapshot_flex(
2470 &[Constraint::Fixed(20), Constraint::Fill],
2471 Direction::Horizontal,
2472 120,
2473 40,
2474 );
2475 assert_eq!(
2476 snap,
2477 "\
2478Flex Horizontal 120x40 (2 constraints)
2479 [0] x=0 y=0 w=20 h=40
2480 [1] x=20 y=0 w=100 h=40
2481 total=120
2482"
2483 );
2484 }
2485
2486 #[test]
2487 fn snapshot_flex_percentage_mix_120x40() {
2488 let snap = snapshot_flex(
2489 &[
2490 Constraint::Percentage(25.0),
2491 Constraint::Percentage(50.0),
2492 Constraint::Fill,
2493 ],
2494 Direction::Horizontal,
2495 120,
2496 40,
2497 );
2498 assert_eq!(
2499 snap,
2500 "\
2501Flex Horizontal 120x40 (3 constraints)
2502 [0] x=0 y=0 w=30 h=40
2503 [1] x=30 y=0 w=60 h=40
2504 [2] x=90 y=0 w=30 h=40
2505 total=120
2506"
2507 );
2508 }
2509
2510 #[test]
2513 fn snapshot_grid_2x2_80x24() {
2514 let snap = snapshot_grid(
2515 &[Constraint::Fixed(3), Constraint::Fill],
2516 &[Constraint::Fixed(20), Constraint::Fill],
2517 &[
2518 ("header", GridArea::span(0, 0, 1, 2)),
2519 ("sidebar", GridArea::span(1, 0, 1, 1)),
2520 ("content", GridArea::cell(1, 1)),
2521 ],
2522 80,
2523 24,
2524 );
2525 assert_eq!(
2526 snap,
2527 "\
2528Grid 80x24 (2r x 2c)
2529 [0,0] x=0 y=0 w=20 h=3
2530 [0,1] x=20 y=0 w=60 h=3
2531 [1,0] x=0 y=3 w=20 h=21
2532 [1,1] x=20 y=3 w=60 h=21
2533 area(header) x=0 y=0 w=80 h=3
2534 area(sidebar) x=0 y=3 w=20 h=21
2535 area(content) x=20 y=3 w=60 h=21
2536"
2537 );
2538 }
2539
2540 #[test]
2541 fn snapshot_grid_3x3_80x24() {
2542 let snap = snapshot_grid(
2543 &[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
2544 &[
2545 Constraint::Fixed(10),
2546 Constraint::Fill,
2547 Constraint::Fixed(10),
2548 ],
2549 &[],
2550 80,
2551 24,
2552 );
2553 assert_eq!(
2554 snap,
2555 "\
2556Grid 80x24 (3r x 3c)
2557 [0,0] x=0 y=0 w=10 h=1
2558 [0,1] x=10 y=0 w=60 h=1
2559 [0,2] x=70 y=0 w=10 h=1
2560 [1,0] x=0 y=1 w=10 h=22
2561 [1,1] x=10 y=1 w=60 h=22
2562 [1,2] x=70 y=1 w=10 h=22
2563 [2,0] x=0 y=23 w=10 h=1
2564 [2,1] x=10 y=23 w=60 h=1
2565 [2,2] x=70 y=23 w=10 h=1
2566"
2567 );
2568 }
2569
2570 #[test]
2573 fn snapshot_grid_2x2_120x40() {
2574 let snap = snapshot_grid(
2575 &[Constraint::Fixed(3), Constraint::Fill],
2576 &[Constraint::Fixed(20), Constraint::Fill],
2577 &[
2578 ("header", GridArea::span(0, 0, 1, 2)),
2579 ("sidebar", GridArea::span(1, 0, 1, 1)),
2580 ("content", GridArea::cell(1, 1)),
2581 ],
2582 120,
2583 40,
2584 );
2585 assert_eq!(
2586 snap,
2587 "\
2588Grid 120x40 (2r x 2c)
2589 [0,0] x=0 y=0 w=20 h=3
2590 [0,1] x=20 y=0 w=100 h=3
2591 [1,0] x=0 y=3 w=20 h=37
2592 [1,1] x=20 y=3 w=100 h=37
2593 area(header) x=0 y=0 w=120 h=3
2594 area(sidebar) x=0 y=3 w=20 h=37
2595 area(content) x=20 y=3 w=100 h=37
2596"
2597 );
2598 }
2599
2600 #[test]
2601 fn snapshot_grid_dashboard_120x40() {
2602 let snap = snapshot_grid(
2603 &[
2604 Constraint::Fixed(3),
2605 Constraint::Percentage(60.0),
2606 Constraint::Fill,
2607 ],
2608 &[Constraint::Percentage(30.0), Constraint::Fill],
2609 &[
2610 ("nav", GridArea::span(0, 0, 1, 2)),
2611 ("chart", GridArea::cell(1, 0)),
2612 ("detail", GridArea::cell(1, 1)),
2613 ("log", GridArea::span(2, 0, 1, 2)),
2614 ],
2615 120,
2616 40,
2617 );
2618 assert_eq!(
2619 snap,
2620 "\
2621Grid 120x40 (3r x 2c)
2622 [0,0] x=0 y=0 w=36 h=3
2623 [0,1] x=36 y=0 w=84 h=3
2624 [1,0] x=0 y=3 w=36 h=24
2625 [1,1] x=36 y=3 w=84 h=24
2626 [2,0] x=0 y=27 w=36 h=13
2627 [2,1] x=36 y=27 w=84 h=13
2628 area(nav) x=0 y=0 w=120 h=3
2629 area(chart) x=0 y=3 w=36 h=24
2630 area(detail) x=36 y=3 w=84 h=24
2631 area(log) x=0 y=27 w=120 h=13
2632"
2633 );
2634 }
2635 }
2636}