quvyta-framework 0.1.24

A Rust framework for building terminal applications
Documentation
//! Rows, columns and stacks.

use super::context::{MeasureCx, PaintCx};
use super::place::{Placed, with_spill};
use super::wrap::{self, Piece};
use super::{Align, LayoutProps, Length, Node, Widget};
use crate::geometry::{Rect, Size, clamp_u16};

/// The direction a container lays its children out in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Axis {
    Row,
    Column,
    Stack,
}

/// A container of child nodes.
pub(crate) struct Flex<Msg> {
    axis: Axis,
    children: Vec<Node<Msg>>,
    /// Whether a row moves children that do not fit to the next line.
    wrap: bool,
    /// Empty rows between the lines of a wrapping row.
    line_gap: u16,
}

impl<Msg> Flex<Msg> {
    pub(crate) fn new(axis: Axis, children: Vec<Node<Msg>>) -> Self {
        Self { axis, children, wrap: false, line_gap: 0 }
    }

    /// Makes a row move children that do not fit to the next line.
    pub(crate) fn set_wrap(&mut self, wrap: bool) {
        self.wrap = wrap;
    }

    /// Leaves `rows` empty rows between the lines of a wrapping row.
    pub(crate) fn set_line_gap(&mut self, rows: u16) {
        self.line_gap = rows;
    }
}

/// Main-axis and cross-axis extent of a size for `axis`.
fn split(axis: Axis, size: Size) -> (u16, u16) {
    match axis {
        Axis::Row => (size.width, size.height),
        Axis::Column | Axis::Stack => (size.height, size.width),
    }
}

fn join(axis: Axis, main: u16, cross: u16) -> Size {
    match axis {
        Axis::Row => Size::new(main, cross),
        Axis::Column | Axis::Stack => Size::new(cross, main),
    }
}

fn lengths(axis: Axis, layout: LayoutProps) -> (Length, Length) {
    match axis {
        Axis::Row => (layout.width, layout.height),
        Axis::Column | Axis::Stack => (layout.height, layout.width),
    }
}

fn offset(align: Align, free: u16) -> u16 {
    match align {
        Align::Start => 0,
        Align::Center => free / 2,
        Align::End => free,
    }
}

/// The cells `gap` takes between `count` children.
fn gaps(count: usize, gap: u16) -> u16 {
    gap.saturating_mul(clamp_u16(i32::try_from(count).unwrap_or(i32::MAX) - 1))
}

fn is_fill<Msg>(axis: Axis, child: &Node<Msg>) -> bool {
    matches!(lengths(axis, child.layout).0, Length::Fill(_))
}

