1#![forbid(unsafe_code)]
2
3pub mod cache;
43pub mod debug;
44pub mod dep_graph;
45pub mod direction;
46pub mod egraph;
47pub mod grid;
48pub mod incremental;
49pub mod pane;
50#[cfg(test)]
51mod repro_max_constraint;
52#[cfg(test)]
53mod repro_space_around;
54pub mod responsive;
55pub mod responsive_layout;
56pub mod veb_tree;
57pub mod visibility;
58pub mod workspace;
59
60pub use cache::{
61 CoherenceCache, CoherenceId, LayoutCache, LayoutCacheKey, LayoutCacheStats, S3FifoLayoutCache,
62};
63pub use direction::{FlowDirection, LogicalAlignment, LogicalSides, mirror_rects_horizontal};
64pub use ftui_core::geometry::{Rect, Sides, Size};
65pub use grid::{Grid, GridArea, GridLayout};
66pub use pane::{
67 PANE_DEFAULT_MARGIN_CELLS, PANE_DEFAULT_PADDING_CELLS, PANE_DRAG_RESIZE_DEFAULT_HYSTERESIS,
68 PANE_DRAG_RESIZE_DEFAULT_THRESHOLD, PANE_EDGE_GRIP_INSET_CELLS, PANE_MAGNETIC_FIELD_CELLS,
69 PANE_SEMANTIC_INPUT_EVENT_SCHEMA_VERSION, PANE_SEMANTIC_INPUT_TRACE_SCHEMA_VERSION,
70 PANE_SNAP_DEFAULT_HYSTERESIS_BPS, PANE_SNAP_DEFAULT_STEP_BPS, PANE_TREE_SCHEMA_VERSION,
71 PaneCancelReason, PaneConstraints, PaneCoordinateNormalizationError, PaneCoordinateNormalizer,
72 PaneCoordinateRoundingPolicy, PaneDockPreview, PaneDockZone, PaneDragBehaviorTuning,
73 PaneDragResizeEffect, PaneDragResizeMachine, PaneDragResizeMachineError,
74 PaneDragResizeNoopReason, PaneDragResizeState, PaneDragResizeTransition, PaneEdgeResizePlan,
75 PaneEdgeResizePlanError, PaneGroupTransformPlan, PaneId, PaneIdAllocator, PaneInertialThrow,
76 PaneInputCoordinate, PaneInteractionPolicyError, PaneInteractionTimeline,
77 PaneInteractionTimelineCheckpointDecision, PaneInteractionTimelineEntry,
78 PaneInteractionTimelineError, PaneInteractionTimelineReplayDiagnostics, PaneInvariantCode,
79 PaneInvariantIssue, PaneInvariantReport, PaneInvariantSeverity, PaneLayout,
80 PaneLayoutIntelligenceMode, PaneLeaf, PaneModelError, PaneModifierSnapshot, PaneMotionVector,
81 PaneNodeKind, PaneNodeRecord, PaneNormalizedCoordinate, PaneOperation, PaneOperationError,
82 PaneOperationFailure, PaneOperationJournalEntry, PaneOperationJournalResult, PaneOperationKind,
83 PaneOperationOutcome, PanePlacement, PanePointerButton, PanePointerPosition, PanePrecisionMode,
84 PanePrecisionPolicy, PanePressureSnapProfile, PaneReflowMovePlan, PaneReflowPlanError,
85 PaneRepairAction, PaneRepairError, PaneRepairFailure, PaneRepairOutcome, PaneResizeDirection,
86 PaneResizeGrip, PaneResizeTarget, PaneScaleFactor, PaneSelectionState, PaneSemanticInputEvent,
87 PaneSemanticInputEventError, PaneSemanticInputEventKind, PaneSemanticInputTrace,
88 PaneSemanticInputTraceError, PaneSemanticInputTraceMetadata,
89 PaneSemanticReplayConformanceArtifact, PaneSemanticReplayDiffArtifact,
90 PaneSemanticReplayDiffKind, PaneSemanticReplayError, PaneSemanticReplayFixture,
91 PaneSemanticReplayOutcome, PaneSnapDecision, PaneSnapReason, PaneSnapTuning, PaneSplit,
92 PaneSplitRatio, PaneTransaction, PaneTransactionOutcome, PaneTree, PaneTreeSnapshot, SplitAxis,
93};
94pub use responsive::Responsive;
95pub use responsive_layout::{ResponsiveLayout, ResponsiveSplit};
96pub use smallvec;
97use smallvec::SmallVec;
98use std::cmp::min;
99pub use visibility::Visibility;
100pub use workspace::{
101 MigrationResult, WORKSPACE_SCHEMA_VERSION, WorkspaceMetadata, WorkspaceMigrationError,
102 WorkspaceSnapshot, WorkspaceValidationError, migrate_workspace, needs_migration,
103};
104
105const LAYOUT_INLINE_CAP: usize = 8;
110
111pub type Rects = SmallVec<[Rect; LAYOUT_INLINE_CAP]>;
113
114type Sizes = SmallVec<[u16; LAYOUT_INLINE_CAP]>;
116
117#[derive(Debug, Clone, Copy, PartialEq)]
119pub enum Constraint {
120 Fixed(u16),
122 Percentage(f32),
124 Min(u16),
126 Max(u16),
128 Ratio(u32, u32),
130 Fill,
132 FitContent,
137 FitContentBounded {
142 min: u16,
144 max: u16,
146 },
147 FitMin,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
176pub struct LayoutSizeHint {
177 pub min: u16,
179 pub preferred: u16,
181 pub max: Option<u16>,
183}
184
185impl LayoutSizeHint {
186 pub const ZERO: Self = Self {
188 min: 0,
189 preferred: 0,
190 max: None,
191 };
192
193 #[inline]
195 pub const fn exact(size: u16) -> Self {
196 Self {
197 min: size,
198 preferred: size,
199 max: Some(size),
200 }
201 }
202
203 #[inline]
205 pub const fn at_least(min: u16, preferred: u16) -> Self {
206 Self {
207 min,
208 preferred,
209 max: None,
210 }
211 }
212
213 #[inline]
215 pub fn clamp(&self, value: u16) -> u16 {
216 let max = self.max.unwrap_or(u16::MAX);
217 value.min(max).max(self.min)
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
223pub enum Direction {
224 #[default]
226 Vertical,
227 Horizontal,
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
233pub enum Alignment {
234 #[default]
236 Start,
237 Center,
239 End,
241 SpaceAround,
243 SpaceBetween,
245}
246
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
260pub enum OverflowBehavior {
261 #[default]
265 Clip,
266 Visible,
269 Scroll {
273 max_content: Option<u16>,
276 },
277 Wrap,
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
295pub enum Breakpoint {
296 Xs,
298 Sm,
300 Md,
302 Lg,
304 Xl,
306}
307
308impl Breakpoint {
309 pub const ALL: [Breakpoint; 5] = [
311 Breakpoint::Xs,
312 Breakpoint::Sm,
313 Breakpoint::Md,
314 Breakpoint::Lg,
315 Breakpoint::Xl,
316 ];
317
318 #[inline]
320 const fn index(self) -> u8 {
321 match self {
322 Breakpoint::Xs => 0,
323 Breakpoint::Sm => 1,
324 Breakpoint::Md => 2,
325 Breakpoint::Lg => 3,
326 Breakpoint::Xl => 4,
327 }
328 }
329
330 #[must_use]
332 pub const fn label(self) -> &'static str {
333 match self {
334 Breakpoint::Xs => "xs",
335 Breakpoint::Sm => "sm",
336 Breakpoint::Md => "md",
337 Breakpoint::Lg => "lg",
338 Breakpoint::Xl => "xl",
339 }
340 }
341}
342
343impl std::fmt::Display for Breakpoint {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345 f.write_str(self.label())
346 }
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
354pub struct Breakpoints {
355 pub sm: u16,
357 pub md: u16,
359 pub lg: u16,
361 pub xl: u16,
363}
364
365impl Breakpoints {
366 pub const DEFAULT: Self = Self {
368 sm: 60,
369 md: 90,
370 lg: 120,
371 xl: 160,
372 };
373
374 pub const fn new(sm: u16, md: u16, lg: u16) -> Self {
378 let md = if md < sm { sm } else { md };
379 let lg = if lg < md { md } else { lg };
380 let xl = match lg.checked_add(40) {
382 Some(v) => v,
383 None => u16::MAX,
384 };
385 Self { sm, md, lg, xl }
386 }
387
388 pub const fn new_with_xl(sm: u16, md: u16, lg: u16, xl: u16) -> Self {
392 let md = if md < sm { sm } else { md };
393 let lg = if lg < md { md } else { lg };
394 let xl = if xl < lg { lg } else { xl };
395 Self { sm, md, lg, xl }
396 }
397
398 #[inline]
400 pub const fn classify_width(self, width: u16) -> Breakpoint {
401 if width >= self.xl {
402 Breakpoint::Xl
403 } else if width >= self.lg {
404 Breakpoint::Lg
405 } else if width >= self.md {
406 Breakpoint::Md
407 } else if width >= self.sm {
408 Breakpoint::Sm
409 } else {
410 Breakpoint::Xs
411 }
412 }
413
414 #[inline]
416 pub const fn classify_size(self, size: Size) -> Breakpoint {
417 self.classify_width(size.width)
418 }
419
420 #[inline]
422 pub const fn at_least(self, width: u16, min: Breakpoint) -> bool {
423 self.classify_width(width).index() >= min.index()
424 }
425
426 #[inline]
428 pub const fn between(self, width: u16, min: Breakpoint, max: Breakpoint) -> bool {
429 let idx = self.classify_width(width).index();
430 idx >= min.index() && idx <= max.index()
431 }
432
433 #[must_use]
435 pub const fn threshold(self, bp: Breakpoint) -> u16 {
436 match bp {
437 Breakpoint::Xs => 0,
438 Breakpoint::Sm => self.sm,
439 Breakpoint::Md => self.md,
440 Breakpoint::Lg => self.lg,
441 Breakpoint::Xl => self.xl,
442 }
443 }
444
445 #[must_use]
447 pub const fn thresholds(self) -> [(Breakpoint, u16); 5] {
448 [
449 (Breakpoint::Xs, 0),
450 (Breakpoint::Sm, self.sm),
451 (Breakpoint::Md, self.md),
452 (Breakpoint::Lg, self.lg),
453 (Breakpoint::Xl, self.xl),
454 ]
455 }
456}
457
458#[derive(Debug, Clone, Copy, Default)]
460pub struct Measurement {
461 pub min_width: u16,
463 pub min_height: u16,
465 pub max_width: Option<u16>,
467 pub max_height: Option<u16>,
469}
470
471impl Measurement {
472 #[must_use]
474 pub fn fixed(width: u16, height: u16) -> Self {
475 Self {
476 min_width: width,
477 min_height: height,
478 max_width: Some(width),
479 max_height: Some(height),
480 }
481 }
482
483 #[must_use]
485 pub fn flexible(min_width: u16, min_height: u16) -> Self {
486 Self {
487 min_width,
488 min_height,
489 max_width: None,
490 max_height: None,
491 }
492 }
493}
494
495#[derive(Debug, Clone, Default)]
497pub struct Flex {
498 direction: Direction,
499 constraints: Vec<Constraint>,
500 margin: Sides,
501 gap: u16,
502 alignment: Alignment,
503 flow_direction: direction::FlowDirection,
504 overflow: OverflowBehavior,
505}
506
507impl Flex {
508 #[must_use]
510 pub fn vertical() -> Self {
511 Self {
512 direction: Direction::Vertical,
513 ..Default::default()
514 }
515 }
516
517 #[must_use]
519 pub fn horizontal() -> Self {
520 Self {
521 direction: Direction::Horizontal,
522 ..Default::default()
523 }
524 }
525
526 #[must_use]
528 pub fn direction(mut self, direction: Direction) -> Self {
529 self.direction = direction;
530 self
531 }
532
533 #[must_use]
535 pub fn constraints(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
536 self.constraints = constraints.into_iter().collect();
537 self
538 }
539
540 #[must_use]
542 pub fn margin(mut self, margin: Sides) -> Self {
543 self.margin = margin;
544 self
545 }
546
547 #[must_use]
549 pub fn gap(mut self, gap: u16) -> Self {
550 self.gap = gap;
551 self
552 }
553
554 #[must_use]
556 pub fn alignment(mut self, alignment: Alignment) -> Self {
557 self.alignment = alignment;
558 self
559 }
560
561 #[must_use]
567 pub fn flow_direction(mut self, flow: direction::FlowDirection) -> Self {
568 self.flow_direction = flow;
569 self
570 }
571
572 #[must_use]
574 pub fn overflow(mut self, overflow: OverflowBehavior) -> Self {
575 self.overflow = overflow;
576 self
577 }
578
579 #[must_use]
581 pub fn overflow_behavior(&self) -> OverflowBehavior {
582 self.overflow
583 }
584
585 #[must_use]
587 pub fn constraint_count(&self) -> usize {
588 self.constraints.len()
589 }
590
591 pub fn split(&self, area: Rect) -> Rects {
593 let inner = area.inner(self.margin);
595 if inner.is_empty() {
596 return self.constraints.iter().map(|_| Rect::default()).collect();
597 }
598
599 let total_size = match self.direction {
600 Direction::Horizontal => inner.width,
601 Direction::Vertical => inner.height,
602 };
603
604 let count = self.constraints.len();
605 if count == 0 {
606 return Rects::new();
607 }
608
609 let gap_count = count - 1;
611 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
612 let available_size = total_size.saturating_sub(total_gap);
613
614 let sizes = solve_constraints(&self.constraints, available_size);
616
617 let mut rects = self.sizes_to_rects(inner, &sizes);
619
620 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
622 direction::mirror_rects_horizontal(&mut rects, inner);
623 }
624
625 rects
626 }
627
628 fn sizes_to_rects(&self, area: Rect, sizes: &[u16]) -> Rects {
629 let mut rects = SmallVec::with_capacity(sizes.len());
630 if sizes.is_empty() {
631 return rects;
632 }
633
634 let total_items_size: u16 = sizes.iter().fold(0u16, |acc, &s| acc.saturating_add(s));
635 let total_available = match self.direction {
636 Direction::Horizontal => area.width,
637 Direction::Vertical => area.height,
638 };
639
640 let (start_shift, use_formula) = match self.alignment {
642 Alignment::Start => (0, None),
643 Alignment::End => {
644 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
645 .min(u16::MAX as u64) as u16;
646 let used = total_items_size.saturating_add(gap_space);
647 (total_available.saturating_sub(used), None)
648 }
649 Alignment::Center => {
650 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
651 .min(u16::MAX as u64) as u16;
652 let used = total_items_size.saturating_add(gap_space);
653 (total_available.saturating_sub(used) / 2, None)
654 }
655 Alignment::SpaceBetween => {
656 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
657 .min(u16::MAX as u64) as u16;
658 let used = total_items_size.saturating_add(gap_space);
659 let leftover = total_available.saturating_sub(used);
660 let slots = sizes.len().saturating_sub(1);
661 if slots > 0 {
662 (0, Some((leftover, slots, 0))) } else {
664 (0, None)
665 }
666 }
667 Alignment::SpaceAround => {
668 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
669 .min(u16::MAX as u64) as u16;
670 let used = total_items_size.saturating_add(gap_space);
671 let leftover = total_available.saturating_sub(used);
672 let slots = sizes.len() * 2;
673 if slots > 0 {
674 (0, Some((leftover, slots, 1))) } else {
676 (0, None)
677 }
678 }
679 };
680
681 let mut accumulated_size = 0;
682
683 for (i, &size) in sizes.iter().enumerate() {
684 let explicit_gap_so_far = if i > 0 {
685 (i as u64 * self.gap as u64).min(u16::MAX as u64) as u16
686 } else {
687 0
688 };
689
690 let gap_offset = if let Some((leftover, slots, mode)) = use_formula {
691 if mode == 0 {
692 if i == 0 {
694 0
695 } else {
696 explicit_gap_so_far
697 .saturating_add((leftover as u64 * i as u64 / slots as u64) as u16)
698 }
699 } else {
700 let numerator = leftover as u64 * (2 * i as u64 + 1);
702 let denominator = slots as u64;
703 let raw = (numerator + (denominator / 2)) / denominator;
704 explicit_gap_so_far.saturating_add(raw.min(u64::from(u16::MAX)) as u16)
705 }
706 } else {
707 explicit_gap_so_far
709 };
710
711 let pos = match self.direction {
712 Direction::Horizontal => area
713 .x
714 .saturating_add(start_shift)
715 .saturating_add(accumulated_size)
716 .saturating_add(gap_offset),
717 Direction::Vertical => area
718 .y
719 .saturating_add(start_shift)
720 .saturating_add(accumulated_size)
721 .saturating_add(gap_offset),
722 };
723
724 let rect = match self.direction {
725 Direction::Horizontal => Rect {
726 x: pos,
727 y: area.y,
728 width: size.min(area.right().saturating_sub(pos)),
729 height: area.height,
730 },
731 Direction::Vertical => Rect {
732 x: area.x,
733 y: pos,
734 width: area.width,
735 height: size.min(area.bottom().saturating_sub(pos)),
736 },
737 };
738 rects.push(rect);
739 accumulated_size = accumulated_size.saturating_add(size);
740 }
741
742 rects
743 }
744
745 pub fn split_with_measurer<F>(&self, area: Rect, measurer: F) -> Rects
769 where
770 F: Fn(usize, u16) -> LayoutSizeHint,
771 {
772 let inner = area.inner(self.margin);
774 if inner.is_empty() {
775 return self.constraints.iter().map(|_| Rect::default()).collect();
776 }
777
778 let total_size = match self.direction {
779 Direction::Horizontal => inner.width,
780 Direction::Vertical => inner.height,
781 };
782
783 let count = self.constraints.len();
784 if count == 0 {
785 return Rects::new();
786 }
787
788 let gap_count = count - 1;
790 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
791 let available_size = total_size.saturating_sub(total_gap);
792
793 let sizes =
795 solve_constraints_with_hints(&self.constraints, available_size, &measurer, None);
796
797 let mut rects = self.sizes_to_rects(inner, &sizes);
799
800 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
802 direction::mirror_rects_horizontal(&mut rects, inner);
803 }
804
805 rects
806 }
807 pub fn split_with_measurer_stably<F>(
812 &self,
813 area: Rect,
814 measurer: F,
815 cache: &mut CoherenceCache,
816 ) -> Rects
817 where
818 F: Fn(usize, u16) -> LayoutSizeHint,
819 {
820 let inner = area.inner(self.margin);
822 if inner.is_empty() {
823 return self.constraints.iter().map(|_| Rect::default()).collect();
824 }
825
826 let total_size = match self.direction {
827 Direction::Horizontal => inner.width,
828 Direction::Vertical => inner.height,
829 };
830
831 let count = self.constraints.len();
832 if count == 0 {
833 return Rects::new();
834 }
835
836 let gap_count = count - 1;
838 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
839 let available_size = total_size.saturating_sub(total_gap);
840
841 let id = CoherenceId::new(&self.constraints, self.direction);
843 let sizes = solve_constraints_with_hints(
844 &self.constraints,
845 available_size,
846 &measurer,
847 Some((cache, id)),
848 );
849
850 let mut rects = self.sizes_to_rects(inner, &sizes);
852
853 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
855 direction::mirror_rects_horizontal(&mut rects, inner);
856 }
857
858 rects
859 }
860}
861
862pub(crate) fn solve_constraints(constraints: &[Constraint], available_size: u16) -> Sizes {
867 solve_constraints_with_hints(
869 constraints,
870 available_size,
871 &|_, _| LayoutSizeHint::ZERO,
872 None,
873 )
874}
875
876pub(crate) fn solve_constraints_with_hints<F>(
881 constraints: &[Constraint],
882 available_size: u16,
883 measurer: &F,
884 mut coherence: Option<(&mut CoherenceCache, CoherenceId)>,
885) -> Sizes
886where
887 F: Fn(usize, u16) -> LayoutSizeHint,
888{
889 const WEIGHT_SCALE: u64 = 10_000;
890
891 let mut sizes: Sizes = smallvec::smallvec![0u16; constraints.len()];
892 let mut remaining = available_size;
893 let mut grow_indices: SmallVec<[usize; LAYOUT_INLINE_CAP]> = SmallVec::new();
894
895 let grow_weight = |constraint: Constraint| -> u64 {
896 match constraint {
897 Constraint::Min(_) | Constraint::Max(_) | Constraint::Fill => WEIGHT_SCALE,
898 _ => 0,
899 }
900 };
901
902 for (i, &constraint) in constraints.iter().enumerate() {
905 match constraint {
906 Constraint::Fixed(size) => {
907 let size = min(size, remaining);
908 sizes[i] = size;
909 remaining = remaining.saturating_sub(size);
910 }
911 Constraint::Min(min_size) => {
912 let size = min(min_size, remaining);
913 sizes[i] = size;
914 remaining = remaining.saturating_sub(size);
915 }
917 Constraint::FitMin => {
918 let hint = measurer(i, remaining);
919 let size = min(hint.min, remaining);
920 sizes[i] = size;
921 remaining = remaining.saturating_sub(size);
922 }
923 Constraint::FitContent => {
924 let hint = measurer(i, remaining);
925 let size = min(hint.min, remaining);
926 sizes[i] = size;
927 remaining = remaining.saturating_sub(size);
928 }
929 Constraint::FitContentBounded { min: min_bound, .. } => {
930 let size = min(min_bound, remaining);
932 sizes[i] = size;
933 remaining = remaining.saturating_sub(size);
934 }
935 _ => {} }
937 }
938
939 for (i, &constraint) in constraints.iter().enumerate() {
942 match constraint {
943 Constraint::Percentage(p) => {
944 let target = (available_size as f32 * p / 100.0)
945 .round()
946 .min(u16::MAX as f32) as u16;
947 let needed = target.saturating_sub(sizes[i]);
948 let alloc = min(needed, remaining);
949 sizes[i] = sizes[i].saturating_add(alloc);
950 remaining = remaining.saturating_sub(alloc);
951 }
952 Constraint::Ratio(n, d) => {
953 let target = if d == 0 {
954 0
955 } else {
956 (u64::from(available_size) * u64::from(n) / u64::from(d)).min(u16::MAX as u64)
957 as u16
958 };
959 let needed = target.saturating_sub(sizes[i]);
960 let alloc = min(needed, remaining);
961 sizes[i] = sizes[i].saturating_add(alloc);
962 remaining = remaining.saturating_sub(alloc);
963 }
964 Constraint::FitContent => {
965 let hint = measurer(i, remaining);
966 let preferred = hint
967 .preferred
968 .max(sizes[i])
969 .min(hint.max.unwrap_or(u16::MAX));
970 let needed = preferred.saturating_sub(sizes[i]);
971 let alloc = min(needed, remaining);
972 sizes[i] = sizes[i].saturating_add(alloc);
973 remaining = remaining.saturating_sub(alloc);
974 }
975 Constraint::FitContentBounded { max: max_bound, .. } => {
976 let hint = measurer(i, remaining);
977 let preferred = hint.preferred.max(sizes[i]).min(max_bound);
978 let needed = preferred.saturating_sub(sizes[i]);
979 let alloc = min(needed, remaining);
980 sizes[i] = sizes[i].saturating_add(alloc);
981 remaining = remaining.saturating_sub(alloc);
982 }
983 Constraint::Min(_) => {
984 grow_indices.push(i);
985 }
986 Constraint::Max(_) => {
987 grow_indices.push(i);
988 }
989 Constraint::Fill => {
990 grow_indices.push(i);
991 }
992 _ => {} }
994 }
995
996 loop {
998 if remaining == 0 || grow_indices.is_empty() {
999 break;
1000 }
1001
1002 let mut total_weight = 0u128;
1003 for &i in &grow_indices {
1004 let weight = grow_weight(constraints[i]);
1005 if weight > 0 {
1006 total_weight = total_weight.saturating_add(u128::from(weight));
1007 }
1008 }
1009
1010 if total_weight == 0 {
1011 break;
1012 }
1013
1014 let space_to_distribute = remaining;
1015 let mut shares: SmallVec<[u16; LAYOUT_INLINE_CAP]> =
1016 smallvec::smallvec![0u16; constraints.len()];
1017
1018 let targets: Vec<f64> = grow_indices
1020 .iter()
1021 .map(|&i| {
1022 let weight = grow_weight(constraints[i]);
1023 (space_to_distribute as f64 * weight as f64) / total_weight as f64
1024 })
1025 .collect();
1026
1027 let prev_alloc = coherence
1029 .as_ref()
1030 .and_then(|(cache, id)| cache.get(id))
1031 .map(|full_prev| {
1032 grow_indices
1034 .iter()
1035 .map(|&i| full_prev.get(i).copied().unwrap_or(0))
1036 .collect()
1037 });
1038
1039 let distributed = round_layout_stable(&targets, space_to_distribute, prev_alloc);
1041
1042 for (k, &i) in grow_indices.iter().enumerate() {
1043 shares[i] = distributed[k];
1044 }
1045
1046 let mut violations = Vec::new();
1048 for &i in &grow_indices {
1049 if let Constraint::Max(max_val) = constraints[i]
1050 && sizes[i].saturating_add(shares[i]) > max_val
1051 {
1052 violations.push(i);
1053 }
1054 }
1055
1056 if violations.is_empty() {
1057 for &i in &grow_indices {
1059 sizes[i] = sizes[i].saturating_add(shares[i]);
1060 }
1061 if let Some((cache, id)) = coherence.as_mut() {
1062 if distributed.len() == targets.len() {
1065 let mut full_shares: Sizes = smallvec::smallvec![0u16; constraints.len()];
1066 for (k, &i) in grow_indices.iter().enumerate() {
1067 full_shares[i] = distributed[k];
1068 }
1069 cache.store(*id, full_shares);
1070 }
1071 }
1072 break;
1073 }
1074
1075 for i in violations {
1077 if let Constraint::Max(max_val) = constraints[i] {
1078 let consumed = max_val.saturating_sub(sizes[i]);
1081 sizes[i] = max_val;
1082 remaining = remaining.saturating_sub(consumed);
1083
1084 if let Some(pos) = grow_indices.iter().position(|&x| x == i) {
1086 grow_indices.remove(pos);
1087 }
1088 }
1089 }
1090 }
1091
1092 sizes
1093}
1094
1095pub type PreviousAllocation = Option<Sizes>;
1105
1106pub fn round_layout_stable(targets: &[f64], total: u16, prev: PreviousAllocation) -> Sizes {
1168 let n = targets.len();
1169 if n == 0 {
1170 return Sizes::new();
1171 }
1172
1173 let floors: Sizes = targets
1175 .iter()
1176 .map(|&r| (r.max(0.0).floor() as u64).min(u16::MAX as u64) as u16)
1177 .collect();
1178
1179 let floor_sum: u64 = floors.iter().map(|&x| u64::from(x)).sum();
1180 let total_u64 = u64::from(total);
1181
1182 if floor_sum > total_u64 {
1184 return redistribute_overflow(&floors, total);
1185 }
1186
1187 let deficit = (total_u64 - floor_sum) as u16;
1188
1189 if deficit == 0 {
1190 return floors;
1192 }
1193
1194 let mut priority: SmallVec<[(usize, f64, bool); LAYOUT_INLINE_CAP]> = targets
1196 .iter()
1197 .enumerate()
1198 .map(|(i, &r)| {
1199 let remainder = r - (floors[i] as f64);
1200 let ceil_val = floors[i].saturating_add(1);
1201 let prev_used_ceil = prev
1203 .as_ref()
1204 .is_some_and(|p| p.get(i).copied() == Some(ceil_val));
1205 (i, remainder, prev_used_ceil)
1206 })
1207 .collect();
1208
1209 priority.sort_by(|a, b| {
1211 b.1.partial_cmp(&a.1)
1212 .unwrap_or(std::cmp::Ordering::Equal)
1213 .then_with(|| {
1214 b.2.cmp(&a.2)
1216 })
1217 .then_with(|| {
1218 a.0.cmp(&b.0)
1220 })
1221 });
1222
1223 let mut result = floors;
1225 let mut remaining_deficit = deficit;
1226
1227 if remaining_deficit as usize >= n {
1233 let per_item = remaining_deficit / (n as u16);
1234 for val in result.iter_mut() {
1235 *val = val.saturating_add(per_item);
1236 }
1237 remaining_deficit %= n as u16;
1238 }
1239
1240 if remaining_deficit > 0 {
1241 for &(i, _, _) in priority.iter().take(remaining_deficit as usize) {
1242 result[i] = result[i].saturating_add(1);
1243 }
1244 }
1245
1246 result
1247}
1248
1249fn redistribute_overflow(floors: &[u16], total: u16) -> Sizes {
1254 let mut result: Sizes = floors.iter().copied().collect();
1255 let current_sum: u64 = result.iter().map(|&x| u64::from(x)).sum();
1256 let total_u64 = u64::from(total);
1257 let n = result.len();
1258
1259 if current_sum <= total_u64 || n == 0 {
1260 return result;
1261 }
1262
1263 let mut overflow = current_sum - total_u64;
1264
1265 while overflow > 0 {
1266 let &max_val = result.iter().max().unwrap_or(&0);
1267 if max_val == 0 {
1268 for val in result.iter_mut() {
1272 *val = 0;
1273 }
1274 break;
1275 }
1276
1277 let count_max = result.iter().filter(|&&v| v == max_val).count() as u64;
1278 let &next_max = result.iter().filter(|&&v| v < max_val).max().unwrap_or(&0);
1279
1280 let delta = (max_val - next_max) as u64;
1281 let required_per_item = overflow.div_ceil(count_max);
1282 let reduce_per_item = delta.min(required_per_item).max(1) as u16;
1283
1284 let mut reduced_any = false;
1285 for val in result.iter_mut() {
1286 if *val == max_val {
1287 let amount = u64::from(*val)
1288 .min(u64::from(reduce_per_item))
1289 .min(overflow) as u16;
1290 if amount > 0 {
1291 *val -= amount;
1292 overflow -= u64::from(amount);
1293 reduced_any = true;
1294 }
1295 if overflow == 0 {
1296 break;
1297 }
1298 }
1299 }
1300
1301 if !reduced_any {
1302 for val in result.iter_mut() {
1304 if overflow == 0 {
1305 break;
1306 }
1307 if *val > 0 {
1308 *val -= 1;
1309 overflow -= 1;
1310 }
1311 }
1312 break;
1313 }
1314 }
1315
1316 result
1317}
1318
1319#[cfg(test)]
1320mod tests {
1321 use super::*;
1322
1323 #[test]
1324 fn fixed_split() {
1325 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(20)]);
1326 let rects = flex.split(Rect::new(0, 0, 100, 10));
1327 assert_eq!(rects.len(), 2);
1328 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1329 assert_eq!(rects[1], Rect::new(10, 0, 20, 10)); }
1331
1332 #[test]
1333 fn percentage_split() {
1334 let flex = Flex::horizontal()
1335 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1336 let rects = flex.split(Rect::new(0, 0, 100, 10));
1337 assert_eq!(rects[0].width, 50);
1338 assert_eq!(rects[1].width, 50);
1339 }
1340
1341 #[test]
1342 fn gap_handling() {
1343 let flex = Flex::horizontal()
1344 .gap(5)
1345 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1346 let rects = flex.split(Rect::new(0, 0, 100, 10));
1347 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1351 assert_eq!(rects[1], Rect::new(15, 0, 10, 10));
1352 }
1353
1354 #[test]
1355 fn mixed_constraints() {
1356 let flex = Flex::horizontal().constraints([
1357 Constraint::Fixed(10),
1358 Constraint::Min(10), Constraint::Percentage(10.0), ]);
1361
1362 let rects = flex.split(Rect::new(0, 0, 100, 1));
1370 assert_eq!(rects[0].width, 10); assert_eq!(rects[2].width, 10); assert_eq!(rects[1].width, 80); }
1374
1375 #[test]
1376 fn measurement_fixed_constraints() {
1377 let fixed = Measurement::fixed(5, 7);
1378 assert_eq!(fixed.min_width, 5);
1379 assert_eq!(fixed.min_height, 7);
1380 assert_eq!(fixed.max_width, Some(5));
1381 assert_eq!(fixed.max_height, Some(7));
1382 }
1383
1384 #[test]
1385 fn measurement_flexible_constraints() {
1386 let flexible = Measurement::flexible(2, 3);
1387 assert_eq!(flexible.min_width, 2);
1388 assert_eq!(flexible.min_height, 3);
1389 assert_eq!(flexible.max_width, None);
1390 assert_eq!(flexible.max_height, None);
1391 }
1392
1393 #[test]
1394 fn breakpoints_classify_defaults() {
1395 let bp = Breakpoints::DEFAULT;
1396 assert_eq!(bp.classify_width(20), Breakpoint::Xs);
1397 assert_eq!(bp.classify_width(60), Breakpoint::Sm);
1398 assert_eq!(bp.classify_width(90), Breakpoint::Md);
1399 assert_eq!(bp.classify_width(120), Breakpoint::Lg);
1400 }
1401
1402 #[test]
1403 fn breakpoints_at_least_and_between() {
1404 let bp = Breakpoints::new(50, 80, 110);
1405 assert!(bp.at_least(85, Breakpoint::Sm));
1406 assert!(bp.between(85, Breakpoint::Sm, Breakpoint::Md));
1407 assert!(!bp.between(85, Breakpoint::Lg, Breakpoint::Lg));
1408 }
1409
1410 #[test]
1411 fn alignment_end() {
1412 let flex = Flex::horizontal()
1413 .alignment(Alignment::End)
1414 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1415 let rects = flex.split(Rect::new(0, 0, 100, 10));
1416 assert_eq!(rects[0], Rect::new(80, 0, 10, 10));
1418 assert_eq!(rects[1], Rect::new(90, 0, 10, 10));
1419 }
1420
1421 #[test]
1422 fn alignment_center() {
1423 let flex = Flex::horizontal()
1424 .alignment(Alignment::Center)
1425 .constraints([Constraint::Fixed(20), Constraint::Fixed(20)]);
1426 let rects = flex.split(Rect::new(0, 0, 100, 10));
1427 assert_eq!(rects[0], Rect::new(30, 0, 20, 10));
1429 assert_eq!(rects[1], Rect::new(50, 0, 20, 10));
1430 }
1431
1432 #[test]
1433 fn alignment_space_between() {
1434 let flex = Flex::horizontal()
1435 .alignment(Alignment::SpaceBetween)
1436 .constraints([
1437 Constraint::Fixed(10),
1438 Constraint::Fixed(10),
1439 Constraint::Fixed(10),
1440 ]);
1441 let rects = flex.split(Rect::new(0, 0, 100, 10));
1442 assert_eq!(rects[0].x, 0);
1444 assert_eq!(rects[1].x, 45); assert_eq!(rects[2].x, 90); }
1447
1448 #[test]
1449 fn vertical_alignment() {
1450 let flex = Flex::vertical()
1451 .alignment(Alignment::End)
1452 .constraints([Constraint::Fixed(5), Constraint::Fixed(5)]);
1453 let rects = flex.split(Rect::new(0, 0, 10, 100));
1454 assert_eq!(rects[0], Rect::new(0, 90, 10, 5));
1456 assert_eq!(rects[1], Rect::new(0, 95, 10, 5));
1457 }
1458
1459 #[test]
1460 fn nested_flex_support() {
1461 let outer = Flex::horizontal()
1463 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1464 let outer_rects = outer.split(Rect::new(0, 0, 100, 100));
1465
1466 let inner = Flex::vertical().constraints([Constraint::Fixed(30), Constraint::Min(10)]);
1468 let inner_rects = inner.split(outer_rects[0]);
1469
1470 assert_eq!(inner_rects[0], Rect::new(0, 0, 50, 30));
1471 assert_eq!(inner_rects[1], Rect::new(0, 30, 50, 70));
1472 }
1473
1474 #[test]
1476 fn invariant_total_size_does_not_exceed_available() {
1477 for total in [10u16, 50, 100, 255] {
1479 let flex = Flex::horizontal().constraints([
1480 Constraint::Fixed(30),
1481 Constraint::Percentage(50.0),
1482 Constraint::Min(20),
1483 ]);
1484 let rects = flex.split(Rect::new(0, 0, total, 10));
1485 let total_width: u16 = rects.iter().map(|r| r.width).sum();
1486 assert!(
1487 total_width <= total,
1488 "Total width {} exceeded available {} for constraints",
1489 total_width,
1490 total
1491 );
1492 }
1493 }
1494
1495 #[test]
1496 fn invariant_empty_area_produces_empty_rects() {
1497 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1498 let rects = flex.split(Rect::new(0, 0, 0, 0));
1499 assert!(rects.iter().all(|r| r.is_empty()));
1500 }
1501
1502 #[test]
1503 fn invariant_no_constraints_produces_empty_vec() {
1504 let flex = Flex::horizontal().constraints([]);
1505 let rects = flex.split(Rect::new(0, 0, 100, 100));
1506 assert!(rects.is_empty());
1507 }
1508
1509 #[test]
1512 fn ratio_constraint_splits_proportionally() {
1513 let flex =
1514 Flex::horizontal().constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
1515 let rects = flex.split(Rect::new(0, 0, 90, 10));
1516 assert_eq!(rects[0].width, 30);
1517 assert_eq!(rects[1].width, 60);
1518 }
1519
1520 #[test]
1521 fn ratio_constraint_with_zero_denominator() {
1522 let flex = Flex::horizontal().constraints([Constraint::Ratio(1, 0)]);
1524 let rects = flex.split(Rect::new(0, 0, 100, 10));
1525 assert_eq!(rects.len(), 1);
1526 }
1527
1528 #[test]
1529 fn ratio_is_absolute_fraction() {
1530 let area = Rect::new(0, 0, 100, 1);
1531
1532 let rects = Flex::horizontal()
1534 .constraints([Constraint::Percentage(25.0)])
1535 .split(area);
1536 assert_eq!(rects[0].width, 25);
1537
1538 let rects = Flex::horizontal()
1541 .constraints([Constraint::Ratio(1, 4)])
1542 .split(area);
1543 assert_eq!(rects[0].width, 25);
1544 }
1545
1546 #[test]
1547 fn ratio_is_independent_of_grow_items() {
1548 let area = Rect::new(0, 0, 100, 1);
1549
1550 let rects = Flex::horizontal()
1552 .constraints([Constraint::Ratio(1, 4), Constraint::Fill])
1553 .split(area);
1554 assert_eq!(rects[0].width, 25);
1555 assert_eq!(rects[1].width, 75);
1556 }
1557
1558 #[test]
1559 fn ratio_zero_numerator_should_be_zero() {
1560 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
1563 let rects = flex.split(Rect::new(0, 0, 100, 1));
1564
1565 assert_eq!(rects[0].width, 100, "Fill should take all space");
1567 assert_eq!(rects[1].width, 0, "Ratio(0, 1) should be width 0");
1568 }
1569
1570 #[test]
1573 fn max_constraint_clamps_size() {
1574 let flex = Flex::horizontal().constraints([Constraint::Max(20), Constraint::Fixed(30)]);
1575 let rects = flex.split(Rect::new(0, 0, 100, 10));
1576 assert!(rects[0].width <= 20);
1577 assert_eq!(rects[1].width, 30);
1578 }
1579
1580 #[test]
1581 fn percentage_rounding_never_exceeds_available() {
1582 let constraints = [
1583 Constraint::Percentage(33.4),
1584 Constraint::Percentage(33.3),
1585 Constraint::Percentage(33.3),
1586 ];
1587 let sizes = solve_constraints(&constraints, 7);
1588 let total: u16 = sizes.iter().sum();
1589 assert!(total <= 7, "percent rounding overflowed: {sizes:?}");
1590 assert!(sizes.iter().all(|size| *size <= 7));
1591 }
1592
1593 #[test]
1594 fn tiny_area_saturates_fixed_and_min() {
1595 let constraints = [Constraint::Fixed(5), Constraint::Min(3), Constraint::Max(2)];
1596 let sizes = solve_constraints(&constraints, 2);
1597 assert_eq!(sizes[0], 2);
1598 assert_eq!(sizes[1], 0);
1599 assert_eq!(sizes[2], 0);
1600 assert_eq!(sizes.iter().sum::<u16>(), 2);
1601 }
1602
1603 #[test]
1604 fn ratio_distribution_sums_to_available() {
1605 let constraints = [Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)];
1607 let sizes = solve_constraints(&constraints, 5);
1608 assert_eq!(sizes.iter().sum::<u16>(), 4);
1609 assert_eq!(sizes[0], 1);
1610 assert_eq!(sizes[1], 3);
1611 }
1612
1613 #[test]
1614 fn flex_gap_exceeds_area_yields_zero_widths() {
1615 let flex = Flex::horizontal()
1616 .gap(5)
1617 .constraints([Constraint::Fixed(1), Constraint::Fixed(1)]);
1618 let rects = flex.split(Rect::new(0, 0, 3, 1));
1619 assert_eq!(rects.len(), 2);
1620 assert_eq!(rects[0].width, 0);
1621 assert_eq!(rects[1].width, 0);
1622 }
1623
1624 #[test]
1627 fn alignment_space_around() {
1628 let flex = Flex::horizontal()
1629 .alignment(Alignment::SpaceAround)
1630 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1631 let rects = flex.split(Rect::new(0, 0, 100, 10));
1632
1633 assert_eq!(rects[0].x, 20);
1636 assert_eq!(rects[1].x, 70);
1637 }
1638
1639 #[test]
1642 fn vertical_gap() {
1643 let flex = Flex::vertical()
1644 .gap(5)
1645 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1646 let rects = flex.split(Rect::new(0, 0, 50, 100));
1647 assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
1648 assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
1649 }
1650
1651 #[test]
1654 fn vertical_center() {
1655 let flex = Flex::vertical()
1656 .alignment(Alignment::Center)
1657 .constraints([Constraint::Fixed(10)]);
1658 let rects = flex.split(Rect::new(0, 0, 50, 100));
1659 assert_eq!(rects[0].y, 45);
1661 assert_eq!(rects[0].height, 10);
1662 }
1663
1664 #[test]
1667 fn single_min_takes_all() {
1668 let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
1669 let rects = flex.split(Rect::new(0, 0, 80, 24));
1670 assert_eq!(rects[0].width, 80);
1671 }
1672
1673 #[test]
1676 fn fixed_exceeds_available_clamped() {
1677 let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
1678 let rects = flex.split(Rect::new(0, 0, 100, 10));
1679 assert_eq!(rects[0].width, 60);
1681 assert_eq!(rects[1].width, 40);
1682 }
1683
1684 #[test]
1687 fn percentage_overflow_clamped() {
1688 let flex = Flex::horizontal()
1689 .constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
1690 let rects = flex.split(Rect::new(0, 0, 100, 10));
1691 assert_eq!(rects[0].width, 80);
1692 assert_eq!(rects[1].width, 20); }
1694
1695 #[test]
1698 fn margin_reduces_split_area() {
1699 let flex = Flex::horizontal()
1700 .margin(Sides::all(10))
1701 .constraints([Constraint::Fixed(20), Constraint::Min(0)]);
1702 let rects = flex.split(Rect::new(0, 0, 100, 100));
1703 assert_eq!(rects[0].x, 10);
1705 assert_eq!(rects[0].y, 10);
1706 assert_eq!(rects[0].width, 20);
1707 assert_eq!(rects[0].height, 80);
1708 }
1709
1710 #[test]
1713 fn builder_methods_chain() {
1714 let flex = Flex::vertical()
1715 .direction(Direction::Horizontal)
1716 .gap(3)
1717 .margin(Sides::all(1))
1718 .alignment(Alignment::End)
1719 .constraints([Constraint::Fixed(10)]);
1720 let rects = flex.split(Rect::new(0, 0, 50, 50));
1721 assert_eq!(rects.len(), 1);
1722 }
1723
1724 #[test]
1727 fn space_between_single_item() {
1728 let flex = Flex::horizontal()
1729 .alignment(Alignment::SpaceBetween)
1730 .constraints([Constraint::Fixed(10)]);
1731 let rects = flex.split(Rect::new(0, 0, 100, 10));
1732 assert_eq!(rects[0].x, 0);
1734 assert_eq!(rects[0].width, 10);
1735 }
1736
1737 #[test]
1738 fn invariant_rects_within_bounds() {
1739 let area = Rect::new(10, 20, 80, 60);
1740 let flex = Flex::horizontal()
1741 .margin(Sides::all(5))
1742 .gap(2)
1743 .constraints([
1744 Constraint::Fixed(15),
1745 Constraint::Percentage(30.0),
1746 Constraint::Min(10),
1747 ]);
1748 let rects = flex.split(area);
1749
1750 let inner = area.inner(Sides::all(5));
1752 for rect in &rects {
1753 assert!(
1754 rect.x >= inner.x && rect.right() <= inner.right(),
1755 "Rect {:?} exceeds horizontal bounds of {:?}",
1756 rect,
1757 inner
1758 );
1759 assert!(
1760 rect.y >= inner.y && rect.bottom() <= inner.bottom(),
1761 "Rect {:?} exceeds vertical bounds of {:?}",
1762 rect,
1763 inner
1764 );
1765 }
1766 }
1767
1768 #[test]
1771 fn fill_takes_remaining_space() {
1772 let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
1773 let rects = flex.split(Rect::new(0, 0, 100, 10));
1774 assert_eq!(rects[0].width, 20);
1775 assert_eq!(rects[1].width, 80); }
1777
1778 #[test]
1779 fn multiple_fills_share_space() {
1780 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
1781 let rects = flex.split(Rect::new(0, 0, 100, 10));
1782 assert_eq!(rects[0].width, 50);
1783 assert_eq!(rects[1].width, 50);
1784 }
1785
1786 #[test]
1789 fn fit_content_uses_preferred_size() {
1790 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1791 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1792 if idx == 0 {
1793 LayoutSizeHint {
1794 min: 5,
1795 preferred: 30,
1796 max: None,
1797 }
1798 } else {
1799 LayoutSizeHint::ZERO
1800 }
1801 });
1802 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
1805
1806 #[test]
1807 fn fit_content_clamps_to_available() {
1808 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
1809 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1810 min: 5,
1811 preferred: 80,
1812 max: None,
1813 });
1814 assert_eq!(rects[0].width, 80);
1816 assert_eq!(rects[1].width, 20);
1817 }
1818
1819 #[test]
1820 fn fit_content_without_measurer_gets_zero() {
1821 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1823 let rects = flex.split(Rect::new(0, 0, 100, 10));
1824 assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
1827
1828 #[test]
1829 fn fit_content_zero_area_returns_empty_rects() {
1830 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1831 let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
1832 min: 5,
1833 preferred: 10,
1834 max: None,
1835 });
1836 assert_eq!(rects.len(), 2);
1837 assert_eq!(rects[0].width, 0);
1838 assert_eq!(rects[0].height, 0);
1839 assert_eq!(rects[1].width, 0);
1840 assert_eq!(rects[1].height, 0);
1841 }
1842
1843 #[test]
1844 fn fit_content_tiny_available_clamps_to_remaining() {
1845 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1846 let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
1847 min: 5,
1848 preferred: 10,
1849 max: None,
1850 });
1851 assert_eq!(rects[0].width, 1);
1852 assert_eq!(rects[1].width, 0);
1853 }
1854
1855 #[test]
1858 fn fit_content_bounded_clamps_to_min() {
1859 let flex = Flex::horizontal().constraints([
1860 Constraint::FitContentBounded { min: 20, max: 50 },
1861 Constraint::Fill,
1862 ]);
1863 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1864 min: 5,
1865 preferred: 10, max: None,
1867 });
1868 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
1870 }
1871
1872 #[test]
1873 fn fit_content_bounded_respects_small_available() {
1874 let flex = Flex::horizontal().constraints([
1875 Constraint::FitContentBounded { min: 20, max: 50 },
1876 Constraint::Fill,
1877 ]);
1878 let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
1879 min: 5,
1880 preferred: 10,
1881 max: None,
1882 });
1883 assert_eq!(rects[0].width, 5);
1885 assert_eq!(rects[1].width, 0);
1886 }
1887
1888 #[test]
1889 fn fit_content_bounded_clamps_to_max() {
1890 let flex = Flex::horizontal().constraints([
1891 Constraint::FitContentBounded { min: 10, max: 30 },
1892 Constraint::Fill,
1893 ]);
1894 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1895 min: 5,
1896 preferred: 50, max: None,
1898 });
1899 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
1901 }
1902
1903 #[test]
1904 fn fit_content_bounded_uses_preferred_when_in_range() {
1905 let flex = Flex::horizontal().constraints([
1906 Constraint::FitContentBounded { min: 10, max: 50 },
1907 Constraint::Fill,
1908 ]);
1909 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1910 min: 5,
1911 preferred: 35, max: None,
1913 });
1914 assert_eq!(rects[0].width, 35);
1915 assert_eq!(rects[1].width, 65);
1916 }
1917
1918 #[test]
1921 fn fit_min_uses_minimum_size() {
1922 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1923 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1924 if idx == 0 {
1925 LayoutSizeHint {
1926 min: 15,
1927 preferred: 40,
1928 max: None,
1929 }
1930 } else {
1931 LayoutSizeHint::ZERO
1932 }
1933 });
1934 assert_eq!(rects[0].width, 15, "FitMin should strict size to min");
1937 assert_eq!(rects[1].width, 85, "Fill should take remaining space");
1938 }
1939
1940 #[test]
1941 fn fit_min_without_measurer_gets_zero() {
1942 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1943 let rects = flex.split(Rect::new(0, 0, 100, 10));
1944 assert_eq!(rects[0].width, 0);
1947 assert_eq!(rects[1].width, 100);
1948 }
1949
1950 #[test]
1953 fn layout_size_hint_zero_is_default() {
1954 assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
1955 }
1956
1957 #[test]
1958 fn layout_size_hint_exact() {
1959 let h = LayoutSizeHint::exact(25);
1960 assert_eq!(h.min, 25);
1961 assert_eq!(h.preferred, 25);
1962 assert_eq!(h.max, Some(25));
1963 }
1964
1965 #[test]
1966 fn layout_size_hint_at_least() {
1967 let h = LayoutSizeHint::at_least(10, 30);
1968 assert_eq!(h.min, 10);
1969 assert_eq!(h.preferred, 30);
1970 assert_eq!(h.max, None);
1971 }
1972
1973 #[test]
1974 fn layout_size_hint_clamp() {
1975 let h = LayoutSizeHint {
1976 min: 10,
1977 preferred: 20,
1978 max: Some(30),
1979 };
1980 assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
1984
1985 #[test]
1986 fn layout_size_hint_clamp_unbounded() {
1987 let h = LayoutSizeHint::at_least(5, 10);
1988 assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
1991
1992 #[test]
1993 fn layout_size_hint_clamp_min_greater_than_max() {
1994 let h = LayoutSizeHint {
1996 min: 20,
1997 preferred: 20,
1998 max: Some(10),
1999 };
2000 assert_eq!(h.clamp(5), 20); assert_eq!(h.clamp(15), 20); assert_eq!(h.clamp(25), 20); }
2004
2005 #[test]
2008 fn fit_content_with_fixed_and_fill() {
2009 let flex = Flex::horizontal().constraints([
2010 Constraint::Fixed(20),
2011 Constraint::FitContent,
2012 Constraint::Fill,
2013 ]);
2014 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
2015 if idx == 1 {
2016 LayoutSizeHint {
2017 min: 5,
2018 preferred: 25,
2019 max: None,
2020 }
2021 } else {
2022 LayoutSizeHint::ZERO
2023 }
2024 });
2025 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
2029
2030 #[test]
2031 fn total_allocation_never_exceeds_available_with_fit_content() {
2032 for available in [10u16, 50, 100, 255] {
2033 let flex = Flex::horizontal().constraints([
2034 Constraint::FitContent,
2035 Constraint::FitContent,
2036 Constraint::Fill,
2037 ]);
2038 let rects =
2039 flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
2040 min: 10,
2041 preferred: 40,
2042 max: None,
2043 });
2044 let total: u16 = rects.iter().map(|r| r.width).sum();
2045 assert!(
2046 total <= available,
2047 "Total {} exceeded available {} with FitContent",
2048 total,
2049 available
2050 );
2051 }
2052 }
2053
2054 mod rounding_tests {
2059 use super::super::*;
2060
2061 #[test]
2064 fn rounding_conserves_sum_exact() {
2065 let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
2066 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2067 assert_eq!(result.as_slice(), &[10u16, 20u16, 10u16]);
2068 }
2069
2070 #[test]
2071 fn rounding_conserves_sum_fractional() {
2072 let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
2073 assert_eq!(
2074 result.iter().copied().sum::<u16>(),
2075 40,
2076 "Sum must equal total: {:?}",
2077 result
2078 );
2079 }
2080
2081 #[test]
2082 fn rounding_conserves_sum_many_fractions() {
2083 let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
2084 let result = round_layout_stable(&targets, 100, None);
2085 assert_eq!(
2086 result.iter().copied().sum::<u16>(),
2087 100,
2088 "Sum must be exactly 100: {:?}",
2089 result
2090 );
2091 }
2092
2093 #[test]
2094 fn rounding_conserves_sum_all_half() {
2095 let targets = vec![10.5, 10.5, 10.5, 10.5];
2096 let result = round_layout_stable(&targets, 42, None);
2097 assert_eq!(
2098 result.iter().copied().sum::<u16>(),
2099 42,
2100 "Sum must be exactly 42: {:?}",
2101 result
2102 );
2103 }
2104
2105 #[test]
2108 fn rounding_displacement_bounded() {
2109 let targets = vec![33.33, 33.33, 33.34];
2110 let result = round_layout_stable(&targets, 100, None);
2111 assert_eq!(result.iter().copied().sum::<u16>(), 100);
2112
2113 for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
2114 let floor = r.floor() as u16;
2115 let ceil = floor + 1;
2116 assert!(
2117 x == floor || x == ceil,
2118 "Element {} = {} not in {{floor={}, ceil={}}} of target {}",
2119 i,
2120 x,
2121 floor,
2122 ceil,
2123 r
2124 );
2125 }
2126 }
2127
2128 #[test]
2131 fn temporal_tiebreak_stable_when_unchanged() {
2132 let targets = vec![10.5, 10.5, 10.5, 10.5];
2133 let first = round_layout_stable(&targets, 42, None);
2134 let second = round_layout_stable(&targets, 42, Some(first.clone()));
2135 assert_eq!(
2136 first, second,
2137 "Identical targets should produce identical results"
2138 );
2139 }
2140
2141 #[test]
2142 fn temporal_tiebreak_prefers_previous_direction() {
2143 let targets = vec![10.5, 10.5];
2144 let total = 21;
2145 let first = round_layout_stable(&targets, total, None);
2146 assert_eq!(first.iter().copied().sum::<u16>(), total);
2147 let second = round_layout_stable(&targets, total, Some(first.clone()));
2148 assert_eq!(first, second, "Should maintain rounding direction");
2149 }
2150
2151 #[test]
2152 fn temporal_tiebreak_adapts_to_changed_targets() {
2153 let targets_a = vec![10.5, 10.5];
2154 let result_a = round_layout_stable(&targets_a, 21, None);
2155 let targets_b = vec![15.7, 5.3];
2156 let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
2157 assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
2158 assert!(result_b[0] > result_b[1], "Should follow larger target");
2159 }
2160
2161 #[test]
2164 fn property_min_displacement_brute_force_small() {
2165 let targets = vec![3.3, 3.3, 3.4];
2166 let total: u16 = 10;
2167 let result = round_layout_stable(&targets, total, None);
2168 let our_displacement: f64 = result
2169 .iter()
2170 .zip(targets.iter())
2171 .map(|(&x, &r)| (x as f64 - r).abs())
2172 .sum();
2173
2174 let mut min_displacement = f64::MAX;
2175 let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
2176 let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
2177
2178 for a in floors[0]..=ceils[0] {
2179 for b in floors[1]..=ceils[1] {
2180 for c in floors[2]..=ceils[2] {
2181 if a + b + c == total {
2182 let disp = (a as f64 - targets[0]).abs()
2183 + (b as f64 - targets[1]).abs()
2184 + (c as f64 - targets[2]).abs();
2185 if disp < min_displacement {
2186 min_displacement = disp;
2187 }
2188 }
2189 }
2190 }
2191 }
2192
2193 assert!(
2194 (our_displacement - min_displacement).abs() < 1e-10,
2195 "Our displacement {} should match optimal {}: {:?}",
2196 our_displacement,
2197 min_displacement,
2198 result
2199 );
2200 }
2201
2202 #[test]
2205 fn rounding_deterministic() {
2206 let targets = vec![7.7, 8.3, 14.0];
2207 let a = round_layout_stable(&targets, 30, None);
2208 let b = round_layout_stable(&targets, 30, None);
2209 assert_eq!(a, b, "Same inputs must produce identical outputs");
2210 }
2211
2212 #[test]
2215 fn rounding_empty_targets() {
2216 let result = round_layout_stable(&[], 0, None);
2217 assert!(result.is_empty());
2218 }
2219
2220 #[test]
2221 fn rounding_single_element() {
2222 let result = round_layout_stable(&[10.7], 11, None);
2223 assert_eq!(result.as_slice(), &[11u16]);
2224 }
2225
2226 #[test]
2227 fn rounding_zero_total() {
2228 let result = round_layout_stable(&[5.0, 5.0], 0, None);
2229 assert_eq!(result.iter().copied().sum::<u16>(), 0);
2230 }
2231
2232 #[test]
2233 fn rounding_zero_total_with_large_overflow_reaches_zero() {
2234 let result = round_layout_stable(&[65535.0, 65535.0], 0, None);
2235 assert_eq!(result.as_slice(), &[0u16, 0u16]);
2236 assert_eq!(result.iter().copied().sum::<u16>(), 0);
2237 }
2238
2239 #[test]
2240 fn rounding_all_zeros() {
2241 let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
2242 assert_eq!(result.as_slice(), &[0u16, 0u16, 0u16]);
2243 }
2244
2245 #[test]
2246 fn rounding_integer_targets() {
2247 let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
2248 assert_eq!(result.as_slice(), &[10u16, 20u16, 30u16]);
2249 }
2250
2251 #[test]
2252 fn rounding_large_deficit() {
2253 let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
2254 assert_eq!(result.iter().copied().sum::<u16>(), 3);
2255 assert_eq!(result.as_slice(), &[1u16, 1u16, 1u16]);
2256 }
2257
2258 #[test]
2259 fn rounding_with_prev_different_length() {
2260 let result = round_layout_stable(
2261 &[10.5, 10.5],
2262 21,
2263 Some(smallvec::smallvec![11u16, 10u16, 5u16]),
2264 );
2265 assert_eq!(result.iter().copied().sum::<u16>(), 21);
2266 }
2267
2268 #[test]
2269 fn rounding_very_small_fractions() {
2270 let targets = vec![10.001, 20.001, 9.998];
2271 let result = round_layout_stable(&targets, 40, None);
2272 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2273 }
2274
2275 #[test]
2276 fn rounding_conserves_sum_stress() {
2277 let n = 50;
2278 let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
2279 let total = 120u16;
2280 let result = round_layout_stable(&targets, total, None);
2281 assert_eq!(
2282 result.iter().copied().sum::<u16>(),
2283 total,
2284 "Sum must be exactly {} for {} items: {:?}",
2285 total,
2286 n,
2287 result
2288 );
2289 }
2290 }
2291
2292 mod property_constraint_tests {
2297 use super::super::*;
2298
2299 struct Lcg(u64);
2301
2302 impl Lcg {
2303 fn new(seed: u64) -> Self {
2304 Self(seed)
2305 }
2306 fn next_u32(&mut self) -> u32 {
2307 self.0 = self
2308 .0
2309 .wrapping_mul(6_364_136_223_846_793_005)
2310 .wrapping_add(1);
2311 (self.0 >> 33) as u32
2312 }
2313 fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
2314 if lo >= hi {
2315 return lo;
2316 }
2317 lo + (self.next_u32() % (hi - lo) as u32) as u16
2318 }
2319 fn next_f32(&mut self) -> f32 {
2320 (self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
2321 }
2322 }
2323
2324 fn random_constraint(rng: &mut Lcg) -> Constraint {
2326 match rng.next_u32() % 7 {
2327 0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
2328 1 => Constraint::Percentage(rng.next_f32() * 100.0),
2329 2 => Constraint::Min(rng.next_u16_range(0, 40)),
2330 3 => Constraint::Max(rng.next_u16_range(5, 120)),
2331 4 => {
2332 let n = rng.next_u32() % 5 + 1;
2333 let d = rng.next_u32() % 5 + 1;
2334 Constraint::Ratio(n, d)
2335 }
2336 5 => Constraint::Fill,
2337 _ => Constraint::FitContent,
2338 }
2339 }
2340
2341 #[test]
2342 fn property_constraints_respected_fixed() {
2343 let mut rng = Lcg::new(0xDEAD_BEEF);
2344 for _ in 0..200 {
2345 let fixed_val = rng.next_u16_range(1, 60);
2346 let avail = rng.next_u16_range(10, 200);
2347 let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
2348 let rects = flex.split(Rect::new(0, 0, avail, 10));
2349 assert!(
2350 rects[0].width <= fixed_val.min(avail),
2351 "Fixed({}) in avail {} -> width {}",
2352 fixed_val,
2353 avail,
2354 rects[0].width
2355 );
2356 }
2357 }
2358
2359 #[test]
2360 fn property_constraints_respected_max() {
2361 let mut rng = Lcg::new(0xCAFE_BABE);
2362 for _ in 0..200 {
2363 let max_val = rng.next_u16_range(5, 80);
2364 let avail = rng.next_u16_range(10, 200);
2365 let flex =
2366 Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
2367 let rects = flex.split(Rect::new(0, 0, avail, 10));
2368 assert!(
2369 rects[0].width <= max_val,
2370 "Max({}) in avail {} -> width {}",
2371 max_val,
2372 avail,
2373 rects[0].width
2374 );
2375 }
2376 }
2377
2378 #[test]
2379 fn property_constraints_respected_min() {
2380 let mut rng = Lcg::new(0xBAAD_F00D);
2381 for _ in 0..200 {
2382 let min_val = rng.next_u16_range(0, 40);
2383 let avail = rng.next_u16_range(min_val.max(1), 200);
2384 let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
2385 let rects = flex.split(Rect::new(0, 0, avail, 10));
2386 assert!(
2387 rects[0].width >= min_val,
2388 "Min({}) in avail {} -> width {}",
2389 min_val,
2390 avail,
2391 rects[0].width
2392 );
2393 }
2394 }
2395
2396 #[test]
2397 fn property_constraints_respected_ratio_proportional() {
2398 let mut rng = Lcg::new(0x1234_5678);
2399 for _ in 0..200 {
2400 let n1 = rng.next_u32() % 5 + 1;
2401 let n2 = rng.next_u32() % 5 + 1;
2402 let d = n1 + n2;
2403 let avail = rng.next_u16_range(20, 200);
2404 let flex = Flex::horizontal()
2405 .constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
2406 let rects = flex.split(Rect::new(0, 0, avail, 10));
2407 let w1 = rects[0].width as f64;
2408 let w2 = rects[1].width as f64;
2409 let total = w1 + w2;
2410 if total > 0.0 {
2411 let expected_ratio = n1 as f64 / d as f64;
2412 let actual_ratio = w1 / total;
2413 assert!(
2414 (actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
2415 "Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
2416 n1,
2417 d,
2418 n1,
2419 n2,
2420 avail,
2421 expected_ratio,
2422 actual_ratio,
2423 w1,
2424 w2
2425 );
2426 }
2427 }
2428 }
2429
2430 #[test]
2431 fn property_total_allocation_never_exceeds_available() {
2432 let mut rng = Lcg::new(0xFACE_FEED);
2433 for _ in 0..500 {
2434 let n = (rng.next_u32() % 6 + 1) as usize;
2435 let constraints: Vec<Constraint> =
2436 (0..n).map(|_| random_constraint(&mut rng)).collect();
2437 let avail = rng.next_u16_range(5, 200);
2438 let dir = if rng.next_u32().is_multiple_of(2) {
2439 Direction::Horizontal
2440 } else {
2441 Direction::Vertical
2442 };
2443 let flex = Flex::default().direction(dir).constraints(constraints);
2444 let area = Rect::new(0, 0, avail, avail);
2445 let rects = flex.split(area);
2446 let total: u16 = rects
2447 .iter()
2448 .map(|r| match dir {
2449 Direction::Horizontal => r.width,
2450 Direction::Vertical => r.height,
2451 })
2452 .sum();
2453 assert!(
2454 total <= avail,
2455 "Total {} exceeded available {} with {} constraints",
2456 total,
2457 avail,
2458 n
2459 );
2460 }
2461 }
2462
2463 #[test]
2464 fn property_no_overlap_horizontal() {
2465 let mut rng = Lcg::new(0xABCD_1234);
2466 for _ in 0..300 {
2467 let n = (rng.next_u32() % 5 + 2) as usize;
2468 let constraints: Vec<Constraint> =
2469 (0..n).map(|_| random_constraint(&mut rng)).collect();
2470 let avail = rng.next_u16_range(20, 200);
2471 let flex = Flex::horizontal().constraints(constraints);
2472 let rects = flex.split(Rect::new(0, 0, avail, 10));
2473
2474 for i in 1..rects.len() {
2475 let prev_end = rects[i - 1].x + rects[i - 1].width;
2476 assert!(
2477 rects[i].x >= prev_end,
2478 "Overlap at {}: prev ends {}, next starts {}",
2479 i,
2480 prev_end,
2481 rects[i].x
2482 );
2483 }
2484 }
2485 }
2486
2487 #[test]
2488 fn property_deterministic_across_runs() {
2489 let mut rng = Lcg::new(0x9999_8888);
2490 for _ in 0..100 {
2491 let n = (rng.next_u32() % 5 + 1) as usize;
2492 let constraints: Vec<Constraint> =
2493 (0..n).map(|_| random_constraint(&mut rng)).collect();
2494 let avail = rng.next_u16_range(10, 200);
2495 let r1 = Flex::horizontal()
2496 .constraints(constraints.clone())
2497 .split(Rect::new(0, 0, avail, 10));
2498 let r2 = Flex::horizontal()
2499 .constraints(constraints)
2500 .split(Rect::new(0, 0, avail, 10));
2501 assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
2502 }
2503 }
2504 }
2505
2506 mod property_temporal_tests {
2511 use super::super::*;
2512 use crate::cache::{CoherenceCache, CoherenceId};
2513
2514 struct Lcg(u64);
2516
2517 impl Lcg {
2518 fn new(seed: u64) -> Self {
2519 Self(seed)
2520 }
2521 fn next_u32(&mut self) -> u32 {
2522 self.0 = self
2523 .0
2524 .wrapping_mul(6_364_136_223_846_793_005)
2525 .wrapping_add(1);
2526 (self.0 >> 33) as u32
2527 }
2528 }
2529
2530 #[test]
2531 fn property_temporal_stability_small_resize() {
2532 let constraints = [
2533 Constraint::Percentage(33.3),
2534 Constraint::Percentage(33.3),
2535 Constraint::Fill,
2536 ];
2537 let mut coherence = CoherenceCache::new(64);
2538 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2539
2540 for total in [80u16, 100, 120] {
2541 let flex = Flex::horizontal().constraints(constraints);
2542 let rects = flex.split(Rect::new(0, 0, total, 10));
2543 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2544
2545 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2546 let prev = coherence.get(&id);
2547 let rounded = round_layout_stable(&targets, total, prev);
2548
2549 if let Some(old) = coherence.get(&id) {
2550 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2551 assert!(
2552 max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
2553 "max_disp={} too large for size change {} -> {}",
2554 max_disp,
2555 old.iter().copied().sum::<u16>(),
2556 total
2557 );
2558 let _ = sum_disp;
2559 }
2560 coherence.store(id, rounded);
2561 }
2562 }
2563
2564 #[test]
2565 fn property_temporal_stability_random_walk() {
2566 let constraints = [
2567 Constraint::Ratio(1, 3),
2568 Constraint::Ratio(1, 3),
2569 Constraint::Ratio(1, 3),
2570 ];
2571 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2572 let mut coherence = CoherenceCache::new(64);
2573 let mut rng = Lcg::new(0x5555_AAAA);
2574 let mut total: u16 = 90;
2575
2576 for step in 0..200 {
2577 let prev_total = total;
2578 let delta = (rng.next_u32() % 7) as i32 - 3;
2579 total = (total as i32 + delta).clamp(10, 250) as u16;
2580
2581 let flex = Flex::horizontal().constraints(constraints);
2582 let rects = flex.split(Rect::new(0, 0, total, 10));
2583 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2584
2585 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2586 let prev = coherence.get(&id);
2587 let rounded = round_layout_stable(&targets, total, prev);
2588
2589 if coherence.get(&id).is_some() {
2590 let (_, max_disp) = coherence.displacement(&id, &rounded);
2591 let size_change = total.abs_diff(prev_total);
2592 assert!(
2593 max_disp <= size_change as u32 + 2,
2594 "step {}: max_disp={} exceeds size_change={} + 2",
2595 step,
2596 max_disp,
2597 size_change
2598 );
2599 }
2600 coherence.store(id, rounded);
2601 }
2602 }
2603
2604 #[test]
2605 fn property_temporal_stability_identical_frames() {
2606 let constraints = [
2607 Constraint::Fixed(20),
2608 Constraint::Fill,
2609 Constraint::Fixed(15),
2610 ];
2611 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2612 let mut coherence = CoherenceCache::new(64);
2613
2614 let flex = Flex::horizontal().constraints(constraints);
2615 let rects = flex.split(Rect::new(0, 0, 100, 10));
2616 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2617 coherence.store(id, widths.iter().copied().collect());
2618
2619 for _ in 0..10 {
2620 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2621 let prev = coherence.get(&id);
2622 let rounded = round_layout_stable(&targets, 100, prev);
2623 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2624 assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
2625 coherence.store(id, rounded);
2626 }
2627 }
2628
2629 #[test]
2630 fn property_temporal_coherence_sweep() {
2631 let constraints = [
2632 Constraint::Percentage(25.0),
2633 Constraint::Percentage(50.0),
2634 Constraint::Fill,
2635 ];
2636 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2637 let mut coherence = CoherenceCache::new(64);
2638 let mut total_displacement: u64 = 0;
2639
2640 for total in 60u16..=140 {
2641 let flex = Flex::horizontal().constraints(constraints);
2642 let rects = flex.split(Rect::new(0, 0, total, 10));
2643 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2644
2645 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2646 let prev = coherence.get(&id);
2647 let rounded = round_layout_stable(&targets, total, prev);
2648
2649 if coherence.get(&id).is_some() {
2650 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2651 total_displacement += sum_disp;
2652 }
2653 coherence.store(id, rounded);
2654 }
2655
2656 assert!(
2657 total_displacement <= 80 * 3,
2658 "Total displacement {} exceeds bound for 80-step sweep",
2659 total_displacement
2660 );
2661 }
2662 }
2663
2664 mod snapshot_layout_tests {
2669 use super::super::*;
2670 use crate::grid::{Grid, GridArea};
2671
2672 fn snapshot_flex(
2673 constraints: &[Constraint],
2674 dir: Direction,
2675 width: u16,
2676 height: u16,
2677 ) -> String {
2678 let flex = Flex::default()
2679 .direction(dir)
2680 .constraints(constraints.iter().copied());
2681 let rects = flex.split(Rect::new(0, 0, width, height));
2682 let mut out = format!(
2683 "Flex {:?} {}x{} ({} constraints)\n",
2684 dir,
2685 width,
2686 height,
2687 constraints.len()
2688 );
2689 for (i, r) in rects.iter().enumerate() {
2690 out.push_str(&format!(
2691 " [{}] x={} y={} w={} h={}\n",
2692 i, r.x, r.y, r.width, r.height
2693 ));
2694 }
2695 let total: u16 = rects
2696 .iter()
2697 .map(|r| match dir {
2698 Direction::Horizontal => r.width,
2699 Direction::Vertical => r.height,
2700 })
2701 .sum();
2702 out.push_str(&format!(" total={}\n", total));
2703 out
2704 }
2705
2706 fn snapshot_grid(
2707 rows: &[Constraint],
2708 cols: &[Constraint],
2709 areas: &[(&str, GridArea)],
2710 width: u16,
2711 height: u16,
2712 ) -> String {
2713 let mut grid = Grid::new()
2714 .rows(rows.iter().copied())
2715 .columns(cols.iter().copied());
2716 for &(name, area) in areas {
2717 grid = grid.area(name, area);
2718 }
2719 let layout = grid.split(Rect::new(0, 0, width, height));
2720
2721 let mut out = format!(
2722 "Grid {}x{} ({}r x {}c)\n",
2723 width,
2724 height,
2725 rows.len(),
2726 cols.len()
2727 );
2728 for r in 0..rows.len() {
2729 for c in 0..cols.len() {
2730 let rect = layout.cell(r, c);
2731 out.push_str(&format!(
2732 " [{},{}] x={} y={} w={} h={}\n",
2733 r, c, rect.x, rect.y, rect.width, rect.height
2734 ));
2735 }
2736 }
2737 for &(name, _) in areas {
2738 if let Some(rect) = layout.area(name) {
2739 out.push_str(&format!(
2740 " area({}) x={} y={} w={} h={}\n",
2741 name, rect.x, rect.y, rect.width, rect.height
2742 ));
2743 }
2744 }
2745 out
2746 }
2747
2748 #[test]
2751 fn snapshot_flex_thirds_80x24() {
2752 let snap = snapshot_flex(
2753 &[
2754 Constraint::Ratio(1, 3),
2755 Constraint::Ratio(1, 3),
2756 Constraint::Ratio(1, 3),
2757 ],
2758 Direction::Horizontal,
2759 80,
2760 24,
2761 );
2762 assert_eq!(
2763 snap,
2764 "\
2765Flex Horizontal 80x24 (3 constraints)
2766 [0] x=0 y=0 w=26 h=24
2767 [1] x=26 y=0 w=26 h=24
2768 [2] x=52 y=0 w=26 h=24
2769 total=78
2770"
2771 );
2772 }
2773
2774 #[test]
2775 fn snapshot_flex_sidebar_content_80x24() {
2776 let snap = snapshot_flex(
2777 &[Constraint::Fixed(20), Constraint::Fill],
2778 Direction::Horizontal,
2779 80,
2780 24,
2781 );
2782 assert_eq!(
2783 snap,
2784 "\
2785Flex Horizontal 80x24 (2 constraints)
2786 [0] x=0 y=0 w=20 h=24
2787 [1] x=20 y=0 w=60 h=24
2788 total=80
2789"
2790 );
2791 }
2792
2793 #[test]
2794 fn snapshot_flex_header_body_footer_80x24() {
2795 let snap = snapshot_flex(
2796 &[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
2797 Direction::Vertical,
2798 80,
2799 24,
2800 );
2801 assert_eq!(
2802 snap,
2803 "\
2804Flex Vertical 80x24 (3 constraints)
2805 [0] x=0 y=0 w=80 h=3
2806 [1] x=0 y=3 w=80 h=20
2807 [2] x=0 y=23 w=80 h=1
2808 total=24
2809"
2810 );
2811 }
2812
2813 #[test]
2816 fn snapshot_flex_thirds_120x40() {
2817 let snap = snapshot_flex(
2818 &[
2819 Constraint::Ratio(1, 3),
2820 Constraint::Ratio(1, 3),
2821 Constraint::Ratio(1, 3),
2822 ],
2823 Direction::Horizontal,
2824 120,
2825 40,
2826 );
2827 assert_eq!(
2828 snap,
2829 "\
2830Flex Horizontal 120x40 (3 constraints)
2831 [0] x=0 y=0 w=40 h=40
2832 [1] x=40 y=0 w=40 h=40
2833 [2] x=80 y=0 w=40 h=40
2834 total=120
2835"
2836 );
2837 }
2838
2839 #[test]
2840 fn snapshot_flex_sidebar_content_120x40() {
2841 let snap = snapshot_flex(
2842 &[Constraint::Fixed(20), Constraint::Fill],
2843 Direction::Horizontal,
2844 120,
2845 40,
2846 );
2847 assert_eq!(
2848 snap,
2849 "\
2850Flex Horizontal 120x40 (2 constraints)
2851 [0] x=0 y=0 w=20 h=40
2852 [1] x=20 y=0 w=100 h=40
2853 total=120
2854"
2855 );
2856 }
2857
2858 #[test]
2859 fn snapshot_flex_percentage_mix_120x40() {
2860 let snap = snapshot_flex(
2861 &[
2862 Constraint::Percentage(25.0),
2863 Constraint::Percentage(50.0),
2864 Constraint::Fill,
2865 ],
2866 Direction::Horizontal,
2867 120,
2868 40,
2869 );
2870 assert_eq!(
2871 snap,
2872 "\
2873Flex Horizontal 120x40 (3 constraints)
2874 [0] x=0 y=0 w=30 h=40
2875 [1] x=30 y=0 w=60 h=40
2876 [2] x=90 y=0 w=30 h=40
2877 total=120
2878"
2879 );
2880 }
2881
2882 #[test]
2885 fn snapshot_grid_2x2_80x24() {
2886 let snap = snapshot_grid(
2887 &[Constraint::Fixed(3), Constraint::Fill],
2888 &[Constraint::Fixed(20), Constraint::Fill],
2889 &[
2890 ("header", GridArea::span(0, 0, 1, 2)),
2891 ("sidebar", GridArea::span(1, 0, 1, 1)),
2892 ("content", GridArea::cell(1, 1)),
2893 ],
2894 80,
2895 24,
2896 );
2897 assert_eq!(
2898 snap,
2899 "\
2900Grid 80x24 (2r x 2c)
2901 [0,0] x=0 y=0 w=20 h=3
2902 [0,1] x=20 y=0 w=60 h=3
2903 [1,0] x=0 y=3 w=20 h=21
2904 [1,1] x=20 y=3 w=60 h=21
2905 area(header) x=0 y=0 w=80 h=3
2906 area(sidebar) x=0 y=3 w=20 h=21
2907 area(content) x=20 y=3 w=60 h=21
2908"
2909 );
2910 }
2911
2912 #[test]
2913 fn snapshot_grid_3x3_80x24() {
2914 let snap = snapshot_grid(
2915 &[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
2916 &[
2917 Constraint::Fixed(10),
2918 Constraint::Fill,
2919 Constraint::Fixed(10),
2920 ],
2921 &[],
2922 80,
2923 24,
2924 );
2925 assert_eq!(
2926 snap,
2927 "\
2928Grid 80x24 (3r x 3c)
2929 [0,0] x=0 y=0 w=10 h=1
2930 [0,1] x=10 y=0 w=60 h=1
2931 [0,2] x=70 y=0 w=10 h=1
2932 [1,0] x=0 y=1 w=10 h=22
2933 [1,1] x=10 y=1 w=60 h=22
2934 [1,2] x=70 y=1 w=10 h=22
2935 [2,0] x=0 y=23 w=10 h=1
2936 [2,1] x=10 y=23 w=60 h=1
2937 [2,2] x=70 y=23 w=10 h=1
2938"
2939 );
2940 }
2941
2942 #[test]
2945 fn snapshot_grid_2x2_120x40() {
2946 let snap = snapshot_grid(
2947 &[Constraint::Fixed(3), Constraint::Fill],
2948 &[Constraint::Fixed(20), Constraint::Fill],
2949 &[
2950 ("header", GridArea::span(0, 0, 1, 2)),
2951 ("sidebar", GridArea::span(1, 0, 1, 1)),
2952 ("content", GridArea::cell(1, 1)),
2953 ],
2954 120,
2955 40,
2956 );
2957 assert_eq!(
2958 snap,
2959 "\
2960Grid 120x40 (2r x 2c)
2961 [0,0] x=0 y=0 w=20 h=3
2962 [0,1] x=20 y=0 w=100 h=3
2963 [1,0] x=0 y=3 w=20 h=37
2964 [1,1] x=20 y=3 w=100 h=37
2965 area(header) x=0 y=0 w=120 h=3
2966 area(sidebar) x=0 y=3 w=20 h=37
2967 area(content) x=20 y=3 w=100 h=37
2968"
2969 );
2970 }
2971
2972 #[test]
2973 fn snapshot_grid_dashboard_120x40() {
2974 let snap = snapshot_grid(
2975 &[
2976 Constraint::Fixed(3),
2977 Constraint::Percentage(60.0),
2978 Constraint::Fill,
2979 ],
2980 &[Constraint::Percentage(30.0), Constraint::Fill],
2981 &[
2982 ("nav", GridArea::span(0, 0, 1, 2)),
2983 ("chart", GridArea::cell(1, 0)),
2984 ("detail", GridArea::cell(1, 1)),
2985 ("log", GridArea::span(2, 0, 1, 2)),
2986 ],
2987 120,
2988 40,
2989 );
2990 assert_eq!(
2991 snap,
2992 "\
2993Grid 120x40 (3r x 2c)
2994 [0,0] x=0 y=0 w=36 h=3
2995 [0,1] x=36 y=0 w=84 h=3
2996 [1,0] x=0 y=3 w=36 h=24
2997 [1,1] x=36 y=3 w=84 h=24
2998 [2,0] x=0 y=27 w=36 h=13
2999 [2,1] x=36 y=27 w=84 h=13
3000 area(nav) x=0 y=0 w=120 h=3
3001 area(chart) x=0 y=3 w=36 h=24
3002 area(detail) x=36 y=3 w=84 h=24
3003 area(log) x=0 y=27 w=120 h=13
3004"
3005 );
3006 }
3007 }
3008}