use std::time::Duration;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use crate::element::{AnyElement, Element};
pub struct Col<'a> {
children: Vec<AnyElement<'a>>,
gap: u16,
}
pub fn col<'a>() -> Col<'a> {
Col {
children: Vec::new(),
gap: 0,
}
}
impl<'a> Col<'a> {
pub fn gap(mut self, rows: u16) -> Self {
self.gap = rows;
self
}
pub fn child(mut self, child: impl Element + 'a) -> Self {
self.children.push(Box::new(child));
self
}
pub fn children<I>(mut self, children: I) -> Self
where
I: IntoIterator,
I::Item: Element + 'a,
{
self.children
.extend(children.into_iter().map(|c| Box::new(c) as AnyElement<'a>));
self
}
}
impl Element for Col<'_> {
fn height(&self, width: u16) -> u16 {
let content: u16 = self
.children
.iter()
.map(|c| c.height(width))
.fold(0, u16::saturating_add);
let gaps = self
.gap
.saturating_mul(self.children.len().saturating_sub(1) as u16);
content.saturating_add(gaps)
}
fn render(&self, area: Rect, buf: &mut Buffer) {
let mut y = area.y;
let bottom = area.bottom();
for (i, child) in self.children.iter().enumerate() {
if i > 0 {
y = y.saturating_add(self.gap);
}
if y >= bottom {
break;
}
let h = child.height(area.width).min(bottom - y);
if h > 0 {
child.render(Rect::new(area.x, y, area.width, h), buf);
}
y = y.saturating_add(h);
}
}
fn animated(&self) -> Option<Duration> {
self.children.iter().filter_map(|c| c.animated()).min()
}
fn cursor(&self, area: Rect) -> Option<(u16, u16)> {
let mut y_offset: u16 = 0;
for (i, child) in self.children.iter().enumerate() {
if i > 0 {
y_offset = y_offset.saturating_add(self.gap);
}
if y_offset >= area.height {
break;
}
let h = child
.height(area.width)
.min(area.height.saturating_sub(y_offset));
let child_area = Rect::new(area.x, area.y.saturating_add(y_offset), area.width, h);
if let Some((col, row)) = child.cursor(child_area)
&& row < h
{
return Some((col, row.saturating_add(y_offset)));
}
y_offset = y_offset.saturating_add(h);
}
None
}
}
#[derive(Clone, Copy)]
pub enum Width {
Fixed(u16),
Fill,
}
pub struct Row<'a> {
cells: Vec<(Width, AnyElement<'a>)>,
}
pub fn row<'a>() -> Row<'a> {
Row { cells: Vec::new() }
}
impl<'a> Row<'a> {
pub fn fixed(mut self, cols: u16, child: impl Element + 'a) -> Self {
self.cells.push((Width::Fixed(cols), Box::new(child)));
self
}
pub fn fill(mut self, child: impl Element + 'a) -> Self {
self.cells.push((Width::Fill, Box::new(child)));
self
}
fn allocate(&self, total: u16) -> Vec<u16> {
let mut widths = vec![0u16; self.cells.len()];
let mut remaining = total;
let mut fills = Vec::new();
for (i, (w, _)) in self.cells.iter().enumerate() {
match w {
Width::Fixed(n) => {
let take = (*n).min(remaining);
widths[i] = take;
remaining -= take;
}
Width::Fill => fills.push(i),
}
}
if !fills.is_empty() {
let each = remaining / fills.len() as u16;
let extra = remaining % fills.len() as u16;
for (j, &i) in fills.iter().enumerate() {
widths[i] = each + u16::from((j as u16) < extra);
}
}
widths
}
}
impl Element for Row<'_> {
fn height(&self, width: u16) -> u16 {
let widths = self.allocate(width);
self.cells
.iter()
.zip(&widths)
.map(|((_, c), &w)| if w == 0 { 0 } else { c.height(w) })
.max()
.unwrap_or(0)
}
fn render(&self, area: Rect, buf: &mut Buffer) {
let widths = self.allocate(area.width);
let mut x = area.x;
for ((_, child), &w) in self.cells.iter().zip(&widths) {
if w > 0 {
child.render(Rect::new(x, area.y, w, area.height), buf);
}
x = x.saturating_add(w);
}
}
fn animated(&self) -> Option<Duration> {
self.cells.iter().filter_map(|(_, c)| c.animated()).min()
}
fn cursor(&self, area: Rect) -> Option<(u16, u16)> {
let widths = self.allocate(area.width);
let mut x_offset: u16 = 0;
for ((_, child), &w) in self.cells.iter().zip(&widths) {
let cell_area = Rect::new(area.x.saturating_add(x_offset), area.y, w, area.height);
if w > 0
&& let Some((col, row)) = child.cursor(cell_area)
{
return Some((col.saturating_add(x_offset), row));
}
x_offset = x_offset.saturating_add(w);
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::element::ElementExt;
use crate::text::text;
fn rendered(el: &impl Element, width: u16) -> Vec<String> {
let height = el.height(width);
let area = Rect::new(0, 0, width, height);
let mut buf = Buffer::empty(area);
el.render(area, &mut buf);
(0..height)
.map(|y| {
let mut line: String = (0..width).map(|x| buf[(x, y)].symbol()).collect();
while line.ends_with(' ') {
line.pop();
}
line
})
.collect()
}
#[test]
fn col_stacks_children() {
let el = col().child(text("one")).child(text("two"));
assert_eq!(el.height(10), 2);
assert_eq!(rendered(&el, 10), vec!["one", "two"]);
}
#[test]
fn col_gap_inserts_blank_rows() {
let el = col().gap(1).child(text("a")).child(text("b"));
assert_eq!(el.height(10), 3);
assert_eq!(rendered(&el, 10), vec!["a", "", "b"]);
}
#[test]
fn col_sums_wrapped_heights() {
let el = col().child(text("hello world")).child(text("x"));
assert_eq!(el.height(6), 3); }
#[test]
fn row_fixed_and_fill() {
let el = row().fixed(2, text("> ")).fill(text("hello"));
assert_eq!(el.height(10), 1);
assert_eq!(rendered(&el, 10), vec!["> hello"]);
}
#[test]
fn row_fill_gets_remaining_width_for_wrapping() {
let el = row().fixed(4, text("gut ")).fill(text("hello world"));
assert_eq!(el.height(10), 2);
assert_eq!(rendered(&el, 10), vec!["gut hello", " world"]);
}
#[test]
fn row_splits_fill_equally_with_leftmost_remainder() {
let el = row()
.fill(empty_cell())
.fill(empty_cell())
.fill(empty_cell());
assert_eq!(el.allocate(10), vec![4, 3, 3]);
}
fn empty_cell() -> crate::element::Empty {
crate::element::Empty
}
#[test]
fn row_fixed_clamps_to_available() {
let el = row().fixed(8, text("aaaaaaaa")).fixed(8, text("b"));
assert_eq!(el.allocate(10), vec![8, 2]);
}
#[test]
fn col_cursor_offsets_by_child_position() {
struct CursorAt(u16, u16);
impl Element for CursorAt {
fn height(&self, _w: u16) -> u16 {
1
}
fn render(&self, _a: Rect, _b: &mut Buffer) {}
fn cursor(&self, _a: Rect) -> Option<(u16, u16)> {
Some((self.0, self.1))
}
}
let el = col().child(text("above")).child(CursorAt(3, 0).pad_left(2));
let area = Rect::new(0, 0, 10, 2);
assert_eq!(el.cursor(area), Some((5, 1)));
}
#[test]
fn cursor_of_clipped_child_is_suppressed() {
struct CursorAt(u16, u16);
impl Element for CursorAt {
fn height(&self, _w: u16) -> u16 {
1
}
fn render(&self, _a: Rect, _b: &mut Buffer) {}
fn cursor(&self, _a: Rect) -> Option<(u16, u16)> {
Some((self.0, self.1))
}
}
let el = col()
.child(text("a"))
.child(text("b"))
.child(CursorAt(0, 0));
assert_eq!(el.cursor(Rect::new(0, 0, 10, 2)), None);
assert_eq!(el.cursor(Rect::new(0, 0, 10, 3)), Some((0, 2)));
}
}