impl<Msg: 'static> Flex<Msg> {
    /// Cross-axis extent available to a child given its cross length.
    fn cross_available(cross: Length, available: u16) -> u16 {
        match cross {
            Length::Cells(cells) => cells.min(available),
            Length::Auto | Length::Fill(_) => available,
        }
    }

    /// Sizes along the main axis of `children`, laid out in a run of `main` cells.
    fn main_sizes(
        &self,
        children: &[&Node<Msg>],
        measure: &mut dyn FnMut(&Node<Msg>, Size) -> Size,
        main: u16,
        cross: u16,
        gap: u16,
    ) -> Vec<u16> {
        let mut sizes = vec![0u16; children.len()];
        let mut used = gaps(children.len(), gap);
        let mut weights = 0u32;
        for (i, child) in children.iter().enumerate() {
            let (main_len, cross_len) = lengths(self.axis, child.layout);
            match main_len {
                Length::Cells(cells) => sizes[i] = cells,
                Length::Auto => {
                    let available = join(self.axis, main.saturating_sub(used), Self::cross_available(cross_len, cross));
                    sizes[i] = split(self.axis, measure(child, available)).0;
                }
                Length::Fill(weight) => weights += u32::from(weight.max(1)),
            }
            if !matches!(main_len, Length::Fill(_)) {
                used = used.saturating_add(sizes[i]);
            }
        }
        let remaining = u32::from(main.saturating_sub(used));
        let mut given = 0u32;
        let mut last_fill = None;
        for (i, child) in children.iter().enumerate() {
            if let (Length::Fill(weight), _) = lengths(self.axis, child.layout)
                && let Some(share) = (remaining * u32::from(weight.max(1))).checked_div(weights)
            {
                sizes[i] = u16::try_from(share).unwrap_or(u16::MAX);
                given += share;
                last_fill = Some(i);
            }
        }
        if let Some(i) = last_fill {
            let rest = u16::try_from(remaining - given).unwrap_or(0);
            sizes[i] = sizes[i].saturating_add(rest);
        }
        sizes
    }

    /// The main and cross extent `children` want side by side along the axis.
    fn measure_run(&self, cx: &mut MeasureCx<'_>, children: &[&Node<Msg>], available: Size, gap: u16) -> (u16, u16) {
        let (main_avail, cross_avail) = split(self.axis, available);
        let gaps = gaps(children.len(), gap);
        let mut main_total = gaps;
        let mut cross_max = 0u16;
        let mut remaining = main_avail.saturating_sub(gaps);
        // Sized children first and filling ones after, with what the others leave, as paint lays
        // them out: a filling child measured first would take the room of the siblings after it,
        // and a sibling squeezed to nothing may wrap to many lines and make the row tall.
        let sized = children.iter().filter(|child| !is_fill(self.axis, child));
        let filling = children.iter().filter(|child| is_fill(self.axis, child));
        for (fill, child) in sized.map(|child| (false, child)).chain(filling.map(|child| (true, child))) {
            let (main_len, cross_len) = lengths(self.axis, child.layout);
            // A child with a width of its own is measured at that width, the one paint gives it;
            // measured at the whole room, text inside it wraps to fewer lines than it is drawn in.
            let main_room = match main_len {
                Length::Cells(cells) => cells.min(remaining),
                Length::Auto | Length::Fill(_) => remaining,
            };
            let child_avail = join(self.axis, main_room, Self::cross_available(cross_len, cross_avail));
            let measured = split(self.axis, cx.measure_child(child, child_avail));
            let main_size = match main_len {
                Length::Cells(cells) => cells.min(remaining),
                Length::Auto | Length::Fill(_) => measured.0,
            };
            let cross_size = match cross_len {
                Length::Cells(cells) => cells.min(cross_avail),
                Length::Auto | Length::Fill(_) => measured.1,
            };
            main_total = main_total.saturating_add(main_size);
            if !fill {
                remaining = remaining.saturating_sub(main_size);
            }
            cross_max = cross_max.max(cross_size);
        }
        (main_total.min(main_avail), cross_max)
    }

    /// Paints `children` side by side along the axis in `area`.
    fn paint_run(&self, cx: &mut PaintCx<'_>, children: &[&Node<Msg>], area: Rect, layout: LayoutProps) {
        let (main, cross) = split(self.axis, area.size());
        let sizes = {
            let mut measure = |node: &Node<Msg>, available: Size| cx.measure_child(node, available);
            self.main_sizes(children, &mut measure, main, cross, layout.gap)
        };
        let total = sizes.iter().fold(gaps(children.len(), layout.gap), |sum, size| sum.saturating_add(*size));
        let mut position = i32::from(offset(layout.justify, main.saturating_sub(total)));
        for (child, main_size) in children.iter().zip(sizes) {
            let (_, cross_len) = lengths(self.axis, child.layout);
            let cross_size = match cross_len {
                Length::Fill(_) => cross,
                Length::Cells(cells) => cells.min(cross),
                Length::Auto => {
                    let available = join(self.axis, main_size, cross);
                    split(self.axis, cx.measure_child(child, available)).1
                }
            };
            let cross_offset = i32::from(offset(layout.align, cross - cross_size));
            let rect = match self.axis {
                Axis::Row => Rect::new(area.x + position, area.y + cross_offset, main_size, cross_size),
                Axis::Column | Axis::Stack => {
                    Rect::new(area.x + cross_offset, area.y + position, cross_size, main_size)
                }
            };
            cx.paint_child(child, rect);
            position += i32::from(main_size) + i32::from(layout.gap);
        }
    }

    /// The lines of a wrapping row `available.width` cells wide, or `None` when every child
    /// fits on one line and the row lays out as any other.
    fn lines(
        &self,
        measure: &mut dyn FnMut(&Node<Msg>, Size) -> Size,
        available: Size,
        gap: u16,
    ) -> Option<Vec<Vec<&Node<Msg>>>> {
        if !self.wrap || self.axis != Axis::Row {
            return None;
        }
        let pieces: Vec<Piece> = self
            .children
            .iter()
            .map(|child| {
                let width = match child.layout.width {
                    Length::Cells(cells) => cells,
                    Length::Auto | Length::Fill(_) => {
                        let height = Self::cross_available(child.layout.height, available.height);
                        measure(child, Size::new(available.width, height)).width
                    }
                };
                Piece { width: width.min(available.width), spacer: width == 0 && is_fill(self.axis, child) }
            })
            .collect();
        let lines = wrap::break_lines(&pieces, available.width, gap);
        (lines.len() > 1)
            .then(|| lines.into_iter().map(|line| line.into_iter().map(|i| &self.children[i]).collect()).collect())
    }
}

