#![forbid(unsafe_code)]
use crate::geometry::Size;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Axis {
Horizontal,
Vertical,
}
impl Axis {
#[inline]
pub fn main(self, size: Size) -> f64 {
match self {
Axis::Horizontal => size.width,
Axis::Vertical => size.height,
}
}
#[inline]
pub fn cross(self, size: Size) -> f64 {
match self {
Axis::Horizontal => size.height,
Axis::Vertical => size.width,
}
}
#[inline]
pub fn size(self, main: f64, cross: f64) -> Size {
match self {
Axis::Horizontal => Size::new(main, cross),
Axis::Vertical => Size::new(cross, main),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum LayoutDirection {
#[default]
Ltr,
Rtl,
}
impl LayoutDirection {
#[inline]
pub fn is_rtl(self) -> bool {
self == LayoutDirection::Rtl
}
pub fn for_language(language: &str) -> Self {
let primary = language
.split(['-', '_', '.', '@'])
.next()
.unwrap_or("")
.to_ascii_lowercase();
const RTL: &[&str] = &[
"ar", "arc", "ckb", "dv", "fa", "he", "iw", "ji", "ks", "ku", "nqo", "ps", "sd", "syr",
"ug", "ur", "yi",
];
if RTL.contains(&primary.as_str()) {
LayoutDirection::Rtl
} else {
LayoutDirection::Ltr
}
}
pub fn from_env() -> Self {
["LC_ALL", "LC_MESSAGES", "LANG"]
.iter()
.filter_map(|k| std::env::var(k).ok())
.find(|v| !v.is_empty())
.map_or(LayoutDirection::Ltr, |v| Self::for_language(&v))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Constraints {
pub min: Size,
pub max: Size,
}
impl Constraints {
#[inline]
pub fn loose(max: Size) -> Self {
Self {
min: Size::ZERO,
max,
}
}
#[inline]
pub fn tight(size: Size) -> Self {
Self {
min: size,
max: size,
}
}
#[inline]
pub fn constrain(&self, size: Size) -> Size {
size.clamp(self.min, self.max)
}
pub fn deflate(&self, horizontal: f64, vertical: f64) -> Self {
let max = Size::new(
(self.max.width - horizontal).max(0.0),
(self.max.height - vertical).max(0.0),
);
Self {
min: self.min.clamp(Size::ZERO, max),
max,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct FlexItem {
pub basis: f64,
pub grow: f64,
pub shrink: f64,
pub min: f64,
pub max: f64,
}
impl FlexItem {
#[inline]
pub fn fixed(basis: f64) -> Self {
Self {
basis,
grow: 0.0,
shrink: 0.0,
min: 0.0,
max: f64::INFINITY,
}
}
#[inline]
pub fn flex(grow: f64) -> Self {
Self {
grow,
..Self::fixed(0.0)
}
}
#[inline]
pub fn with_grow(mut self, grow: f64) -> Self {
self.grow = grow;
self
}
#[inline]
pub fn with_shrink(mut self, shrink: f64) -> Self {
self.shrink = shrink;
self
}
#[inline]
pub fn with_bounds(mut self, min: f64, max: f64) -> Self {
self.min = min;
self.max = max;
self
}
#[inline]
pub fn clamp(&self, length: f64) -> f64 {
clamp_len(length, self.min, self.max)
}
}
#[inline]
pub fn clamp_len(length: f64, min: f64, max: f64) -> f64 {
length.min(max).max(min)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Span {
pub offset: f64,
pub length: f64,
}
impl Span {
#[inline]
pub fn mirrored(self, extent: f64) -> Span {
Span {
offset: extent - self.offset - self.length,
length: self.length,
}
}
}
pub fn mirror_spans(spans: &mut [Span], extent: f64) {
for s in spans {
*s = s.mirrored(extent);
}
}
pub fn solve_main_axis(available: f64, gap: f64, items: &[FlexItem]) -> Vec<Span> {
if items.is_empty() {
return Vec::new();
}
let total_gap = gap * (items.len() as f64 - 1.0);
let base: Vec<f64> = items.iter().map(|i| i.clamp(i.basis)).collect();
let free = available - base.iter().sum::<f64>() - total_gap;
let mut lengths = base.clone();
if free > 0.0 {
let mut frozen: Vec<bool> = items.iter().map(|i| i.grow <= 0.0).collect();
loop {
let taken: f64 = (0..items.len())
.filter(|&k| frozen[k])
.map(|k| lengths[k] - base[k])
.sum();
let remaining = (free - taken).max(0.0);
let weight: f64 = (0..items.len())
.filter(|&k| !frozen[k])
.map(|k| items[k].grow)
.sum();
if weight <= 0.0 {
break;
}
let mut violated = false;
for k in 0..items.len() {
if frozen[k] {
continue;
}
let target = base[k] + remaining * items[k].grow / weight;
if target > items[k].max {
lengths[k] = items[k].max.max(base[k]);
frozen[k] = true;
violated = true;
} else {
lengths[k] = target;
}
}
if !violated {
break;
}
}
} else if free < 0.0 {
let deficit = -free;
let mut frozen: Vec<bool> = items
.iter()
.zip(&base)
.map(|(i, b)| i.shrink <= 0.0 || *b <= 0.0)
.collect();
loop {
let given: f64 = (0..items.len())
.filter(|&k| frozen[k])
.map(|k| base[k] - lengths[k])
.sum();
let remaining = (deficit - given).max(0.0);
let weight: f64 = (0..items.len())
.filter(|&k| !frozen[k])
.map(|k| items[k].shrink * base[k])
.sum();
if weight <= 0.0 {
break;
}
let mut violated = false;
for k in 0..items.len() {
if frozen[k] {
continue;
}
let target = base[k] - remaining * items[k].shrink * base[k] / weight;
let floor = items[k].min.max(0.0);
if target < floor {
lengths[k] = floor.min(base[k]);
frozen[k] = true;
violated = true;
} else {
lengths[k] = target;
}
}
if !violated {
break;
}
}
}
pack(&lengths, gap)
}
fn pack(lengths: &[f64], gap: f64) -> Vec<Span> {
let mut spans = Vec::with_capacity(lengths.len());
let mut cursor = 0.0;
for &length in lengths {
spans.push(Span {
offset: cursor,
length,
});
cursor += length + gap;
}
spans
}
pub fn break_lines(available: f64, gap: f64, lengths: &[f64]) -> Vec<std::ops::Range<usize>> {
let mut lines = Vec::new();
let mut start = 0;
let mut used = 0.0;
for (k, &len) in lengths.iter().enumerate() {
if k == start {
used = len;
} else if used + gap + len > available + 1e-9 {
lines.push(start..k);
start = k;
used = len;
} else {
used += gap + len;
}
}
if start < lengths.len() {
lines.push(start..lengths.len());
}
lines
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Track {
Fixed(f64),
Fr(f64),
Auto,
}
impl Track {
#[inline]
pub fn px(length: f64) -> Self {
Track::Fixed(length)
}
#[inline]
pub fn fr(weight: f64) -> Self {
Track::Fr(weight)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct TrackItem {
pub start: usize,
pub span: usize,
pub size: f64,
}
pub fn solve_tracks(
tracks: &[Track],
available: Option<f64>,
gap: f64,
items: &[TrackItem],
) -> Vec<Span> {
let n = tracks.len();
if n == 0 {
return Vec::new();
}
let mut sizes: Vec<f64> = tracks
.iter()
.map(|t| match t {
Track::Fixed(v) => v.max(0.0),
_ => 0.0,
})
.collect();
let mut fr_content = vec![0.0_f64; n];
for it in items.iter().filter(|it| it.span <= 1) {
let k = it.start.min(n - 1);
match tracks[k] {
Track::Auto => sizes[k] = sizes[k].max(it.size),
Track::Fr(_) => fr_content[k] = fr_content[k].max(it.size),
Track::Fixed(_) => {}
}
}
let mut spanning: Vec<&TrackItem> = items.iter().filter(|it| it.span > 1).collect();
spanning.sort_by_key(|it| it.span);
for it in spanning {
let start = it.start.min(n - 1);
let range = start..(start + it.span).min(n);
let current: f64 = range.clone().map(|k| sizes[k] + fr_content[k]).sum::<f64>()
+ gap * (range.len() as f64 - 1.0);
let deficit = it.size - current;
if deficit <= 0.0 {
continue;
}
let autos: Vec<usize> = range
.clone()
.filter(|&k| tracks[k] == Track::Auto)
.collect();
if !autos.is_empty() {
for &k in &autos {
sizes[k] += deficit / autos.len() as f64;
}
continue;
}
let frs: Vec<usize> = range
.filter(|&k| matches!(tracks[k], Track::Fr(_)))
.collect();
for &k in &frs {
fr_content[k] += deficit / frs.len() as f64;
}
}
let weight = |t: &Track| match t {
Track::Fr(w) => w.max(0.0),
_ => 0.0,
};
let total_weight: f64 = tracks.iter().map(weight).sum();
if total_weight > 0.0 {
let unit = match available {
Some(avail) => {
let used: f64 = sizes.iter().sum::<f64>() + gap * (n as f64 - 1.0);
(avail - used).max(0.0) / total_weight
}
None => tracks
.iter()
.zip(&fr_content)
.filter(|(t, _)| weight(t) > 0.0)
.map(|(t, c)| c / weight(t))
.fold(0.0_f64, f64::max),
};
for (size, t) in sizes.iter_mut().zip(tracks) {
if matches!(t, Track::Fr(_)) {
*size = unit * weight(t);
}
}
}
pack(&sizes, gap)
}
pub fn span_extent(spans: &[Span], start: usize, span: usize) -> (f64, f64) {
if spans.is_empty() {
return (0.0, 0.0);
}
let first = start.min(spans.len() - 1);
let last = (first + span.max(1) - 1).min(spans.len() - 1);
let offset = spans[first].offset;
(offset, spans[last].offset + spans[last].length - offset)
}
pub fn place_cells(columns: usize, spans: &[(usize, usize)]) -> (Vec<(usize, usize)>, usize) {
let columns = columns.max(1);
let mut occupied: Vec<Vec<bool>> = Vec::new();
let mut placed = Vec::with_capacity(spans.len());
let (mut row, mut col) = (0usize, 0usize);
let mut rows = 0;
for &(cs, rs) in spans {
let cs = cs.clamp(1, columns);
let rs = rs.max(1);
loop {
if col + cs > columns {
row += 1;
col = 0;
continue;
}
let free = (row..row + rs).all(|r| {
occupied
.get(r)
.is_none_or(|line| line[col..col + cs].iter().all(|o| !o))
});
if free {
break;
}
col += 1;
}
for r in row..row + rs {
if occupied.len() <= r {
occupied.resize(r + 1, vec![false; columns]);
}
for cell in &mut occupied[r][col..col + cs] {
*cell = true;
}
}
placed.push((row, col));
rows = rows.max(row + rs);
col += cs;
}
(placed, rows)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fixed_items_pack_with_gap() {
let spans = solve_main_axis(200.0, 10.0, &[FlexItem::fixed(40.0), FlexItem::fixed(60.0)]);
assert_eq!(
spans[0],
Span {
offset: 0.0,
length: 40.0
}
);
assert_eq!(
spans[1],
Span {
offset: 50.0,
length: 60.0
}
);
}
#[test]
fn grow_distributes_free_space() {
let spans = solve_main_axis(
200.0,
20.0,
&[
FlexItem::fixed(40.0),
FlexItem::flex(1.0),
FlexItem::flex(1.0),
],
);
assert_eq!(spans[0].length, 40.0);
assert_eq!(spans[1].length, 60.0);
assert_eq!(spans[2].length, 60.0);
assert_eq!(spans[1].offset, 60.0);
assert_eq!(spans[2].offset, 140.0);
}
#[test]
fn overflow_clamps_free_to_zero() {
let spans = solve_main_axis(30.0, 0.0, &[FlexItem::fixed(40.0), FlexItem::flex(1.0)]);
assert_eq!(spans[1].length, 0.0);
}
#[test]
fn axis_main_cross_roundtrip() {
let s = Axis::Vertical.size(10.0, 4.0);
assert_eq!(s, Size::new(4.0, 10.0));
assert_eq!(Axis::Vertical.main(s), 10.0);
assert_eq!(Axis::Vertical.cross(s), 4.0);
}
fn lengths(spans: &[Span]) -> Vec<f64> {
spans.iter().map(|s| s.length).collect()
}
fn offsets(spans: &[Span]) -> Vec<f64> {
spans.iter().map(|s| s.offset).collect()
}
#[test]
fn grow_stops_at_max_and_the_rest_goes_to_others() {
let spans = solve_main_axis(
300.0,
0.0,
&[
FlexItem::flex(1.0).with_bounds(0.0, 100.0),
FlexItem::flex(1.0),
],
);
assert_eq!(lengths(&spans), vec![100.0, 200.0]);
assert_eq!(offsets(&spans), vec![0.0, 100.0]);
}
#[test]
fn grow_with_every_item_capped_leaves_space_unused() {
let spans = solve_main_axis(
500.0,
10.0,
&[
FlexItem::flex(1.0).with_bounds(0.0, 50.0),
FlexItem::flex(3.0).with_bounds(0.0, 80.0),
],
);
assert_eq!(lengths(&spans), vec![50.0, 80.0]);
assert_eq!(offsets(&spans), vec![0.0, 60.0]);
}
#[test]
fn grow_cascading_caps_redistribute_repeatedly() {
let spans = solve_main_axis(
300.0,
0.0,
&[
FlexItem::flex(1.0).with_bounds(0.0, 40.0),
FlexItem::flex(1.0).with_bounds(0.0, 120.0),
FlexItem::flex(1.0),
],
);
assert_eq!(lengths(&spans), vec![40.0, 120.0, 140.0]);
}
#[test]
fn min_and_max_clamp_the_basis() {
let spans = solve_main_axis(
1000.0,
0.0,
&[
FlexItem::fixed(10.0).with_bounds(60.0, f64::INFINITY),
FlexItem::fixed(500.0).with_bounds(0.0, 200.0),
],
);
assert_eq!(lengths(&spans), vec![60.0, 200.0]);
assert_eq!(clamp_len(5.0, 50.0, 20.0), 50.0);
}
#[test]
fn grow_item_with_min_keeps_it_when_space_runs_out() {
let spans = solve_main_axis(
100.0,
0.0,
&[
FlexItem::fixed(100.0),
FlexItem::flex(1.0).with_bounds(80.0, f64::INFINITY),
],
);
assert_eq!(lengths(&spans), vec![100.0, 80.0]);
}
#[test]
fn shrink_absorbs_overflow_weighted_by_basis() {
let spans = solve_main_axis(
200.0,
0.0,
&[
FlexItem::fixed(100.0).with_shrink(1.0),
FlexItem::fixed(300.0).with_shrink(1.0),
],
);
assert_eq!(lengths(&spans), vec![50.0, 150.0]);
}
#[test]
fn shrink_stops_at_min() {
let spans = solve_main_axis(
200.0,
0.0,
&[
FlexItem::fixed(100.0)
.with_shrink(1.0)
.with_bounds(90.0, f64::INFINITY),
FlexItem::fixed(300.0).with_shrink(1.0),
],
);
assert_eq!(lengths(&spans), vec![90.0, 110.0]);
}
#[test]
fn non_shrinking_items_overflow_and_shrink_never_goes_negative() {
let spans = solve_main_axis(
50.0,
0.0,
&[
FlexItem::fixed(100.0),
FlexItem::fixed(20.0).with_shrink(1.0),
],
);
assert_eq!(lengths(&spans), vec![100.0, 0.0]);
}
#[test]
fn shrink_accounts_for_gaps() {
let spans = solve_main_axis(
120.0,
20.0,
&[
FlexItem::fixed(100.0).with_shrink(1.0),
FlexItem::fixed(100.0).with_shrink(1.0),
],
);
assert_eq!(lengths(&spans), vec![50.0, 50.0]);
assert_eq!(offsets(&spans), vec![0.0, 70.0]);
}
#[test]
fn break_lines_flows_items_with_gap() {
assert_eq!(
break_lines(100.0, 10.0, &[40.0, 40.0, 40.0, 40.0, 40.0]),
vec![0..2, 2..4, 4..5]
);
assert_eq!(break_lines(90.0, 10.0, &[40.0, 40.0]), vec![0..2]);
}
#[test]
fn break_lines_gives_oversized_items_their_own_line() {
assert_eq!(
break_lines(100.0, 0.0, &[30.0, 150.0, 30.0, 30.0]),
vec![0..1, 1..2, 2..4]
);
assert_eq!(break_lines(f64::INFINITY, 5.0, &[1e6, 1e6]), vec![0..2]);
assert!(break_lines(100.0, 0.0, &[]).is_empty());
}
fn item(start: usize, span: usize, size: f64) -> TrackItem {
TrackItem { start, span, size }
}
#[test]
fn tracks_fixed_auto_fr_mix() {
let tracks = [
Track::px(100.0),
Track::Auto,
Track::fr(1.0),
Track::fr(3.0),
];
let spans = solve_tracks(
&tracks,
Some(400.0),
10.0,
&[item(1, 1, 40.0), item(1, 1, 60.0), item(2, 1, 500.0)],
);
assert_eq!(lengths(&spans), vec![100.0, 60.0, 52.5, 157.5]);
assert_eq!(offsets(&spans), vec![0.0, 110.0, 180.0, 242.5]);
}
#[test]
fn tracks_fr_squeezes_to_zero_when_space_is_short() {
let spans = solve_tracks(&[Track::px(100.0), Track::fr(1.0)], Some(80.0), 0.0, &[]);
assert_eq!(lengths(&spans), vec![100.0, 0.0]);
}
#[test]
fn tracks_fr_indefinite_sizes_to_content_proportionally() {
let spans = solve_tracks(
&[Track::fr(1.0), Track::fr(2.0), Track::Auto],
None,
5.0,
&[item(0, 1, 30.0), item(1, 1, 40.0), item(2, 1, 12.0)],
);
assert_eq!(lengths(&spans), vec![30.0, 60.0, 12.0]);
assert_eq!(offsets(&spans), vec![0.0, 35.0, 100.0]);
}
#[test]
fn tracks_spanning_item_grows_auto_tracks() {
let spans = solve_tracks(
&[Track::Auto, Track::Auto],
None,
10.0,
&[item(0, 1, 20.0), item(1, 1, 30.0), item(0, 2, 100.0)],
);
assert_eq!(lengths(&spans), vec![40.0, 50.0]);
let spans = solve_tracks(
&[Track::Auto, Track::px(50.0)],
None,
0.0,
&[item(0, 1, 20.0), item(0, 2, 60.0)],
);
assert_eq!(lengths(&spans), vec![20.0, 50.0]);
}
#[test]
fn span_extent_includes_gaps() {
let spans = solve_tracks(&[Track::px(10.0); 3], None, 5.0, &[]);
assert_eq!(span_extent(&spans, 0, 3), (0.0, 40.0));
assert_eq!(span_extent(&spans, 1, 1), (15.0, 10.0));
assert_eq!(span_extent(&spans, 2, 4), (30.0, 10.0));
}
#[test]
fn place_cells_flows_row_major() {
let (cells, rows) = place_cells(3, &[(1, 1); 7]);
assert_eq!(
cells,
vec![(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0)]
);
assert_eq!(rows, 3);
}
#[test]
fn place_cells_honors_spans() {
let (cells, rows) = place_cells(3, &[(1, 2), (1, 1), (2, 1), (1, 1), (5, 1)]);
assert_eq!(cells, vec![(0, 0), (0, 1), (1, 1), (2, 0), (3, 0)]);
assert_eq!(rows, 4);
}
#[test]
fn mirrored_spans_reverse_a_row_from_the_far_edge() {
let mut spans =
solve_main_axis(200.0, 10.0, &[FlexItem::fixed(40.0), FlexItem::fixed(60.0)]);
mirror_spans(&mut spans, 200.0);
assert_eq!(
spans,
vec![
Span {
offset: 160.0,
length: 40.0
},
Span {
offset: 90.0,
length: 60.0
},
]
);
mirror_spans(&mut spans, 200.0);
assert_eq!(spans[0].offset, 0.0);
assert_eq!(spans[1].offset, 50.0);
}
#[test]
fn mirrored_tracks_count_columns_from_the_right() {
let mut cols = solve_tracks(
&[Track::px(50.0), Track::fr(1.0), Track::px(30.0)],
Some(200.0),
10.0,
&[],
);
mirror_spans(&mut cols, 200.0);
assert_eq!(cols[0].offset, 150.0);
assert_eq!(cols[1].offset, 40.0);
assert_eq!(cols[1].length, 100.0);
assert_eq!(cols[2].offset, 0.0);
}
#[test]
fn layout_direction_from_language_tags() {
for tag in [
"he",
"he-IL",
"ar_EG.UTF-8",
"fa",
"UR",
"yi",
"ckb-IQ",
"ps@latin",
] {
assert_eq!(
LayoutDirection::for_language(tag),
LayoutDirection::Rtl,
"{tag}"
);
}
for tag in ["", "C", "POSIX", "en_US.UTF-8", "fr-CA", "ja", "hi", "heb"] {
assert_eq!(
LayoutDirection::for_language(tag),
LayoutDirection::Ltr,
"{tag}"
);
}
assert!(LayoutDirection::Rtl.is_rtl() && !LayoutDirection::default().is_rtl());
}
}