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