#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::math::Vec2;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn right(self) -> f32 {
self.x + self.width
}
pub fn bottom(self) -> f32 {
self.y + self.height
}
pub fn center(self) -> Vec2 {
Vec2::new(self.x + self.width * 0.5, self.y + self.height * 0.5)
}
pub fn translate(self, delta: Vec2) -> Self {
Self::new(self.x + delta.x, self.y + delta.y, self.width, self.height)
}
pub fn is_empty(self) -> bool {
self.width <= 0.0 || self.height <= 0.0
}
pub fn is_finite(self) -> bool {
self.x.is_finite()
&& self.y.is_finite()
&& self.width.is_finite()
&& self.height.is_finite()
&& self.right().is_finite()
&& self.bottom().is_finite()
}
pub fn inset(self, inset: Insets) -> Self {
if !self.is_finite() || !inset.is_finite() {
return Self::ZERO;
}
let x = self.x + inset.left;
let y = self.y + inset.top;
let width = (self.width - inset.left - inset.right).max(0.0);
let height = (self.height - inset.top - inset.bottom).max(0.0);
let rect = Self::new(x, y, width, height);
if rect.is_finite() {
rect
} else {
Self::ZERO
}
}
pub fn contains(self, x: f32, y: f32) -> bool {
self.is_finite()
&& !self.is_empty()
&& x.is_finite()
&& y.is_finite()
&& x >= self.x
&& x <= self.right()
&& y >= self.y
&& y <= self.bottom()
}
pub fn intersection(self, other: Self) -> Option<Self> {
if !self.is_finite() || !other.is_finite() {
return None;
}
let x = self.x.max(other.x);
let y = self.y.max(other.y);
let right = self.right().min(other.right());
let bottom = self.bottom().min(other.bottom());
let width = right - x;
let height = bottom - y;
(width > 0.0 && height > 0.0).then(|| Self::new(x, y, width, height))
}
pub fn union(self, other: Self) -> Self {
if !self.is_finite() {
return if other.is_finite() { other } else { Self::ZERO };
}
if !other.is_finite() {
return self;
}
if self.is_empty() {
return other;
}
if other.is_empty() {
return self;
}
let x = self.x.min(other.x);
let y = self.y.min(other.y);
let right = self.right().max(other.right());
let bottom = self.bottom().max(other.bottom());
let rect = Self::new(x, y, right - x, bottom - y);
if rect.is_finite() {
rect
} else {
Self::ZERO
}
}
pub fn clamp_within(self, bounds: Self) -> Self {
if !self.is_finite() || !bounds.is_finite() || self.is_empty() || bounds.is_empty() {
return Self::ZERO;
}
let width = self.width.min(bounds.width).max(0.0);
let height = self.height.min(bounds.height).max(0.0);
let max_x = bounds.x + (bounds.width - width).max(0.0);
let max_y = bounds.y + (bounds.height - height).max(0.0);
let rect = Self::new(
self.x.clamp(bounds.x, max_x),
self.y.clamp(bounds.y, max_y),
width,
height,
);
if rect.is_finite() {
rect
} else {
Self::ZERO
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Insets {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Insets {
pub const fn all(value: f32) -> Self {
Self {
top: value,
right: value,
bottom: value,
left: value,
}
}
pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
Self {
top: vertical,
right: horizontal,
bottom: vertical,
left: horizontal,
}
}
pub fn is_finite(self) -> bool {
self.top.is_finite()
&& self.right.is_finite()
&& self.bottom.is_finite()
&& self.left.is_finite()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Size {
pub const ZERO: Self = Self::new(0.0, 0.0);
pub const INFINITE: Self = Self::new(f32::INFINITY, f32::INFINITY);
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
pub fn is_finite(self) -> bool {
self.width.is_finite() && self.height.is_finite()
}
pub fn sanitized(self) -> Self {
Self::new(finite_px(self.width, 0.0), finite_px(self.height, 0.0))
}
pub fn clamp(self, min: Self, max: Self) -> Self {
let min = min.sanitized();
let max = max.sanitized_or_infinite();
Self::new(
finite_px(self.width, 0.0).clamp(min.width.min(max.width), max.width.max(min.width)),
finite_px(self.height, 0.0)
.clamp(min.height.min(max.height), max.height.max(min.height)),
)
}
fn sanitized_or_infinite(self) -> Self {
Self::new(
if self.width.is_finite() {
self.width.max(0.0)
} else {
f32::INFINITY
},
if self.height.is_finite() {
self.height.max(0.0)
} else {
f32::INFINITY
},
)
}
}
impl Default for Size {
fn default() -> Self {
Self::ZERO
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum FluidConstraint {
Min(f32),
Max,
}
impl FluidConstraint {
fn stack(self, other: Self) -> Self {
match (self, other) {
(Self::Min(left), Self::Min(right)) => {
Self::Min(finite_px(left, 0.0) + finite_px(right, 0.0))
}
(Self::Min(min), Self::Max) | (Self::Max, Self::Min(min)) => {
Self::Min(finite_px(min, 0.0))
}
(Self::Max, Self::Max) => Self::Max,
}
}
fn cross(self, other: Self) -> Self {
match (self, other) {
(Self::Min(left), Self::Min(right)) => {
Self::Min(finite_px(left, 0.0).max(finite_px(right, 0.0)))
}
(Self::Min(min), Self::Max) | (Self::Max, Self::Min(min)) => {
Self::Min(finite_px(min, 0.0))
}
(Self::Max, Self::Max) => Self::Max,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum LengthBounds {
Min(f32),
Max(f32),
Both { min: f32, max: f32 },
}
impl LengthBounds {
pub fn min(self, min: f32) -> Self {
let min = finite_px(min, 0.0);
match self {
Self::Min(_) => Self::Min(min),
Self::Max(max) | Self::Both { max, .. } => Self::Both {
min,
max: finite_px(max, min).max(min),
},
}
}
pub fn max(self, max: f32) -> Self {
let max = finite_px(max, 0.0);
match self {
Self::Max(_) => Self::Max(max),
Self::Min(min) | Self::Both { min, .. } => Self::Both {
min: finite_px(min, 0.0).min(max),
max,
},
}
}
fn constraint(self) -> FluidConstraint {
match self {
Self::Min(min) | Self::Both { min, .. } => FluidConstraint::Min(finite_px(min, 0.0)),
Self::Max(_) => FluidConstraint::Max,
}
}
fn apply(self, min_value: &mut f32, max_value: &mut f32) {
match self {
Self::Min(min) => *min_value = finite_px(min, 0.0).min(*max_value).max(*min_value),
Self::Max(max) => *max_value = finite_px(max, 0.0).min(*max_value).max(*min_value),
Self::Both { min, max } => {
*min_value = finite_px(min, 0.0).min(*max_value).max(*min_value);
*max_value = finite_px(max, *min_value).min(*max_value).max(*min_value);
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum LengthSizing {
Fit,
Fill(u16),
Shrink,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum Length {
Fit,
Fill,
FillPortion(u16),
Shrink,
Fixed(f32),
Bounded {
bounds: LengthBounds,
sizing: LengthSizing,
},
Fluid(FluidConstraint),
}
impl Length {
pub const fn fixed(px: f32) -> Self {
Self::Fixed(px)
}
pub const fn fill_portion(portion: u16) -> Self {
Self::FillPortion(portion)
}
pub fn min(self, min: f32) -> Self {
let min = finite_px(min, 0.0);
match self {
Self::Fixed(_) => self,
Self::Bounded { bounds, sizing } => Self::Bounded {
bounds: bounds.min(min),
sizing,
},
length => Self::Bounded {
bounds: LengthBounds::Min(min),
sizing: length.sizing(),
},
}
}
pub fn max(self, max: f32) -> Self {
let max = finite_px(max, 0.0);
match self {
Self::Fixed(_) => self,
Self::Bounded { bounds, sizing } => Self::Bounded {
bounds: bounds.max(max),
sizing,
},
length => Self::Bounded {
bounds: LengthBounds::Max(max),
sizing: length.sizing(),
},
}
}
pub fn fill_factor(self) -> u16 {
match self {
Self::Fill | Self::Fluid(_) => 1,
Self::FillPortion(portion) => portion.max(1),
Self::Bounded {
sizing: LengthSizing::Fill(portion),
..
} => portion.max(1),
Self::Fit | Self::Shrink | Self::Fixed(_) | Self::Bounded { .. } => 0,
}
}
pub fn is_fill(self) -> bool {
self.fill_factor() != 0
}
pub fn is_fit(self) -> bool {
matches!(self, Self::Fit)
}
pub fn stack(self, other: Self) -> Self {
self.merge_with(other, FluidConstraint::stack)
}
pub fn cross(self, other: Self) -> Self {
self.merge_with(other, FluidConstraint::cross)
}
fn sizing(self) -> LengthSizing {
match self {
Self::Fill => LengthSizing::Fill(1),
Self::FillPortion(portion) => LengthSizing::Fill(portion.max(1)),
Self::Shrink => LengthSizing::Shrink,
_ => LengthSizing::Fit,
}
}
fn merge_with(
self,
other: Self,
merge: impl Fn(FluidConstraint, FluidConstraint) -> FluidConstraint,
) -> Self {
match (self, other) {
(Self::Shrink | Self::Fixed(_) | Self::Fill | Self::FillPortion(_), _) => self,
(Self::Fluid(left), Self::Fluid(right)) => Self::Fluid(merge(left, right)),
(
Self::Fluid(left),
Self::Bounded {
bounds,
sizing: LengthSizing::Fill(_),
},
) => Self::Fluid(merge(left, bounds.constraint())),
(Self::Fluid(constraint), _) => Self::Fluid(constraint),
(Self::Bounded { bounds, sizing }, _) => Self::Bounded { bounds, sizing },
(
_,
Self::Bounded {
bounds,
sizing: LengthSizing::Fill(_),
},
) => Self::Fluid(bounds.constraint()),
(_, Self::Fluid(constraint)) => Self::Fluid(constraint),
(_, Self::Fill | Self::FillPortion(_)) => Self::Fill,
_ => Self::Fit,
}
}
fn resolve_axis(self, min: f32, max: f32, intrinsic: f32, compressed: bool) -> f32 {
let min = finite_px(min, 0.0);
let max = if max.is_finite() {
max.max(min)
} else {
f32::INFINITY
};
let intrinsic = finite_px(intrinsic, min);
match self {
Self::Fill
| Self::FillPortion(_)
| Self::Fluid(_)
| Self::Bounded {
sizing: LengthSizing::Fill(_),
..
} if !compressed => {
if max.is_finite() {
max
} else {
intrinsic.max(min)
}
}
Self::Fixed(px) => finite_px(px, min).clamp(min, max),
Self::Bounded { bounds, sizing } => {
let mut bounded_min = min;
let mut bounded_max = max;
bounds.apply(&mut bounded_min, &mut bounded_max);
match sizing {
LengthSizing::Fill(_) if !compressed => bounded_max,
LengthSizing::Shrink => {
bounded_min.min(intrinsic).clamp(bounded_min, bounded_max)
}
_ => intrinsic.clamp(bounded_min, bounded_max),
}
}
Self::Shrink => min.min(intrinsic).clamp(min, max),
_ => intrinsic.clamp(min, max),
}
}
}
impl From<f32> for Length {
fn from(value: f32) -> Self {
Self::Fixed(value)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LayoutLimits {
pub min: Size,
pub max: Size,
pub compress_width: bool,
pub compress_height: bool,
}
impl LayoutLimits {
pub const NONE: Self = Self {
min: Size::ZERO,
max: Size::INFINITE,
compress_width: false,
compress_height: false,
};
pub const fn new(min: Size, max: Size) -> Self {
Self {
min,
max,
compress_width: false,
compress_height: false,
}
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
match width.into() {
Length::Shrink => self.compress_width = true,
Length::Fit | Length::Fluid(_) => self.compress_width = false,
Length::Fixed(px) => {
let px = finite_px(px, self.min.width).clamp(self.min.width, self.max.width);
self.min.width = px;
self.max.width = px;
self.compress_width = false;
}
Length::Bounded { bounds, sizing } => {
bounds.apply(&mut self.min.width, &mut self.max.width);
if matches!(sizing, LengthSizing::Shrink) {
self.compress_width = true;
} else if matches!(sizing, LengthSizing::Fit) {
self.compress_width = false;
}
}
Length::Fill | Length::FillPortion(_) => {}
}
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
match height.into() {
Length::Shrink => self.compress_height = true,
Length::Fit | Length::Fluid(_) => self.compress_height = false,
Length::Fixed(px) => {
let px = finite_px(px, self.min.height).clamp(self.min.height, self.max.height);
self.min.height = px;
self.max.height = px;
self.compress_height = false;
}
Length::Bounded { bounds, sizing } => {
bounds.apply(&mut self.min.height, &mut self.max.height);
if matches!(sizing, LengthSizing::Shrink) {
self.compress_height = true;
} else if matches!(sizing, LengthSizing::Fit) {
self.compress_height = false;
}
}
Length::Fill | Length::FillPortion(_) => {}
}
self
}
pub fn shrink(self, size: Size) -> Self {
let size = size.sanitized();
Self {
min: Size::new(
(self.min.width - size.width).max(0.0),
(self.min.height - size.height).max(0.0),
),
max: Size::new(
(self.max.width - size.width).max(0.0),
(self.max.height - size.height).max(0.0),
),
compress_width: self.compress_width,
compress_height: self.compress_height,
}
}
pub fn loose(self) -> Self {
Self {
min: Size::ZERO,
..self
}
}
pub fn resolve(
self,
width: impl Into<Length>,
height: impl Into<Length>,
intrinsic: Size,
) -> Size {
Size::new(
width.into().resolve_axis(
self.min.width,
self.max.width,
intrinsic.width,
self.compress_width,
),
height.into().resolve_axis(
self.min.height,
self.max.height,
intrinsic.height,
self.compress_height,
),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum LayoutAxis {
Horizontal,
Vertical,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum LayoutAlign {
Start,
Center,
End,
Stretch,
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LayoutItem {
pub width: Length,
pub height: Length,
pub intrinsic_size: Size,
pub min_size: Size,
}
impl LayoutItem {
pub fn new(width: impl Into<Length>, height: impl Into<Length>, intrinsic_size: Size) -> Self {
Self {
width: width.into(),
height: height.into(),
intrinsic_size: intrinsic_size.sanitized(),
min_size: Size::ZERO,
}
}
pub fn with_min_size(mut self, min_size: Size) -> Self {
self.min_size = min_size.sanitized();
self
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct LayoutNode {
pub bounds: Rect,
pub children: Vec<LayoutNode>,
}
impl LayoutNode {
pub fn new(bounds: Rect) -> Self {
Self {
bounds: sanitize_rect(bounds),
children: Vec::new(),
}
}
pub fn with_children(bounds: Rect, children: Vec<LayoutNode>) -> Self {
Self {
bounds: sanitize_rect(bounds),
children,
}
}
pub fn size(&self) -> Size {
Size::new(self.bounds.width, self.bounds.height)
}
pub fn translate(mut self, delta: Vec2) -> Self {
self.bounds = self.bounds.translate(delta);
self.children = self
.children
.into_iter()
.map(|child| child.translate(delta))
.collect();
self
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct FlexLayout {
pub axis: LayoutAxis,
pub bounds: Rect,
pub gap_px: f32,
pub padding: Insets,
pub align: LayoutAlign,
}
impl FlexLayout {
pub fn row(bounds: Rect) -> Self {
Self::new(LayoutAxis::Horizontal, bounds)
}
pub fn column(bounds: Rect) -> Self {
Self::new(LayoutAxis::Vertical, bounds)
}
pub fn new(axis: LayoutAxis, bounds: Rect) -> Self {
Self {
axis,
bounds: sanitize_rect(bounds),
gap_px: 0.0,
padding: Insets::default(),
align: LayoutAlign::Stretch,
}
}
pub fn with_gap(mut self, gap_px: f32) -> Self {
self.gap_px = finite_px(gap_px, 0.0).clamp(0.0, 512.0);
self
}
pub fn with_padding(mut self, padding: Insets) -> Self {
self.padding = if padding.is_finite() {
padding
} else {
Insets::default()
};
self
}
pub fn with_align(mut self, align: LayoutAlign) -> Self {
self.align = align;
self
}
pub fn resolve(self, items: &[LayoutItem]) -> LayoutNode {
let content = sanitize_rect(self.bounds).inset(self.padding);
if content.is_empty() || items.is_empty() {
return LayoutNode::new(content);
}
let gap = finite_px(self.gap_px, 0.0).clamp(0.0, 512.0);
let total_gap = gap * items.len().saturating_sub(1) as f32;
let available_main = (main_len(content, self.axis) - total_gap).max(0.0);
let mut fixed_main = 0.0;
let mut fill_factor = 0_u32;
for item in items {
let length = item.main_length(self.axis);
let factor = length.fill_factor();
if factor == 0 {
fixed_main += item.resolve_main(self.axis, available_main);
} else {
fill_factor = fill_factor.saturating_add(factor as u32);
}
}
let remaining = (available_main - fixed_main).max(0.0);
let mut cursor = main_start(content, self.axis);
let mut children = Vec::with_capacity(items.len());
for item in items {
let main = if item.main_length(self.axis).fill_factor() == 0 {
item.resolve_main(self.axis, available_main)
} else if fill_factor == 0 {
0.0
} else {
remaining * item.main_length(self.axis).fill_factor() as f32 / fill_factor as f32
};
let cross = item.resolve_cross(self.axis, cross_len(content, self.axis), self.align);
let cross_origin = align_cross(content, self.axis, cross, self.align);
let rect = rect_from_axes(content, self.axis, cursor, main, cross_origin, cross);
children.push(LayoutNode::new(rect));
cursor += main + gap;
}
LayoutNode::with_children(content, children)
}
}
impl LayoutItem {
fn main_length(&self, axis: LayoutAxis) -> Length {
match axis {
LayoutAxis::Horizontal => self.width,
LayoutAxis::Vertical => self.height,
}
}
fn cross_length(&self, axis: LayoutAxis) -> Length {
match axis {
LayoutAxis::Horizontal => self.height,
LayoutAxis::Vertical => self.width,
}
}
fn intrinsic_main(&self, axis: LayoutAxis) -> f32 {
match axis {
LayoutAxis::Horizontal => self.intrinsic_size.width,
LayoutAxis::Vertical => self.intrinsic_size.height,
}
}
fn intrinsic_cross(&self, axis: LayoutAxis) -> f32 {
match axis {
LayoutAxis::Horizontal => self.intrinsic_size.height,
LayoutAxis::Vertical => self.intrinsic_size.width,
}
}
fn min_main(&self, axis: LayoutAxis) -> f32 {
match axis {
LayoutAxis::Horizontal => self.min_size.width,
LayoutAxis::Vertical => self.min_size.height,
}
}
fn min_cross(&self, axis: LayoutAxis) -> f32 {
match axis {
LayoutAxis::Horizontal => self.min_size.height,
LayoutAxis::Vertical => self.min_size.width,
}
}
fn resolve_main(&self, axis: LayoutAxis, available: f32) -> f32 {
self.main_length(axis)
.resolve_axis(
self.min_main(axis),
available.max(self.min_main(axis)),
self.intrinsic_main(axis),
false,
)
.max(0.0)
}
fn resolve_cross(&self, axis: LayoutAxis, available: f32, align: LayoutAlign) -> f32 {
if matches!(align, LayoutAlign::Stretch) && self.cross_length(axis).fill_factor() != 0 {
return available.max(0.0);
}
self.cross_length(axis)
.resolve_axis(
self.min_cross(axis),
available.max(self.min_cross(axis)),
self.intrinsic_cross(axis),
false,
)
.max(0.0)
}
}
pub fn layout_row(bounds: Rect, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
FlexLayout::row(bounds).with_gap(gap_px).resolve(items)
}
pub fn layout_column(bounds: Rect, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
FlexLayout::column(bounds).with_gap(gap_px).resolve(items)
}
pub fn layout_stack(bounds: Rect, items: &[LayoutItem]) -> LayoutNode {
let bounds = sanitize_rect(bounds);
let children = items.iter().map(|_| LayoutNode::new(bounds)).collect();
LayoutNode::with_children(bounds, children)
}
pub fn layout_grid(bounds: Rect, columns: usize, gap_px: f32, items: &[LayoutItem]) -> LayoutNode {
let bounds = sanitize_rect(bounds);
if bounds.is_empty() || columns == 0 || items.is_empty() {
return LayoutNode::new(bounds);
}
let columns = columns.min(items.len()).max(1);
let rows = items.len().div_ceil(columns).max(1);
let gap = finite_px(gap_px, 0.0).clamp(0.0, 512.0);
let cell_width =
((bounds.width - gap * columns.saturating_sub(1) as f32) / columns as f32).max(0.0);
let cell_height =
((bounds.height - gap * rows.saturating_sub(1) as f32) / rows as f32).max(0.0);
let children = items
.iter()
.enumerate()
.map(|(index, _)| {
let column = index % columns;
let row = index / columns;
LayoutNode::new(Rect::new(
bounds.x + (cell_width + gap) * column as f32,
bounds.y + (cell_height + gap) * row as f32,
cell_width,
cell_height,
))
})
.collect();
LayoutNode::with_children(bounds, children)
}
pub fn layout_space(
bounds: Rect,
width: impl Into<Length>,
height: impl Into<Length>,
intrinsic: Size,
) -> LayoutNode {
let bounds = sanitize_rect(bounds);
let limits = LayoutLimits::new(Size::ZERO, Size::new(bounds.width, bounds.height));
let size = limits.resolve(width, height, intrinsic);
LayoutNode::new(Rect::new(bounds.x, bounds.y, size.width, size.height))
}
fn main_len(rect: Rect, axis: LayoutAxis) -> f32 {
match axis {
LayoutAxis::Horizontal => rect.width,
LayoutAxis::Vertical => rect.height,
}
}
fn cross_len(rect: Rect, axis: LayoutAxis) -> f32 {
match axis {
LayoutAxis::Horizontal => rect.height,
LayoutAxis::Vertical => rect.width,
}
}
fn main_start(rect: Rect, axis: LayoutAxis) -> f32 {
match axis {
LayoutAxis::Horizontal => rect.x,
LayoutAxis::Vertical => rect.y,
}
}
fn align_cross(rect: Rect, axis: LayoutAxis, cross: f32, align: LayoutAlign) -> f32 {
let start = match axis {
LayoutAxis::Horizontal => rect.y,
LayoutAxis::Vertical => rect.x,
};
let available = cross_len(rect, axis);
match align {
LayoutAlign::Start | LayoutAlign::Stretch => start,
LayoutAlign::Center => start + (available - cross).max(0.0) * 0.5,
LayoutAlign::End => start + (available - cross).max(0.0),
}
}
fn rect_from_axes(
_content: Rect,
axis: LayoutAxis,
main: f32,
main_len: f32,
cross: f32,
cross_len: f32,
) -> Rect {
match axis {
LayoutAxis::Horizontal => Rect::new(main, cross, main_len, cross_len),
LayoutAxis::Vertical => Rect::new(cross, main, cross_len, main_len),
}
}
fn sanitize_rect(rect: Rect) -> Rect {
if rect.is_finite() && rect.width >= 0.0 && rect.height >= 0.0 {
rect
} else {
Rect::ZERO
}
}
fn finite_px(value: f32, fallback: f32) -> f32 {
if value.is_finite() {
value.max(0.0)
} else if fallback.is_finite() {
fallback.max(0.0)
} else {
0.0
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AppLayout {
pub root: Rect,
pub header: Rect,
pub main: Rect,
pub sidebar: Rect,
pub prompt: Rect,
pub status: Rect,
pub sidebar_collapsed: bool,
}
pub fn compute_shell_layout(viewport: Rect) -> AppLayout {
let viewport = if viewport.is_finite() && !viewport.is_empty() {
viewport
} else {
Rect::ZERO
};
let outer_gap = 16.0;
let inner_gap = 12.0;
let status_height = 32.0;
let prompt_height = if viewport.height < 680.0 { 92.0 } else { 126.0 };
let header_height = if viewport.height < 620.0 { 0.0 } else { 58.0 };
let content = viewport.inset(Insets::all(outer_gap));
if content.height < status_height + inner_gap + prompt_height || content.width <= 0.0 {
return AppLayout {
root: viewport,
header: Rect::ZERO,
main: Rect::ZERO,
sidebar: Rect::ZERO,
prompt: Rect::ZERO,
status: Rect::ZERO,
sidebar_collapsed: true,
};
}
let sidebar_collapsed = viewport.width < 980.0;
let sidebar_width = if sidebar_collapsed {
0.0
} else {
(viewport.width * 0.27).clamp(300.0, 440.0)
};
let header = Rect::new(content.x, content.y, content.width, header_height);
let status = Rect::new(
content.x,
content.bottom() - status_height,
content.width,
status_height,
);
let prompt = Rect::new(
content.x,
status.y - inner_gap - prompt_height,
content.width,
prompt_height,
);
let body_y = header.y + header.height + if header_height > 0.0 { inner_gap } else { 0.0 };
let body_h = (prompt.y - inner_gap - body_y).max(0.0);
let sidebar = if sidebar_collapsed {
Rect::ZERO
} else {
Rect::new(
content.right() - sidebar_width,
body_y,
sidebar_width,
body_h,
)
};
let main_width = if sidebar_collapsed {
content.width
} else {
(content.width - sidebar_width - inner_gap).max(0.0)
};
let main = Rect::new(content.x, body_y, main_width, body_h);
AppLayout {
root: viewport,
header,
main,
sidebar,
prompt,
status,
sidebar_collapsed,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn layout_collapses_sidebar_on_small_width() {
let layout = compute_shell_layout(Rect::new(0.0, 0.0, 800.0, 700.0));
assert!(layout.sidebar_collapsed);
assert!(layout.sidebar.is_empty());
assert!(layout.main.width > 700.0);
}
#[test]
fn layout_keeps_prompt_and_status_visible() {
let layout = compute_shell_layout(Rect::new(0.0, 0.0, 1440.0, 900.0));
assert!(layout.main.height > 500.0);
assert!(layout.prompt.y > layout.main.y);
assert!(layout.status.y > layout.prompt.y);
}
#[test]
fn layout_returns_empty_children_for_malformed_or_tiny_viewports() {
let malformed = compute_shell_layout(Rect::new(f32::NAN, 0.0, 100.0, 100.0));
let tiny = compute_shell_layout(Rect::new(0.0, 0.0, 64.0, 80.0));
assert_eq!(malformed.root, Rect::ZERO);
assert_eq!(malformed.prompt, Rect::ZERO);
assert_eq!(tiny.root, Rect::new(0.0, 0.0, 64.0, 80.0));
assert_eq!(tiny.status, Rect::ZERO);
assert!(tiny.sidebar_collapsed);
}
#[test]
fn length_limits_resolve_fixed_fill_and_bounded_lengths() {
let limits = LayoutLimits::new(Size::new(20.0, 10.0), Size::new(300.0, 120.0));
assert_eq!(Length::Fixed(500.0).fill_factor(), 0);
assert_eq!(Length::FillPortion(3).fill_factor(), 3);
assert_eq!(
limits
.resolve(Length::Fixed(500.0), Length::Fit, Size::new(50.0, 30.0))
.width,
300.0
);
assert_eq!(
limits
.resolve(Length::Fill, Length::Fit, Size::new(50.0, 30.0))
.width,
300.0
);
assert_eq!(
limits
.resolve(Length::Fit.max(80.0), Length::Fit, Size::new(140.0, 30.0))
.width,
80.0
);
assert!(limits.width(Length::Shrink).compress_width);
}
#[test]
fn flex_row_distributes_fill_portions_after_fixed_space() {
let items = [
LayoutItem::new(100.0, Length::Fill, Size::new(100.0, 20.0)),
LayoutItem::new(Length::FillPortion(1), Length::Fill, Size::new(10.0, 20.0)),
LayoutItem::new(Length::FillPortion(2), Length::Fill, Size::new(10.0, 20.0)),
];
let node = layout_row(Rect::new(0.0, 0.0, 320.0, 40.0), 10.0, &items);
assert_eq!(node.children.len(), 3);
assert_eq!(node.children[0].bounds, Rect::new(0.0, 0.0, 100.0, 40.0));
assert!((node.children[1].bounds.width - 66.666_67).abs() < 0.01);
assert!((node.children[2].bounds.width - 133.333_34).abs() < 0.01);
assert!((node.children[2].bounds.x - 186.666_67).abs() < 0.01);
}
#[test]
fn grid_stack_and_space_layouts_are_finite() {
let items = [
LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
LayoutItem::new(Length::Fit, Length::Fit, Size::new(20.0, 10.0)),
];
let grid = layout_grid(Rect::new(0.0, 0.0, 210.0, 100.0), 2, 10.0, &items);
let stack = layout_stack(Rect::new(4.0, 5.0, 30.0, 20.0), &items[..2]);
let space = layout_space(
Rect::new(0.0, 0.0, 80.0, 60.0),
Length::Fill,
12.0,
Size::ZERO,
);
assert_eq!(grid.children.len(), 3);
assert_eq!(grid.children[0].bounds, Rect::new(0.0, 0.0, 100.0, 45.0));
assert_eq!(grid.children[2].bounds, Rect::new(0.0, 55.0, 100.0, 45.0));
assert_eq!(stack.children[0].bounds, Rect::new(4.0, 5.0, 30.0, 20.0));
assert_eq!(space.bounds, Rect::new(0.0, 0.0, 80.0, 12.0));
}
#[test]
fn rect_intersection_union_and_translation_are_stable() {
let a = Rect::new(10.0, 20.0, 100.0, 80.0);
let b = Rect::new(80.0, 60.0, 80.0, 80.0);
assert_eq!(a.center(), Vec2::new(60.0, 60.0));
assert_eq!(
a.translate(Vec2::new(5.0, -10.0)),
Rect::new(15.0, 10.0, 100.0, 80.0)
);
assert_eq!(a.intersection(b), Some(Rect::new(80.0, 60.0, 30.0, 40.0)));
assert_eq!(a.union(b), Rect::new(10.0, 20.0, 150.0, 120.0));
assert_eq!(a.intersection(Rect::new(120.0, 20.0, 10.0, 10.0)), None);
}
#[test]
fn rect_clamp_keeps_popovers_inside_bounds() {
let bounds = Rect::new(0.0, 0.0, 300.0, 200.0);
assert_eq!(
Rect::new(260.0, 180.0, 90.0, 60.0).clamp_within(bounds),
Rect::new(210.0, 140.0, 90.0, 60.0)
);
assert_eq!(
Rect::new(-20.0, -30.0, 360.0, 260.0).clamp_within(bounds),
bounds
);
assert_eq!(Rect::ZERO.clamp_within(bounds), Rect::ZERO);
}
#[test]
fn rect_clamp_handles_tiny_rounding_ranges_without_panic() {
let x = f32::from_bits(0x2042_a815);
let width = f32::from_bits(0x2390_0b54);
let bounds = Rect::new(x, 0.0, width, 1.0);
let clamped = bounds.clamp_within(bounds);
assert!(clamped.is_finite());
assert_eq!(clamped.x, bounds.x);
}
#[test]
fn rect_helpers_tolerate_non_finite_public_values() {
let valid = Rect::new(0.0, 0.0, 10.0, 10.0);
let malformed = Rect::new(f32::NAN, 0.0, 10.0, 10.0);
let overflowing = Rect::new(f32::MAX, 0.0, f32::MAX, 10.0);
assert!(!malformed.is_finite());
assert!(!overflowing.is_finite());
assert_eq!(malformed.intersection(valid), None);
assert_eq!(overflowing.intersection(valid), None);
assert_eq!(malformed.union(valid), valid);
assert_eq!(overflowing.union(valid), valid);
assert_eq!(valid.clamp_within(malformed), Rect::ZERO);
assert_eq!(valid.clamp_within(overflowing), Rect::ZERO);
assert_eq!(valid.inset(Insets::all(f32::INFINITY)), Rect::ZERO);
assert_eq!(overflowing.inset(Insets::all(1.0)), Rect::ZERO);
assert!(!valid.contains(f32::NAN, 1.0));
assert!(!malformed.contains(1.0, 1.0));
assert!(!overflowing.contains(f32::MAX, 1.0));
}
}