use crate::{
flexbox::divide_integer,
layout::{Rect, Vec2},
Layout, Orientation,
};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug)]
pub struct Props {
pub column: usize,
pub row: usize,
pub column_span: usize,
pub row_span: usize,
}
impl Props {
pub const fn is_in_row(&self, row: usize) -> bool {
self.row <= row && row < self.row + self.row_span
}
pub const fn is_in_column(&self, column: usize) -> bool {
self.column <= column && column < self.column + self.column_span
}
pub const fn is_in(&self, vec: Vec2) -> bool {
self.is_in_column(vec.x) && self.is_in_row(vec.y)
}
}
impl Props {
pub const fn new(
column: usize,
row: usize,
column_span: usize,
row_span: usize,
) -> Self {
Self {
column,
row,
column_span,
row_span,
}
}
}
pub trait GridLayout: Layout {
fn props(&self) -> &Props;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Sizing {
Fixed(usize),
Fractional(usize),
Auto,
AutoFixed(usize),
FrFixed { fr: usize, fixed: usize },
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Layouted {
pub sizes: Vec<Rect>,
pub columns: Vec<usize>,
pub rows: Vec<usize>,
}
#[derive(Clone, Copy, Debug)]
pub struct GridConfig<'a> {
pub template_rows: &'a [Sizing],
pub template_columns: &'a [Sizing],
pub row_gap: usize,
pub column_gap: usize,
pub fill_space: bool,
pub minimum: bool,
}
pub fn layout_grid(
rect: Rect,
cfg: GridConfig,
items: &[impl GridLayout],
) -> Layouted {
let mut ungapped_rect = rect;
ungapped_rect.size.x = ungapped_rect.size.x.saturating_sub(
(cfg.template_columns.len().max(1) - 1) * cfg.column_gap,
);
ungapped_rect.size.y = ungapped_rect
.size
.y
.saturating_sub((cfg.template_rows.len().max(1) - 1) * cfg.row_gap);
let mut sizes = Vec::with_capacity(items.len());
let (rows, columns) = calculate(ungapped_rect, items, &cfg);
for item in items {
let props = item.props();
let column_idx = props.column.min(columns.len());
let column_end_idx =
(column_idx + props.column_span).min(columns.len());
let row_idx = props.row.min(rows.len());
let row_end_idx = (row_idx + props.row_span).min(rows.len());
let mut column: usize = columns[..column_idx].iter().sum();
let mut column_end: usize = columns[..column_end_idx].iter().sum();
let mut row: usize = rows[..row_idx].iter().sum();
let mut row_end: usize = rows[..row_end_idx].iter().sum();
column += column_idx * cfg.column_gap;
row += row_idx * cfg.row_gap;
column_end += (column_end_idx - 1) * cfg.column_gap;
row_end += (row_end_idx - 1) * cfg.row_gap;
let rect = Rect::new(
rect.start.x + column,
rect.start.y + row,
column_end - column,
row_end - row,
);
sizes.push(rect);
}
Layouted {
sizes,
rows,
columns,
}
}
#[must_use]
pub fn calculate(
rect: Rect,
items: &[impl GridLayout],
cfg: &GridConfig,
) -> (Vec<usize>, Vec<usize>) {
let (auto_count_row, frac_row, frac_row_total, fixed_row) =
count(cfg.template_rows);
let (auto_count_column, frac_column, frac_column_total, fixed_column) =
count(cfg.template_columns);
let mut remaining_row = rect.size.y.saturating_sub(fixed_row);
let mut remaining_column = rect.size.x.saturating_sub(fixed_column);
let mut rows_autoed = cfg.template_rows.to_owned();
calc_autos(
rect,
&mut rows_autoed,
items,
Orientation::Vertical,
&mut remaining_row,
cfg.minimum,
);
let mut columns_autoed = cfg.template_columns.to_owned();
calc_autos(
rect,
&mut columns_autoed,
items,
Orientation::Horizontal,
&mut remaining_column,
cfg.minimum,
);
let rows_fred = calc_frfixed(
rows_autoed,
&mut remaining_row,
frac_row,
frac_row_total,
cfg.fill_space,
);
let columns_fred = calc_frfixed(
columns_autoed,
&mut remaining_column,
frac_column,
frac_column_total,
cfg.fill_space,
);
let rows = calc_auto_fixed(rows_fred, &mut remaining_row, cfg.fill_space);
let columns =
calc_auto_fixed(columns_fred, &mut remaining_column, cfg.fill_space);
(rows, columns)
}
#[must_use]
fn count(template: &[Sizing]) -> (usize, usize, usize, usize) {
let mut auto_count = 0_usize;
let mut fixed = 0_usize;
let mut frac = 0_usize;
let mut frac_total = 0_usize;
for size in template {
match size {
Sizing::Fractional(n) => {
frac += n;
frac_total += 1;
}
Sizing::Fixed(n) => {
fixed += n;
}
Sizing::AutoFixed(n) => {
fixed += n;
}
Sizing::Auto => auto_count += 1,
Sizing::FrFixed { fr, fixed: _ } => {
frac += fr;
frac_total += 1;
}
}
}
(auto_count, frac, frac_total, fixed)
}
fn calc_autos(
rect: Rect,
template: &mut [Sizing],
items: &[impl GridLayout],
orientation: Orientation,
remaining: &mut usize,
minimum: bool,
) {
for item in template.iter_mut() {
if let Sizing::Auto = item {
*item = Sizing::AutoFixed(0);
}
if let Sizing::Fractional(fr) = item {
*item = Sizing::FrFixed { fr: *fr, fixed: 0 };
}
}
let max_colrows = items
.iter()
.map(|x| match orientation {
Orientation::Vertical => x.props().row_span,
Orientation::Horizontal => x.props().column_span,
})
.max()
.unwrap_or(0);
let minimum_iter = if minimum {
[true].iter()
} else {
[true, false].iter()
};
for minimum in minimum_iter {
for spans in 0..=max_colrows {
let items = items.iter().filter(|x| match orientation {
Orientation::Vertical => x.props().row_span == spans,
Orientation::Horizontal => x.props().column_span == spans,
});
for item in items {
let required = item.prefered_size();
let required = if *minimum {
required.minimum
} else {
required.natural
};
let required = required.in_orientation(orientation);
let start = match orientation {
Orientation::Vertical => item.props().row,
Orientation::Horizontal => item.props().column,
};
ensure_alloc(
&mut template[start..(start + spans)],
required,
remaining,
);
}
}
}
}
fn ensure_alloc(
template: &mut [Sizing],
required: usize,
remaining: &mut usize,
) {
let mut auto_fixed_count = 0usize;
let mut sum = 0;
for colrow in template.iter() {
match colrow {
Sizing::Fixed(x) => sum += *x,
Sizing::Fractional(_) => unreachable!(
"all `Fractional`s have been converted to `FrFixed`s"
),
Sizing::Auto => {
unreachable!("all `Auto`s have been converted to `AutoFixed`s")
}
Sizing::AutoFixed(x) => {
auto_fixed_count += 1;
sum += *x;
}
Sizing::FrFixed { fr: _, fixed } => {
auto_fixed_count += 1;
sum += *fixed;
}
}
}
let mut needed = required.saturating_sub(sum);
if needed > *remaining {
needed = *remaining;
}
let mut divided = divide_integer(needed, auto_fixed_count);
for colrow in template {
if let Sizing::AutoFixed(x) = colrow {
let next = divided.next().unwrap();
*x += next;
*remaining = remaining.saturating_sub(next);
} else if let Sizing::FrFixed { fr: _, fixed } = colrow {
let next = divided.next().unwrap();
*fixed += next;
*remaining = remaining.saturating_sub(next);
}
}
assert!(divided.next().is_none());
}
#[must_use]
fn calc_frfixed(
autoed: Vec<Sizing>,
remaining: &mut usize,
frac: usize,
frac_total: usize,
fill_space: bool,
) -> Vec<Sizing> {
let mut added = 0_usize;
let mut total = Vec::with_capacity(autoed.len());
*remaining += autoed
.iter()
.filter_map(|x| match x {
Sizing::FrFixed { fr: _, fixed } => Some(fixed),
_ => None,
})
.sum::<usize>();
let min_part: usize = autoed
.iter()
.filter_map(|x| match x {
Sizing::FrFixed { fr, fixed } => Some((fixed / fr) + fixed % fr),
Sizing::Fractional(_) => {
unreachable!("all Fractionals should be FrFixeds")
}
_ => None,
})
.sum();
let part = if fill_space {
min_part.max(*remaining / frac.max(1))
} else {
min_part
};
for (i, size) in autoed.into_iter().enumerate() {
match size {
Sizing::FrFixed { fixed: _, fr } => {
let mut space = part * fr;
if fill_space && i == frac_total - 1 {
space = remaining.saturating_sub(added);
}
added += space;
total.push(Sizing::Fixed(space));
}
other => total.push(other),
}
}
*remaining = remaining.saturating_sub(added);
total
}
#[must_use]
fn calc_auto_fixed(
fred: Vec<Sizing>,
remaining: &mut usize,
fill_space: bool,
) -> Vec<usize> {
let auto_fixed_count = fred
.iter()
.filter(|x| matches!(x, Sizing::AutoFixed(_)))
.map(|_| 1)
.sum();
let mut added = 0_usize;
let mut total = Vec::with_capacity(fred.len());
let mut space = divide_integer(*remaining, auto_fixed_count);
for size in fred.into_iter() {
match size {
Sizing::AutoFixed(n) => {
let space = space.next().unwrap().min(*remaining - added);
if fill_space {
added += space;
total.push(space + n);
} else {
total.push(n);
}
}
Sizing::Fixed(n) => total.push(n),
_ => unreachable!("Only Fixed and AutoFixed!"),
}
}
*remaining -= added;
total
}