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