use std::time::Duration;
use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
pub trait Element {
fn height(&self, width: u16) -> u16;
fn render(&self, area: Rect, buf: &mut Buffer);
fn animated(&self) -> Option<Duration> {
None
}
fn cursor(&self, _area: Rect) -> Option<(u16, u16)> {
None
}
}
pub type AnyElement<'a> = Box<dyn Element + 'a>;
impl Element for Box<dyn Element + '_> {
fn height(&self, width: u16) -> u16 {
self.as_ref().height(width)
}
fn render(&self, area: Rect, buf: &mut Buffer) {
self.as_ref().render(area, buf)
}
fn animated(&self) -> Option<Duration> {
self.as_ref().animated()
}
fn cursor(&self, area: Rect) -> Option<(u16, u16)> {
self.as_ref().cursor(area)
}
}
pub trait ElementExt: Element + Sized {
fn any<'a>(self) -> AnyElement<'a>
where
Self: 'a,
{
Box::new(self)
}
fn pad_left(self, cols: u16) -> Padded<Self> {
Padded {
inner: self,
left: cols,
top: 0,
}
}
fn pad_top(self, rows: u16) -> Padded<Self> {
Padded {
inner: self,
left: 0,
top: rows,
}
}
}
impl<E: Element> ElementExt for E {}
pub trait Fluent: Sized {
fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
if cond { f(self) } else { self }
}
fn when_some<T>(self, value: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
match value {
Some(v) => f(self, v),
None => self,
}
}
}
impl<T> Fluent for T {}
pub struct Padded<E> {
inner: E,
left: u16,
top: u16,
}
impl<E: Element> Element for Padded<E> {
fn height(&self, width: u16) -> u16 {
let inner_width = width.saturating_sub(self.left);
if inner_width == 0 {
return self.top;
}
self.top.saturating_add(self.inner.height(inner_width))
}
fn render(&self, area: Rect, buf: &mut Buffer) {
let inner = Rect::new(
area.x.saturating_add(self.left),
area.y.saturating_add(self.top),
area.width.saturating_sub(self.left),
area.height.saturating_sub(self.top),
);
if inner.width == 0 || inner.height == 0 {
return;
}
self.inner.render(inner, buf);
}
fn animated(&self) -> Option<Duration> {
self.inner.animated()
}
fn cursor(&self, area: Rect) -> Option<(u16, u16)> {
let inner = Rect::new(
area.x.saturating_add(self.left),
area.y.saturating_add(self.top),
area.width.saturating_sub(self.left),
area.height.saturating_sub(self.top),
);
self.inner
.cursor(inner)
.map(|(col, row)| (col.saturating_add(self.left), row.saturating_add(self.top)))
}
}
pub struct Empty;
pub fn empty() -> Empty {
Empty
}
impl Element for Empty {
fn height(&self, _width: u16) -> u16 {
0
}
fn render(&self, _area: Rect, _buf: &mut Buffer) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::text::text;
#[test]
fn empty_is_zero_height() {
assert_eq!(Element::height(&empty(), 80), 0);
}
#[test]
fn padded_adds_top_rows_and_left_cols() {
let el = text("hi").pad_left(2).pad_top(1);
assert_eq!(el.height(10), 2);
let area = Rect::new(0, 0, 10, 2);
let mut buf = Buffer::empty(area);
el.render(area, &mut buf);
assert_eq!(buf[(2, 1)].symbol(), "h");
assert_eq!(buf[(3, 1)].symbol(), "i");
assert_eq!(buf[(0, 0)].symbol(), " ");
}
#[test]
fn padded_narrows_wrap_width() {
let el = text("hello world").pad_left(4);
assert_eq!(el.height(10), 2);
}
#[test]
fn any_erases_heterogeneous_branches() {
let branch =
|b: bool| -> AnyElement<'static> { if b { text("yes").any() } else { empty().any() } };
assert_eq!(branch(true).height(80), 1);
assert_eq!(branch(false).height(80), 0);
}
}