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]
1246 fn percentage_rounding_never_exceeds_available() {
1247 let constraints = [
1248 Constraint::Percentage(33.4),
1249 Constraint::Percentage(33.3),
1250 Constraint::Percentage(33.3),
1251 ];
1252 let sizes = solve_constraints(&constraints, 7);
1253 let total: u16 = sizes.iter().sum();
1254 assert!(total <= 7, "percent rounding overflowed: {sizes:?}");
1255 assert!(sizes.iter().all(|size| *size <= 7));
1256 }
1257
1258 #[test]
1259 fn tiny_area_saturates_fixed_and_min() {
1260 let constraints = [Constraint::Fixed(5), Constraint::Min(3), Constraint::Max(2)];
1261 let sizes = solve_constraints(&constraints, 2);
1262 assert_eq!(sizes[0], 2);
1263 assert_eq!(sizes[1], 0);
1264 assert_eq!(sizes[2], 0);
1265 assert_eq!(sizes.iter().sum::<u16>(), 2);
1266 }
1267
1268 #[test]
1269 fn ratio_distribution_sums_to_available() {
1270 let constraints = [Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)];
1271 let sizes = solve_constraints(&constraints, 5);
1272 assert_eq!(sizes.iter().sum::<u16>(), 5);
1273 assert_eq!(sizes[0], 1);
1274 assert_eq!(sizes[1], 4);
1275 }
1276
1277 #[test]
1278 fn flex_gap_exceeds_area_yields_zero_widths() {
1279 let flex = Flex::horizontal()
1280 .gap(5)
1281 .constraints([Constraint::Fixed(1), Constraint::Fixed(1)]);
1282 let rects = flex.split(Rect::new(0, 0, 3, 1));
1283 assert_eq!(rects.len(), 2);
1284 assert_eq!(rects[0].width, 0);
1285 assert_eq!(rects[1].width, 0);
1286 }
1287
1288 #[test]
1291 fn alignment_space_around() {
1292 let flex = Flex::horizontal()
1293 .alignment(Alignment::SpaceAround)
1294 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1295 let rects = flex.split(Rect::new(0, 0, 100, 10));
1296
1297 assert_eq!(rects[0].x, 20);
1300 assert_eq!(rects[1].x, 70);
1301 }
1302
1303 #[test]
1306 fn vertical_gap() {
1307 let flex = Flex::vertical()
1308 .gap(5)
1309 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1310 let rects = flex.split(Rect::new(0, 0, 50, 100));
1311 assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
1312 assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
1313 }
1314
1315 #[test]
1318 fn vertical_center() {
1319 let flex = Flex::vertical()
1320 .alignment(Alignment::Center)
1321 .constraints([Constraint::Fixed(10)]);
1322 let rects = flex.split(Rect::new(0, 0, 50, 100));
1323 assert_eq!(rects[0].y, 45);
1325 assert_eq!(rects[0].height, 10);
1326 }
1327
1328 #[test]
1331 fn single_min_takes_all() {
1332 let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
1333 let rects = flex.split(Rect::new(0, 0, 80, 24));
1334 assert_eq!(rects[0].width, 80);
1335 }
1336
1337 #[test]
1340 fn fixed_exceeds_available_clamped() {
1341 let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
1342 let rects = flex.split(Rect::new(0, 0, 100, 10));
1343 assert_eq!(rects[0].width, 60);
1345 assert_eq!(rects[1].width, 40);
1346 }
1347
1348 #[test]
1351 fn percentage_overflow_clamped() {
1352 let flex = Flex::horizontal()
1353 .constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
1354 let rects = flex.split(Rect::new(0, 0, 100, 10));
1355 assert_eq!(rects[0].width, 80);
1356 assert_eq!(rects[1].width, 20); }
1358
1359 #[test]
1362 fn margin_reduces_split_area() {
1363 let flex = Flex::horizontal()
1364 .margin(Sides::all(10))
1365 .constraints([Constraint::Fixed(20), Constraint::Min(0)]);
1366 let rects = flex.split(Rect::new(0, 0, 100, 100));
1367 assert_eq!(rects[0].x, 10);
1369 assert_eq!(rects[0].y, 10);
1370 assert_eq!(rects[0].width, 20);
1371 assert_eq!(rects[0].height, 80);
1372 }
1373
1374 #[test]
1377 fn builder_methods_chain() {
1378 let flex = Flex::vertical()
1379 .direction(Direction::Horizontal)
1380 .gap(3)
1381 .margin(Sides::all(1))
1382 .alignment(Alignment::End)
1383 .constraints([Constraint::Fixed(10)]);
1384 let rects = flex.split(Rect::new(0, 0, 50, 50));
1385 assert_eq!(rects.len(), 1);
1386 }
1387
1388 #[test]
1391 fn space_between_single_item() {
1392 let flex = Flex::horizontal()
1393 .alignment(Alignment::SpaceBetween)
1394 .constraints([Constraint::Fixed(10)]);
1395 let rects = flex.split(Rect::new(0, 0, 100, 10));
1396 assert_eq!(rects[0].x, 0);
1398 assert_eq!(rects[0].width, 10);
1399 }
1400
1401 #[test]
1402 fn invariant_rects_within_bounds() {
1403 let area = Rect::new(10, 20, 80, 60);
1404 let flex = Flex::horizontal()
1405 .margin(Sides::all(5))
1406 .gap(2)
1407 .constraints([
1408 Constraint::Fixed(15),
1409 Constraint::Percentage(30.0),
1410 Constraint::Min(10),
1411 ]);
1412 let rects = flex.split(area);
1413
1414 let inner = area.inner(Sides::all(5));
1416 for rect in &rects {
1417 assert!(
1418 rect.x >= inner.x && rect.right() <= inner.right(),
1419 "Rect {:?} exceeds horizontal bounds of {:?}",
1420 rect,
1421 inner
1422 );
1423 assert!(
1424 rect.y >= inner.y && rect.bottom() <= inner.bottom(),
1425 "Rect {:?} exceeds vertical bounds of {:?}",
1426 rect,
1427 inner
1428 );
1429 }
1430 }
1431
1432 #[test]
1435 fn fill_takes_remaining_space() {
1436 let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
1437 let rects = flex.split(Rect::new(0, 0, 100, 10));
1438 assert_eq!(rects[0].width, 20);
1439 assert_eq!(rects[1].width, 80); }
1441
1442 #[test]
1443 fn multiple_fills_share_space() {
1444 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
1445 let rects = flex.split(Rect::new(0, 0, 100, 10));
1446 assert_eq!(rects[0].width, 50);
1447 assert_eq!(rects[1].width, 50);
1448 }
1449
1450 #[test]
1453 fn fit_content_uses_preferred_size() {
1454 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1455 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1456 if idx == 0 {
1457 LayoutSizeHint {
1458 min: 5,
1459 preferred: 30,
1460 max: None,
1461 }
1462 } else {
1463 LayoutSizeHint::ZERO
1464 }
1465 });
1466 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
1469
1470 #[test]
1471 fn fit_content_clamps_to_available() {
1472 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
1473 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1474 min: 10,
1475 preferred: 80,
1476 max: None,
1477 });
1478 assert_eq!(rects[0].width, 80);
1480 assert_eq!(rects[1].width, 20);
1481 }
1482
1483 #[test]
1484 fn fit_content_without_measurer_gets_zero() {
1485 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1487 let rects = flex.split(Rect::new(0, 0, 100, 10));
1488 assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
1491
1492 #[test]
1493 fn fit_content_zero_area_returns_empty_rects() {
1494 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1495 let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
1496 min: 5,
1497 preferred: 10,
1498 max: None,
1499 });
1500 assert_eq!(rects.len(), 2);
1501 assert_eq!(rects[0].width, 0);
1502 assert_eq!(rects[0].height, 0);
1503 assert_eq!(rects[1].width, 0);
1504 assert_eq!(rects[1].height, 0);
1505 }
1506
1507 #[test]
1508 fn fit_content_tiny_available_clamps_to_remaining() {
1509 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1510 let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
1511 min: 5,
1512 preferred: 10,
1513 max: None,
1514 });
1515 assert_eq!(rects[0].width, 1);
1516 assert_eq!(rects[1].width, 0);
1517 }
1518
1519 #[test]
1522 fn fit_content_bounded_clamps_to_min() {
1523 let flex = Flex::horizontal().constraints([
1524 Constraint::FitContentBounded { min: 20, max: 50 },
1525 Constraint::Fill,
1526 ]);
1527 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1528 min: 5,
1529 preferred: 10, max: None,
1531 });
1532 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
1534 }
1535
1536 #[test]
1537 fn fit_content_bounded_respects_small_available() {
1538 let flex = Flex::horizontal().constraints([
1539 Constraint::FitContentBounded { min: 20, max: 50 },
1540 Constraint::Fill,
1541 ]);
1542 let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
1543 min: 5,
1544 preferred: 10,
1545 max: None,
1546 });
1547 assert_eq!(rects[0].width, 5);
1549 assert_eq!(rects[1].width, 0);
1550 }
1551
1552 #[test]
1553 fn fit_content_vertical_uses_preferred_height() {
1554 let flex = Flex::vertical().constraints([Constraint::FitContent, Constraint::Fill]);
1555 let rects = flex.split_with_measurer(Rect::new(0, 0, 10, 10), |idx, _| {
1556 if idx == 0 {
1557 LayoutSizeHint {
1558 min: 1,
1559 preferred: 4,
1560 max: None,
1561 }
1562 } else {
1563 LayoutSizeHint::ZERO
1564 }
1565 });
1566 assert_eq!(rects[0].height, 4);
1567 assert_eq!(rects[1].height, 6);
1568 }
1569
1570 #[test]
1571 fn fit_content_bounded_clamps_to_max() {
1572 let flex = Flex::horizontal().constraints([
1573 Constraint::FitContentBounded { min: 10, max: 30 },
1574 Constraint::Fill,
1575 ]);
1576 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1577 min: 5,
1578 preferred: 50, max: None,
1580 });
1581 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
1583 }
1584
1585 #[test]
1586 fn fit_content_bounded_uses_preferred_when_in_range() {
1587 let flex = Flex::horizontal().constraints([
1588 Constraint::FitContentBounded { min: 10, max: 50 },
1589 Constraint::Fill,
1590 ]);
1591 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1592 min: 5,
1593 preferred: 35, max: None,
1595 });
1596 assert_eq!(rects[0].width, 35);
1597 assert_eq!(rects[1].width, 65);
1598 }
1599
1600 #[test]
1603 fn fit_min_uses_minimum_size() {
1604 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1605 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1606 if idx == 0 {
1607 LayoutSizeHint {
1608 min: 15,
1609 preferred: 40,
1610 max: None,
1611 }
1612 } else {
1613 LayoutSizeHint::ZERO
1614 }
1615 });
1616 let total: u16 = rects.iter().map(|r| r.width).sum();
1630 assert_eq!(total, 100);
1631 assert!(rects[0].width >= 15, "FitMin should get at least minimum");
1632 }
1633
1634 #[test]
1635 fn fit_min_without_measurer_gets_zero() {
1636 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1637 let rects = flex.split(Rect::new(0, 0, 100, 10));
1638 assert_eq!(rects[0].width, 50);
1641 assert_eq!(rects[1].width, 50);
1642 }
1643
1644 #[test]
1647 fn layout_size_hint_zero_is_default() {
1648 assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
1649 }
1650
1651 #[test]
1652 fn layout_size_hint_exact() {
1653 let h = LayoutSizeHint::exact(25);
1654 assert_eq!(h.min, 25);
1655 assert_eq!(h.preferred, 25);
1656 assert_eq!(h.max, Some(25));
1657 }
1658
1659 #[test]
1660 fn layout_size_hint_at_least() {
1661 let h = LayoutSizeHint::at_least(10, 30);
1662 assert_eq!(h.min, 10);
1663 assert_eq!(h.preferred, 30);
1664 assert_eq!(h.max, None);
1665 }
1666
1667 #[test]
1668 fn layout_size_hint_clamp() {
1669 let h = LayoutSizeHint {
1670 min: 10,
1671 preferred: 20,
1672 max: Some(30),
1673 };
1674 assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
1678
1679 #[test]
1680 fn layout_size_hint_clamp_unbounded() {
1681 let h = LayoutSizeHint::at_least(5, 10);
1682 assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
1685
1686 #[test]
1689 fn fit_content_with_fixed_and_fill() {
1690 let flex = Flex::horizontal().constraints([
1691 Constraint::Fixed(20),
1692 Constraint::FitContent,
1693 Constraint::Fill,
1694 ]);
1695 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1696 if idx == 1 {
1697 LayoutSizeHint {
1698 min: 5,
1699 preferred: 25,
1700 max: None,
1701 }
1702 } else {
1703 LayoutSizeHint::ZERO
1704 }
1705 });
1706 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
1710
1711 #[test]
1712 fn total_allocation_never_exceeds_available_with_fit_content() {
1713 for available in [10u16, 50, 100, 255] {
1714 let flex = Flex::horizontal().constraints([
1715 Constraint::FitContent,
1716 Constraint::FitContent,
1717 Constraint::Fill,
1718 ]);
1719 let rects =
1720 flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
1721 min: 10,
1722 preferred: 40,
1723 max: None,
1724 });
1725 let total: u16 = rects.iter().map(|r| r.width).sum();
1726 assert!(
1727 total <= available,
1728 "Total {} exceeded available {} with FitContent",
1729 total,
1730 available
1731 );
1732 }
1733 }
1734
1735 mod rounding_tests {
1740 use super::super::*;
1741
1742 #[test]
1745 fn rounding_conserves_sum_exact() {
1746 let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
1747 assert_eq!(result.iter().copied().sum::<u16>(), 40);
1748 assert_eq!(result, vec![10, 20, 10]);
1749 }
1750
1751 #[test]
1752 fn rounding_conserves_sum_fractional() {
1753 let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
1754 assert_eq!(
1755 result.iter().copied().sum::<u16>(),
1756 40,
1757 "Sum must equal total: {:?}",
1758 result
1759 );
1760 }
1761
1762 #[test]
1763 fn rounding_conserves_sum_many_fractions() {
1764 let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
1765 let result = round_layout_stable(&targets, 100, None);
1766 assert_eq!(
1767 result.iter().copied().sum::<u16>(),
1768 100,
1769 "Sum must be exactly 100: {:?}",
1770 result
1771 );
1772 }
1773
1774 #[test]
1775 fn rounding_conserves_sum_all_half() {
1776 let targets = vec![10.5, 10.5, 10.5, 10.5];
1777 let result = round_layout_stable(&targets, 42, None);
1778 assert_eq!(
1779 result.iter().copied().sum::<u16>(),
1780 42,
1781 "Sum must be exactly 42: {:?}",
1782 result
1783 );
1784 }
1785
1786 #[test]
1789 fn rounding_displacement_bounded() {
1790 let targets = vec![33.33, 33.33, 33.34];
1791 let result = round_layout_stable(&targets, 100, None);
1792 assert_eq!(result.iter().copied().sum::<u16>(), 100);
1793
1794 for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
1795 let floor = r.floor() as u16;
1796 let ceil = floor + 1;
1797 assert!(
1798 x == floor || x == ceil,
1799 "Element {} = {} not in {{floor={}, ceil={}}} of target {}",
1800 i,
1801 x,
1802 floor,
1803 ceil,
1804 r
1805 );
1806 }
1807 }
1808
1809 #[test]
1812 fn temporal_tiebreak_stable_when_unchanged() {
1813 let targets = vec![10.5, 10.5, 10.5, 10.5];
1814 let first = round_layout_stable(&targets, 42, None);
1815 let second = round_layout_stable(&targets, 42, Some(first.clone()));
1816 assert_eq!(
1817 first, second,
1818 "Identical targets should produce identical results"
1819 );
1820 }
1821
1822 #[test]
1823 fn temporal_tiebreak_prefers_previous_direction() {
1824 let targets = vec![10.5, 10.5];
1825 let total = 21;
1826 let first = round_layout_stable(&targets, total, None);
1827 assert_eq!(first.iter().copied().sum::<u16>(), total);
1828 let second = round_layout_stable(&targets, total, Some(first.clone()));
1829 assert_eq!(first, second, "Should maintain rounding direction");
1830 }
1831
1832 #[test]
1833 fn temporal_tiebreak_adapts_to_changed_targets() {
1834 let targets_a = vec![10.5, 10.5];
1835 let result_a = round_layout_stable(&targets_a, 21, None);
1836 let targets_b = vec![15.7, 5.3];
1837 let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
1838 assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
1839 assert!(result_b[0] > result_b[1], "Should follow larger target");
1840 }
1841
1842 #[test]
1845 fn property_min_displacement_brute_force_small() {
1846 let targets = vec![3.3, 3.3, 3.4];
1847 let total: u16 = 10;
1848 let result = round_layout_stable(&targets, total, None);
1849 let our_displacement: f64 = result
1850 .iter()
1851 .zip(targets.iter())
1852 .map(|(&x, &r)| (x as f64 - r).abs())
1853 .sum();
1854
1855 let mut min_displacement = f64::MAX;
1856 let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
1857 let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
1858
1859 for a in floors[0]..=ceils[0] {
1860 for b in floors[1]..=ceils[1] {
1861 for c in floors[2]..=ceils[2] {
1862 if a + b + c == total {
1863 let disp = (a as f64 - targets[0]).abs()
1864 + (b as f64 - targets[1]).abs()
1865 + (c as f64 - targets[2]).abs();
1866 if disp < min_displacement {
1867 min_displacement = disp;
1868 }
1869 }
1870 }
1871 }
1872 }
1873
1874 assert!(
1875 (our_displacement - min_displacement).abs() < 1e-10,
1876 "Our displacement {} should match optimal {}: {:?}",
1877 our_displacement,
1878 min_displacement,
1879 result
1880 );
1881 }
1882
1883 #[test]
1886 fn rounding_deterministic() {
1887 let targets = vec![7.7, 8.3, 14.0];
1888 let a = round_layout_stable(&targets, 30, None);
1889 let b = round_layout_stable(&targets, 30, None);
1890 assert_eq!(a, b, "Same inputs must produce identical outputs");
1891 }
1892
1893 #[test]
1896 fn rounding_empty_targets() {
1897 let result = round_layout_stable(&[], 0, None);
1898 assert!(result.is_empty());
1899 }
1900
1901 #[test]
1902 fn rounding_single_element() {
1903 let result = round_layout_stable(&[10.7], 11, None);
1904 assert_eq!(result, vec![11]);
1905 }
1906
1907 #[test]
1908 fn rounding_zero_total() {
1909 let result = round_layout_stable(&[5.0, 5.0], 0, None);
1910 assert_eq!(result.iter().copied().sum::<u16>(), 0);
1911 }
1912
1913 #[test]
1914 fn rounding_all_zeros() {
1915 let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
1916 assert_eq!(result, vec![0, 0, 0]);
1917 }
1918
1919 #[test]
1920 fn rounding_integer_targets() {
1921 let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
1922 assert_eq!(result, vec![10, 20, 30]);
1923 }
1924
1925 #[test]
1926 fn rounding_large_deficit() {
1927 let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
1928 assert_eq!(result.iter().copied().sum::<u16>(), 3);
1929 assert_eq!(result, vec![1, 1, 1]);
1930 }
1931
1932 #[test]
1933 fn rounding_with_prev_different_length() {
1934 let result = round_layout_stable(&[10.5, 10.5], 21, Some(vec![11, 10, 5]));
1935 assert_eq!(result.iter().copied().sum::<u16>(), 21);
1936 }
1937
1938 #[test]
1939 fn rounding_very_small_fractions() {
1940 let targets = vec![10.001, 20.001, 9.998];
1941 let result = round_layout_stable(&targets, 40, None);
1942 assert_eq!(result.iter().copied().sum::<u16>(), 40);
1943 }
1944
1945 #[test]
1946 fn rounding_conserves_sum_stress() {
1947 let n = 50;
1948 let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
1949 let total = 120u16;
1950 let result = round_layout_stable(&targets, total, None);
1951 assert_eq!(
1952 result.iter().copied().sum::<u16>(),
1953 total,
1954 "Sum must be exactly {} for {} items: {:?}",
1955 total,
1956 n,
1957 result
1958 );
1959 }
1960 }
1961
1962 mod property_constraint_tests {
1967 use super::super::*;
1968
1969 struct Lcg(u64);
1971
1972 impl Lcg {
1973 fn new(seed: u64) -> Self {
1974 Self(seed)
1975 }
1976 fn next_u32(&mut self) -> u32 {
1977 self.0 = self
1978 .0
1979 .wrapping_mul(6_364_136_223_846_793_005)
1980 .wrapping_add(1);
1981 (self.0 >> 33) as u32
1982 }
1983 fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
1984 if lo >= hi {
1985 return lo;
1986 }
1987 lo + (self.next_u32() % (hi - lo) as u32) as u16
1988 }
1989 fn next_f32(&mut self) -> f32 {
1990 (self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
1991 }
1992 }
1993
1994 fn random_constraint(rng: &mut Lcg) -> Constraint {
1996 match rng.next_u32() % 7 {
1997 0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
1998 1 => Constraint::Percentage(rng.next_f32() * 100.0),
1999 2 => Constraint::Min(rng.next_u16_range(0, 40)),
2000 3 => Constraint::Max(rng.next_u16_range(5, 120)),
2001 4 => {
2002 let n = rng.next_u32() % 5 + 1;
2003 let d = rng.next_u32() % 5 + 1;
2004 Constraint::Ratio(n, d)
2005 }
2006 5 => Constraint::Fill,
2007 _ => Constraint::FitContent,
2008 }
2009 }
2010
2011 #[test]
2012 fn property_constraints_respected_fixed() {
2013 let mut rng = Lcg::new(0xDEAD_BEEF);
2014 for _ in 0..200 {
2015 let fixed_val = rng.next_u16_range(1, 60);
2016 let avail = rng.next_u16_range(10, 200);
2017 let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
2018 let rects = flex.split(Rect::new(0, 0, avail, 10));
2019 assert!(
2020 rects[0].width <= fixed_val.min(avail),
2021 "Fixed({}) in avail {} -> width {}",
2022 fixed_val,
2023 avail,
2024 rects[0].width
2025 );
2026 }
2027 }
2028
2029 #[test]
2030 fn property_constraints_respected_max() {
2031 let mut rng = Lcg::new(0xCAFE_BABE);
2032 for _ in 0..200 {
2033 let max_val = rng.next_u16_range(5, 80);
2034 let avail = rng.next_u16_range(10, 200);
2035 let flex =
2036 Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
2037 let rects = flex.split(Rect::new(0, 0, avail, 10));
2038 assert!(
2039 rects[0].width <= max_val,
2040 "Max({}) in avail {} -> width {}",
2041 max_val,
2042 avail,
2043 rects[0].width
2044 );
2045 }
2046 }
2047
2048 #[test]
2049 fn property_constraints_respected_min() {
2050 let mut rng = Lcg::new(0xBAAD_F00D);
2051 for _ in 0..200 {
2052 let min_val = rng.next_u16_range(0, 40);
2053 let avail = rng.next_u16_range(min_val.max(1), 200);
2054 let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
2055 let rects = flex.split(Rect::new(0, 0, avail, 10));
2056 assert!(
2057 rects[0].width >= min_val,
2058 "Min({}) in avail {} -> width {}",
2059 min_val,
2060 avail,
2061 rects[0].width
2062 );
2063 }
2064 }
2065
2066 #[test]
2067 fn property_constraints_respected_ratio_proportional() {
2068 let mut rng = Lcg::new(0x1234_5678);
2069 for _ in 0..200 {
2070 let n1 = rng.next_u32() % 5 + 1;
2071 let n2 = rng.next_u32() % 5 + 1;
2072 let d = rng.next_u32() % 5 + 1;
2073 let avail = rng.next_u16_range(20, 200);
2074 let flex = Flex::horizontal()
2075 .constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
2076 let rects = flex.split(Rect::new(0, 0, avail, 10));
2077 let w1 = rects[0].width as f64;
2078 let w2 = rects[1].width as f64;
2079 let total = w1 + w2;
2080 if total > 0.0 {
2081 let expected_ratio = n1 as f64 / (n1 + n2) as f64;
2082 let actual_ratio = w1 / total;
2083 assert!(
2084 (actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
2085 "Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
2086 n1,
2087 d,
2088 n1,
2089 n2,
2090 avail,
2091 expected_ratio,
2092 actual_ratio,
2093 w1,
2094 w2
2095 );
2096 }
2097 }
2098 }
2099
2100 #[test]
2101 fn property_total_allocation_never_exceeds_available() {
2102 let mut rng = Lcg::new(0xFACE_FEED);
2103 for _ in 0..500 {
2104 let n = (rng.next_u32() % 6 + 1) as usize;
2105 let constraints: Vec<Constraint> =
2106 (0..n).map(|_| random_constraint(&mut rng)).collect();
2107 let avail = rng.next_u16_range(5, 200);
2108 let dir = if rng.next_u32().is_multiple_of(2) {
2109 Direction::Horizontal
2110 } else {
2111 Direction::Vertical
2112 };
2113 let flex = Flex::default().direction(dir).constraints(constraints);
2114 let area = Rect::new(0, 0, avail, avail);
2115 let rects = flex.split(area);
2116 let total: u16 = rects
2117 .iter()
2118 .map(|r| match dir {
2119 Direction::Horizontal => r.width,
2120 Direction::Vertical => r.height,
2121 })
2122 .sum();
2123 assert!(
2124 total <= avail,
2125 "Total {} exceeded available {} with {} constraints",
2126 total,
2127 avail,
2128 n
2129 );
2130 }
2131 }
2132
2133 #[test]
2134 fn property_no_overlap_horizontal() {
2135 let mut rng = Lcg::new(0xABCD_1234);
2136 for _ in 0..300 {
2137 let n = (rng.next_u32() % 5 + 2) as usize;
2138 let constraints: Vec<Constraint> =
2139 (0..n).map(|_| random_constraint(&mut rng)).collect();
2140 let avail = rng.next_u16_range(20, 200);
2141 let flex = Flex::horizontal().constraints(constraints);
2142 let rects = flex.split(Rect::new(0, 0, avail, 10));
2143
2144 for i in 1..rects.len() {
2145 let prev_end = rects[i - 1].x + rects[i - 1].width;
2146 assert!(
2147 rects[i].x >= prev_end,
2148 "Overlap at {}: prev ends {}, next starts {}",
2149 i,
2150 prev_end,
2151 rects[i].x
2152 );
2153 }
2154 }
2155 }
2156
2157 #[test]
2158 fn property_deterministic_across_runs() {
2159 let mut rng = Lcg::new(0x9999_8888);
2160 for _ in 0..100 {
2161 let n = (rng.next_u32() % 5 + 1) as usize;
2162 let constraints: Vec<Constraint> =
2163 (0..n).map(|_| random_constraint(&mut rng)).collect();
2164 let avail = rng.next_u16_range(10, 200);
2165 let r1 = Flex::horizontal()
2166 .constraints(constraints.clone())
2167 .split(Rect::new(0, 0, avail, 10));
2168 let r2 = Flex::horizontal()
2169 .constraints(constraints)
2170 .split(Rect::new(0, 0, avail, 10));
2171 assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
2172 }
2173 }
2174 }
2175
2176 mod property_temporal_tests {
2181 use super::super::*;
2182 use crate::cache::{CoherenceCache, CoherenceId};
2183
2184 struct Lcg(u64);
2186
2187 impl Lcg {
2188 fn new(seed: u64) -> Self {
2189 Self(seed)
2190 }
2191 fn next_u32(&mut self) -> u32 {
2192 self.0 = self
2193 .0
2194 .wrapping_mul(6_364_136_223_846_793_005)
2195 .wrapping_add(1);
2196 (self.0 >> 33) as u32
2197 }
2198 }
2199
2200 #[test]
2201 fn property_temporal_stability_small_resize() {
2202 let constraints = [
2203 Constraint::Percentage(33.3),
2204 Constraint::Percentage(33.3),
2205 Constraint::Fill,
2206 ];
2207 let mut coherence = CoherenceCache::new(64);
2208 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2209
2210 for total in [80u16, 100, 120] {
2211 let flex = Flex::horizontal().constraints(constraints);
2212 let rects = flex.split(Rect::new(0, 0, total, 10));
2213 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2214
2215 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2216 let prev = coherence.get(&id);
2217 let rounded = round_layout_stable(&targets, total, prev);
2218
2219 if let Some(old) = coherence.get(&id) {
2220 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2221 assert!(
2222 max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
2223 "max_disp={} too large for size change {} -> {}",
2224 max_disp,
2225 old.iter().copied().sum::<u16>(),
2226 total
2227 );
2228 let _ = sum_disp;
2229 }
2230 coherence.store(id, rounded);
2231 }
2232 }
2233
2234 #[test]
2235 fn property_temporal_stability_random_walk() {
2236 let constraints = [
2237 Constraint::Ratio(1, 3),
2238 Constraint::Ratio(1, 3),
2239 Constraint::Ratio(1, 3),
2240 ];
2241 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2242 let mut coherence = CoherenceCache::new(64);
2243 let mut rng = Lcg::new(0x5555_AAAA);
2244 let mut total: u16 = 90;
2245
2246 for step in 0..200 {
2247 let prev_total = total;
2248 let delta = (rng.next_u32() % 7) as i32 - 3;
2249 total = (total as i32 + delta).clamp(10, 250) as u16;
2250
2251 let flex = Flex::horizontal().constraints(constraints);
2252 let rects = flex.split(Rect::new(0, 0, total, 10));
2253 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2254
2255 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2256 let prev = coherence.get(&id);
2257 let rounded = round_layout_stable(&targets, total, prev);
2258
2259 if coherence.get(&id).is_some() {
2260 let (_, max_disp) = coherence.displacement(&id, &rounded);
2261 let size_change = total.abs_diff(prev_total);
2262 assert!(
2263 max_disp <= size_change as u32 + 2,
2264 "step {}: max_disp={} exceeds size_change={} + 2",
2265 step,
2266 max_disp,
2267 size_change
2268 );
2269 }
2270 coherence.store(id, rounded);
2271 }
2272 }
2273
2274 #[test]
2275 fn property_temporal_stability_identical_frames() {
2276 let constraints = [
2277 Constraint::Fixed(20),
2278 Constraint::Fill,
2279 Constraint::Fixed(15),
2280 ];
2281 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2282 let mut coherence = CoherenceCache::new(64);
2283
2284 let flex = Flex::horizontal().constraints(constraints);
2285 let rects = flex.split(Rect::new(0, 0, 100, 10));
2286 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2287 coherence.store(id, widths.clone());
2288
2289 for _ in 0..10 {
2290 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2291 let prev = coherence.get(&id);
2292 let rounded = round_layout_stable(&targets, 100, prev);
2293 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2294 assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
2295 assert_eq!(max_disp, 0);
2296 coherence.store(id, rounded);
2297 }
2298 }
2299
2300 #[test]
2301 fn property_temporal_coherence_sweep() {
2302 let constraints = [
2303 Constraint::Percentage(25.0),
2304 Constraint::Percentage(50.0),
2305 Constraint::Fill,
2306 ];
2307 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2308 let mut coherence = CoherenceCache::new(64);
2309 let mut total_displacement: u64 = 0;
2310
2311 for total in 60u16..=140 {
2312 let flex = Flex::horizontal().constraints(constraints);
2313 let rects = flex.split(Rect::new(0, 0, total, 10));
2314 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2315
2316 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2317 let prev = coherence.get(&id);
2318 let rounded = round_layout_stable(&targets, total, prev);
2319
2320 if coherence.get(&id).is_some() {
2321 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2322 total_displacement += sum_disp;
2323 }
2324 coherence.store(id, rounded);
2325 }
2326
2327 assert!(
2328 total_displacement <= 80 * 3,
2329 "Total displacement {} exceeds bound for 80-step sweep",
2330 total_displacement
2331 );
2332 }
2333 }
2334
2335 mod snapshot_layout_tests {
2340 use super::super::*;
2341 use crate::grid::{Grid, GridArea};
2342
2343 fn snapshot_flex(
2344 constraints: &[Constraint],
2345 dir: Direction,
2346 width: u16,
2347 height: u16,
2348 ) -> String {
2349 let flex = Flex::default()
2350 .direction(dir)
2351 .constraints(constraints.iter().copied());
2352 let rects = flex.split(Rect::new(0, 0, width, height));
2353 let mut out = format!(
2354 "Flex {:?} {}x{} ({} constraints)\n",
2355 dir,
2356 width,
2357 height,
2358 constraints.len()
2359 );
2360 for (i, r) in rects.iter().enumerate() {
2361 out.push_str(&format!(
2362 " [{}] x={} y={} w={} h={}\n",
2363 i, r.x, r.y, r.width, r.height
2364 ));
2365 }
2366 let total: u16 = rects
2367 .iter()
2368 .map(|r| match dir {
2369 Direction::Horizontal => r.width,
2370 Direction::Vertical => r.height,
2371 })
2372 .sum();
2373 out.push_str(&format!(" total={}\n", total));
2374 out
2375 }
2376
2377 fn snapshot_grid(
2378 rows: &[Constraint],
2379 cols: &[Constraint],
2380 areas: &[(&str, GridArea)],
2381 width: u16,
2382 height: u16,
2383 ) -> String {
2384 let mut grid = Grid::new()
2385 .rows(rows.iter().copied())
2386 .columns(cols.iter().copied());
2387 for &(name, area) in areas {
2388 grid = grid.area(name, area);
2389 }
2390 let layout = grid.split(Rect::new(0, 0, width, height));
2391
2392 let mut out = format!(
2393 "Grid {}x{} ({}r x {}c)\n",
2394 width,
2395 height,
2396 rows.len(),
2397 cols.len()
2398 );
2399 for r in 0..rows.len() {
2400 for c in 0..cols.len() {
2401 let rect = layout.cell(r, c);
2402 out.push_str(&format!(
2403 " [{},{}] x={} y={} w={} h={}\n",
2404 r, c, rect.x, rect.y, rect.width, rect.height
2405 ));
2406 }
2407 }
2408 for &(name, _) in areas {
2409 if let Some(rect) = layout.area(name) {
2410 out.push_str(&format!(
2411 " area({}) x={} y={} w={} h={}\n",
2412 name, rect.x, rect.y, rect.width, rect.height
2413 ));
2414 }
2415 }
2416 out
2417 }
2418
2419 #[test]
2422 fn snapshot_flex_thirds_80x24() {
2423 let snap = snapshot_flex(
2424 &[
2425 Constraint::Ratio(1, 3),
2426 Constraint::Ratio(1, 3),
2427 Constraint::Ratio(1, 3),
2428 ],
2429 Direction::Horizontal,
2430 80,
2431 24,
2432 );
2433 assert_eq!(
2434 snap,
2435 "\
2436Flex Horizontal 80x24 (3 constraints)
2437 [0] x=0 y=0 w=26 h=24
2438 [1] x=26 y=0 w=26 h=24
2439 [2] x=52 y=0 w=28 h=24
2440 total=80
2441"
2442 );
2443 }
2444
2445 #[test]
2446 fn snapshot_flex_sidebar_content_80x24() {
2447 let snap = snapshot_flex(
2448 &[Constraint::Fixed(20), Constraint::Fill],
2449 Direction::Horizontal,
2450 80,
2451 24,
2452 );
2453 assert_eq!(
2454 snap,
2455 "\
2456Flex Horizontal 80x24 (2 constraints)
2457 [0] x=0 y=0 w=20 h=24
2458 [1] x=20 y=0 w=60 h=24
2459 total=80
2460"
2461 );
2462 }
2463
2464 #[test]
2465 fn snapshot_flex_header_body_footer_80x24() {
2466 let snap = snapshot_flex(
2467 &[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
2468 Direction::Vertical,
2469 80,
2470 24,
2471 );
2472 assert_eq!(
2473 snap,
2474 "\
2475Flex Vertical 80x24 (3 constraints)
2476 [0] x=0 y=0 w=80 h=3
2477 [1] x=0 y=3 w=80 h=20
2478 [2] x=0 y=23 w=80 h=1
2479 total=24
2480"
2481 );
2482 }
2483
2484 #[test]
2487 fn snapshot_flex_thirds_120x40() {
2488 let snap = snapshot_flex(
2489 &[
2490 Constraint::Ratio(1, 3),
2491 Constraint::Ratio(1, 3),
2492 Constraint::Ratio(1, 3),
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=40 h=40
2503 [1] x=40 y=0 w=40 h=40
2504 [2] x=80 y=0 w=40 h=40
2505 total=120
2506"
2507 );
2508 }
2509
2510 #[test]
2511 fn snapshot_flex_sidebar_content_120x40() {
2512 let snap = snapshot_flex(
2513 &[Constraint::Fixed(20), Constraint::Fill],
2514 Direction::Horizontal,
2515 120,
2516 40,
2517 );
2518 assert_eq!(
2519 snap,
2520 "\
2521Flex Horizontal 120x40 (2 constraints)
2522 [0] x=0 y=0 w=20 h=40
2523 [1] x=20 y=0 w=100 h=40
2524 total=120
2525"
2526 );
2527 }
2528
2529 #[test]
2530 fn snapshot_flex_percentage_mix_120x40() {
2531 let snap = snapshot_flex(
2532 &[
2533 Constraint::Percentage(25.0),
2534 Constraint::Percentage(50.0),
2535 Constraint::Fill,
2536 ],
2537 Direction::Horizontal,
2538 120,
2539 40,
2540 );
2541 assert_eq!(
2542 snap,
2543 "\
2544Flex Horizontal 120x40 (3 constraints)
2545 [0] x=0 y=0 w=30 h=40
2546 [1] x=30 y=0 w=60 h=40
2547 [2] x=90 y=0 w=30 h=40
2548 total=120
2549"
2550 );
2551 }
2552
2553 #[test]
2556 fn snapshot_grid_2x2_80x24() {
2557 let snap = snapshot_grid(
2558 &[Constraint::Fixed(3), Constraint::Fill],
2559 &[Constraint::Fixed(20), Constraint::Fill],
2560 &[
2561 ("header", GridArea::span(0, 0, 1, 2)),
2562 ("sidebar", GridArea::span(1, 0, 1, 1)),
2563 ("content", GridArea::cell(1, 1)),
2564 ],
2565 80,
2566 24,
2567 );
2568 assert_eq!(
2569 snap,
2570 "\
2571Grid 80x24 (2r x 2c)
2572 [0,0] x=0 y=0 w=20 h=3
2573 [0,1] x=20 y=0 w=60 h=3
2574 [1,0] x=0 y=3 w=20 h=21
2575 [1,1] x=20 y=3 w=60 h=21
2576 area(header) x=0 y=0 w=80 h=3
2577 area(sidebar) x=0 y=3 w=20 h=21
2578 area(content) x=20 y=3 w=60 h=21
2579"
2580 );
2581 }
2582
2583 #[test]
2584 fn snapshot_grid_3x3_80x24() {
2585 let snap = snapshot_grid(
2586 &[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
2587 &[
2588 Constraint::Fixed(10),
2589 Constraint::Fill,
2590 Constraint::Fixed(10),
2591 ],
2592 &[],
2593 80,
2594 24,
2595 );
2596 assert_eq!(
2597 snap,
2598 "\
2599Grid 80x24 (3r x 3c)
2600 [0,0] x=0 y=0 w=10 h=1
2601 [0,1] x=10 y=0 w=60 h=1
2602 [0,2] x=70 y=0 w=10 h=1
2603 [1,0] x=0 y=1 w=10 h=22
2604 [1,1] x=10 y=1 w=60 h=22
2605 [1,2] x=70 y=1 w=10 h=22
2606 [2,0] x=0 y=23 w=10 h=1
2607 [2,1] x=10 y=23 w=60 h=1
2608 [2,2] x=70 y=23 w=10 h=1
2609"
2610 );
2611 }
2612
2613 #[test]
2616 fn snapshot_grid_2x2_120x40() {
2617 let snap = snapshot_grid(
2618 &[Constraint::Fixed(3), Constraint::Fill],
2619 &[Constraint::Fixed(20), Constraint::Fill],
2620 &[
2621 ("header", GridArea::span(0, 0, 1, 2)),
2622 ("sidebar", GridArea::span(1, 0, 1, 1)),
2623 ("content", GridArea::cell(1, 1)),
2624 ],
2625 120,
2626 40,
2627 );
2628 assert_eq!(
2629 snap,
2630 "\
2631Grid 120x40 (2r x 2c)
2632 [0,0] x=0 y=0 w=20 h=3
2633 [0,1] x=20 y=0 w=100 h=3
2634 [1,0] x=0 y=3 w=20 h=37
2635 [1,1] x=20 y=3 w=100 h=37
2636 area(header) x=0 y=0 w=120 h=3
2637 area(sidebar) x=0 y=3 w=20 h=37
2638 area(content) x=20 y=3 w=100 h=37
2639"
2640 );
2641 }
2642
2643 #[test]
2644 fn snapshot_grid_dashboard_120x40() {
2645 let snap = snapshot_grid(
2646 &[
2647 Constraint::Fixed(3),
2648 Constraint::Percentage(60.0),
2649 Constraint::Fill,
2650 ],
2651 &[Constraint::Percentage(30.0), Constraint::Fill],
2652 &[
2653 ("nav", GridArea::span(0, 0, 1, 2)),
2654 ("chart", GridArea::cell(1, 0)),
2655 ("detail", GridArea::cell(1, 1)),
2656 ("log", GridArea::span(2, 0, 1, 2)),
2657 ],
2658 120,
2659 40,
2660 );
2661 assert_eq!(
2662 snap,
2663 "\
2664Grid 120x40 (3r x 2c)
2665 [0,0] x=0 y=0 w=36 h=3
2666 [0,1] x=36 y=0 w=84 h=3
2667 [1,0] x=0 y=3 w=36 h=24
2668 [1,1] x=36 y=3 w=84 h=24
2669 [2,0] x=0 y=27 w=36 h=13
2670 [2,1] x=36 y=27 w=84 h=13
2671 area(nav) x=0 y=0 w=120 h=3
2672 area(chart) x=0 y=3 w=36 h=24
2673 area(detail) x=36 y=3 w=84 h=24
2674 area(log) x=0 y=27 w=120 h=13
2675"
2676 );
2677 }
2678 }
2679}