layout_engine 0.7.0

A small project to mimic css flexbox and css grid
Documentation
use crate::{
    layout::{LayoutInfo, Rect, Vec2},
    Alignment, Layout, LayoutExt, Orientation,
};

/// These are properties which each flexbox item will have.
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Props {
    /// Expand?
    ///
    /// The number refers to the ratio of remaining space it will get.
    pub expand: usize,
    /// How to align this
    pub align: Alignment,
}

impl Default for Props {
    fn default() -> Self {
        Self::new()
    }
}

impl Props {
    /// Creates a new props, with 0 expand and aligned to
    /// take up all the space given to it.
    pub const fn new() -> Self {
        Self {
            expand: 0,
            align: Alignment::Expand,
        }
    }
}

/// A trait for items which can be used with a flexbox.
///
/// All items are required to have [`Layout`] and to have
/// [`Props`]
pub trait FlexLayout: Layout {
    fn props(&self) -> &Props;
}

/// Positions items in a flexbox layout.
///
/// # Parameters
///
///  - `rect`: the space the flexbox has available
///  - `should_wrap`: should the flexbox wrap if not enough space
///  - `minimum`: don't bother with naturual sizing
///  - `expand`: should the flexbox take up the max space?
///  - `row_gap`: what is the gap between rows?
///  - `column_gap`: what is the gap between columns?
///  - `orientation`: is this horizontal or vertical
///  - `items`: the [`FlexLayout`] to align.
///  - `expand_in_row`: should items within a row be expanded?
pub fn layout_flexbox(
    rect: Rect,
    should_wrap: bool,
    minimum: bool,
    expand: bool,
    row_gap: usize,
    column_gap: usize,
    orientation: Orientation,
    items: &[impl FlexLayout],
) -> Vec<Rect> {
    // Get the wrapping information (which widgets
    // must be on seperate rows.)
    let mut wrap = if should_wrap {
        wrap(rect.size, orientation, row_gap, column_gap, items, minimum)
    } else {
        vec![0]
    };
    let wrapped_len = wrap.len();
    wrap.push(items.len()); // We push this to make the windows work

    // Work out the minimum sub-size of each row
    let mut wrap_row_length = Vec::with_capacity(wrapped_len);
    let mut sub_size_accumulate = 0;
    for window in wrap.windows(2) {
        // Get the items on this row
        let start = window[0];
        let end = window[1];
        let items = &items[start..end];

        // Calculate the minimum rows sub size.
        // This represents the size in the non primary
        // direction, i.e. if it was a Vertical flexbox
        // it would be its width.
        let sub_size: usize = items
            .iter()
            .map(|x| {
                x.sub_for_prim(
                    rect.size.in_orientation(orientation),
                    orientation,
                )
                .minimum
            })
            .max()
            .unwrap_or(1)
            .min(
                rect.size
                    .in_orientation(!orientation)
                    .saturating_sub(sub_size_accumulate),
            )
            .max(1);
        sub_size_accumulate += sub_size;
        wrap_row_length.push(sub_size);
    }

    // Convert this to the naturual sub-size (if possible)
    if !minimum {
        for (sub_size, window) in
            wrap_row_length.iter_mut().zip(wrap.windows(2))
        {
            // Get the items on this row
            let start = window[0];
            let end = window[1];
            let items = &items[start..end];

            sub_size_accumulate -= *sub_size;

            let new_sub_size: usize = items
                .iter()
                .map(|x| {
                    x.sub_for_prim(
                        rect.size.in_orientation(orientation),
                        orientation,
                    )
                    .natural
                })
                .max()
                .unwrap_or(1)
                .min(
                    rect.size
                        .in_orientation(!orientation)
                        .saturating_sub(sub_size_accumulate),
                )
                .max(1);
            sub_size_accumulate += new_sub_size;
            *sub_size = new_sub_size;
        }
    }

    // Add free space to row
    if expand {
        let rect_sub_size = rect.size.in_orientation(!orientation);
        let free_space = rect_sub_size.saturating_sub(sub_size_accumulate);
        let div_iter = divide_integer(free_space, wrapped_len);
        for (wrap_row_item, n) in wrap_row_length.iter_mut().zip(div_iter) {
            *wrap_row_item += n;
        }
    }

    // This is the vec to store the final layout information
    let mut vec = Vec::with_capacity(items.len());

    // This determines the amount of
    // space that must be left before the widget
    // in the sub-direction, i.e. the space of the
    // previous rows.
    let mut wrap_accumulate = 0;

    // Now calculate each row independently
    let mut sub_size_accumulate = 0;
    for (sub_size, window) in wrap_row_length.iter().zip(wrap.windows(2)) {
        // Get the items on this row
        let start = window[0];
        let end = window[1];
        let items = &items[start..end];

        // Get the size in the both directions
        let size = match orientation {
            Orientation::Vertical => Vec2::new(*sub_size, rect.size.y),
            Orientation::Horizontal => Vec2::new(rect.size.x, *sub_size),
        };
        sub_size_accumulate += sub_size;

        // Gets the padding (i.e. row_gap or column_gap)
        // in the primary direction.
        let padding = match orientation {
            Orientation::Vertical => row_gap,
            Orientation::Horizontal => column_gap,
        };

        // Get the layout information for this row
        let layout = layout_flexbox_no_wrap(
            size,
            padding,
            orientation,
            items,
            minimum,
            expand,
        );

        // Now produce the absolute layout for each item.
        for item_li in layout {
            //  Push the absolute layout
            vec.push(match orientation {
                Orientation::Vertical => Rect::new(
                    rect.start.x + wrap_accumulate,
                    rect.start.y + item_li.start,
                    *sub_size,
                    item_li.end - item_li.start,
                ),
                Orientation::Horizontal => Rect::new(
                    rect.start.x + item_li.start,
                    rect.start.y + wrap_accumulate,
                    item_li.end - item_li.start,
                    *sub_size,
                ),
            });
        }

        // Update the wrap_accumulation
        wrap_accumulate += sub_size;
        wrap_accumulate += match orientation {
            Orientation::Vertical => column_gap,
            Orientation::Horizontal => row_gap,
        };
    }

    vec
}