impl<Msg: 'static> Widget<Msg> for Flex<Msg> {
    fn measure(&self, cx: &mut MeasureCx<'_>, available: Size) -> Size {
        let layout = cx.layout();
        if self.axis == Axis::Stack {
            let mut size = Size::default();
            for child in &self.children {
                // A placed child reaches as far as its rectangle does, from the stack's corner.
                let child_size = match Placed::of(child) {
                    Some(rect) => Size::new(
                        clamp_u16(rect.right().max(0)).min(available.width),
                        clamp_u16(rect.bottom().max(0)).min(available.height),
                    ),
                    None => cx.measure_child(child, available),
                };
                size = Size::new(size.width.max(child_size.width), size.height.max(child_size.height));
            }
            return size;
        }
        let lines = {
            let mut measure = |node: &Node<Msg>, available: Size| cx.measure_child(node, available);
            self.lines(&mut measure, available, layout.gap)
        };
        if let Some(lines) = lines {
            let (mut width, mut height) = (0u16, 0u16);
            for (i, line) in lines.iter().enumerate() {
                let (line_width, line_height) = self.measure_run(cx, line, available, layout.gap);
                width = width.max(line_width);
                let before = if i == 0 { 0 } else { self.line_gap };
                height = height.saturating_add(before).saturating_add(line_height);
            }
            return Size::new(width, height.min(available.height));
        }
        let children: Vec<&Node<Msg>> = self.children.iter().collect();
        let (main, cross) = self.measure_run(cx, &children, available, layout.gap);
        join(self.axis, main, cross)
    }

    fn paint(&self, cx: &mut PaintCx<'_>, area: Rect) {
        let layout = cx.layout();
        if self.axis == Axis::Stack {
            for child in &self.children {
                if let Some(placed) = Placed::of(child) {
                    let rect = Rect::new(area.x + placed.x, area.y + placed.y, placed.width, placed.height);
                    cx.paint_child_spilling(child, rect, with_spill(rect));
                    continue;
                }
                let measured = cx.measure_child(child, area.size());
                let width = match child.layout.width {
                    Length::Fill(_) => area.width,
                    Length::Cells(cells) => cells.min(area.width),
                    Length::Auto => measured.width,
                };
                let height = match child.layout.height {
                    Length::Fill(_) => area.height,
                    Length::Cells(cells) => cells.min(area.height),
                    Length::Auto => measured.height,
                };
                let rect = Rect::new(
                    area.x + i32::from(offset(layout.align, area.width - width)),
                    area.y + i32::from(offset(layout.justify, area.height - height)),
                    width,
                    height,
                );
                cx.paint_child(child, rect);
            }
            return;
        }
        let lines = {
            let mut measure = |node: &Node<Msg>, available: Size| cx.measure_child(node, available);
            self.lines(&mut measure, area.size(), layout.gap)
        };
        if let Some(lines) = lines {
            let mut top = area.y;
            for line in &lines {
                let room = clamp_u16(area.bottom() - top);
                let height = {
                    let mut measure_cx = MeasureCx::for_frame(cx.env, &mut cx.frame.measures);
                    self.measure_run(&mut measure_cx, line, Size::new(area.width, room), layout.gap).1
                };
                self.paint_run(cx, line, Rect::new(area.x, top, area.width, height.min(room)), layout);
                top += i32::from(height) + i32::from(self.line_gap);
            }
            return;
        }
        let children: Vec<&Node<Msg>> = self.children.iter().collect();
        self.paint_run(cx, &children, area, layout);
    }

    fn children(&self) -> &[Node<Msg>] {
        &self.children
    }

    fn children_mut(&mut self) -> &mut [Node<Msg>] {
        &mut self.children
    }
}