use ratatui_core::buffer::Buffer;
use ratatui_core::layout::Rect;
use ratatui_core::style::Style;
use ratatui_core::text::Line;
use ratatui_core::widgets::Widget;
use ratatui_widgets::block::{Block, Padding};
use ratatui_widgets::borders::{BorderType, Borders};
use crate::cells::Cells;
use crate::component::Component;
use crate::impl_slot_children;
use crate::insets::Insets;
use crate::node::{Layout, WidthConstraint};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Direction {
#[default]
Column,
Row,
}
#[derive(typed_builder::TypedBuilder)]
pub struct View {
#[builder(default, setter(into))]
pub direction: Direction,
#[builder(default, setter(into))]
pub border: Option<BorderType>,
#[builder(default, setter(into))]
pub border_style: Style,
#[builder(default, setter(into))]
pub title: Option<String>,
#[builder(default, setter(into))]
pub title_bottom: Option<String>,
#[builder(default, setter(into))]
pub title_style: Style,
#[builder(default, setter(into))]
pub title_bottom_style: Style,
#[builder(default, setter(into))]
pub padding: Cells,
#[builder(default, setter(into))]
pub padding_top: Option<Cells>,
#[builder(default, setter(into))]
pub padding_bottom: Option<Cells>,
#[builder(default, setter(into))]
pub padding_left: Option<Cells>,
#[builder(default, setter(into))]
pub padding_right: Option<Cells>,
#[builder(default, setter(into))]
pub width: WidthConstraint,
#[builder(default, setter(into))]
pub style: Style,
}
impl Default for View {
fn default() -> Self {
Self {
direction: Direction::Column,
border: None,
border_style: Style::default(),
title: None,
title_bottom: None,
title_style: Style::default(),
title_bottom_style: Style::default(),
padding: Cells::ZERO,
padding_top: None,
padding_bottom: None,
padding_left: None,
padding_right: None,
width: WidthConstraint::Fill,
style: Style::default(),
}
}
}
impl View {
fn effective_padding(&self) -> (u16, u16, u16, u16) {
let base = self.padding.0;
(
self.padding_top.map_or(base, |c| c.0),
self.padding_right.map_or(base, |c| c.0),
self.padding_bottom.map_or(base, |c| c.0),
self.padding_left.map_or(base, |c| c.0),
)
}
fn build_block(&self) -> Block<'_> {
let mut block = Block::new().style(self.style);
if let Some(border_type) = self.border {
block = block
.borders(Borders::ALL)
.border_type(border_type)
.border_style(self.border_style);
}
if let Some(ref title) = self.title {
block = block.title_top(Line::from(title.as_str()).style(self.title_style));
}
if let Some(ref title) = self.title_bottom {
block = block.title_bottom(Line::from(title.as_str()).style(self.title_bottom_style));
}
let (pt, pr, pb, pl) = self.effective_padding();
if pt > 0 || pr > 0 || pb > 0 || pl > 0 {
block = block.padding(Padding::new(pl, pr, pt, pb));
}
block
}
}
impl Component for View {
type State = ();
fn render(&self, area: Rect, buf: &mut Buffer, _state: &()) {
self.build_block().render(area, buf);
}
fn content_inset(&self, _state: &()) -> Insets {
let has_border = self.border.is_some();
let border: u16 = if has_border { 1 } else { 0 };
let (pt, pr, pb, pl) = self.effective_padding();
Insets {
top: border + pt,
right: border + pr,
bottom: border + pb,
left: border + pl,
}
}
fn layout(&self) -> Layout {
match self.direction {
Direction::Column => Layout::Vertical,
Direction::Row => Layout::Horizontal,
}
}
fn width_constraint(&self) -> WidthConstraint {
self.width
}
}
impl_slot_children!(View);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_view_is_vertical_no_border() {
let v = View::default();
assert_eq!(v.direction, Direction::Column);
assert!(v.border.is_none());
assert_eq!(v.padding, Cells::ZERO);
assert_eq!(v.layout(), Layout::Vertical);
assert_eq!(v.content_inset(&()), Insets::ZERO);
}
#[test]
fn row_direction_maps_to_horizontal_layout() {
let v = View {
direction: Direction::Row,
..View::default()
};
assert_eq!(v.layout(), Layout::Horizontal);
}
#[test]
fn border_adds_one_cell_inset_per_side() {
let v = View {
border: Some(BorderType::Plain),
..View::default()
};
let insets = v.content_inset(&());
assert_eq!(insets, Insets::all(1));
}
#[test]
fn border_plus_padding() {
let v = View {
border: Some(BorderType::Rounded),
padding: Cells(2),
..View::default()
};
let insets = v.content_inset(&());
assert_eq!(insets, Insets::all(3));
}
#[test]
fn padding_without_border() {
let v = View {
padding: Cells(1),
..View::default()
};
let insets = v.content_inset(&());
assert_eq!(insets, Insets::all(1));
}
#[test]
fn side_specific_padding_overrides_general() {
let v = View {
padding: Cells(1),
padding_left: Some(Cells(3)),
padding_top: Some(Cells(0)),
..View::default()
};
let insets = v.content_inset(&());
assert_eq!(
insets,
Insets {
top: 0,
right: 1,
bottom: 1,
left: 3,
}
);
}
#[test]
fn side_specific_padding_with_border() {
let v = View {
border: Some(BorderType::Plain),
padding: Cells(1),
padding_left: Some(Cells(2)),
..View::default()
};
let insets = v.content_inset(&());
assert_eq!(
insets,
Insets {
top: 2, right: 2, bottom: 2, left: 3, }
);
}
#[test]
fn width_constraint_passthrough() {
let v = View {
width: WidthConstraint::Fixed(20),
..View::default()
};
assert_eq!(v.width_constraint(), WidthConstraint::Fixed(20));
}
#[test]
fn render_plain_border() {
let v = View {
border: Some(BorderType::Plain),
..View::default()
};
let area = Rect::new(0, 0, 10, 5);
let mut buf = Buffer::empty(area);
v.render(area, &mut buf, &());
let tl = buf.cell((0, 0)).unwrap();
assert_eq!(tl.symbol(), "┌");
let tr = buf.cell((9, 0)).unwrap();
assert_eq!(tr.symbol(), "┐");
let bl = buf.cell((0, 4)).unwrap();
assert_eq!(bl.symbol(), "└");
let br = buf.cell((9, 4)).unwrap();
assert_eq!(br.symbol(), "┘");
}
#[test]
fn render_with_title() {
let v = View {
border: Some(BorderType::Plain),
title: Some("Test".into()),
..View::default()
};
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
v.render(area, &mut buf, &());
let t = buf.cell((1, 0)).unwrap();
assert_eq!(t.symbol(), "T");
}
#[test]
fn render_with_title_bottom() {
let v = View {
border: Some(BorderType::Plain),
title_bottom: Some("Bottom".into()),
..View::default()
};
let area = Rect::new(0, 0, 20, 5);
let mut buf = Buffer::empty(area);
v.render(area, &mut buf, &());
let b = buf.cell((1, 4)).unwrap();
assert_eq!(b.symbol(), "B");
}
#[test]
fn render_no_border_produces_empty_buffer() {
let v = View::default();
let area = Rect::new(0, 0, 10, 5);
let mut buf = Buffer::empty(area);
v.render(area, &mut buf, &());
for y in 0..5 {
for x in 0..10 {
assert_eq!(buf.cell((x, y)).unwrap().symbol(), " ");
}
}
}
}