1use tuirealm::ratatui::buffer::Buffer;
6use tuirealm::ratatui::layout::Rect;
7use tuirealm::ratatui::style::Style;
8use tuirealm::ratatui::widgets::{Block, StatefulWidget, Widget};
9use unicode_width::UnicodeWidthStr;
10
11use super::{Node, NodeValue, Tree, TreeState};
12
13pub struct TreeWidget<'a, V: NodeValue> {
15 block: Option<Block<'a>>,
17 style: Style,
19 highlight_style: Style,
21 highlight_symbol: Option<&'a str>,
23 indent_size: usize,
25 tree: &'a Tree<V>,
27}
28
29impl<'a, V: NodeValue> TreeWidget<'a, V> {
30 pub fn new(tree: &'a Tree<V>) -> Self {
32 Self {
33 block: None,
34 style: Style::default(),
35 highlight_style: Style::default(),
36 highlight_symbol: None,
37 indent_size: 4,
38 tree,
39 }
40 }
41
42 pub fn block(mut self, block: Block<'a>) -> Self {
44 self.block = Some(block);
45 self
46 }
47
48 pub fn style(mut self, s: Style) -> Self {
50 self.style = s;
51 self
52 }
53
54 pub fn highlight_style(mut self, s: Style) -> Self {
56 self.highlight_style = s;
57 self
58 }
59
60 pub fn highlight_symbol(mut self, s: &'a str) -> Self {
62 self.highlight_symbol = Some(s);
63 self
64 }
65
66 pub fn indent_size(mut self, sz: usize) -> Self {
68 self.indent_size = sz;
69 self
70 }
71}
72
73struct Render {
74 depth: usize,
75 skip_rows: usize,
76}
77
78impl<V: NodeValue> Widget for TreeWidget<'_, V> {
79 fn render(self, area: Rect, buf: &mut Buffer) {
80 let mut state = TreeState::default();
81 StatefulWidget::render(self, area, buf, &mut state);
82 }
83}
84
85impl<V: NodeValue> StatefulWidget for TreeWidget<'_, V> {
86 type State = TreeState;
87
88 fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
89 buf.set_style(area, self.style);
91 let area = match self.block.take() {
93 Some(b) => {
94 let inner_area = b.inner(area);
95 b.render(area, buf);
96 inner_area
97 }
98 None => area,
99 };
100 if area.width < 1 || area.height < 1 {
102 return;
103 }
104 let mut render = Render {
106 depth: 1,
107 skip_rows: self.calc_rows_to_skip(state, area.height),
108 };
109 self.iter_nodes(self.tree.root(), area, buf, state, &mut render);
110 }
111}
112
113impl<V: NodeValue> TreeWidget<'_, V> {
114 fn iter_nodes(
115 &self,
116 node: &Node<V>,
117 mut area: Rect,
118 buf: &mut Buffer,
119 state: &TreeState,
120 render: &mut Render,
121 ) -> Rect {
122 area = self.render_node(node, area, buf, state, render);
124 if state.is_open(node) {
126 render.depth += 1;
128 for child in node.iter() {
129 if area.height == 0 {
130 break;
131 }
132 area = self.iter_nodes(child, area, buf, state, render);
133 }
134 render.depth -= 1;
136 }
137 area
138 }
139
140 fn render_node(
141 &self,
142 node: &Node<V>,
143 area: Rect,
144 buf: &mut Buffer,
145 state: &TreeState,
146 render: &mut Render,
147 ) -> Rect {
148 if render.skip_rows > 0 {
150 render.skip_rows -= 1;
151 return area;
152 }
153 let highlight_symbol = match state.is_selected(node) {
154 true => Some(self.highlight_symbol.unwrap_or_default()),
155 false => None,
156 };
157 let node_area = Rect {
159 x: area.x,
160 y: area.y,
161 width: area.width,
162 height: 1,
163 };
164 let style = match state.is_selected(node) {
166 false => self.style,
167 true => self.highlight_style,
168 };
169 buf.set_style(node_area, style);
171 let indent_size = render.depth * self.indent_size;
173 let indent_size = match state.is_selected(node) {
174 true if highlight_symbol.is_some() => {
175 indent_size.saturating_sub(highlight_symbol.unwrap().width() + 1)
176 }
177 _ => indent_size,
178 };
179 let width: usize = (area.width + area.x) as usize;
180 let (start_x, start_y) = buf.set_stringn(
182 area.x,
183 area.y,
184 " ".repeat(indent_size),
185 width - indent_size,
186 style,
187 );
188 let (start_x, start_y) = highlight_symbol
190 .map(|x| buf.set_stringn(start_x, start_y, x, width - start_x as usize, style))
191 .map(|(x, y)| buf.set_stringn(x, y, " ", width - start_x as usize, style))
192 .unwrap_or((start_x, start_y));
193
194 let mut start_x = start_x;
195 let mut start_y = start_y;
196 for (text, part_style) in node.value().render_parts_iter() {
197 let part_style = part_style.unwrap_or(style);
198 (start_x, start_y) =
200 buf.set_stringn(start_x, start_y, text, width - start_x as usize, part_style);
201 }
202 let write_after = if state.is_open(node) {
204 " \u{25bc}" } else if node.is_leaf() {
207 " "
209 } else {
210 " \u{25b6}" };
213 let _ = buf.set_stringn(
214 start_x,
215 start_y,
216 write_after,
217 width - start_x as usize,
218 style,
219 );
220 Rect {
222 x: area.x,
223 y: area.y + 1,
224 width: area.width,
225 height: area.height - 1,
226 }
227 }
228
229 fn calc_rows_to_skip(&self, state: &TreeState, height: u16) -> usize {
231 let selected = match state.selected() {
233 Some(s) => s,
234 None => return 0,
235 };
236
237 fn visit_nodes<V: NodeValue>(
239 node: &Node<V>,
240 state: &TreeState,
241 selected: &str,
242 selected_idx: &mut usize,
243 size: &mut usize,
244 ) {
245 *size += 1;
246 if node.id().as_str() == selected {
247 *selected_idx = *size;
248 }
249
250 if !state.is_closed(node) {
251 for child in node.iter() {
252 visit_nodes(child, state, selected, selected_idx, size);
253 }
254 }
255 }
256
257 let selected_idx: &mut usize = &mut 0;
258 let size = &mut 0;
259 visit_nodes(self.tree.root(), state, selected, selected_idx, size);
260
261 let render_area_h = height as usize;
262 let num_lines_to_show_at_top = render_area_h / 2;
263 let offset_max = (*size).saturating_sub(render_area_h);
264 (*selected_idx)
265 .saturating_sub(num_lines_to_show_at_top)
266 .min(offset_max)
267 }
268}
269
270#[cfg(test)]
271mod test {
272
273 use pretty_assertions::assert_eq;
274 use tuirealm::ratatui::Terminal;
275 use tuirealm::ratatui::backend::TestBackend;
276 use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout};
277 use tuirealm::ratatui::style::Color;
278
279 use super::*;
280 use crate::mock::mock_tree;
281
282 #[test]
283 fn should_construct_default_widget() {
284 let tree = mock_tree();
285 let widget = TreeWidget::new(&tree);
286 assert_eq!(widget.block, None);
287 assert_eq!(widget.highlight_style, Style::default());
288 assert_eq!(widget.highlight_symbol, None);
289 assert_eq!(widget.indent_size, 4);
290 assert_eq!(widget.style, Style::default());
291 }
292
293 #[test]
294 fn should_construct_widget() {
295 let tree = mock_tree();
296 let widget = TreeWidget::new(&tree)
297 .block(Block::default())
298 .highlight_style(Style::default().fg(Color::Red))
299 .highlight_symbol(">")
300 .indent_size(8)
301 .style(Style::default().fg(Color::LightRed));
302 assert!(widget.block.is_some());
303 assert_eq!(widget.highlight_style.fg.unwrap(), Color::Red);
304 assert_eq!(widget.indent_size, 8);
305 assert_eq!(widget.highlight_symbol.unwrap(), ">");
306 assert_eq!(widget.style.fg.unwrap(), Color::LightRed);
307 }
308
309 #[test]
310 fn should_have_no_row_to_skip_when_in_first_height_elements() {
311 let tree = mock_tree();
312 let mut state = TreeState::default();
313 let aa2 = tree.root().query(&String::from("aA2")).unwrap();
315 state.select(tree.root(), aa2);
316 let widget = TreeWidget::new(&tree);
318 assert_eq!(widget.calc_rows_to_skip(&state, 8), 2);
320 assert_eq!(widget.calc_rows_to_skip(&state, 6), 3);
322 }
323
324 #[test]
325 fn should_have_rows_to_skip_when_out_of_viewport() {
326 let tree = mock_tree();
327 let mut state = TreeState::default();
328 state.force_open(&["/", "a", "aA", "aB", "aC", "b", "bA", "bB"]);
330 let bb2 = tree.root().query(&String::from("bB2")).unwrap();
332 state.select(tree.root(), bb2);
333 let widget = TreeWidget::new(&tree);
335 assert_eq!(widget.calc_rows_to_skip(&state, 8), 17);
337 }
338
339 #[test]
340 fn should_not_panic_per_layout_direction() {
341 let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
342 let tree = mock_tree();
343 let constraints = [[50, 50], [100, 0]];
344 for direction in [LayoutDirection::Vertical, LayoutDirection::Horizontal] {
345 for constraint in constraints {
346 let widget = TreeWidget::new(&tree);
347 terminal
348 .draw(|frame| {
349 let layout = Layout::default()
350 .direction(direction)
351 .constraints(Constraint::from_percentages(constraint))
352 .split(frame.area());
353 frame.render_widget(widget, layout[1])
354 })
355 .unwrap();
356 }
357 }
358 }
359
360 #[test]
361 fn should_not_panic_when_layout_nested() {
362 let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
363 let tree = mock_tree();
364 let constraints = [[50, 50], [100, 0]];
365 let directions = [LayoutDirection::Vertical, LayoutDirection::Horizontal];
366 for outer_direction in directions {
367 for inner_direction in directions {
368 for outer_constraint in constraints {
369 for inner_constraint in constraints {
370 let widget = TreeWidget::new(&tree);
371 terminal
372 .draw(|frame| {
373 let layout = Layout::default()
374 .direction(outer_direction)
375 .constraints(Constraint::from_percentages(outer_constraint))
376 .split(frame.area());
377 let nested_layout = Layout::default()
378 .direction(inner_direction)
379 .constraints(inner_constraint)
380 .split(layout[1]);
381 frame.render_widget(widget, nested_layout[1])
382 })
383 .unwrap();
384 }
385 }
386 }
387 }
388 }
389}