1use ratatui::layout::{Constraint, Rect};
2use ratatui::prelude::Buffer;
3use ratatui::widgets::{
4 Block, Borders, Cell, Row, Scrollbar, ScrollbarOrientation, ScrollbarState, StatefulWidget,
5 Table, TableState,
6};
7use smallvec::SmallVec;
8
9use crate::columns::TreeColumns;
10use crate::context::TreeRowContext;
11use crate::glyphs::{TreeGlyphs, TreeLabelRenderer};
12use crate::model::{NoFilter, TreeFilter, TreeFilterConfig, TreeModel};
13use crate::state::{TreeListViewState, VisibleNode};
14use crate::style::TreeListViewStyle;
15
16pub struct TreeListView<'a, T, L, C, F = NoFilter>
18where
19 T: TreeModel,
20 L: TreeLabelRenderer<T>,
21 C: TreeColumns<T>,
22 F: TreeFilter<T>,
23{
24 model: &'a T,
25 label: &'a L,
26 columns: &'a C,
27 style: TreeListViewStyle<'a>,
28 glyphs: TreeGlyphs<'a>,
29 filter: F,
30 filter_config: TreeFilterConfig,
31}
32
33impl<'a, T, L, C> TreeListView<'a, T, L, C, NoFilter>
34where
35 T: TreeModel,
36 L: TreeLabelRenderer<T>,
37 C: TreeColumns<T>,
38{
39 pub const fn new(
40 model: &'a T,
41 label: &'a L,
42 columns: &'a C,
43 style: TreeListViewStyle<'a>,
44 ) -> Self {
45 Self {
46 model,
47 label,
48 columns,
49 style,
50 glyphs: TreeGlyphs::unicode(),
51 filter: NoFilter,
52 filter_config: TreeFilterConfig::disabled(),
53 }
54 }
55
56 pub const fn glyphs(mut self, glyphs: TreeGlyphs<'a>) -> Self {
57 self.glyphs = glyphs;
58 self
59 }
60
61 pub fn with_filter<F>(
62 self,
63 filter: F,
64 filter_config: TreeFilterConfig,
65 ) -> TreeListView<'a, T, L, C, F>
66 where
67 F: TreeFilter<T>,
68 {
69 TreeListView {
70 model: self.model,
71 label: self.label,
72 columns: self.columns,
73 style: self.style,
74 glyphs: self.glyphs,
75 filter,
76 filter_config,
77 }
78 }
79}
80
81impl<'a, T, L, C, F> TreeListView<'a, T, L, C, F>
82where
83 T: TreeModel,
84 L: TreeLabelRenderer<T>,
85 C: TreeColumns<T>,
86 F: TreeFilter<T>,
87{
88 #[inline]
89 fn build_rows(
90 &self,
91 nodes: &[VisibleNode<T::Id>],
92 state: &TreeListViewState<T::Id>,
93 ) -> Vec<Row<'a>> {
94 let mut rows = Vec::with_capacity(nodes.len());
95 for node in nodes {
96 let has_children = !self.model.children(node.id).is_empty();
97 let is_expanded = state.is_expanded(node.parent, node.id);
98 let is_marked = state.node_is_marked(node.id);
99 let ctx = TreeRowContext {
100 level: node.level,
101 is_tail_stack: node.is_tail_stack.as_slice(),
102 is_expanded,
103 has_children,
104 is_marked,
105 draw_lines: state.draw_lines(),
106 line_style: self.style.line_style,
107 };
108 let label_cell = self.label.cell(self.model, node.id, &ctx, &self.glyphs);
109 let mut cells = SmallVec::<[Cell; 8]>::new();
110 cells.push(label_cell);
111 cells.extend(self.columns.cells(self.model, node.id));
112 let mut row = Row::new(cells);
113 if is_marked {
114 row = row.style(self.style.mark_style);
115 }
116 rows.push(row);
117 }
118 rows
119 }
120
121 #[inline]
122 fn build_table(
123 &self,
124 rows: Vec<Row<'a>>,
125 constraints: &[Constraint],
126 block: Block<'a>,
127 header: Option<Row<'a>>,
128 ) -> Table<'a> {
129 let mut table = Table::new(rows, constraints.iter().copied())
130 .style(self.style.block_style)
131 .block(block)
132 .row_highlight_style(self.style.highlight_style)
133 .highlight_symbol(self.style.highlight_symbol);
134 if let Some(header) = header {
135 table = table.header(header);
136 }
137 table
138 }
139
140 #[inline]
141 fn render_scrollbar(
142 &self,
143 area: Rect,
144 buf: &mut Buffer,
145 state: &TreeListViewState<T::Id>,
146 inner_height: usize,
147 scroll_rows: usize,
148 ) {
149 let scroll_len = scroll_rows.saturating_add(1);
150 let position = state
151 .list_state()
152 .offset()
153 .min(scroll_len.saturating_sub(1));
154 let mut scrollbar_state = ScrollbarState::new(scroll_len)
155 .position(position)
156 .viewport_content_length(inner_height);
157 Scrollbar::default()
158 .orientation(ScrollbarOrientation::VerticalRight)
159 .render(area, buf, &mut scrollbar_state);
160 }
161}
162
163impl<T, L, C, F> StatefulWidget for TreeListView<'_, T, L, C, F>
164where
165 T: TreeModel,
166 L: TreeLabelRenderer<T>,
167 C: TreeColumns<T>,
168 F: TreeFilter<T>,
169{
170 type State = TreeListViewState<T::Id>;
171
172 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
173 if self.filter_config.enabled {
174 state.ensure_visible_nodes_filtered(self.model, &self.filter, self.filter_config);
175 } else {
176 state.ensure_visible_nodes(self.model);
177 }
178 state.ensure_mark_cache(self.model);
179
180 let header = self.columns.header();
181 let header_height = u16::from(header.is_some());
182
183 let mut block = Block::default().borders(self.style.borders);
184 if let Some(title) = self.style.title.clone() {
185 block = block.title(title);
186 }
187 block = block
188 .style(self.style.block_style)
189 .border_style(self.style.border_style);
190
191 let inner_height = block.inner(area).height.saturating_sub(header_height) as usize;
192 state.ensure_selection_visible_with_policy(inner_height, self.style.scroll_policy);
193
194 let visible_nodes = state.visible_nodes();
195 let total_rows = visible_nodes.len();
196 let (range_start, range_end) = if self.style.virtualize_rows {
197 let start = state.list_state().offset().min(total_rows);
198 let end = (start + inner_height).min(total_rows);
199 (start, end)
200 } else {
201 (0, total_rows)
202 };
203
204 let nodes = &visible_nodes[range_start..range_end];
205 let rows = self.build_rows(nodes, state);
206
207 let scroll_rows = total_rows.saturating_sub(inner_height);
208
209 let mut local_state = if self.style.virtualize_rows {
210 Some(*state.list_state())
211 } else {
212 None
213 };
214 let table_state: &mut TableState = local_state.as_mut().map_or_else(
215 || state.list_state_mut(),
216 |state_ref| {
217 *state_ref.offset_mut() = 0;
218 if let Some(selected) = state_ref.selected() {
219 if selected < range_start || selected >= range_end {
220 state_ref.select(None);
221 } else {
222 state_ref.select(Some(selected - range_start));
223 }
224 }
225 state_ref
226 },
227 );
228
229 let (table_area, table_block, constraints, header, scrollbar_area) = if scroll_rows > 0 {
230 let table_area = Rect {
231 width: area.width.saturating_sub(1),
232 ..area
233 };
234 let scrollbar_area = Rect {
235 x: area.x + area.width - 1,
236 y: area.y,
237 width: 1,
238 height: area.height,
239 };
240 let mut table_borders = self.style.borders;
241 table_borders.remove(Borders::RIGHT);
242 let table_block = block.borders(table_borders);
243 let constraints = self
244 .columns
245 .constraints_for_area(table_block.inner(table_area));
246 (
247 table_area,
248 table_block,
249 constraints,
250 header.clone(),
251 Some(scrollbar_area),
252 )
253 } else {
254 let constraints = self.columns.constraints_for_area(block.inner(area));
255 (area, block, constraints, header, None)
256 };
257
258 let table = self.build_table(rows, constraints.as_slice(), table_block, header);
259 table.render(table_area, buf, table_state);
260
261 if let Some(scrollbar_area) = scrollbar_area {
262 self.render_scrollbar(scrollbar_area, buf, state, inner_height, scroll_rows);
263 }
264 }
265}
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270 use ratatui::layout::Constraint;
271 use ratatui::widgets::StatefulWidget;
272
273 struct TestModel {
274 children: Vec<Vec<usize>>,
275 names: Vec<String>,
276 }
277
278 impl TestModel {
279 fn new(child_count: usize) -> Self {
280 let mut children = vec![Vec::new(); child_count + 1];
281 let mut names = Vec::with_capacity(child_count + 1);
282 names.push("root".to_string());
283 for idx in 1..=child_count {
284 children[0].push(idx);
285 names.push(format!("node-{idx}"));
286 }
287 Self { children, names }
288 }
289 }
290
291 impl TreeModel for TestModel {
292 type Id = usize;
293
294 fn root(&self) -> Option<Self::Id> {
295 Some(0)
296 }
297
298 fn children(&self, id: Self::Id) -> &[Self::Id] {
299 &self.children[id]
300 }
301
302 fn contains(&self, id: Self::Id) -> bool {
303 id < self.children.len()
304 }
305 }
306
307 struct Label;
308
309 impl TreeLabelRenderer<TestModel> for Label {
310 fn cell<'a>(
311 &'a self,
312 model: &'a TestModel,
313 id: usize,
314 _ctx: &TreeRowContext,
315 _glyphs: &TreeGlyphs<'a>,
316 ) -> Cell<'a> {
317 Cell::from(model.names[id].as_str())
318 }
319 }
320
321 struct Columns;
322
323 impl TreeColumns<TestModel> for Columns {
324 fn label_constraint(&self) -> Constraint {
325 Constraint::Percentage(100)
326 }
327
328 fn other_constraints(&self) -> &[Constraint] {
329 &[]
330 }
331
332 fn cells<'a>(&'a self, _model: &'a TestModel, _id: usize) -> SmallVec<[Cell<'a>; 8]> {
333 SmallVec::new()
334 }
335 }
336
337 #[test]
338 fn render_smoke_with_scrollbar() {
339 let model = TestModel::new(12);
340 let label = Label;
341 let columns = Columns;
342 let style = TreeListViewStyle::default();
343 let widget = TreeListView::new(&model, &label, &columns, style);
344
345 let mut state = TreeListViewState::new();
346 state.set_expanded(0, None, true);
347
348 let area = Rect::new(0, 0, 20, 6);
349 let mut buffer = Buffer::empty(area);
350
351 widget.render(area, &mut buffer, &mut state);
352 }
353}