oxicode_vtui/presentation/
renderable.rs1use ratatui::{
13 buffer::Buffer,
14 layout::Rect,
15 text::Line,
16 widgets::{Paragraph, Widget},
17};
18
19pub trait Renderable {
26 fn render(&self, area: Rect, buf: &mut Buffer);
28
29 fn desired_height(&self, width: u16) -> u16;
31}
32
33#[derive(Default)]
35pub struct Column<'a> {
36 children: Vec<Box<dyn Renderable + 'a>>,
37}
38
39impl<'a> Column<'a> {
40 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn push(&mut self, child: impl Renderable + 'a) {
47 self.children.push(Box::new(child));
48 }
49
50 pub fn is_empty(&self) -> bool {
52 self.children.is_empty()
53 }
54}
55
56impl Renderable for Column<'_> {
57 fn render(&self, area: Rect, buf: &mut Buffer) {
58 let mut y = area.y;
59 for child in &self.children {
60 if y >= area.bottom() {
61 break;
62 }
63 let height = child.desired_height(area.width).min(area.bottom() - y);
64 if height == 0 {
65 continue;
66 }
67 let child_area = Rect::new(area.x, y, area.width, height);
68 child.render(child_area, buf);
69 y = y.saturating_add(height);
70 }
71 }
72
73 fn desired_height(&self, width: u16) -> u16 {
74 self.children.iter().fold(0_u16, |height, child| {
75 height.saturating_add(child.desired_height(width))
76 })
77 }
78}
79
80#[derive(Clone, Debug, Default)]
83pub struct TextCell {
84 lines: Vec<Line<'static>>,
85}
86
87impl TextCell {
88 pub fn new(lines: Vec<Line<'static>>) -> Self {
90 Self { lines }
91 }
92}
93
94impl Renderable for TextCell {
95 fn render(&self, area: Rect, buf: &mut Buffer) {
96 if !area.is_empty() {
97 Paragraph::new(self.lines.clone()).render(area, buf);
98 }
99 }
100
101 fn desired_height(&self, width: u16) -> u16 {
102 if width == 0 {
103 return 0;
104 }
105 self.lines.iter().fold(0_u16, |height, line| {
106 let rows = (line.width() as u16).max(1).div_ceil(width);
107 height.saturating_add(rows)
108 })
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use ratatui::{style::Style, text::Span};
116
117 #[test]
118 fn column_measures_and_clips_children_to_its_area() {
119 let mut column = Column::new();
120 column.push(TextCell::new(vec![Line::from("one")]));
121 column.push(TextCell::new(vec![Line::from(Span::styled(
122 "two",
123 Style::default(),
124 ))]));
125
126 assert_eq!(column.desired_height(10), 2);
127 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
128 column.render(Rect::new(0, 0, 10, 1), &mut buffer);
129 assert_eq!(buffer[(0, 0)].symbol(), "o");
130 }
131
132 #[test]
133 fn text_cell_measures_wrapped_lines() {
134 let cell = TextCell::new(vec![Line::from("123456")]);
135 assert_eq!(cell.desired_height(4), 2);
136 }
137}