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;
50pub mod pane_command;
51pub mod pane_execution;
52pub mod pane_memory;
53pub mod pane_monitors;
54pub mod pane_persistent;
55pub mod pane_retention;
56#[cfg(test)]
57mod repro_max_constraint;
58#[cfg(test)]
59mod repro_space_around;
60pub mod responsive;
61pub mod responsive_layout;
62pub mod veb_tree;
63pub mod visibility;
64pub mod workspace;
65
66pub use cache::{
67 CoherenceCache, CoherenceId, LayoutCache, LayoutCacheKey, LayoutCacheStats, S3FifoLayoutCache,
68};
69pub use direction::{FlowDirection, LogicalAlignment, LogicalSides, mirror_rects_horizontal};
70pub use ftui_core::geometry::{Rect, Sides, Size};
71pub use grid::{Grid, GridArea, GridLayout};
72pub use pane::{
73 PANE_AFFORDANCE_EMPHASIS_FULL_BPS, PANE_DEFAULT_MARGIN_CELLS, PANE_DEFAULT_PADDING_CELLS,
74 PANE_DRAG_RESIZE_DEFAULT_HYSTERESIS, PANE_DRAG_RESIZE_DEFAULT_THRESHOLD,
75 PANE_EDGE_GRIP_INSET_CELLS, PANE_MAGNETIC_FIELD_CELLS,
76 PANE_SEMANTIC_INPUT_EVENT_SCHEMA_VERSION, PANE_SEMANTIC_INPUT_TRACE_SCHEMA_VERSION,
77 PANE_SNAP_DEFAULT_HYSTERESIS_BPS, PANE_SNAP_DEFAULT_STEP_BPS, PANE_TREE_SCHEMA_VERSION,
78 PaneAffordanceMotion, PaneCancelReason, PaneConstraints, PaneCoordinateNormalizationError,
79 PaneCoordinateNormalizer, PaneCoordinateRoundingPolicy, PaneDockPreview, PaneDockZone,
80 PaneDragBehaviorTuning, PaneDragResizeEffect, PaneDragResizeMachine,
81 PaneDragResizeMachineError, PaneDragResizeNoopReason, PaneDragResizeState,
82 PaneDragResizeTransition, PaneEdgeResizePlan, PaneEdgeResizePlanError, PaneGroupTransformPlan,
83 PaneId, PaneIdAllocator, PaneInertialThrow, PaneInputCoordinate, PaneInteractionPolicyError,
84 PaneInteractionTimeline, PaneInteractionTimelineCheckpointDecision,
85 PaneInteractionTimelineEntry, PaneInteractionTimelineError,
86 PaneInteractionTimelineReplayDiagnostics, PaneInteractionTimelineRetentionDiagnostics,
87 PaneInvariantCode, PaneInvariantIssue, PaneInvariantReport, PaneInvariantSeverity, PaneLayout,
88 PaneLayoutIntelligenceMode, PaneLeaf, PaneModelError, PaneModifierSnapshot, PaneMotionVector,
89 PaneNodeKind, PaneNodeRecord, PaneNormalizedCoordinate, PaneOperation, PaneOperationError,
90 PaneOperationFailure, PaneOperationFamily, PaneOperationJournalEntry,
91 PaneOperationJournalResult, PaneOperationKind, PaneOperationOutcome, PanePlacement,
92 PanePointerButton, PanePointerPosition, PanePrecisionMode, PanePrecisionPolicy,
93 PanePressureSnapProfile, PaneReflowMovePlan, PaneReflowPlanError, PaneRepairAction,
94 PaneRepairError, PaneRepairFailure, PaneRepairOutcome, PaneResizeDirection, PaneResizeGrip,
95 PaneResizeTarget, PaneScaleFactor, PaneSelectionState, PaneSemanticInputEvent,
96 PaneSemanticInputEventError, PaneSemanticInputEventKind, PaneSemanticInputTrace,
97 PaneSemanticInputTraceError, PaneSemanticInputTraceMetadata,
98 PaneSemanticReplayConformanceArtifact, PaneSemanticReplayDiffArtifact,
99 PaneSemanticReplayDiffKind, PaneSemanticReplayError, PaneSemanticReplayFixture,
100 PaneSemanticReplayOutcome, PaneSnapDecision, PaneSnapReason, PaneSnapTuning, PaneSplit,
101 PaneSplitRatio, PaneTransaction, PaneTransactionOutcome, PaneTree, PaneTreeSnapshot, SplitAxis,
102};
103pub use pane_command::{
104 PaneAccessibilityPreferences, PaneAnnouncement, PaneAnnouncementCategory, PaneAnnouncer,
105 PaneCardinalDirection, PaneCommand, PaneCommandAcceleration, PaneCommandEffect,
106 PaneCommandNoopReason, PaneCommandResolution, PaneFocusContext, PaneFocusOrdinal,
107 PaneKeymapOwner, PaneKeymapPrecedence, announce_command, focus_cyclic, focus_directional,
108 focus_edge, focus_order, resolve as resolve_pane_command,
109};
110pub use pane_execution::{
111 PaneExecutionDecision, PaneExecutionPolicy, PaneStrategyReason, PaneWorkloadProfile,
112};
113pub use pane_memory::{
114 PANE_MEMORY_TELEMETRY_SCHEMA_VERSION, PaneMemoryComparison, PaneMemoryDriver,
115 PaneMemoryStrategy, PaneMemoryStrategyFootprint, pane_memory_comparison,
116};
117pub use pane_monitors::{
118 PaneAssumption, PaneMonitorReport, PaneMonitorStatus, PaneMonitorThresholds,
119 PaneMonitorVerdict, monitor_fallback_frequency, monitor_latency_envelope, monitor_replay_depth,
120 monitor_retention_pressure, monitor_selector_churn,
121};
122pub use pane_persistent::{
123 PaneVersionRetention, PaneVersionStore, PaneVersioningReport, PersistentApplyError,
124 PersistentApplyStrategy, PersistentNode, VersionedPaneTree,
125};
126pub use pane_retention::{
127 PaneRetentionBudget, PaneRetentionDecision, PaneRetentionOutcome, PaneRetentionPolicy,
128 apply_to_timeline as apply_retention_to_timeline,
129 apply_to_version_store as apply_retention_to_version_store,
130};
131pub use responsive::Responsive;
132pub use responsive_layout::{ResponsiveLayout, ResponsiveSplit};
133pub use smallvec;
134use smallvec::SmallVec;
135use std::cmp::min;
136pub use visibility::Visibility;
137pub use workspace::{
138 MigrationResult, WORKSPACE_SCHEMA_VERSION, WorkspaceMetadata, WorkspaceMigrationError,
139 WorkspaceSnapshot, WorkspaceSnapshotJsonError, WorkspaceValidationError,
140 canonicalize_workspace_snapshot, decode_workspace_snapshot_json, migrate_workspace,
141 needs_migration, to_canonical_workspace_snapshot_json,
142};
143
144const LAYOUT_INLINE_CAP: usize = 8;
149
150pub type Rects = SmallVec<[Rect; LAYOUT_INLINE_CAP]>;
152
153type Sizes = SmallVec<[u16; LAYOUT_INLINE_CAP]>;
155
156type Constraints = SmallVec<[Constraint; LAYOUT_INLINE_CAP]>;
158
159#[derive(Debug, Clone, Copy, PartialEq)]
161pub enum Constraint {
162 Fixed(u16),
164 Percentage(f32),
166 Min(u16),
168 Max(u16),
170 Ratio(u32, u32),
172 Fill,
174 FitContent,
179 FitContentBounded {
184 min: u16,
186 max: u16,
188 },
189 FitMin,
193}
194
195#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
218pub struct LayoutSizeHint {
219 pub min: u16,
221 pub preferred: u16,
223 pub max: Option<u16>,
225}
226
227impl LayoutSizeHint {
228 pub const ZERO: Self = Self {
230 min: 0,
231 preferred: 0,
232 max: None,
233 };
234
235 #[inline]
237 pub const fn exact(size: u16) -> Self {
238 Self {
239 min: size,
240 preferred: size,
241 max: Some(size),
242 }
243 }
244
245 #[inline]
247 pub const fn at_least(min: u16, preferred: u16) -> Self {
248 Self {
249 min,
250 preferred,
251 max: None,
252 }
253 }
254
255 #[inline]
257 pub fn clamp(&self, value: u16) -> u16 {
258 let max = self.max.unwrap_or(u16::MAX);
259 value.min(max).max(self.min)
260 }
261}
262
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
265pub enum Direction {
266 #[default]
268 Vertical,
269 Horizontal,
271}
272
273#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
275pub enum Alignment {
276 #[default]
278 Start,
279 Center,
281 End,
283 SpaceAround,
285 SpaceBetween,
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
302pub enum OverflowBehavior {
303 #[default]
307 Clip,
308 Visible,
311 Scroll {
315 max_content: Option<u16>,
318 },
319 Wrap,
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
337pub enum Breakpoint {
338 Xs,
340 Sm,
342 Md,
344 Lg,
346 Xl,
348}
349
350impl Breakpoint {
351 pub const ALL: [Breakpoint; 5] = [
353 Breakpoint::Xs,
354 Breakpoint::Sm,
355 Breakpoint::Md,
356 Breakpoint::Lg,
357 Breakpoint::Xl,
358 ];
359
360 #[inline]
362 const fn index(self) -> u8 {
363 match self {
364 Breakpoint::Xs => 0,
365 Breakpoint::Sm => 1,
366 Breakpoint::Md => 2,
367 Breakpoint::Lg => 3,
368 Breakpoint::Xl => 4,
369 }
370 }
371
372 #[must_use]
374 pub const fn label(self) -> &'static str {
375 match self {
376 Breakpoint::Xs => "xs",
377 Breakpoint::Sm => "sm",
378 Breakpoint::Md => "md",
379 Breakpoint::Lg => "lg",
380 Breakpoint::Xl => "xl",
381 }
382 }
383}
384
385impl std::fmt::Display for Breakpoint {
386 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
387 f.write_str(self.label())
388 }
389}
390
391#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub struct Breakpoints {
397 pub sm: u16,
399 pub md: u16,
401 pub lg: u16,
403 pub xl: u16,
405}
406
407impl Breakpoints {
408 pub const DEFAULT: Self = Self {
410 sm: 60,
411 md: 90,
412 lg: 120,
413 xl: 160,
414 };
415
416 pub const fn new(sm: u16, md: u16, lg: u16) -> Self {
420 let md = if md < sm { sm } else { md };
421 let lg = if lg < md { md } else { lg };
422 let xl = match lg.checked_add(40) {
424 Some(v) => v,
425 None => u16::MAX,
426 };
427 Self { sm, md, lg, xl }
428 }
429
430 pub const fn new_with_xl(sm: u16, md: u16, lg: u16, xl: u16) -> Self {
434 let md = if md < sm { sm } else { md };
435 let lg = if lg < md { md } else { lg };
436 let xl = if xl < lg { lg } else { xl };
437 Self { sm, md, lg, xl }
438 }
439
440 #[inline]
442 pub const fn classify_width(self, width: u16) -> Breakpoint {
443 if width >= self.xl {
444 Breakpoint::Xl
445 } else if width >= self.lg {
446 Breakpoint::Lg
447 } else if width >= self.md {
448 Breakpoint::Md
449 } else if width >= self.sm {
450 Breakpoint::Sm
451 } else {
452 Breakpoint::Xs
453 }
454 }
455
456 #[inline]
458 pub const fn classify_size(self, size: Size) -> Breakpoint {
459 self.classify_width(size.width)
460 }
461
462 #[inline]
464 pub const fn at_least(self, width: u16, min: Breakpoint) -> bool {
465 self.classify_width(width).index() >= min.index()
466 }
467
468 #[inline]
470 pub const fn between(self, width: u16, min: Breakpoint, max: Breakpoint) -> bool {
471 let idx = self.classify_width(width).index();
472 idx >= min.index() && idx <= max.index()
473 }
474
475 #[must_use]
477 pub const fn threshold(self, bp: Breakpoint) -> u16 {
478 match bp {
479 Breakpoint::Xs => 0,
480 Breakpoint::Sm => self.sm,
481 Breakpoint::Md => self.md,
482 Breakpoint::Lg => self.lg,
483 Breakpoint::Xl => self.xl,
484 }
485 }
486
487 #[must_use]
489 pub const fn thresholds(self) -> [(Breakpoint, u16); 5] {
490 [
491 (Breakpoint::Xs, 0),
492 (Breakpoint::Sm, self.sm),
493 (Breakpoint::Md, self.md),
494 (Breakpoint::Lg, self.lg),
495 (Breakpoint::Xl, self.xl),
496 ]
497 }
498}
499
500#[derive(Debug, Clone, Copy, Default)]
502pub struct Measurement {
503 pub min_width: u16,
505 pub min_height: u16,
507 pub max_width: Option<u16>,
509 pub max_height: Option<u16>,
511}
512
513impl Measurement {
514 #[must_use]
516 pub fn fixed(width: u16, height: u16) -> Self {
517 Self {
518 min_width: width,
519 min_height: height,
520 max_width: Some(width),
521 max_height: Some(height),
522 }
523 }
524
525 #[must_use]
527 pub fn flexible(min_width: u16, min_height: u16) -> Self {
528 Self {
529 min_width,
530 min_height,
531 max_width: None,
532 max_height: None,
533 }
534 }
535}
536
537#[derive(Debug, Clone, Default)]
539pub struct Flex {
540 direction: Direction,
541 constraints: Constraints,
542 margin: Sides,
543 gap: u16,
544 alignment: Alignment,
545 flow_direction: direction::FlowDirection,
546 overflow: OverflowBehavior,
547}
548
549impl Flex {
550 #[must_use]
552 pub fn vertical() -> Self {
553 Self {
554 direction: Direction::Vertical,
555 ..Default::default()
556 }
557 }
558
559 #[must_use]
561 pub fn horizontal() -> Self {
562 Self {
563 direction: Direction::Horizontal,
564 ..Default::default()
565 }
566 }
567
568 #[must_use]
570 pub fn direction(mut self, direction: Direction) -> Self {
571 self.direction = direction;
572 self
573 }
574
575 #[must_use]
577 pub fn constraints(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
578 self.constraints = constraints.into_iter().collect();
579 self
580 }
581
582 #[must_use]
584 pub fn margin(mut self, margin: Sides) -> Self {
585 self.margin = margin;
586 self
587 }
588
589 #[must_use]
591 pub fn gap(mut self, gap: u16) -> Self {
592 self.gap = gap;
593 self
594 }
595
596 #[must_use]
598 pub fn alignment(mut self, alignment: Alignment) -> Self {
599 self.alignment = alignment;
600 self
601 }
602
603 #[must_use]
609 pub fn flow_direction(mut self, flow: direction::FlowDirection) -> Self {
610 self.flow_direction = flow;
611 self
612 }
613
614 #[must_use]
616 pub fn overflow(mut self, overflow: OverflowBehavior) -> Self {
617 self.overflow = overflow;
618 self
619 }
620
621 #[must_use]
623 pub fn overflow_behavior(&self) -> OverflowBehavior {
624 self.overflow
625 }
626
627 #[must_use]
629 pub fn constraint_count(&self) -> usize {
630 self.constraints.len()
631 }
632
633 pub fn split(&self, area: Rect) -> Rects {
635 let inner = area.inner(self.margin);
637 if inner.is_empty() {
638 return self.constraints.iter().map(|_| Rect::default()).collect();
639 }
640
641 let total_size = match self.direction {
642 Direction::Horizontal => inner.width,
643 Direction::Vertical => inner.height,
644 };
645
646 let count = self.constraints.len();
647 if count == 0 {
648 return Rects::new();
649 }
650
651 let gap_count = count - 1;
653 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
654 let available_size = total_size.saturating_sub(total_gap);
655
656 let sizes = solve_constraints(&self.constraints, available_size);
658
659 let mut rects = self.sizes_to_rects(inner, &sizes);
661
662 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
664 direction::mirror_rects_horizontal(&mut rects, inner);
665 }
666
667 rects
668 }
669
670 fn sizes_to_rects(&self, area: Rect, sizes: &[u16]) -> Rects {
671 let mut rects = SmallVec::with_capacity(sizes.len());
672 if sizes.is_empty() {
673 return rects;
674 }
675
676 let total_items_size: u16 = sizes.iter().fold(0u16, |acc, &s| acc.saturating_add(s));
677 let total_available = match self.direction {
678 Direction::Horizontal => area.width,
679 Direction::Vertical => area.height,
680 };
681
682 let (start_shift, use_formula) = match self.alignment {
684 Alignment::Start => (0, None),
685 Alignment::End => {
686 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
687 .min(u16::MAX as u64) as u16;
688 let used = total_items_size.saturating_add(gap_space);
689 (total_available.saturating_sub(used), None)
690 }
691 Alignment::Center => {
692 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
693 .min(u16::MAX as u64) as u16;
694 let used = total_items_size.saturating_add(gap_space);
695 (total_available.saturating_sub(used) / 2, None)
696 }
697 Alignment::SpaceBetween => {
698 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
699 .min(u16::MAX as u64) as u16;
700 let used = total_items_size.saturating_add(gap_space);
701 let leftover = total_available.saturating_sub(used);
702 let slots = sizes.len().saturating_sub(1);
703 if slots > 0 {
704 (0, Some((leftover, slots, 0))) } else {
706 (0, None)
707 }
708 }
709 Alignment::SpaceAround => {
710 let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
711 .min(u16::MAX as u64) as u16;
712 let used = total_items_size.saturating_add(gap_space);
713 let leftover = total_available.saturating_sub(used);
714 let slots = sizes.len() * 2;
715 if slots > 0 {
716 (0, Some((leftover, slots, 1))) } else {
718 (0, None)
719 }
720 }
721 };
722
723 let mut accumulated_size = 0;
724
725 for (i, &size) in sizes.iter().enumerate() {
726 let explicit_gap_so_far = if i > 0 {
727 (i as u64 * self.gap as u64).min(u16::MAX as u64) as u16
728 } else {
729 0
730 };
731
732 let gap_offset = if let Some((leftover, slots, mode)) = use_formula {
733 if mode == 0 {
734 if i == 0 {
736 0
737 } else {
738 explicit_gap_so_far
739 .saturating_add((leftover as u64 * i as u64 / slots as u64) as u16)
740 }
741 } else {
742 let numerator = leftover as u64 * (2 * i as u64 + 1);
744 let denominator = slots as u64;
745 let raw = (numerator + (denominator / 2)) / denominator;
746 explicit_gap_so_far.saturating_add(raw.min(u64::from(u16::MAX)) as u16)
747 }
748 } else {
749 explicit_gap_so_far
751 };
752
753 let pos = match self.direction {
754 Direction::Horizontal => area
755 .x
756 .saturating_add(start_shift)
757 .saturating_add(accumulated_size)
758 .saturating_add(gap_offset),
759 Direction::Vertical => area
760 .y
761 .saturating_add(start_shift)
762 .saturating_add(accumulated_size)
763 .saturating_add(gap_offset),
764 };
765
766 let rect = match self.direction {
767 Direction::Horizontal => Rect {
768 x: pos,
769 y: area.y,
770 width: size.min(area.right().saturating_sub(pos)),
771 height: area.height,
772 },
773 Direction::Vertical => Rect {
774 x: area.x,
775 y: pos,
776 width: area.width,
777 height: size.min(area.bottom().saturating_sub(pos)),
778 },
779 };
780 rects.push(rect);
781 accumulated_size = accumulated_size.saturating_add(size);
782 }
783
784 rects
785 }
786
787 pub fn split_with_measurer<F>(&self, area: Rect, measurer: F) -> Rects
811 where
812 F: Fn(usize, u16) -> LayoutSizeHint,
813 {
814 let inner = area.inner(self.margin);
816 if inner.is_empty() {
817 return self.constraints.iter().map(|_| Rect::default()).collect();
818 }
819
820 let total_size = match self.direction {
821 Direction::Horizontal => inner.width,
822 Direction::Vertical => inner.height,
823 };
824
825 let count = self.constraints.len();
826 if count == 0 {
827 return Rects::new();
828 }
829
830 let gap_count = count - 1;
832 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
833 let available_size = total_size.saturating_sub(total_gap);
834
835 let sizes =
837 solve_constraints_with_hints(&self.constraints, available_size, &measurer, None);
838
839 let mut rects = self.sizes_to_rects(inner, &sizes);
841
842 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
844 direction::mirror_rects_horizontal(&mut rects, inner);
845 }
846
847 rects
848 }
849 pub fn split_with_measurer_stably<F>(
854 &self,
855 area: Rect,
856 measurer: F,
857 cache: &mut CoherenceCache,
858 ) -> Rects
859 where
860 F: Fn(usize, u16) -> LayoutSizeHint,
861 {
862 let inner = area.inner(self.margin);
864 if inner.is_empty() {
865 return self.constraints.iter().map(|_| Rect::default()).collect();
866 }
867
868 let total_size = match self.direction {
869 Direction::Horizontal => inner.width,
870 Direction::Vertical => inner.height,
871 };
872
873 let count = self.constraints.len();
874 if count == 0 {
875 return Rects::new();
876 }
877
878 let gap_count = count - 1;
880 let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
881 let available_size = total_size.saturating_sub(total_gap);
882
883 let id = CoherenceId::new(&self.constraints, self.direction);
885 let sizes = solve_constraints_with_hints(
886 &self.constraints,
887 available_size,
888 &measurer,
889 Some((cache, id)),
890 );
891
892 let mut rects = self.sizes_to_rects(inner, &sizes);
894
895 if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
897 direction::mirror_rects_horizontal(&mut rects, inner);
898 }
899
900 rects
901 }
902}
903
904pub(crate) fn solve_constraints(constraints: &[Constraint], available_size: u16) -> Sizes {
909 solve_constraints_with_hints(
911 constraints,
912 available_size,
913 &|_, _| LayoutSizeHint::ZERO,
914 None,
915 )
916}
917
918pub(crate) fn solve_constraints_with_hints<F>(
923 constraints: &[Constraint],
924 available_size: u16,
925 measurer: &F,
926 mut coherence: Option<(&mut CoherenceCache, CoherenceId)>,
927) -> Sizes
928where
929 F: Fn(usize, u16) -> LayoutSizeHint,
930{
931 const WEIGHT_SCALE: u64 = 10_000;
932
933 let mut sizes: Sizes = smallvec::smallvec![0u16; constraints.len()];
934 let mut remaining = available_size;
935 let mut grow_indices: SmallVec<[usize; LAYOUT_INLINE_CAP]> = SmallVec::new();
936
937 let grow_weight = |constraint: Constraint| -> u64 {
938 match constraint {
939 Constraint::Min(_) | Constraint::Max(_) | Constraint::Fill => WEIGHT_SCALE,
940 _ => 0,
941 }
942 };
943
944 for (i, &constraint) in constraints.iter().enumerate() {
947 match constraint {
948 Constraint::Fixed(size) => {
949 let size = min(size, remaining);
950 sizes[i] = size;
951 remaining = remaining.saturating_sub(size);
952 }
953 Constraint::Min(min_size) => {
954 let size = min(min_size, remaining);
955 sizes[i] = size;
956 remaining = remaining.saturating_sub(size);
957 }
959 Constraint::FitMin => {
960 let hint = measurer(i, remaining);
961 let size = min(hint.min, remaining);
962 sizes[i] = size;
963 remaining = remaining.saturating_sub(size);
964 }
965 Constraint::FitContent => {
966 let hint = measurer(i, remaining);
967 let size = min(hint.min, remaining);
968 sizes[i] = size;
969 remaining = remaining.saturating_sub(size);
970 }
971 Constraint::FitContentBounded { min: min_bound, .. } => {
972 let size = min(min_bound, remaining);
974 sizes[i] = size;
975 remaining = remaining.saturating_sub(size);
976 }
977 _ => {} }
979 }
980
981 for (i, &constraint) in constraints.iter().enumerate() {
984 match constraint {
985 Constraint::Percentage(p) => {
986 let target = (available_size as f32 * p / 100.0)
987 .round()
988 .min(u16::MAX as f32) as u16;
989 let needed = target.saturating_sub(sizes[i]);
990 let alloc = min(needed, remaining);
991 sizes[i] = sizes[i].saturating_add(alloc);
992 remaining = remaining.saturating_sub(alloc);
993 }
994 Constraint::Ratio(n, d) => {
995 let target = if d == 0 {
996 0
997 } else {
998 (u64::from(available_size) * u64::from(n) / u64::from(d)).min(u16::MAX as u64)
999 as u16
1000 };
1001 let needed = target.saturating_sub(sizes[i]);
1002 let alloc = min(needed, remaining);
1003 sizes[i] = sizes[i].saturating_add(alloc);
1004 remaining = remaining.saturating_sub(alloc);
1005 }
1006 Constraint::FitContent => {
1007 let hint = measurer(i, remaining);
1008 let preferred = hint
1009 .preferred
1010 .max(sizes[i])
1011 .min(hint.max.unwrap_or(u16::MAX));
1012 let needed = preferred.saturating_sub(sizes[i]);
1013 let alloc = min(needed, remaining);
1014 sizes[i] = sizes[i].saturating_add(alloc);
1015 remaining = remaining.saturating_sub(alloc);
1016 }
1017 Constraint::FitContentBounded { max: max_bound, .. } => {
1018 let hint = measurer(i, remaining);
1019 let preferred = hint.preferred.max(sizes[i]).min(max_bound);
1020 let needed = preferred.saturating_sub(sizes[i]);
1021 let alloc = min(needed, remaining);
1022 sizes[i] = sizes[i].saturating_add(alloc);
1023 remaining = remaining.saturating_sub(alloc);
1024 }
1025 Constraint::Min(_) => {
1026 grow_indices.push(i);
1027 }
1028 Constraint::Max(_) => {
1029 grow_indices.push(i);
1030 }
1031 Constraint::Fill => {
1032 grow_indices.push(i);
1033 }
1034 _ => {} }
1036 }
1037
1038 loop {
1040 if remaining == 0 || grow_indices.is_empty() {
1041 break;
1042 }
1043
1044 let mut total_weight = 0u128;
1045 for &i in &grow_indices {
1046 let weight = grow_weight(constraints[i]);
1047 if weight > 0 {
1048 total_weight = total_weight.saturating_add(u128::from(weight));
1049 }
1050 }
1051
1052 if total_weight == 0 {
1053 break;
1054 }
1055
1056 let space_to_distribute = remaining;
1057 let mut shares: SmallVec<[u16; LAYOUT_INLINE_CAP]> =
1058 smallvec::smallvec![0u16; constraints.len()];
1059
1060 let targets: Vec<f64> = grow_indices
1062 .iter()
1063 .map(|&i| {
1064 let weight = grow_weight(constraints[i]);
1065 (space_to_distribute as f64 * weight as f64) / total_weight as f64
1066 })
1067 .collect();
1068
1069 let prev_alloc = coherence
1071 .as_ref()
1072 .and_then(|(cache, id)| cache.get(id))
1073 .map(|full_prev| {
1074 grow_indices
1076 .iter()
1077 .map(|&i| full_prev.get(i).copied().unwrap_or(0))
1078 .collect()
1079 });
1080
1081 let distributed = round_layout_stable(&targets, space_to_distribute, prev_alloc);
1083
1084 for (k, &i) in grow_indices.iter().enumerate() {
1085 shares[i] = distributed[k];
1086 }
1087
1088 let mut violations = Vec::new();
1090 for &i in &grow_indices {
1091 if let Constraint::Max(max_val) = constraints[i]
1092 && sizes[i].saturating_add(shares[i]) > max_val
1093 {
1094 violations.push(i);
1095 }
1096 }
1097
1098 if violations.is_empty() {
1099 for &i in &grow_indices {
1101 sizes[i] = sizes[i].saturating_add(shares[i]);
1102 }
1103 if let Some((cache, id)) = coherence.as_mut() {
1104 if distributed.len() == targets.len() {
1107 let mut full_shares: Sizes = smallvec::smallvec![0u16; constraints.len()];
1108 for (k, &i) in grow_indices.iter().enumerate() {
1109 full_shares[i] = distributed[k];
1110 }
1111 cache.store(*id, full_shares);
1112 }
1113 }
1114 break;
1115 }
1116
1117 for i in violations {
1119 if let Constraint::Max(max_val) = constraints[i] {
1120 let consumed = max_val.saturating_sub(sizes[i]);
1123 sizes[i] = max_val;
1124 remaining = remaining.saturating_sub(consumed);
1125
1126 if let Some(pos) = grow_indices.iter().position(|&x| x == i) {
1128 grow_indices.remove(pos);
1129 }
1130 }
1131 }
1132 }
1133
1134 sizes
1135}
1136
1137pub type PreviousAllocation = Option<Sizes>;
1147
1148pub fn round_layout_stable(targets: &[f64], total: u16, prev: PreviousAllocation) -> Sizes {
1210 let n = targets.len();
1211 if n == 0 {
1212 return Sizes::new();
1213 }
1214
1215 let floors: Sizes = targets
1217 .iter()
1218 .map(|&r| (r.max(0.0).floor() as u64).min(u16::MAX as u64) as u16)
1219 .collect();
1220
1221 let floor_sum: u64 = floors.iter().map(|&x| u64::from(x)).sum();
1222 let total_u64 = u64::from(total);
1223
1224 if floor_sum > total_u64 {
1226 return redistribute_overflow(&floors, total);
1227 }
1228
1229 let deficit = (total_u64 - floor_sum) as u16;
1230
1231 if deficit == 0 {
1232 return floors;
1234 }
1235
1236 let mut priority: SmallVec<[(usize, f64, bool); LAYOUT_INLINE_CAP]> = targets
1238 .iter()
1239 .enumerate()
1240 .map(|(i, &r)| {
1241 let raw_remainder = r - (floors[i] as f64);
1244 let remainder = if raw_remainder.is_finite() {
1245 raw_remainder
1246 } else {
1247 0.0
1248 };
1249 let ceil_val = floors[i].saturating_add(1);
1250 let prev_used_ceil = prev
1252 .as_ref()
1253 .is_some_and(|p| p.get(i).copied() == Some(ceil_val));
1254 (i, remainder, prev_used_ceil)
1255 })
1256 .collect();
1257
1258 priority.sort_by(|a, b| {
1264 b.1.total_cmp(&a.1)
1265 .then_with(|| {
1266 b.2.cmp(&a.2)
1268 })
1269 .then_with(|| {
1270 a.0.cmp(&b.0)
1272 })
1273 });
1274
1275 let mut result = floors;
1277 let mut remaining_deficit = deficit;
1278
1279 if remaining_deficit as usize >= n {
1285 let per_item = remaining_deficit / (n as u16);
1286 for val in result.iter_mut() {
1287 *val = val.saturating_add(per_item);
1288 }
1289 remaining_deficit %= n as u16;
1290 }
1291
1292 if remaining_deficit > 0 {
1293 for &(i, _, _) in priority.iter().take(remaining_deficit as usize) {
1294 result[i] = result[i].saturating_add(1);
1295 }
1296 }
1297
1298 result
1299}
1300
1301fn redistribute_overflow(floors: &[u16], total: u16) -> Sizes {
1306 let mut result: Sizes = floors.iter().copied().collect();
1307 let current_sum: u64 = result.iter().map(|&x| u64::from(x)).sum();
1308 let total_u64 = u64::from(total);
1309 let n = result.len();
1310
1311 if current_sum <= total_u64 || n == 0 {
1312 return result;
1313 }
1314
1315 let mut overflow = current_sum - total_u64;
1316
1317 while overflow > 0 {
1318 let &max_val = result.iter().max().unwrap_or(&0);
1319 if max_val == 0 {
1320 for val in result.iter_mut() {
1324 *val = 0;
1325 }
1326 break;
1327 }
1328
1329 let count_max = result.iter().filter(|&&v| v == max_val).count() as u64;
1330 let &next_max = result.iter().filter(|&&v| v < max_val).max().unwrap_or(&0);
1331
1332 let delta = (max_val - next_max) as u64;
1333 let required_per_item = overflow.div_ceil(count_max);
1334 let reduce_per_item = delta.min(required_per_item).max(1) as u16;
1335
1336 let mut reduced_any = false;
1337 for val in result.iter_mut() {
1338 if *val == max_val {
1339 let amount = u64::from(*val)
1340 .min(u64::from(reduce_per_item))
1341 .min(overflow) as u16;
1342 if amount > 0 {
1343 *val -= amount;
1344 overflow -= u64::from(amount);
1345 reduced_any = true;
1346 }
1347 if overflow == 0 {
1348 break;
1349 }
1350 }
1351 }
1352
1353 if !reduced_any {
1354 for val in result.iter_mut() {
1356 if overflow == 0 {
1357 break;
1358 }
1359 if *val > 0 {
1360 *val -= 1;
1361 overflow -= 1;
1362 }
1363 }
1364 break;
1365 }
1366 }
1367
1368 result
1369}
1370
1371#[cfg(test)]
1372mod tests {
1373 use super::*;
1374
1375 #[test]
1376 fn round_layout_stable_nan_targets_do_not_panic_the_sort() {
1377 let mut targets: Vec<f64> = (0..64).map(|i| (i as f64) * 0.37 + 0.1).collect();
1382 targets[7] = f64::NAN;
1383 targets[23] = f64::INFINITY;
1384 targets[41] = f64::NEG_INFINITY;
1385
1386 let sizes = round_layout_stable(&targets, 120, None);
1389 assert_eq!(sizes.len(), 64);
1390
1391 let nan_targets = [f64::NAN; 8];
1394 let sizes = round_layout_stable(&nan_targets, 40, None);
1395 let sum: u64 = sizes.iter().map(|&x| u64::from(x)).sum();
1396 assert_eq!(sum, 40);
1397 }
1398
1399 #[test]
1400 fn fixed_split() {
1401 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(20)]);
1402 let rects = flex.split(Rect::new(0, 0, 100, 10));
1403 assert_eq!(rects.len(), 2);
1404 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1405 assert_eq!(rects[1], Rect::new(10, 0, 20, 10)); }
1407
1408 #[test]
1409 fn percentage_split() {
1410 let flex = Flex::horizontal()
1411 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1412 let rects = flex.split(Rect::new(0, 0, 100, 10));
1413 assert_eq!(rects[0].width, 50);
1414 assert_eq!(rects[1].width, 50);
1415 }
1416
1417 #[test]
1418 fn gap_handling() {
1419 let flex = Flex::horizontal()
1420 .gap(5)
1421 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1422 let rects = flex.split(Rect::new(0, 0, 100, 10));
1423 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1427 assert_eq!(rects[1], Rect::new(15, 0, 10, 10));
1428 }
1429
1430 #[test]
1431 fn mixed_constraints() {
1432 let flex = Flex::horizontal().constraints([
1433 Constraint::Fixed(10),
1434 Constraint::Min(10), Constraint::Percentage(10.0), ]);
1437
1438 let rects = flex.split(Rect::new(0, 0, 100, 1));
1446 assert_eq!(rects[0].width, 10); assert_eq!(rects[2].width, 10); assert_eq!(rects[1].width, 80); }
1450
1451 #[test]
1452 fn measurement_fixed_constraints() {
1453 let fixed = Measurement::fixed(5, 7);
1454 assert_eq!(fixed.min_width, 5);
1455 assert_eq!(fixed.min_height, 7);
1456 assert_eq!(fixed.max_width, Some(5));
1457 assert_eq!(fixed.max_height, Some(7));
1458 }
1459
1460 #[test]
1461 fn measurement_flexible_constraints() {
1462 let flexible = Measurement::flexible(2, 3);
1463 assert_eq!(flexible.min_width, 2);
1464 assert_eq!(flexible.min_height, 3);
1465 assert_eq!(flexible.max_width, None);
1466 assert_eq!(flexible.max_height, None);
1467 }
1468
1469 #[test]
1470 fn breakpoints_classify_defaults() {
1471 let bp = Breakpoints::DEFAULT;
1472 assert_eq!(bp.classify_width(20), Breakpoint::Xs);
1473 assert_eq!(bp.classify_width(60), Breakpoint::Sm);
1474 assert_eq!(bp.classify_width(90), Breakpoint::Md);
1475 assert_eq!(bp.classify_width(120), Breakpoint::Lg);
1476 }
1477
1478 #[test]
1479 fn breakpoints_at_least_and_between() {
1480 let bp = Breakpoints::new(50, 80, 110);
1481 assert!(bp.at_least(85, Breakpoint::Sm));
1482 assert!(bp.between(85, Breakpoint::Sm, Breakpoint::Md));
1483 assert!(!bp.between(85, Breakpoint::Lg, Breakpoint::Lg));
1484 }
1485
1486 #[test]
1487 fn alignment_end() {
1488 let flex = Flex::horizontal()
1489 .alignment(Alignment::End)
1490 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1491 let rects = flex.split(Rect::new(0, 0, 100, 10));
1492 assert_eq!(rects[0], Rect::new(80, 0, 10, 10));
1494 assert_eq!(rects[1], Rect::new(90, 0, 10, 10));
1495 }
1496
1497 #[test]
1498 fn alignment_center() {
1499 let flex = Flex::horizontal()
1500 .alignment(Alignment::Center)
1501 .constraints([Constraint::Fixed(20), Constraint::Fixed(20)]);
1502 let rects = flex.split(Rect::new(0, 0, 100, 10));
1503 assert_eq!(rects[0], Rect::new(30, 0, 20, 10));
1505 assert_eq!(rects[1], Rect::new(50, 0, 20, 10));
1506 }
1507
1508 #[test]
1509 fn alignment_space_between() {
1510 let flex = Flex::horizontal()
1511 .alignment(Alignment::SpaceBetween)
1512 .constraints([
1513 Constraint::Fixed(10),
1514 Constraint::Fixed(10),
1515 Constraint::Fixed(10),
1516 ]);
1517 let rects = flex.split(Rect::new(0, 0, 100, 10));
1518 assert_eq!(rects[0].x, 0);
1520 assert_eq!(rects[1].x, 45); assert_eq!(rects[2].x, 90); }
1523
1524 #[test]
1525 fn vertical_alignment() {
1526 let flex = Flex::vertical()
1527 .alignment(Alignment::End)
1528 .constraints([Constraint::Fixed(5), Constraint::Fixed(5)]);
1529 let rects = flex.split(Rect::new(0, 0, 10, 100));
1530 assert_eq!(rects[0], Rect::new(0, 90, 10, 5));
1532 assert_eq!(rects[1], Rect::new(0, 95, 10, 5));
1533 }
1534
1535 #[test]
1536 fn nested_flex_support() {
1537 let outer = Flex::horizontal()
1539 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1540 let outer_rects = outer.split(Rect::new(0, 0, 100, 100));
1541
1542 let inner = Flex::vertical().constraints([Constraint::Fixed(30), Constraint::Min(10)]);
1544 let inner_rects = inner.split(outer_rects[0]);
1545
1546 assert_eq!(inner_rects[0], Rect::new(0, 0, 50, 30));
1547 assert_eq!(inner_rects[1], Rect::new(0, 30, 50, 70));
1548 }
1549
1550 #[test]
1552 fn invariant_total_size_does_not_exceed_available() {
1553 for total in [10u16, 50, 100, 255] {
1555 let flex = Flex::horizontal().constraints([
1556 Constraint::Fixed(30),
1557 Constraint::Percentage(50.0),
1558 Constraint::Min(20),
1559 ]);
1560 let rects = flex.split(Rect::new(0, 0, total, 10));
1561 let total_width: u16 = rects.iter().map(|r| r.width).sum();
1562 assert!(
1563 total_width <= total,
1564 "Total width {} exceeded available {} for constraints",
1565 total_width,
1566 total
1567 );
1568 }
1569 }
1570
1571 #[test]
1572 fn invariant_empty_area_produces_empty_rects() {
1573 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1574 let rects = flex.split(Rect::new(0, 0, 0, 0));
1575 assert!(rects.iter().all(|r| r.is_empty()));
1576 }
1577
1578 #[test]
1579 fn invariant_no_constraints_produces_empty_vec() {
1580 let flex = Flex::horizontal().constraints([]);
1581 let rects = flex.split(Rect::new(0, 0, 100, 100));
1582 assert!(rects.is_empty());
1583 }
1584
1585 #[test]
1586 fn flex_constraints_stay_inline_for_common_layouts() {
1587 let flex = Flex::horizontal().constraints([Constraint::Fixed(1); LAYOUT_INLINE_CAP]);
1588 assert_eq!(flex.constraint_count(), LAYOUT_INLINE_CAP);
1589 assert!(!flex.constraints.spilled());
1590
1591 let rects = flex.split(Rect::new(0, 0, LAYOUT_INLINE_CAP as u16, 1));
1592 assert_eq!(rects.len(), LAYOUT_INLINE_CAP);
1593 assert!(rects.iter().all(|rect| rect.width == 1));
1594 }
1595
1596 #[test]
1599 fn ratio_constraint_splits_proportionally() {
1600 let flex =
1601 Flex::horizontal().constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
1602 let rects = flex.split(Rect::new(0, 0, 90, 10));
1603 assert_eq!(rects[0].width, 30);
1604 assert_eq!(rects[1].width, 60);
1605 }
1606
1607 #[test]
1608 fn ratio_constraint_with_zero_denominator() {
1609 let flex = Flex::horizontal().constraints([Constraint::Ratio(1, 0)]);
1611 let rects = flex.split(Rect::new(0, 0, 100, 10));
1612 assert_eq!(rects.len(), 1);
1613 }
1614
1615 #[test]
1616 fn ratio_is_absolute_fraction() {
1617 let area = Rect::new(0, 0, 100, 1);
1618
1619 let rects = Flex::horizontal()
1621 .constraints([Constraint::Percentage(25.0)])
1622 .split(area);
1623 assert_eq!(rects[0].width, 25);
1624
1625 let rects = Flex::horizontal()
1628 .constraints([Constraint::Ratio(1, 4)])
1629 .split(area);
1630 assert_eq!(rects[0].width, 25);
1631 }
1632
1633 #[test]
1634 fn ratio_is_independent_of_grow_items() {
1635 let area = Rect::new(0, 0, 100, 1);
1636
1637 let rects = Flex::horizontal()
1639 .constraints([Constraint::Ratio(1, 4), Constraint::Fill])
1640 .split(area);
1641 assert_eq!(rects[0].width, 25);
1642 assert_eq!(rects[1].width, 75);
1643 }
1644
1645 #[test]
1646 fn ratio_zero_numerator_should_be_zero() {
1647 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
1650 let rects = flex.split(Rect::new(0, 0, 100, 1));
1651
1652 assert_eq!(rects[0].width, 100, "Fill should take all space");
1654 assert_eq!(rects[1].width, 0, "Ratio(0, 1) should be width 0");
1655 }
1656
1657 #[test]
1660 fn max_constraint_clamps_size() {
1661 let flex = Flex::horizontal().constraints([Constraint::Max(20), Constraint::Fixed(30)]);
1662 let rects = flex.split(Rect::new(0, 0, 100, 10));
1663 assert!(rects[0].width <= 20);
1664 assert_eq!(rects[1].width, 30);
1665 }
1666
1667 #[test]
1668 fn percentage_rounding_never_exceeds_available() {
1669 let constraints = [
1670 Constraint::Percentage(33.4),
1671 Constraint::Percentage(33.3),
1672 Constraint::Percentage(33.3),
1673 ];
1674 let sizes = solve_constraints(&constraints, 7);
1675 let total: u16 = sizes.iter().sum();
1676 assert!(total <= 7, "percent rounding overflowed: {sizes:?}");
1677 assert!(sizes.iter().all(|size| *size <= 7));
1678 }
1679
1680 #[test]
1681 fn tiny_area_saturates_fixed_and_min() {
1682 let constraints = [Constraint::Fixed(5), Constraint::Min(3), Constraint::Max(2)];
1683 let sizes = solve_constraints(&constraints, 2);
1684 assert_eq!(sizes[0], 2);
1685 assert_eq!(sizes[1], 0);
1686 assert_eq!(sizes[2], 0);
1687 assert_eq!(sizes.iter().sum::<u16>(), 2);
1688 }
1689
1690 #[test]
1691 fn ratio_distribution_sums_to_available() {
1692 let constraints = [Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)];
1694 let sizes = solve_constraints(&constraints, 5);
1695 assert_eq!(sizes.iter().sum::<u16>(), 4);
1696 assert_eq!(sizes[0], 1);
1697 assert_eq!(sizes[1], 3);
1698 }
1699
1700 #[test]
1701 fn flex_gap_exceeds_area_yields_zero_widths() {
1702 let flex = Flex::horizontal()
1703 .gap(5)
1704 .constraints([Constraint::Fixed(1), Constraint::Fixed(1)]);
1705 let rects = flex.split(Rect::new(0, 0, 3, 1));
1706 assert_eq!(rects.len(), 2);
1707 assert_eq!(rects[0].width, 0);
1708 assert_eq!(rects[1].width, 0);
1709 }
1710
1711 #[test]
1714 fn alignment_space_around() {
1715 let flex = Flex::horizontal()
1716 .alignment(Alignment::SpaceAround)
1717 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1718 let rects = flex.split(Rect::new(0, 0, 100, 10));
1719
1720 assert_eq!(rects[0].x, 20);
1723 assert_eq!(rects[1].x, 70);
1724 }
1725
1726 #[test]
1729 fn vertical_gap() {
1730 let flex = Flex::vertical()
1731 .gap(5)
1732 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1733 let rects = flex.split(Rect::new(0, 0, 50, 100));
1734 assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
1735 assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
1736 }
1737
1738 #[test]
1741 fn vertical_center() {
1742 let flex = Flex::vertical()
1743 .alignment(Alignment::Center)
1744 .constraints([Constraint::Fixed(10)]);
1745 let rects = flex.split(Rect::new(0, 0, 50, 100));
1746 assert_eq!(rects[0].y, 45);
1748 assert_eq!(rects[0].height, 10);
1749 }
1750
1751 #[test]
1754 fn single_min_takes_all() {
1755 let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
1756 let rects = flex.split(Rect::new(0, 0, 80, 24));
1757 assert_eq!(rects[0].width, 80);
1758 }
1759
1760 #[test]
1763 fn fixed_exceeds_available_clamped() {
1764 let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
1765 let rects = flex.split(Rect::new(0, 0, 100, 10));
1766 assert_eq!(rects[0].width, 60);
1768 assert_eq!(rects[1].width, 40);
1769 }
1770
1771 #[test]
1774 fn percentage_overflow_clamped() {
1775 let flex = Flex::horizontal()
1776 .constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
1777 let rects = flex.split(Rect::new(0, 0, 100, 10));
1778 assert_eq!(rects[0].width, 80);
1779 assert_eq!(rects[1].width, 20); }
1781
1782 #[test]
1785 fn margin_reduces_split_area() {
1786 let flex = Flex::horizontal()
1787 .margin(Sides::all(10))
1788 .constraints([Constraint::Fixed(20), Constraint::Min(0)]);
1789 let rects = flex.split(Rect::new(0, 0, 100, 100));
1790 assert_eq!(rects[0].x, 10);
1792 assert_eq!(rects[0].y, 10);
1793 assert_eq!(rects[0].width, 20);
1794 assert_eq!(rects[0].height, 80);
1795 }
1796
1797 #[test]
1800 fn builder_methods_chain() {
1801 let flex = Flex::vertical()
1802 .direction(Direction::Horizontal)
1803 .gap(3)
1804 .margin(Sides::all(1))
1805 .alignment(Alignment::End)
1806 .constraints([Constraint::Fixed(10)]);
1807 let rects = flex.split(Rect::new(0, 0, 50, 50));
1808 assert_eq!(rects.len(), 1);
1809 }
1810
1811 #[test]
1814 fn space_between_single_item() {
1815 let flex = Flex::horizontal()
1816 .alignment(Alignment::SpaceBetween)
1817 .constraints([Constraint::Fixed(10)]);
1818 let rects = flex.split(Rect::new(0, 0, 100, 10));
1819 assert_eq!(rects[0].x, 0);
1821 assert_eq!(rects[0].width, 10);
1822 }
1823
1824 #[test]
1825 fn invariant_rects_within_bounds() {
1826 let area = Rect::new(10, 20, 80, 60);
1827 let flex = Flex::horizontal()
1828 .margin(Sides::all(5))
1829 .gap(2)
1830 .constraints([
1831 Constraint::Fixed(15),
1832 Constraint::Percentage(30.0),
1833 Constraint::Min(10),
1834 ]);
1835 let rects = flex.split(area);
1836
1837 let inner = area.inner(Sides::all(5));
1839 for rect in &rects {
1840 assert!(
1841 rect.x >= inner.x && rect.right() <= inner.right(),
1842 "Rect {:?} exceeds horizontal bounds of {:?}",
1843 rect,
1844 inner
1845 );
1846 assert!(
1847 rect.y >= inner.y && rect.bottom() <= inner.bottom(),
1848 "Rect {:?} exceeds vertical bounds of {:?}",
1849 rect,
1850 inner
1851 );
1852 }
1853 }
1854
1855 #[test]
1858 fn fill_takes_remaining_space() {
1859 let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
1860 let rects = flex.split(Rect::new(0, 0, 100, 10));
1861 assert_eq!(rects[0].width, 20);
1862 assert_eq!(rects[1].width, 80); }
1864
1865 #[test]
1866 fn multiple_fills_share_space() {
1867 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
1868 let rects = flex.split(Rect::new(0, 0, 100, 10));
1869 assert_eq!(rects[0].width, 50);
1870 assert_eq!(rects[1].width, 50);
1871 }
1872
1873 #[test]
1876 fn fit_content_uses_preferred_size() {
1877 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1878 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1879 if idx == 0 {
1880 LayoutSizeHint {
1881 min: 5,
1882 preferred: 30,
1883 max: None,
1884 }
1885 } else {
1886 LayoutSizeHint::ZERO
1887 }
1888 });
1889 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
1892
1893 #[test]
1894 fn fit_content_clamps_to_available() {
1895 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
1896 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1897 min: 5,
1898 preferred: 80,
1899 max: None,
1900 });
1901 assert_eq!(rects[0].width, 80);
1903 assert_eq!(rects[1].width, 20);
1904 }
1905
1906 #[test]
1907 fn fit_content_without_measurer_gets_zero() {
1908 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1910 let rects = flex.split(Rect::new(0, 0, 100, 10));
1911 assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
1914
1915 #[test]
1916 fn fit_content_zero_area_returns_empty_rects() {
1917 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1918 let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
1919 min: 5,
1920 preferred: 10,
1921 max: None,
1922 });
1923 assert_eq!(rects.len(), 2);
1924 assert_eq!(rects[0].width, 0);
1925 assert_eq!(rects[0].height, 0);
1926 assert_eq!(rects[1].width, 0);
1927 assert_eq!(rects[1].height, 0);
1928 }
1929
1930 #[test]
1931 fn fit_content_tiny_available_clamps_to_remaining() {
1932 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1933 let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
1934 min: 5,
1935 preferred: 10,
1936 max: None,
1937 });
1938 assert_eq!(rects[0].width, 1);
1939 assert_eq!(rects[1].width, 0);
1940 }
1941
1942 #[test]
1945 fn fit_content_bounded_clamps_to_min() {
1946 let flex = Flex::horizontal().constraints([
1947 Constraint::FitContentBounded { min: 20, max: 50 },
1948 Constraint::Fill,
1949 ]);
1950 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1951 min: 5,
1952 preferred: 10, max: None,
1954 });
1955 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
1957 }
1958
1959 #[test]
1960 fn fit_content_bounded_respects_small_available() {
1961 let flex = Flex::horizontal().constraints([
1962 Constraint::FitContentBounded { min: 20, max: 50 },
1963 Constraint::Fill,
1964 ]);
1965 let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
1966 min: 5,
1967 preferred: 10,
1968 max: None,
1969 });
1970 assert_eq!(rects[0].width, 5);
1972 assert_eq!(rects[1].width, 0);
1973 }
1974
1975 #[test]
1976 fn fit_content_bounded_clamps_to_max() {
1977 let flex = Flex::horizontal().constraints([
1978 Constraint::FitContentBounded { min: 10, max: 30 },
1979 Constraint::Fill,
1980 ]);
1981 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1982 min: 5,
1983 preferred: 50, max: None,
1985 });
1986 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
1988 }
1989
1990 #[test]
1991 fn fit_content_bounded_uses_preferred_when_in_range() {
1992 let flex = Flex::horizontal().constraints([
1993 Constraint::FitContentBounded { min: 10, max: 50 },
1994 Constraint::Fill,
1995 ]);
1996 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1997 min: 5,
1998 preferred: 35, max: None,
2000 });
2001 assert_eq!(rects[0].width, 35);
2002 assert_eq!(rects[1].width, 65);
2003 }
2004
2005 #[test]
2008 fn fit_min_uses_minimum_size() {
2009 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
2010 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
2011 if idx == 0 {
2012 LayoutSizeHint {
2013 min: 15,
2014 preferred: 40,
2015 max: None,
2016 }
2017 } else {
2018 LayoutSizeHint::ZERO
2019 }
2020 });
2021 assert_eq!(rects[0].width, 15, "FitMin should strict size to min");
2024 assert_eq!(rects[1].width, 85, "Fill should take remaining space");
2025 }
2026
2027 #[test]
2028 fn fit_min_without_measurer_gets_zero() {
2029 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
2030 let rects = flex.split(Rect::new(0, 0, 100, 10));
2031 assert_eq!(rects[0].width, 0);
2034 assert_eq!(rects[1].width, 100);
2035 }
2036
2037 #[test]
2040 fn layout_size_hint_zero_is_default() {
2041 assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
2042 }
2043
2044 #[test]
2045 fn layout_size_hint_exact() {
2046 let h = LayoutSizeHint::exact(25);
2047 assert_eq!(h.min, 25);
2048 assert_eq!(h.preferred, 25);
2049 assert_eq!(h.max, Some(25));
2050 }
2051
2052 #[test]
2053 fn layout_size_hint_at_least() {
2054 let h = LayoutSizeHint::at_least(10, 30);
2055 assert_eq!(h.min, 10);
2056 assert_eq!(h.preferred, 30);
2057 assert_eq!(h.max, None);
2058 }
2059
2060 #[test]
2061 fn layout_size_hint_clamp() {
2062 let h = LayoutSizeHint {
2063 min: 10,
2064 preferred: 20,
2065 max: Some(30),
2066 };
2067 assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
2071
2072 #[test]
2073 fn layout_size_hint_clamp_unbounded() {
2074 let h = LayoutSizeHint::at_least(5, 10);
2075 assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
2078
2079 #[test]
2080 fn layout_size_hint_clamp_min_greater_than_max() {
2081 let h = LayoutSizeHint {
2083 min: 20,
2084 preferred: 20,
2085 max: Some(10),
2086 };
2087 assert_eq!(h.clamp(5), 20); assert_eq!(h.clamp(15), 20); assert_eq!(h.clamp(25), 20); }
2091
2092 #[test]
2095 fn fit_content_with_fixed_and_fill() {
2096 let flex = Flex::horizontal().constraints([
2097 Constraint::Fixed(20),
2098 Constraint::FitContent,
2099 Constraint::Fill,
2100 ]);
2101 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
2102 if idx == 1 {
2103 LayoutSizeHint {
2104 min: 5,
2105 preferred: 25,
2106 max: None,
2107 }
2108 } else {
2109 LayoutSizeHint::ZERO
2110 }
2111 });
2112 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
2116
2117 #[test]
2118 fn total_allocation_never_exceeds_available_with_fit_content() {
2119 for available in [10u16, 50, 100, 255] {
2120 let flex = Flex::horizontal().constraints([
2121 Constraint::FitContent,
2122 Constraint::FitContent,
2123 Constraint::Fill,
2124 ]);
2125 let rects =
2126 flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
2127 min: 10,
2128 preferred: 40,
2129 max: None,
2130 });
2131 let total: u16 = rects.iter().map(|r| r.width).sum();
2132 assert!(
2133 total <= available,
2134 "Total {} exceeded available {} with FitContent",
2135 total,
2136 available
2137 );
2138 }
2139 }
2140
2141 mod rounding_tests {
2146 use super::super::*;
2147
2148 #[test]
2151 fn rounding_conserves_sum_exact() {
2152 let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
2153 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2154 assert_eq!(result.as_slice(), &[10u16, 20u16, 10u16]);
2155 }
2156
2157 #[test]
2158 fn rounding_conserves_sum_fractional() {
2159 let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
2160 assert_eq!(
2161 result.iter().copied().sum::<u16>(),
2162 40,
2163 "Sum must equal total: {:?}",
2164 result
2165 );
2166 }
2167
2168 #[test]
2169 fn rounding_conserves_sum_many_fractions() {
2170 let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
2171 let result = round_layout_stable(&targets, 100, None);
2172 assert_eq!(
2173 result.iter().copied().sum::<u16>(),
2174 100,
2175 "Sum must be exactly 100: {:?}",
2176 result
2177 );
2178 }
2179
2180 #[test]
2181 fn rounding_conserves_sum_all_half() {
2182 let targets = vec![10.5, 10.5, 10.5, 10.5];
2183 let result = round_layout_stable(&targets, 42, None);
2184 assert_eq!(
2185 result.iter().copied().sum::<u16>(),
2186 42,
2187 "Sum must be exactly 42: {:?}",
2188 result
2189 );
2190 }
2191
2192 #[test]
2195 fn rounding_displacement_bounded() {
2196 let targets = vec![33.33, 33.33, 33.34];
2197 let result = round_layout_stable(&targets, 100, None);
2198 assert_eq!(result.iter().copied().sum::<u16>(), 100);
2199
2200 for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
2201 let floor = r.floor() as u16;
2202 let ceil = floor + 1;
2203 assert!(
2204 x == floor || x == ceil,
2205 "Element {} = {} not in {{floor={}, ceil={}}} of target {}",
2206 i,
2207 x,
2208 floor,
2209 ceil,
2210 r
2211 );
2212 }
2213 }
2214
2215 #[test]
2218 fn temporal_tiebreak_stable_when_unchanged() {
2219 let targets = vec![10.5, 10.5, 10.5, 10.5];
2220 let first = round_layout_stable(&targets, 42, None);
2221 let second = round_layout_stable(&targets, 42, Some(first.clone()));
2222 assert_eq!(
2223 first, second,
2224 "Identical targets should produce identical results"
2225 );
2226 }
2227
2228 #[test]
2229 fn temporal_tiebreak_prefers_previous_direction() {
2230 let targets = vec![10.5, 10.5];
2231 let total = 21;
2232 let first = round_layout_stable(&targets, total, None);
2233 assert_eq!(first.iter().copied().sum::<u16>(), total);
2234 let second = round_layout_stable(&targets, total, Some(first.clone()));
2235 assert_eq!(first, second, "Should maintain rounding direction");
2236 }
2237
2238 #[test]
2239 fn temporal_tiebreak_adapts_to_changed_targets() {
2240 let targets_a = vec![10.5, 10.5];
2241 let result_a = round_layout_stable(&targets_a, 21, None);
2242 let targets_b = vec![15.7, 5.3];
2243 let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
2244 assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
2245 assert!(result_b[0] > result_b[1], "Should follow larger target");
2246 }
2247
2248 #[test]
2251 fn property_min_displacement_brute_force_small() {
2252 let targets = vec![3.3, 3.3, 3.4];
2253 let total: u16 = 10;
2254 let result = round_layout_stable(&targets, total, None);
2255 let our_displacement: f64 = result
2256 .iter()
2257 .zip(targets.iter())
2258 .map(|(&x, &r)| (x as f64 - r).abs())
2259 .sum();
2260
2261 let mut min_displacement = f64::MAX;
2262 let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
2263 let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
2264
2265 for a in floors[0]..=ceils[0] {
2266 for b in floors[1]..=ceils[1] {
2267 for c in floors[2]..=ceils[2] {
2268 if a + b + c == total {
2269 let disp = (a as f64 - targets[0]).abs()
2270 + (b as f64 - targets[1]).abs()
2271 + (c as f64 - targets[2]).abs();
2272 if disp < min_displacement {
2273 min_displacement = disp;
2274 }
2275 }
2276 }
2277 }
2278 }
2279
2280 assert!(
2281 (our_displacement - min_displacement).abs() < 1e-10,
2282 "Our displacement {} should match optimal {}: {:?}",
2283 our_displacement,
2284 min_displacement,
2285 result
2286 );
2287 }
2288
2289 #[test]
2292 fn rounding_deterministic() {
2293 let targets = vec![7.7, 8.3, 14.0];
2294 let a = round_layout_stable(&targets, 30, None);
2295 let b = round_layout_stable(&targets, 30, None);
2296 assert_eq!(a, b, "Same inputs must produce identical outputs");
2297 }
2298
2299 #[test]
2302 fn rounding_empty_targets() {
2303 let result = round_layout_stable(&[], 0, None);
2304 assert!(result.is_empty());
2305 }
2306
2307 #[test]
2308 fn rounding_single_element() {
2309 let result = round_layout_stable(&[10.7], 11, None);
2310 assert_eq!(result.as_slice(), &[11u16]);
2311 }
2312
2313 #[test]
2314 fn rounding_zero_total() {
2315 let result = round_layout_stable(&[5.0, 5.0], 0, None);
2316 assert_eq!(result.iter().copied().sum::<u16>(), 0);
2317 }
2318
2319 #[test]
2320 fn rounding_zero_total_with_large_overflow_reaches_zero() {
2321 let result = round_layout_stable(&[65535.0, 65535.0], 0, None);
2322 assert_eq!(result.as_slice(), &[0u16, 0u16]);
2323 assert_eq!(result.iter().copied().sum::<u16>(), 0);
2324 }
2325
2326 #[test]
2327 fn rounding_all_zeros() {
2328 let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
2329 assert_eq!(result.as_slice(), &[0u16, 0u16, 0u16]);
2330 }
2331
2332 #[test]
2333 fn rounding_integer_targets() {
2334 let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
2335 assert_eq!(result.as_slice(), &[10u16, 20u16, 30u16]);
2336 }
2337
2338 #[test]
2339 fn rounding_large_deficit() {
2340 let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
2341 assert_eq!(result.iter().copied().sum::<u16>(), 3);
2342 assert_eq!(result.as_slice(), &[1u16, 1u16, 1u16]);
2343 }
2344
2345 #[test]
2346 fn rounding_with_prev_different_length() {
2347 let result = round_layout_stable(
2348 &[10.5, 10.5],
2349 21,
2350 Some(smallvec::smallvec![11u16, 10u16, 5u16]),
2351 );
2352 assert_eq!(result.iter().copied().sum::<u16>(), 21);
2353 }
2354
2355 #[test]
2356 fn rounding_very_small_fractions() {
2357 let targets = vec![10.001, 20.001, 9.998];
2358 let result = round_layout_stable(&targets, 40, None);
2359 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2360 }
2361
2362 #[test]
2363 fn rounding_conserves_sum_stress() {
2364 let n = 50;
2365 let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
2366 let total = 120u16;
2367 let result = round_layout_stable(&targets, total, None);
2368 assert_eq!(
2369 result.iter().copied().sum::<u16>(),
2370 total,
2371 "Sum must be exactly {} for {} items: {:?}",
2372 total,
2373 n,
2374 result
2375 );
2376 }
2377 }
2378
2379 mod property_constraint_tests {
2384 use super::super::*;
2385
2386 struct Lcg(u64);
2388
2389 impl Lcg {
2390 fn new(seed: u64) -> Self {
2391 Self(seed)
2392 }
2393 fn next_u32(&mut self) -> u32 {
2394 self.0 = self
2395 .0
2396 .wrapping_mul(6_364_136_223_846_793_005)
2397 .wrapping_add(1);
2398 (self.0 >> 33) as u32
2399 }
2400 fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
2401 if lo >= hi {
2402 return lo;
2403 }
2404 lo + (self.next_u32() % (hi - lo) as u32) as u16
2405 }
2406 fn next_f32(&mut self) -> f32 {
2407 (self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
2408 }
2409 }
2410
2411 fn random_constraint(rng: &mut Lcg) -> Constraint {
2413 match rng.next_u32() % 7 {
2414 0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
2415 1 => Constraint::Percentage(rng.next_f32() * 100.0),
2416 2 => Constraint::Min(rng.next_u16_range(0, 40)),
2417 3 => Constraint::Max(rng.next_u16_range(5, 120)),
2418 4 => {
2419 let n = rng.next_u32() % 5 + 1;
2420 let d = rng.next_u32() % 5 + 1;
2421 Constraint::Ratio(n, d)
2422 }
2423 5 => Constraint::Fill,
2424 _ => Constraint::FitContent,
2425 }
2426 }
2427
2428 #[test]
2429 fn property_constraints_respected_fixed() {
2430 let mut rng = Lcg::new(0xDEAD_BEEF);
2431 for _ in 0..200 {
2432 let fixed_val = rng.next_u16_range(1, 60);
2433 let avail = rng.next_u16_range(10, 200);
2434 let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
2435 let rects = flex.split(Rect::new(0, 0, avail, 10));
2436 assert!(
2437 rects[0].width <= fixed_val.min(avail),
2438 "Fixed({}) in avail {} -> width {}",
2439 fixed_val,
2440 avail,
2441 rects[0].width
2442 );
2443 }
2444 }
2445
2446 #[test]
2447 fn property_constraints_respected_max() {
2448 let mut rng = Lcg::new(0xCAFE_BABE);
2449 for _ in 0..200 {
2450 let max_val = rng.next_u16_range(5, 80);
2451 let avail = rng.next_u16_range(10, 200);
2452 let flex =
2453 Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
2454 let rects = flex.split(Rect::new(0, 0, avail, 10));
2455 assert!(
2456 rects[0].width <= max_val,
2457 "Max({}) in avail {} -> width {}",
2458 max_val,
2459 avail,
2460 rects[0].width
2461 );
2462 }
2463 }
2464
2465 #[test]
2466 fn property_constraints_respected_min() {
2467 let mut rng = Lcg::new(0xBAAD_F00D);
2468 for _ in 0..200 {
2469 let min_val = rng.next_u16_range(0, 40);
2470 let avail = rng.next_u16_range(min_val.max(1), 200);
2471 let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
2472 let rects = flex.split(Rect::new(0, 0, avail, 10));
2473 assert!(
2474 rects[0].width >= min_val,
2475 "Min({}) in avail {} -> width {}",
2476 min_val,
2477 avail,
2478 rects[0].width
2479 );
2480 }
2481 }
2482
2483 #[test]
2484 fn property_constraints_respected_ratio_proportional() {
2485 let mut rng = Lcg::new(0x1234_5678);
2486 for _ in 0..200 {
2487 let n1 = rng.next_u32() % 5 + 1;
2488 let n2 = rng.next_u32() % 5 + 1;
2489 let d = n1 + n2;
2490 let avail = rng.next_u16_range(20, 200);
2491 let flex = Flex::horizontal()
2492 .constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
2493 let rects = flex.split(Rect::new(0, 0, avail, 10));
2494 let w1 = rects[0].width as f64;
2495 let w2 = rects[1].width as f64;
2496 let total = w1 + w2;
2497 if total > 0.0 {
2498 let expected_ratio = n1 as f64 / d as f64;
2499 let actual_ratio = w1 / total;
2500 assert!(
2501 (actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
2502 "Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
2503 n1,
2504 d,
2505 n1,
2506 n2,
2507 avail,
2508 expected_ratio,
2509 actual_ratio,
2510 w1,
2511 w2
2512 );
2513 }
2514 }
2515 }
2516
2517 #[test]
2518 fn property_total_allocation_never_exceeds_available() {
2519 let mut rng = Lcg::new(0xFACE_FEED);
2520 for _ in 0..500 {
2521 let n = (rng.next_u32() % 6 + 1) as usize;
2522 let constraints: Vec<Constraint> =
2523 (0..n).map(|_| random_constraint(&mut rng)).collect();
2524 let avail = rng.next_u16_range(5, 200);
2525 let dir = if rng.next_u32().is_multiple_of(2) {
2526 Direction::Horizontal
2527 } else {
2528 Direction::Vertical
2529 };
2530 let flex = Flex::default().direction(dir).constraints(constraints);
2531 let area = Rect::new(0, 0, avail, avail);
2532 let rects = flex.split(area);
2533 let total: u16 = rects
2534 .iter()
2535 .map(|r| match dir {
2536 Direction::Horizontal => r.width,
2537 Direction::Vertical => r.height,
2538 })
2539 .sum();
2540 assert!(
2541 total <= avail,
2542 "Total {} exceeded available {} with {} constraints",
2543 total,
2544 avail,
2545 n
2546 );
2547 }
2548 }
2549
2550 #[test]
2551 fn property_no_overlap_horizontal() {
2552 let mut rng = Lcg::new(0xABCD_1234);
2553 for _ in 0..300 {
2554 let n = (rng.next_u32() % 5 + 2) as usize;
2555 let constraints: Vec<Constraint> =
2556 (0..n).map(|_| random_constraint(&mut rng)).collect();
2557 let avail = rng.next_u16_range(20, 200);
2558 let flex = Flex::horizontal().constraints(constraints);
2559 let rects = flex.split(Rect::new(0, 0, avail, 10));
2560
2561 for i in 1..rects.len() {
2562 let prev_end = rects[i - 1].x + rects[i - 1].width;
2563 assert!(
2564 rects[i].x >= prev_end,
2565 "Overlap at {}: prev ends {}, next starts {}",
2566 i,
2567 prev_end,
2568 rects[i].x
2569 );
2570 }
2571 }
2572 }
2573
2574 #[test]
2575 fn property_deterministic_across_runs() {
2576 let mut rng = Lcg::new(0x9999_8888);
2577 for _ in 0..100 {
2578 let n = (rng.next_u32() % 5 + 1) as usize;
2579 let constraints: Vec<Constraint> =
2580 (0..n).map(|_| random_constraint(&mut rng)).collect();
2581 let avail = rng.next_u16_range(10, 200);
2582 let r1 = Flex::horizontal()
2583 .constraints(constraints.clone())
2584 .split(Rect::new(0, 0, avail, 10));
2585 let r2 = Flex::horizontal()
2586 .constraints(constraints)
2587 .split(Rect::new(0, 0, avail, 10));
2588 assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
2589 }
2590 }
2591 }
2592
2593 mod property_temporal_tests {
2598 use super::super::*;
2599 use crate::cache::{CoherenceCache, CoherenceId};
2600
2601 struct Lcg(u64);
2603
2604 impl Lcg {
2605 fn new(seed: u64) -> Self {
2606 Self(seed)
2607 }
2608 fn next_u32(&mut self) -> u32 {
2609 self.0 = self
2610 .0
2611 .wrapping_mul(6_364_136_223_846_793_005)
2612 .wrapping_add(1);
2613 (self.0 >> 33) as u32
2614 }
2615 }
2616
2617 #[test]
2618 fn property_temporal_stability_small_resize() {
2619 let constraints = [
2620 Constraint::Percentage(33.3),
2621 Constraint::Percentage(33.3),
2622 Constraint::Fill,
2623 ];
2624 let mut coherence = CoherenceCache::new(64);
2625 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2626
2627 for total in [80u16, 100, 120] {
2628 let flex = Flex::horizontal().constraints(constraints);
2629 let rects = flex.split(Rect::new(0, 0, total, 10));
2630 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2631
2632 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2633 let prev = coherence.get(&id);
2634 let rounded = round_layout_stable(&targets, total, prev);
2635
2636 if let Some(old) = coherence.get(&id) {
2637 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2638 assert!(
2639 max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
2640 "max_disp={} too large for size change {} -> {}",
2641 max_disp,
2642 old.iter().copied().sum::<u16>(),
2643 total
2644 );
2645 let _ = sum_disp;
2646 }
2647 coherence.store(id, rounded);
2648 }
2649 }
2650
2651 #[test]
2652 fn property_temporal_stability_random_walk() {
2653 let constraints = [
2654 Constraint::Ratio(1, 3),
2655 Constraint::Ratio(1, 3),
2656 Constraint::Ratio(1, 3),
2657 ];
2658 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2659 let mut coherence = CoherenceCache::new(64);
2660 let mut rng = Lcg::new(0x5555_AAAA);
2661 let mut total: u16 = 90;
2662
2663 for step in 0..200 {
2664 let prev_total = total;
2665 let delta = (rng.next_u32() % 7) as i32 - 3;
2666 total = (total as i32 + delta).clamp(10, 250) as u16;
2667
2668 let flex = Flex::horizontal().constraints(constraints);
2669 let rects = flex.split(Rect::new(0, 0, total, 10));
2670 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2671
2672 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2673 let prev = coherence.get(&id);
2674 let rounded = round_layout_stable(&targets, total, prev);
2675
2676 if coherence.get(&id).is_some() {
2677 let (_, max_disp) = coherence.displacement(&id, &rounded);
2678 let size_change = total.abs_diff(prev_total);
2679 assert!(
2680 max_disp <= size_change as u32 + 2,
2681 "step {}: max_disp={} exceeds size_change={} + 2",
2682 step,
2683 max_disp,
2684 size_change
2685 );
2686 }
2687 coherence.store(id, rounded);
2688 }
2689 }
2690
2691 #[test]
2692 fn property_temporal_stability_identical_frames() {
2693 let constraints = [
2694 Constraint::Fixed(20),
2695 Constraint::Fill,
2696 Constraint::Fixed(15),
2697 ];
2698 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2699 let mut coherence = CoherenceCache::new(64);
2700
2701 let flex = Flex::horizontal().constraints(constraints);
2702 let rects = flex.split(Rect::new(0, 0, 100, 10));
2703 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2704 coherence.store(id, widths.iter().copied().collect());
2705
2706 for _ in 0..10 {
2707 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2708 let prev = coherence.get(&id);
2709 let rounded = round_layout_stable(&targets, 100, prev);
2710 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2711 assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
2712 coherence.store(id, rounded);
2713 }
2714 }
2715
2716 #[test]
2717 fn property_temporal_coherence_sweep() {
2718 let constraints = [
2719 Constraint::Percentage(25.0),
2720 Constraint::Percentage(50.0),
2721 Constraint::Fill,
2722 ];
2723 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2724 let mut coherence = CoherenceCache::new(64);
2725 let mut total_displacement: u64 = 0;
2726
2727 for total in 60u16..=140 {
2728 let flex = Flex::horizontal().constraints(constraints);
2729 let rects = flex.split(Rect::new(0, 0, total, 10));
2730 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2731
2732 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2733 let prev = coherence.get(&id);
2734 let rounded = round_layout_stable(&targets, total, prev);
2735
2736 if coherence.get(&id).is_some() {
2737 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2738 total_displacement += sum_disp;
2739 }
2740 coherence.store(id, rounded);
2741 }
2742
2743 assert!(
2744 total_displacement <= 80 * 3,
2745 "Total displacement {} exceeds bound for 80-step sweep",
2746 total_displacement
2747 );
2748 }
2749 }
2750
2751 mod snapshot_layout_tests {
2756 use super::super::*;
2757 use crate::grid::{Grid, GridArea};
2758
2759 fn snapshot_flex(
2760 constraints: &[Constraint],
2761 dir: Direction,
2762 width: u16,
2763 height: u16,
2764 ) -> String {
2765 let flex = Flex::default()
2766 .direction(dir)
2767 .constraints(constraints.iter().copied());
2768 let rects = flex.split(Rect::new(0, 0, width, height));
2769 let mut out = format!(
2770 "Flex {:?} {}x{} ({} constraints)\n",
2771 dir,
2772 width,
2773 height,
2774 constraints.len()
2775 );
2776 for (i, r) in rects.iter().enumerate() {
2777 out.push_str(&format!(
2778 " [{}] x={} y={} w={} h={}\n",
2779 i, r.x, r.y, r.width, r.height
2780 ));
2781 }
2782 let total: u16 = rects
2783 .iter()
2784 .map(|r| match dir {
2785 Direction::Horizontal => r.width,
2786 Direction::Vertical => r.height,
2787 })
2788 .sum();
2789 out.push_str(&format!(" total={}\n", total));
2790 out
2791 }
2792
2793 fn snapshot_grid(
2794 rows: &[Constraint],
2795 cols: &[Constraint],
2796 areas: &[(&str, GridArea)],
2797 width: u16,
2798 height: u16,
2799 ) -> String {
2800 let mut grid = Grid::new()
2801 .rows(rows.iter().copied())
2802 .columns(cols.iter().copied());
2803 for &(name, area) in areas {
2804 grid = grid.area(name, area);
2805 }
2806 let layout = grid.split(Rect::new(0, 0, width, height));
2807
2808 let mut out = format!(
2809 "Grid {}x{} ({}r x {}c)\n",
2810 width,
2811 height,
2812 rows.len(),
2813 cols.len()
2814 );
2815 for r in 0..rows.len() {
2816 for c in 0..cols.len() {
2817 let rect = layout.cell(r, c);
2818 out.push_str(&format!(
2819 " [{},{}] x={} y={} w={} h={}\n",
2820 r, c, rect.x, rect.y, rect.width, rect.height
2821 ));
2822 }
2823 }
2824 for &(name, _) in areas {
2825 if let Some(rect) = layout.area(name) {
2826 out.push_str(&format!(
2827 " area({}) x={} y={} w={} h={}\n",
2828 name, rect.x, rect.y, rect.width, rect.height
2829 ));
2830 }
2831 }
2832 out
2833 }
2834
2835 #[test]
2838 fn snapshot_flex_thirds_80x24() {
2839 let snap = snapshot_flex(
2840 &[
2841 Constraint::Ratio(1, 3),
2842 Constraint::Ratio(1, 3),
2843 Constraint::Ratio(1, 3),
2844 ],
2845 Direction::Horizontal,
2846 80,
2847 24,
2848 );
2849 assert_eq!(
2850 snap,
2851 "\
2852Flex Horizontal 80x24 (3 constraints)
2853 [0] x=0 y=0 w=26 h=24
2854 [1] x=26 y=0 w=26 h=24
2855 [2] x=52 y=0 w=26 h=24
2856 total=78
2857"
2858 );
2859 }
2860
2861 #[test]
2862 fn snapshot_flex_sidebar_content_80x24() {
2863 let snap = snapshot_flex(
2864 &[Constraint::Fixed(20), Constraint::Fill],
2865 Direction::Horizontal,
2866 80,
2867 24,
2868 );
2869 assert_eq!(
2870 snap,
2871 "\
2872Flex Horizontal 80x24 (2 constraints)
2873 [0] x=0 y=0 w=20 h=24
2874 [1] x=20 y=0 w=60 h=24
2875 total=80
2876"
2877 );
2878 }
2879
2880 #[test]
2881 fn snapshot_flex_header_body_footer_80x24() {
2882 let snap = snapshot_flex(
2883 &[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
2884 Direction::Vertical,
2885 80,
2886 24,
2887 );
2888 assert_eq!(
2889 snap,
2890 "\
2891Flex Vertical 80x24 (3 constraints)
2892 [0] x=0 y=0 w=80 h=3
2893 [1] x=0 y=3 w=80 h=20
2894 [2] x=0 y=23 w=80 h=1
2895 total=24
2896"
2897 );
2898 }
2899
2900 #[test]
2903 fn snapshot_flex_thirds_120x40() {
2904 let snap = snapshot_flex(
2905 &[
2906 Constraint::Ratio(1, 3),
2907 Constraint::Ratio(1, 3),
2908 Constraint::Ratio(1, 3),
2909 ],
2910 Direction::Horizontal,
2911 120,
2912 40,
2913 );
2914 assert_eq!(
2915 snap,
2916 "\
2917Flex Horizontal 120x40 (3 constraints)
2918 [0] x=0 y=0 w=40 h=40
2919 [1] x=40 y=0 w=40 h=40
2920 [2] x=80 y=0 w=40 h=40
2921 total=120
2922"
2923 );
2924 }
2925
2926 #[test]
2927 fn snapshot_flex_sidebar_content_120x40() {
2928 let snap = snapshot_flex(
2929 &[Constraint::Fixed(20), Constraint::Fill],
2930 Direction::Horizontal,
2931 120,
2932 40,
2933 );
2934 assert_eq!(
2935 snap,
2936 "\
2937Flex Horizontal 120x40 (2 constraints)
2938 [0] x=0 y=0 w=20 h=40
2939 [1] x=20 y=0 w=100 h=40
2940 total=120
2941"
2942 );
2943 }
2944
2945 #[test]
2946 fn snapshot_flex_percentage_mix_120x40() {
2947 let snap = snapshot_flex(
2948 &[
2949 Constraint::Percentage(25.0),
2950 Constraint::Percentage(50.0),
2951 Constraint::Fill,
2952 ],
2953 Direction::Horizontal,
2954 120,
2955 40,
2956 );
2957 assert_eq!(
2958 snap,
2959 "\
2960Flex Horizontal 120x40 (3 constraints)
2961 [0] x=0 y=0 w=30 h=40
2962 [1] x=30 y=0 w=60 h=40
2963 [2] x=90 y=0 w=30 h=40
2964 total=120
2965"
2966 );
2967 }
2968
2969 #[test]
2972 fn snapshot_grid_2x2_80x24() {
2973 let snap = snapshot_grid(
2974 &[Constraint::Fixed(3), Constraint::Fill],
2975 &[Constraint::Fixed(20), Constraint::Fill],
2976 &[
2977 ("header", GridArea::span(0, 0, 1, 2)),
2978 ("sidebar", GridArea::span(1, 0, 1, 1)),
2979 ("content", GridArea::cell(1, 1)),
2980 ],
2981 80,
2982 24,
2983 );
2984 assert_eq!(
2985 snap,
2986 "\
2987Grid 80x24 (2r x 2c)
2988 [0,0] x=0 y=0 w=20 h=3
2989 [0,1] x=20 y=0 w=60 h=3
2990 [1,0] x=0 y=3 w=20 h=21
2991 [1,1] x=20 y=3 w=60 h=21
2992 area(header) x=0 y=0 w=80 h=3
2993 area(sidebar) x=0 y=3 w=20 h=21
2994 area(content) x=20 y=3 w=60 h=21
2995"
2996 );
2997 }
2998
2999 #[test]
3000 fn snapshot_grid_3x3_80x24() {
3001 let snap = snapshot_grid(
3002 &[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
3003 &[
3004 Constraint::Fixed(10),
3005 Constraint::Fill,
3006 Constraint::Fixed(10),
3007 ],
3008 &[],
3009 80,
3010 24,
3011 );
3012 assert_eq!(
3013 snap,
3014 "\
3015Grid 80x24 (3r x 3c)
3016 [0,0] x=0 y=0 w=10 h=1
3017 [0,1] x=10 y=0 w=60 h=1
3018 [0,2] x=70 y=0 w=10 h=1
3019 [1,0] x=0 y=1 w=10 h=22
3020 [1,1] x=10 y=1 w=60 h=22
3021 [1,2] x=70 y=1 w=10 h=22
3022 [2,0] x=0 y=23 w=10 h=1
3023 [2,1] x=10 y=23 w=60 h=1
3024 [2,2] x=70 y=23 w=10 h=1
3025"
3026 );
3027 }
3028
3029 #[test]
3032 fn snapshot_grid_2x2_120x40() {
3033 let snap = snapshot_grid(
3034 &[Constraint::Fixed(3), Constraint::Fill],
3035 &[Constraint::Fixed(20), Constraint::Fill],
3036 &[
3037 ("header", GridArea::span(0, 0, 1, 2)),
3038 ("sidebar", GridArea::span(1, 0, 1, 1)),
3039 ("content", GridArea::cell(1, 1)),
3040 ],
3041 120,
3042 40,
3043 );
3044 assert_eq!(
3045 snap,
3046 "\
3047Grid 120x40 (2r x 2c)
3048 [0,0] x=0 y=0 w=20 h=3
3049 [0,1] x=20 y=0 w=100 h=3
3050 [1,0] x=0 y=3 w=20 h=37
3051 [1,1] x=20 y=3 w=100 h=37
3052 area(header) x=0 y=0 w=120 h=3
3053 area(sidebar) x=0 y=3 w=20 h=37
3054 area(content) x=20 y=3 w=100 h=37
3055"
3056 );
3057 }
3058
3059 #[test]
3060 fn snapshot_grid_dashboard_120x40() {
3061 let snap = snapshot_grid(
3062 &[
3063 Constraint::Fixed(3),
3064 Constraint::Percentage(60.0),
3065 Constraint::Fill,
3066 ],
3067 &[Constraint::Percentage(30.0), Constraint::Fill],
3068 &[
3069 ("nav", GridArea::span(0, 0, 1, 2)),
3070 ("chart", GridArea::cell(1, 0)),
3071 ("detail", GridArea::cell(1, 1)),
3072 ("log", GridArea::span(2, 0, 1, 2)),
3073 ],
3074 120,
3075 40,
3076 );
3077 assert_eq!(
3078 snap,
3079 "\
3080Grid 120x40 (3r x 2c)
3081 [0,0] x=0 y=0 w=36 h=3
3082 [0,1] x=36 y=0 w=84 h=3
3083 [1,0] x=0 y=3 w=36 h=24
3084 [1,1] x=36 y=3 w=84 h=24
3085 [2,0] x=0 y=27 w=36 h=13
3086 [2,1] x=36 y=27 w=84 h=13
3087 area(nav) x=0 y=0 w=120 h=3
3088 area(chart) x=0 y=3 w=36 h=24
3089 area(detail) x=36 y=3 w=84 h=24
3090 area(log) x=0 y=27 w=120 h=13
3091"
3092 );
3093 }
3094 }
3095}