Skip to main content

intuituive/
layout.rs

1//! Layout: how a container shares its space among its children.
2//!
3//! A child asks for a [`Size`] along its parent's axis, optionally clamped
4//! with [`Node::min_size`](crate::Node::min_size) and
5//! [`Node::max_size`](crate::Node::max_size). The solver gives every child
6//! its fixed, percentage or content size first, then shares what is left
7//! among the flexible children by weight. A flexible child that hits its
8//! minimum or maximum keeps it, and the rest is shared again among the
9//! others, as CSS flexbox resolves flexible lengths. When the children ask for more than there is, they shrink from
10//! the end: down to their minimums first, then past them.
11//!
12//! [`grid`](crate::grid) places children in rows and columns, each a track
13//! solved the same way, with children spanning several tracks.
14//!
15//! The solver is also a function of its own: [`split`] lays [`Track`]s out
16//! in a rectangle, as a column or row does its children, and
17//! [`split_with_spacers`] gives the room between them too, for a widget
18//! that places its own parts (a pop-up beside a cell, a gauge's ticks).
19
20use crate::node::Axis;
21use crate::screen::Rect;
22
23/// How much of its parent's axis a child takes.
24///
25/// More kinds may come: match with a wildcard arm.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum Size {
29    /// Exactly this many cells.
30    Fixed(u16),
31    /// This percentage of the parent.
32    Percent(u16),
33    /// A share, by weight, of what the other children leave.
34    Flex(u16),
35    /// As much as its content needs: the lines a text renders to, a
36    /// panel's content and border, a list's rows. Measured again when a
37    /// signal the content reads changes.
38    Auto,
39    /// `a / b` of the parent, as a percentage is: ratatui's
40    /// `Constraint::Ratio`. Of the room after gaps, rounded down; the cells
41    /// that rounding leaves go to the last ratio of the parent, so three
42    /// `Ratio(1, 3)` fill it exactly. A zero `b` is no room at all, and a
43    /// ratio over one is the whole parent.
44    Ratio(u32, u32),
45}
46
47/// `total × a / b` cells, rounded down: none when `b` is 0, at most
48/// `total`.
49pub(crate) fn ratio_of(total: u32, a: u32, b: u32) -> u32 {
50    if b == 0 {
51        return 0;
52    }
53    (u64::from(total) * u64::from(a) / u64::from(b)).min(u64::from(total)) as u32
54}
55
56/// The cells a group of ratios takes of `total` together, rounded down
57/// once: `total × Σ a / b`, worked out exactly (each ratio at most one).
58fn ratios_of(total: u32, ratios: &[(u32, u32)]) -> u32 {
59    fn gcd(mut a: u128, mut b: u128) -> u128 {
60        while b != 0 {
61            (a, b) = (b, a % b);
62        }
63        a.max(1)
64    }
65    let (mut num, mut den) = (0u128, 1u128);
66    for &(a, b) in ratios.iter().filter(|(_, b)| *b != 0) {
67        let (a, b) = (u128::from(a.min(b)), u128::from(b));
68        let sum = num
69            .checked_mul(b)
70            .and_then(|n| a.checked_mul(den).and_then(|m| n.checked_add(m)));
71        match (sum, den.checked_mul(b)) {
72            (Some(n), Some(d)) => {
73                let g = gcd(n, d);
74                (num, den) = (n / g, d / g);
75            }
76            // Denominators too large to add exactly (many large primes):
77            // near enough.
78            _ => {
79                let exact: f64 = ratios
80                    .iter()
81                    .filter(|(_, b)| *b != 0)
82                    .map(|&(a, b)| f64::from(a.min(b)) / f64::from(b))
83                    .sum();
84                return (f64::from(total) * exact).floor().min(f64::from(total)) as u32;
85            }
86        }
87    }
88    (u128::from(total) * num / den).min(u128::from(total)) as u32
89}
90
91/// How the lines of a text sit across its width: CSS's `text-align`, and
92/// ratatui's `Alignment` on a paragraph.
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
94pub enum TextAlign {
95    /// At the left: the default.
96    #[default]
97    Left,
98    /// Each line centred.
99    Center,
100    /// Each line at the right.
101    Right,
102    /// Each line but the last spread to the whole width, the words spaced
103    /// out evenly.
104    Justify,
105}
106
107/// Where a child sits across its parent's axis: across a
108/// [`column`](crate::column)'s width, or a [`row`](crate::row)'s height.
109/// CSS's `align-items` and `align-self`.
110#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
111pub enum Align {
112    /// At the start (left in a column, top in a row), as wide or tall as
113    /// its content.
114    Start,
115    /// Centred, as wide or tall as its content.
116    Center,
117    /// At the end, as wide or tall as its content.
118    End,
119    /// The whole width or height: the default.
120    #[default]
121    Stretch,
122}
123
124/// Where children sit along their parent's axis when they leave room over:
125/// CSS's `justify-content`. A container with a flexible child has none left
126/// over (the child takes it), as in CSS.
127#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
128pub enum Justify {
129    /// Packed at the start: the default.
130    #[default]
131    Start,
132    /// Packed in the middle.
133    Center,
134    /// Packed at the end.
135    End,
136    /// The first at the start, the last at the end, the room shared
137    /// evenly between them.
138    SpaceBetween,
139    /// The room shared evenly around each child: half as much at the ends
140    /// as between two children.
141    SpaceAround,
142    /// The room shared evenly, the ends included.
143    SpaceEvenly,
144}
145
146/// Where the lines of a [wrapping](crate::Node::wrap) row or column sit
147/// across its axis when they leave room over: CSS's `align-content`.
148#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
149pub enum AlignContent {
150    Start,
151    Center,
152    End,
153    SpaceBetween,
154    SpaceAround,
155    SpaceEvenly,
156    /// The lines grow to share the room: the default.
157    #[default]
158    Stretch,
159}
160
161/// How a [`column`](crate::column) or [`row`](crate::row) places its
162/// children: the gap between them, alignment across and along its axis,
163/// and wrapping. [`MeasureCx::stack_flow`](crate::widget::MeasureCx::stack_flow)
164/// takes one.
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
166pub struct StackLayout {
167    pub gap: u16,
168    pub align_items: Align,
169    pub justify: Justify,
170    pub wrap: bool,
171    pub align_content: AlignContent,
172    /// How the lengths are solved (`Solver::Native` by default).
173    pub solver: crate::solver::Solver,
174}
175
176/// One track to lay out with [`split`]: a [`Size`] and its clamps, as a
177/// child of a column or row has them.
178///
179/// ```
180/// use intuituive::layout::Track;
181/// use intuituive::Size;
182///
183/// let sidebar = Track::new(Size::Percent(25)).min(20).max(40);
184/// let status = Track::new(Size::Auto).content(3); // three rows of content
185/// # let _ = (sidebar, status);
186/// ```
187#[derive(Clone, Copy, Debug, PartialEq, Eq)]
188#[must_use = "builder methods return a new value; it does nothing until it is used"]
189pub struct Track {
190    pub(crate) size: Size,
191    pub(crate) min: u16,
192    pub(crate) max: u16,
193    /// The measured content, for [`Size::Auto`].
194    pub(crate) content: u16,
195}
196
197impl Track {
198    /// A track of `size`, unclamped, with no content.
199    pub fn new(size: Size) -> Track {
200        Track {
201            size,
202            min: 0,
203            max: u16::MAX,
204            content: 0,
205        }
206    }
207
208    /// Never less than `cells`, while there is room (CSS's `min-width`).
209    pub fn min(mut self, cells: u16) -> Track {
210        self.min = cells;
211        self
212    }
213
214    /// Never more than `cells`.
215    pub fn max(mut self, cells: u16) -> Track {
216        self.max = cells;
217        self
218    }
219
220    /// For [`Size::Auto`]: the cells its content needs.
221    pub fn content(mut self, cells: u16) -> Track {
222        self.content = cells;
223        self
224    }
225
226    fn clamp(&self, value: u32) -> u32 {
227        // A minimum above the maximum wins, as in CSS.
228        value.min(self.max as u32).max(self.min as u32)
229    }
230}
231
232/// The length of each track when they share `total` cells with `gap`
233/// cells between neighbours.
234pub(crate) fn solve(total: u16, gap: u16, tracks: &[Track]) -> Vec<u16> {
235    let n = tracks.len();
236    if n == 0 {
237        return Vec::new();
238    }
239    let total = (total as u32).saturating_sub(gap as u32 * (n as u32 - 1));
240    // Ratios are rounded down each, and what that loses of the group's
241    // exact total goes to the last of them, as a flexible share's
242    // remainder does.
243    let ratios: Vec<(u32, u32)> = tracks
244        .iter()
245        .filter_map(|t| match t.size {
246            Size::Ratio(a, b) => Some((a, b)),
247            _ => None,
248        })
249        .collect();
250    let mut ratio_rest = ratios_of(total, &ratios).saturating_sub(
251        ratios
252            .iter()
253            .map(|&(a, b)| ratio_of(total, a, b))
254            .sum::<u32>(),
255    );
256    let last_ratio = tracks
257        .iter()
258        .rposition(|t| matches!(t.size, Size::Ratio(..)));
259    let mut sizes: Vec<u32> = tracks
260        .iter()
261        .enumerate()
262        .map(|(i, t)| match t.size {
263            Size::Fixed(cells) => t.clamp(cells as u32),
264            Size::Percent(p) => t.clamp(total * p.min(100) as u32 / 100),
265            Size::Ratio(a, b) => {
266                let rest = if Some(i) == last_ratio {
267                    std::mem::take(&mut ratio_rest)
268                } else {
269                    0
270                };
271                t.clamp(ratio_of(total, a, b) + rest)
272            }
273            Size::Auto => t.clamp(t.content as u32),
274            Size::Flex(_) => 0,
275        })
276        .collect();
277    let mut frozen: Vec<bool> = tracks
278        .iter()
279        .map(|t| !matches!(t.size, Size::Flex(_)))
280        .collect();
281    // Share what is left by weight. A share outside its clamps is fixed at
282    // the clamp, and the rest shared again among the others.
283    loop {
284        let open: Vec<usize> = (0..n).filter(|&i| !frozen[i]).collect();
285        if open.is_empty() {
286            break;
287        }
288        let used: u32 = (0..n).filter(|&i| frozen[i]).map(|i| sizes[i]).sum();
289        let free = total.saturating_sub(used);
290        let weight = |i: usize| match tracks[i].size {
291            Size::Flex(w) => w as u32,
292            _ => 0,
293        };
294        let weights: u32 = open.iter().map(|&i| weight(i)).sum();
295        let mut shares = vec![0u32; n];
296        let mut left = free;
297        // The remainder goes to the last open track with a weight.
298        let last = open.iter().rev().copied().find(|&i| weight(i) > 0);
299        for &i in &open {
300            if weight(i) == 0 {
301                continue;
302            }
303            let share = if Some(i) == last {
304                left
305            } else {
306                free * weight(i) / weights
307            };
308            left -= share;
309            shares[i] = share;
310        }
311        // As CSS flexbox does: if clamping grows the shares overall, fix
312        // only the tracks that hit a minimum; if it shrinks them, only those
313        // that hit a maximum; otherwise all of them.
314        let fitted: Vec<u32> = (0..n).map(|i| tracks[i].clamp(shares[i])).collect();
315        let net: i64 = open
316            .iter()
317            .map(|&i| fitted[i] as i64 - shares[i] as i64)
318            .sum();
319        let mut clamped = false;
320        for &i in &open {
321            let hit = match net.signum() {
322                1 => fitted[i] > shares[i],
323                -1 => fitted[i] < shares[i],
324                _ => fitted[i] != shares[i],
325            };
326            if hit {
327                sizes[i] = fitted[i];
328                frozen[i] = true;
329                clamped = true;
330            }
331        }
332        if !clamped {
333            for &i in &open {
334                sizes[i] = shares[i];
335            }
336            break;
337        }
338    }
339    // Too much asked for: shrink from the end, to the minimums first.
340    let mut over = sizes.iter().sum::<u32>().saturating_sub(total);
341    for floor in [true, false] {
342        for i in (0..n).rev() {
343            if over == 0 {
344                break;
345            }
346            let keep = if floor { tracks[i].min as u32 } else { 0 };
347            let cut = sizes[i].saturating_sub(keep).min(over);
348            sizes[i] -= cut;
349            over -= cut;
350        }
351    }
352    sizes.into_iter().map(|s| s as u16).collect()
353}
354
355/// Grow the content ([`Size::Auto`]) tracks among `tracks[from..from + n]`
356/// so that, with the gaps between them, they hold `need` cells: what a
357/// child spanning those tracks needs. The shortfall is shared evenly among
358/// the content tracks it spans (the remainder to the last); a span with
359/// none has its size fixed by the others.
360pub(crate) fn grow_for_span(
361    tracks: &mut [Track],
362    total: u16,
363    gap: u16,
364    from: usize,
365    n: usize,
366    need: u16,
367) {
368    let span = from..(from + n).min(tracks.len());
369    let held: u32 = tracks[span.clone()]
370        .iter()
371        .map(|t| match t.size {
372            Size::Auto => t.content as u32,
373            Size::Fixed(cells) => cells as u32,
374            Size::Percent(p) => total as u32 * p.min(100) as u32 / 100,
375            Size::Ratio(a, b) => ratio_of(total as u32, a, b),
376            Size::Flex(_) => 0,
377        })
378        .sum::<u32>()
379        + gap as u32 * (span.len().saturating_sub(1)) as u32;
380    let short = (need as u32).saturating_sub(held);
381    let content: Vec<usize> = span.filter(|&i| tracks[i].size == Size::Auto).collect();
382    if short == 0 || content.is_empty() {
383        return;
384    }
385    let each = short / content.len() as u32;
386    let last = *content.last().expect("not empty");
387    for &i in &content {
388        let extra = if i == last {
389            short - each * (content.len() as u32 - 1)
390        } else {
391            each
392        };
393        tracks[i].content = (tracks[i].content as u32 + extra).min(u16::MAX as u32) as u16;
394    }
395}
396
397/// Where each track starts, from `start`, with `gap` between them.
398pub(crate) fn offsets(start: u16, gap: u16, sizes: &[u16]) -> Vec<u16> {
399    let mut at = start;
400    sizes
401        .iter()
402        .map(|&size| {
403            let here = at;
404            at = at.saturating_add(size).saturating_add(gap);
405            here
406        })
407        .collect()
408}
409
410/// Room shared out by `justify` before item `i` of `k`, when `free` cells
411/// are left over. Each share is computed from the total, so rounding never
412/// drifts: the last item of [`Justify::End`] or [`Justify::SpaceBetween`]
413/// ends exactly at the end.
414fn spread(justify: Justify, free: u32, i: u32, k: u32) -> u32 {
415    // In u64: tens of thousands of items times the free room overflow u32.
416    let (free, i, k) = (free as u64, i as u64, k as u64);
417    let share = match justify {
418        Justify::Start => 0,
419        Justify::Center => free / 2,
420        Justify::End => free,
421        Justify::SpaceBetween if k > 1 => free * i / (k - 1),
422        Justify::SpaceBetween => 0,
423        Justify::SpaceAround => free * (2 * i + 1) / (2 * k),
424        Justify::SpaceEvenly => free * (i + 1) / (k + 1),
425    };
426    share as u32
427}
428
429/// Where each of `sizes` starts along `total` cells from `start`, `gap`
430/// apart, with the room they leave placed by `justify`.
431pub(crate) fn justify_offsets(
432    start: u16,
433    total: u16,
434    gap: u16,
435    sizes: &[u16],
436    justify: Justify,
437) -> Vec<u16> {
438    let k = sizes.len() as u32;
439    let used: u64 =
440        sizes.iter().map(|&n| n as u64).sum::<u64>() + gap as u64 * k.saturating_sub(1) as u64;
441    let free = (total as u64).saturating_sub(used) as u32;
442    let mut at = start as u64;
443    sizes
444        .iter()
445        .enumerate()
446        .map(|(i, &size)| {
447            let here = at + spread(justify, free, i as u32, k) as u64;
448            at += size as u64 + gap as u64;
449            here.min(u16::MAX as u64) as u16
450        })
451        .collect()
452}
453
454/// Where an item `content` cells across sits in a line of `line` cells at
455/// `at`, and how many cells it takes: all of them for [`Align::Stretch`].
456pub(crate) fn align_cross(at: u16, line: u16, content: u16, align: Align) -> (u16, u16) {
457    let size = content.min(line);
458    let free = line - size;
459    match align {
460        Align::Stretch => (at, line),
461        Align::Start => (at, size),
462        Align::Center => (at.saturating_add(free / 2), size),
463        Align::End => (at.saturating_add(free), size),
464    }
465}
466
467/// Break items whose main sizes are `bases` into lines of at most `total`
468/// cells with `gap` between items, as CSS wraps flex items: each line takes
469/// items while they fit, and always at least one.
470pub(crate) fn break_lines(total: u16, gap: u16, bases: &[u16]) -> Vec<std::ops::Range<usize>> {
471    let mut lines = Vec::new();
472    let mut first = 0;
473    let mut used = 0u32;
474    for (i, &base) in bases.iter().enumerate() {
475        let need = if i == first {
476            base as u32
477        } else {
478            used + gap as u32 + base as u32
479        };
480        if i > first && need > total as u32 {
481            lines.push(first..i);
482            first = i;
483            used = base as u32;
484        } else {
485            used = need;
486        }
487    }
488    if first < bases.len() {
489        lines.push(first..bases.len());
490    }
491    lines
492}
493
494/// Where each line of a wrapped container starts across its axis, and how
495/// many cells it takes, given each line's own size (its tallest or widest
496/// item) and `total` cells from `start` with `gap` between lines.
497pub(crate) fn place_lines(
498    start: u16,
499    total: u16,
500    gap: u16,
501    lines: &[u16],
502    align: AlignContent,
503) -> Vec<(u16, u16)> {
504    let k = lines.len() as u32;
505    let used: u64 =
506        lines.iter().map(|&n| n as u64).sum::<u64>() + gap as u64 * k.saturating_sub(1) as u64;
507    let free = (total as u64).saturating_sub(used) as u32;
508    let justify = match align {
509        AlignContent::Start | AlignContent::Stretch => Justify::Start,
510        AlignContent::Center => Justify::Center,
511        AlignContent::End => Justify::End,
512        AlignContent::SpaceBetween => Justify::SpaceBetween,
513        AlignContent::SpaceAround => Justify::SpaceAround,
514        AlignContent::SpaceEvenly => Justify::SpaceEvenly,
515    };
516    let mut at = start as u64;
517    lines
518        .iter()
519        .enumerate()
520        .map(|(i, &size)| {
521            let i = i as u32;
522            // Stretch: line `i` grows by its share of the room, so the
523            // lines fill it exactly.
524            let grow = if align == AlignContent::Stretch {
525                let (free, i, k) = (free as u64, i as u64, k as u64);
526                (free * (i + 1) / k - free * i / k) as u32
527            } else {
528                0
529            };
530            let here = at + spread(justify, free, i, k) as u64;
531            let size = size as u32 + grow;
532            at += size as u64 + gap as u64;
533            (
534                here.min(u16::MAX as u64) as u16,
535                size.min(u16::MAX as u32) as u16,
536            )
537        })
538        .collect()
539}
540
541/// A child's place in a grid: its first column and row, and how many of
542/// each it spans.
543#[derive(Clone, Copy, Debug, PartialEq, Eq)]
544pub(crate) struct Cellspan {
545    pub column: usize,
546    pub row: usize,
547    pub columns: usize,
548    pub rows: usize,
549}
550
551/// Place children with `spans` (columns, rows) in a grid `columns` wide,
552/// row by row, each in the first free place at or after the previous
553/// child's. Returns the places and the number of rows used.
554pub(crate) fn place(columns: usize, spans: &[(u16, u16)]) -> (Vec<Cellspan>, usize) {
555    let columns = columns.max(1);
556    let mut taken: Vec<Vec<bool>> = Vec::new();
557    let (mut row, mut column) = (0usize, 0usize);
558    let mut places = Vec::with_capacity(spans.len());
559    for &(wide, tall) in spans {
560        let wide = (wide.max(1) as usize).min(columns);
561        let tall = tall.max(1) as usize;
562        loop {
563            if column + wide > columns {
564                row += 1;
565                column = 0;
566                continue;
567            }
568            let fits = (row..row + tall).all(|r| {
569                (column..column + wide).all(|c| !taken.get(r).is_some_and(|cells| cells[c]))
570            });
571            if fits {
572                break;
573            }
574            column += 1;
575        }
576        while taken.len() < row + tall {
577            taken.push(vec![false; columns]);
578        }
579        for cells in &mut taken[row..row + tall] {
580            for cell in &mut cells[column..column + wide] {
581                *cell = true;
582            }
583        }
584        places.push(Cellspan {
585            column,
586            row,
587            columns: wide,
588            rows: tall,
589        });
590        column += wide;
591    }
592    (places, taken.len())
593}
594
595/// Lay `tracks` out along `axis` in `rect`, as a [`column`](crate::column)
596/// or [`row`](crate::row) lays out its children: each by its [`Size`] within
597/// its clamps, the flexible ones sharing what is left, `flow.gap` cells
598/// apart, and what they leave over placed by `flow.justify`. Each takes the
599/// whole of `rect` across the axis (tracks have no content across it, so
600/// `flow.align_items` and wrapping do not apply). ratatui's `Layout::split`;
601/// with `flow.solver` [`Solver::Ratatui`](crate::Solver::Ratatui), solved as
602/// it solves it.
603///
604/// ```
605/// use intuituive::layout::{split, Track};
606/// use intuituive::node::Axis;
607/// use intuituive::screen::Rect;
608/// use intuituive::{Size, StackLayout};
609///
610/// let tracks = [Track::new(Size::Fixed(3)), Track::new(Size::Flex(1))];
611/// let areas = split(Rect::new(0, 0, 20, 10), Axis::Vertical, &tracks, StackLayout::default());
612/// assert_eq!(areas, [Rect::new(0, 0, 20, 3), Rect::new(0, 3, 20, 7)]);
613/// ```
614pub fn split(rect: Rect, axis: Axis, tracks: &[Track], flow: StackLayout) -> Vec<Rect> {
615    split_with_spacers(rect, axis, tracks, flow).0
616}
617
618/// [`split`], and the room around the tracks: one spacer before the first,
619/// one between each two, and one after the last (`tracks.len() + 1` in all,
620/// some of them empty), as ratatui's `Layout::split_with_spacers`. With
621/// [`Justify::SpaceBetween`] the spacers between are the gaps and the ends
622/// are empty; with [`Justify::Center`] the ends hold the room left over.
623///
624/// ```
625/// use intuituive::layout::{split_with_spacers, Track};
626/// use intuituive::node::Axis;
627/// use intuituive::screen::Rect;
628/// use intuituive::{Justify, Size, StackLayout};
629///
630/// let flow = StackLayout { justify: Justify::Center, ..StackLayout::default() };
631/// let tracks = [Track::new(Size::Fixed(4)), Track::new(Size::Fixed(4))];
632/// let (areas, spacers) = split_with_spacers(Rect::new(0, 0, 20, 1), Axis::Horizontal, &tracks, flow);
633/// assert_eq!(areas, [Rect::new(6, 0, 4, 1), Rect::new(10, 0, 4, 1)]);
634/// assert_eq!(spacers[0], Rect::new(0, 0, 6, 1));
635/// assert_eq!(spacers[2], Rect::new(14, 0, 6, 1));
636/// ```
637pub fn split_with_spacers(
638    rect: Rect,
639    axis: Axis,
640    tracks: &[Track],
641    flow: StackLayout,
642) -> (Vec<Rect>, Vec<Rect>) {
643    let (start, total, across, breadth) = match axis {
644        Axis::Vertical => (rect.y, rect.height, rect.x, rect.width),
645        Axis::Horizontal => (rect.x, rect.width, rect.y, rect.height),
646    };
647    let piece = |at: u16, length: u16| match axis {
648        Axis::Vertical => Rect::new(across, at, breadth, length),
649        Axis::Horizontal => Rect::new(at, across, length, breadth),
650    };
651    let (at, sizes): (Vec<u16>, Vec<u16>) = if flow.solver == crate::solver::Solver::Native {
652        let sizes = solve(total, flow.gap, tracks);
653        let at = justify_offsets(start, total, flow.gap, &sizes, flow.justify);
654        (at, sizes)
655    } else {
656        crate::solver::split(start, total, flow.gap, tracks, flow.solver, flow.justify)
657            .into_iter()
658            .unzip()
659    };
660    let end = start.saturating_add(total);
661    let areas: Vec<Rect> = at
662        .iter()
663        .zip(&sizes)
664        .map(|(&at, &size)| {
665            let at = at.min(end);
666            piece(at, size.min(end - at))
667        })
668        .collect();
669    // Between the end of each area (or the start) and the start of the
670    // next (or the end).
671    let mut spacers = Vec::with_capacity(areas.len() + 1);
672    let mut from = start;
673    for (&at, &size) in at.iter().zip(&sizes) {
674        let to = at.clamp(from, end);
675        spacers.push(piece(from, to - from));
676        from = at.saturating_add(size).clamp(to, end);
677    }
678    spacers.push(piece(from, end - from));
679    (areas, spacers)
680}
681
682/// What [`squarify`] lays out: a tile for each weight, in the order
683/// given (empty for a weight of nothing, or one grouped into the rest),
684/// and the tile the rest share, with how many weights it holds.
685#[derive(Clone, Debug, Default, PartialEq, Eq)]
686pub struct Squarified {
687    pub tiles: Vec<Rect>,
688    pub rest: Option<(Rect, usize)>,
689}
690
691/// A treemap of `rect`: a tile for each of `weights`, its area in
692/// proportion to its weight, laid out by the squarified algorithm (Bruls,
693/// Huizing and van Wijk) so tiles are as near square as the weights let
694/// them be. `aspect` is a cell's height over its width (about 2 on most
695/// terminals), so a tile looks square rather than counts square. A tile
696/// is never smaller than `min` (columns, rows): the smallest weights that
697/// would make one are grouped into one tile for the rest, as large as
698/// they are together (and twice the smallest tile at least, or larger
699/// still where its share would be a strip too thin for a tile), when
700/// there is a rest at all. Tiles cover `rect` exactly, edge to edge.
701///
702/// ```
703/// use intuituive::layout::squarify;
704/// use intuituive::screen::Rect;
705///
706/// let area = Rect::new(0, 0, 20, 10);
707/// let map = squarify(area, &[6.0, 3.0, 1.0], (1, 1), 1.0);
708/// assert_eq!(map.tiles[0], Rect::new(0, 0, 12, 10));
709/// assert_eq!(map.tiles[1], Rect::new(12, 0, 8, 8));
710/// assert_eq!(map.tiles[2], Rect::new(12, 8, 8, 2));
711/// assert_eq!(map.rest, None);
712///
713/// // With tiles of 8 x 3 at least, the two smallest share the rest.
714/// let map = squarify(area, &[6.0, 3.0, 0.4, 0.4], (8, 3), 2.0);
715/// assert_eq!(map.tiles[0], Rect::new(0, 0, 11, 10));
716/// assert_eq!(map.tiles[1], Rect::new(11, 0, 9, 6));
717/// assert!(map.tiles[2].is_empty() && map.tiles[3].is_empty());
718/// assert_eq!(map.rest, Some((Rect::new(11, 6, 9, 4), 2)));
719/// ```
720pub fn squarify(rect: Rect, weights: &[f64], min: (u16, u16), aspect: f64) -> Squarified {
721    let mut out = Squarified {
722        tiles: vec![Rect::default(); weights.len()],
723        rest: None,
724    };
725    let mut order: Vec<usize> = (0..weights.len())
726        .filter(|&i| weights[i].is_finite() && weights[i] > 0.0)
727        .collect();
728    order.sort_by(|&a, &b| weights[b].total_cmp(&weights[a]));
729    let total: f64 = order.iter().map(|&i| weights[i]).sum();
730    if rect.is_empty() || order.is_empty() || total <= 0.0 {
731        return out;
732    }
733    let aspect = if aspect.is_finite() && aspect > 0.0 {
734        aspect
735    } else {
736        1.0
737    };
738    let fits = |r: Rect| r.width >= min.0.max(1) && r.height >= min.1.max(1);
739    // Those whose own share of the area could make a tile, at most.
740    let cells = rect.width as f64 * rect.height as f64;
741    let least = min.0.max(1) as f64 * min.1.max(1) as f64;
742    let mut k = order
743        .iter()
744        .take_while(|&&i| weights[i] / total * cells >= least)
745        .count();
746    if k == order.len() {
747        let tiles = squarify_tiles(rect, &order, weights, None, aspect);
748        if tiles.iter().all(|(_, r)| fits(*r)) {
749            for (i, r) in tiles {
750                out.tiles[i] = r;
751            }
752            return out;
753        }
754        k -= 1;
755    }
756    // The first `k`, and the rest grouped, smaller until all fit.
757    loop {
758        // The rest takes room for a tile at least: twice the smallest.
759        let rest: f64 = order[k..].iter().map(|&i| weights[i]).sum();
760        let mut rest = rest.max(total * 2.0 * least / cells);
761        let mut tiles = squarify_tiles(rect, &order[..k], weights, Some(rest), aspect);
762        // A rest too thin for a tile (a strip beside one large item) is
763        // grown until it fits, while the items' tiles still do.
764        for _ in 0..8 {
765            let Some(&(_, r)) = tiles.iter().find(|(i, _)| *i == usize::MAX) else {
766                break;
767            };
768            if k == 0 || fits(r) || !tiles.iter().all(|(i, r)| *i == usize::MAX || fits(*r)) {
769                break;
770            }
771            let grow = |have: u16, want: u16| want.max(1) as f64 / have.max(1) as f64;
772            let by = grow(r.width, min.0).max(grow(r.height, min.1)).max(1.1);
773            rest *= by;
774            tiles = squarify_tiles(rect, &order[..k], weights, Some(rest), aspect);
775        }
776        if k == 0 || tiles.iter().all(|(_, r)| fits(*r)) {
777            for (i, r) in tiles {
778                match i {
779                    usize::MAX => out.rest = Some((r, order.len() - k)),
780                    i => out.tiles[i] = r,
781                }
782            }
783            return out;
784        }
785        k -= 1;
786    }
787}
788
789/// [`squarify`]'s tiles for `order`'s weights, with a last tile of
790/// `rest`'s weight (numbered `usize::MAX`), in cells.
791fn squarify_tiles(
792    rect: Rect,
793    order: &[usize],
794    weights: &[f64],
795    rest: Option<f64>,
796    aspect: f64,
797) -> Vec<(usize, Rect)> {
798    let mut items: Vec<(usize, f64)> = order.iter().map(|&i| (i, weights[i])).collect();
799    if let Some(rest) = rest.filter(|r| *r > 0.0) {
800        items.push((usize::MAX, rest));
801    }
802    // Laid out where a cell is `aspect` units tall, so tiles look square.
803    let (width, height) = (rect.width as f64, rect.height as f64 * aspect);
804    let total: f64 = items.iter().map(|(_, w)| w).sum();
805    let scale = width * height / total;
806    let areas: Vec<(usize, f64)> = items.iter().map(|&(i, w)| (i, w * scale)).collect();
807    let mut free = (0.0, 0.0, width, height);
808    let mut placed: Vec<(usize, (f64, f64, f64, f64))> = Vec::new();
809    // The worst ratio of a strip of `areas` along a side `side` long.
810    let worst = |row: &[(usize, f64)], side: f64| -> f64 {
811        let sum: f64 = row.iter().map(|(_, a)| a).sum();
812        row.iter()
813            .map(|(_, a)| {
814                let along = side * side * a / (sum * sum);
815                along.max(1.0 / along)
816            })
817            .fold(0.0, f64::max)
818    };
819    let mut lay = |row: &[(usize, f64)], free: &mut (f64, f64, f64, f64)| {
820        let (x, y, w, h) = *free;
821        let sum: f64 = row.iter().map(|(_, a)| a).sum();
822        if w >= h {
823            // A column down the left.
824            let across = if h > 0.0 { sum / h } else { 0.0 };
825            let mut at = y;
826            for &(i, a) in row {
827                let down = if across > 0.0 { a / across } else { 0.0 };
828                placed.push((i, (x, at, across, down)));
829                at += down;
830            }
831            *free = (x + across, y, (w - across).max(0.0), h);
832        } else {
833            // A row along the top.
834            let down = if w > 0.0 { sum / w } else { 0.0 };
835            let mut at = x;
836            for &(i, a) in row {
837                let across = if down > 0.0 { a / down } else { 0.0 };
838                placed.push((i, (at, y, across, down)));
839                at += across;
840            }
841            *free = (x, y + down, w, (h - down).max(0.0));
842        }
843    };
844    let mut row: Vec<(usize, f64)> = Vec::new();
845    for &item in &areas {
846        let side = free.2.min(free.3);
847        let mut longer = row.clone();
848        longer.push(item);
849        if row.is_empty() || worst(&longer, side) <= worst(&row, side) {
850            row = longer;
851        } else {
852            lay(&row, &mut free);
853            row = vec![item];
854        }
855    }
856    if !row.is_empty() {
857        lay(&row, &mut free);
858    }
859    // To cells: each edge rounded, so neighbours meet exactly.
860    let x_of = |x: f64| {
861        (rect.x as f64 + x)
862            .round()
863            .clamp(rect.x as f64, rect.right() as f64) as u16
864    };
865    let y_of = |y: f64| {
866        (rect.y as f64 + y / aspect)
867            .round()
868            .clamp(rect.y as f64, rect.bottom() as f64) as u16
869    };
870    placed
871        .into_iter()
872        .map(|(i, (x, y, w, h))| {
873            let (left, top) = (x_of(x), y_of(y));
874            let (right, bottom) = (x_of(x + w), y_of(y + h));
875            (i, Rect::new(left, top, right - left, bottom - top))
876        })
877        .collect()
878}
879
880#[cfg(test)]
881mod tests {
882    use super::*;
883
884    fn track(size: Size) -> Track {
885        Track::new(size)
886    }
887
888    #[test]
889    fn three_thirds_fill_the_room_exactly() {
890        let third = track(Size::Ratio(1, 3));
891        assert_eq!(solve(100, 0, &[third, third, third]), vec![33, 33, 34]);
892        // Of the room after gaps, as a percentage is: 98 cells.
893        assert_eq!(solve(100, 1, &[third, third, third]), vec![32, 32, 34]);
894        // A zero denominator is no room; over one is the whole parent.
895        assert_eq!(solve(10, 0, &[track(Size::Ratio(1, 0))]), vec![0]);
896        assert_eq!(solve(10, 0, &[track(Size::Ratio(3, 2))]), vec![10]);
897    }
898
899    #[test]
900    fn ratios_mix_with_fixed_and_flexible_tracks() {
901        let tracks = [
902            track(Size::Fixed(10)),
903            track(Size::Ratio(1, 4)),
904            track(Size::Flex(1)),
905            track(Size::Ratio(1, 4)),
906        ];
907        // 25 each of 100, and the flexible track takes what is left.
908        assert_eq!(solve(100, 0, &tracks), vec![10, 25, 40, 25]);
909        // 1/3 and 1/6 of 50: 16 and 8, and the half-cell lost to rounding
910        // goes to the last ratio (25 together).
911        let tracks = [track(Size::Ratio(1, 3)), track(Size::Ratio(1, 6))];
912        assert_eq!(solve(50, 0, &tracks), vec![16, 9]);
913        let mut capped = track(Size::Ratio(1, 2));
914        capped.max = 10;
915        assert_eq!(solve(40, 0, &[capped, track(Size::Flex(1))]), vec![10, 30]);
916    }
917
918    #[test]
919    fn spacers_are_the_room_between_and_around() {
920        let rect = Rect::new(0, 0, 20, 1);
921        let tracks = [track(Size::Fixed(4)), track(Size::Fixed(6))];
922        let flow = |justify| StackLayout {
923            justify,
924            ..StackLayout::default()
925        };
926        let (areas, spacers) =
927            split_with_spacers(rect, Axis::Horizontal, &tracks, flow(Justify::SpaceBetween));
928        assert_eq!(areas, [Rect::new(0, 0, 4, 1), Rect::new(14, 0, 6, 1)]);
929        // The ends are empty, and the gap is the room between.
930        assert_eq!(
931            spacers,
932            [
933                Rect::new(0, 0, 0, 1),
934                Rect::new(4, 0, 10, 1),
935                Rect::new(20, 0, 0, 1)
936            ]
937        );
938        let (areas, spacers) =
939            split_with_spacers(rect, Axis::Horizontal, &tracks, flow(Justify::Center));
940        assert_eq!(areas, [Rect::new(5, 0, 4, 1), Rect::new(9, 0, 6, 1)]);
941        assert_eq!(
942            spacers,
943            [
944                Rect::new(0, 0, 5, 1),
945                Rect::new(9, 0, 0, 1),
946                Rect::new(15, 0, 5, 1)
947            ]
948        );
949        // A gap is a spacer too, and down the axis the spacers are rows.
950        let flow = StackLayout {
951            gap: 1,
952            ..StackLayout::default()
953        };
954        let (areas, spacers) = split_with_spacers(
955            Rect::new(2, 1, 5, 10),
956            Axis::Vertical,
957            &[track(Size::Fixed(2)), track(Size::Flex(1))],
958            flow,
959        );
960        assert_eq!(areas, [Rect::new(2, 1, 5, 2), Rect::new(2, 4, 5, 7)]);
961        assert_eq!(spacers[1], Rect::new(2, 3, 5, 1));
962        assert_eq!(spacers[2], Rect::new(2, 11, 5, 0));
963        // No tracks: one spacer, the whole rectangle.
964        assert_eq!(
965            split_with_spacers(rect, Axis::Horizontal, &[], flow).1,
966            [rect]
967        );
968    }
969
970    #[test]
971    fn a_grid_of_ratio_tracks_shares_its_width() {
972        let third = track(Size::Ratio(1, 3));
973        // Three columns, a gap of 2 between: 56 of 60, 18, 18 and 20.
974        assert_eq!(solve(60, 2, &[third, third, third]), vec![18, 18, 20]);
975    }
976
977    #[test]
978    fn flex_shares_what_fixed_and_percent_leave() {
979        let tracks = [
980            track(Size::Fixed(10)),
981            track(Size::Percent(50)),
982            track(Size::Flex(1)),
983            track(Size::Flex(3)),
984        ];
985        assert_eq!(solve(100, 0, &tracks), vec![10, 50, 10, 30]);
986    }
987
988    #[test]
989    fn gaps_come_out_of_the_total() {
990        let tracks = [track(Size::Flex(1)), track(Size::Flex(1))];
991        assert_eq!(solve(21, 1, &tracks), vec![10, 10]);
992    }
993
994    #[test]
995    fn a_clamped_flex_track_gives_its_share_to_the_others() {
996        let mut capped = track(Size::Flex(1));
997        capped.max = 5;
998        let tracks = [capped, track(Size::Flex(1))];
999        assert_eq!(solve(30, 0, &tracks), vec![5, 25]);
1000        let mut floor = track(Size::Flex(1));
1001        floor.min = 20;
1002        let tracks = [floor, track(Size::Flex(1))];
1003        assert_eq!(solve(30, 0, &tracks), vec![20, 10]);
1004    }
1005
1006    #[test]
1007    fn overflow_shrinks_from_the_end_to_the_minimums_first() {
1008        let mut first = track(Size::Fixed(10));
1009        first.min = 4;
1010        let mut second = track(Size::Fixed(10));
1011        second.min = 8;
1012        assert_eq!(solve(16, 0, &[first, second]), vec![8, 8]);
1013        assert_eq!(solve(10, 0, &[first, second]), vec![4, 6]);
1014        assert_eq!(solve(4, 0, &[first, second]), vec![4, 0]);
1015    }
1016
1017    #[test]
1018    fn auto_takes_its_content() {
1019        let mut auto = track(Size::Auto);
1020        auto.content = 3;
1021        assert_eq!(solve(10, 0, &[auto, track(Size::Flex(1))]), vec![3, 7]);
1022    }
1023
1024    #[test]
1025    fn a_span_grows_the_content_tracks_it_covers() {
1026        let mut tracks = [track(Size::Auto), track(Size::Fixed(2)), track(Size::Auto)];
1027        tracks[0].content = 1;
1028        // Holds 1 + 2 + 0 + two gaps = 5; needs 10: 5 more, shared 2 and 3.
1029        grow_for_span(&mut tracks, 20, 1, 0, 3, 10);
1030        assert_eq!((tracks[0].content, tracks[2].content), (3, 3));
1031        // Already enough: nothing changes.
1032        grow_for_span(&mut tracks, 20, 1, 0, 2, 4);
1033        assert_eq!(tracks[0].content, 3);
1034    }
1035
1036    // The expected positions are those CSS flexbox computes for the same
1037    // boxes, rounded down to whole cells: a 20-cell container, items 4 and 6
1038    // cells wide.
1039
1040    #[test]
1041    fn justify_places_what_the_items_leave() {
1042        let at = |j| justify_offsets(0, 20, 0, &[4, 6], j);
1043        assert_eq!(at(Justify::Start), [0, 4]);
1044        assert_eq!(at(Justify::Center), [5, 9]);
1045        assert_eq!(at(Justify::End), [10, 14]);
1046        assert_eq!(at(Justify::SpaceBetween), [0, 14]);
1047        // 10 free: 2.5 at each end, 5 between (CSS: 2.5 and 11.5).
1048        assert_eq!(at(Justify::SpaceAround), [2, 11]);
1049        // 10 free: 3.33 at each end and between (CSS: 3.33 and 10.67).
1050        assert_eq!(at(Justify::SpaceEvenly), [3, 10]);
1051    }
1052
1053    #[test]
1054    fn justify_counts_gaps_and_never_overflows() {
1055        assert_eq!(
1056            justify_offsets(2, 20, 1, &[4, 6], Justify::End),
1057            [11, 16],
1058            "ends at 2 + 20"
1059        );
1060        // No room left: every justify packs at the start.
1061        for j in [Justify::Center, Justify::End, Justify::SpaceEvenly] {
1062            assert_eq!(justify_offsets(0, 10, 0, &[4, 6], j), [0, 4]);
1063        }
1064        // One item between: CSS puts it at the start.
1065        assert_eq!(justify_offsets(0, 10, 0, &[4], Justify::SpaceBetween), [0]);
1066    }
1067
1068    #[test]
1069    fn items_align_across_the_line() {
1070        assert_eq!(align_cross(1, 10, 4, Align::Start), (1, 4));
1071        assert_eq!(align_cross(1, 10, 4, Align::Center), (4, 4));
1072        assert_eq!(align_cross(1, 10, 4, Align::End), (7, 4));
1073        assert_eq!(align_cross(1, 10, 4, Align::Stretch), (1, 10));
1074        // Wider than the line: cut to it.
1075        assert_eq!(align_cross(0, 3, 8, Align::Center), (0, 3));
1076    }
1077
1078    #[test]
1079    fn lines_break_where_the_next_item_does_not_fit() {
1080        assert_eq!(
1081            break_lines(10, 1, &[4, 4, 4, 9, 11]),
1082            [0..2, 2..3, 3..4, 4..5]
1083        );
1084        // Exactly full: 4 + 1 + 5 = 10.
1085        assert_eq!(break_lines(10, 1, &[4, 5, 1]), [0..2, 2..3]);
1086        assert!(break_lines(10, 1, &[]).is_empty());
1087    }
1088
1089    #[test]
1090    fn align_content_places_the_lines() {
1091        // Two lines 2 and 3 tall in 10 cells, gap 1: 4 left over.
1092        let lines = |a| place_lines(0, 10, 1, &[2, 3], a);
1093        assert_eq!(lines(AlignContent::Start), [(0, 2), (3, 3)]);
1094        assert_eq!(lines(AlignContent::End), [(4, 2), (7, 3)]);
1095        assert_eq!(lines(AlignContent::Center), [(2, 2), (5, 3)]);
1096        assert_eq!(lines(AlignContent::SpaceBetween), [(0, 2), (7, 3)]);
1097        // Stretch: each line grows by 2 and they fill the 10 cells.
1098        assert_eq!(lines(AlignContent::Stretch), [(0, 4), (5, 5)]);
1099    }
1100
1101    #[test]
1102    fn places_flow_row_by_row_around_spans() {
1103        let (places, rows) = place(3, &[(2, 1), (1, 2), (1, 1), (1, 1), (3, 1)]);
1104        let at: Vec<(usize, usize)> = places.iter().map(|p| (p.column, p.row)).collect();
1105        assert_eq!(at, vec![(0, 0), (2, 0), (0, 1), (1, 1), (0, 2)]);
1106        assert_eq!(rows, 3);
1107    }
1108}