1#![forbid(unsafe_code)]
2
3pub mod cache;
43pub mod debug;
44pub mod dep_graph;
45pub mod direction;
46pub mod grid;
47pub mod incremental;
48pub mod pane;
49#[cfg(test)]
50mod repro_max_constraint;
51#[cfg(test)]
52mod repro_space_around;
53pub mod responsive;
54pub mod responsive_layout;
55pub mod visibility;
56pub mod workspace;
57
58pub use cache::{
59 CoherenceCache, CoherenceId, LayoutCache, LayoutCacheKey, LayoutCacheStats, S3FifoLayoutCache,
60};
61pub use direction::{FlowDirection, LogicalAlignment, LogicalSides, mirror_rects_horizontal};
62pub use ftui_core::geometry::{Rect, Sides, Size};
63pub use grid::{Grid, GridArea, GridLayout};
64pub use pane::{
65 PANE_DEFAULT_MARGIN_CELLS, PANE_DEFAULT_PADDING_CELLS, PANE_DRAG_RESIZE_DEFAULT_HYSTERESIS,
66 PANE_DRAG_RESIZE_DEFAULT_THRESHOLD, PANE_EDGE_GRIP_INSET_CELLS, PANE_MAGNETIC_FIELD_CELLS,
67 PANE_SEMANTIC_INPUT_EVENT_SCHEMA_VERSION, PANE_SEMANTIC_INPUT_TRACE_SCHEMA_VERSION,
68 PANE_SNAP_DEFAULT_HYSTERESIS_BPS, PANE_SNAP_DEFAULT_STEP_BPS, PANE_TREE_SCHEMA_VERSION,
69 PaneCancelReason, PaneConstraints, PaneCoordinateNormalizationError, PaneCoordinateNormalizer,
70 PaneCoordinateRoundingPolicy, PaneDockPreview, PaneDockZone, PaneDragBehaviorTuning,
71 PaneDragResizeEffect, PaneDragResizeMachine, PaneDragResizeMachineError,
72 PaneDragResizeNoopReason, PaneDragResizeState, PaneDragResizeTransition, PaneEdgeResizePlan,
73 PaneEdgeResizePlanError, PaneGroupTransformPlan, PaneId, PaneIdAllocator, PaneInertialThrow,
74 PaneInputCoordinate, PaneInteractionPolicyError, PaneInteractionTimeline,
75 PaneInteractionTimelineEntry, PaneInteractionTimelineError, PaneInvariantCode,
76 PaneInvariantIssue, PaneInvariantReport, PaneInvariantSeverity, PaneLayout,
77 PaneLayoutIntelligenceMode, PaneLeaf, PaneModelError, PaneModifierSnapshot, PaneMotionVector,
78 PaneNodeKind, PaneNodeRecord, PaneNormalizedCoordinate, PaneOperation, PaneOperationError,
79 PaneOperationFailure, PaneOperationJournalEntry, PaneOperationJournalResult, PaneOperationKind,
80 PaneOperationOutcome, PanePlacement, PanePointerButton, PanePointerPosition, PanePrecisionMode,
81 PanePrecisionPolicy, PanePressureSnapProfile, PaneReflowMovePlan, PaneReflowPlanError,
82 PaneRepairAction, PaneRepairError, PaneRepairFailure, PaneRepairOutcome, PaneResizeDirection,
83 PaneResizeGrip, PaneResizeTarget, PaneScaleFactor, PaneSelectionState, PaneSemanticInputEvent,
84 PaneSemanticInputEventError, PaneSemanticInputEventKind, PaneSemanticInputTrace,
85 PaneSemanticInputTraceError, PaneSemanticInputTraceMetadata,
86 PaneSemanticReplayConformanceArtifact, PaneSemanticReplayDiffArtifact,
87 PaneSemanticReplayDiffKind, PaneSemanticReplayError, PaneSemanticReplayFixture,
88 PaneSemanticReplayOutcome, PaneSnapDecision, PaneSnapReason, PaneSnapTuning, PaneSplit,
89 PaneSplitRatio, PaneTransaction, PaneTransactionOutcome, PaneTree, PaneTreeSnapshot, SplitAxis,
90};
91pub use responsive::Responsive;
92pub use responsive_layout::{ResponsiveLayout, ResponsiveSplit};
93use std::cmp::min;
94pub use visibility::Visibility;
95pub use workspace::{
96 MigrationResult, WORKSPACE_SCHEMA_VERSION, WorkspaceMetadata, WorkspaceMigrationError,
97 WorkspaceSnapshot, WorkspaceValidationError, migrate_workspace, needs_migration,
98};
99
100#[derive(Debug, Clone, Copy, PartialEq)]
102pub enum Constraint {
103 Fixed(u16),
105 Percentage(f32),
107 Min(u16),
109 Max(u16),
111 Ratio(u32, u32),
113 Fill,
115 FitContent,
120 FitContentBounded {
125 min: u16,
127 max: u16,
129 },
130 FitMin,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
159pub struct LayoutSizeHint {
160 pub min: u16,
162 pub preferred: u16,
164 pub max: Option<u16>,
166}
167
168impl LayoutSizeHint {
169 pub const ZERO: Self = Self {
171 min: 0,
172 preferred: 0,
173 max: None,
174 };
175
176 #[inline]
178 pub const fn exact(size: u16) -> Self {
179 Self {
180 min: size,
181 preferred: size,
182 max: Some(size),
183 }
184 }
185
186 #[inline]
188 pub const fn at_least(min: u16, preferred: u16) -> Self {
189 Self {
190 min,
191 preferred,
192 max: None,
193 }
194 }
195
196 #[inline]
198 pub fn clamp(&self, value: u16) -> u16 {
199 let max = self.max.unwrap_or(u16::MAX);
200 value.max(self.min).min(max)
201 }
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
206pub enum Direction {
207 #[default]
209 Vertical,
210 Horizontal,
212}
213
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
216pub enum Alignment {
217 #[default]
219 Start,
220 Center,
222 End,
224 SpaceAround,
226 SpaceBetween,
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
243pub enum Breakpoint {
244 Xs,
246 Sm,
248 Md,
250 Lg,
252 Xl,
254}
255
256impl Breakpoint {
257 pub const ALL: [Breakpoint; 5] = [
259 Breakpoint::Xs,
260 Breakpoint::Sm,
261 Breakpoint::Md,
262 Breakpoint::Lg,
263 Breakpoint::Xl,
264 ];
265
266 #[inline]
268 const fn index(self) -> u8 {
269 match self {
270 Breakpoint::Xs => 0,
271 Breakpoint::Sm => 1,
272 Breakpoint::Md => 2,
273 Breakpoint::Lg => 3,
274 Breakpoint::Xl => 4,
275 }
276 }
277
278 #[must_use]
280 pub const fn label(self) -> &'static str {
281 match self {
282 Breakpoint::Xs => "xs",
283 Breakpoint::Sm => "sm",
284 Breakpoint::Md => "md",
285 Breakpoint::Lg => "lg",
286 Breakpoint::Xl => "xl",
287 }
288 }
289}
290
291impl std::fmt::Display for Breakpoint {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 f.write_str(self.label())
294 }
295}
296
297#[derive(Debug, Clone, Copy, PartialEq, Eq)]
302pub struct Breakpoints {
303 pub sm: u16,
305 pub md: u16,
307 pub lg: u16,
309 pub xl: u16,
311}
312
313impl Breakpoints {
314 pub const DEFAULT: Self = Self {
316 sm: 60,
317 md: 90,
318 lg: 120,
319 xl: 160,
320 };
321
322 pub const fn new(sm: u16, md: u16, lg: u16) -> Self {
326 let md = if md < sm { sm } else { md };
327 let lg = if lg < md { md } else { lg };
328 let xl = if lg + 40 > lg { lg + 40 } else { u16::MAX };
330 Self { sm, md, lg, xl }
331 }
332
333 pub const fn new_with_xl(sm: u16, md: u16, lg: u16, xl: u16) -> Self {
337 let md = if md < sm { sm } else { md };
338 let lg = if lg < md { md } else { lg };
339 let xl = if xl < lg { lg } else { xl };
340 Self { sm, md, lg, xl }
341 }
342
343 #[inline]
345 pub const fn classify_width(self, width: u16) -> Breakpoint {
346 if width >= self.xl {
347 Breakpoint::Xl
348 } else if width >= self.lg {
349 Breakpoint::Lg
350 } else if width >= self.md {
351 Breakpoint::Md
352 } else if width >= self.sm {
353 Breakpoint::Sm
354 } else {
355 Breakpoint::Xs
356 }
357 }
358
359 #[inline]
361 pub const fn classify_size(self, size: Size) -> Breakpoint {
362 self.classify_width(size.width)
363 }
364
365 #[inline]
367 pub const fn at_least(self, width: u16, min: Breakpoint) -> bool {
368 self.classify_width(width).index() >= min.index()
369 }
370
371 #[inline]
373 pub const fn between(self, width: u16, min: Breakpoint, max: Breakpoint) -> bool {
374 let idx = self.classify_width(width).index();
375 idx >= min.index() && idx <= max.index()
376 }
377
378 #[must_use]
380 pub const fn threshold(self, bp: Breakpoint) -> u16 {
381 match bp {
382 Breakpoint::Xs => 0,
383 Breakpoint::Sm => self.sm,
384 Breakpoint::Md => self.md,
385 Breakpoint::Lg => self.lg,
386 Breakpoint::Xl => self.xl,
387 }
388 }
389
390 #[must_use]
392 pub const fn thresholds(self) -> [(Breakpoint, u16); 5] {
393 [
394 (Breakpoint::Xs, 0),
395 (Breakpoint::Sm, self.sm),
396 (Breakpoint::Md, self.md),
397 (Breakpoint::Lg, self.lg),
398 (Breakpoint::Xl, self.xl),
399 ]
400 }
401}
402
403#[derive(Debug, Clone, Copy, Default)]
405pub struct Measurement {
406 pub min_width: u16,
408 pub min_height: u16,
410 pub max_width: Option<u16>,
412 pub max_height: Option<u16>,
414}
415
416impl Measurement {
417 #[must_use]
419 pub fn fixed(width: u16, height: u16) -> Self {
420 Self {
421 min_width: width,
422 min_height: height,
423 max_width: Some(width),
424 max_height: Some(height),
425 }
426 }
427
428 #[must_use]
430 pub fn flexible(min_width: u16, min_height: u16) -> Self {
431 Self {
432 min_width,
433 min_height,
434 max_width: None,
435 max_height: None,
436 }
437 }
438}
439
440#[derive(Debug, Clone, Default)]
442pub struct Flex {
443 direction: Direction,
444 constraints: Vec<Constraint>,
445 margin: Sides,
446 gap: u16,
447 alignment: Alignment,
448 flow_direction: direction::FlowDirection,
449}
450
451impl Flex {
452 #[must_use]
454 pub fn vertical() -> Self {
455 Self {
456 direction: Direction::Vertical,
457 ..Default::default()
458 }
459 }
460
461 #[must_use]
463 pub fn horizontal() -> Self {
464 Self {
465 direction: Direction::Horizontal,
466 ..Default::default()
467 }
468 }
469
470 #[must_use]
472 pub fn direction(mut self, direction: Direction) -> Self {
473 self.direction = direction;
474 self
475 }
476
477 #[must_use]
479 pub fn constraints(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
480 self.constraints = constraints.into_iter().collect();
481 self
482 }
483
484 #[must_use]
486 pub fn margin(mut self, margin: Sides) -> Self {
487 self.margin = margin;
488 self
489 }
490
491 #[must_use]
493 pub fn gap(mut self, gap: u16) -> Self {
494 self.gap = gap;
495 self
496 }
497
498 #[must_use]
500 pub fn alignment(mut self, alignment: Alignment) -> Self {
501 self.alignment = alignment;
502 self
503 }
504
505 #[must_use]
511 pub fn flow_direction(mut self, flow: direction::FlowDirection) -> Self {
512 self.flow_direction = flow;
513 self
514 }
515
516 #[must_use]
518 pub fn constraint_count(&self) -> usize {
519 self.constraints.len()
520 }
521
522 pub fn split(&self, area: Rect) -> Vec<Rect> {
524 let inner = area.inner(self.margin);
526 if inner.is_empty() {
527 return self.constraints.iter().map(|_| Rect::default()).collect();
528 }
529
530 let total_size = match self.direction {
531 Direction::Horizontal => inner.width,
532 Direction::Vertical => inner.height,
533 };
534
535 let count = self.constraints.len();
536 if count == 0 {
537 return Vec::new();
538 }
539
540 let gap_count = count - 1;
542 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
543 let available_size = total_size.saturating_sub(total_gap);
544
545 let sizes = solve_constraints(&self.constraints, available_size);
547
548 let mut rects = self.sizes_to_rects(inner, &sizes);
550
551 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
553 direction::mirror_rects_horizontal(&mut rects, inner);
554 }
555
556 rects
557 }
558
559 fn sizes_to_rects(&self, area: Rect, sizes: &[u16]) -> Vec<Rect> {
560 let mut rects = Vec::with_capacity(sizes.len());
561 if sizes.is_empty() {
562 return rects;
563 }
564
565 let total_items_size: u16 = sizes.iter().sum();
566 let total_available = match self.direction {
567 Direction::Horizontal => area.width,
568 Direction::Vertical => area.height,
569 };
570
571 let (start_shift, use_formula) = match self.alignment {
573 Alignment::Start => (0, None),
574 Alignment::End => {
575 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
576 .min(u16::MAX as u64) as u16;
577 let used = total_items_size.saturating_add(gap_space);
578 (total_available.saturating_sub(used), None)
579 }
580 Alignment::Center => {
581 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
582 .min(u16::MAX as u64) as u16;
583 let used = total_items_size.saturating_add(gap_space);
584 (total_available.saturating_sub(used) / 2, None)
585 }
586 Alignment::SpaceBetween => {
587 let leftover = total_available.saturating_sub(total_items_size);
588 let slots = sizes.len().saturating_sub(1);
589 if slots > 0 {
590 (0, Some((leftover, slots, 0))) } else {
592 (0, None)
593 }
594 }
595 Alignment::SpaceAround => {
596 let leftover = total_available.saturating_sub(total_items_size);
597 let slots = sizes.len() * 2;
598 if slots > 0 {
599 (0, Some((leftover, slots, 1))) } else {
601 (0, None)
602 }
603 }
604 };
605
606 let mut accumulated_size = 0;
607
608 for (i, &size) in sizes.iter().enumerate() {
609 let gap_offset = if let Some((leftover, slots, mode)) = use_formula {
610 if mode == 0 {
611 if i == 0 {
613 0
614 } else {
615 (leftover as u64 * i as u64 / slots as u64) as u16
616 }
617 } else {
618 let numerator = leftover as u64 * (2 * i as u64 + 1);
622 let denominator = slots as u64;
623 let midpoint = sizes.len() / 2;
624 let raw = if sizes.len() % 2 == 1 && i == midpoint {
625 (numerator + (denominator / 2)) / denominator
626 } else if i < midpoint {
627 numerator / denominator
628 } else {
629 numerator.div_ceil(denominator)
630 };
631 raw.min(u64::from(u16::MAX)) as u16
632 }
633 } else {
634 if i > 0 {
636 (i as u64 * self.gap as u64).min(u16::MAX as u64) as u16
637 } else {
638 0
639 }
640 };
641
642 let pos = match self.direction {
643 Direction::Horizontal => area
644 .x
645 .saturating_add(start_shift)
646 .saturating_add(accumulated_size)
647 .saturating_add(gap_offset),
648 Direction::Vertical => area
649 .y
650 .saturating_add(start_shift)
651 .saturating_add(accumulated_size)
652 .saturating_add(gap_offset),
653 };
654
655 let rect = match self.direction {
656 Direction::Horizontal => Rect {
657 x: pos,
658 y: area.y,
659 width: size,
660 height: area.height,
661 },
662 Direction::Vertical => Rect {
663 x: area.x,
664 y: pos,
665 width: area.width,
666 height: size,
667 },
668 };
669 rects.push(rect);
670 accumulated_size = accumulated_size.saturating_add(size);
671 }
672
673 rects
674 }
675
676 pub fn split_with_measurer<F>(&self, area: Rect, measurer: F) -> Vec<Rect>
700 where
701 F: Fn(usize, u16) -> LayoutSizeHint,
702 {
703 let inner = area.inner(self.margin);
705 if inner.is_empty() {
706 return self.constraints.iter().map(|_| Rect::default()).collect();
707 }
708
709 let total_size = match self.direction {
710 Direction::Horizontal => inner.width,
711 Direction::Vertical => inner.height,
712 };
713
714 let count = self.constraints.len();
715 if count == 0 {
716 return Vec::new();
717 }
718
719 let gap_count = count - 1;
721 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
722 let available_size = total_size.saturating_sub(total_gap);
723
724 let sizes = solve_constraints_with_hints(&self.constraints, available_size, &measurer);
726
727 let mut rects = self.sizes_to_rects(inner, &sizes);
729
730 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
732 direction::mirror_rects_horizontal(&mut rects, inner);
733 }
734
735 rects
736 }
737}
738
739pub(crate) fn solve_constraints(constraints: &[Constraint], available_size: u16) -> Vec<u16> {
744 solve_constraints_with_hints(constraints, available_size, &|_, _| LayoutSizeHint::ZERO)
746}
747
748pub(crate) fn solve_constraints_with_hints<F>(
753 constraints: &[Constraint],
754 available_size: u16,
755 measurer: &F,
756) -> Vec<u16>
757where
758 F: Fn(usize, u16) -> LayoutSizeHint,
759{
760 const WEIGHT_SCALE: u64 = 10_000;
761
762 let mut sizes = vec![0u16; constraints.len()];
763 let mut remaining = available_size;
764 let mut grow_indices = Vec::new();
765
766 let grow_weight = |constraint: Constraint| -> u64 {
767 match constraint {
768 Constraint::Ratio(n, d) => {
769 if n == 0 {
770 0
771 } else {
772 let scaled = (u128::from(n) * u128::from(WEIGHT_SCALE)) / u128::from(d.max(1));
773 scaled.max(1).min(u128::from(u64::MAX)) as u64
774 }
775 }
776 Constraint::Min(_) | Constraint::Max(_) | Constraint::Fill | Constraint::FitMin => {
777 WEIGHT_SCALE
778 }
779 _ => 0,
780 }
781 };
782
783 for (i, &constraint) in constraints.iter().enumerate() {
785 match constraint {
786 Constraint::Fixed(size) => {
787 let size = min(size, remaining);
788 sizes[i] = size;
789 remaining = remaining.saturating_sub(size);
790 }
791 Constraint::Percentage(p) => {
792 let size = (available_size as f32 * p / 100.0)
793 .round()
794 .min(u16::MAX as f32) as u16;
795 let size = min(size, remaining);
796 sizes[i] = size;
797 remaining = remaining.saturating_sub(size);
798 }
799 Constraint::Ratio(_, _) => {
800 grow_indices.push(i);
802 }
803 Constraint::Min(min_size) => {
804 let size = min(min_size, remaining);
805 sizes[i] = size;
806 remaining = remaining.saturating_sub(size);
807 grow_indices.push(i);
808 }
809 Constraint::Max(_) => {
810 grow_indices.push(i);
812 }
813 Constraint::Fill => {
814 grow_indices.push(i);
816 }
817 Constraint::FitContent => {
818 let hint = measurer(i, remaining);
820 let size = min(hint.preferred, remaining);
821 sizes[i] = size;
822 remaining = remaining.saturating_sub(size);
823 }
825 Constraint::FitContentBounded {
826 min: min_bound,
827 max: max_bound,
828 } => {
829 let hint = measurer(i, remaining);
831 let preferred = hint.preferred.max(min_bound).min(max_bound);
832 let size = min(preferred, remaining);
833 sizes[i] = size;
834 remaining = remaining.saturating_sub(size);
835 }
836 Constraint::FitMin => {
837 let hint = measurer(i, remaining);
839 let size = min(hint.min, remaining);
840 sizes[i] = size;
841 remaining = remaining.saturating_sub(size);
842 grow_indices.push(i);
844 }
845 }
846 }
847
848 loop {
850 if remaining == 0 || grow_indices.is_empty() {
851 break;
852 }
853
854 let mut total_weight = 0u128;
855 let mut last_weighted_pos = None;
856 for (grow_pos, &i) in grow_indices.iter().enumerate() {
857 let weight = grow_weight(constraints[i]);
858 if weight > 0 {
859 total_weight = total_weight.saturating_add(u128::from(weight));
860 last_weighted_pos = Some(grow_pos);
861 }
862 }
863
864 if total_weight == 0 {
865 break;
866 }
867
868 let space_to_distribute = remaining;
869 let mut allocated = 0u16;
870 let mut shares = vec![0u16; constraints.len()];
871 let last_weighted_pos = last_weighted_pos.unwrap_or_default();
872
873 for (grow_pos, &i) in grow_indices.iter().enumerate() {
874 let weight = grow_weight(constraints[i]);
875 if weight == 0 {
876 continue;
877 }
878
879 let size = if grow_pos == last_weighted_pos {
881 space_to_distribute.saturating_sub(allocated)
882 } else {
883 let scaled = (u128::from(space_to_distribute) * u128::from(weight)) / total_weight;
884 let s = u16::try_from(scaled).unwrap_or(u16::MAX);
885 min(s, space_to_distribute.saturating_sub(allocated))
886 };
887
888 shares[i] = size;
889 allocated = allocated.saturating_add(size);
890 }
891
892 let mut violations = Vec::new();
894 for &i in &grow_indices {
895 if let Constraint::Max(max_val) = constraints[i]
896 && sizes[i].saturating_add(shares[i]) > max_val
897 {
898 violations.push(i);
899 }
900 }
901
902 if violations.is_empty() {
903 for &i in &grow_indices {
905 sizes[i] = sizes[i].saturating_add(shares[i]);
906 }
907 break;
908 }
909
910 for i in violations {
912 if let Constraint::Max(max_val) = constraints[i] {
913 let consumed = max_val.saturating_sub(sizes[i]);
916 sizes[i] = max_val;
917 remaining = remaining.saturating_sub(consumed);
918
919 if let Some(pos) = grow_indices.iter().position(|&x| x == i) {
921 grow_indices.remove(pos);
922 }
923 }
924 }
925 }
926
927 sizes
928}
929
930pub type PreviousAllocation = Option<Vec<u16>>;
940
941pub fn round_layout_stable(targets: &[f64], total: u16, prev: PreviousAllocation) -> Vec<u16> {
1003 let n = targets.len();
1004 if n == 0 {
1005 return Vec::new();
1006 }
1007
1008 let floors: Vec<u16> = targets
1010 .iter()
1011 .map(|&r| (r.max(0.0).floor() as u64).min(u16::MAX as u64) as u16)
1012 .collect();
1013
1014 let floor_sum: u16 = floors.iter().copied().sum();
1015
1016 let deficit = total.saturating_sub(floor_sum);
1018
1019 if deficit == 0 {
1020 if floor_sum > total {
1023 return redistribute_overflow(&floors, total);
1024 }
1025 return floors;
1026 }
1027
1028 let mut priority: Vec<(usize, f64, bool)> = targets
1030 .iter()
1031 .enumerate()
1032 .map(|(i, &r)| {
1033 let remainder = r - (floors[i] as f64);
1034 let ceil_val = floors[i].saturating_add(1);
1035 let prev_used_ceil = prev
1037 .as_ref()
1038 .is_some_and(|p| p.get(i).copied() == Some(ceil_val));
1039 (i, remainder, prev_used_ceil)
1040 })
1041 .collect();
1042
1043 priority.sort_by(|a, b| {
1045 b.1.partial_cmp(&a.1)
1046 .unwrap_or(std::cmp::Ordering::Equal)
1047 .then_with(|| {
1048 b.2.cmp(&a.2)
1050 })
1051 .then_with(|| {
1052 a.0.cmp(&b.0)
1054 })
1055 });
1056
1057 let mut result = floors;
1059 let distribute = (deficit as usize).min(n);
1060 for &(i, _, _) in priority.iter().take(distribute) {
1061 result[i] = result[i].saturating_add(1);
1062 }
1063
1064 result
1065}
1066
1067fn redistribute_overflow(floors: &[u16], total: u16) -> Vec<u16> {
1072 let mut result = floors.to_vec();
1073 let mut current_sum: u16 = result.iter().copied().sum();
1074
1075 while current_sum > total {
1077 if let Some((idx, _)) = result
1079 .iter()
1080 .enumerate()
1081 .filter(|item| *item.1 > 0)
1082 .max_by_key(|item| *item.1)
1083 {
1084 result[idx] = result[idx].saturating_sub(1);
1085 current_sum = current_sum.saturating_sub(1);
1086 } else {
1087 break;
1088 }
1089 }
1090
1091 result
1092}
1093
1094#[cfg(test)]
1095mod tests {
1096 use super::*;
1097
1098 #[test]
1099 fn fixed_split() {
1100 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(20)]);
1101 let rects = flex.split(Rect::new(0, 0, 100, 10));
1102 assert_eq!(rects.len(), 2);
1103 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1104 assert_eq!(rects[1], Rect::new(10, 0, 20, 10)); }
1106
1107 #[test]
1108 fn percentage_split() {
1109 let flex = Flex::horizontal()
1110 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1111 let rects = flex.split(Rect::new(0, 0, 100, 10));
1112 assert_eq!(rects[0].width, 50);
1113 assert_eq!(rects[1].width, 50);
1114 }
1115
1116 #[test]
1117 fn gap_handling() {
1118 let flex = Flex::horizontal()
1119 .gap(5)
1120 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1121 let rects = flex.split(Rect::new(0, 0, 100, 10));
1122 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1126 assert_eq!(rects[1], Rect::new(15, 0, 10, 10));
1127 }
1128
1129 #[test]
1130 fn mixed_constraints() {
1131 let flex = Flex::horizontal().constraints([
1132 Constraint::Fixed(10),
1133 Constraint::Min(10), Constraint::Percentage(10.0), ]);
1136
1137 let rects = flex.split(Rect::new(0, 0, 100, 1));
1145 assert_eq!(rects[0].width, 10); assert_eq!(rects[2].width, 10); assert_eq!(rects[1].width, 80); }
1149
1150 #[test]
1151 fn measurement_fixed_constraints() {
1152 let fixed = Measurement::fixed(5, 7);
1153 assert_eq!(fixed.min_width, 5);
1154 assert_eq!(fixed.min_height, 7);
1155 assert_eq!(fixed.max_width, Some(5));
1156 assert_eq!(fixed.max_height, Some(7));
1157 }
1158
1159 #[test]
1160 fn measurement_flexible_constraints() {
1161 let flexible = Measurement::flexible(2, 3);
1162 assert_eq!(flexible.min_width, 2);
1163 assert_eq!(flexible.min_height, 3);
1164 assert_eq!(flexible.max_width, None);
1165 assert_eq!(flexible.max_height, None);
1166 }
1167
1168 #[test]
1169 fn breakpoints_classify_defaults() {
1170 let bp = Breakpoints::DEFAULT;
1171 assert_eq!(bp.classify_width(20), Breakpoint::Xs);
1172 assert_eq!(bp.classify_width(60), Breakpoint::Sm);
1173 assert_eq!(bp.classify_width(90), Breakpoint::Md);
1174 assert_eq!(bp.classify_width(120), Breakpoint::Lg);
1175 }
1176
1177 #[test]
1178 fn breakpoints_at_least_and_between() {
1179 let bp = Breakpoints::new(50, 80, 110);
1180 assert!(bp.at_least(85, Breakpoint::Sm));
1181 assert!(bp.between(85, Breakpoint::Sm, Breakpoint::Md));
1182 assert!(!bp.between(85, Breakpoint::Lg, Breakpoint::Lg));
1183 }
1184
1185 #[test]
1186 fn alignment_end() {
1187 let flex = Flex::horizontal()
1188 .alignment(Alignment::End)
1189 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1190 let rects = flex.split(Rect::new(0, 0, 100, 10));
1191 assert_eq!(rects[0], Rect::new(80, 0, 10, 10));
1193 assert_eq!(rects[1], Rect::new(90, 0, 10, 10));
1194 }
1195
1196 #[test]
1197 fn alignment_center() {
1198 let flex = Flex::horizontal()
1199 .alignment(Alignment::Center)
1200 .constraints([Constraint::Fixed(20), Constraint::Fixed(20)]);
1201 let rects = flex.split(Rect::new(0, 0, 100, 10));
1202 assert_eq!(rects[0], Rect::new(30, 0, 20, 10));
1204 assert_eq!(rects[1], Rect::new(50, 0, 20, 10));
1205 }
1206
1207 #[test]
1208 fn alignment_space_between() {
1209 let flex = Flex::horizontal()
1210 .alignment(Alignment::SpaceBetween)
1211 .constraints([
1212 Constraint::Fixed(10),
1213 Constraint::Fixed(10),
1214 Constraint::Fixed(10),
1215 ]);
1216 let rects = flex.split(Rect::new(0, 0, 100, 10));
1217 assert_eq!(rects[0].x, 0);
1219 assert_eq!(rects[1].x, 45); assert_eq!(rects[2].x, 90); }
1222
1223 #[test]
1224 fn vertical_alignment() {
1225 let flex = Flex::vertical()
1226 .alignment(Alignment::End)
1227 .constraints([Constraint::Fixed(5), Constraint::Fixed(5)]);
1228 let rects = flex.split(Rect::new(0, 0, 10, 100));
1229 assert_eq!(rects[0], Rect::new(0, 90, 10, 5));
1231 assert_eq!(rects[1], Rect::new(0, 95, 10, 5));
1232 }
1233
1234 #[test]
1235 fn nested_flex_support() {
1236 let outer = Flex::horizontal()
1238 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1239 let outer_rects = outer.split(Rect::new(0, 0, 100, 100));
1240
1241 let inner = Flex::vertical().constraints([Constraint::Fixed(30), Constraint::Min(10)]);
1243 let inner_rects = inner.split(outer_rects[0]);
1244
1245 assert_eq!(inner_rects[0], Rect::new(0, 0, 50, 30));
1246 assert_eq!(inner_rects[1], Rect::new(0, 30, 50, 70));
1247 }
1248
1249 #[test]
1251 fn invariant_total_size_does_not_exceed_available() {
1252 for total in [10u16, 50, 100, 255] {
1254 let flex = Flex::horizontal().constraints([
1255 Constraint::Fixed(30),
1256 Constraint::Percentage(50.0),
1257 Constraint::Min(20),
1258 ]);
1259 let rects = flex.split(Rect::new(0, 0, total, 10));
1260 let total_width: u16 = rects.iter().map(|r| r.width).sum();
1261 assert!(
1262 total_width <= total,
1263 "Total width {} exceeded available {} for constraints",
1264 total_width,
1265 total
1266 );
1267 }
1268 }
1269
1270 #[test]
1271 fn invariant_empty_area_produces_empty_rects() {
1272 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1273 let rects = flex.split(Rect::new(0, 0, 0, 0));
1274 assert!(rects.iter().all(|r| r.is_empty()));
1275 }
1276
1277 #[test]
1278 fn invariant_no_constraints_produces_empty_vec() {
1279 let flex = Flex::horizontal().constraints([]);
1280 let rects = flex.split(Rect::new(0, 0, 100, 100));
1281 assert!(rects.is_empty());
1282 }
1283
1284 #[test]
1287 fn ratio_constraint_splits_proportionally() {
1288 let flex =
1289 Flex::horizontal().constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
1290 let rects = flex.split(Rect::new(0, 0, 90, 10));
1291 assert_eq!(rects[0].width, 30);
1292 assert_eq!(rects[1].width, 60);
1293 }
1294
1295 #[test]
1296 fn ratio_constraint_with_zero_denominator() {
1297 let flex = Flex::horizontal().constraints([Constraint::Ratio(1, 0)]);
1299 let rects = flex.split(Rect::new(0, 0, 100, 10));
1300 assert_eq!(rects.len(), 1);
1301 }
1302
1303 #[test]
1304 fn ratio_is_weighted_not_an_absolute_fraction() {
1305 let area = Rect::new(0, 0, 100, 1);
1306
1307 let rects = Flex::horizontal()
1309 .constraints([Constraint::Percentage(25.0)])
1310 .split(area);
1311 assert_eq!(rects[0].width, 25);
1312
1313 let rects = Flex::horizontal()
1315 .constraints([Constraint::Ratio(1, 4)])
1316 .split(area);
1317 assert_eq!(rects[0].width, 100);
1318 }
1319
1320 #[test]
1321 fn ratio_is_weighted_against_other_grow_items() {
1322 let area = Rect::new(0, 0, 100, 1);
1323
1324 let rects = Flex::horizontal()
1327 .constraints([Constraint::Ratio(1, 4), Constraint::Fill])
1328 .split(area);
1329 assert_eq!(rects[0].width, 20);
1330 assert_eq!(rects[1].width, 80);
1331 }
1332
1333 #[test]
1334 fn ratio_zero_numerator_should_be_zero() {
1335 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
1338 let rects = flex.split(Rect::new(0, 0, 100, 1));
1339
1340 assert_eq!(rects[0].width, 100, "Fill should take all space");
1342 assert_eq!(rects[1].width, 0, "Ratio(0, 1) should be width 0");
1343 }
1344
1345 #[test]
1348 fn max_constraint_clamps_size() {
1349 let flex = Flex::horizontal().constraints([Constraint::Max(20), Constraint::Fixed(30)]);
1350 let rects = flex.split(Rect::new(0, 0, 100, 10));
1351 assert!(rects[0].width <= 20);
1352 assert_eq!(rects[1].width, 30);
1353 }
1354
1355 #[test]
1356 fn percentage_rounding_never_exceeds_available() {
1357 let constraints = [
1358 Constraint::Percentage(33.4),
1359 Constraint::Percentage(33.3),
1360 Constraint::Percentage(33.3),
1361 ];
1362 let sizes = solve_constraints(&constraints, 7);
1363 let total: u16 = sizes.iter().sum();
1364 assert!(total <= 7, "percent rounding overflowed: {sizes:?}");
1365 assert!(sizes.iter().all(|size| *size <= 7));
1366 }
1367
1368 #[test]
1369 fn tiny_area_saturates_fixed_and_min() {
1370 let constraints = [Constraint::Fixed(5), Constraint::Min(3), Constraint::Max(2)];
1371 let sizes = solve_constraints(&constraints, 2);
1372 assert_eq!(sizes[0], 2);
1373 assert_eq!(sizes[1], 0);
1374 assert_eq!(sizes[2], 0);
1375 assert_eq!(sizes.iter().sum::<u16>(), 2);
1376 }
1377
1378 #[test]
1379 fn ratio_distribution_sums_to_available() {
1380 let constraints = [Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)];
1381 let sizes = solve_constraints(&constraints, 5);
1382 assert_eq!(sizes.iter().sum::<u16>(), 5);
1383 assert_eq!(sizes[0], 1);
1384 assert_eq!(sizes[1], 4);
1385 }
1386
1387 #[test]
1388 fn flex_gap_exceeds_area_yields_zero_widths() {
1389 let flex = Flex::horizontal()
1390 .gap(5)
1391 .constraints([Constraint::Fixed(1), Constraint::Fixed(1)]);
1392 let rects = flex.split(Rect::new(0, 0, 3, 1));
1393 assert_eq!(rects.len(), 2);
1394 assert_eq!(rects[0].width, 0);
1395 assert_eq!(rects[1].width, 0);
1396 }
1397
1398 #[test]
1401 fn alignment_space_around() {
1402 let flex = Flex::horizontal()
1403 .alignment(Alignment::SpaceAround)
1404 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1405 let rects = flex.split(Rect::new(0, 0, 100, 10));
1406
1407 assert_eq!(rects[0].x, 20);
1410 assert_eq!(rects[1].x, 70);
1411 }
1412
1413 #[test]
1416 fn vertical_gap() {
1417 let flex = Flex::vertical()
1418 .gap(5)
1419 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1420 let rects = flex.split(Rect::new(0, 0, 50, 100));
1421 assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
1422 assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
1423 }
1424
1425 #[test]
1428 fn vertical_center() {
1429 let flex = Flex::vertical()
1430 .alignment(Alignment::Center)
1431 .constraints([Constraint::Fixed(10)]);
1432 let rects = flex.split(Rect::new(0, 0, 50, 100));
1433 assert_eq!(rects[0].y, 45);
1435 assert_eq!(rects[0].height, 10);
1436 }
1437
1438 #[test]
1441 fn single_min_takes_all() {
1442 let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
1443 let rects = flex.split(Rect::new(0, 0, 80, 24));
1444 assert_eq!(rects[0].width, 80);
1445 }
1446
1447 #[test]
1450 fn fixed_exceeds_available_clamped() {
1451 let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
1452 let rects = flex.split(Rect::new(0, 0, 100, 10));
1453 assert_eq!(rects[0].width, 60);
1455 assert_eq!(rects[1].width, 40);
1456 }
1457
1458 #[test]
1461 fn percentage_overflow_clamped() {
1462 let flex = Flex::horizontal()
1463 .constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
1464 let rects = flex.split(Rect::new(0, 0, 100, 10));
1465 assert_eq!(rects[0].width, 80);
1466 assert_eq!(rects[1].width, 20); }
1468
1469 #[test]
1472 fn margin_reduces_split_area() {
1473 let flex = Flex::horizontal()
1474 .margin(Sides::all(10))
1475 .constraints([Constraint::Fixed(20), Constraint::Min(0)]);
1476 let rects = flex.split(Rect::new(0, 0, 100, 100));
1477 assert_eq!(rects[0].x, 10);
1479 assert_eq!(rects[0].y, 10);
1480 assert_eq!(rects[0].width, 20);
1481 assert_eq!(rects[0].height, 80);
1482 }
1483
1484 #[test]
1487 fn builder_methods_chain() {
1488 let flex = Flex::vertical()
1489 .direction(Direction::Horizontal)
1490 .gap(3)
1491 .margin(Sides::all(1))
1492 .alignment(Alignment::End)
1493 .constraints([Constraint::Fixed(10)]);
1494 let rects = flex.split(Rect::new(0, 0, 50, 50));
1495 assert_eq!(rects.len(), 1);
1496 }
1497
1498 #[test]
1501 fn space_between_single_item() {
1502 let flex = Flex::horizontal()
1503 .alignment(Alignment::SpaceBetween)
1504 .constraints([Constraint::Fixed(10)]);
1505 let rects = flex.split(Rect::new(0, 0, 100, 10));
1506 assert_eq!(rects[0].x, 0);
1508 assert_eq!(rects[0].width, 10);
1509 }
1510
1511 #[test]
1512 fn invariant_rects_within_bounds() {
1513 let area = Rect::new(10, 20, 80, 60);
1514 let flex = Flex::horizontal()
1515 .margin(Sides::all(5))
1516 .gap(2)
1517 .constraints([
1518 Constraint::Fixed(15),
1519 Constraint::Percentage(30.0),
1520 Constraint::Min(10),
1521 ]);
1522 let rects = flex.split(area);
1523
1524 let inner = area.inner(Sides::all(5));
1526 for rect in &rects {
1527 assert!(
1528 rect.x >= inner.x && rect.right() <= inner.right(),
1529 "Rect {:?} exceeds horizontal bounds of {:?}",
1530 rect,
1531 inner
1532 );
1533 assert!(
1534 rect.y >= inner.y && rect.bottom() <= inner.bottom(),
1535 "Rect {:?} exceeds vertical bounds of {:?}",
1536 rect,
1537 inner
1538 );
1539 }
1540 }
1541
1542 #[test]
1545 fn fill_takes_remaining_space() {
1546 let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
1547 let rects = flex.split(Rect::new(0, 0, 100, 10));
1548 assert_eq!(rects[0].width, 20);
1549 assert_eq!(rects[1].width, 80); }
1551
1552 #[test]
1553 fn multiple_fills_share_space() {
1554 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
1555 let rects = flex.split(Rect::new(0, 0, 100, 10));
1556 assert_eq!(rects[0].width, 50);
1557 assert_eq!(rects[1].width, 50);
1558 }
1559
1560 #[test]
1563 fn fit_content_uses_preferred_size() {
1564 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1565 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1566 if idx == 0 {
1567 LayoutSizeHint {
1568 min: 5,
1569 preferred: 30,
1570 max: None,
1571 }
1572 } else {
1573 LayoutSizeHint::ZERO
1574 }
1575 });
1576 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
1579
1580 #[test]
1581 fn fit_content_clamps_to_available() {
1582 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
1583 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1584 min: 10,
1585 preferred: 80,
1586 max: None,
1587 });
1588 assert_eq!(rects[0].width, 80);
1590 assert_eq!(rects[1].width, 20);
1591 }
1592
1593 #[test]
1594 fn fit_content_without_measurer_gets_zero() {
1595 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1597 let rects = flex.split(Rect::new(0, 0, 100, 10));
1598 assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
1601
1602 #[test]
1603 fn fit_content_zero_area_returns_empty_rects() {
1604 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1605 let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
1606 min: 5,
1607 preferred: 10,
1608 max: None,
1609 });
1610 assert_eq!(rects.len(), 2);
1611 assert_eq!(rects[0].width, 0);
1612 assert_eq!(rects[0].height, 0);
1613 assert_eq!(rects[1].width, 0);
1614 assert_eq!(rects[1].height, 0);
1615 }
1616
1617 #[test]
1618 fn fit_content_tiny_available_clamps_to_remaining() {
1619 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1620 let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
1621 min: 5,
1622 preferred: 10,
1623 max: None,
1624 });
1625 assert_eq!(rects[0].width, 1);
1626 assert_eq!(rects[1].width, 0);
1627 }
1628
1629 #[test]
1632 fn fit_content_bounded_clamps_to_min() {
1633 let flex = Flex::horizontal().constraints([
1634 Constraint::FitContentBounded { min: 20, max: 50 },
1635 Constraint::Fill,
1636 ]);
1637 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1638 min: 5,
1639 preferred: 10, max: None,
1641 });
1642 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
1644 }
1645
1646 #[test]
1647 fn fit_content_bounded_respects_small_available() {
1648 let flex = Flex::horizontal().constraints([
1649 Constraint::FitContentBounded { min: 20, max: 50 },
1650 Constraint::Fill,
1651 ]);
1652 let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
1653 min: 5,
1654 preferred: 10,
1655 max: None,
1656 });
1657 assert_eq!(rects[0].width, 5);
1659 assert_eq!(rects[1].width, 0);
1660 }
1661
1662 #[test]
1663 fn fit_content_vertical_uses_preferred_height() {
1664 let flex = Flex::vertical().constraints([Constraint::FitContent, Constraint::Fill]);
1665 let rects = flex.split_with_measurer(Rect::new(0, 0, 10, 10), |idx, _| {
1666 if idx == 0 {
1667 LayoutSizeHint {
1668 min: 1,
1669 preferred: 4,
1670 max: None,
1671 }
1672 } else {
1673 LayoutSizeHint::ZERO
1674 }
1675 });
1676 assert_eq!(rects[0].height, 4);
1677 assert_eq!(rects[1].height, 6);
1678 }
1679
1680 #[test]
1681 fn fit_content_bounded_clamps_to_max() {
1682 let flex = Flex::horizontal().constraints([
1683 Constraint::FitContentBounded { min: 10, max: 30 },
1684 Constraint::Fill,
1685 ]);
1686 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1687 min: 5,
1688 preferred: 50, max: None,
1690 });
1691 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
1693 }
1694
1695 #[test]
1696 fn fit_content_bounded_uses_preferred_when_in_range() {
1697 let flex = Flex::horizontal().constraints([
1698 Constraint::FitContentBounded { min: 10, max: 50 },
1699 Constraint::Fill,
1700 ]);
1701 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1702 min: 5,
1703 preferred: 35, max: None,
1705 });
1706 assert_eq!(rects[0].width, 35);
1707 assert_eq!(rects[1].width, 65);
1708 }
1709
1710 #[test]
1713 fn fit_min_uses_minimum_size() {
1714 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1715 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1716 if idx == 0 {
1717 LayoutSizeHint {
1718 min: 15,
1719 preferred: 40,
1720 max: None,
1721 }
1722 } else {
1723 LayoutSizeHint::ZERO
1724 }
1725 });
1726 let total: u16 = rects.iter().map(|r| r.width).sum();
1740 assert_eq!(total, 100);
1741 assert!(rects[0].width >= 15, "FitMin should get at least minimum");
1742 }
1743
1744 #[test]
1745 fn fit_min_without_measurer_gets_zero() {
1746 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1747 let rects = flex.split(Rect::new(0, 0, 100, 10));
1748 assert_eq!(rects[0].width, 50);
1751 assert_eq!(rects[1].width, 50);
1752 }
1753
1754 #[test]
1757 fn layout_size_hint_zero_is_default() {
1758 assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
1759 }
1760
1761 #[test]
1762 fn layout_size_hint_exact() {
1763 let h = LayoutSizeHint::exact(25);
1764 assert_eq!(h.min, 25);
1765 assert_eq!(h.preferred, 25);
1766 assert_eq!(h.max, Some(25));
1767 }
1768
1769 #[test]
1770 fn layout_size_hint_at_least() {
1771 let h = LayoutSizeHint::at_least(10, 30);
1772 assert_eq!(h.min, 10);
1773 assert_eq!(h.preferred, 30);
1774 assert_eq!(h.max, None);
1775 }
1776
1777 #[test]
1778 fn layout_size_hint_clamp() {
1779 let h = LayoutSizeHint {
1780 min: 10,
1781 preferred: 20,
1782 max: Some(30),
1783 };
1784 assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
1788
1789 #[test]
1790 fn layout_size_hint_clamp_unbounded() {
1791 let h = LayoutSizeHint::at_least(5, 10);
1792 assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
1795
1796 #[test]
1799 fn fit_content_with_fixed_and_fill() {
1800 let flex = Flex::horizontal().constraints([
1801 Constraint::Fixed(20),
1802 Constraint::FitContent,
1803 Constraint::Fill,
1804 ]);
1805 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1806 if idx == 1 {
1807 LayoutSizeHint {
1808 min: 5,
1809 preferred: 25,
1810 max: None,
1811 }
1812 } else {
1813 LayoutSizeHint::ZERO
1814 }
1815 });
1816 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
1820
1821 #[test]
1822 fn total_allocation_never_exceeds_available_with_fit_content() {
1823 for available in [10u16, 50, 100, 255] {
1824 let flex = Flex::horizontal().constraints([
1825 Constraint::FitContent,
1826 Constraint::FitContent,
1827 Constraint::Fill,
1828 ]);
1829 let rects =
1830 flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
1831 min: 10,
1832 preferred: 40,
1833 max: None,
1834 });
1835 let total: u16 = rects.iter().map(|r| r.width).sum();
1836 assert!(
1837 total <= available,
1838 "Total {} exceeded available {} with FitContent",
1839 total,
1840 available
1841 );
1842 }
1843 }
1844
1845 mod rounding_tests {
1850 use super::super::*;
1851
1852 #[test]
1855 fn rounding_conserves_sum_exact() {
1856 let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
1857 assert_eq!(result.iter().copied().sum::<u16>(), 40);
1858 assert_eq!(result, vec![10, 20, 10]);
1859 }
1860
1861 #[test]
1862 fn rounding_conserves_sum_fractional() {
1863 let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
1864 assert_eq!(
1865 result.iter().copied().sum::<u16>(),
1866 40,
1867 "Sum must equal total: {:?}",
1868 result
1869 );
1870 }
1871
1872 #[test]
1873 fn rounding_conserves_sum_many_fractions() {
1874 let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
1875 let result = round_layout_stable(&targets, 100, None);
1876 assert_eq!(
1877 result.iter().copied().sum::<u16>(),
1878 100,
1879 "Sum must be exactly 100: {:?}",
1880 result
1881 );
1882 }
1883
1884 #[test]
1885 fn rounding_conserves_sum_all_half() {
1886 let targets = vec![10.5, 10.5, 10.5, 10.5];
1887 let result = round_layout_stable(&targets, 42, None);
1888 assert_eq!(
1889 result.iter().copied().sum::<u16>(),
1890 42,
1891 "Sum must be exactly 42: {:?}",
1892 result
1893 );
1894 }
1895
1896 #[test]
1899 fn rounding_displacement_bounded() {
1900 let targets = vec![33.33, 33.33, 33.34];
1901 let result = round_layout_stable(&targets, 100, None);
1902 assert_eq!(result.iter().copied().sum::<u16>(), 100);
1903
1904 for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
1905 let floor = r.floor() as u16;
1906 let ceil = floor + 1;
1907 assert!(
1908 x == floor || x == ceil,
1909 "Element {} = {} not in {{floor={}, ceil={}}} of target {}",
1910 i,
1911 x,
1912 floor,
1913 ceil,
1914 r
1915 );
1916 }
1917 }
1918
1919 #[test]
1922 fn temporal_tiebreak_stable_when_unchanged() {
1923 let targets = vec![10.5, 10.5, 10.5, 10.5];
1924 let first = round_layout_stable(&targets, 42, None);
1925 let second = round_layout_stable(&targets, 42, Some(first.clone()));
1926 assert_eq!(
1927 first, second,
1928 "Identical targets should produce identical results"
1929 );
1930 }
1931
1932 #[test]
1933 fn temporal_tiebreak_prefers_previous_direction() {
1934 let targets = vec![10.5, 10.5];
1935 let total = 21;
1936 let first = round_layout_stable(&targets, total, None);
1937 assert_eq!(first.iter().copied().sum::<u16>(), total);
1938 let second = round_layout_stable(&targets, total, Some(first.clone()));
1939 assert_eq!(first, second, "Should maintain rounding direction");
1940 }
1941
1942 #[test]
1943 fn temporal_tiebreak_adapts_to_changed_targets() {
1944 let targets_a = vec![10.5, 10.5];
1945 let result_a = round_layout_stable(&targets_a, 21, None);
1946 let targets_b = vec![15.7, 5.3];
1947 let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
1948 assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
1949 assert!(result_b[0] > result_b[1], "Should follow larger target");
1950 }
1951
1952 #[test]
1955 fn property_min_displacement_brute_force_small() {
1956 let targets = vec![3.3, 3.3, 3.4];
1957 let total: u16 = 10;
1958 let result = round_layout_stable(&targets, total, None);
1959 let our_displacement: f64 = result
1960 .iter()
1961 .zip(targets.iter())
1962 .map(|(&x, &r)| (x as f64 - r).abs())
1963 .sum();
1964
1965 let mut min_displacement = f64::MAX;
1966 let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
1967 let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
1968
1969 for a in floors[0]..=ceils[0] {
1970 for b in floors[1]..=ceils[1] {
1971 for c in floors[2]..=ceils[2] {
1972 if a + b + c == total {
1973 let disp = (a as f64 - targets[0]).abs()
1974 + (b as f64 - targets[1]).abs()
1975 + (c as f64 - targets[2]).abs();
1976 if disp < min_displacement {
1977 min_displacement = disp;
1978 }
1979 }
1980 }
1981 }
1982 }
1983
1984 assert!(
1985 (our_displacement - min_displacement).abs() < 1e-10,
1986 "Our displacement {} should match optimal {}: {:?}",
1987 our_displacement,
1988 min_displacement,
1989 result
1990 );
1991 }
1992
1993 #[test]
1996 fn rounding_deterministic() {
1997 let targets = vec![7.7, 8.3, 14.0];
1998 let a = round_layout_stable(&targets, 30, None);
1999 let b = round_layout_stable(&targets, 30, None);
2000 assert_eq!(a, b, "Same inputs must produce identical outputs");
2001 }
2002
2003 #[test]
2006 fn rounding_empty_targets() {
2007 let result = round_layout_stable(&[], 0, None);
2008 assert!(result.is_empty());
2009 }
2010
2011 #[test]
2012 fn rounding_single_element() {
2013 let result = round_layout_stable(&[10.7], 11, None);
2014 assert_eq!(result, vec![11]);
2015 }
2016
2017 #[test]
2018 fn rounding_zero_total() {
2019 let result = round_layout_stable(&[5.0, 5.0], 0, None);
2020 assert_eq!(result.iter().copied().sum::<u16>(), 0);
2021 }
2022
2023 #[test]
2024 fn rounding_all_zeros() {
2025 let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
2026 assert_eq!(result, vec![0, 0, 0]);
2027 }
2028
2029 #[test]
2030 fn rounding_integer_targets() {
2031 let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
2032 assert_eq!(result, vec![10, 20, 30]);
2033 }
2034
2035 #[test]
2036 fn rounding_large_deficit() {
2037 let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
2038 assert_eq!(result.iter().copied().sum::<u16>(), 3);
2039 assert_eq!(result, vec![1, 1, 1]);
2040 }
2041
2042 #[test]
2043 fn rounding_with_prev_different_length() {
2044 let result = round_layout_stable(&[10.5, 10.5], 21, Some(vec![11, 10, 5]));
2045 assert_eq!(result.iter().copied().sum::<u16>(), 21);
2046 }
2047
2048 #[test]
2049 fn rounding_very_small_fractions() {
2050 let targets = vec![10.001, 20.001, 9.998];
2051 let result = round_layout_stable(&targets, 40, None);
2052 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2053 }
2054
2055 #[test]
2056 fn rounding_conserves_sum_stress() {
2057 let n = 50;
2058 let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
2059 let total = 120u16;
2060 let result = round_layout_stable(&targets, total, None);
2061 assert_eq!(
2062 result.iter().copied().sum::<u16>(),
2063 total,
2064 "Sum must be exactly {} for {} items: {:?}",
2065 total,
2066 n,
2067 result
2068 );
2069 }
2070 }
2071
2072 mod property_constraint_tests {
2077 use super::super::*;
2078
2079 struct Lcg(u64);
2081
2082 impl Lcg {
2083 fn new(seed: u64) -> Self {
2084 Self(seed)
2085 }
2086 fn next_u32(&mut self) -> u32 {
2087 self.0 = self
2088 .0
2089 .wrapping_mul(6_364_136_223_846_793_005)
2090 .wrapping_add(1);
2091 (self.0 >> 33) as u32
2092 }
2093 fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
2094 if lo >= hi {
2095 return lo;
2096 }
2097 lo + (self.next_u32() % (hi - lo) as u32) as u16
2098 }
2099 fn next_f32(&mut self) -> f32 {
2100 (self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
2101 }
2102 }
2103
2104 fn random_constraint(rng: &mut Lcg) -> Constraint {
2106 match rng.next_u32() % 7 {
2107 0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
2108 1 => Constraint::Percentage(rng.next_f32() * 100.0),
2109 2 => Constraint::Min(rng.next_u16_range(0, 40)),
2110 3 => Constraint::Max(rng.next_u16_range(5, 120)),
2111 4 => {
2112 let n = rng.next_u32() % 5 + 1;
2113 let d = rng.next_u32() % 5 + 1;
2114 Constraint::Ratio(n, d)
2115 }
2116 5 => Constraint::Fill,
2117 _ => Constraint::FitContent,
2118 }
2119 }
2120
2121 #[test]
2122 fn property_constraints_respected_fixed() {
2123 let mut rng = Lcg::new(0xDEAD_BEEF);
2124 for _ in 0..200 {
2125 let fixed_val = rng.next_u16_range(1, 60);
2126 let avail = rng.next_u16_range(10, 200);
2127 let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
2128 let rects = flex.split(Rect::new(0, 0, avail, 10));
2129 assert!(
2130 rects[0].width <= fixed_val.min(avail),
2131 "Fixed({}) in avail {} -> width {}",
2132 fixed_val,
2133 avail,
2134 rects[0].width
2135 );
2136 }
2137 }
2138
2139 #[test]
2140 fn property_constraints_respected_max() {
2141 let mut rng = Lcg::new(0xCAFE_BABE);
2142 for _ in 0..200 {
2143 let max_val = rng.next_u16_range(5, 80);
2144 let avail = rng.next_u16_range(10, 200);
2145 let flex =
2146 Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
2147 let rects = flex.split(Rect::new(0, 0, avail, 10));
2148 assert!(
2149 rects[0].width <= max_val,
2150 "Max({}) in avail {} -> width {}",
2151 max_val,
2152 avail,
2153 rects[0].width
2154 );
2155 }
2156 }
2157
2158 #[test]
2159 fn property_constraints_respected_min() {
2160 let mut rng = Lcg::new(0xBAAD_F00D);
2161 for _ in 0..200 {
2162 let min_val = rng.next_u16_range(0, 40);
2163 let avail = rng.next_u16_range(min_val.max(1), 200);
2164 let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
2165 let rects = flex.split(Rect::new(0, 0, avail, 10));
2166 assert!(
2167 rects[0].width >= min_val,
2168 "Min({}) in avail {} -> width {}",
2169 min_val,
2170 avail,
2171 rects[0].width
2172 );
2173 }
2174 }
2175
2176 #[test]
2177 fn property_constraints_respected_ratio_proportional() {
2178 let mut rng = Lcg::new(0x1234_5678);
2179 for _ in 0..200 {
2180 let n1 = rng.next_u32() % 5 + 1;
2181 let n2 = rng.next_u32() % 5 + 1;
2182 let d = rng.next_u32() % 5 + 1;
2183 let avail = rng.next_u16_range(20, 200);
2184 let flex = Flex::horizontal()
2185 .constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
2186 let rects = flex.split(Rect::new(0, 0, avail, 10));
2187 let w1 = rects[0].width as f64;
2188 let w2 = rects[1].width as f64;
2189 let total = w1 + w2;
2190 if total > 0.0 {
2191 let expected_ratio = n1 as f64 / (n1 + n2) as f64;
2192 let actual_ratio = w1 / total;
2193 assert!(
2194 (actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
2195 "Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
2196 n1,
2197 d,
2198 n1,
2199 n2,
2200 avail,
2201 expected_ratio,
2202 actual_ratio,
2203 w1,
2204 w2
2205 );
2206 }
2207 }
2208 }
2209
2210 #[test]
2211 fn property_total_allocation_never_exceeds_available() {
2212 let mut rng = Lcg::new(0xFACE_FEED);
2213 for _ in 0..500 {
2214 let n = (rng.next_u32() % 6 + 1) as usize;
2215 let constraints: Vec<Constraint> =
2216 (0..n).map(|_| random_constraint(&mut rng)).collect();
2217 let avail = rng.next_u16_range(5, 200);
2218 let dir = if rng.next_u32().is_multiple_of(2) {
2219 Direction::Horizontal
2220 } else {
2221 Direction::Vertical
2222 };
2223 let flex = Flex::default().direction(dir).constraints(constraints);
2224 let area = Rect::new(0, 0, avail, avail);
2225 let rects = flex.split(area);
2226 let total: u16 = rects
2227 .iter()
2228 .map(|r| match dir {
2229 Direction::Horizontal => r.width,
2230 Direction::Vertical => r.height,
2231 })
2232 .sum();
2233 assert!(
2234 total <= avail,
2235 "Total {} exceeded available {} with {} constraints",
2236 total,
2237 avail,
2238 n
2239 );
2240 }
2241 }
2242
2243 #[test]
2244 fn property_no_overlap_horizontal() {
2245 let mut rng = Lcg::new(0xABCD_1234);
2246 for _ in 0..300 {
2247 let n = (rng.next_u32() % 5 + 2) as usize;
2248 let constraints: Vec<Constraint> =
2249 (0..n).map(|_| random_constraint(&mut rng)).collect();
2250 let avail = rng.next_u16_range(20, 200);
2251 let flex = Flex::horizontal().constraints(constraints);
2252 let rects = flex.split(Rect::new(0, 0, avail, 10));
2253
2254 for i in 1..rects.len() {
2255 let prev_end = rects[i - 1].x + rects[i - 1].width;
2256 assert!(
2257 rects[i].x >= prev_end,
2258 "Overlap at {}: prev ends {}, next starts {}",
2259 i,
2260 prev_end,
2261 rects[i].x
2262 );
2263 }
2264 }
2265 }
2266
2267 #[test]
2268 fn property_deterministic_across_runs() {
2269 let mut rng = Lcg::new(0x9999_8888);
2270 for _ in 0..100 {
2271 let n = (rng.next_u32() % 5 + 1) as usize;
2272 let constraints: Vec<Constraint> =
2273 (0..n).map(|_| random_constraint(&mut rng)).collect();
2274 let avail = rng.next_u16_range(10, 200);
2275 let r1 = Flex::horizontal()
2276 .constraints(constraints.clone())
2277 .split(Rect::new(0, 0, avail, 10));
2278 let r2 = Flex::horizontal()
2279 .constraints(constraints)
2280 .split(Rect::new(0, 0, avail, 10));
2281 assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
2282 }
2283 }
2284 }
2285
2286 mod property_temporal_tests {
2291 use super::super::*;
2292 use crate::cache::{CoherenceCache, CoherenceId};
2293
2294 struct Lcg(u64);
2296
2297 impl Lcg {
2298 fn new(seed: u64) -> Self {
2299 Self(seed)
2300 }
2301 fn next_u32(&mut self) -> u32 {
2302 self.0 = self
2303 .0
2304 .wrapping_mul(6_364_136_223_846_793_005)
2305 .wrapping_add(1);
2306 (self.0 >> 33) as u32
2307 }
2308 }
2309
2310 #[test]
2311 fn property_temporal_stability_small_resize() {
2312 let constraints = [
2313 Constraint::Percentage(33.3),
2314 Constraint::Percentage(33.3),
2315 Constraint::Fill,
2316 ];
2317 let mut coherence = CoherenceCache::new(64);
2318 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2319
2320 for total in [80u16, 100, 120] {
2321 let flex = Flex::horizontal().constraints(constraints);
2322 let rects = flex.split(Rect::new(0, 0, total, 10));
2323 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2324
2325 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2326 let prev = coherence.get(&id);
2327 let rounded = round_layout_stable(&targets, total, prev);
2328
2329 if let Some(old) = coherence.get(&id) {
2330 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2331 assert!(
2332 max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
2333 "max_disp={} too large for size change {} -> {}",
2334 max_disp,
2335 old.iter().copied().sum::<u16>(),
2336 total
2337 );
2338 let _ = sum_disp;
2339 }
2340 coherence.store(id, rounded);
2341 }
2342 }
2343
2344 #[test]
2345 fn property_temporal_stability_random_walk() {
2346 let constraints = [
2347 Constraint::Ratio(1, 3),
2348 Constraint::Ratio(1, 3),
2349 Constraint::Ratio(1, 3),
2350 ];
2351 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2352 let mut coherence = CoherenceCache::new(64);
2353 let mut rng = Lcg::new(0x5555_AAAA);
2354 let mut total: u16 = 90;
2355
2356 for step in 0..200 {
2357 let prev_total = total;
2358 let delta = (rng.next_u32() % 7) as i32 - 3;
2359 total = (total as i32 + delta).clamp(10, 250) as u16;
2360
2361 let flex = Flex::horizontal().constraints(constraints);
2362 let rects = flex.split(Rect::new(0, 0, total, 10));
2363 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2364
2365 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2366 let prev = coherence.get(&id);
2367 let rounded = round_layout_stable(&targets, total, prev);
2368
2369 if coherence.get(&id).is_some() {
2370 let (_, max_disp) = coherence.displacement(&id, &rounded);
2371 let size_change = total.abs_diff(prev_total);
2372 assert!(
2373 max_disp <= size_change as u32 + 2,
2374 "step {}: max_disp={} exceeds size_change={} + 2",
2375 step,
2376 max_disp,
2377 size_change
2378 );
2379 }
2380 coherence.store(id, rounded);
2381 }
2382 }
2383
2384 #[test]
2385 fn property_temporal_stability_identical_frames() {
2386 let constraints = [
2387 Constraint::Fixed(20),
2388 Constraint::Fill,
2389 Constraint::Fixed(15),
2390 ];
2391 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2392 let mut coherence = CoherenceCache::new(64);
2393
2394 let flex = Flex::horizontal().constraints(constraints);
2395 let rects = flex.split(Rect::new(0, 0, 100, 10));
2396 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2397 coherence.store(id, widths.clone());
2398
2399 for _ in 0..10 {
2400 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2401 let prev = coherence.get(&id);
2402 let rounded = round_layout_stable(&targets, 100, prev);
2403 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2404 assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
2405 assert_eq!(max_disp, 0);
2406 coherence.store(id, rounded);
2407 }
2408 }
2409
2410 #[test]
2411 fn property_temporal_coherence_sweep() {
2412 let constraints = [
2413 Constraint::Percentage(25.0),
2414 Constraint::Percentage(50.0),
2415 Constraint::Fill,
2416 ];
2417 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2418 let mut coherence = CoherenceCache::new(64);
2419 let mut total_displacement: u64 = 0;
2420
2421 for total in 60u16..=140 {
2422 let flex = Flex::horizontal().constraints(constraints);
2423 let rects = flex.split(Rect::new(0, 0, total, 10));
2424 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2425
2426 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2427 let prev = coherence.get(&id);
2428 let rounded = round_layout_stable(&targets, total, prev);
2429
2430 if coherence.get(&id).is_some() {
2431 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2432 total_displacement += sum_disp;
2433 }
2434 coherence.store(id, rounded);
2435 }
2436
2437 assert!(
2438 total_displacement <= 80 * 3,
2439 "Total displacement {} exceeds bound for 80-step sweep",
2440 total_displacement
2441 );
2442 }
2443 }
2444
2445 mod snapshot_layout_tests {
2450 use super::super::*;
2451 use crate::grid::{Grid, GridArea};
2452
2453 fn snapshot_flex(
2454 constraints: &[Constraint],
2455 dir: Direction,
2456 width: u16,
2457 height: u16,
2458 ) -> String {
2459 let flex = Flex::default()
2460 .direction(dir)
2461 .constraints(constraints.iter().copied());
2462 let rects = flex.split(Rect::new(0, 0, width, height));
2463 let mut out = format!(
2464 "Flex {:?} {}x{} ({} constraints)\n",
2465 dir,
2466 width,
2467 height,
2468 constraints.len()
2469 );
2470 for (i, r) in rects.iter().enumerate() {
2471 out.push_str(&format!(
2472 " [{}] x={} y={} w={} h={}\n",
2473 i, r.x, r.y, r.width, r.height
2474 ));
2475 }
2476 let total: u16 = rects
2477 .iter()
2478 .map(|r| match dir {
2479 Direction::Horizontal => r.width,
2480 Direction::Vertical => r.height,
2481 })
2482 .sum();
2483 out.push_str(&format!(" total={}\n", total));
2484 out
2485 }
2486
2487 fn snapshot_grid(
2488 rows: &[Constraint],
2489 cols: &[Constraint],
2490 areas: &[(&str, GridArea)],
2491 width: u16,
2492 height: u16,
2493 ) -> String {
2494 let mut grid = Grid::new()
2495 .rows(rows.iter().copied())
2496 .columns(cols.iter().copied());
2497 for &(name, area) in areas {
2498 grid = grid.area(name, area);
2499 }
2500 let layout = grid.split(Rect::new(0, 0, width, height));
2501
2502 let mut out = format!(
2503 "Grid {}x{} ({}r x {}c)\n",
2504 width,
2505 height,
2506 rows.len(),
2507 cols.len()
2508 );
2509 for r in 0..rows.len() {
2510 for c in 0..cols.len() {
2511 let rect = layout.cell(r, c);
2512 out.push_str(&format!(
2513 " [{},{}] x={} y={} w={} h={}\n",
2514 r, c, rect.x, rect.y, rect.width, rect.height
2515 ));
2516 }
2517 }
2518 for &(name, _) in areas {
2519 if let Some(rect) = layout.area(name) {
2520 out.push_str(&format!(
2521 " area({}) x={} y={} w={} h={}\n",
2522 name, rect.x, rect.y, rect.width, rect.height
2523 ));
2524 }
2525 }
2526 out
2527 }
2528
2529 #[test]
2532 fn snapshot_flex_thirds_80x24() {
2533 let snap = snapshot_flex(
2534 &[
2535 Constraint::Ratio(1, 3),
2536 Constraint::Ratio(1, 3),
2537 Constraint::Ratio(1, 3),
2538 ],
2539 Direction::Horizontal,
2540 80,
2541 24,
2542 );
2543 assert_eq!(
2544 snap,
2545 "\
2546Flex Horizontal 80x24 (3 constraints)
2547 [0] x=0 y=0 w=26 h=24
2548 [1] x=26 y=0 w=26 h=24
2549 [2] x=52 y=0 w=28 h=24
2550 total=80
2551"
2552 );
2553 }
2554
2555 #[test]
2556 fn snapshot_flex_sidebar_content_80x24() {
2557 let snap = snapshot_flex(
2558 &[Constraint::Fixed(20), Constraint::Fill],
2559 Direction::Horizontal,
2560 80,
2561 24,
2562 );
2563 assert_eq!(
2564 snap,
2565 "\
2566Flex Horizontal 80x24 (2 constraints)
2567 [0] x=0 y=0 w=20 h=24
2568 [1] x=20 y=0 w=60 h=24
2569 total=80
2570"
2571 );
2572 }
2573
2574 #[test]
2575 fn snapshot_flex_header_body_footer_80x24() {
2576 let snap = snapshot_flex(
2577 &[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
2578 Direction::Vertical,
2579 80,
2580 24,
2581 );
2582 assert_eq!(
2583 snap,
2584 "\
2585Flex Vertical 80x24 (3 constraints)
2586 [0] x=0 y=0 w=80 h=3
2587 [1] x=0 y=3 w=80 h=20
2588 [2] x=0 y=23 w=80 h=1
2589 total=24
2590"
2591 );
2592 }
2593
2594 #[test]
2597 fn snapshot_flex_thirds_120x40() {
2598 let snap = snapshot_flex(
2599 &[
2600 Constraint::Ratio(1, 3),
2601 Constraint::Ratio(1, 3),
2602 Constraint::Ratio(1, 3),
2603 ],
2604 Direction::Horizontal,
2605 120,
2606 40,
2607 );
2608 assert_eq!(
2609 snap,
2610 "\
2611Flex Horizontal 120x40 (3 constraints)
2612 [0] x=0 y=0 w=40 h=40
2613 [1] x=40 y=0 w=40 h=40
2614 [2] x=80 y=0 w=40 h=40
2615 total=120
2616"
2617 );
2618 }
2619
2620 #[test]
2621 fn snapshot_flex_sidebar_content_120x40() {
2622 let snap = snapshot_flex(
2623 &[Constraint::Fixed(20), Constraint::Fill],
2624 Direction::Horizontal,
2625 120,
2626 40,
2627 );
2628 assert_eq!(
2629 snap,
2630 "\
2631Flex Horizontal 120x40 (2 constraints)
2632 [0] x=0 y=0 w=20 h=40
2633 [1] x=20 y=0 w=100 h=40
2634 total=120
2635"
2636 );
2637 }
2638
2639 #[test]
2640 fn snapshot_flex_percentage_mix_120x40() {
2641 let snap = snapshot_flex(
2642 &[
2643 Constraint::Percentage(25.0),
2644 Constraint::Percentage(50.0),
2645 Constraint::Fill,
2646 ],
2647 Direction::Horizontal,
2648 120,
2649 40,
2650 );
2651 assert_eq!(
2652 snap,
2653 "\
2654Flex Horizontal 120x40 (3 constraints)
2655 [0] x=0 y=0 w=30 h=40
2656 [1] x=30 y=0 w=60 h=40
2657 [2] x=90 y=0 w=30 h=40
2658 total=120
2659"
2660 );
2661 }
2662
2663 #[test]
2666 fn snapshot_grid_2x2_80x24() {
2667 let snap = snapshot_grid(
2668 &[Constraint::Fixed(3), Constraint::Fill],
2669 &[Constraint::Fixed(20), Constraint::Fill],
2670 &[
2671 ("header", GridArea::span(0, 0, 1, 2)),
2672 ("sidebar", GridArea::span(1, 0, 1, 1)),
2673 ("content", GridArea::cell(1, 1)),
2674 ],
2675 80,
2676 24,
2677 );
2678 assert_eq!(
2679 snap,
2680 "\
2681Grid 80x24 (2r x 2c)
2682 [0,0] x=0 y=0 w=20 h=3
2683 [0,1] x=20 y=0 w=60 h=3
2684 [1,0] x=0 y=3 w=20 h=21
2685 [1,1] x=20 y=3 w=60 h=21
2686 area(header) x=0 y=0 w=80 h=3
2687 area(sidebar) x=0 y=3 w=20 h=21
2688 area(content) x=20 y=3 w=60 h=21
2689"
2690 );
2691 }
2692
2693 #[test]
2694 fn snapshot_grid_3x3_80x24() {
2695 let snap = snapshot_grid(
2696 &[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
2697 &[
2698 Constraint::Fixed(10),
2699 Constraint::Fill,
2700 Constraint::Fixed(10),
2701 ],
2702 &[],
2703 80,
2704 24,
2705 );
2706 assert_eq!(
2707 snap,
2708 "\
2709Grid 80x24 (3r x 3c)
2710 [0,0] x=0 y=0 w=10 h=1
2711 [0,1] x=10 y=0 w=60 h=1
2712 [0,2] x=70 y=0 w=10 h=1
2713 [1,0] x=0 y=1 w=10 h=22
2714 [1,1] x=10 y=1 w=60 h=22
2715 [1,2] x=70 y=1 w=10 h=22
2716 [2,0] x=0 y=23 w=10 h=1
2717 [2,1] x=10 y=23 w=60 h=1
2718 [2,2] x=70 y=23 w=10 h=1
2719"
2720 );
2721 }
2722
2723 #[test]
2726 fn snapshot_grid_2x2_120x40() {
2727 let snap = snapshot_grid(
2728 &[Constraint::Fixed(3), Constraint::Fill],
2729 &[Constraint::Fixed(20), Constraint::Fill],
2730 &[
2731 ("header", GridArea::span(0, 0, 1, 2)),
2732 ("sidebar", GridArea::span(1, 0, 1, 1)),
2733 ("content", GridArea::cell(1, 1)),
2734 ],
2735 120,
2736 40,
2737 );
2738 assert_eq!(
2739 snap,
2740 "\
2741Grid 120x40 (2r x 2c)
2742 [0,0] x=0 y=0 w=20 h=3
2743 [0,1] x=20 y=0 w=100 h=3
2744 [1,0] x=0 y=3 w=20 h=37
2745 [1,1] x=20 y=3 w=100 h=37
2746 area(header) x=0 y=0 w=120 h=3
2747 area(sidebar) x=0 y=3 w=20 h=37
2748 area(content) x=20 y=3 w=100 h=37
2749"
2750 );
2751 }
2752
2753 #[test]
2754 fn snapshot_grid_dashboard_120x40() {
2755 let snap = snapshot_grid(
2756 &[
2757 Constraint::Fixed(3),
2758 Constraint::Percentage(60.0),
2759 Constraint::Fill,
2760 ],
2761 &[Constraint::Percentage(30.0), Constraint::Fill],
2762 &[
2763 ("nav", GridArea::span(0, 0, 1, 2)),
2764 ("chart", GridArea::cell(1, 0)),
2765 ("detail", GridArea::cell(1, 1)),
2766 ("log", GridArea::span(2, 0, 1, 2)),
2767 ],
2768 120,
2769 40,
2770 );
2771 assert_eq!(
2772 snap,
2773 "\
2774Grid 120x40 (3r x 2c)
2775 [0,0] x=0 y=0 w=36 h=3
2776 [0,1] x=36 y=0 w=84 h=3
2777 [1,0] x=0 y=3 w=36 h=24
2778 [1,1] x=36 y=3 w=84 h=24
2779 [2,0] x=0 y=27 w=36 h=13
2780 [2,1] x=36 y=27 w=84 h=13
2781 area(nav) x=0 y=0 w=120 h=3
2782 area(chart) x=0 y=3 w=36 h=24
2783 area(detail) x=36 y=3 w=84 h=24
2784 area(log) x=0 y=27 w=120 h=13
2785"
2786 );
2787 }
2788 }
2789}