1#![forbid(unsafe_code)]
2
3pub mod cache;
43pub mod debug;
44pub mod direction;
45pub mod grid;
46pub mod pane;
47#[cfg(test)]
48mod repro_max_constraint;
49#[cfg(test)]
50mod repro_space_around;
51pub mod responsive;
52pub mod responsive_layout;
53pub mod visibility;
54pub mod workspace;
55
56pub use cache::{
57 CoherenceCache, CoherenceId, LayoutCache, LayoutCacheKey, LayoutCacheStats, S3FifoLayoutCache,
58};
59pub use direction::{FlowDirection, LogicalAlignment, LogicalSides, mirror_rects_horizontal};
60pub use ftui_core::geometry::{Rect, Sides, Size};
61pub use grid::{Grid, GridArea, GridLayout};
62pub use pane::{
63 PANE_DRAG_RESIZE_DEFAULT_HYSTERESIS, PANE_DRAG_RESIZE_DEFAULT_THRESHOLD,
64 PANE_SEMANTIC_INPUT_EVENT_SCHEMA_VERSION, PANE_SEMANTIC_INPUT_TRACE_SCHEMA_VERSION,
65 PANE_SNAP_DEFAULT_HYSTERESIS_BPS, PANE_SNAP_DEFAULT_STEP_BPS, PANE_TREE_SCHEMA_VERSION,
66 PaneCancelReason, PaneConstraints, PaneCoordinateNormalizationError, PaneCoordinateNormalizer,
67 PaneCoordinateRoundingPolicy, PaneDragBehaviorTuning, PaneDragResizeEffect,
68 PaneDragResizeMachine, PaneDragResizeMachineError, PaneDragResizeNoopReason,
69 PaneDragResizeState, PaneDragResizeTransition, PaneId, PaneIdAllocator, PaneInputCoordinate,
70 PaneInteractionPolicyError, PaneInvariantCode, PaneInvariantIssue, PaneInvariantReport,
71 PaneInvariantSeverity, PaneLayout, PaneLeaf, PaneModelError, PaneModifierSnapshot,
72 PaneNodeKind, PaneNodeRecord, PaneNormalizedCoordinate, PaneOperation, PaneOperationError,
73 PaneOperationFailure, PaneOperationJournalEntry, PaneOperationJournalResult, PaneOperationKind,
74 PaneOperationOutcome, PanePlacement, PanePointerButton, PanePointerPosition, PanePrecisionMode,
75 PanePrecisionPolicy, PaneRepairAction, PaneRepairError, PaneRepairFailure, PaneRepairOutcome,
76 PaneResizeDirection, PaneResizeTarget, PaneScaleFactor, PaneSemanticInputEvent,
77 PaneSemanticInputEventError, PaneSemanticInputEventKind, PaneSemanticInputTrace,
78 PaneSemanticInputTraceError, PaneSemanticInputTraceMetadata,
79 PaneSemanticReplayConformanceArtifact, PaneSemanticReplayDiffArtifact,
80 PaneSemanticReplayDiffKind, PaneSemanticReplayError, PaneSemanticReplayFixture,
81 PaneSemanticReplayOutcome, PaneSnapDecision, PaneSnapReason, PaneSnapTuning, PaneSplit,
82 PaneSplitRatio, PaneTransaction, PaneTransactionOutcome, PaneTree, PaneTreeSnapshot, SplitAxis,
83};
84pub use responsive::Responsive;
85pub use responsive_layout::{ResponsiveLayout, ResponsiveSplit};
86use std::cmp::min;
87pub use visibility::Visibility;
88pub use workspace::{
89 MigrationResult, WORKSPACE_SCHEMA_VERSION, WorkspaceMetadata, WorkspaceMigrationError,
90 WorkspaceSnapshot, WorkspaceValidationError, migrate_workspace, needs_migration,
91};
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95pub enum Constraint {
96 Fixed(u16),
98 Percentage(f32),
100 Min(u16),
102 Max(u16),
104 Ratio(u32, u32),
106 Fill,
108 FitContent,
113 FitContentBounded {
118 min: u16,
120 max: u16,
122 },
123 FitMin,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub struct LayoutSizeHint {
153 pub min: u16,
155 pub preferred: u16,
157 pub max: Option<u16>,
159}
160
161impl LayoutSizeHint {
162 pub const ZERO: Self = Self {
164 min: 0,
165 preferred: 0,
166 max: None,
167 };
168
169 #[inline]
171 pub const fn exact(size: u16) -> Self {
172 Self {
173 min: size,
174 preferred: size,
175 max: Some(size),
176 }
177 }
178
179 #[inline]
181 pub const fn at_least(min: u16, preferred: u16) -> Self {
182 Self {
183 min,
184 preferred,
185 max: None,
186 }
187 }
188
189 #[inline]
191 pub fn clamp(&self, value: u16) -> u16 {
192 let max = self.max.unwrap_or(u16::MAX);
193 value.max(self.min).min(max)
194 }
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
199pub enum Direction {
200 #[default]
202 Vertical,
203 Horizontal,
205}
206
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
209pub enum Alignment {
210 #[default]
212 Start,
213 Center,
215 End,
217 SpaceAround,
219 SpaceBetween,
221}
222
223#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
236pub enum Breakpoint {
237 Xs,
239 Sm,
241 Md,
243 Lg,
245 Xl,
247}
248
249impl Breakpoint {
250 pub const ALL: [Breakpoint; 5] = [
252 Breakpoint::Xs,
253 Breakpoint::Sm,
254 Breakpoint::Md,
255 Breakpoint::Lg,
256 Breakpoint::Xl,
257 ];
258
259 #[inline]
261 const fn index(self) -> u8 {
262 match self {
263 Breakpoint::Xs => 0,
264 Breakpoint::Sm => 1,
265 Breakpoint::Md => 2,
266 Breakpoint::Lg => 3,
267 Breakpoint::Xl => 4,
268 }
269 }
270
271 #[must_use]
273 pub const fn label(self) -> &'static str {
274 match self {
275 Breakpoint::Xs => "xs",
276 Breakpoint::Sm => "sm",
277 Breakpoint::Md => "md",
278 Breakpoint::Lg => "lg",
279 Breakpoint::Xl => "xl",
280 }
281 }
282}
283
284impl std::fmt::Display for Breakpoint {
285 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286 f.write_str(self.label())
287 }
288}
289
290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295pub struct Breakpoints {
296 pub sm: u16,
298 pub md: u16,
300 pub lg: u16,
302 pub xl: u16,
304}
305
306impl Breakpoints {
307 pub const DEFAULT: Self = Self {
309 sm: 60,
310 md: 90,
311 lg: 120,
312 xl: 160,
313 };
314
315 pub const fn new(sm: u16, md: u16, lg: u16) -> Self {
319 let md = if md < sm { sm } else { md };
320 let lg = if lg < md { md } else { lg };
321 let xl = if lg + 40 > lg { lg + 40 } else { u16::MAX };
323 Self { sm, md, lg, xl }
324 }
325
326 pub const fn new_with_xl(sm: u16, md: u16, lg: u16, xl: u16) -> Self {
330 let md = if md < sm { sm } else { md };
331 let lg = if lg < md { md } else { lg };
332 let xl = if xl < lg { lg } else { xl };
333 Self { sm, md, lg, xl }
334 }
335
336 #[inline]
338 pub const fn classify_width(self, width: u16) -> Breakpoint {
339 if width >= self.xl {
340 Breakpoint::Xl
341 } else if width >= self.lg {
342 Breakpoint::Lg
343 } else if width >= self.md {
344 Breakpoint::Md
345 } else if width >= self.sm {
346 Breakpoint::Sm
347 } else {
348 Breakpoint::Xs
349 }
350 }
351
352 #[inline]
354 pub const fn classify_size(self, size: Size) -> Breakpoint {
355 self.classify_width(size.width)
356 }
357
358 #[inline]
360 pub const fn at_least(self, width: u16, min: Breakpoint) -> bool {
361 self.classify_width(width).index() >= min.index()
362 }
363
364 #[inline]
366 pub const fn between(self, width: u16, min: Breakpoint, max: Breakpoint) -> bool {
367 let idx = self.classify_width(width).index();
368 idx >= min.index() && idx <= max.index()
369 }
370
371 #[must_use]
373 pub const fn threshold(self, bp: Breakpoint) -> u16 {
374 match bp {
375 Breakpoint::Xs => 0,
376 Breakpoint::Sm => self.sm,
377 Breakpoint::Md => self.md,
378 Breakpoint::Lg => self.lg,
379 Breakpoint::Xl => self.xl,
380 }
381 }
382
383 #[must_use]
385 pub const fn thresholds(self) -> [(Breakpoint, u16); 5] {
386 [
387 (Breakpoint::Xs, 0),
388 (Breakpoint::Sm, self.sm),
389 (Breakpoint::Md, self.md),
390 (Breakpoint::Lg, self.lg),
391 (Breakpoint::Xl, self.xl),
392 ]
393 }
394}
395
396#[derive(Debug, Clone, Copy, Default)]
398pub struct Measurement {
399 pub min_width: u16,
401 pub min_height: u16,
403 pub max_width: Option<u16>,
405 pub max_height: Option<u16>,
407}
408
409impl Measurement {
410 #[must_use]
412 pub fn fixed(width: u16, height: u16) -> Self {
413 Self {
414 min_width: width,
415 min_height: height,
416 max_width: Some(width),
417 max_height: Some(height),
418 }
419 }
420
421 #[must_use]
423 pub fn flexible(min_width: u16, min_height: u16) -> Self {
424 Self {
425 min_width,
426 min_height,
427 max_width: None,
428 max_height: None,
429 }
430 }
431}
432
433#[derive(Debug, Clone, Default)]
435pub struct Flex {
436 direction: Direction,
437 constraints: Vec<Constraint>,
438 margin: Sides,
439 gap: u16,
440 alignment: Alignment,
441 flow_direction: direction::FlowDirection,
442}
443
444impl Flex {
445 #[must_use]
447 pub fn vertical() -> Self {
448 Self {
449 direction: Direction::Vertical,
450 ..Default::default()
451 }
452 }
453
454 #[must_use]
456 pub fn horizontal() -> Self {
457 Self {
458 direction: Direction::Horizontal,
459 ..Default::default()
460 }
461 }
462
463 #[must_use]
465 pub fn direction(mut self, direction: Direction) -> Self {
466 self.direction = direction;
467 self
468 }
469
470 #[must_use]
472 pub fn constraints(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
473 self.constraints = constraints.into_iter().collect();
474 self
475 }
476
477 #[must_use]
479 pub fn margin(mut self, margin: Sides) -> Self {
480 self.margin = margin;
481 self
482 }
483
484 #[must_use]
486 pub fn gap(mut self, gap: u16) -> Self {
487 self.gap = gap;
488 self
489 }
490
491 #[must_use]
493 pub fn alignment(mut self, alignment: Alignment) -> Self {
494 self.alignment = alignment;
495 self
496 }
497
498 #[must_use]
504 pub fn flow_direction(mut self, flow: direction::FlowDirection) -> Self {
505 self.flow_direction = flow;
506 self
507 }
508
509 #[must_use]
511 pub fn constraint_count(&self) -> usize {
512 self.constraints.len()
513 }
514
515 pub fn split(&self, area: Rect) -> Vec<Rect> {
517 let inner = area.inner(self.margin);
519 if inner.is_empty() {
520 return self.constraints.iter().map(|_| Rect::default()).collect();
521 }
522
523 let total_size = match self.direction {
524 Direction::Horizontal => inner.width,
525 Direction::Vertical => inner.height,
526 };
527
528 let count = self.constraints.len();
529 if count == 0 {
530 return Vec::new();
531 }
532
533 let gap_count = count - 1;
535 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
536 let available_size = total_size.saturating_sub(total_gap);
537
538 let sizes = solve_constraints(&self.constraints, available_size);
540
541 let mut rects = self.sizes_to_rects(inner, &sizes);
543
544 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
546 direction::mirror_rects_horizontal(&mut rects, inner);
547 }
548
549 rects
550 }
551
552 fn sizes_to_rects(&self, area: Rect, sizes: &[u16]) -> Vec<Rect> {
553 let mut rects = Vec::with_capacity(sizes.len());
554
555 let total_gaps = if sizes.len() > 1 {
557 let gap_count = sizes.len() - 1;
558 (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16
559 } else {
560 0
561 };
562 let total_used: u16 = sizes.iter().sum::<u16>().saturating_add(total_gaps);
563 let total_available = match self.direction {
564 Direction::Horizontal => area.width,
565 Direction::Vertical => area.height,
566 };
567 let leftover = total_available.saturating_sub(total_used);
568
569 let (start_offset, extra_gap) = match self.alignment {
571 Alignment::Start => (0, 0),
572 Alignment::End => (leftover, 0),
573 Alignment::Center => (leftover / 2, 0),
574 Alignment::SpaceBetween => (0, 0),
575 Alignment::SpaceAround => {
576 if sizes.is_empty() {
577 (0, 0)
578 } else {
579 let slots = sizes.len() * 2;
582 let unit = (leftover as usize / slots) as u16;
583 let rem = (leftover as usize % slots) as u16;
584 (unit + rem / 2, 0)
585 }
586 }
587 };
588
589 let mut current_pos = match self.direction {
590 Direction::Horizontal => area.x.saturating_add(start_offset),
591 Direction::Vertical => area.y.saturating_add(start_offset),
592 };
593
594 for (i, &size) in sizes.iter().enumerate() {
595 let rect = match self.direction {
596 Direction::Horizontal => Rect {
597 x: current_pos,
598 y: area.y,
599 width: size,
600 height: area.height,
601 },
602 Direction::Vertical => Rect {
603 x: area.x,
604 y: current_pos,
605 width: area.width,
606 height: size,
607 },
608 };
609 rects.push(rect);
610
611 current_pos = current_pos
613 .saturating_add(size)
614 .saturating_add(self.gap)
615 .saturating_add(extra_gap);
616
617 match self.alignment {
619 Alignment::SpaceBetween => {
620 if sizes.len() > 1 && i < sizes.len() - 1 {
621 let count = sizes.len() - 1; let base = (leftover as usize / count) as u16;
624 let rem = leftover as usize % count;
625 let extra = base + if i < rem { 1 } else { 0 };
627 current_pos = current_pos.saturating_add(extra);
628 }
629 }
630 Alignment::SpaceAround => {
631 if !sizes.is_empty() {
632 let slots = sizes.len() * 2; let unit = (leftover as usize / slots) as u16;
634 current_pos = current_pos.saturating_add(unit.saturating_mul(2));
635 }
636 }
637 _ => {}
638 }
639 }
640
641 rects
642 }
643
644 pub fn split_with_measurer<F>(&self, area: Rect, measurer: F) -> Vec<Rect>
668 where
669 F: Fn(usize, u16) -> LayoutSizeHint,
670 {
671 let inner = area.inner(self.margin);
673 if inner.is_empty() {
674 return self.constraints.iter().map(|_| Rect::default()).collect();
675 }
676
677 let total_size = match self.direction {
678 Direction::Horizontal => inner.width,
679 Direction::Vertical => inner.height,
680 };
681
682 let count = self.constraints.len();
683 if count == 0 {
684 return Vec::new();
685 }
686
687 let gap_count = count - 1;
689 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
690 let available_size = total_size.saturating_sub(total_gap);
691
692 let sizes = solve_constraints_with_hints(&self.constraints, available_size, &measurer);
694
695 let mut rects = self.sizes_to_rects(inner, &sizes);
697
698 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
700 direction::mirror_rects_horizontal(&mut rects, inner);
701 }
702
703 rects
704 }
705}
706
707pub(crate) fn solve_constraints(constraints: &[Constraint], available_size: u16) -> Vec<u16> {
712 solve_constraints_with_hints(constraints, available_size, &|_, _| LayoutSizeHint::ZERO)
714}
715
716pub(crate) fn solve_constraints_with_hints<F>(
721 constraints: &[Constraint],
722 available_size: u16,
723 measurer: &F,
724) -> Vec<u16>
725where
726 F: Fn(usize, u16) -> LayoutSizeHint,
727{
728 let mut sizes = vec![0u16; constraints.len()];
729 let mut remaining = available_size;
730 let mut grow_indices = Vec::new();
731
732 for (i, &constraint) in constraints.iter().enumerate() {
734 match constraint {
735 Constraint::Fixed(size) => {
736 let size = min(size, remaining);
737 sizes[i] = size;
738 remaining = remaining.saturating_sub(size);
739 }
740 Constraint::Percentage(p) => {
741 let size = (available_size as f32 * p / 100.0)
742 .round()
743 .min(u16::MAX as f32) as u16;
744 let size = min(size, remaining);
745 sizes[i] = size;
746 remaining = remaining.saturating_sub(size);
747 }
748 Constraint::Min(min_size) => {
749 let size = min(min_size, remaining);
750 sizes[i] = size;
751 remaining = remaining.saturating_sub(size);
752 grow_indices.push(i);
753 }
754 Constraint::Max(_) => {
755 grow_indices.push(i);
757 }
758 Constraint::Ratio(_, _) => {
759 grow_indices.push(i);
761 }
762 Constraint::Fill => {
763 grow_indices.push(i);
765 }
766 Constraint::FitContent => {
767 let hint = measurer(i, remaining);
769 let size = min(hint.preferred, remaining);
770 sizes[i] = size;
771 remaining = remaining.saturating_sub(size);
772 }
774 Constraint::FitContentBounded {
775 min: min_bound,
776 max: max_bound,
777 } => {
778 let hint = measurer(i, remaining);
780 let preferred = hint.preferred.max(min_bound).min(max_bound);
781 let size = min(preferred, remaining);
782 sizes[i] = size;
783 remaining = remaining.saturating_sub(size);
784 }
785 Constraint::FitMin => {
786 let hint = measurer(i, remaining);
788 let size = min(hint.min, remaining);
789 sizes[i] = size;
790 remaining = remaining.saturating_sub(size);
791 grow_indices.push(i);
793 }
794 }
795 }
796
797 loop {
799 if remaining == 0 || grow_indices.is_empty() {
800 break;
801 }
802
803 let mut total_weight = 0u64;
804 const WEIGHT_SCALE: u64 = 10_000;
805
806 for &i in &grow_indices {
807 match constraints[i] {
808 Constraint::Ratio(n, d) => {
809 let w = n as u64 * WEIGHT_SCALE / d.max(1) as u64;
810 total_weight += w;
812 }
813 _ => total_weight += WEIGHT_SCALE,
814 }
815 }
816
817 if total_weight == 0 {
818 break;
820 }
821
822 let space_to_distribute = remaining;
823 let mut allocated = 0;
824 let mut shares = vec![0u16; constraints.len()];
825
826 for (idx, &i) in grow_indices.iter().enumerate() {
827 let weight = match constraints[i] {
828 Constraint::Ratio(n, d) => n as u64 * WEIGHT_SCALE / d.max(1) as u64,
829 _ => WEIGHT_SCALE,
830 };
831
832 let size = if idx == grow_indices.len() - 1 {
835 if weight == 0 {
837 0
838 } else {
839 space_to_distribute.saturating_sub(allocated)
840 }
841 } else {
842 let s = (space_to_distribute as u64 * weight / total_weight) as u16;
843 min(s, space_to_distribute.saturating_sub(allocated))
844 };
845
846 shares[i] = size;
847 allocated += size;
848 }
849
850 let mut violations = Vec::new();
852 for &i in &grow_indices {
853 if let Constraint::Max(max_val) = constraints[i]
854 && sizes[i].saturating_add(shares[i]) > max_val
855 {
856 violations.push(i);
857 }
858 }
859
860 if violations.is_empty() {
861 for &i in &grow_indices {
863 sizes[i] = sizes[i].saturating_add(shares[i]);
864 }
865 break;
866 }
867
868 for i in violations {
870 if let Constraint::Max(max_val) = constraints[i] {
871 let consumed = max_val.saturating_sub(sizes[i]);
874 sizes[i] = max_val;
875 remaining = remaining.saturating_sub(consumed);
876
877 if let Some(pos) = grow_indices.iter().position(|&x| x == i) {
879 grow_indices.remove(pos);
880 }
881 }
882 }
883 }
884
885 sizes
886}
887
888pub type PreviousAllocation = Option<Vec<u16>>;
898
899pub fn round_layout_stable(targets: &[f64], total: u16, prev: PreviousAllocation) -> Vec<u16> {
961 let n = targets.len();
962 if n == 0 {
963 return Vec::new();
964 }
965
966 let floors: Vec<u16> = targets
968 .iter()
969 .map(|&r| (r.max(0.0).floor() as u64).min(u16::MAX as u64) as u16)
970 .collect();
971
972 let floor_sum: u16 = floors.iter().copied().sum();
973
974 let deficit = total.saturating_sub(floor_sum);
976
977 if deficit == 0 {
978 if floor_sum > total {
981 return redistribute_overflow(&floors, total);
982 }
983 return floors;
984 }
985
986 let mut priority: Vec<(usize, f64, bool)> = targets
988 .iter()
989 .enumerate()
990 .map(|(i, &r)| {
991 let remainder = r - (floors[i] as f64);
992 let ceil_val = floors[i].saturating_add(1);
993 let prev_used_ceil = prev
995 .as_ref()
996 .is_some_and(|p| p.get(i).copied() == Some(ceil_val));
997 (i, remainder, prev_used_ceil)
998 })
999 .collect();
1000
1001 priority.sort_by(|a, b| {
1003 b.1.partial_cmp(&a.1)
1004 .unwrap_or(std::cmp::Ordering::Equal)
1005 .then_with(|| {
1006 b.2.cmp(&a.2)
1008 })
1009 .then_with(|| {
1010 a.0.cmp(&b.0)
1012 })
1013 });
1014
1015 let mut result = floors;
1017 let distribute = (deficit as usize).min(n);
1018 for &(i, _, _) in priority.iter().take(distribute) {
1019 result[i] = result[i].saturating_add(1);
1020 }
1021
1022 result
1023}
1024
1025fn redistribute_overflow(floors: &[u16], total: u16) -> Vec<u16> {
1030 let mut result = floors.to_vec();
1031 let mut current_sum: u16 = result.iter().copied().sum();
1032
1033 while current_sum > total {
1035 if let Some((idx, _)) = result
1037 .iter()
1038 .enumerate()
1039 .filter(|item| *item.1 > 0)
1040 .max_by_key(|item| *item.1)
1041 {
1042 result[idx] = result[idx].saturating_sub(1);
1043 current_sum = current_sum.saturating_sub(1);
1044 } else {
1045 break;
1046 }
1047 }
1048
1049 result
1050}
1051
1052#[cfg(test)]
1053mod tests {
1054 use super::*;
1055
1056 #[test]
1057 fn fixed_split() {
1058 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(20)]);
1059 let rects = flex.split(Rect::new(0, 0, 100, 10));
1060 assert_eq!(rects.len(), 2);
1061 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1062 assert_eq!(rects[1], Rect::new(10, 0, 20, 10)); }
1064
1065 #[test]
1066 fn percentage_split() {
1067 let flex = Flex::horizontal()
1068 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1069 let rects = flex.split(Rect::new(0, 0, 100, 10));
1070 assert_eq!(rects[0].width, 50);
1071 assert_eq!(rects[1].width, 50);
1072 }
1073
1074 #[test]
1075 fn gap_handling() {
1076 let flex = Flex::horizontal()
1077 .gap(5)
1078 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1079 let rects = flex.split(Rect::new(0, 0, 100, 10));
1080 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1084 assert_eq!(rects[1], Rect::new(15, 0, 10, 10));
1085 }
1086
1087 #[test]
1088 fn mixed_constraints() {
1089 let flex = Flex::horizontal().constraints([
1090 Constraint::Fixed(10),
1091 Constraint::Min(10), Constraint::Percentage(10.0), ]);
1094
1095 let rects = flex.split(Rect::new(0, 0, 100, 1));
1103 assert_eq!(rects[0].width, 10); assert_eq!(rects[2].width, 10); assert_eq!(rects[1].width, 80); }
1107
1108 #[test]
1109 fn measurement_fixed_constraints() {
1110 let fixed = Measurement::fixed(5, 7);
1111 assert_eq!(fixed.min_width, 5);
1112 assert_eq!(fixed.min_height, 7);
1113 assert_eq!(fixed.max_width, Some(5));
1114 assert_eq!(fixed.max_height, Some(7));
1115 }
1116
1117 #[test]
1118 fn measurement_flexible_constraints() {
1119 let flexible = Measurement::flexible(2, 3);
1120 assert_eq!(flexible.min_width, 2);
1121 assert_eq!(flexible.min_height, 3);
1122 assert_eq!(flexible.max_width, None);
1123 assert_eq!(flexible.max_height, None);
1124 }
1125
1126 #[test]
1127 fn breakpoints_classify_defaults() {
1128 let bp = Breakpoints::DEFAULT;
1129 assert_eq!(bp.classify_width(20), Breakpoint::Xs);
1130 assert_eq!(bp.classify_width(60), Breakpoint::Sm);
1131 assert_eq!(bp.classify_width(90), Breakpoint::Md);
1132 assert_eq!(bp.classify_width(120), Breakpoint::Lg);
1133 }
1134
1135 #[test]
1136 fn breakpoints_at_least_and_between() {
1137 let bp = Breakpoints::new(50, 80, 110);
1138 assert!(bp.at_least(85, Breakpoint::Sm));
1139 assert!(bp.between(85, Breakpoint::Sm, Breakpoint::Md));
1140 assert!(!bp.between(85, Breakpoint::Lg, Breakpoint::Lg));
1141 }
1142
1143 #[test]
1144 fn alignment_end() {
1145 let flex = Flex::horizontal()
1146 .alignment(Alignment::End)
1147 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1148 let rects = flex.split(Rect::new(0, 0, 100, 10));
1149 assert_eq!(rects[0], Rect::new(80, 0, 10, 10));
1151 assert_eq!(rects[1], Rect::new(90, 0, 10, 10));
1152 }
1153
1154 #[test]
1155 fn alignment_center() {
1156 let flex = Flex::horizontal()
1157 .alignment(Alignment::Center)
1158 .constraints([Constraint::Fixed(20), Constraint::Fixed(20)]);
1159 let rects = flex.split(Rect::new(0, 0, 100, 10));
1160 assert_eq!(rects[0], Rect::new(30, 0, 20, 10));
1162 assert_eq!(rects[1], Rect::new(50, 0, 20, 10));
1163 }
1164
1165 #[test]
1166 fn alignment_space_between() {
1167 let flex = Flex::horizontal()
1168 .alignment(Alignment::SpaceBetween)
1169 .constraints([
1170 Constraint::Fixed(10),
1171 Constraint::Fixed(10),
1172 Constraint::Fixed(10),
1173 ]);
1174 let rects = flex.split(Rect::new(0, 0, 100, 10));
1175 assert_eq!(rects[0].x, 0);
1177 assert_eq!(rects[1].x, 45); assert_eq!(rects[2].x, 90); }
1180
1181 #[test]
1182 fn vertical_alignment() {
1183 let flex = Flex::vertical()
1184 .alignment(Alignment::End)
1185 .constraints([Constraint::Fixed(5), Constraint::Fixed(5)]);
1186 let rects = flex.split(Rect::new(0, 0, 10, 100));
1187 assert_eq!(rects[0], Rect::new(0, 90, 10, 5));
1189 assert_eq!(rects[1], Rect::new(0, 95, 10, 5));
1190 }
1191
1192 #[test]
1193 fn nested_flex_support() {
1194 let outer = Flex::horizontal()
1196 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1197 let outer_rects = outer.split(Rect::new(0, 0, 100, 100));
1198
1199 let inner = Flex::vertical().constraints([Constraint::Fixed(30), Constraint::Min(10)]);
1201 let inner_rects = inner.split(outer_rects[0]);
1202
1203 assert_eq!(inner_rects[0], Rect::new(0, 0, 50, 30));
1204 assert_eq!(inner_rects[1], Rect::new(0, 30, 50, 70));
1205 }
1206
1207 #[test]
1209 fn invariant_total_size_does_not_exceed_available() {
1210 for total in [10u16, 50, 100, 255] {
1212 let flex = Flex::horizontal().constraints([
1213 Constraint::Fixed(30),
1214 Constraint::Percentage(50.0),
1215 Constraint::Min(20),
1216 ]);
1217 let rects = flex.split(Rect::new(0, 0, total, 10));
1218 let total_width: u16 = rects.iter().map(|r| r.width).sum();
1219 assert!(
1220 total_width <= total,
1221 "Total width {} exceeded available {} for constraints",
1222 total_width,
1223 total
1224 );
1225 }
1226 }
1227
1228 #[test]
1229 fn invariant_empty_area_produces_empty_rects() {
1230 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1231 let rects = flex.split(Rect::new(0, 0, 0, 0));
1232 assert!(rects.iter().all(|r| r.is_empty()));
1233 }
1234
1235 #[test]
1236 fn invariant_no_constraints_produces_empty_vec() {
1237 let flex = Flex::horizontal().constraints([]);
1238 let rects = flex.split(Rect::new(0, 0, 100, 100));
1239 assert!(rects.is_empty());
1240 }
1241
1242 #[test]
1245 fn ratio_constraint_splits_proportionally() {
1246 let flex =
1247 Flex::horizontal().constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
1248 let rects = flex.split(Rect::new(0, 0, 90, 10));
1249 assert_eq!(rects[0].width, 30);
1250 assert_eq!(rects[1].width, 60);
1251 }
1252
1253 #[test]
1254 fn ratio_constraint_with_zero_denominator() {
1255 let flex = Flex::horizontal().constraints([Constraint::Ratio(1, 0)]);
1257 let rects = flex.split(Rect::new(0, 0, 100, 10));
1258 assert_eq!(rects.len(), 1);
1259 }
1260
1261 #[test]
1262 fn ratio_is_weighted_not_an_absolute_fraction() {
1263 let area = Rect::new(0, 0, 100, 1);
1264
1265 let rects = Flex::horizontal()
1267 .constraints([Constraint::Percentage(25.0)])
1268 .split(area);
1269 assert_eq!(rects[0].width, 25);
1270
1271 let rects = Flex::horizontal()
1273 .constraints([Constraint::Ratio(1, 4)])
1274 .split(area);
1275 assert_eq!(rects[0].width, 100);
1276 }
1277
1278 #[test]
1279 fn ratio_is_weighted_against_other_grow_items() {
1280 let area = Rect::new(0, 0, 100, 1);
1281
1282 let rects = Flex::horizontal()
1285 .constraints([Constraint::Ratio(1, 4), Constraint::Fill])
1286 .split(area);
1287 assert_eq!(rects[0].width, 20);
1288 assert_eq!(rects[1].width, 80);
1289 }
1290
1291 #[test]
1292 fn ratio_zero_numerator_should_be_zero() {
1293 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
1296 let rects = flex.split(Rect::new(0, 0, 100, 1));
1297
1298 assert_eq!(rects[0].width, 100, "Fill should take all space");
1300 assert_eq!(rects[1].width, 0, "Ratio(0, 1) should be width 0");
1301 }
1302
1303 #[test]
1306 fn max_constraint_clamps_size() {
1307 let flex = Flex::horizontal().constraints([Constraint::Max(20), Constraint::Fixed(30)]);
1308 let rects = flex.split(Rect::new(0, 0, 100, 10));
1309 assert!(rects[0].width <= 20);
1310 assert_eq!(rects[1].width, 30);
1311 }
1312
1313 #[test]
1314 fn percentage_rounding_never_exceeds_available() {
1315 let constraints = [
1316 Constraint::Percentage(33.4),
1317 Constraint::Percentage(33.3),
1318 Constraint::Percentage(33.3),
1319 ];
1320 let sizes = solve_constraints(&constraints, 7);
1321 let total: u16 = sizes.iter().sum();
1322 assert!(total <= 7, "percent rounding overflowed: {sizes:?}");
1323 assert!(sizes.iter().all(|size| *size <= 7));
1324 }
1325
1326 #[test]
1327 fn tiny_area_saturates_fixed_and_min() {
1328 let constraints = [Constraint::Fixed(5), Constraint::Min(3), Constraint::Max(2)];
1329 let sizes = solve_constraints(&constraints, 2);
1330 assert_eq!(sizes[0], 2);
1331 assert_eq!(sizes[1], 0);
1332 assert_eq!(sizes[2], 0);
1333 assert_eq!(sizes.iter().sum::<u16>(), 2);
1334 }
1335
1336 #[test]
1337 fn ratio_distribution_sums_to_available() {
1338 let constraints = [Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)];
1339 let sizes = solve_constraints(&constraints, 5);
1340 assert_eq!(sizes.iter().sum::<u16>(), 5);
1341 assert_eq!(sizes[0], 1);
1342 assert_eq!(sizes[1], 4);
1343 }
1344
1345 #[test]
1346 fn flex_gap_exceeds_area_yields_zero_widths() {
1347 let flex = Flex::horizontal()
1348 .gap(5)
1349 .constraints([Constraint::Fixed(1), Constraint::Fixed(1)]);
1350 let rects = flex.split(Rect::new(0, 0, 3, 1));
1351 assert_eq!(rects.len(), 2);
1352 assert_eq!(rects[0].width, 0);
1353 assert_eq!(rects[1].width, 0);
1354 }
1355
1356 #[test]
1359 fn alignment_space_around() {
1360 let flex = Flex::horizontal()
1361 .alignment(Alignment::SpaceAround)
1362 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1363 let rects = flex.split(Rect::new(0, 0, 100, 10));
1364
1365 assert_eq!(rects[0].x, 20);
1368 assert_eq!(rects[1].x, 70);
1369 }
1370
1371 #[test]
1374 fn vertical_gap() {
1375 let flex = Flex::vertical()
1376 .gap(5)
1377 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1378 let rects = flex.split(Rect::new(0, 0, 50, 100));
1379 assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
1380 assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
1381 }
1382
1383 #[test]
1386 fn vertical_center() {
1387 let flex = Flex::vertical()
1388 .alignment(Alignment::Center)
1389 .constraints([Constraint::Fixed(10)]);
1390 let rects = flex.split(Rect::new(0, 0, 50, 100));
1391 assert_eq!(rects[0].y, 45);
1393 assert_eq!(rects[0].height, 10);
1394 }
1395
1396 #[test]
1399 fn single_min_takes_all() {
1400 let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
1401 let rects = flex.split(Rect::new(0, 0, 80, 24));
1402 assert_eq!(rects[0].width, 80);
1403 }
1404
1405 #[test]
1408 fn fixed_exceeds_available_clamped() {
1409 let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
1410 let rects = flex.split(Rect::new(0, 0, 100, 10));
1411 assert_eq!(rects[0].width, 60);
1413 assert_eq!(rects[1].width, 40);
1414 }
1415
1416 #[test]
1419 fn percentage_overflow_clamped() {
1420 let flex = Flex::horizontal()
1421 .constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
1422 let rects = flex.split(Rect::new(0, 0, 100, 10));
1423 assert_eq!(rects[0].width, 80);
1424 assert_eq!(rects[1].width, 20); }
1426
1427 #[test]
1430 fn margin_reduces_split_area() {
1431 let flex = Flex::horizontal()
1432 .margin(Sides::all(10))
1433 .constraints([Constraint::Fixed(20), Constraint::Min(0)]);
1434 let rects = flex.split(Rect::new(0, 0, 100, 100));
1435 assert_eq!(rects[0].x, 10);
1437 assert_eq!(rects[0].y, 10);
1438 assert_eq!(rects[0].width, 20);
1439 assert_eq!(rects[0].height, 80);
1440 }
1441
1442 #[test]
1445 fn builder_methods_chain() {
1446 let flex = Flex::vertical()
1447 .direction(Direction::Horizontal)
1448 .gap(3)
1449 .margin(Sides::all(1))
1450 .alignment(Alignment::End)
1451 .constraints([Constraint::Fixed(10)]);
1452 let rects = flex.split(Rect::new(0, 0, 50, 50));
1453 assert_eq!(rects.len(), 1);
1454 }
1455
1456 #[test]
1459 fn space_between_single_item() {
1460 let flex = Flex::horizontal()
1461 .alignment(Alignment::SpaceBetween)
1462 .constraints([Constraint::Fixed(10)]);
1463 let rects = flex.split(Rect::new(0, 0, 100, 10));
1464 assert_eq!(rects[0].x, 0);
1466 assert_eq!(rects[0].width, 10);
1467 }
1468
1469 #[test]
1470 fn invariant_rects_within_bounds() {
1471 let area = Rect::new(10, 20, 80, 60);
1472 let flex = Flex::horizontal()
1473 .margin(Sides::all(5))
1474 .gap(2)
1475 .constraints([
1476 Constraint::Fixed(15),
1477 Constraint::Percentage(30.0),
1478 Constraint::Min(10),
1479 ]);
1480 let rects = flex.split(area);
1481
1482 let inner = area.inner(Sides::all(5));
1484 for rect in &rects {
1485 assert!(
1486 rect.x >= inner.x && rect.right() <= inner.right(),
1487 "Rect {:?} exceeds horizontal bounds of {:?}",
1488 rect,
1489 inner
1490 );
1491 assert!(
1492 rect.y >= inner.y && rect.bottom() <= inner.bottom(),
1493 "Rect {:?} exceeds vertical bounds of {:?}",
1494 rect,
1495 inner
1496 );
1497 }
1498 }
1499
1500 #[test]
1503 fn fill_takes_remaining_space() {
1504 let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
1505 let rects = flex.split(Rect::new(0, 0, 100, 10));
1506 assert_eq!(rects[0].width, 20);
1507 assert_eq!(rects[1].width, 80); }
1509
1510 #[test]
1511 fn multiple_fills_share_space() {
1512 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
1513 let rects = flex.split(Rect::new(0, 0, 100, 10));
1514 assert_eq!(rects[0].width, 50);
1515 assert_eq!(rects[1].width, 50);
1516 }
1517
1518 #[test]
1521 fn fit_content_uses_preferred_size() {
1522 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1523 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1524 if idx == 0 {
1525 LayoutSizeHint {
1526 min: 5,
1527 preferred: 30,
1528 max: None,
1529 }
1530 } else {
1531 LayoutSizeHint::ZERO
1532 }
1533 });
1534 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
1537
1538 #[test]
1539 fn fit_content_clamps_to_available() {
1540 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
1541 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1542 min: 10,
1543 preferred: 80,
1544 max: None,
1545 });
1546 assert_eq!(rects[0].width, 80);
1548 assert_eq!(rects[1].width, 20);
1549 }
1550
1551 #[test]
1552 fn fit_content_without_measurer_gets_zero() {
1553 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1555 let rects = flex.split(Rect::new(0, 0, 100, 10));
1556 assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
1559
1560 #[test]
1561 fn fit_content_zero_area_returns_empty_rects() {
1562 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1563 let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
1564 min: 5,
1565 preferred: 10,
1566 max: None,
1567 });
1568 assert_eq!(rects.len(), 2);
1569 assert_eq!(rects[0].width, 0);
1570 assert_eq!(rects[0].height, 0);
1571 assert_eq!(rects[1].width, 0);
1572 assert_eq!(rects[1].height, 0);
1573 }
1574
1575 #[test]
1576 fn fit_content_tiny_available_clamps_to_remaining() {
1577 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1578 let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
1579 min: 5,
1580 preferred: 10,
1581 max: None,
1582 });
1583 assert_eq!(rects[0].width, 1);
1584 assert_eq!(rects[1].width, 0);
1585 }
1586
1587 #[test]
1590 fn fit_content_bounded_clamps_to_min() {
1591 let flex = Flex::horizontal().constraints([
1592 Constraint::FitContentBounded { min: 20, max: 50 },
1593 Constraint::Fill,
1594 ]);
1595 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1596 min: 5,
1597 preferred: 10, max: None,
1599 });
1600 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
1602 }
1603
1604 #[test]
1605 fn fit_content_bounded_respects_small_available() {
1606 let flex = Flex::horizontal().constraints([
1607 Constraint::FitContentBounded { min: 20, max: 50 },
1608 Constraint::Fill,
1609 ]);
1610 let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
1611 min: 5,
1612 preferred: 10,
1613 max: None,
1614 });
1615 assert_eq!(rects[0].width, 5);
1617 assert_eq!(rects[1].width, 0);
1618 }
1619
1620 #[test]
1621 fn fit_content_vertical_uses_preferred_height() {
1622 let flex = Flex::vertical().constraints([Constraint::FitContent, Constraint::Fill]);
1623 let rects = flex.split_with_measurer(Rect::new(0, 0, 10, 10), |idx, _| {
1624 if idx == 0 {
1625 LayoutSizeHint {
1626 min: 1,
1627 preferred: 4,
1628 max: None,
1629 }
1630 } else {
1631 LayoutSizeHint::ZERO
1632 }
1633 });
1634 assert_eq!(rects[0].height, 4);
1635 assert_eq!(rects[1].height, 6);
1636 }
1637
1638 #[test]
1639 fn fit_content_bounded_clamps_to_max() {
1640 let flex = Flex::horizontal().constraints([
1641 Constraint::FitContentBounded { min: 10, max: 30 },
1642 Constraint::Fill,
1643 ]);
1644 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1645 min: 5,
1646 preferred: 50, max: None,
1648 });
1649 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
1651 }
1652
1653 #[test]
1654 fn fit_content_bounded_uses_preferred_when_in_range() {
1655 let flex = Flex::horizontal().constraints([
1656 Constraint::FitContentBounded { min: 10, max: 50 },
1657 Constraint::Fill,
1658 ]);
1659 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1660 min: 5,
1661 preferred: 35, max: None,
1663 });
1664 assert_eq!(rects[0].width, 35);
1665 assert_eq!(rects[1].width, 65);
1666 }
1667
1668 #[test]
1671 fn fit_min_uses_minimum_size() {
1672 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1673 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1674 if idx == 0 {
1675 LayoutSizeHint {
1676 min: 15,
1677 preferred: 40,
1678 max: None,
1679 }
1680 } else {
1681 LayoutSizeHint::ZERO
1682 }
1683 });
1684 let total: u16 = rects.iter().map(|r| r.width).sum();
1698 assert_eq!(total, 100);
1699 assert!(rects[0].width >= 15, "FitMin should get at least minimum");
1700 }
1701
1702 #[test]
1703 fn fit_min_without_measurer_gets_zero() {
1704 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1705 let rects = flex.split(Rect::new(0, 0, 100, 10));
1706 assert_eq!(rects[0].width, 50);
1709 assert_eq!(rects[1].width, 50);
1710 }
1711
1712 #[test]
1715 fn layout_size_hint_zero_is_default() {
1716 assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
1717 }
1718
1719 #[test]
1720 fn layout_size_hint_exact() {
1721 let h = LayoutSizeHint::exact(25);
1722 assert_eq!(h.min, 25);
1723 assert_eq!(h.preferred, 25);
1724 assert_eq!(h.max, Some(25));
1725 }
1726
1727 #[test]
1728 fn layout_size_hint_at_least() {
1729 let h = LayoutSizeHint::at_least(10, 30);
1730 assert_eq!(h.min, 10);
1731 assert_eq!(h.preferred, 30);
1732 assert_eq!(h.max, None);
1733 }
1734
1735 #[test]
1736 fn layout_size_hint_clamp() {
1737 let h = LayoutSizeHint {
1738 min: 10,
1739 preferred: 20,
1740 max: Some(30),
1741 };
1742 assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
1746
1747 #[test]
1748 fn layout_size_hint_clamp_unbounded() {
1749 let h = LayoutSizeHint::at_least(5, 10);
1750 assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
1753
1754 #[test]
1757 fn fit_content_with_fixed_and_fill() {
1758 let flex = Flex::horizontal().constraints([
1759 Constraint::Fixed(20),
1760 Constraint::FitContent,
1761 Constraint::Fill,
1762 ]);
1763 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1764 if idx == 1 {
1765 LayoutSizeHint {
1766 min: 5,
1767 preferred: 25,
1768 max: None,
1769 }
1770 } else {
1771 LayoutSizeHint::ZERO
1772 }
1773 });
1774 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
1778
1779 #[test]
1780 fn total_allocation_never_exceeds_available_with_fit_content() {
1781 for available in [10u16, 50, 100, 255] {
1782 let flex = Flex::horizontal().constraints([
1783 Constraint::FitContent,
1784 Constraint::FitContent,
1785 Constraint::Fill,
1786 ]);
1787 let rects =
1788 flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
1789 min: 10,
1790 preferred: 40,
1791 max: None,
1792 });
1793 let total: u16 = rects.iter().map(|r| r.width).sum();
1794 assert!(
1795 total <= available,
1796 "Total {} exceeded available {} with FitContent",
1797 total,
1798 available
1799 );
1800 }
1801 }
1802
1803 mod rounding_tests {
1808 use super::super::*;
1809
1810 #[test]
1813 fn rounding_conserves_sum_exact() {
1814 let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
1815 assert_eq!(result.iter().copied().sum::<u16>(), 40);
1816 assert_eq!(result, vec![10, 20, 10]);
1817 }
1818
1819 #[test]
1820 fn rounding_conserves_sum_fractional() {
1821 let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
1822 assert_eq!(
1823 result.iter().copied().sum::<u16>(),
1824 40,
1825 "Sum must equal total: {:?}",
1826 result
1827 );
1828 }
1829
1830 #[test]
1831 fn rounding_conserves_sum_many_fractions() {
1832 let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
1833 let result = round_layout_stable(&targets, 100, None);
1834 assert_eq!(
1835 result.iter().copied().sum::<u16>(),
1836 100,
1837 "Sum must be exactly 100: {:?}",
1838 result
1839 );
1840 }
1841
1842 #[test]
1843 fn rounding_conserves_sum_all_half() {
1844 let targets = vec![10.5, 10.5, 10.5, 10.5];
1845 let result = round_layout_stable(&targets, 42, None);
1846 assert_eq!(
1847 result.iter().copied().sum::<u16>(),
1848 42,
1849 "Sum must be exactly 42: {:?}",
1850 result
1851 );
1852 }
1853
1854 #[test]
1857 fn rounding_displacement_bounded() {
1858 let targets = vec![33.33, 33.33, 33.34];
1859 let result = round_layout_stable(&targets, 100, None);
1860 assert_eq!(result.iter().copied().sum::<u16>(), 100);
1861
1862 for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
1863 let floor = r.floor() as u16;
1864 let ceil = floor + 1;
1865 assert!(
1866 x == floor || x == ceil,
1867 "Element {} = {} not in {{floor={}, ceil={}}} of target {}",
1868 i,
1869 x,
1870 floor,
1871 ceil,
1872 r
1873 );
1874 }
1875 }
1876
1877 #[test]
1880 fn temporal_tiebreak_stable_when_unchanged() {
1881 let targets = vec![10.5, 10.5, 10.5, 10.5];
1882 let first = round_layout_stable(&targets, 42, None);
1883 let second = round_layout_stable(&targets, 42, Some(first.clone()));
1884 assert_eq!(
1885 first, second,
1886 "Identical targets should produce identical results"
1887 );
1888 }
1889
1890 #[test]
1891 fn temporal_tiebreak_prefers_previous_direction() {
1892 let targets = vec![10.5, 10.5];
1893 let total = 21;
1894 let first = round_layout_stable(&targets, total, None);
1895 assert_eq!(first.iter().copied().sum::<u16>(), total);
1896 let second = round_layout_stable(&targets, total, Some(first.clone()));
1897 assert_eq!(first, second, "Should maintain rounding direction");
1898 }
1899
1900 #[test]
1901 fn temporal_tiebreak_adapts_to_changed_targets() {
1902 let targets_a = vec![10.5, 10.5];
1903 let result_a = round_layout_stable(&targets_a, 21, None);
1904 let targets_b = vec![15.7, 5.3];
1905 let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
1906 assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
1907 assert!(result_b[0] > result_b[1], "Should follow larger target");
1908 }
1909
1910 #[test]
1913 fn property_min_displacement_brute_force_small() {
1914 let targets = vec![3.3, 3.3, 3.4];
1915 let total: u16 = 10;
1916 let result = round_layout_stable(&targets, total, None);
1917 let our_displacement: f64 = result
1918 .iter()
1919 .zip(targets.iter())
1920 .map(|(&x, &r)| (x as f64 - r).abs())
1921 .sum();
1922
1923 let mut min_displacement = f64::MAX;
1924 let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
1925 let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
1926
1927 for a in floors[0]..=ceils[0] {
1928 for b in floors[1]..=ceils[1] {
1929 for c in floors[2]..=ceils[2] {
1930 if a + b + c == total {
1931 let disp = (a as f64 - targets[0]).abs()
1932 + (b as f64 - targets[1]).abs()
1933 + (c as f64 - targets[2]).abs();
1934 if disp < min_displacement {
1935 min_displacement = disp;
1936 }
1937 }
1938 }
1939 }
1940 }
1941
1942 assert!(
1943 (our_displacement - min_displacement).abs() < 1e-10,
1944 "Our displacement {} should match optimal {}: {:?}",
1945 our_displacement,
1946 min_displacement,
1947 result
1948 );
1949 }
1950
1951 #[test]
1954 fn rounding_deterministic() {
1955 let targets = vec![7.7, 8.3, 14.0];
1956 let a = round_layout_stable(&targets, 30, None);
1957 let b = round_layout_stable(&targets, 30, None);
1958 assert_eq!(a, b, "Same inputs must produce identical outputs");
1959 }
1960
1961 #[test]
1964 fn rounding_empty_targets() {
1965 let result = round_layout_stable(&[], 0, None);
1966 assert!(result.is_empty());
1967 }
1968
1969 #[test]
1970 fn rounding_single_element() {
1971 let result = round_layout_stable(&[10.7], 11, None);
1972 assert_eq!(result, vec![11]);
1973 }
1974
1975 #[test]
1976 fn rounding_zero_total() {
1977 let result = round_layout_stable(&[5.0, 5.0], 0, None);
1978 assert_eq!(result.iter().copied().sum::<u16>(), 0);
1979 }
1980
1981 #[test]
1982 fn rounding_all_zeros() {
1983 let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
1984 assert_eq!(result, vec![0, 0, 0]);
1985 }
1986
1987 #[test]
1988 fn rounding_integer_targets() {
1989 let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
1990 assert_eq!(result, vec![10, 20, 30]);
1991 }
1992
1993 #[test]
1994 fn rounding_large_deficit() {
1995 let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
1996 assert_eq!(result.iter().copied().sum::<u16>(), 3);
1997 assert_eq!(result, vec![1, 1, 1]);
1998 }
1999
2000 #[test]
2001 fn rounding_with_prev_different_length() {
2002 let result = round_layout_stable(&[10.5, 10.5], 21, Some(vec![11, 10, 5]));
2003 assert_eq!(result.iter().copied().sum::<u16>(), 21);
2004 }
2005
2006 #[test]
2007 fn rounding_very_small_fractions() {
2008 let targets = vec![10.001, 20.001, 9.998];
2009 let result = round_layout_stable(&targets, 40, None);
2010 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2011 }
2012
2013 #[test]
2014 fn rounding_conserves_sum_stress() {
2015 let n = 50;
2016 let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
2017 let total = 120u16;
2018 let result = round_layout_stable(&targets, total, None);
2019 assert_eq!(
2020 result.iter().copied().sum::<u16>(),
2021 total,
2022 "Sum must be exactly {} for {} items: {:?}",
2023 total,
2024 n,
2025 result
2026 );
2027 }
2028 }
2029
2030 mod property_constraint_tests {
2035 use super::super::*;
2036
2037 struct Lcg(u64);
2039
2040 impl Lcg {
2041 fn new(seed: u64) -> Self {
2042 Self(seed)
2043 }
2044 fn next_u32(&mut self) -> u32 {
2045 self.0 = self
2046 .0
2047 .wrapping_mul(6_364_136_223_846_793_005)
2048 .wrapping_add(1);
2049 (self.0 >> 33) as u32
2050 }
2051 fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
2052 if lo >= hi {
2053 return lo;
2054 }
2055 lo + (self.next_u32() % (hi - lo) as u32) as u16
2056 }
2057 fn next_f32(&mut self) -> f32 {
2058 (self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
2059 }
2060 }
2061
2062 fn random_constraint(rng: &mut Lcg) -> Constraint {
2064 match rng.next_u32() % 7 {
2065 0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
2066 1 => Constraint::Percentage(rng.next_f32() * 100.0),
2067 2 => Constraint::Min(rng.next_u16_range(0, 40)),
2068 3 => Constraint::Max(rng.next_u16_range(5, 120)),
2069 4 => {
2070 let n = rng.next_u32() % 5 + 1;
2071 let d = rng.next_u32() % 5 + 1;
2072 Constraint::Ratio(n, d)
2073 }
2074 5 => Constraint::Fill,
2075 _ => Constraint::FitContent,
2076 }
2077 }
2078
2079 #[test]
2080 fn property_constraints_respected_fixed() {
2081 let mut rng = Lcg::new(0xDEAD_BEEF);
2082 for _ in 0..200 {
2083 let fixed_val = rng.next_u16_range(1, 60);
2084 let avail = rng.next_u16_range(10, 200);
2085 let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
2086 let rects = flex.split(Rect::new(0, 0, avail, 10));
2087 assert!(
2088 rects[0].width <= fixed_val.min(avail),
2089 "Fixed({}) in avail {} -> width {}",
2090 fixed_val,
2091 avail,
2092 rects[0].width
2093 );
2094 }
2095 }
2096
2097 #[test]
2098 fn property_constraints_respected_max() {
2099 let mut rng = Lcg::new(0xCAFE_BABE);
2100 for _ in 0..200 {
2101 let max_val = rng.next_u16_range(5, 80);
2102 let avail = rng.next_u16_range(10, 200);
2103 let flex =
2104 Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
2105 let rects = flex.split(Rect::new(0, 0, avail, 10));
2106 assert!(
2107 rects[0].width <= max_val,
2108 "Max({}) in avail {} -> width {}",
2109 max_val,
2110 avail,
2111 rects[0].width
2112 );
2113 }
2114 }
2115
2116 #[test]
2117 fn property_constraints_respected_min() {
2118 let mut rng = Lcg::new(0xBAAD_F00D);
2119 for _ in 0..200 {
2120 let min_val = rng.next_u16_range(0, 40);
2121 let avail = rng.next_u16_range(min_val.max(1), 200);
2122 let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
2123 let rects = flex.split(Rect::new(0, 0, avail, 10));
2124 assert!(
2125 rects[0].width >= min_val,
2126 "Min({}) in avail {} -> width {}",
2127 min_val,
2128 avail,
2129 rects[0].width
2130 );
2131 }
2132 }
2133
2134 #[test]
2135 fn property_constraints_respected_ratio_proportional() {
2136 let mut rng = Lcg::new(0x1234_5678);
2137 for _ in 0..200 {
2138 let n1 = rng.next_u32() % 5 + 1;
2139 let n2 = rng.next_u32() % 5 + 1;
2140 let d = rng.next_u32() % 5 + 1;
2141 let avail = rng.next_u16_range(20, 200);
2142 let flex = Flex::horizontal()
2143 .constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
2144 let rects = flex.split(Rect::new(0, 0, avail, 10));
2145 let w1 = rects[0].width as f64;
2146 let w2 = rects[1].width as f64;
2147 let total = w1 + w2;
2148 if total > 0.0 {
2149 let expected_ratio = n1 as f64 / (n1 + n2) as f64;
2150 let actual_ratio = w1 / total;
2151 assert!(
2152 (actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
2153 "Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
2154 n1,
2155 d,
2156 n1,
2157 n2,
2158 avail,
2159 expected_ratio,
2160 actual_ratio,
2161 w1,
2162 w2
2163 );
2164 }
2165 }
2166 }
2167
2168 #[test]
2169 fn property_total_allocation_never_exceeds_available() {
2170 let mut rng = Lcg::new(0xFACE_FEED);
2171 for _ in 0..500 {
2172 let n = (rng.next_u32() % 6 + 1) as usize;
2173 let constraints: Vec<Constraint> =
2174 (0..n).map(|_| random_constraint(&mut rng)).collect();
2175 let avail = rng.next_u16_range(5, 200);
2176 let dir = if rng.next_u32().is_multiple_of(2) {
2177 Direction::Horizontal
2178 } else {
2179 Direction::Vertical
2180 };
2181 let flex = Flex::default().direction(dir).constraints(constraints);
2182 let area = Rect::new(0, 0, avail, avail);
2183 let rects = flex.split(area);
2184 let total: u16 = rects
2185 .iter()
2186 .map(|r| match dir {
2187 Direction::Horizontal => r.width,
2188 Direction::Vertical => r.height,
2189 })
2190 .sum();
2191 assert!(
2192 total <= avail,
2193 "Total {} exceeded available {} with {} constraints",
2194 total,
2195 avail,
2196 n
2197 );
2198 }
2199 }
2200
2201 #[test]
2202 fn property_no_overlap_horizontal() {
2203 let mut rng = Lcg::new(0xABCD_1234);
2204 for _ in 0..300 {
2205 let n = (rng.next_u32() % 5 + 2) as usize;
2206 let constraints: Vec<Constraint> =
2207 (0..n).map(|_| random_constraint(&mut rng)).collect();
2208 let avail = rng.next_u16_range(20, 200);
2209 let flex = Flex::horizontal().constraints(constraints);
2210 let rects = flex.split(Rect::new(0, 0, avail, 10));
2211
2212 for i in 1..rects.len() {
2213 let prev_end = rects[i - 1].x + rects[i - 1].width;
2214 assert!(
2215 rects[i].x >= prev_end,
2216 "Overlap at {}: prev ends {}, next starts {}",
2217 i,
2218 prev_end,
2219 rects[i].x
2220 );
2221 }
2222 }
2223 }
2224
2225 #[test]
2226 fn property_deterministic_across_runs() {
2227 let mut rng = Lcg::new(0x9999_8888);
2228 for _ in 0..100 {
2229 let n = (rng.next_u32() % 5 + 1) as usize;
2230 let constraints: Vec<Constraint> =
2231 (0..n).map(|_| random_constraint(&mut rng)).collect();
2232 let avail = rng.next_u16_range(10, 200);
2233 let r1 = Flex::horizontal()
2234 .constraints(constraints.clone())
2235 .split(Rect::new(0, 0, avail, 10));
2236 let r2 = Flex::horizontal()
2237 .constraints(constraints)
2238 .split(Rect::new(0, 0, avail, 10));
2239 assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
2240 }
2241 }
2242 }
2243
2244 mod property_temporal_tests {
2249 use super::super::*;
2250 use crate::cache::{CoherenceCache, CoherenceId};
2251
2252 struct Lcg(u64);
2254
2255 impl Lcg {
2256 fn new(seed: u64) -> Self {
2257 Self(seed)
2258 }
2259 fn next_u32(&mut self) -> u32 {
2260 self.0 = self
2261 .0
2262 .wrapping_mul(6_364_136_223_846_793_005)
2263 .wrapping_add(1);
2264 (self.0 >> 33) as u32
2265 }
2266 }
2267
2268 #[test]
2269 fn property_temporal_stability_small_resize() {
2270 let constraints = [
2271 Constraint::Percentage(33.3),
2272 Constraint::Percentage(33.3),
2273 Constraint::Fill,
2274 ];
2275 let mut coherence = CoherenceCache::new(64);
2276 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2277
2278 for total in [80u16, 100, 120] {
2279 let flex = Flex::horizontal().constraints(constraints);
2280 let rects = flex.split(Rect::new(0, 0, total, 10));
2281 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2282
2283 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2284 let prev = coherence.get(&id);
2285 let rounded = round_layout_stable(&targets, total, prev);
2286
2287 if let Some(old) = coherence.get(&id) {
2288 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2289 assert!(
2290 max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
2291 "max_disp={} too large for size change {} -> {}",
2292 max_disp,
2293 old.iter().copied().sum::<u16>(),
2294 total
2295 );
2296 let _ = sum_disp;
2297 }
2298 coherence.store(id, rounded);
2299 }
2300 }
2301
2302 #[test]
2303 fn property_temporal_stability_random_walk() {
2304 let constraints = [
2305 Constraint::Ratio(1, 3),
2306 Constraint::Ratio(1, 3),
2307 Constraint::Ratio(1, 3),
2308 ];
2309 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2310 let mut coherence = CoherenceCache::new(64);
2311 let mut rng = Lcg::new(0x5555_AAAA);
2312 let mut total: u16 = 90;
2313
2314 for step in 0..200 {
2315 let prev_total = total;
2316 let delta = (rng.next_u32() % 7) as i32 - 3;
2317 total = (total as i32 + delta).clamp(10, 250) as u16;
2318
2319 let flex = Flex::horizontal().constraints(constraints);
2320 let rects = flex.split(Rect::new(0, 0, total, 10));
2321 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2322
2323 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2324 let prev = coherence.get(&id);
2325 let rounded = round_layout_stable(&targets, total, prev);
2326
2327 if coherence.get(&id).is_some() {
2328 let (_, max_disp) = coherence.displacement(&id, &rounded);
2329 let size_change = total.abs_diff(prev_total);
2330 assert!(
2331 max_disp <= size_change as u32 + 2,
2332 "step {}: max_disp={} exceeds size_change={} + 2",
2333 step,
2334 max_disp,
2335 size_change
2336 );
2337 }
2338 coherence.store(id, rounded);
2339 }
2340 }
2341
2342 #[test]
2343 fn property_temporal_stability_identical_frames() {
2344 let constraints = [
2345 Constraint::Fixed(20),
2346 Constraint::Fill,
2347 Constraint::Fixed(15),
2348 ];
2349 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2350 let mut coherence = CoherenceCache::new(64);
2351
2352 let flex = Flex::horizontal().constraints(constraints);
2353 let rects = flex.split(Rect::new(0, 0, 100, 10));
2354 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2355 coherence.store(id, widths.clone());
2356
2357 for _ in 0..10 {
2358 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2359 let prev = coherence.get(&id);
2360 let rounded = round_layout_stable(&targets, 100, prev);
2361 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2362 assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
2363 assert_eq!(max_disp, 0);
2364 coherence.store(id, rounded);
2365 }
2366 }
2367
2368 #[test]
2369 fn property_temporal_coherence_sweep() {
2370 let constraints = [
2371 Constraint::Percentage(25.0),
2372 Constraint::Percentage(50.0),
2373 Constraint::Fill,
2374 ];
2375 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2376 let mut coherence = CoherenceCache::new(64);
2377 let mut total_displacement: u64 = 0;
2378
2379 for total in 60u16..=140 {
2380 let flex = Flex::horizontal().constraints(constraints);
2381 let rects = flex.split(Rect::new(0, 0, total, 10));
2382 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2383
2384 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2385 let prev = coherence.get(&id);
2386 let rounded = round_layout_stable(&targets, total, prev);
2387
2388 if coherence.get(&id).is_some() {
2389 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2390 total_displacement += sum_disp;
2391 }
2392 coherence.store(id, rounded);
2393 }
2394
2395 assert!(
2396 total_displacement <= 80 * 3,
2397 "Total displacement {} exceeds bound for 80-step sweep",
2398 total_displacement
2399 );
2400 }
2401 }
2402
2403 mod snapshot_layout_tests {
2408 use super::super::*;
2409 use crate::grid::{Grid, GridArea};
2410
2411 fn snapshot_flex(
2412 constraints: &[Constraint],
2413 dir: Direction,
2414 width: u16,
2415 height: u16,
2416 ) -> String {
2417 let flex = Flex::default()
2418 .direction(dir)
2419 .constraints(constraints.iter().copied());
2420 let rects = flex.split(Rect::new(0, 0, width, height));
2421 let mut out = format!(
2422 "Flex {:?} {}x{} ({} constraints)\n",
2423 dir,
2424 width,
2425 height,
2426 constraints.len()
2427 );
2428 for (i, r) in rects.iter().enumerate() {
2429 out.push_str(&format!(
2430 " [{}] x={} y={} w={} h={}\n",
2431 i, r.x, r.y, r.width, r.height
2432 ));
2433 }
2434 let total: u16 = rects
2435 .iter()
2436 .map(|r| match dir {
2437 Direction::Horizontal => r.width,
2438 Direction::Vertical => r.height,
2439 })
2440 .sum();
2441 out.push_str(&format!(" total={}\n", total));
2442 out
2443 }
2444
2445 fn snapshot_grid(
2446 rows: &[Constraint],
2447 cols: &[Constraint],
2448 areas: &[(&str, GridArea)],
2449 width: u16,
2450 height: u16,
2451 ) -> String {
2452 let mut grid = Grid::new()
2453 .rows(rows.iter().copied())
2454 .columns(cols.iter().copied());
2455 for &(name, area) in areas {
2456 grid = grid.area(name, area);
2457 }
2458 let layout = grid.split(Rect::new(0, 0, width, height));
2459
2460 let mut out = format!(
2461 "Grid {}x{} ({}r x {}c)\n",
2462 width,
2463 height,
2464 rows.len(),
2465 cols.len()
2466 );
2467 for r in 0..rows.len() {
2468 for c in 0..cols.len() {
2469 let rect = layout.cell(r, c);
2470 out.push_str(&format!(
2471 " [{},{}] x={} y={} w={} h={}\n",
2472 r, c, rect.x, rect.y, rect.width, rect.height
2473 ));
2474 }
2475 }
2476 for &(name, _) in areas {
2477 if let Some(rect) = layout.area(name) {
2478 out.push_str(&format!(
2479 " area({}) x={} y={} w={} h={}\n",
2480 name, rect.x, rect.y, rect.width, rect.height
2481 ));
2482 }
2483 }
2484 out
2485 }
2486
2487 #[test]
2490 fn snapshot_flex_thirds_80x24() {
2491 let snap = snapshot_flex(
2492 &[
2493 Constraint::Ratio(1, 3),
2494 Constraint::Ratio(1, 3),
2495 Constraint::Ratio(1, 3),
2496 ],
2497 Direction::Horizontal,
2498 80,
2499 24,
2500 );
2501 assert_eq!(
2502 snap,
2503 "\
2504Flex Horizontal 80x24 (3 constraints)
2505 [0] x=0 y=0 w=26 h=24
2506 [1] x=26 y=0 w=26 h=24
2507 [2] x=52 y=0 w=28 h=24
2508 total=80
2509"
2510 );
2511 }
2512
2513 #[test]
2514 fn snapshot_flex_sidebar_content_80x24() {
2515 let snap = snapshot_flex(
2516 &[Constraint::Fixed(20), Constraint::Fill],
2517 Direction::Horizontal,
2518 80,
2519 24,
2520 );
2521 assert_eq!(
2522 snap,
2523 "\
2524Flex Horizontal 80x24 (2 constraints)
2525 [0] x=0 y=0 w=20 h=24
2526 [1] x=20 y=0 w=60 h=24
2527 total=80
2528"
2529 );
2530 }
2531
2532 #[test]
2533 fn snapshot_flex_header_body_footer_80x24() {
2534 let snap = snapshot_flex(
2535 &[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
2536 Direction::Vertical,
2537 80,
2538 24,
2539 );
2540 assert_eq!(
2541 snap,
2542 "\
2543Flex Vertical 80x24 (3 constraints)
2544 [0] x=0 y=0 w=80 h=3
2545 [1] x=0 y=3 w=80 h=20
2546 [2] x=0 y=23 w=80 h=1
2547 total=24
2548"
2549 );
2550 }
2551
2552 #[test]
2555 fn snapshot_flex_thirds_120x40() {
2556 let snap = snapshot_flex(
2557 &[
2558 Constraint::Ratio(1, 3),
2559 Constraint::Ratio(1, 3),
2560 Constraint::Ratio(1, 3),
2561 ],
2562 Direction::Horizontal,
2563 120,
2564 40,
2565 );
2566 assert_eq!(
2567 snap,
2568 "\
2569Flex Horizontal 120x40 (3 constraints)
2570 [0] x=0 y=0 w=40 h=40
2571 [1] x=40 y=0 w=40 h=40
2572 [2] x=80 y=0 w=40 h=40
2573 total=120
2574"
2575 );
2576 }
2577
2578 #[test]
2579 fn snapshot_flex_sidebar_content_120x40() {
2580 let snap = snapshot_flex(
2581 &[Constraint::Fixed(20), Constraint::Fill],
2582 Direction::Horizontal,
2583 120,
2584 40,
2585 );
2586 assert_eq!(
2587 snap,
2588 "\
2589Flex Horizontal 120x40 (2 constraints)
2590 [0] x=0 y=0 w=20 h=40
2591 [1] x=20 y=0 w=100 h=40
2592 total=120
2593"
2594 );
2595 }
2596
2597 #[test]
2598 fn snapshot_flex_percentage_mix_120x40() {
2599 let snap = snapshot_flex(
2600 &[
2601 Constraint::Percentage(25.0),
2602 Constraint::Percentage(50.0),
2603 Constraint::Fill,
2604 ],
2605 Direction::Horizontal,
2606 120,
2607 40,
2608 );
2609 assert_eq!(
2610 snap,
2611 "\
2612Flex Horizontal 120x40 (3 constraints)
2613 [0] x=0 y=0 w=30 h=40
2614 [1] x=30 y=0 w=60 h=40
2615 [2] x=90 y=0 w=30 h=40
2616 total=120
2617"
2618 );
2619 }
2620
2621 #[test]
2624 fn snapshot_grid_2x2_80x24() {
2625 let snap = snapshot_grid(
2626 &[Constraint::Fixed(3), Constraint::Fill],
2627 &[Constraint::Fixed(20), Constraint::Fill],
2628 &[
2629 ("header", GridArea::span(0, 0, 1, 2)),
2630 ("sidebar", GridArea::span(1, 0, 1, 1)),
2631 ("content", GridArea::cell(1, 1)),
2632 ],
2633 80,
2634 24,
2635 );
2636 assert_eq!(
2637 snap,
2638 "\
2639Grid 80x24 (2r x 2c)
2640 [0,0] x=0 y=0 w=20 h=3
2641 [0,1] x=20 y=0 w=60 h=3
2642 [1,0] x=0 y=3 w=20 h=21
2643 [1,1] x=20 y=3 w=60 h=21
2644 area(header) x=0 y=0 w=80 h=3
2645 area(sidebar) x=0 y=3 w=20 h=21
2646 area(content) x=20 y=3 w=60 h=21
2647"
2648 );
2649 }
2650
2651 #[test]
2652 fn snapshot_grid_3x3_80x24() {
2653 let snap = snapshot_grid(
2654 &[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
2655 &[
2656 Constraint::Fixed(10),
2657 Constraint::Fill,
2658 Constraint::Fixed(10),
2659 ],
2660 &[],
2661 80,
2662 24,
2663 );
2664 assert_eq!(
2665 snap,
2666 "\
2667Grid 80x24 (3r x 3c)
2668 [0,0] x=0 y=0 w=10 h=1
2669 [0,1] x=10 y=0 w=60 h=1
2670 [0,2] x=70 y=0 w=10 h=1
2671 [1,0] x=0 y=1 w=10 h=22
2672 [1,1] x=10 y=1 w=60 h=22
2673 [1,2] x=70 y=1 w=10 h=22
2674 [2,0] x=0 y=23 w=10 h=1
2675 [2,1] x=10 y=23 w=60 h=1
2676 [2,2] x=70 y=23 w=10 h=1
2677"
2678 );
2679 }
2680
2681 #[test]
2684 fn snapshot_grid_2x2_120x40() {
2685 let snap = snapshot_grid(
2686 &[Constraint::Fixed(3), Constraint::Fill],
2687 &[Constraint::Fixed(20), Constraint::Fill],
2688 &[
2689 ("header", GridArea::span(0, 0, 1, 2)),
2690 ("sidebar", GridArea::span(1, 0, 1, 1)),
2691 ("content", GridArea::cell(1, 1)),
2692 ],
2693 120,
2694 40,
2695 );
2696 assert_eq!(
2697 snap,
2698 "\
2699Grid 120x40 (2r x 2c)
2700 [0,0] x=0 y=0 w=20 h=3
2701 [0,1] x=20 y=0 w=100 h=3
2702 [1,0] x=0 y=3 w=20 h=37
2703 [1,1] x=20 y=3 w=100 h=37
2704 area(header) x=0 y=0 w=120 h=3
2705 area(sidebar) x=0 y=3 w=20 h=37
2706 area(content) x=20 y=3 w=100 h=37
2707"
2708 );
2709 }
2710
2711 #[test]
2712 fn snapshot_grid_dashboard_120x40() {
2713 let snap = snapshot_grid(
2714 &[
2715 Constraint::Fixed(3),
2716 Constraint::Percentage(60.0),
2717 Constraint::Fill,
2718 ],
2719 &[Constraint::Percentage(30.0), Constraint::Fill],
2720 &[
2721 ("nav", GridArea::span(0, 0, 1, 2)),
2722 ("chart", GridArea::cell(1, 0)),
2723 ("detail", GridArea::cell(1, 1)),
2724 ("log", GridArea::span(2, 0, 1, 2)),
2725 ],
2726 120,
2727 40,
2728 );
2729 assert_eq!(
2730 snap,
2731 "\
2732Grid 120x40 (3r x 2c)
2733 [0,0] x=0 y=0 w=36 h=3
2734 [0,1] x=36 y=0 w=84 h=3
2735 [1,0] x=0 y=3 w=36 h=24
2736 [1,1] x=36 y=3 w=84 h=24
2737 [2,0] x=0 y=27 w=36 h=13
2738 [2,1] x=36 y=27 w=84 h=13
2739 area(nav) x=0 y=0 w=120 h=3
2740 area(chart) x=0 y=3 w=36 h=24
2741 area(detail) x=36 y=3 w=84 h=24
2742 area(log) x=0 y=27 w=120 h=13
2743"
2744 );
2745 }
2746 }
2747}