use std::{cell::RefCell, num::NonZeroUsize, rc::Rc};
use lru::LruCache;
use tuirealm::ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect},
widgets::{Block, Paragraph, Widget},
};
type Rects = Rc<[Rect]>;
type Cache = LruCache<(Rect, UniformDynamicGrid), Rects>;
const CACHE_SIZE: NonZeroUsize = NonZeroUsize::new(10).unwrap();
thread_local! {
static LAYOUT_CACHE: RefCell<Cache> = RefCell::new(Cache::new(
CACHE_SIZE
));
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, PartialOrd, Eq)]
pub struct UniformDynamicGrid {
elems: usize,
elem_width: u16,
elem_height: u16,
row_spacing: u16,
draw_row_low_space: bool,
distribute_row_space: bool,
focus_node: Option<usize>,
}
#[allow(dead_code)]
impl UniformDynamicGrid {
pub fn new(elems: usize, elem_height: u16, elem_width: u16) -> Self {
Self {
elems,
elem_width,
elem_height,
row_spacing: 0,
draw_row_low_space: false,
distribute_row_space: false,
focus_node: None,
}
}
pub fn with_row_spacing(mut self, spacing: u16) -> Self {
self.row_spacing = spacing;
self
}
pub fn draw_row_low_space(mut self) -> Self {
self.draw_row_low_space = true;
self
}
pub fn distribute_row_space(mut self) -> Self {
self.distribute_row_space = true;
self
}
pub fn focus_node(mut self, focus: Option<usize>) -> Self {
self.focus_node = focus;
self
}
pub fn split(&self, area: Rect) -> Rects {
LAYOUT_CACHE.with_borrow_mut(|c| {
let key = (area, *self);
c.get_or_insert(key, || self.get_areas_inner(area)).clone()
})
}
fn get_areas_inner(&self, area: Rect) -> Rects {
let mut remaining_area = area;
let mut cells = Vec::new();
let mut remaining_elems = 0..self.elems;
let elems_per_row = {
let result = remaining_area
.width
.checked_div(self.elem_width)
.unwrap_or_default();
if result == 0 && remaining_area.width != 0 {
1
} else {
result
}
};
let mut rows_to_skip = if let Some(focus_idx) = self.focus_node {
let rows = if elems_per_row != 0 {
self.elems.div_ceil(usize::from(elems_per_row))
} else {
0
};
let total_height = usize::from(self.elem_height) * rows;
if total_height > usize::from(remaining_area.height) {
focus_idx
.checked_div(usize::from(elems_per_row))
.unwrap_or_default()
} else {
0
}
} else {
0
};
while !remaining_elems.is_empty()
&& !remaining_area.is_empty()
&& (remaining_area.height >= self.elem_height || self.draw_row_low_space)
{
if rows_to_skip > 0 {
for _ in 0..elems_per_row {
if remaining_elems.next().is_none() {
break;
}
cells.push(Rect::default());
}
rows_to_skip -= 1;
continue;
}
let spacing = if remaining_area == area {
0
} else {
self.row_spacing
};
let [_row_spacer, row_area, remainder] = Layout::vertical([
Constraint::Length(spacing),
Constraint::Length(self.elem_height),
Constraint::Fill(0),
])
.areas(remaining_area);
remaining_area = remainder;
let chunks = if self.distribute_row_space {
let constraints = (0..elems_per_row).map(|_| Constraint::Min(self.elem_width));
Layout::horizontal(constraints).split(row_area)
} else {
let constraints = (0..elems_per_row).map(|_| Constraint::Length(self.elem_width));
Layout::horizontal(constraints).split(row_area)
};
for chunk in chunks.iter() {
if remaining_elems.next().is_none() {
break;
}
cells.push(*chunk);
}
}
for _ in remaining_elems {
cells.push(Rect::default());
}
cells.into()
}
}
impl Widget for UniformDynamicGrid {
fn render(self, area: Rect, buf: &mut Buffer) {
let cells = self.split(area);
for (i, cell) in cells.iter().enumerate() {
Paragraph::new(format!("Area {:02}", i + 1))
.block(Block::bordered())
.render(*cell, buf);
}
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use tuirealm::ratatui::layout::Rect;
use super::UniformDynamicGrid;
#[test]
fn should_zero_on_zero_area() {
let area = Rect::new(0, 0, 0, 0);
let areas = UniformDynamicGrid::new(3, 3, 10).split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 0, 0));
assert_eq!(areas[1], Rect::new(0, 0, 0, 0));
assert_eq!(areas[2], Rect::new(0, 0, 0, 0));
}
#[test]
fn should_not_panic_not_enough_width_for_one_elem_with_focus_node() {
let area = Rect::new(0, 0, 3, 10);
let areas = UniformDynamicGrid::new(3, 3, 10)
.focus_node(Some(0))
.split(area);
assert_eq!(areas.len(), 3);
}
#[test]
fn should_split_all_single_row() {
let area = Rect::new(0, 0, 30, 3);
let areas = UniformDynamicGrid::new(3, 3, 10).split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 10, 3));
assert_eq!(areas[1], Rect::new(10, 0, 10, 3));
assert_eq!(areas[2], Rect::new(20, 0, 10, 3));
}
#[test]
fn should_split_all_2_rows() {
let area = Rect::new(0, 0, 20, 6);
let areas = UniformDynamicGrid::new(3, 3, 10).split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 10, 3));
assert_eq!(areas[1], Rect::new(10, 0, 10, 3));
assert_eq!(areas[2], Rect::new(0, 3, 10, 3));
}
#[test]
fn should_not_split_new_row_low_space() {
let area = Rect::new(0, 0, 20, 3);
let areas = UniformDynamicGrid::new(3, 3, 10).split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 10, 3));
assert_eq!(areas[1], Rect::new(10, 0, 10, 3));
assert_eq!(areas[2], Rect::new(0, 0, 0, 0));
}
#[test]
fn should_split_new_row_low_space() {
let area = Rect::new(0, 0, 20, 3);
let areas = UniformDynamicGrid::new(3, 3, 10)
.draw_row_low_space()
.split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 10, 3));
assert_eq!(areas[1], Rect::new(10, 0, 10, 3));
assert_eq!(areas[2], Rect::new(0, 0, 0, 0));
}
#[test]
fn should_have_row_spacing() {
let area = Rect::new(0, 0, 20, 7);
let areas = UniformDynamicGrid::new(3, 3, 10)
.with_row_spacing(1)
.split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 10, 3));
assert_eq!(areas[1], Rect::new(10, 0, 10, 3));
assert_eq!(areas[2], Rect::new(0, 4, 10, 3));
}
#[test]
fn should_split_all_single_row_no_leftover_space() {
let area = Rect::new(0, 0, 33, 3);
let areas = UniformDynamicGrid::new(3, 3, 10)
.distribute_row_space()
.split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 11, 3));
assert_eq!(areas[1], Rect::new(11, 0, 11, 3));
assert_eq!(areas[2], Rect::new(22, 0, 11, 3));
}
#[test]
fn should_not_skip_if_enough_area() {
let area = Rect::new(0, 0, 20, 6);
let areas = UniformDynamicGrid::new(3, 3, 10)
.focus_node(Some(2))
.split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 10, 3));
assert_eq!(areas[1], Rect::new(10, 0, 10, 3));
assert_eq!(areas[2], Rect::new(0, 3, 10, 3));
}
#[test]
fn should_skip_if_not_enough_area() {
let area = Rect::new(0, 0, 30, 7);
let areas = UniformDynamicGrid::new(7, 3, 10)
.focus_node(Some(3))
.split(area);
assert_eq!(areas.len(), 7);
assert_eq!(areas[0], Rect::new(0, 0, 0, 0));
assert_eq!(areas[1], Rect::new(0, 0, 0, 0));
assert_eq!(areas[2], Rect::new(0, 0, 0, 0));
assert_eq!(areas[3], Rect::new(0, 0, 10, 3));
assert_eq!(areas[4], Rect::new(10, 0, 10, 3));
assert_eq!(areas[5], Rect::new(20, 0, 10, 3));
assert_eq!(areas[6], Rect::new(0, 3, 10, 3));
}
#[test]
fn should_draw_in_lower_width() {
let area = Rect::new(0, 0, 3, 10);
let areas = UniformDynamicGrid::new(3, 3, 10).split(area);
assert_eq!(areas.len(), 3);
assert_eq!(areas[0], Rect::new(0, 0, 3, 3));
assert_eq!(areas[1], Rect::new(0, 3, 3, 3));
assert_eq!(areas[2], Rect::new(0, 6, 3, 3));
}
}