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 remainder = r - (floors[i] as f64);
1242 let ceil_val = floors[i].saturating_add(1);
1243 let prev_used_ceil = prev
1245 .as_ref()
1246 .is_some_and(|p| p.get(i).copied() == Some(ceil_val));
1247 (i, remainder, prev_used_ceil)
1248 })
1249 .collect();
1250
1251 priority.sort_by(|a, b| {
1253 b.1.partial_cmp(&a.1)
1254 .unwrap_or(std::cmp::Ordering::Equal)
1255 .then_with(|| {
1256 b.2.cmp(&a.2)
1258 })
1259 .then_with(|| {
1260 a.0.cmp(&b.0)
1262 })
1263 });
1264
1265 let mut result = floors;
1267 let mut remaining_deficit = deficit;
1268
1269 if remaining_deficit as usize >= n {
1275 let per_item = remaining_deficit / (n as u16);
1276 for val in result.iter_mut() {
1277 *val = val.saturating_add(per_item);
1278 }
1279 remaining_deficit %= n as u16;
1280 }
1281
1282 if remaining_deficit > 0 {
1283 for &(i, _, _) in priority.iter().take(remaining_deficit as usize) {
1284 result[i] = result[i].saturating_add(1);
1285 }
1286 }
1287
1288 result
1289}
1290
1291fn redistribute_overflow(floors: &[u16], total: u16) -> Sizes {
1296 let mut result: Sizes = floors.iter().copied().collect();
1297 let current_sum: u64 = result.iter().map(|&x| u64::from(x)).sum();
1298 let total_u64 = u64::from(total);
1299 let n = result.len();
1300
1301 if current_sum <= total_u64 || n == 0 {
1302 return result;
1303 }
1304
1305 let mut overflow = current_sum - total_u64;
1306
1307 while overflow > 0 {
1308 let &max_val = result.iter().max().unwrap_or(&0);
1309 if max_val == 0 {
1310 for val in result.iter_mut() {
1314 *val = 0;
1315 }
1316 break;
1317 }
1318
1319 let count_max = result.iter().filter(|&&v| v == max_val).count() as u64;
1320 let &next_max = result.iter().filter(|&&v| v < max_val).max().unwrap_or(&0);
1321
1322 let delta = (max_val - next_max) as u64;
1323 let required_per_item = overflow.div_ceil(count_max);
1324 let reduce_per_item = delta.min(required_per_item).max(1) as u16;
1325
1326 let mut reduced_any = false;
1327 for val in result.iter_mut() {
1328 if *val == max_val {
1329 let amount = u64::from(*val)
1330 .min(u64::from(reduce_per_item))
1331 .min(overflow) as u16;
1332 if amount > 0 {
1333 *val -= amount;
1334 overflow -= u64::from(amount);
1335 reduced_any = true;
1336 }
1337 if overflow == 0 {
1338 break;
1339 }
1340 }
1341 }
1342
1343 if !reduced_any {
1344 for val in result.iter_mut() {
1346 if overflow == 0 {
1347 break;
1348 }
1349 if *val > 0 {
1350 *val -= 1;
1351 overflow -= 1;
1352 }
1353 }
1354 break;
1355 }
1356 }
1357
1358 result
1359}
1360
1361#[cfg(test)]
1362mod tests {
1363 use super::*;
1364
1365 #[test]
1366 fn fixed_split() {
1367 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(20)]);
1368 let rects = flex.split(Rect::new(0, 0, 100, 10));
1369 assert_eq!(rects.len(), 2);
1370 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1371 assert_eq!(rects[1], Rect::new(10, 0, 20, 10)); }
1373
1374 #[test]
1375 fn percentage_split() {
1376 let flex = Flex::horizontal()
1377 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1378 let rects = flex.split(Rect::new(0, 0, 100, 10));
1379 assert_eq!(rects[0].width, 50);
1380 assert_eq!(rects[1].width, 50);
1381 }
1382
1383 #[test]
1384 fn gap_handling() {
1385 let flex = Flex::horizontal()
1386 .gap(5)
1387 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1388 let rects = flex.split(Rect::new(0, 0, 100, 10));
1389 assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
1393 assert_eq!(rects[1], Rect::new(15, 0, 10, 10));
1394 }
1395
1396 #[test]
1397 fn mixed_constraints() {
1398 let flex = Flex::horizontal().constraints([
1399 Constraint::Fixed(10),
1400 Constraint::Min(10), Constraint::Percentage(10.0), ]);
1403
1404 let rects = flex.split(Rect::new(0, 0, 100, 1));
1412 assert_eq!(rects[0].width, 10); assert_eq!(rects[2].width, 10); assert_eq!(rects[1].width, 80); }
1416
1417 #[test]
1418 fn measurement_fixed_constraints() {
1419 let fixed = Measurement::fixed(5, 7);
1420 assert_eq!(fixed.min_width, 5);
1421 assert_eq!(fixed.min_height, 7);
1422 assert_eq!(fixed.max_width, Some(5));
1423 assert_eq!(fixed.max_height, Some(7));
1424 }
1425
1426 #[test]
1427 fn measurement_flexible_constraints() {
1428 let flexible = Measurement::flexible(2, 3);
1429 assert_eq!(flexible.min_width, 2);
1430 assert_eq!(flexible.min_height, 3);
1431 assert_eq!(flexible.max_width, None);
1432 assert_eq!(flexible.max_height, None);
1433 }
1434
1435 #[test]
1436 fn breakpoints_classify_defaults() {
1437 let bp = Breakpoints::DEFAULT;
1438 assert_eq!(bp.classify_width(20), Breakpoint::Xs);
1439 assert_eq!(bp.classify_width(60), Breakpoint::Sm);
1440 assert_eq!(bp.classify_width(90), Breakpoint::Md);
1441 assert_eq!(bp.classify_width(120), Breakpoint::Lg);
1442 }
1443
1444 #[test]
1445 fn breakpoints_at_least_and_between() {
1446 let bp = Breakpoints::new(50, 80, 110);
1447 assert!(bp.at_least(85, Breakpoint::Sm));
1448 assert!(bp.between(85, Breakpoint::Sm, Breakpoint::Md));
1449 assert!(!bp.between(85, Breakpoint::Lg, Breakpoint::Lg));
1450 }
1451
1452 #[test]
1453 fn alignment_end() {
1454 let flex = Flex::horizontal()
1455 .alignment(Alignment::End)
1456 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1457 let rects = flex.split(Rect::new(0, 0, 100, 10));
1458 assert_eq!(rects[0], Rect::new(80, 0, 10, 10));
1460 assert_eq!(rects[1], Rect::new(90, 0, 10, 10));
1461 }
1462
1463 #[test]
1464 fn alignment_center() {
1465 let flex = Flex::horizontal()
1466 .alignment(Alignment::Center)
1467 .constraints([Constraint::Fixed(20), Constraint::Fixed(20)]);
1468 let rects = flex.split(Rect::new(0, 0, 100, 10));
1469 assert_eq!(rects[0], Rect::new(30, 0, 20, 10));
1471 assert_eq!(rects[1], Rect::new(50, 0, 20, 10));
1472 }
1473
1474 #[test]
1475 fn alignment_space_between() {
1476 let flex = Flex::horizontal()
1477 .alignment(Alignment::SpaceBetween)
1478 .constraints([
1479 Constraint::Fixed(10),
1480 Constraint::Fixed(10),
1481 Constraint::Fixed(10),
1482 ]);
1483 let rects = flex.split(Rect::new(0, 0, 100, 10));
1484 assert_eq!(rects[0].x, 0);
1486 assert_eq!(rects[1].x, 45); assert_eq!(rects[2].x, 90); }
1489
1490 #[test]
1491 fn vertical_alignment() {
1492 let flex = Flex::vertical()
1493 .alignment(Alignment::End)
1494 .constraints([Constraint::Fixed(5), Constraint::Fixed(5)]);
1495 let rects = flex.split(Rect::new(0, 0, 10, 100));
1496 assert_eq!(rects[0], Rect::new(0, 90, 10, 5));
1498 assert_eq!(rects[1], Rect::new(0, 95, 10, 5));
1499 }
1500
1501 #[test]
1502 fn nested_flex_support() {
1503 let outer = Flex::horizontal()
1505 .constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
1506 let outer_rects = outer.split(Rect::new(0, 0, 100, 100));
1507
1508 let inner = Flex::vertical().constraints([Constraint::Fixed(30), Constraint::Min(10)]);
1510 let inner_rects = inner.split(outer_rects[0]);
1511
1512 assert_eq!(inner_rects[0], Rect::new(0, 0, 50, 30));
1513 assert_eq!(inner_rects[1], Rect::new(0, 30, 50, 70));
1514 }
1515
1516 #[test]
1518 fn invariant_total_size_does_not_exceed_available() {
1519 for total in [10u16, 50, 100, 255] {
1521 let flex = Flex::horizontal().constraints([
1522 Constraint::Fixed(30),
1523 Constraint::Percentage(50.0),
1524 Constraint::Min(20),
1525 ]);
1526 let rects = flex.split(Rect::new(0, 0, total, 10));
1527 let total_width: u16 = rects.iter().map(|r| r.width).sum();
1528 assert!(
1529 total_width <= total,
1530 "Total width {} exceeded available {} for constraints",
1531 total_width,
1532 total
1533 );
1534 }
1535 }
1536
1537 #[test]
1538 fn invariant_empty_area_produces_empty_rects() {
1539 let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1540 let rects = flex.split(Rect::new(0, 0, 0, 0));
1541 assert!(rects.iter().all(|r| r.is_empty()));
1542 }
1543
1544 #[test]
1545 fn invariant_no_constraints_produces_empty_vec() {
1546 let flex = Flex::horizontal().constraints([]);
1547 let rects = flex.split(Rect::new(0, 0, 100, 100));
1548 assert!(rects.is_empty());
1549 }
1550
1551 #[test]
1552 fn flex_constraints_stay_inline_for_common_layouts() {
1553 let flex = Flex::horizontal().constraints([Constraint::Fixed(1); LAYOUT_INLINE_CAP]);
1554 assert_eq!(flex.constraint_count(), LAYOUT_INLINE_CAP);
1555 assert!(!flex.constraints.spilled());
1556
1557 let rects = flex.split(Rect::new(0, 0, LAYOUT_INLINE_CAP as u16, 1));
1558 assert_eq!(rects.len(), LAYOUT_INLINE_CAP);
1559 assert!(rects.iter().all(|rect| rect.width == 1));
1560 }
1561
1562 #[test]
1565 fn ratio_constraint_splits_proportionally() {
1566 let flex =
1567 Flex::horizontal().constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
1568 let rects = flex.split(Rect::new(0, 0, 90, 10));
1569 assert_eq!(rects[0].width, 30);
1570 assert_eq!(rects[1].width, 60);
1571 }
1572
1573 #[test]
1574 fn ratio_constraint_with_zero_denominator() {
1575 let flex = Flex::horizontal().constraints([Constraint::Ratio(1, 0)]);
1577 let rects = flex.split(Rect::new(0, 0, 100, 10));
1578 assert_eq!(rects.len(), 1);
1579 }
1580
1581 #[test]
1582 fn ratio_is_absolute_fraction() {
1583 let area = Rect::new(0, 0, 100, 1);
1584
1585 let rects = Flex::horizontal()
1587 .constraints([Constraint::Percentage(25.0)])
1588 .split(area);
1589 assert_eq!(rects[0].width, 25);
1590
1591 let rects = Flex::horizontal()
1594 .constraints([Constraint::Ratio(1, 4)])
1595 .split(area);
1596 assert_eq!(rects[0].width, 25);
1597 }
1598
1599 #[test]
1600 fn ratio_is_independent_of_grow_items() {
1601 let area = Rect::new(0, 0, 100, 1);
1602
1603 let rects = Flex::horizontal()
1605 .constraints([Constraint::Ratio(1, 4), Constraint::Fill])
1606 .split(area);
1607 assert_eq!(rects[0].width, 25);
1608 assert_eq!(rects[1].width, 75);
1609 }
1610
1611 #[test]
1612 fn ratio_zero_numerator_should_be_zero() {
1613 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
1616 let rects = flex.split(Rect::new(0, 0, 100, 1));
1617
1618 assert_eq!(rects[0].width, 100, "Fill should take all space");
1620 assert_eq!(rects[1].width, 0, "Ratio(0, 1) should be width 0");
1621 }
1622
1623 #[test]
1626 fn max_constraint_clamps_size() {
1627 let flex = Flex::horizontal().constraints([Constraint::Max(20), Constraint::Fixed(30)]);
1628 let rects = flex.split(Rect::new(0, 0, 100, 10));
1629 assert!(rects[0].width <= 20);
1630 assert_eq!(rects[1].width, 30);
1631 }
1632
1633 #[test]
1634 fn percentage_rounding_never_exceeds_available() {
1635 let constraints = [
1636 Constraint::Percentage(33.4),
1637 Constraint::Percentage(33.3),
1638 Constraint::Percentage(33.3),
1639 ];
1640 let sizes = solve_constraints(&constraints, 7);
1641 let total: u16 = sizes.iter().sum();
1642 assert!(total <= 7, "percent rounding overflowed: {sizes:?}");
1643 assert!(sizes.iter().all(|size| *size <= 7));
1644 }
1645
1646 #[test]
1647 fn tiny_area_saturates_fixed_and_min() {
1648 let constraints = [Constraint::Fixed(5), Constraint::Min(3), Constraint::Max(2)];
1649 let sizes = solve_constraints(&constraints, 2);
1650 assert_eq!(sizes[0], 2);
1651 assert_eq!(sizes[1], 0);
1652 assert_eq!(sizes[2], 0);
1653 assert_eq!(sizes.iter().sum::<u16>(), 2);
1654 }
1655
1656 #[test]
1657 fn ratio_distribution_sums_to_available() {
1658 let constraints = [Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)];
1660 let sizes = solve_constraints(&constraints, 5);
1661 assert_eq!(sizes.iter().sum::<u16>(), 4);
1662 assert_eq!(sizes[0], 1);
1663 assert_eq!(sizes[1], 3);
1664 }
1665
1666 #[test]
1667 fn flex_gap_exceeds_area_yields_zero_widths() {
1668 let flex = Flex::horizontal()
1669 .gap(5)
1670 .constraints([Constraint::Fixed(1), Constraint::Fixed(1)]);
1671 let rects = flex.split(Rect::new(0, 0, 3, 1));
1672 assert_eq!(rects.len(), 2);
1673 assert_eq!(rects[0].width, 0);
1674 assert_eq!(rects[1].width, 0);
1675 }
1676
1677 #[test]
1680 fn alignment_space_around() {
1681 let flex = Flex::horizontal()
1682 .alignment(Alignment::SpaceAround)
1683 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1684 let rects = flex.split(Rect::new(0, 0, 100, 10));
1685
1686 assert_eq!(rects[0].x, 20);
1689 assert_eq!(rects[1].x, 70);
1690 }
1691
1692 #[test]
1695 fn vertical_gap() {
1696 let flex = Flex::vertical()
1697 .gap(5)
1698 .constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
1699 let rects = flex.split(Rect::new(0, 0, 50, 100));
1700 assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
1701 assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
1702 }
1703
1704 #[test]
1707 fn vertical_center() {
1708 let flex = Flex::vertical()
1709 .alignment(Alignment::Center)
1710 .constraints([Constraint::Fixed(10)]);
1711 let rects = flex.split(Rect::new(0, 0, 50, 100));
1712 assert_eq!(rects[0].y, 45);
1714 assert_eq!(rects[0].height, 10);
1715 }
1716
1717 #[test]
1720 fn single_min_takes_all() {
1721 let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
1722 let rects = flex.split(Rect::new(0, 0, 80, 24));
1723 assert_eq!(rects[0].width, 80);
1724 }
1725
1726 #[test]
1729 fn fixed_exceeds_available_clamped() {
1730 let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
1731 let rects = flex.split(Rect::new(0, 0, 100, 10));
1732 assert_eq!(rects[0].width, 60);
1734 assert_eq!(rects[1].width, 40);
1735 }
1736
1737 #[test]
1740 fn percentage_overflow_clamped() {
1741 let flex = Flex::horizontal()
1742 .constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
1743 let rects = flex.split(Rect::new(0, 0, 100, 10));
1744 assert_eq!(rects[0].width, 80);
1745 assert_eq!(rects[1].width, 20); }
1747
1748 #[test]
1751 fn margin_reduces_split_area() {
1752 let flex = Flex::horizontal()
1753 .margin(Sides::all(10))
1754 .constraints([Constraint::Fixed(20), Constraint::Min(0)]);
1755 let rects = flex.split(Rect::new(0, 0, 100, 100));
1756 assert_eq!(rects[0].x, 10);
1758 assert_eq!(rects[0].y, 10);
1759 assert_eq!(rects[0].width, 20);
1760 assert_eq!(rects[0].height, 80);
1761 }
1762
1763 #[test]
1766 fn builder_methods_chain() {
1767 let flex = Flex::vertical()
1768 .direction(Direction::Horizontal)
1769 .gap(3)
1770 .margin(Sides::all(1))
1771 .alignment(Alignment::End)
1772 .constraints([Constraint::Fixed(10)]);
1773 let rects = flex.split(Rect::new(0, 0, 50, 50));
1774 assert_eq!(rects.len(), 1);
1775 }
1776
1777 #[test]
1780 fn space_between_single_item() {
1781 let flex = Flex::horizontal()
1782 .alignment(Alignment::SpaceBetween)
1783 .constraints([Constraint::Fixed(10)]);
1784 let rects = flex.split(Rect::new(0, 0, 100, 10));
1785 assert_eq!(rects[0].x, 0);
1787 assert_eq!(rects[0].width, 10);
1788 }
1789
1790 #[test]
1791 fn invariant_rects_within_bounds() {
1792 let area = Rect::new(10, 20, 80, 60);
1793 let flex = Flex::horizontal()
1794 .margin(Sides::all(5))
1795 .gap(2)
1796 .constraints([
1797 Constraint::Fixed(15),
1798 Constraint::Percentage(30.0),
1799 Constraint::Min(10),
1800 ]);
1801 let rects = flex.split(area);
1802
1803 let inner = area.inner(Sides::all(5));
1805 for rect in &rects {
1806 assert!(
1807 rect.x >= inner.x && rect.right() <= inner.right(),
1808 "Rect {:?} exceeds horizontal bounds of {:?}",
1809 rect,
1810 inner
1811 );
1812 assert!(
1813 rect.y >= inner.y && rect.bottom() <= inner.bottom(),
1814 "Rect {:?} exceeds vertical bounds of {:?}",
1815 rect,
1816 inner
1817 );
1818 }
1819 }
1820
1821 #[test]
1824 fn fill_takes_remaining_space() {
1825 let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
1826 let rects = flex.split(Rect::new(0, 0, 100, 10));
1827 assert_eq!(rects[0].width, 20);
1828 assert_eq!(rects[1].width, 80); }
1830
1831 #[test]
1832 fn multiple_fills_share_space() {
1833 let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
1834 let rects = flex.split(Rect::new(0, 0, 100, 10));
1835 assert_eq!(rects[0].width, 50);
1836 assert_eq!(rects[1].width, 50);
1837 }
1838
1839 #[test]
1842 fn fit_content_uses_preferred_size() {
1843 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1844 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1845 if idx == 0 {
1846 LayoutSizeHint {
1847 min: 5,
1848 preferred: 30,
1849 max: None,
1850 }
1851 } else {
1852 LayoutSizeHint::ZERO
1853 }
1854 });
1855 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
1858
1859 #[test]
1860 fn fit_content_clamps_to_available() {
1861 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
1862 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1863 min: 5,
1864 preferred: 80,
1865 max: None,
1866 });
1867 assert_eq!(rects[0].width, 80);
1869 assert_eq!(rects[1].width, 20);
1870 }
1871
1872 #[test]
1873 fn fit_content_without_measurer_gets_zero() {
1874 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1876 let rects = flex.split(Rect::new(0, 0, 100, 10));
1877 assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
1880
1881 #[test]
1882 fn fit_content_zero_area_returns_empty_rects() {
1883 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1884 let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
1885 min: 5,
1886 preferred: 10,
1887 max: None,
1888 });
1889 assert_eq!(rects.len(), 2);
1890 assert_eq!(rects[0].width, 0);
1891 assert_eq!(rects[0].height, 0);
1892 assert_eq!(rects[1].width, 0);
1893 assert_eq!(rects[1].height, 0);
1894 }
1895
1896 #[test]
1897 fn fit_content_tiny_available_clamps_to_remaining() {
1898 let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
1899 let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
1900 min: 5,
1901 preferred: 10,
1902 max: None,
1903 });
1904 assert_eq!(rects[0].width, 1);
1905 assert_eq!(rects[1].width, 0);
1906 }
1907
1908 #[test]
1911 fn fit_content_bounded_clamps_to_min() {
1912 let flex = Flex::horizontal().constraints([
1913 Constraint::FitContentBounded { min: 20, max: 50 },
1914 Constraint::Fill,
1915 ]);
1916 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1917 min: 5,
1918 preferred: 10, max: None,
1920 });
1921 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
1923 }
1924
1925 #[test]
1926 fn fit_content_bounded_respects_small_available() {
1927 let flex = Flex::horizontal().constraints([
1928 Constraint::FitContentBounded { min: 20, max: 50 },
1929 Constraint::Fill,
1930 ]);
1931 let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
1932 min: 5,
1933 preferred: 10,
1934 max: None,
1935 });
1936 assert_eq!(rects[0].width, 5);
1938 assert_eq!(rects[1].width, 0);
1939 }
1940
1941 #[test]
1942 fn fit_content_bounded_clamps_to_max() {
1943 let flex = Flex::horizontal().constraints([
1944 Constraint::FitContentBounded { min: 10, max: 30 },
1945 Constraint::Fill,
1946 ]);
1947 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1948 min: 5,
1949 preferred: 50, max: None,
1951 });
1952 assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
1954 }
1955
1956 #[test]
1957 fn fit_content_bounded_uses_preferred_when_in_range() {
1958 let flex = Flex::horizontal().constraints([
1959 Constraint::FitContentBounded { min: 10, max: 50 },
1960 Constraint::Fill,
1961 ]);
1962 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
1963 min: 5,
1964 preferred: 35, max: None,
1966 });
1967 assert_eq!(rects[0].width, 35);
1968 assert_eq!(rects[1].width, 65);
1969 }
1970
1971 #[test]
1974 fn fit_min_uses_minimum_size() {
1975 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1976 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
1977 if idx == 0 {
1978 LayoutSizeHint {
1979 min: 15,
1980 preferred: 40,
1981 max: None,
1982 }
1983 } else {
1984 LayoutSizeHint::ZERO
1985 }
1986 });
1987 assert_eq!(rects[0].width, 15, "FitMin should strict size to min");
1990 assert_eq!(rects[1].width, 85, "Fill should take remaining space");
1991 }
1992
1993 #[test]
1994 fn fit_min_without_measurer_gets_zero() {
1995 let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
1996 let rects = flex.split(Rect::new(0, 0, 100, 10));
1997 assert_eq!(rects[0].width, 0);
2000 assert_eq!(rects[1].width, 100);
2001 }
2002
2003 #[test]
2006 fn layout_size_hint_zero_is_default() {
2007 assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
2008 }
2009
2010 #[test]
2011 fn layout_size_hint_exact() {
2012 let h = LayoutSizeHint::exact(25);
2013 assert_eq!(h.min, 25);
2014 assert_eq!(h.preferred, 25);
2015 assert_eq!(h.max, Some(25));
2016 }
2017
2018 #[test]
2019 fn layout_size_hint_at_least() {
2020 let h = LayoutSizeHint::at_least(10, 30);
2021 assert_eq!(h.min, 10);
2022 assert_eq!(h.preferred, 30);
2023 assert_eq!(h.max, None);
2024 }
2025
2026 #[test]
2027 fn layout_size_hint_clamp() {
2028 let h = LayoutSizeHint {
2029 min: 10,
2030 preferred: 20,
2031 max: Some(30),
2032 };
2033 assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
2037
2038 #[test]
2039 fn layout_size_hint_clamp_unbounded() {
2040 let h = LayoutSizeHint::at_least(5, 10);
2041 assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
2044
2045 #[test]
2046 fn layout_size_hint_clamp_min_greater_than_max() {
2047 let h = LayoutSizeHint {
2049 min: 20,
2050 preferred: 20,
2051 max: Some(10),
2052 };
2053 assert_eq!(h.clamp(5), 20); assert_eq!(h.clamp(15), 20); assert_eq!(h.clamp(25), 20); }
2057
2058 #[test]
2061 fn fit_content_with_fixed_and_fill() {
2062 let flex = Flex::horizontal().constraints([
2063 Constraint::Fixed(20),
2064 Constraint::FitContent,
2065 Constraint::Fill,
2066 ]);
2067 let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
2068 if idx == 1 {
2069 LayoutSizeHint {
2070 min: 5,
2071 preferred: 25,
2072 max: None,
2073 }
2074 } else {
2075 LayoutSizeHint::ZERO
2076 }
2077 });
2078 assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
2082
2083 #[test]
2084 fn total_allocation_never_exceeds_available_with_fit_content() {
2085 for available in [10u16, 50, 100, 255] {
2086 let flex = Flex::horizontal().constraints([
2087 Constraint::FitContent,
2088 Constraint::FitContent,
2089 Constraint::Fill,
2090 ]);
2091 let rects =
2092 flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
2093 min: 10,
2094 preferred: 40,
2095 max: None,
2096 });
2097 let total: u16 = rects.iter().map(|r| r.width).sum();
2098 assert!(
2099 total <= available,
2100 "Total {} exceeded available {} with FitContent",
2101 total,
2102 available
2103 );
2104 }
2105 }
2106
2107 mod rounding_tests {
2112 use super::super::*;
2113
2114 #[test]
2117 fn rounding_conserves_sum_exact() {
2118 let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
2119 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2120 assert_eq!(result.as_slice(), &[10u16, 20u16, 10u16]);
2121 }
2122
2123 #[test]
2124 fn rounding_conserves_sum_fractional() {
2125 let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
2126 assert_eq!(
2127 result.iter().copied().sum::<u16>(),
2128 40,
2129 "Sum must equal total: {:?}",
2130 result
2131 );
2132 }
2133
2134 #[test]
2135 fn rounding_conserves_sum_many_fractions() {
2136 let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
2137 let result = round_layout_stable(&targets, 100, None);
2138 assert_eq!(
2139 result.iter().copied().sum::<u16>(),
2140 100,
2141 "Sum must be exactly 100: {:?}",
2142 result
2143 );
2144 }
2145
2146 #[test]
2147 fn rounding_conserves_sum_all_half() {
2148 let targets = vec![10.5, 10.5, 10.5, 10.5];
2149 let result = round_layout_stable(&targets, 42, None);
2150 assert_eq!(
2151 result.iter().copied().sum::<u16>(),
2152 42,
2153 "Sum must be exactly 42: {:?}",
2154 result
2155 );
2156 }
2157
2158 #[test]
2161 fn rounding_displacement_bounded() {
2162 let targets = vec![33.33, 33.33, 33.34];
2163 let result = round_layout_stable(&targets, 100, None);
2164 assert_eq!(result.iter().copied().sum::<u16>(), 100);
2165
2166 for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
2167 let floor = r.floor() as u16;
2168 let ceil = floor + 1;
2169 assert!(
2170 x == floor || x == ceil,
2171 "Element {} = {} not in {{floor={}, ceil={}}} of target {}",
2172 i,
2173 x,
2174 floor,
2175 ceil,
2176 r
2177 );
2178 }
2179 }
2180
2181 #[test]
2184 fn temporal_tiebreak_stable_when_unchanged() {
2185 let targets = vec![10.5, 10.5, 10.5, 10.5];
2186 let first = round_layout_stable(&targets, 42, None);
2187 let second = round_layout_stable(&targets, 42, Some(first.clone()));
2188 assert_eq!(
2189 first, second,
2190 "Identical targets should produce identical results"
2191 );
2192 }
2193
2194 #[test]
2195 fn temporal_tiebreak_prefers_previous_direction() {
2196 let targets = vec![10.5, 10.5];
2197 let total = 21;
2198 let first = round_layout_stable(&targets, total, None);
2199 assert_eq!(first.iter().copied().sum::<u16>(), total);
2200 let second = round_layout_stable(&targets, total, Some(first.clone()));
2201 assert_eq!(first, second, "Should maintain rounding direction");
2202 }
2203
2204 #[test]
2205 fn temporal_tiebreak_adapts_to_changed_targets() {
2206 let targets_a = vec![10.5, 10.5];
2207 let result_a = round_layout_stable(&targets_a, 21, None);
2208 let targets_b = vec![15.7, 5.3];
2209 let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
2210 assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
2211 assert!(result_b[0] > result_b[1], "Should follow larger target");
2212 }
2213
2214 #[test]
2217 fn property_min_displacement_brute_force_small() {
2218 let targets = vec![3.3, 3.3, 3.4];
2219 let total: u16 = 10;
2220 let result = round_layout_stable(&targets, total, None);
2221 let our_displacement: f64 = result
2222 .iter()
2223 .zip(targets.iter())
2224 .map(|(&x, &r)| (x as f64 - r).abs())
2225 .sum();
2226
2227 let mut min_displacement = f64::MAX;
2228 let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
2229 let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
2230
2231 for a in floors[0]..=ceils[0] {
2232 for b in floors[1]..=ceils[1] {
2233 for c in floors[2]..=ceils[2] {
2234 if a + b + c == total {
2235 let disp = (a as f64 - targets[0]).abs()
2236 + (b as f64 - targets[1]).abs()
2237 + (c as f64 - targets[2]).abs();
2238 if disp < min_displacement {
2239 min_displacement = disp;
2240 }
2241 }
2242 }
2243 }
2244 }
2245
2246 assert!(
2247 (our_displacement - min_displacement).abs() < 1e-10,
2248 "Our displacement {} should match optimal {}: {:?}",
2249 our_displacement,
2250 min_displacement,
2251 result
2252 );
2253 }
2254
2255 #[test]
2258 fn rounding_deterministic() {
2259 let targets = vec![7.7, 8.3, 14.0];
2260 let a = round_layout_stable(&targets, 30, None);
2261 let b = round_layout_stable(&targets, 30, None);
2262 assert_eq!(a, b, "Same inputs must produce identical outputs");
2263 }
2264
2265 #[test]
2268 fn rounding_empty_targets() {
2269 let result = round_layout_stable(&[], 0, None);
2270 assert!(result.is_empty());
2271 }
2272
2273 #[test]
2274 fn rounding_single_element() {
2275 let result = round_layout_stable(&[10.7], 11, None);
2276 assert_eq!(result.as_slice(), &[11u16]);
2277 }
2278
2279 #[test]
2280 fn rounding_zero_total() {
2281 let result = round_layout_stable(&[5.0, 5.0], 0, None);
2282 assert_eq!(result.iter().copied().sum::<u16>(), 0);
2283 }
2284
2285 #[test]
2286 fn rounding_zero_total_with_large_overflow_reaches_zero() {
2287 let result = round_layout_stable(&[65535.0, 65535.0], 0, None);
2288 assert_eq!(result.as_slice(), &[0u16, 0u16]);
2289 assert_eq!(result.iter().copied().sum::<u16>(), 0);
2290 }
2291
2292 #[test]
2293 fn rounding_all_zeros() {
2294 let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
2295 assert_eq!(result.as_slice(), &[0u16, 0u16, 0u16]);
2296 }
2297
2298 #[test]
2299 fn rounding_integer_targets() {
2300 let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
2301 assert_eq!(result.as_slice(), &[10u16, 20u16, 30u16]);
2302 }
2303
2304 #[test]
2305 fn rounding_large_deficit() {
2306 let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
2307 assert_eq!(result.iter().copied().sum::<u16>(), 3);
2308 assert_eq!(result.as_slice(), &[1u16, 1u16, 1u16]);
2309 }
2310
2311 #[test]
2312 fn rounding_with_prev_different_length() {
2313 let result = round_layout_stable(
2314 &[10.5, 10.5],
2315 21,
2316 Some(smallvec::smallvec![11u16, 10u16, 5u16]),
2317 );
2318 assert_eq!(result.iter().copied().sum::<u16>(), 21);
2319 }
2320
2321 #[test]
2322 fn rounding_very_small_fractions() {
2323 let targets = vec![10.001, 20.001, 9.998];
2324 let result = round_layout_stable(&targets, 40, None);
2325 assert_eq!(result.iter().copied().sum::<u16>(), 40);
2326 }
2327
2328 #[test]
2329 fn rounding_conserves_sum_stress() {
2330 let n = 50;
2331 let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
2332 let total = 120u16;
2333 let result = round_layout_stable(&targets, total, None);
2334 assert_eq!(
2335 result.iter().copied().sum::<u16>(),
2336 total,
2337 "Sum must be exactly {} for {} items: {:?}",
2338 total,
2339 n,
2340 result
2341 );
2342 }
2343 }
2344
2345 mod property_constraint_tests {
2350 use super::super::*;
2351
2352 struct Lcg(u64);
2354
2355 impl Lcg {
2356 fn new(seed: u64) -> Self {
2357 Self(seed)
2358 }
2359 fn next_u32(&mut self) -> u32 {
2360 self.0 = self
2361 .0
2362 .wrapping_mul(6_364_136_223_846_793_005)
2363 .wrapping_add(1);
2364 (self.0 >> 33) as u32
2365 }
2366 fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
2367 if lo >= hi {
2368 return lo;
2369 }
2370 lo + (self.next_u32() % (hi - lo) as u32) as u16
2371 }
2372 fn next_f32(&mut self) -> f32 {
2373 (self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
2374 }
2375 }
2376
2377 fn random_constraint(rng: &mut Lcg) -> Constraint {
2379 match rng.next_u32() % 7 {
2380 0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
2381 1 => Constraint::Percentage(rng.next_f32() * 100.0),
2382 2 => Constraint::Min(rng.next_u16_range(0, 40)),
2383 3 => Constraint::Max(rng.next_u16_range(5, 120)),
2384 4 => {
2385 let n = rng.next_u32() % 5 + 1;
2386 let d = rng.next_u32() % 5 + 1;
2387 Constraint::Ratio(n, d)
2388 }
2389 5 => Constraint::Fill,
2390 _ => Constraint::FitContent,
2391 }
2392 }
2393
2394 #[test]
2395 fn property_constraints_respected_fixed() {
2396 let mut rng = Lcg::new(0xDEAD_BEEF);
2397 for _ in 0..200 {
2398 let fixed_val = rng.next_u16_range(1, 60);
2399 let avail = rng.next_u16_range(10, 200);
2400 let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
2401 let rects = flex.split(Rect::new(0, 0, avail, 10));
2402 assert!(
2403 rects[0].width <= fixed_val.min(avail),
2404 "Fixed({}) in avail {} -> width {}",
2405 fixed_val,
2406 avail,
2407 rects[0].width
2408 );
2409 }
2410 }
2411
2412 #[test]
2413 fn property_constraints_respected_max() {
2414 let mut rng = Lcg::new(0xCAFE_BABE);
2415 for _ in 0..200 {
2416 let max_val = rng.next_u16_range(5, 80);
2417 let avail = rng.next_u16_range(10, 200);
2418 let flex =
2419 Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
2420 let rects = flex.split(Rect::new(0, 0, avail, 10));
2421 assert!(
2422 rects[0].width <= max_val,
2423 "Max({}) in avail {} -> width {}",
2424 max_val,
2425 avail,
2426 rects[0].width
2427 );
2428 }
2429 }
2430
2431 #[test]
2432 fn property_constraints_respected_min() {
2433 let mut rng = Lcg::new(0xBAAD_F00D);
2434 for _ in 0..200 {
2435 let min_val = rng.next_u16_range(0, 40);
2436 let avail = rng.next_u16_range(min_val.max(1), 200);
2437 let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
2438 let rects = flex.split(Rect::new(0, 0, avail, 10));
2439 assert!(
2440 rects[0].width >= min_val,
2441 "Min({}) in avail {} -> width {}",
2442 min_val,
2443 avail,
2444 rects[0].width
2445 );
2446 }
2447 }
2448
2449 #[test]
2450 fn property_constraints_respected_ratio_proportional() {
2451 let mut rng = Lcg::new(0x1234_5678);
2452 for _ in 0..200 {
2453 let n1 = rng.next_u32() % 5 + 1;
2454 let n2 = rng.next_u32() % 5 + 1;
2455 let d = n1 + n2;
2456 let avail = rng.next_u16_range(20, 200);
2457 let flex = Flex::horizontal()
2458 .constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
2459 let rects = flex.split(Rect::new(0, 0, avail, 10));
2460 let w1 = rects[0].width as f64;
2461 let w2 = rects[1].width as f64;
2462 let total = w1 + w2;
2463 if total > 0.0 {
2464 let expected_ratio = n1 as f64 / d as f64;
2465 let actual_ratio = w1 / total;
2466 assert!(
2467 (actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
2468 "Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
2469 n1,
2470 d,
2471 n1,
2472 n2,
2473 avail,
2474 expected_ratio,
2475 actual_ratio,
2476 w1,
2477 w2
2478 );
2479 }
2480 }
2481 }
2482
2483 #[test]
2484 fn property_total_allocation_never_exceeds_available() {
2485 let mut rng = Lcg::new(0xFACE_FEED);
2486 for _ in 0..500 {
2487 let n = (rng.next_u32() % 6 + 1) as usize;
2488 let constraints: Vec<Constraint> =
2489 (0..n).map(|_| random_constraint(&mut rng)).collect();
2490 let avail = rng.next_u16_range(5, 200);
2491 let dir = if rng.next_u32().is_multiple_of(2) {
2492 Direction::Horizontal
2493 } else {
2494 Direction::Vertical
2495 };
2496 let flex = Flex::default().direction(dir).constraints(constraints);
2497 let area = Rect::new(0, 0, avail, avail);
2498 let rects = flex.split(area);
2499 let total: u16 = rects
2500 .iter()
2501 .map(|r| match dir {
2502 Direction::Horizontal => r.width,
2503 Direction::Vertical => r.height,
2504 })
2505 .sum();
2506 assert!(
2507 total <= avail,
2508 "Total {} exceeded available {} with {} constraints",
2509 total,
2510 avail,
2511 n
2512 );
2513 }
2514 }
2515
2516 #[test]
2517 fn property_no_overlap_horizontal() {
2518 let mut rng = Lcg::new(0xABCD_1234);
2519 for _ in 0..300 {
2520 let n = (rng.next_u32() % 5 + 2) as usize;
2521 let constraints: Vec<Constraint> =
2522 (0..n).map(|_| random_constraint(&mut rng)).collect();
2523 let avail = rng.next_u16_range(20, 200);
2524 let flex = Flex::horizontal().constraints(constraints);
2525 let rects = flex.split(Rect::new(0, 0, avail, 10));
2526
2527 for i in 1..rects.len() {
2528 let prev_end = rects[i - 1].x + rects[i - 1].width;
2529 assert!(
2530 rects[i].x >= prev_end,
2531 "Overlap at {}: prev ends {}, next starts {}",
2532 i,
2533 prev_end,
2534 rects[i].x
2535 );
2536 }
2537 }
2538 }
2539
2540 #[test]
2541 fn property_deterministic_across_runs() {
2542 let mut rng = Lcg::new(0x9999_8888);
2543 for _ in 0..100 {
2544 let n = (rng.next_u32() % 5 + 1) as usize;
2545 let constraints: Vec<Constraint> =
2546 (0..n).map(|_| random_constraint(&mut rng)).collect();
2547 let avail = rng.next_u16_range(10, 200);
2548 let r1 = Flex::horizontal()
2549 .constraints(constraints.clone())
2550 .split(Rect::new(0, 0, avail, 10));
2551 let r2 = Flex::horizontal()
2552 .constraints(constraints)
2553 .split(Rect::new(0, 0, avail, 10));
2554 assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
2555 }
2556 }
2557 }
2558
2559 mod property_temporal_tests {
2564 use super::super::*;
2565 use crate::cache::{CoherenceCache, CoherenceId};
2566
2567 struct Lcg(u64);
2569
2570 impl Lcg {
2571 fn new(seed: u64) -> Self {
2572 Self(seed)
2573 }
2574 fn next_u32(&mut self) -> u32 {
2575 self.0 = self
2576 .0
2577 .wrapping_mul(6_364_136_223_846_793_005)
2578 .wrapping_add(1);
2579 (self.0 >> 33) as u32
2580 }
2581 }
2582
2583 #[test]
2584 fn property_temporal_stability_small_resize() {
2585 let constraints = [
2586 Constraint::Percentage(33.3),
2587 Constraint::Percentage(33.3),
2588 Constraint::Fill,
2589 ];
2590 let mut coherence = CoherenceCache::new(64);
2591 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2592
2593 for total in [80u16, 100, 120] {
2594 let flex = Flex::horizontal().constraints(constraints);
2595 let rects = flex.split(Rect::new(0, 0, total, 10));
2596 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2597
2598 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2599 let prev = coherence.get(&id);
2600 let rounded = round_layout_stable(&targets, total, prev);
2601
2602 if let Some(old) = coherence.get(&id) {
2603 let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
2604 assert!(
2605 max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
2606 "max_disp={} too large for size change {} -> {}",
2607 max_disp,
2608 old.iter().copied().sum::<u16>(),
2609 total
2610 );
2611 let _ = sum_disp;
2612 }
2613 coherence.store(id, rounded);
2614 }
2615 }
2616
2617 #[test]
2618 fn property_temporal_stability_random_walk() {
2619 let constraints = [
2620 Constraint::Ratio(1, 3),
2621 Constraint::Ratio(1, 3),
2622 Constraint::Ratio(1, 3),
2623 ];
2624 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2625 let mut coherence = CoherenceCache::new(64);
2626 let mut rng = Lcg::new(0x5555_AAAA);
2627 let mut total: u16 = 90;
2628
2629 for step in 0..200 {
2630 let prev_total = total;
2631 let delta = (rng.next_u32() % 7) as i32 - 3;
2632 total = (total as i32 + delta).clamp(10, 250) as u16;
2633
2634 let flex = Flex::horizontal().constraints(constraints);
2635 let rects = flex.split(Rect::new(0, 0, total, 10));
2636 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2637
2638 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2639 let prev = coherence.get(&id);
2640 let rounded = round_layout_stable(&targets, total, prev);
2641
2642 if coherence.get(&id).is_some() {
2643 let (_, max_disp) = coherence.displacement(&id, &rounded);
2644 let size_change = total.abs_diff(prev_total);
2645 assert!(
2646 max_disp <= size_change as u32 + 2,
2647 "step {}: max_disp={} exceeds size_change={} + 2",
2648 step,
2649 max_disp,
2650 size_change
2651 );
2652 }
2653 coherence.store(id, rounded);
2654 }
2655 }
2656
2657 #[test]
2658 fn property_temporal_stability_identical_frames() {
2659 let constraints = [
2660 Constraint::Fixed(20),
2661 Constraint::Fill,
2662 Constraint::Fixed(15),
2663 ];
2664 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2665 let mut coherence = CoherenceCache::new(64);
2666
2667 let flex = Flex::horizontal().constraints(constraints);
2668 let rects = flex.split(Rect::new(0, 0, 100, 10));
2669 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2670 coherence.store(id, widths.iter().copied().collect());
2671
2672 for _ in 0..10 {
2673 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2674 let prev = coherence.get(&id);
2675 let rounded = round_layout_stable(&targets, 100, prev);
2676 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2677 assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
2678 coherence.store(id, rounded);
2679 }
2680 }
2681
2682 #[test]
2683 fn property_temporal_coherence_sweep() {
2684 let constraints = [
2685 Constraint::Percentage(25.0),
2686 Constraint::Percentage(50.0),
2687 Constraint::Fill,
2688 ];
2689 let id = CoherenceId::new(&constraints, Direction::Horizontal);
2690 let mut coherence = CoherenceCache::new(64);
2691 let mut total_displacement: u64 = 0;
2692
2693 for total in 60u16..=140 {
2694 let flex = Flex::horizontal().constraints(constraints);
2695 let rects = flex.split(Rect::new(0, 0, total, 10));
2696 let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
2697
2698 let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
2699 let prev = coherence.get(&id);
2700 let rounded = round_layout_stable(&targets, total, prev);
2701
2702 if coherence.get(&id).is_some() {
2703 let (sum_disp, _) = coherence.displacement(&id, &rounded);
2704 total_displacement += sum_disp;
2705 }
2706 coherence.store(id, rounded);
2707 }
2708
2709 assert!(
2710 total_displacement <= 80 * 3,
2711 "Total displacement {} exceeds bound for 80-step sweep",
2712 total_displacement
2713 );
2714 }
2715 }
2716
2717 mod snapshot_layout_tests {
2722 use super::super::*;
2723 use crate::grid::{Grid, GridArea};
2724
2725 fn snapshot_flex(
2726 constraints: &[Constraint],
2727 dir: Direction,
2728 width: u16,
2729 height: u16,
2730 ) -> String {
2731 let flex = Flex::default()
2732 .direction(dir)
2733 .constraints(constraints.iter().copied());
2734 let rects = flex.split(Rect::new(0, 0, width, height));
2735 let mut out = format!(
2736 "Flex {:?} {}x{} ({} constraints)\n",
2737 dir,
2738 width,
2739 height,
2740 constraints.len()
2741 );
2742 for (i, r) in rects.iter().enumerate() {
2743 out.push_str(&format!(
2744 " [{}] x={} y={} w={} h={}\n",
2745 i, r.x, r.y, r.width, r.height
2746 ));
2747 }
2748 let total: u16 = rects
2749 .iter()
2750 .map(|r| match dir {
2751 Direction::Horizontal => r.width,
2752 Direction::Vertical => r.height,
2753 })
2754 .sum();
2755 out.push_str(&format!(" total={}\n", total));
2756 out
2757 }
2758
2759 fn snapshot_grid(
2760 rows: &[Constraint],
2761 cols: &[Constraint],
2762 areas: &[(&str, GridArea)],
2763 width: u16,
2764 height: u16,
2765 ) -> String {
2766 let mut grid = Grid::new()
2767 .rows(rows.iter().copied())
2768 .columns(cols.iter().copied());
2769 for &(name, area) in areas {
2770 grid = grid.area(name, area);
2771 }
2772 let layout = grid.split(Rect::new(0, 0, width, height));
2773
2774 let mut out = format!(
2775 "Grid {}x{} ({}r x {}c)\n",
2776 width,
2777 height,
2778 rows.len(),
2779 cols.len()
2780 );
2781 for r in 0..rows.len() {
2782 for c in 0..cols.len() {
2783 let rect = layout.cell(r, c);
2784 out.push_str(&format!(
2785 " [{},{}] x={} y={} w={} h={}\n",
2786 r, c, rect.x, rect.y, rect.width, rect.height
2787 ));
2788 }
2789 }
2790 for &(name, _) in areas {
2791 if let Some(rect) = layout.area(name) {
2792 out.push_str(&format!(
2793 " area({}) x={} y={} w={} h={}\n",
2794 name, rect.x, rect.y, rect.width, rect.height
2795 ));
2796 }
2797 }
2798 out
2799 }
2800
2801 #[test]
2804 fn snapshot_flex_thirds_80x24() {
2805 let snap = snapshot_flex(
2806 &[
2807 Constraint::Ratio(1, 3),
2808 Constraint::Ratio(1, 3),
2809 Constraint::Ratio(1, 3),
2810 ],
2811 Direction::Horizontal,
2812 80,
2813 24,
2814 );
2815 assert_eq!(
2816 snap,
2817 "\
2818Flex Horizontal 80x24 (3 constraints)
2819 [0] x=0 y=0 w=26 h=24
2820 [1] x=26 y=0 w=26 h=24
2821 [2] x=52 y=0 w=26 h=24
2822 total=78
2823"
2824 );
2825 }
2826
2827 #[test]
2828 fn snapshot_flex_sidebar_content_80x24() {
2829 let snap = snapshot_flex(
2830 &[Constraint::Fixed(20), Constraint::Fill],
2831 Direction::Horizontal,
2832 80,
2833 24,
2834 );
2835 assert_eq!(
2836 snap,
2837 "\
2838Flex Horizontal 80x24 (2 constraints)
2839 [0] x=0 y=0 w=20 h=24
2840 [1] x=20 y=0 w=60 h=24
2841 total=80
2842"
2843 );
2844 }
2845
2846 #[test]
2847 fn snapshot_flex_header_body_footer_80x24() {
2848 let snap = snapshot_flex(
2849 &[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
2850 Direction::Vertical,
2851 80,
2852 24,
2853 );
2854 assert_eq!(
2855 snap,
2856 "\
2857Flex Vertical 80x24 (3 constraints)
2858 [0] x=0 y=0 w=80 h=3
2859 [1] x=0 y=3 w=80 h=20
2860 [2] x=0 y=23 w=80 h=1
2861 total=24
2862"
2863 );
2864 }
2865
2866 #[test]
2869 fn snapshot_flex_thirds_120x40() {
2870 let snap = snapshot_flex(
2871 &[
2872 Constraint::Ratio(1, 3),
2873 Constraint::Ratio(1, 3),
2874 Constraint::Ratio(1, 3),
2875 ],
2876 Direction::Horizontal,
2877 120,
2878 40,
2879 );
2880 assert_eq!(
2881 snap,
2882 "\
2883Flex Horizontal 120x40 (3 constraints)
2884 [0] x=0 y=0 w=40 h=40
2885 [1] x=40 y=0 w=40 h=40
2886 [2] x=80 y=0 w=40 h=40
2887 total=120
2888"
2889 );
2890 }
2891
2892 #[test]
2893 fn snapshot_flex_sidebar_content_120x40() {
2894 let snap = snapshot_flex(
2895 &[Constraint::Fixed(20), Constraint::Fill],
2896 Direction::Horizontal,
2897 120,
2898 40,
2899 );
2900 assert_eq!(
2901 snap,
2902 "\
2903Flex Horizontal 120x40 (2 constraints)
2904 [0] x=0 y=0 w=20 h=40
2905 [1] x=20 y=0 w=100 h=40
2906 total=120
2907"
2908 );
2909 }
2910
2911 #[test]
2912 fn snapshot_flex_percentage_mix_120x40() {
2913 let snap = snapshot_flex(
2914 &[
2915 Constraint::Percentage(25.0),
2916 Constraint::Percentage(50.0),
2917 Constraint::Fill,
2918 ],
2919 Direction::Horizontal,
2920 120,
2921 40,
2922 );
2923 assert_eq!(
2924 snap,
2925 "\
2926Flex Horizontal 120x40 (3 constraints)
2927 [0] x=0 y=0 w=30 h=40
2928 [1] x=30 y=0 w=60 h=40
2929 [2] x=90 y=0 w=30 h=40
2930 total=120
2931"
2932 );
2933 }
2934
2935 #[test]
2938 fn snapshot_grid_2x2_80x24() {
2939 let snap = snapshot_grid(
2940 &[Constraint::Fixed(3), Constraint::Fill],
2941 &[Constraint::Fixed(20), Constraint::Fill],
2942 &[
2943 ("header", GridArea::span(0, 0, 1, 2)),
2944 ("sidebar", GridArea::span(1, 0, 1, 1)),
2945 ("content", GridArea::cell(1, 1)),
2946 ],
2947 80,
2948 24,
2949 );
2950 assert_eq!(
2951 snap,
2952 "\
2953Grid 80x24 (2r x 2c)
2954 [0,0] x=0 y=0 w=20 h=3
2955 [0,1] x=20 y=0 w=60 h=3
2956 [1,0] x=0 y=3 w=20 h=21
2957 [1,1] x=20 y=3 w=60 h=21
2958 area(header) x=0 y=0 w=80 h=3
2959 area(sidebar) x=0 y=3 w=20 h=21
2960 area(content) x=20 y=3 w=60 h=21
2961"
2962 );
2963 }
2964
2965 #[test]
2966 fn snapshot_grid_3x3_80x24() {
2967 let snap = snapshot_grid(
2968 &[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
2969 &[
2970 Constraint::Fixed(10),
2971 Constraint::Fill,
2972 Constraint::Fixed(10),
2973 ],
2974 &[],
2975 80,
2976 24,
2977 );
2978 assert_eq!(
2979 snap,
2980 "\
2981Grid 80x24 (3r x 3c)
2982 [0,0] x=0 y=0 w=10 h=1
2983 [0,1] x=10 y=0 w=60 h=1
2984 [0,2] x=70 y=0 w=10 h=1
2985 [1,0] x=0 y=1 w=10 h=22
2986 [1,1] x=10 y=1 w=60 h=22
2987 [1,2] x=70 y=1 w=10 h=22
2988 [2,0] x=0 y=23 w=10 h=1
2989 [2,1] x=10 y=23 w=60 h=1
2990 [2,2] x=70 y=23 w=10 h=1
2991"
2992 );
2993 }
2994
2995 #[test]
2998 fn snapshot_grid_2x2_120x40() {
2999 let snap = snapshot_grid(
3000 &[Constraint::Fixed(3), Constraint::Fill],
3001 &[Constraint::Fixed(20), Constraint::Fill],
3002 &[
3003 ("header", GridArea::span(0, 0, 1, 2)),
3004 ("sidebar", GridArea::span(1, 0, 1, 1)),
3005 ("content", GridArea::cell(1, 1)),
3006 ],
3007 120,
3008 40,
3009 );
3010 assert_eq!(
3011 snap,
3012 "\
3013Grid 120x40 (2r x 2c)
3014 [0,0] x=0 y=0 w=20 h=3
3015 [0,1] x=20 y=0 w=100 h=3
3016 [1,0] x=0 y=3 w=20 h=37
3017 [1,1] x=20 y=3 w=100 h=37
3018 area(header) x=0 y=0 w=120 h=3
3019 area(sidebar) x=0 y=3 w=20 h=37
3020 area(content) x=20 y=3 w=100 h=37
3021"
3022 );
3023 }
3024
3025 #[test]
3026 fn snapshot_grid_dashboard_120x40() {
3027 let snap = snapshot_grid(
3028 &[
3029 Constraint::Fixed(3),
3030 Constraint::Percentage(60.0),
3031 Constraint::Fill,
3032 ],
3033 &[Constraint::Percentage(30.0), Constraint::Fill],
3034 &[
3035 ("nav", GridArea::span(0, 0, 1, 2)),
3036 ("chart", GridArea::cell(1, 0)),
3037 ("detail", GridArea::cell(1, 1)),
3038 ("log", GridArea::span(2, 0, 1, 2)),
3039 ],
3040 120,
3041 40,
3042 );
3043 assert_eq!(
3044 snap,
3045 "\
3046Grid 120x40 (3r x 2c)
3047 [0,0] x=0 y=0 w=36 h=3
3048 [0,1] x=36 y=0 w=84 h=3
3049 [1,0] x=0 y=3 w=36 h=24
3050 [1,1] x=36 y=3 w=84 h=24
3051 [2,0] x=0 y=27 w=36 h=13
3052 [2,1] x=36 y=27 w=84 h=13
3053 area(nav) x=0 y=0 w=120 h=3
3054 area(chart) x=0 y=3 w=36 h=24
3055 area(detail) x=36 y=3 w=84 h=24
3056 area(log) x=0 y=27 w=120 h=13
3057"
3058 );
3059 }
3060 }
3061}