Skip to main content

ftui_layout/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Layout primitives and solvers.
4//!
5//! This crate provides layout components for terminal UIs:
6//!
7//! - [`Flex`] - 1D constraint-based layout (rows or columns)
8//! - [`Grid`] - 2D constraint-based layout with cell spanning
9//! - [`Constraint`] - Size constraints (Fixed, Percentage, Min, Max, Ratio, FitContent)
10//! - [`debug`] - Layout constraint debugging and introspection
11//! - [`cache`] - Layout result caching for memoization
12//!
13//! # Role in FrankenTUI
14//! `ftui-layout` is the geometry solver for widgets and screens. It converts
15//! constraints into concrete rectangles, with support for intrinsic sizing and
16//! caching to keep layout deterministic and fast.
17//!
18//! # How it fits in the system
19//! The runtime and widgets call into this crate to split a `Rect` into nested
20//! regions. Those regions are then passed to widgets or custom renderers, which
21//! ultimately draw into `ftui-render` frames.
22//!
23//! # Intrinsic Sizing
24//!
25//! The layout system supports content-aware sizing via [`LayoutSizeHint`] and
26//! [`Flex::split_with_measurer`]:
27//!
28//! ```ignore
29//! use ftui_layout::{Flex, Constraint, LayoutSizeHint};
30//!
31//! let flex = Flex::horizontal()
32//!     .constraints([Constraint::FitContent, Constraint::Fill]);
33//!
34//! let rects = flex.split_with_measurer(area, |idx, available| {
35//!     match idx {
36//!         0 => LayoutSizeHint { min: 5, preferred: 20, max: None },
37//!         _ => LayoutSizeHint::ZERO,
38//!     }
39//! });
40//! ```
41
42pub 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
144/// Inline capacity for layout result vectors.
145///
146/// Most layouts use ≤8 constraints, so inlining avoids heap allocation in the
147/// common case. The `SmallVec` spills to the heap transparently when needed.
148const LAYOUT_INLINE_CAP: usize = 8;
149
150/// Stack-inlined vector of rectangles returned by layout split operations.
151pub type Rects = SmallVec<[Rect; LAYOUT_INLINE_CAP]>;
152
153/// Stack-inlined vector of sizes returned by the constraint solver.
154type Sizes = SmallVec<[u16; LAYOUT_INLINE_CAP]>;
155
156/// Stack-inlined vector of flex constraints.
157type Constraints = SmallVec<[Constraint; LAYOUT_INLINE_CAP]>;
158
159/// A constraint on the size of a layout area.
160#[derive(Debug, Clone, Copy, PartialEq)]
161pub enum Constraint {
162    /// An exact size in cells.
163    Fixed(u16),
164    /// A percentage of the total available size (0.0 to 100.0).
165    Percentage(f32),
166    /// A minimum size in cells.
167    Min(u16),
168    /// A maximum size in cells.
169    Max(u16),
170    /// A ratio of the total available space (numerator, denominator).
171    Ratio(u32, u32),
172    /// Fill remaining space (like Min(0) but semantically clearer).
173    Fill,
174    /// Size to fit content using widget's preferred size from [`LayoutSizeHint`].
175    ///
176    /// When used with [`Flex::split_with_measurer`], the measurer callback provides
177    /// the size hints. Defaults to zero size if no measurer is provided.
178    FitContent,
179    /// Fit content but clamp to explicit bounds.
180    ///
181    /// The allocated size will be between `min` and `max`, using the widget's
182    /// preferred size when within range.
183    FitContentBounded {
184        /// Minimum allocation regardless of content size.
185        min: u16,
186        /// Maximum allocation regardless of content size.
187        max: u16,
188    },
189    /// Use widget's minimum size (shrink-to-fit).
190    ///
191    /// Allocates only the minimum space the widget requires.
192    FitMin,
193}
194
195/// Size hint returned by measurer callbacks for intrinsic sizing.
196///
197/// This is a 1D projection of a widget's size constraints along the layout axis.
198/// Use with [`Flex::split_with_measurer`] for content-aware layouts.
199///
200/// # Example
201///
202/// ```
203/// use ftui_layout::LayoutSizeHint;
204///
205/// // A label that needs 5-20 cells, ideally 15
206/// let hint = LayoutSizeHint {
207///     min: 5,
208///     preferred: 15,
209///     max: Some(20),
210/// };
211///
212/// // Clamp allocation to hint bounds
213/// assert_eq!(hint.clamp(10), 10); // Within range
214/// assert_eq!(hint.clamp(3), 5);   // Below min
215/// assert_eq!(hint.clamp(30), 20); // Above max
216/// ```
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
218pub struct LayoutSizeHint {
219    /// Minimum size (widget clips below this).
220    pub min: u16,
221    /// Preferred size (ideal for content).
222    pub preferred: u16,
223    /// Maximum useful size (None = unbounded).
224    pub max: Option<u16>,
225}
226
227impl LayoutSizeHint {
228    /// Zero hint (no minimum, no preferred, unbounded).
229    pub const ZERO: Self = Self {
230        min: 0,
231        preferred: 0,
232        max: None,
233    };
234
235    /// Create an exact size hint (min = preferred = max).
236    #[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    /// Create a hint with minimum and preferred size, unbounded max.
246    #[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    /// Clamp a value to this hint's bounds.
256    #[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/// The direction to layout items.
264#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
265pub enum Direction {
266    /// Top to bottom.
267    #[default]
268    Vertical,
269    /// Left to right.
270    Horizontal,
271}
272
273/// Alignment of items within the layout.
274#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
275pub enum Alignment {
276    /// Align items to the start (left/top).
277    #[default]
278    Start,
279    /// Center items within available space.
280    Center,
281    /// Align items to the end (right/bottom).
282    End,
283    /// Distribute space evenly around each item.
284    SpaceAround,
285    /// Distribute space evenly between items (no outer space).
286    SpaceBetween,
287}
288
289/// How a layout container handles content that exceeds available space.
290///
291/// This enum models the CSS `overflow` property for terminal layouts.
292/// The actual clipping or scrolling is performed by the render layer;
293/// this value acts as a declarative hint attached to [`Flex`] or [`Grid`]
294/// so that widgets and the renderer know how to treat overflow regions.
295///
296/// # Migration rationale
297///
298/// Web components routinely set `overflow: hidden`, `overflow: scroll`, etc.
299/// Without an explicit model the migration code emitter cannot faithfully
300/// translate these semantics. This enum bridges that gap.
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
302pub enum OverflowBehavior {
303    /// Content that exceeds the container is clipped at the boundary.
304    /// This is the safe default for terminals where drawing outside
305    /// an allocated region corrupts neighbouring widgets.
306    #[default]
307    Clip,
308    /// Content is allowed to overflow visually (useful for popovers,
309    /// tooltips, and hit-test regions that extend beyond their container).
310    Visible,
311    /// Content is clipped but a scrollbar region is reserved.
312    /// The `max_content` field, when set, tells the scrollbar how
313    /// large the virtual content area is.
314    Scroll {
315        /// Size of the virtual content area in the overflow direction.
316        /// `None` means "determine from content measurement".
317        max_content: Option<u16>,
318    },
319    /// Items that don't fit are wrapped to the next row/column.
320    /// Only meaningful for [`Flex`] containers.
321    Wrap,
322}
323
324/// Responsive breakpoint tiers for terminal widths.
325///
326/// Ordered from smallest to largest. Each variant represents a width
327/// range determined by [`Breakpoints`].
328///
329/// | Breakpoint | Default Min Width | Typical Use               |
330/// |-----------|-------------------|---------------------------|
331/// | `Xs`      | < 60 cols         | Minimal / ultra-narrow    |
332/// | `Sm`      | 60–89 cols        | Compact layouts           |
333/// | `Md`      | 90–119 cols       | Standard terminal width   |
334/// | `Lg`      | 120–159 cols      | Wide terminals            |
335/// | `Xl`      | 160+ cols         | Ultra-wide / tiled        |
336#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
337pub enum Breakpoint {
338    /// Extra small: narrowest tier.
339    Xs,
340    /// Small: compact layouts.
341    Sm,
342    /// Medium: standard terminal width.
343    Md,
344    /// Large: wide terminals.
345    Lg,
346    /// Extra large: ultra-wide or tiled layouts.
347    Xl,
348}
349
350impl Breakpoint {
351    /// All breakpoints in ascending order.
352    pub const ALL: [Breakpoint; 5] = [
353        Breakpoint::Xs,
354        Breakpoint::Sm,
355        Breakpoint::Md,
356        Breakpoint::Lg,
357        Breakpoint::Xl,
358    ];
359
360    /// Ordinal index (0–4).
361    #[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    /// Short label for display.
373    #[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/// Breakpoint thresholds for responsive layouts.
392///
393/// Each field is the minimum width (in terminal columns) for that breakpoint.
394/// Xs implicitly starts at width 0.
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub struct Breakpoints {
397    /// Minimum width for Sm.
398    pub sm: u16,
399    /// Minimum width for Md.
400    pub md: u16,
401    /// Minimum width for Lg.
402    pub lg: u16,
403    /// Minimum width for Xl.
404    pub xl: u16,
405}
406
407impl Breakpoints {
408    /// Default breakpoints: 60 / 90 / 120 / 160 columns.
409    pub const DEFAULT: Self = Self {
410        sm: 60,
411        md: 90,
412        lg: 120,
413        xl: 160,
414    };
415
416    /// Create breakpoints with explicit thresholds.
417    ///
418    /// Values are sanitized to be monotonically non-decreasing.
419    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        // Default xl to lg + 40 if not specified via new_with_xl.
423        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    /// Create breakpoints with all four explicit thresholds.
431    ///
432    /// Values are sanitized to be monotonically non-decreasing.
433    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    /// Classify a width into a breakpoint bucket.
441    #[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    /// Classify a Size (uses width).
457    #[inline]
458    pub const fn classify_size(self, size: Size) -> Breakpoint {
459        self.classify_width(size.width)
460    }
461
462    /// Check if width is at least a given breakpoint.
463    #[inline]
464    pub const fn at_least(self, width: u16, min: Breakpoint) -> bool {
465        self.classify_width(width).index() >= min.index()
466    }
467
468    /// Check if width is between two breakpoints (inclusive).
469    #[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    /// Get the minimum width threshold for a given breakpoint.
476    #[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    /// Get all thresholds as `(Breakpoint, min_width)` pairs.
488    #[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/// Size negotiation hints for layout.
501#[derive(Debug, Clone, Copy, Default)]
502pub struct Measurement {
503    /// Minimum width in columns.
504    pub min_width: u16,
505    /// Minimum height in rows.
506    pub min_height: u16,
507    /// Maximum width (None = unbounded).
508    pub max_width: Option<u16>,
509    /// Maximum height (None = unbounded).
510    pub max_height: Option<u16>,
511}
512
513impl Measurement {
514    /// Create a fixed-size measurement (min == max).
515    #[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    /// Create a flexible measurement with minimum size and no maximum.
526    #[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/// A flexible layout container.
538#[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    /// Create a new vertical flex layout.
551    #[must_use]
552    pub fn vertical() -> Self {
553        Self {
554            direction: Direction::Vertical,
555            ..Default::default()
556        }
557    }
558
559    /// Create a new horizontal flex layout.
560    #[must_use]
561    pub fn horizontal() -> Self {
562        Self {
563            direction: Direction::Horizontal,
564            ..Default::default()
565        }
566    }
567
568    /// Set the layout direction.
569    #[must_use]
570    pub fn direction(mut self, direction: Direction) -> Self {
571        self.direction = direction;
572        self
573    }
574
575    /// Set the constraints.
576    #[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    /// Set the margin.
583    #[must_use]
584    pub fn margin(mut self, margin: Sides) -> Self {
585        self.margin = margin;
586        self
587    }
588
589    /// Set the gap between items.
590    #[must_use]
591    pub fn gap(mut self, gap: u16) -> Self {
592        self.gap = gap;
593        self
594    }
595
596    /// Set the alignment.
597    #[must_use]
598    pub fn alignment(mut self, alignment: Alignment) -> Self {
599        self.alignment = alignment;
600        self
601    }
602
603    /// Set the horizontal flow direction (LTR or RTL).
604    ///
605    /// When set to [`FlowDirection::Rtl`],
606    /// horizontal layouts are mirrored: the first child appears at the right
607    /// edge instead of the left. Vertical layouts are not affected.
608    #[must_use]
609    pub fn flow_direction(mut self, flow: direction::FlowDirection) -> Self {
610        self.flow_direction = flow;
611        self
612    }
613
614    /// Set the overflow behavior for this container.
615    #[must_use]
616    pub fn overflow(mut self, overflow: OverflowBehavior) -> Self {
617        self.overflow = overflow;
618        self
619    }
620
621    /// Get the current overflow behavior.
622    #[must_use]
623    pub fn overflow_behavior(&self) -> OverflowBehavior {
624        self.overflow
625    }
626
627    /// Number of constraints (and thus output rects from [`split`](Self::split)).
628    #[must_use]
629    pub fn constraint_count(&self) -> usize {
630        self.constraints.len()
631    }
632
633    /// Split the given area into smaller rectangles according to the configuration.
634    pub fn split(&self, area: Rect) -> Rects {
635        // Apply margin
636        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        // Calculate gaps safely
652        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        // Solve constraints to get sizes
657        let sizes = solve_constraints(&self.constraints, available_size);
658
659        // Convert sizes to rects
660        let mut rects = self.sizes_to_rects(inner, &sizes);
661
662        // Mirror horizontally for RTL horizontal layouts.
663        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        // Determine offsets strategy
683        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))) // 0 = Between
705                } 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))) // 1 = Around
717                } 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                    // Between: (Leftover * i) / slots + explicit gaps
735                    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                    // Around: nearest-integer rounding + explicit gaps
743                    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                // Fixed gap
750                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    /// Split area using intrinsic sizing from a measurer callback.
788    ///
789    /// This method enables content-aware layout with [`Constraint::FitContent`],
790    /// [`Constraint::FitContentBounded`], and [`Constraint::FitMin`].
791    ///
792    /// # Arguments
793    ///
794    /// - `area`: Available rectangle
795    /// - `measurer`: Callback that returns [`LayoutSizeHint`] for item at index
796    ///
797    /// # Example
798    ///
799    /// ```ignore
800    /// let flex = Flex::horizontal()
801    ///     .constraints([Constraint::FitContent, Constraint::Fill]);
802    ///
803    /// let rects = flex.split_with_measurer(area, |idx, available| {
804    ///     match idx {
805    ///         0 => LayoutSizeHint { min: 5, preferred: 20, max: None },
806    ///         _ => LayoutSizeHint::ZERO,
807    ///     }
808    /// });
809    /// ```
810    pub fn split_with_measurer<F>(&self, area: Rect, measurer: F) -> Rects
811    where
812        F: Fn(usize, u16) -> LayoutSizeHint,
813    {
814        // Apply margin
815        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        // Calculate gaps safely
831        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        // Solve constraints with hints from measurer
836        let sizes =
837            solve_constraints_with_hints(&self.constraints, available_size, &measurer, None);
838
839        // Convert sizes to rects
840        let mut rects = self.sizes_to_rects(inner, &sizes);
841
842        // Mirror horizontally for RTL horizontal layouts.
843        if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
844            direction::mirror_rects_horizontal(&mut rects, inner);
845        }
846
847        rects
848    }
849    /// Split area using intrinsic sizing and temporal coherence.
850    ///
851    /// Combines the content-aware sizing of [`split_with_measurer`](Self::split_with_measurer)
852    /// with stability across small geometry perturbations.
853    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        // Apply margin
863        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        // Calculate gaps safely
879        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        // Solve constraints with hints and coherence
884        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        // Convert sizes to rects
893        let mut rects = self.sizes_to_rects(inner, &sizes);
894
895        // Mirror horizontally for RTL horizontal layouts.
896        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
904/// Solve 1D constraints to determine sizes.
905///
906/// This shared logic is used by both Flex and Grid layouts.
907/// For intrinsic sizing support, use [`solve_constraints_with_hints`].
908pub(crate) fn solve_constraints(constraints: &[Constraint], available_size: u16) -> Sizes {
909    // Use the with_hints version with a no-op measurer and no coherence
910    solve_constraints_with_hints(
911        constraints,
912        available_size,
913        &|_, _| LayoutSizeHint::ZERO,
914        None,
915    )
916}
917
918/// Solve 1D constraints with intrinsic sizing support.
919///
920/// The measurer callback provides size hints for FitContent, FitContentBounded, and FitMin
921/// constraints. It receives the constraint index and remaining available space.
922pub(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    // Pass 1: Allocate hard minimums (Fixed, Min, FitMin, FitContentBounded min).
945    // These constraints are non-negotiable and take precedence over relative/soft constraints.
946    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                // Min will also be added to grow_indices in Pass 2
958            }
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                // Reserve the minimum bound immediately
973                let size = min(min_bound, remaining);
974                sizes[i] = size;
975                remaining = remaining.saturating_sub(size);
976            }
977            _ => {} // Soft constraints handled in Pass 2
978        }
979    }
980
981    // Pass 2: Allocate soft/relative constraints (Percentage, Ratio, FitContent preferred).
982    // These fill remaining space after hard minimums.
983    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            _ => {} // Hard constraints handled in Pass 1
1035        }
1036    }
1037
1038    // 3. Iterative distribution to flexible constraints
1039    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        // Calculate float targets for fair distribution (Largest Remainder Method)
1061        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        // Get previous allocation if coherence is enabled
1070        let prev_alloc = coherence
1071            .as_ref()
1072            .and_then(|(cache, id)| cache.get(id))
1073            .map(|full_prev| {
1074                // Extract only the shares for the current grow_indices
1075                grow_indices
1076                    .iter()
1077                    .map(|&i| full_prev.get(i).copied().unwrap_or(0))
1078                    .collect()
1079            });
1080
1081        // Distribute space with stable rounding to minimize jitter and error
1082        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        // Check for Max constraint violations
1089        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            // No violations, commit shares and exit
1100            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                // Store full-sized vector mapping constraint index -> share.
1105                // We must inflate the dense `distributed` vector to the sparse constraint space.
1106                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        // Handle violations: clamp to Max and remove from grow pool
1118        for i in violations {
1119            if let Constraint::Max(max_val) = constraints[i] {
1120                // Calculate how much space this item *actually* consumes from remaining
1121                // which is (max - current_size)
1122                let consumed = max_val.saturating_sub(sizes[i]);
1123                sizes[i] = max_val;
1124                remaining = remaining.saturating_sub(consumed);
1125
1126                // Remove from grow indices
1127                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
1137// ---------------------------------------------------------------------------
1138// Stable Layout Rounding: Min-Displacement with Temporal Coherence
1139// ---------------------------------------------------------------------------
1140
1141/// Previous frame's allocation, used as tie-breaker for temporal stability.
1142///
1143/// Pass `None` for the first frame or when no history is available.
1144/// When provided, the rounding algorithm prefers allocations that
1145/// minimize change from the previous frame, reducing visual jitter.
1146pub type PreviousAllocation = Option<Sizes>;
1147
1148/// Round real-valued layout targets to integer cells with exact sum conservation.
1149///
1150/// # Mathematical Model
1151///
1152/// Given real-valued targets `r_i` (from the constraint solver) and a required
1153/// integer total, find integer allocations `x_i` that:
1154///
1155/// ```text
1156/// minimize   Σ_i |x_i − r_i|  +  μ · Σ_i |x_i − x_i_prev|
1157/// subject to Σ_i x_i = total
1158///            x_i ≥ 0
1159/// ```
1160///
1161/// where `x_i_prev` is the previous frame's allocation and `μ` is the temporal
1162/// stability weight (default 0.1).
1163///
1164/// # Algorithm: Largest Remainder with Temporal Tie-Breaking
1165///
1166/// This uses a variant of the Largest Remainder Method (Hamilton's method),
1167/// which provides optimal bounded displacement (|x_i − r_i| < 1 for all i):
1168///
1169/// 1. **Floor phase**: Set `x_i = floor(r_i)` for each element.
1170/// 2. **Deficit**: Compute `D = total − Σ floor(r_i)` extra cells to distribute.
1171/// 3. **Priority sort**: Rank elements by remainder `r_i − floor(r_i)` (descending).
1172///    Break ties using a composite key:
1173///    a. Prefer elements where `x_i_prev = ceil(r_i)` (temporal stability).
1174///    b. Prefer elements with smaller index (determinism).
1175/// 4. **Distribute**: Award one extra cell to each of the top `D` elements.
1176///
1177/// # Properties
1178///
1179/// 1. **Sum conservation**: `Σ x_i = total` exactly (proven by construction).
1180/// 2. **Bounded displacement**: `|x_i − r_i| < 1` for all `i` (since each x_i
1181///    is either `floor(r_i)` or `ceil(r_i)`).
1182/// 3. **Deterministic**: Same inputs → identical outputs (temporal tie-break +
1183///    index tie-break provide total ordering).
1184/// 4. **Temporal coherence**: When targets change slightly, allocations tend to
1185///    stay the same (preferring the previous frame's rounding direction).
1186/// 5. **Optimal displacement**: Among all integer allocations summing to `total`
1187///    with `floor(r_i) ≤ x_i ≤ ceil(r_i)`, the Largest Remainder Method
1188///    minimizes total absolute displacement.
1189///
1190/// # Failure Modes
1191///
1192/// - **All-zero targets**: Returns all zeros. Harmless (empty layout).
1193/// - **Negative deficit**: Can occur if targets sum to less than `total` after
1194///   flooring. The algorithm handles this via the clamp in step 2.
1195/// - **Very large N**: O(N log N) due to sorting. Acceptable for typical
1196///   layout counts (< 100 items).
1197///
1198/// # Example
1199///
1200/// ```
1201/// use ftui_layout::round_layout_stable;
1202///
1203/// // Targets: [10.4, 20.6, 9.0] must sum to 40
1204/// let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
1205/// assert_eq!(result.iter().sum::<u16>(), 40);
1206/// // 10.4 → 10, 20.6 → 21, 9.0 → 9 = 40 ✓
1207/// assert_eq!(result.as_slice(), &[10, 21, 9]);
1208/// ```
1209pub 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    // Step 1: Floor all targets
1216    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    // Step 2: Compute deficit (extra cells to distribute)
1225    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        // Exact fit — no rounding needed
1233        return floors;
1234    }
1235
1236    // Step 3: Compute remainders and build priority list
1237    let mut priority: SmallVec<[(usize, f64, bool); LAYOUT_INLINE_CAP]> = targets
1238        .iter()
1239        .enumerate()
1240        .map(|(i, &r)| {
1241            // Sanitize non-finite targets (NaN/inf): NaN.max(0.0) already
1242            // floors to 0 above; a NaN remainder here would poison the sort.
1243            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            // Temporal stability: did previous allocation use ceil?
1251            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    // Sort by: remainder descending, then temporal preference, then index ascending.
1259    // total_cmp keeps the comparator a TOTAL order: the previous
1260    // partial_cmp().unwrap_or(Equal) made NaN compare Equal to everything
1261    // while real values still ordered — an intransitive comparator that
1262    // std's sort is documented to panic on.
1263    priority.sort_by(|a, b| {
1264        b.1.total_cmp(&a.1)
1265            .then_with(|| {
1266                // Prefer items where prev used ceil (true > false)
1267                b.2.cmp(&a.2)
1268            })
1269            .then_with(|| {
1270                // Deterministic tie-break: smaller index first
1271                a.0.cmp(&b.0)
1272            })
1273    });
1274
1275    // Step 4: Distribute deficit
1276    let mut result = floors;
1277    let mut remaining_deficit = deficit;
1278
1279    // We award at most one extra cell per item to maintain the invariant
1280    // |x_i - r_i| < 1 (bounded displacement). Hamilton's method only
1281    // handles D < n. If D >= n, it implies sum(floors) + n <= total,
1282    // which means sum(targets) was significantly less than total.
1283    // In this case, we distribute the surplus as evenly as possible.
1284    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
1301/// Handle the edge case where floored values exceed total.
1302///
1303/// This can happen with very small totals and many items. We greedily
1304/// reduce the largest items by 1 until the sum matches.
1305fn 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            // Cannot reduce further, even though overflow persists.
1321            // This happens if total < 0 (impossible for u16) or some other
1322            // degenerate state. Force sum to 0.
1323            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            // Hard fallback: should not happen if max_val > 0.
1355            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        // Regression: NaN remainders made the priority comparator
1378        // intransitive (NaN Equal to everything while real values still
1379        // ordered); std's sort is documented to panic on non-total orders.
1380        // Large-ish N to engage the driftsort merge paths.
1381        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        // The invariant under test is "no panic": the sort must stay a
1387        // total order in the presence of non-finite remainders.
1388        let sizes = round_layout_stable(&targets, 120, None);
1389        assert_eq!(sizes.len(), 64);
1390
1391        // And with only-NaN targets, all floors are 0 and the deficit path
1392        // still distributes exactly the requested total.
1393        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)); // Gap is 0 by default
1406    }
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        // Item 1: 0..10
1424        // Gap: 10..15
1425        // Item 2: 15..25
1426        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), // Should take half of remaining (90/2 = 45) + base 10? No, logic is simplified.
1435            Constraint::Percentage(10.0), // 10% of 100 = 10
1436        ]);
1437
1438        // Available: 100
1439        // Fixed(10) -> 10. Rem: 90.
1440        // Percent(10%) -> 10. Rem: 80.
1441        // Min(10) -> 10. Rem: 70.
1442        // Grow candidates: Min(10).
1443        // Distribute 70 to Min(10). Size = 10 + 70 = 80.
1444
1445        let rects = flex.split(Rect::new(0, 0, 100, 1));
1446        assert_eq!(rects[0].width, 10); // Fixed
1447        assert_eq!(rects[2].width, 10); // Percent
1448        assert_eq!(rects[1].width, 80); // Min + Remainder
1449    }
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        // Items should be pushed to the end: leftover = 100 - 20 = 80
1493        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        // Items should be centered: leftover = 100 - 40 = 60, offset = 30
1504        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        // Items: 30 total, leftover = 70, 2 gaps, 35 per gap
1519        assert_eq!(rects[0].x, 0);
1520        assert_eq!(rects[1].x, 45); // 10 + 35
1521        assert_eq!(rects[2].x, 90); // 45 + 10 + 35
1522    }
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        // Vertical: leftover = 100 - 10 = 90
1531        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        // Outer horizontal split
1538        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        // Inner vertical split on the first half
1543        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    // Property-like invariant tests
1551    #[test]
1552    fn invariant_total_size_does_not_exceed_available() {
1553        // Test that constraint solving never allocates more than available
1554        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    // --- Ratio constraint ---
1597
1598    #[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        // Zero denominator should not panic (max(1) guard)
1610        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        // Percentage is absolute against the total available.
1620        let rects = Flex::horizontal()
1621            .constraints([Constraint::Percentage(25.0)])
1622            .split(area);
1623        assert_eq!(rects[0].width, 25);
1624
1625        // Ratio(1, 4) should also be absolute (25% of 100 = 25).
1626        // It does NOT grow to fill remaining space.
1627        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        // Ratio(1, 4) takes 25 fixed. Fill takes remaining 75.
1638        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        // Ratio(0, 1) should logically get 0 space.
1648        // Test with Fill first to expose "last item gets remainder" logic artifact
1649        let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
1650        let rects = flex.split(Rect::new(0, 0, 100, 1));
1651
1652        // Fill should get 100, Ratio should get 0
1653        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    // --- Max constraint ---
1658
1659    #[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        // Since Ratio is absolute, 1/3 of 5 is 1, and 2/3 of 5 is 3.
1693        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    // --- SpaceAround alignment ---
1712
1713    #[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        // SpaceAround: leftover = 80, space_unit = 80/(2*2) = 20
1721        // First item starts at 20, second at 20+10+40=70
1722        assert_eq!(rects[0].x, 20);
1723        assert_eq!(rects[1].x, 70);
1724    }
1725
1726    // --- Vertical with gap ---
1727
1728    #[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    // --- Vertical center alignment ---
1739
1740    #[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        // leftover = 90, offset = 45
1747        assert_eq!(rects[0].y, 45);
1748        assert_eq!(rects[0].height, 10);
1749    }
1750
1751    // --- Single constraint gets all space ---
1752
1753    #[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    // --- Fixed exceeds available ---
1761
1762    #[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        // First gets 60, second gets remaining 40 (clamped)
1767        assert_eq!(rects[0].width, 60);
1768        assert_eq!(rects[1].width, 40);
1769    }
1770
1771    // --- Percentage that sums beyond 100% ---
1772
1773    #[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); // clamped to remaining
1780    }
1781
1782    // --- Margin reduces available space ---
1783
1784    #[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        // Inner: 10,10,80,80
1791        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    // --- Builder chain ---
1798
1799    #[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    // --- SpaceBetween with single item ---
1812
1813    #[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        // Single item: starts at 0, no extra spacing
1820        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        // All rects should be within the inner area (after margin)
1838        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    // --- Fill constraint ---
1856
1857    #[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); // Fill gets remaining
1863    }
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    // --- FitContent constraint ---
1874
1875    #[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); // FitContent gets preferred
1890        assert_eq!(rects[1].width, 70); // Fill gets remainder
1891    }
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        // First FitContent takes 80, second gets remaining 20
1902        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        // Without measurer (via split()), FitContent gets zero from default hint
1909        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); // No preferred size
1912        assert_eq!(rects[1].width, 100); // Fill gets all
1913    }
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    // --- FitContentBounded constraint ---
1943
1944    #[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, // Below min bound
1953            max: None,
1954        });
1955        assert_eq!(rects[0].width, 20); // Clamped to min bound
1956        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        // Available is 5 total, so FitContentBounded must clamp to remaining.
1971        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, // Above max bound
1984            max: None,
1985        });
1986        assert_eq!(rects[0].width, 30); // Clamped to max bound
1987        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, // Within bounds
1999            max: None,
2000        });
2001        assert_eq!(rects[0].width, 35);
2002        assert_eq!(rects[1].width, 65);
2003    }
2004
2005    // --- FitMin constraint ---
2006
2007    #[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        // FitMin gets minimum (15) and DOES NOT grow.
2022        // Fill gets the remaining 85.
2023        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        // Without measurer, min is 0. FitMin gets 0 and does not grow.
2032        // Fill takes all 100.
2033        assert_eq!(rects[0].width, 0);
2034        assert_eq!(rects[1].width, 100);
2035    }
2036
2037    // --- LayoutSizeHint tests ---
2038
2039    #[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); // Below min
2068        assert_eq!(h.clamp(15), 15); // In range
2069        assert_eq!(h.clamp(50), 30); // Above max
2070    }
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); // Below min
2076        assert_eq!(h.clamp(1000), 1000); // No max, stays as-is
2077    }
2078
2079    #[test]
2080    fn layout_size_hint_clamp_min_greater_than_max() {
2081        // When min > max, min should win (strict lower bound)
2082        let h = LayoutSizeHint {
2083            min: 20,
2084            preferred: 20,
2085            max: Some(10),
2086        };
2087        assert_eq!(h.clamp(5), 20); // 20 > 5, clamped to min
2088        assert_eq!(h.clamp(15), 20); // 20 > 15, clamped to min
2089        assert_eq!(h.clamp(25), 20); // 20 > 10, clamped to min
2090    }
2091
2092    // --- Integration: FitContent with other constraints ---
2093
2094    #[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); // Fixed
2113        assert_eq!(rects[1].width, 25); // FitContent preferred
2114        assert_eq!(rects[2].width, 55); // Fill gets remainder
2115    }
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    // -----------------------------------------------------------------------
2142    // Stable Layout Rounding Tests (bd-4kq0.4.1)
2143    // -----------------------------------------------------------------------
2144
2145    mod rounding_tests {
2146        use super::super::*;
2147
2148        // --- Sum conservation (REQUIRED) ---
2149
2150        #[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        // --- Bounded displacement ---
2193
2194        #[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        // --- Temporal tie-break (REQUIRED) ---
2216
2217        #[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        // --- Property: min displacement (REQUIRED) ---
2249
2250        #[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        // --- Determinism ---
2290
2291        #[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        // --- Edge cases ---
2300
2301        #[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    // -----------------------------------------------------------------------
2380    // Property Tests: Constraint Satisfaction (bd-4kq0.4.3)
2381    // -----------------------------------------------------------------------
2382
2383    mod property_constraint_tests {
2384        use super::super::*;
2385
2386        /// Deterministic LCG pseudo-random number generator (no external deps).
2387        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        /// Generate a random constraint from the LCG.
2412        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    // -----------------------------------------------------------------------
2594    // Property Tests: Temporal Stability (bd-4kq0.4.3)
2595    // -----------------------------------------------------------------------
2596
2597    mod property_temporal_tests {
2598        use super::super::*;
2599        use crate::cache::{CoherenceCache, CoherenceId};
2600
2601        /// Deterministic LCG.
2602        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    // -----------------------------------------------------------------------
2752    // Snapshot Regression: Canonical Flex/Grid Layouts (bd-4kq0.4.3)
2753    // -----------------------------------------------------------------------
2754
2755    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        // --- Flex snapshots: 80x24 ---
2836
2837        #[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        // --- Flex snapshots: 120x40 ---
2901
2902        #[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        // --- Grid snapshots: 80x24 ---
2970
2971        #[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        // --- Grid snapshots: 120x40 ---
3030
3031        #[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}