fn layout_flexbox_no_wrap(
    size: Vec2,
    padding: usize,
    orientation: Orientation,
    items: &[impl FlexLayout],
    minimum: bool,
    expand: bool,
) -> Vec<LayoutInfo> {
    // Gets the size in the primary direction
    let prim_size = size
        .in_orientation(orientation)
        .saturating_sub(padding * (items.len().saturating_sub(1)));

    // Calculate the expand total
    let mut expand_total = 0;
    for item in items {
        expand_total += item.props().expand;
    }

    let mut vec = Vec::with_capacity(items.len());

    // Initial pass
    let mut sum = 0;
    for item in items {
        let width =
            item.sub_for_prim(size.in_orientation(!orientation), !orientation);
        let width = width.minimum;
        sum += width;
        vec.push(width);
    }

    // Add the naturual sizing
    if !minimum {
        for (old_width, item) in vec.iter_mut().zip(items) {
            let width = item
                .sub_for_prim(size.in_orientation(!orientation), !orientation);
            let width = width.natural;

            // Check if we can add this without overflow
            if sum + width - *old_width <= size.in_orientation(orientation) {
                *old_width = width;
                sum += width - *old_width;
            }
        }
    }

    // Expand the expands
    if expand {
        let sum = vec.iter().sum();
        let expand_size = prim_size.saturating_sub(sum);
        let mut to_add = divide_integer(expand_size, expand_total);

        for (item, item_size) in items.iter().zip(vec.iter_mut()) {
            let expand = item.props().expand;
            let mut to_alloc = 0;
            for _ in 0..expand {
                let next = to_add.next().unwrap_or(0);
                to_alloc += next;
            }
            *item_size += to_alloc;
        }
    }

    // Convert the widths into LayoutInfo
    let mut lis = Vec::with_capacity(vec.len());
    let mut accumulated = 0;

    for item_size in vec {
        lis.push(LayoutInfo {
            start: accumulated,
            end: accumulated + item_size,
        });
        accumulated += item_size;
        accumulated += padding;
    }

    lis
}

/// Wraps some [`FlexLayout`]s
///
/// You more likely want to call [`layout_flexbox`]
///
/// # Returns
///
/// Indexes which need to be wrapped
pub fn wrap(
    size: Vec2,
    orientation: Orientation,
    row_gap: usize,
    column_gap: usize,
    items: &[impl FlexLayout],
    minimum: bool,
) -> Vec<usize> {
    let mut accumulated = 0;
    let mut vec = vec![0];

    let prim_size = size.in_orientation(orientation);

    for (i, item) in items.iter().enumerate() {
        let og_accumulated = accumulated;

        // Implement support for minimums
        let size =
            item.sub_for_prim(size.in_orientation(!orientation), !orientation);
        accumulated += if minimum { size.minimum } else { size.natural };

        accumulated += match orientation {
            Orientation::Vertical => row_gap,
            Orientation::Horizontal => column_gap,
        };

        if accumulated > prim_size {
            vec.push(i);
            accumulated -= og_accumulated;
        }
    }

    vec
}

pub(crate) fn divide_integer(
    n: usize,
    d: usize,
) -> impl Iterator<Item = usize> {
    let step = n.checked_div(d).unwrap_or(0);
    let rem_step = n.checked_rem(d).unwrap_or(0);
    (0..d).scan(0, move |rem, _| {
        let mut current = step;
        *rem += rem_step;
        if *rem >= d {
            *rem -= d;
            current += 1;
        }
        Some(current)
    })
}