#![forbid(unsafe_code)]
pub mod cache;
pub mod debug;
pub mod dep_graph;
pub mod direction;
pub mod egraph;
pub mod grid;
pub mod incremental;
pub mod pane;
#[cfg(test)]
mod repro_max_constraint;
#[cfg(test)]
mod repro_space_around;
pub mod responsive;
pub mod responsive_layout;
pub mod veb_tree;
pub mod visibility;
pub mod workspace;
pub use cache::{
CoherenceCache, CoherenceId, LayoutCache, LayoutCacheKey, LayoutCacheStats, S3FifoLayoutCache,
};
pub use direction::{FlowDirection, LogicalAlignment, LogicalSides, mirror_rects_horizontal};
pub use ftui_core::geometry::{Rect, Sides, Size};
pub use grid::{Grid, GridArea, GridLayout};
pub use pane::{
PANE_DEFAULT_MARGIN_CELLS, PANE_DEFAULT_PADDING_CELLS, PANE_DRAG_RESIZE_DEFAULT_HYSTERESIS,
PANE_DRAG_RESIZE_DEFAULT_THRESHOLD, PANE_EDGE_GRIP_INSET_CELLS, PANE_MAGNETIC_FIELD_CELLS,
PANE_SEMANTIC_INPUT_EVENT_SCHEMA_VERSION, PANE_SEMANTIC_INPUT_TRACE_SCHEMA_VERSION,
PANE_SNAP_DEFAULT_HYSTERESIS_BPS, PANE_SNAP_DEFAULT_STEP_BPS, PANE_TREE_SCHEMA_VERSION,
PaneCancelReason, PaneConstraints, PaneCoordinateNormalizationError, PaneCoordinateNormalizer,
PaneCoordinateRoundingPolicy, PaneDockPreview, PaneDockZone, PaneDragBehaviorTuning,
PaneDragResizeEffect, PaneDragResizeMachine, PaneDragResizeMachineError,
PaneDragResizeNoopReason, PaneDragResizeState, PaneDragResizeTransition, PaneEdgeResizePlan,
PaneEdgeResizePlanError, PaneGroupTransformPlan, PaneId, PaneIdAllocator, PaneInertialThrow,
PaneInputCoordinate, PaneInteractionPolicyError, PaneInteractionTimeline,
PaneInteractionTimelineCheckpointDecision, PaneInteractionTimelineEntry,
PaneInteractionTimelineError, PaneInteractionTimelineReplayDiagnostics, PaneInvariantCode,
PaneInvariantIssue, PaneInvariantReport, PaneInvariantSeverity, PaneLayout,
PaneLayoutIntelligenceMode, PaneLeaf, PaneModelError, PaneModifierSnapshot, PaneMotionVector,
PaneNodeKind, PaneNodeRecord, PaneNormalizedCoordinate, PaneOperation, PaneOperationError,
PaneOperationFailure, PaneOperationJournalEntry, PaneOperationJournalResult, PaneOperationKind,
PaneOperationOutcome, PanePlacement, PanePointerButton, PanePointerPosition, PanePrecisionMode,
PanePrecisionPolicy, PanePressureSnapProfile, PaneReflowMovePlan, PaneReflowPlanError,
PaneRepairAction, PaneRepairError, PaneRepairFailure, PaneRepairOutcome, PaneResizeDirection,
PaneResizeGrip, PaneResizeTarget, PaneScaleFactor, PaneSelectionState, PaneSemanticInputEvent,
PaneSemanticInputEventError, PaneSemanticInputEventKind, PaneSemanticInputTrace,
PaneSemanticInputTraceError, PaneSemanticInputTraceMetadata,
PaneSemanticReplayConformanceArtifact, PaneSemanticReplayDiffArtifact,
PaneSemanticReplayDiffKind, PaneSemanticReplayError, PaneSemanticReplayFixture,
PaneSemanticReplayOutcome, PaneSnapDecision, PaneSnapReason, PaneSnapTuning, PaneSplit,
PaneSplitRatio, PaneTransaction, PaneTransactionOutcome, PaneTree, PaneTreeSnapshot, SplitAxis,
};
pub use responsive::Responsive;
pub use responsive_layout::{ResponsiveLayout, ResponsiveSplit};
pub use smallvec;
use smallvec::SmallVec;
use std::cmp::min;
pub use visibility::Visibility;
pub use workspace::{
MigrationResult, WORKSPACE_SCHEMA_VERSION, WorkspaceMetadata, WorkspaceMigrationError,
WorkspaceSnapshot, WorkspaceValidationError, migrate_workspace, needs_migration,
};
const LAYOUT_INLINE_CAP: usize = 8;
pub type Rects = SmallVec<[Rect; LAYOUT_INLINE_CAP]>;
type Sizes = SmallVec<[u16; LAYOUT_INLINE_CAP]>;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Constraint {
Fixed(u16),
Percentage(f32),
Min(u16),
Max(u16),
Ratio(u32, u32),
Fill,
FitContent,
FitContentBounded {
min: u16,
max: u16,
},
FitMin,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct LayoutSizeHint {
pub min: u16,
pub preferred: u16,
pub max: Option<u16>,
}
impl LayoutSizeHint {
pub const ZERO: Self = Self {
min: 0,
preferred: 0,
max: None,
};
#[inline]
pub const fn exact(size: u16) -> Self {
Self {
min: size,
preferred: size,
max: Some(size),
}
}
#[inline]
pub const fn at_least(min: u16, preferred: u16) -> Self {
Self {
min,
preferred,
max: None,
}
}
#[inline]
pub fn clamp(&self, value: u16) -> u16 {
let max = self.max.unwrap_or(u16::MAX);
value.min(max).max(self.min)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Direction {
#[default]
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Alignment {
#[default]
Start,
Center,
End,
SpaceAround,
SpaceBetween,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum OverflowBehavior {
#[default]
Clip,
Visible,
Scroll {
max_content: Option<u16>,
},
Wrap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Breakpoint {
Xs,
Sm,
Md,
Lg,
Xl,
}
impl Breakpoint {
pub const ALL: [Breakpoint; 5] = [
Breakpoint::Xs,
Breakpoint::Sm,
Breakpoint::Md,
Breakpoint::Lg,
Breakpoint::Xl,
];
#[inline]
const fn index(self) -> u8 {
match self {
Breakpoint::Xs => 0,
Breakpoint::Sm => 1,
Breakpoint::Md => 2,
Breakpoint::Lg => 3,
Breakpoint::Xl => 4,
}
}
#[must_use]
pub const fn label(self) -> &'static str {
match self {
Breakpoint::Xs => "xs",
Breakpoint::Sm => "sm",
Breakpoint::Md => "md",
Breakpoint::Lg => "lg",
Breakpoint::Xl => "xl",
}
}
}
impl std::fmt::Display for Breakpoint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Breakpoints {
pub sm: u16,
pub md: u16,
pub lg: u16,
pub xl: u16,
}
impl Breakpoints {
pub const DEFAULT: Self = Self {
sm: 60,
md: 90,
lg: 120,
xl: 160,
};
pub const fn new(sm: u16, md: u16, lg: u16) -> Self {
let md = if md < sm { sm } else { md };
let lg = if lg < md { md } else { lg };
let xl = match lg.checked_add(40) {
Some(v) => v,
None => u16::MAX,
};
Self { sm, md, lg, xl }
}
pub const fn new_with_xl(sm: u16, md: u16, lg: u16, xl: u16) -> Self {
let md = if md < sm { sm } else { md };
let lg = if lg < md { md } else { lg };
let xl = if xl < lg { lg } else { xl };
Self { sm, md, lg, xl }
}
#[inline]
pub const fn classify_width(self, width: u16) -> Breakpoint {
if width >= self.xl {
Breakpoint::Xl
} else if width >= self.lg {
Breakpoint::Lg
} else if width >= self.md {
Breakpoint::Md
} else if width >= self.sm {
Breakpoint::Sm
} else {
Breakpoint::Xs
}
}
#[inline]
pub const fn classify_size(self, size: Size) -> Breakpoint {
self.classify_width(size.width)
}
#[inline]
pub const fn at_least(self, width: u16, min: Breakpoint) -> bool {
self.classify_width(width).index() >= min.index()
}
#[inline]
pub const fn between(self, width: u16, min: Breakpoint, max: Breakpoint) -> bool {
let idx = self.classify_width(width).index();
idx >= min.index() && idx <= max.index()
}
#[must_use]
pub const fn threshold(self, bp: Breakpoint) -> u16 {
match bp {
Breakpoint::Xs => 0,
Breakpoint::Sm => self.sm,
Breakpoint::Md => self.md,
Breakpoint::Lg => self.lg,
Breakpoint::Xl => self.xl,
}
}
#[must_use]
pub const fn thresholds(self) -> [(Breakpoint, u16); 5] {
[
(Breakpoint::Xs, 0),
(Breakpoint::Sm, self.sm),
(Breakpoint::Md, self.md),
(Breakpoint::Lg, self.lg),
(Breakpoint::Xl, self.xl),
]
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Measurement {
pub min_width: u16,
pub min_height: u16,
pub max_width: Option<u16>,
pub max_height: Option<u16>,
}
impl Measurement {
#[must_use]
pub fn fixed(width: u16, height: u16) -> Self {
Self {
min_width: width,
min_height: height,
max_width: Some(width),
max_height: Some(height),
}
}
#[must_use]
pub fn flexible(min_width: u16, min_height: u16) -> Self {
Self {
min_width,
min_height,
max_width: None,
max_height: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Flex {
direction: Direction,
constraints: Vec<Constraint>,
margin: Sides,
gap: u16,
alignment: Alignment,
flow_direction: direction::FlowDirection,
overflow: OverflowBehavior,
}
impl Flex {
#[must_use]
pub fn vertical() -> Self {
Self {
direction: Direction::Vertical,
..Default::default()
}
}
#[must_use]
pub fn horizontal() -> Self {
Self {
direction: Direction::Horizontal,
..Default::default()
}
}
#[must_use]
pub fn direction(mut self, direction: Direction) -> Self {
self.direction = direction;
self
}
#[must_use]
pub fn constraints(mut self, constraints: impl IntoIterator<Item = Constraint>) -> Self {
self.constraints = constraints.into_iter().collect();
self
}
#[must_use]
pub fn margin(mut self, margin: Sides) -> Self {
self.margin = margin;
self
}
#[must_use]
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
#[must_use]
pub fn alignment(mut self, alignment: Alignment) -> Self {
self.alignment = alignment;
self
}
#[must_use]
pub fn flow_direction(mut self, flow: direction::FlowDirection) -> Self {
self.flow_direction = flow;
self
}
#[must_use]
pub fn overflow(mut self, overflow: OverflowBehavior) -> Self {
self.overflow = overflow;
self
}
#[must_use]
pub fn overflow_behavior(&self) -> OverflowBehavior {
self.overflow
}
#[must_use]
pub fn constraint_count(&self) -> usize {
self.constraints.len()
}
pub fn split(&self, area: Rect) -> Rects {
let inner = area.inner(self.margin);
if inner.is_empty() {
return self.constraints.iter().map(|_| Rect::default()).collect();
}
let total_size = match self.direction {
Direction::Horizontal => inner.width,
Direction::Vertical => inner.height,
};
let count = self.constraints.len();
if count == 0 {
return Rects::new();
}
let gap_count = count - 1;
let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
let available_size = total_size.saturating_sub(total_gap);
let sizes = solve_constraints(&self.constraints, available_size);
let mut rects = self.sizes_to_rects(inner, &sizes);
if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
direction::mirror_rects_horizontal(&mut rects, inner);
}
rects
}
fn sizes_to_rects(&self, area: Rect, sizes: &[u16]) -> Rects {
let mut rects = SmallVec::with_capacity(sizes.len());
if sizes.is_empty() {
return rects;
}
let total_items_size: u16 = sizes.iter().fold(0u16, |acc, &s| acc.saturating_add(s));
let total_available = match self.direction {
Direction::Horizontal => area.width,
Direction::Vertical => area.height,
};
let (start_shift, use_formula) = match self.alignment {
Alignment::Start => (0, None),
Alignment::End => {
let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
.min(u16::MAX as u64) as u16;
let used = total_items_size.saturating_add(gap_space);
(total_available.saturating_sub(used), None)
}
Alignment::Center => {
let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
.min(u16::MAX as u64) as u16;
let used = total_items_size.saturating_add(gap_space);
(total_available.saturating_sub(used) / 2, None)
}
Alignment::SpaceBetween => {
let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
.min(u16::MAX as u64) as u16;
let used = total_items_size.saturating_add(gap_space);
let leftover = total_available.saturating_sub(used);
let slots = sizes.len().saturating_sub(1);
if slots > 0 {
(0, Some((leftover, slots, 0))) } else {
(0, None)
}
}
Alignment::SpaceAround => {
let gap_space = (sizes.len().saturating_sub(1) as u64 * self.gap as u64)
.min(u16::MAX as u64) as u16;
let used = total_items_size.saturating_add(gap_space);
let leftover = total_available.saturating_sub(used);
let slots = sizes.len() * 2;
if slots > 0 {
(0, Some((leftover, slots, 1))) } else {
(0, None)
}
}
};
let mut accumulated_size = 0;
for (i, &size) in sizes.iter().enumerate() {
let explicit_gap_so_far = if i > 0 {
(i as u64 * self.gap as u64).min(u16::MAX as u64) as u16
} else {
0
};
let gap_offset = if let Some((leftover, slots, mode)) = use_formula {
if mode == 0 {
if i == 0 {
0
} else {
explicit_gap_so_far
.saturating_add((leftover as u64 * i as u64 / slots as u64) as u16)
}
} else {
let numerator = leftover as u64 * (2 * i as u64 + 1);
let denominator = slots as u64;
let raw = (numerator + (denominator / 2)) / denominator;
explicit_gap_so_far.saturating_add(raw.min(u64::from(u16::MAX)) as u16)
}
} else {
explicit_gap_so_far
};
let pos = match self.direction {
Direction::Horizontal => area
.x
.saturating_add(start_shift)
.saturating_add(accumulated_size)
.saturating_add(gap_offset),
Direction::Vertical => area
.y
.saturating_add(start_shift)
.saturating_add(accumulated_size)
.saturating_add(gap_offset),
};
let rect = match self.direction {
Direction::Horizontal => Rect {
x: pos,
y: area.y,
width: size.min(area.right().saturating_sub(pos)),
height: area.height,
},
Direction::Vertical => Rect {
x: area.x,
y: pos,
width: area.width,
height: size.min(area.bottom().saturating_sub(pos)),
},
};
rects.push(rect);
accumulated_size = accumulated_size.saturating_add(size);
}
rects
}
pub fn split_with_measurer<F>(&self, area: Rect, measurer: F) -> Rects
where
F: Fn(usize, u16) -> LayoutSizeHint,
{
let inner = area.inner(self.margin);
if inner.is_empty() {
return self.constraints.iter().map(|_| Rect::default()).collect();
}
let total_size = match self.direction {
Direction::Horizontal => inner.width,
Direction::Vertical => inner.height,
};
let count = self.constraints.len();
if count == 0 {
return Rects::new();
}
let gap_count = count - 1;
let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
let available_size = total_size.saturating_sub(total_gap);
let sizes =
solve_constraints_with_hints(&self.constraints, available_size, &measurer, None);
let mut rects = self.sizes_to_rects(inner, &sizes);
if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
direction::mirror_rects_horizontal(&mut rects, inner);
}
rects
}
pub fn split_with_measurer_stably<F>(
&self,
area: Rect,
measurer: F,
cache: &mut CoherenceCache,
) -> Rects
where
F: Fn(usize, u16) -> LayoutSizeHint,
{
let inner = area.inner(self.margin);
if inner.is_empty() {
return self.constraints.iter().map(|_| Rect::default()).collect();
}
let total_size = match self.direction {
Direction::Horizontal => inner.width,
Direction::Vertical => inner.height,
};
let count = self.constraints.len();
if count == 0 {
return Rects::new();
}
let gap_count = count - 1;
let total_gap = (gap_count as u64 * self.gap as u64).min(u16::MAX as u64) as u16;
let available_size = total_size.saturating_sub(total_gap);
let id = CoherenceId::new(&self.constraints, self.direction);
let sizes = solve_constraints_with_hints(
&self.constraints,
available_size,
&measurer,
Some((cache, id)),
);
let mut rects = self.sizes_to_rects(inner, &sizes);
if self.flow_direction.is_rtl() && self.direction == Direction::Horizontal {
direction::mirror_rects_horizontal(&mut rects, inner);
}
rects
}
}
pub(crate) fn solve_constraints(constraints: &[Constraint], available_size: u16) -> Sizes {
solve_constraints_with_hints(
constraints,
available_size,
&|_, _| LayoutSizeHint::ZERO,
None,
)
}
pub(crate) fn solve_constraints_with_hints<F>(
constraints: &[Constraint],
available_size: u16,
measurer: &F,
mut coherence: Option<(&mut CoherenceCache, CoherenceId)>,
) -> Sizes
where
F: Fn(usize, u16) -> LayoutSizeHint,
{
const WEIGHT_SCALE: u64 = 10_000;
let mut sizes: Sizes = smallvec::smallvec![0u16; constraints.len()];
let mut remaining = available_size;
let mut grow_indices: SmallVec<[usize; LAYOUT_INLINE_CAP]> = SmallVec::new();
let grow_weight = |constraint: Constraint| -> u64 {
match constraint {
Constraint::Min(_) | Constraint::Max(_) | Constraint::Fill => WEIGHT_SCALE,
_ => 0,
}
};
for (i, &constraint) in constraints.iter().enumerate() {
match constraint {
Constraint::Fixed(size) => {
let size = min(size, remaining);
sizes[i] = size;
remaining = remaining.saturating_sub(size);
}
Constraint::Min(min_size) => {
let size = min(min_size, remaining);
sizes[i] = size;
remaining = remaining.saturating_sub(size);
}
Constraint::FitMin => {
let hint = measurer(i, remaining);
let size = min(hint.min, remaining);
sizes[i] = size;
remaining = remaining.saturating_sub(size);
}
Constraint::FitContent => {
let hint = measurer(i, remaining);
let size = min(hint.min, remaining);
sizes[i] = size;
remaining = remaining.saturating_sub(size);
}
Constraint::FitContentBounded { min: min_bound, .. } => {
let size = min(min_bound, remaining);
sizes[i] = size;
remaining = remaining.saturating_sub(size);
}
_ => {} }
}
for (i, &constraint) in constraints.iter().enumerate() {
match constraint {
Constraint::Percentage(p) => {
let target = (available_size as f32 * p / 100.0)
.round()
.min(u16::MAX as f32) as u16;
let needed = target.saturating_sub(sizes[i]);
let alloc = min(needed, remaining);
sizes[i] = sizes[i].saturating_add(alloc);
remaining = remaining.saturating_sub(alloc);
}
Constraint::Ratio(n, d) => {
let target = if d == 0 {
0
} else {
(u64::from(available_size) * u64::from(n) / u64::from(d)).min(u16::MAX as u64)
as u16
};
let needed = target.saturating_sub(sizes[i]);
let alloc = min(needed, remaining);
sizes[i] = sizes[i].saturating_add(alloc);
remaining = remaining.saturating_sub(alloc);
}
Constraint::FitContent => {
let hint = measurer(i, remaining);
let preferred = hint
.preferred
.max(sizes[i])
.min(hint.max.unwrap_or(u16::MAX));
let needed = preferred.saturating_sub(sizes[i]);
let alloc = min(needed, remaining);
sizes[i] = sizes[i].saturating_add(alloc);
remaining = remaining.saturating_sub(alloc);
}
Constraint::FitContentBounded { max: max_bound, .. } => {
let hint = measurer(i, remaining);
let preferred = hint.preferred.max(sizes[i]).min(max_bound);
let needed = preferred.saturating_sub(sizes[i]);
let alloc = min(needed, remaining);
sizes[i] = sizes[i].saturating_add(alloc);
remaining = remaining.saturating_sub(alloc);
}
Constraint::Min(_) => {
grow_indices.push(i);
}
Constraint::Max(_) => {
grow_indices.push(i);
}
Constraint::Fill => {
grow_indices.push(i);
}
_ => {} }
}
loop {
if remaining == 0 || grow_indices.is_empty() {
break;
}
let mut total_weight = 0u128;
for &i in &grow_indices {
let weight = grow_weight(constraints[i]);
if weight > 0 {
total_weight = total_weight.saturating_add(u128::from(weight));
}
}
if total_weight == 0 {
break;
}
let space_to_distribute = remaining;
let mut shares: SmallVec<[u16; LAYOUT_INLINE_CAP]> =
smallvec::smallvec![0u16; constraints.len()];
let targets: Vec<f64> = grow_indices
.iter()
.map(|&i| {
let weight = grow_weight(constraints[i]);
(space_to_distribute as f64 * weight as f64) / total_weight as f64
})
.collect();
let prev_alloc = coherence
.as_ref()
.and_then(|(cache, id)| cache.get(id))
.map(|full_prev| {
grow_indices
.iter()
.map(|&i| full_prev.get(i).copied().unwrap_or(0))
.collect()
});
let distributed = round_layout_stable(&targets, space_to_distribute, prev_alloc);
for (k, &i) in grow_indices.iter().enumerate() {
shares[i] = distributed[k];
}
let mut violations = Vec::new();
for &i in &grow_indices {
if let Constraint::Max(max_val) = constraints[i]
&& sizes[i].saturating_add(shares[i]) > max_val
{
violations.push(i);
}
}
if violations.is_empty() {
for &i in &grow_indices {
sizes[i] = sizes[i].saturating_add(shares[i]);
}
if let Some((cache, id)) = coherence.as_mut() {
if distributed.len() == targets.len() {
let mut full_shares: Sizes = smallvec::smallvec![0u16; constraints.len()];
for (k, &i) in grow_indices.iter().enumerate() {
full_shares[i] = distributed[k];
}
cache.store(*id, full_shares);
}
}
break;
}
for i in violations {
if let Constraint::Max(max_val) = constraints[i] {
let consumed = max_val.saturating_sub(sizes[i]);
sizes[i] = max_val;
remaining = remaining.saturating_sub(consumed);
if let Some(pos) = grow_indices.iter().position(|&x| x == i) {
grow_indices.remove(pos);
}
}
}
}
sizes
}
pub type PreviousAllocation = Option<Sizes>;
pub fn round_layout_stable(targets: &[f64], total: u16, prev: PreviousAllocation) -> Sizes {
let n = targets.len();
if n == 0 {
return Sizes::new();
}
let floors: Sizes = targets
.iter()
.map(|&r| (r.max(0.0).floor() as u64).min(u16::MAX as u64) as u16)
.collect();
let floor_sum: u64 = floors.iter().map(|&x| u64::from(x)).sum();
let total_u64 = u64::from(total);
if floor_sum > total_u64 {
return redistribute_overflow(&floors, total);
}
let deficit = (total_u64 - floor_sum) as u16;
if deficit == 0 {
return floors;
}
let mut priority: SmallVec<[(usize, f64, bool); LAYOUT_INLINE_CAP]> = targets
.iter()
.enumerate()
.map(|(i, &r)| {
let remainder = r - (floors[i] as f64);
let ceil_val = floors[i].saturating_add(1);
let prev_used_ceil = prev
.as_ref()
.is_some_and(|p| p.get(i).copied() == Some(ceil_val));
(i, remainder, prev_used_ceil)
})
.collect();
priority.sort_by(|a, b| {
b.1.partial_cmp(&a.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| {
b.2.cmp(&a.2)
})
.then_with(|| {
a.0.cmp(&b.0)
})
});
let mut result = floors;
let mut remaining_deficit = deficit;
if remaining_deficit as usize >= n {
let per_item = remaining_deficit / (n as u16);
for val in result.iter_mut() {
*val = val.saturating_add(per_item);
}
remaining_deficit %= n as u16;
}
if remaining_deficit > 0 {
for &(i, _, _) in priority.iter().take(remaining_deficit as usize) {
result[i] = result[i].saturating_add(1);
}
}
result
}
fn redistribute_overflow(floors: &[u16], total: u16) -> Sizes {
let mut result: Sizes = floors.iter().copied().collect();
let current_sum: u64 = result.iter().map(|&x| u64::from(x)).sum();
let total_u64 = u64::from(total);
let n = result.len();
if current_sum <= total_u64 || n == 0 {
return result;
}
let mut overflow = current_sum - total_u64;
while overflow > 0 {
let &max_val = result.iter().max().unwrap_or(&0);
if max_val == 0 {
for val in result.iter_mut() {
*val = 0;
}
break;
}
let count_max = result.iter().filter(|&&v| v == max_val).count() as u64;
let &next_max = result.iter().filter(|&&v| v < max_val).max().unwrap_or(&0);
let delta = (max_val - next_max) as u64;
let required_per_item = overflow.div_ceil(count_max);
let reduce_per_item = delta.min(required_per_item).max(1) as u16;
let mut reduced_any = false;
for val in result.iter_mut() {
if *val == max_val {
let amount = u64::from(*val)
.min(u64::from(reduce_per_item))
.min(overflow) as u16;
if amount > 0 {
*val -= amount;
overflow -= u64::from(amount);
reduced_any = true;
}
if overflow == 0 {
break;
}
}
}
if !reduced_any {
for val in result.iter_mut() {
if overflow == 0 {
break;
}
if *val > 0 {
*val -= 1;
overflow -= 1;
}
}
break;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_split() {
let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(20)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects.len(), 2);
assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
assert_eq!(rects[1], Rect::new(10, 0, 20, 10)); }
#[test]
fn percentage_split() {
let flex = Flex::horizontal()
.constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].width, 50);
assert_eq!(rects[1].width, 50);
}
#[test]
fn gap_handling() {
let flex = Flex::horizontal()
.gap(5)
.constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0], Rect::new(0, 0, 10, 10));
assert_eq!(rects[1], Rect::new(15, 0, 10, 10));
}
#[test]
fn mixed_constraints() {
let flex = Flex::horizontal().constraints([
Constraint::Fixed(10),
Constraint::Min(10), Constraint::Percentage(10.0), ]);
let rects = flex.split(Rect::new(0, 0, 100, 1));
assert_eq!(rects[0].width, 10); assert_eq!(rects[2].width, 10); assert_eq!(rects[1].width, 80); }
#[test]
fn measurement_fixed_constraints() {
let fixed = Measurement::fixed(5, 7);
assert_eq!(fixed.min_width, 5);
assert_eq!(fixed.min_height, 7);
assert_eq!(fixed.max_width, Some(5));
assert_eq!(fixed.max_height, Some(7));
}
#[test]
fn measurement_flexible_constraints() {
let flexible = Measurement::flexible(2, 3);
assert_eq!(flexible.min_width, 2);
assert_eq!(flexible.min_height, 3);
assert_eq!(flexible.max_width, None);
assert_eq!(flexible.max_height, None);
}
#[test]
fn breakpoints_classify_defaults() {
let bp = Breakpoints::DEFAULT;
assert_eq!(bp.classify_width(20), Breakpoint::Xs);
assert_eq!(bp.classify_width(60), Breakpoint::Sm);
assert_eq!(bp.classify_width(90), Breakpoint::Md);
assert_eq!(bp.classify_width(120), Breakpoint::Lg);
}
#[test]
fn breakpoints_at_least_and_between() {
let bp = Breakpoints::new(50, 80, 110);
assert!(bp.at_least(85, Breakpoint::Sm));
assert!(bp.between(85, Breakpoint::Sm, Breakpoint::Md));
assert!(!bp.between(85, Breakpoint::Lg, Breakpoint::Lg));
}
#[test]
fn alignment_end() {
let flex = Flex::horizontal()
.alignment(Alignment::End)
.constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0], Rect::new(80, 0, 10, 10));
assert_eq!(rects[1], Rect::new(90, 0, 10, 10));
}
#[test]
fn alignment_center() {
let flex = Flex::horizontal()
.alignment(Alignment::Center)
.constraints([Constraint::Fixed(20), Constraint::Fixed(20)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0], Rect::new(30, 0, 20, 10));
assert_eq!(rects[1], Rect::new(50, 0, 20, 10));
}
#[test]
fn alignment_space_between() {
let flex = Flex::horizontal()
.alignment(Alignment::SpaceBetween)
.constraints([
Constraint::Fixed(10),
Constraint::Fixed(10),
Constraint::Fixed(10),
]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].x, 0);
assert_eq!(rects[1].x, 45); assert_eq!(rects[2].x, 90); }
#[test]
fn vertical_alignment() {
let flex = Flex::vertical()
.alignment(Alignment::End)
.constraints([Constraint::Fixed(5), Constraint::Fixed(5)]);
let rects = flex.split(Rect::new(0, 0, 10, 100));
assert_eq!(rects[0], Rect::new(0, 90, 10, 5));
assert_eq!(rects[1], Rect::new(0, 95, 10, 5));
}
#[test]
fn nested_flex_support() {
let outer = Flex::horizontal()
.constraints([Constraint::Percentage(50.0), Constraint::Percentage(50.0)]);
let outer_rects = outer.split(Rect::new(0, 0, 100, 100));
let inner = Flex::vertical().constraints([Constraint::Fixed(30), Constraint::Min(10)]);
let inner_rects = inner.split(outer_rects[0]);
assert_eq!(inner_rects[0], Rect::new(0, 0, 50, 30));
assert_eq!(inner_rects[1], Rect::new(0, 30, 50, 70));
}
#[test]
fn invariant_total_size_does_not_exceed_available() {
for total in [10u16, 50, 100, 255] {
let flex = Flex::horizontal().constraints([
Constraint::Fixed(30),
Constraint::Percentage(50.0),
Constraint::Min(20),
]);
let rects = flex.split(Rect::new(0, 0, total, 10));
let total_width: u16 = rects.iter().map(|r| r.width).sum();
assert!(
total_width <= total,
"Total width {} exceeded available {} for constraints",
total_width,
total
);
}
}
#[test]
fn invariant_empty_area_produces_empty_rects() {
let flex = Flex::horizontal().constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 0, 0));
assert!(rects.iter().all(|r| r.is_empty()));
}
#[test]
fn invariant_no_constraints_produces_empty_vec() {
let flex = Flex::horizontal().constraints([]);
let rects = flex.split(Rect::new(0, 0, 100, 100));
assert!(rects.is_empty());
}
#[test]
fn ratio_constraint_splits_proportionally() {
let flex =
Flex::horizontal().constraints([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)]);
let rects = flex.split(Rect::new(0, 0, 90, 10));
assert_eq!(rects[0].width, 30);
assert_eq!(rects[1].width, 60);
}
#[test]
fn ratio_constraint_with_zero_denominator() {
let flex = Flex::horizontal().constraints([Constraint::Ratio(1, 0)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects.len(), 1);
}
#[test]
fn ratio_is_absolute_fraction() {
let area = Rect::new(0, 0, 100, 1);
let rects = Flex::horizontal()
.constraints([Constraint::Percentage(25.0)])
.split(area);
assert_eq!(rects[0].width, 25);
let rects = Flex::horizontal()
.constraints([Constraint::Ratio(1, 4)])
.split(area);
assert_eq!(rects[0].width, 25);
}
#[test]
fn ratio_is_independent_of_grow_items() {
let area = Rect::new(0, 0, 100, 1);
let rects = Flex::horizontal()
.constraints([Constraint::Ratio(1, 4), Constraint::Fill])
.split(area);
assert_eq!(rects[0].width, 25);
assert_eq!(rects[1].width, 75);
}
#[test]
fn ratio_zero_numerator_should_be_zero() {
let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Ratio(0, 1)]);
let rects = flex.split(Rect::new(0, 0, 100, 1));
assert_eq!(rects[0].width, 100, "Fill should take all space");
assert_eq!(rects[1].width, 0, "Ratio(0, 1) should be width 0");
}
#[test]
fn max_constraint_clamps_size() {
let flex = Flex::horizontal().constraints([Constraint::Max(20), Constraint::Fixed(30)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert!(rects[0].width <= 20);
assert_eq!(rects[1].width, 30);
}
#[test]
fn percentage_rounding_never_exceeds_available() {
let constraints = [
Constraint::Percentage(33.4),
Constraint::Percentage(33.3),
Constraint::Percentage(33.3),
];
let sizes = solve_constraints(&constraints, 7);
let total: u16 = sizes.iter().sum();
assert!(total <= 7, "percent rounding overflowed: {sizes:?}");
assert!(sizes.iter().all(|size| *size <= 7));
}
#[test]
fn tiny_area_saturates_fixed_and_min() {
let constraints = [Constraint::Fixed(5), Constraint::Min(3), Constraint::Max(2)];
let sizes = solve_constraints(&constraints, 2);
assert_eq!(sizes[0], 2);
assert_eq!(sizes[1], 0);
assert_eq!(sizes[2], 0);
assert_eq!(sizes.iter().sum::<u16>(), 2);
}
#[test]
fn ratio_distribution_sums_to_available() {
let constraints = [Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)];
let sizes = solve_constraints(&constraints, 5);
assert_eq!(sizes.iter().sum::<u16>(), 4);
assert_eq!(sizes[0], 1);
assert_eq!(sizes[1], 3);
}
#[test]
fn flex_gap_exceeds_area_yields_zero_widths() {
let flex = Flex::horizontal()
.gap(5)
.constraints([Constraint::Fixed(1), Constraint::Fixed(1)]);
let rects = flex.split(Rect::new(0, 0, 3, 1));
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].width, 0);
assert_eq!(rects[1].width, 0);
}
#[test]
fn alignment_space_around() {
let flex = Flex::horizontal()
.alignment(Alignment::SpaceAround)
.constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].x, 20);
assert_eq!(rects[1].x, 70);
}
#[test]
fn vertical_gap() {
let flex = Flex::vertical()
.gap(5)
.constraints([Constraint::Fixed(10), Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 50, 100));
assert_eq!(rects[0], Rect::new(0, 0, 50, 10));
assert_eq!(rects[1], Rect::new(0, 15, 50, 10));
}
#[test]
fn vertical_center() {
let flex = Flex::vertical()
.alignment(Alignment::Center)
.constraints([Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 50, 100));
assert_eq!(rects[0].y, 45);
assert_eq!(rects[0].height, 10);
}
#[test]
fn single_min_takes_all() {
let flex = Flex::horizontal().constraints([Constraint::Min(5)]);
let rects = flex.split(Rect::new(0, 0, 80, 24));
assert_eq!(rects[0].width, 80);
}
#[test]
fn fixed_exceeds_available_clamped() {
let flex = Flex::horizontal().constraints([Constraint::Fixed(60), Constraint::Fixed(60)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].width, 60);
assert_eq!(rects[1].width, 40);
}
#[test]
fn percentage_overflow_clamped() {
let flex = Flex::horizontal()
.constraints([Constraint::Percentage(80.0), Constraint::Percentage(80.0)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].width, 80);
assert_eq!(rects[1].width, 20); }
#[test]
fn margin_reduces_split_area() {
let flex = Flex::horizontal()
.margin(Sides::all(10))
.constraints([Constraint::Fixed(20), Constraint::Min(0)]);
let rects = flex.split(Rect::new(0, 0, 100, 100));
assert_eq!(rects[0].x, 10);
assert_eq!(rects[0].y, 10);
assert_eq!(rects[0].width, 20);
assert_eq!(rects[0].height, 80);
}
#[test]
fn builder_methods_chain() {
let flex = Flex::vertical()
.direction(Direction::Horizontal)
.gap(3)
.margin(Sides::all(1))
.alignment(Alignment::End)
.constraints([Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 50, 50));
assert_eq!(rects.len(), 1);
}
#[test]
fn space_between_single_item() {
let flex = Flex::horizontal()
.alignment(Alignment::SpaceBetween)
.constraints([Constraint::Fixed(10)]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].x, 0);
assert_eq!(rects[0].width, 10);
}
#[test]
fn invariant_rects_within_bounds() {
let area = Rect::new(10, 20, 80, 60);
let flex = Flex::horizontal()
.margin(Sides::all(5))
.gap(2)
.constraints([
Constraint::Fixed(15),
Constraint::Percentage(30.0),
Constraint::Min(10),
]);
let rects = flex.split(area);
let inner = area.inner(Sides::all(5));
for rect in &rects {
assert!(
rect.x >= inner.x && rect.right() <= inner.right(),
"Rect {:?} exceeds horizontal bounds of {:?}",
rect,
inner
);
assert!(
rect.y >= inner.y && rect.bottom() <= inner.bottom(),
"Rect {:?} exceeds vertical bounds of {:?}",
rect,
inner
);
}
}
#[test]
fn fill_takes_remaining_space() {
let flex = Flex::horizontal().constraints([Constraint::Fixed(20), Constraint::Fill]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].width, 20);
assert_eq!(rects[1].width, 80); }
#[test]
fn multiple_fills_share_space() {
let flex = Flex::horizontal().constraints([Constraint::Fill, Constraint::Fill]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].width, 50);
assert_eq!(rects[1].width, 50);
}
#[test]
fn fit_content_uses_preferred_size() {
let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
if idx == 0 {
LayoutSizeHint {
min: 5,
preferred: 30,
max: None,
}
} else {
LayoutSizeHint::ZERO
}
});
assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70); }
#[test]
fn fit_content_clamps_to_available() {
let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::FitContent]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
min: 5,
preferred: 80,
max: None,
});
assert_eq!(rects[0].width, 80);
assert_eq!(rects[1].width, 20);
}
#[test]
fn fit_content_without_measurer_gets_zero() {
let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].width, 0); assert_eq!(rects[1].width, 100); }
#[test]
fn fit_content_zero_area_returns_empty_rects() {
let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 0, 0), |_, _| LayoutSizeHint {
min: 5,
preferred: 10,
max: None,
});
assert_eq!(rects.len(), 2);
assert_eq!(rects[0].width, 0);
assert_eq!(rects[0].height, 0);
assert_eq!(rects[1].width, 0);
assert_eq!(rects[1].height, 0);
}
#[test]
fn fit_content_tiny_available_clamps_to_remaining() {
let flex = Flex::horizontal().constraints([Constraint::FitContent, Constraint::Fill]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 1, 1), |_, _| LayoutSizeHint {
min: 5,
preferred: 10,
max: None,
});
assert_eq!(rects[0].width, 1);
assert_eq!(rects[1].width, 0);
}
#[test]
fn fit_content_bounded_clamps_to_min() {
let flex = Flex::horizontal().constraints([
Constraint::FitContentBounded { min: 20, max: 50 },
Constraint::Fill,
]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
min: 5,
preferred: 10, max: None,
});
assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 80);
}
#[test]
fn fit_content_bounded_respects_small_available() {
let flex = Flex::horizontal().constraints([
Constraint::FitContentBounded { min: 20, max: 50 },
Constraint::Fill,
]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 5, 2), |_, _| LayoutSizeHint {
min: 5,
preferred: 10,
max: None,
});
assert_eq!(rects[0].width, 5);
assert_eq!(rects[1].width, 0);
}
#[test]
fn fit_content_bounded_clamps_to_max() {
let flex = Flex::horizontal().constraints([
Constraint::FitContentBounded { min: 10, max: 30 },
Constraint::Fill,
]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
min: 5,
preferred: 50, max: None,
});
assert_eq!(rects[0].width, 30); assert_eq!(rects[1].width, 70);
}
#[test]
fn fit_content_bounded_uses_preferred_when_in_range() {
let flex = Flex::horizontal().constraints([
Constraint::FitContentBounded { min: 10, max: 50 },
Constraint::Fill,
]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |_, _| LayoutSizeHint {
min: 5,
preferred: 35, max: None,
});
assert_eq!(rects[0].width, 35);
assert_eq!(rects[1].width, 65);
}
#[test]
fn fit_min_uses_minimum_size() {
let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
if idx == 0 {
LayoutSizeHint {
min: 15,
preferred: 40,
max: None,
}
} else {
LayoutSizeHint::ZERO
}
});
assert_eq!(rects[0].width, 15, "FitMin should strict size to min");
assert_eq!(rects[1].width, 85, "Fill should take remaining space");
}
#[test]
fn fit_min_without_measurer_gets_zero() {
let flex = Flex::horizontal().constraints([Constraint::FitMin, Constraint::Fill]);
let rects = flex.split(Rect::new(0, 0, 100, 10));
assert_eq!(rects[0].width, 0);
assert_eq!(rects[1].width, 100);
}
#[test]
fn layout_size_hint_zero_is_default() {
assert_eq!(LayoutSizeHint::default(), LayoutSizeHint::ZERO);
}
#[test]
fn layout_size_hint_exact() {
let h = LayoutSizeHint::exact(25);
assert_eq!(h.min, 25);
assert_eq!(h.preferred, 25);
assert_eq!(h.max, Some(25));
}
#[test]
fn layout_size_hint_at_least() {
let h = LayoutSizeHint::at_least(10, 30);
assert_eq!(h.min, 10);
assert_eq!(h.preferred, 30);
assert_eq!(h.max, None);
}
#[test]
fn layout_size_hint_clamp() {
let h = LayoutSizeHint {
min: 10,
preferred: 20,
max: Some(30),
};
assert_eq!(h.clamp(5), 10); assert_eq!(h.clamp(15), 15); assert_eq!(h.clamp(50), 30); }
#[test]
fn layout_size_hint_clamp_unbounded() {
let h = LayoutSizeHint::at_least(5, 10);
assert_eq!(h.clamp(3), 5); assert_eq!(h.clamp(1000), 1000); }
#[test]
fn layout_size_hint_clamp_min_greater_than_max() {
let h = LayoutSizeHint {
min: 20,
preferred: 20,
max: Some(10),
};
assert_eq!(h.clamp(5), 20); assert_eq!(h.clamp(15), 20); assert_eq!(h.clamp(25), 20); }
#[test]
fn fit_content_with_fixed_and_fill() {
let flex = Flex::horizontal().constraints([
Constraint::Fixed(20),
Constraint::FitContent,
Constraint::Fill,
]);
let rects = flex.split_with_measurer(Rect::new(0, 0, 100, 10), |idx, _| {
if idx == 1 {
LayoutSizeHint {
min: 5,
preferred: 25,
max: None,
}
} else {
LayoutSizeHint::ZERO
}
});
assert_eq!(rects[0].width, 20); assert_eq!(rects[1].width, 25); assert_eq!(rects[2].width, 55); }
#[test]
fn total_allocation_never_exceeds_available_with_fit_content() {
for available in [10u16, 50, 100, 255] {
let flex = Flex::horizontal().constraints([
Constraint::FitContent,
Constraint::FitContent,
Constraint::Fill,
]);
let rects =
flex.split_with_measurer(Rect::new(0, 0, available, 10), |_, _| LayoutSizeHint {
min: 10,
preferred: 40,
max: None,
});
let total: u16 = rects.iter().map(|r| r.width).sum();
assert!(
total <= available,
"Total {} exceeded available {} with FitContent",
total,
available
);
}
}
mod rounding_tests {
use super::super::*;
#[test]
fn rounding_conserves_sum_exact() {
let result = round_layout_stable(&[10.0, 20.0, 10.0], 40, None);
assert_eq!(result.iter().copied().sum::<u16>(), 40);
assert_eq!(result.as_slice(), &[10u16, 20u16, 10u16]);
}
#[test]
fn rounding_conserves_sum_fractional() {
let result = round_layout_stable(&[10.4, 20.6, 9.0], 40, None);
assert_eq!(
result.iter().copied().sum::<u16>(),
40,
"Sum must equal total: {:?}",
result
);
}
#[test]
fn rounding_conserves_sum_many_fractions() {
let targets = vec![20.2, 20.2, 20.2, 20.2, 19.2];
let result = round_layout_stable(&targets, 100, None);
assert_eq!(
result.iter().copied().sum::<u16>(),
100,
"Sum must be exactly 100: {:?}",
result
);
}
#[test]
fn rounding_conserves_sum_all_half() {
let targets = vec![10.5, 10.5, 10.5, 10.5];
let result = round_layout_stable(&targets, 42, None);
assert_eq!(
result.iter().copied().sum::<u16>(),
42,
"Sum must be exactly 42: {:?}",
result
);
}
#[test]
fn rounding_displacement_bounded() {
let targets = vec![33.33, 33.33, 33.34];
let result = round_layout_stable(&targets, 100, None);
assert_eq!(result.iter().copied().sum::<u16>(), 100);
for (i, (&x, &r)) in result.iter().zip(targets.iter()).enumerate() {
let floor = r.floor() as u16;
let ceil = floor + 1;
assert!(
x == floor || x == ceil,
"Element {} = {} not in {{floor={}, ceil={}}} of target {}",
i,
x,
floor,
ceil,
r
);
}
}
#[test]
fn temporal_tiebreak_stable_when_unchanged() {
let targets = vec![10.5, 10.5, 10.5, 10.5];
let first = round_layout_stable(&targets, 42, None);
let second = round_layout_stable(&targets, 42, Some(first.clone()));
assert_eq!(
first, second,
"Identical targets should produce identical results"
);
}
#[test]
fn temporal_tiebreak_prefers_previous_direction() {
let targets = vec![10.5, 10.5];
let total = 21;
let first = round_layout_stable(&targets, total, None);
assert_eq!(first.iter().copied().sum::<u16>(), total);
let second = round_layout_stable(&targets, total, Some(first.clone()));
assert_eq!(first, second, "Should maintain rounding direction");
}
#[test]
fn temporal_tiebreak_adapts_to_changed_targets() {
let targets_a = vec![10.5, 10.5];
let result_a = round_layout_stable(&targets_a, 21, None);
let targets_b = vec![15.7, 5.3];
let result_b = round_layout_stable(&targets_b, 21, Some(result_a));
assert_eq!(result_b.iter().copied().sum::<u16>(), 21);
assert!(result_b[0] > result_b[1], "Should follow larger target");
}
#[test]
fn property_min_displacement_brute_force_small() {
let targets = vec![3.3, 3.3, 3.4];
let total: u16 = 10;
let result = round_layout_stable(&targets, total, None);
let our_displacement: f64 = result
.iter()
.zip(targets.iter())
.map(|(&x, &r)| (x as f64 - r).abs())
.sum();
let mut min_displacement = f64::MAX;
let floors: Vec<u16> = targets.iter().map(|&r| r.floor() as u16).collect();
let ceils: Vec<u16> = targets.iter().map(|&r| r.floor() as u16 + 1).collect();
for a in floors[0]..=ceils[0] {
for b in floors[1]..=ceils[1] {
for c in floors[2]..=ceils[2] {
if a + b + c == total {
let disp = (a as f64 - targets[0]).abs()
+ (b as f64 - targets[1]).abs()
+ (c as f64 - targets[2]).abs();
if disp < min_displacement {
min_displacement = disp;
}
}
}
}
}
assert!(
(our_displacement - min_displacement).abs() < 1e-10,
"Our displacement {} should match optimal {}: {:?}",
our_displacement,
min_displacement,
result
);
}
#[test]
fn rounding_deterministic() {
let targets = vec![7.7, 8.3, 14.0];
let a = round_layout_stable(&targets, 30, None);
let b = round_layout_stable(&targets, 30, None);
assert_eq!(a, b, "Same inputs must produce identical outputs");
}
#[test]
fn rounding_empty_targets() {
let result = round_layout_stable(&[], 0, None);
assert!(result.is_empty());
}
#[test]
fn rounding_single_element() {
let result = round_layout_stable(&[10.7], 11, None);
assert_eq!(result.as_slice(), &[11u16]);
}
#[test]
fn rounding_zero_total() {
let result = round_layout_stable(&[5.0, 5.0], 0, None);
assert_eq!(result.iter().copied().sum::<u16>(), 0);
}
#[test]
fn rounding_zero_total_with_large_overflow_reaches_zero() {
let result = round_layout_stable(&[65535.0, 65535.0], 0, None);
assert_eq!(result.as_slice(), &[0u16, 0u16]);
assert_eq!(result.iter().copied().sum::<u16>(), 0);
}
#[test]
fn rounding_all_zeros() {
let result = round_layout_stable(&[0.0, 0.0, 0.0], 0, None);
assert_eq!(result.as_slice(), &[0u16, 0u16, 0u16]);
}
#[test]
fn rounding_integer_targets() {
let result = round_layout_stable(&[10.0, 20.0, 30.0], 60, None);
assert_eq!(result.as_slice(), &[10u16, 20u16, 30u16]);
}
#[test]
fn rounding_large_deficit() {
let result = round_layout_stable(&[0.9, 0.9, 0.9], 3, None);
assert_eq!(result.iter().copied().sum::<u16>(), 3);
assert_eq!(result.as_slice(), &[1u16, 1u16, 1u16]);
}
#[test]
fn rounding_with_prev_different_length() {
let result = round_layout_stable(
&[10.5, 10.5],
21,
Some(smallvec::smallvec![11u16, 10u16, 5u16]),
);
assert_eq!(result.iter().copied().sum::<u16>(), 21);
}
#[test]
fn rounding_very_small_fractions() {
let targets = vec![10.001, 20.001, 9.998];
let result = round_layout_stable(&targets, 40, None);
assert_eq!(result.iter().copied().sum::<u16>(), 40);
}
#[test]
fn rounding_conserves_sum_stress() {
let n = 50;
let targets: Vec<f64> = (0..n).map(|i| 2.0 + (i as f64 * 0.037)).collect();
let total = 120u16;
let result = round_layout_stable(&targets, total, None);
assert_eq!(
result.iter().copied().sum::<u16>(),
total,
"Sum must be exactly {} for {} items: {:?}",
total,
n,
result
);
}
}
mod property_constraint_tests {
use super::super::*;
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u32(&mut self) -> u32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(self.0 >> 33) as u32
}
fn next_u16_range(&mut self, lo: u16, hi: u16) -> u16 {
if lo >= hi {
return lo;
}
lo + (self.next_u32() % (hi - lo) as u32) as u16
}
fn next_f32(&mut self) -> f32 {
(self.next_u32() & 0x00FF_FFFF) as f32 / 16_777_216.0
}
}
fn random_constraint(rng: &mut Lcg) -> Constraint {
match rng.next_u32() % 7 {
0 => Constraint::Fixed(rng.next_u16_range(1, 80)),
1 => Constraint::Percentage(rng.next_f32() * 100.0),
2 => Constraint::Min(rng.next_u16_range(0, 40)),
3 => Constraint::Max(rng.next_u16_range(5, 120)),
4 => {
let n = rng.next_u32() % 5 + 1;
let d = rng.next_u32() % 5 + 1;
Constraint::Ratio(n, d)
}
5 => Constraint::Fill,
_ => Constraint::FitContent,
}
}
#[test]
fn property_constraints_respected_fixed() {
let mut rng = Lcg::new(0xDEAD_BEEF);
for _ in 0..200 {
let fixed_val = rng.next_u16_range(1, 60);
let avail = rng.next_u16_range(10, 200);
let flex = Flex::horizontal().constraints([Constraint::Fixed(fixed_val)]);
let rects = flex.split(Rect::new(0, 0, avail, 10));
assert!(
rects[0].width <= fixed_val.min(avail),
"Fixed({}) in avail {} -> width {}",
fixed_val,
avail,
rects[0].width
);
}
}
#[test]
fn property_constraints_respected_max() {
let mut rng = Lcg::new(0xCAFE_BABE);
for _ in 0..200 {
let max_val = rng.next_u16_range(5, 80);
let avail = rng.next_u16_range(10, 200);
let flex =
Flex::horizontal().constraints([Constraint::Max(max_val), Constraint::Fill]);
let rects = flex.split(Rect::new(0, 0, avail, 10));
assert!(
rects[0].width <= max_val,
"Max({}) in avail {} -> width {}",
max_val,
avail,
rects[0].width
);
}
}
#[test]
fn property_constraints_respected_min() {
let mut rng = Lcg::new(0xBAAD_F00D);
for _ in 0..200 {
let min_val = rng.next_u16_range(0, 40);
let avail = rng.next_u16_range(min_val.max(1), 200);
let flex = Flex::horizontal().constraints([Constraint::Min(min_val)]);
let rects = flex.split(Rect::new(0, 0, avail, 10));
assert!(
rects[0].width >= min_val,
"Min({}) in avail {} -> width {}",
min_val,
avail,
rects[0].width
);
}
}
#[test]
fn property_constraints_respected_ratio_proportional() {
let mut rng = Lcg::new(0x1234_5678);
for _ in 0..200 {
let n1 = rng.next_u32() % 5 + 1;
let n2 = rng.next_u32() % 5 + 1;
let d = n1 + n2;
let avail = rng.next_u16_range(20, 200);
let flex = Flex::horizontal()
.constraints([Constraint::Ratio(n1, d), Constraint::Ratio(n2, d)]);
let rects = flex.split(Rect::new(0, 0, avail, 10));
let w1 = rects[0].width as f64;
let w2 = rects[1].width as f64;
let total = w1 + w2;
if total > 0.0 {
let expected_ratio = n1 as f64 / d as f64;
let actual_ratio = w1 / total;
assert!(
(actual_ratio - expected_ratio).abs() < 0.15 || total < 4.0,
"Ratio({},{})/({}+{}) avail={}: ~{:.2} got {:.2} (w1={}, w2={})",
n1,
d,
n1,
n2,
avail,
expected_ratio,
actual_ratio,
w1,
w2
);
}
}
}
#[test]
fn property_total_allocation_never_exceeds_available() {
let mut rng = Lcg::new(0xFACE_FEED);
for _ in 0..500 {
let n = (rng.next_u32() % 6 + 1) as usize;
let constraints: Vec<Constraint> =
(0..n).map(|_| random_constraint(&mut rng)).collect();
let avail = rng.next_u16_range(5, 200);
let dir = if rng.next_u32().is_multiple_of(2) {
Direction::Horizontal
} else {
Direction::Vertical
};
let flex = Flex::default().direction(dir).constraints(constraints);
let area = Rect::new(0, 0, avail, avail);
let rects = flex.split(area);
let total: u16 = rects
.iter()
.map(|r| match dir {
Direction::Horizontal => r.width,
Direction::Vertical => r.height,
})
.sum();
assert!(
total <= avail,
"Total {} exceeded available {} with {} constraints",
total,
avail,
n
);
}
}
#[test]
fn property_no_overlap_horizontal() {
let mut rng = Lcg::new(0xABCD_1234);
for _ in 0..300 {
let n = (rng.next_u32() % 5 + 2) as usize;
let constraints: Vec<Constraint> =
(0..n).map(|_| random_constraint(&mut rng)).collect();
let avail = rng.next_u16_range(20, 200);
let flex = Flex::horizontal().constraints(constraints);
let rects = flex.split(Rect::new(0, 0, avail, 10));
for i in 1..rects.len() {
let prev_end = rects[i - 1].x + rects[i - 1].width;
assert!(
rects[i].x >= prev_end,
"Overlap at {}: prev ends {}, next starts {}",
i,
prev_end,
rects[i].x
);
}
}
}
#[test]
fn property_deterministic_across_runs() {
let mut rng = Lcg::new(0x9999_8888);
for _ in 0..100 {
let n = (rng.next_u32() % 5 + 1) as usize;
let constraints: Vec<Constraint> =
(0..n).map(|_| random_constraint(&mut rng)).collect();
let avail = rng.next_u16_range(10, 200);
let r1 = Flex::horizontal()
.constraints(constraints.clone())
.split(Rect::new(0, 0, avail, 10));
let r2 = Flex::horizontal()
.constraints(constraints)
.split(Rect::new(0, 0, avail, 10));
assert_eq!(r1, r2, "Determinism violation at avail={}", avail);
}
}
}
mod property_temporal_tests {
use super::super::*;
use crate::cache::{CoherenceCache, CoherenceId};
struct Lcg(u64);
impl Lcg {
fn new(seed: u64) -> Self {
Self(seed)
}
fn next_u32(&mut self) -> u32 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(self.0 >> 33) as u32
}
}
#[test]
fn property_temporal_stability_small_resize() {
let constraints = [
Constraint::Percentage(33.3),
Constraint::Percentage(33.3),
Constraint::Fill,
];
let mut coherence = CoherenceCache::new(64);
let id = CoherenceId::new(&constraints, Direction::Horizontal);
for total in [80u16, 100, 120] {
let flex = Flex::horizontal().constraints(constraints);
let rects = flex.split(Rect::new(0, 0, total, 10));
let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
let prev = coherence.get(&id);
let rounded = round_layout_stable(&targets, total, prev);
if let Some(old) = coherence.get(&id) {
let (sum_disp, max_disp) = coherence.displacement(&id, &rounded);
assert!(
max_disp <= total.abs_diff(old.iter().copied().sum()) as u32 + 1,
"max_disp={} too large for size change {} -> {}",
max_disp,
old.iter().copied().sum::<u16>(),
total
);
let _ = sum_disp;
}
coherence.store(id, rounded);
}
}
#[test]
fn property_temporal_stability_random_walk() {
let constraints = [
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
];
let id = CoherenceId::new(&constraints, Direction::Horizontal);
let mut coherence = CoherenceCache::new(64);
let mut rng = Lcg::new(0x5555_AAAA);
let mut total: u16 = 90;
for step in 0..200 {
let prev_total = total;
let delta = (rng.next_u32() % 7) as i32 - 3;
total = (total as i32 + delta).clamp(10, 250) as u16;
let flex = Flex::horizontal().constraints(constraints);
let rects = flex.split(Rect::new(0, 0, total, 10));
let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
let prev = coherence.get(&id);
let rounded = round_layout_stable(&targets, total, prev);
if coherence.get(&id).is_some() {
let (_, max_disp) = coherence.displacement(&id, &rounded);
let size_change = total.abs_diff(prev_total);
assert!(
max_disp <= size_change as u32 + 2,
"step {}: max_disp={} exceeds size_change={} + 2",
step,
max_disp,
size_change
);
}
coherence.store(id, rounded);
}
}
#[test]
fn property_temporal_stability_identical_frames() {
let constraints = [
Constraint::Fixed(20),
Constraint::Fill,
Constraint::Fixed(15),
];
let id = CoherenceId::new(&constraints, Direction::Horizontal);
let mut coherence = CoherenceCache::new(64);
let flex = Flex::horizontal().constraints(constraints);
let rects = flex.split(Rect::new(0, 0, 100, 10));
let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
coherence.store(id, widths.iter().copied().collect());
for _ in 0..10 {
let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
let prev = coherence.get(&id);
let rounded = round_layout_stable(&targets, 100, prev);
let (sum_disp, _) = coherence.displacement(&id, &rounded);
assert_eq!(sum_disp, 0, "Identical frames: zero displacement");
coherence.store(id, rounded);
}
}
#[test]
fn property_temporal_coherence_sweep() {
let constraints = [
Constraint::Percentage(25.0),
Constraint::Percentage(50.0),
Constraint::Fill,
];
let id = CoherenceId::new(&constraints, Direction::Horizontal);
let mut coherence = CoherenceCache::new(64);
let mut total_displacement: u64 = 0;
for total in 60u16..=140 {
let flex = Flex::horizontal().constraints(constraints);
let rects = flex.split(Rect::new(0, 0, total, 10));
let widths: Vec<u16> = rects.iter().map(|r| r.width).collect();
let targets: Vec<f64> = widths.iter().map(|&w| w as f64).collect();
let prev = coherence.get(&id);
let rounded = round_layout_stable(&targets, total, prev);
if coherence.get(&id).is_some() {
let (sum_disp, _) = coherence.displacement(&id, &rounded);
total_displacement += sum_disp;
}
coherence.store(id, rounded);
}
assert!(
total_displacement <= 80 * 3,
"Total displacement {} exceeds bound for 80-step sweep",
total_displacement
);
}
}
mod snapshot_layout_tests {
use super::super::*;
use crate::grid::{Grid, GridArea};
fn snapshot_flex(
constraints: &[Constraint],
dir: Direction,
width: u16,
height: u16,
) -> String {
let flex = Flex::default()
.direction(dir)
.constraints(constraints.iter().copied());
let rects = flex.split(Rect::new(0, 0, width, height));
let mut out = format!(
"Flex {:?} {}x{} ({} constraints)\n",
dir,
width,
height,
constraints.len()
);
for (i, r) in rects.iter().enumerate() {
out.push_str(&format!(
" [{}] x={} y={} w={} h={}\n",
i, r.x, r.y, r.width, r.height
));
}
let total: u16 = rects
.iter()
.map(|r| match dir {
Direction::Horizontal => r.width,
Direction::Vertical => r.height,
})
.sum();
out.push_str(&format!(" total={}\n", total));
out
}
fn snapshot_grid(
rows: &[Constraint],
cols: &[Constraint],
areas: &[(&str, GridArea)],
width: u16,
height: u16,
) -> String {
let mut grid = Grid::new()
.rows(rows.iter().copied())
.columns(cols.iter().copied());
for &(name, area) in areas {
grid = grid.area(name, area);
}
let layout = grid.split(Rect::new(0, 0, width, height));
let mut out = format!(
"Grid {}x{} ({}r x {}c)\n",
width,
height,
rows.len(),
cols.len()
);
for r in 0..rows.len() {
for c in 0..cols.len() {
let rect = layout.cell(r, c);
out.push_str(&format!(
" [{},{}] x={} y={} w={} h={}\n",
r, c, rect.x, rect.y, rect.width, rect.height
));
}
}
for &(name, _) in areas {
if let Some(rect) = layout.area(name) {
out.push_str(&format!(
" area({}) x={} y={} w={} h={}\n",
name, rect.x, rect.y, rect.width, rect.height
));
}
}
out
}
#[test]
fn snapshot_flex_thirds_80x24() {
let snap = snapshot_flex(
&[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
],
Direction::Horizontal,
80,
24,
);
assert_eq!(
snap,
"\
Flex Horizontal 80x24 (3 constraints)
[0] x=0 y=0 w=26 h=24
[1] x=26 y=0 w=26 h=24
[2] x=52 y=0 w=26 h=24
total=78
"
);
}
#[test]
fn snapshot_flex_sidebar_content_80x24() {
let snap = snapshot_flex(
&[Constraint::Fixed(20), Constraint::Fill],
Direction::Horizontal,
80,
24,
);
assert_eq!(
snap,
"\
Flex Horizontal 80x24 (2 constraints)
[0] x=0 y=0 w=20 h=24
[1] x=20 y=0 w=60 h=24
total=80
"
);
}
#[test]
fn snapshot_flex_header_body_footer_80x24() {
let snap = snapshot_flex(
&[Constraint::Fixed(3), Constraint::Fill, Constraint::Fixed(1)],
Direction::Vertical,
80,
24,
);
assert_eq!(
snap,
"\
Flex Vertical 80x24 (3 constraints)
[0] x=0 y=0 w=80 h=3
[1] x=0 y=3 w=80 h=20
[2] x=0 y=23 w=80 h=1
total=24
"
);
}
#[test]
fn snapshot_flex_thirds_120x40() {
let snap = snapshot_flex(
&[
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
Constraint::Ratio(1, 3),
],
Direction::Horizontal,
120,
40,
);
assert_eq!(
snap,
"\
Flex Horizontal 120x40 (3 constraints)
[0] x=0 y=0 w=40 h=40
[1] x=40 y=0 w=40 h=40
[2] x=80 y=0 w=40 h=40
total=120
"
);
}
#[test]
fn snapshot_flex_sidebar_content_120x40() {
let snap = snapshot_flex(
&[Constraint::Fixed(20), Constraint::Fill],
Direction::Horizontal,
120,
40,
);
assert_eq!(
snap,
"\
Flex Horizontal 120x40 (2 constraints)
[0] x=0 y=0 w=20 h=40
[1] x=20 y=0 w=100 h=40
total=120
"
);
}
#[test]
fn snapshot_flex_percentage_mix_120x40() {
let snap = snapshot_flex(
&[
Constraint::Percentage(25.0),
Constraint::Percentage(50.0),
Constraint::Fill,
],
Direction::Horizontal,
120,
40,
);
assert_eq!(
snap,
"\
Flex Horizontal 120x40 (3 constraints)
[0] x=0 y=0 w=30 h=40
[1] x=30 y=0 w=60 h=40
[2] x=90 y=0 w=30 h=40
total=120
"
);
}
#[test]
fn snapshot_grid_2x2_80x24() {
let snap = snapshot_grid(
&[Constraint::Fixed(3), Constraint::Fill],
&[Constraint::Fixed(20), Constraint::Fill],
&[
("header", GridArea::span(0, 0, 1, 2)),
("sidebar", GridArea::span(1, 0, 1, 1)),
("content", GridArea::cell(1, 1)),
],
80,
24,
);
assert_eq!(
snap,
"\
Grid 80x24 (2r x 2c)
[0,0] x=0 y=0 w=20 h=3
[0,1] x=20 y=0 w=60 h=3
[1,0] x=0 y=3 w=20 h=21
[1,1] x=20 y=3 w=60 h=21
area(header) x=0 y=0 w=80 h=3
area(sidebar) x=0 y=3 w=20 h=21
area(content) x=20 y=3 w=60 h=21
"
);
}
#[test]
fn snapshot_grid_3x3_80x24() {
let snap = snapshot_grid(
&[Constraint::Fixed(1), Constraint::Fill, Constraint::Fixed(1)],
&[
Constraint::Fixed(10),
Constraint::Fill,
Constraint::Fixed(10),
],
&[],
80,
24,
);
assert_eq!(
snap,
"\
Grid 80x24 (3r x 3c)
[0,0] x=0 y=0 w=10 h=1
[0,1] x=10 y=0 w=60 h=1
[0,2] x=70 y=0 w=10 h=1
[1,0] x=0 y=1 w=10 h=22
[1,1] x=10 y=1 w=60 h=22
[1,2] x=70 y=1 w=10 h=22
[2,0] x=0 y=23 w=10 h=1
[2,1] x=10 y=23 w=60 h=1
[2,2] x=70 y=23 w=10 h=1
"
);
}
#[test]
fn snapshot_grid_2x2_120x40() {
let snap = snapshot_grid(
&[Constraint::Fixed(3), Constraint::Fill],
&[Constraint::Fixed(20), Constraint::Fill],
&[
("header", GridArea::span(0, 0, 1, 2)),
("sidebar", GridArea::span(1, 0, 1, 1)),
("content", GridArea::cell(1, 1)),
],
120,
40,
);
assert_eq!(
snap,
"\
Grid 120x40 (2r x 2c)
[0,0] x=0 y=0 w=20 h=3
[0,1] x=20 y=0 w=100 h=3
[1,0] x=0 y=3 w=20 h=37
[1,1] x=20 y=3 w=100 h=37
area(header) x=0 y=0 w=120 h=3
area(sidebar) x=0 y=3 w=20 h=37
area(content) x=20 y=3 w=100 h=37
"
);
}
#[test]
fn snapshot_grid_dashboard_120x40() {
let snap = snapshot_grid(
&[
Constraint::Fixed(3),
Constraint::Percentage(60.0),
Constraint::Fill,
],
&[Constraint::Percentage(30.0), Constraint::Fill],
&[
("nav", GridArea::span(0, 0, 1, 2)),
("chart", GridArea::cell(1, 0)),
("detail", GridArea::cell(1, 1)),
("log", GridArea::span(2, 0, 1, 2)),
],
120,
40,
);
assert_eq!(
snap,
"\
Grid 120x40 (3r x 2c)
[0,0] x=0 y=0 w=36 h=3
[0,1] x=36 y=0 w=84 h=3
[1,0] x=0 y=3 w=36 h=24
[1,1] x=36 y=3 w=84 h=24
[2,0] x=0 y=27 w=36 h=13
[2,1] x=36 y=27 w=84 h=13
area(nav) x=0 y=0 w=120 h=3
area(chart) x=0 y=3 w=36 h=24
area(detail) x=36 y=3 w=84 h=24
area(log) x=0 y=27 w=120 h=13
"
);
}
}
}