1use tuirealm::ratatui::buffer::Buffer;
6use tuirealm::ratatui::layout::Rect;
7use tuirealm::ratatui::style::Style;
8use tuirealm::ratatui::text::Line;
9use tuirealm::ratatui::widgets::{Block, StatefulWidget, Widget};
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<Line<'a>>,
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_str<S: Into<Line<'a>>>(mut self, s: S) -> Self {
62 self.highlight_symbol = Some(s.into());
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 => self.highlight_symbol.as_ref(),
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| {
191 buf.set_line(
192 start_x,
193 start_y,
194 x,
195 u16::try_from(width - start_x as usize).unwrap_or(u16::MAX),
196 )
197 })
198 .map(|(x, y)| buf.set_stringn(x, y, " ", width - start_x as usize, style))
199 .unwrap_or((start_x, start_y));
200
201 let mut start_x = start_x;
202 let mut start_y = start_y;
203 for (text, part_style) in node.value().render_parts_iter() {
204 let part_style = part_style.unwrap_or(style);
205 (start_x, start_y) =
207 buf.set_stringn(start_x, start_y, text, width - start_x as usize, part_style);
208 }
209 let write_after = if state.is_open(node) {
211 " \u{25bc}" } else if node.is_leaf() {
214 " "
216 } else {
217 " \u{25b6}" };
220 let _ = buf.set_stringn(
221 start_x,
222 start_y,
223 write_after,
224 width - start_x as usize,
225 style,
226 );
227 Rect {
229 x: area.x,
230 y: area.y + 1,
231 width: area.width,
232 height: area.height - 1,
233 }
234 }
235
236 fn calc_rows_to_skip(&self, state: &TreeState, height: u16) -> usize {
238 let selected = match state.selected() {
240 Some(s) => s,
241 None => return 0,
242 };
243
244 fn visit_nodes<V: NodeValue>(
246 node: &Node<V>,
247 state: &TreeState,
248 selected: &str,
249 selected_idx: &mut usize,
250 size: &mut usize,
251 ) {
252 *size += 1;
253 if node.id().as_str() == selected {
254 *selected_idx = *size;
255 }
256
257 if !state.is_closed(node) {
258 for child in node.iter() {
259 visit_nodes(child, state, selected, selected_idx, size);
260 }
261 }
262 }
263
264 let selected_idx: &mut usize = &mut 0;
265 let size = &mut 0;
266 visit_nodes(self.tree.root(), state, selected, selected_idx, size);
267
268 let render_area_h = height as usize;
269 let num_lines_to_show_at_top = render_area_h / 2;
270 let offset_max = (*size).saturating_sub(render_area_h);
271 (*selected_idx)
272 .saturating_sub(num_lines_to_show_at_top)
273 .min(offset_max)
274 }
275}
276
277#[cfg(test)]
278mod test {
279
280 use pretty_assertions::assert_eq;
281 use tuirealm::ratatui::Terminal;
282 use tuirealm::ratatui::backend::TestBackend;
283 use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout};
284 use tuirealm::ratatui::style::Color;
285
286 use super::*;
287 use crate::mock::mock_tree;
288
289 #[test]
290 fn should_construct_default_widget() {
291 let tree = mock_tree();
292 let widget = TreeWidget::new(&tree);
293 assert_eq!(widget.block, None);
294 assert_eq!(widget.highlight_style, Style::default());
295 assert_eq!(widget.highlight_symbol, None);
296 assert_eq!(widget.indent_size, 4);
297 assert_eq!(widget.style, Style::default());
298 }
299
300 #[test]
301 fn should_construct_widget() {
302 let tree = mock_tree();
303 let widget = TreeWidget::new(&tree)
304 .block(Block::default())
305 .highlight_style(Style::default().fg(Color::Red))
306 .highlight_str(">")
307 .indent_size(8)
308 .style(Style::default().fg(Color::LightRed));
309 assert!(widget.block.is_some());
310 assert_eq!(widget.highlight_style.fg.unwrap(), Color::Red);
311 assert_eq!(widget.indent_size, 8);
312 assert_eq!(widget.highlight_symbol.unwrap(), Line::raw(">"));
313 assert_eq!(widget.style.fg.unwrap(), Color::LightRed);
314 }
315
316 #[test]
317 fn should_have_no_row_to_skip_when_in_first_height_elements() {
318 let tree = mock_tree();
319 let mut state = TreeState::default();
320 let aa2 = tree.root().query(&String::from("aA2")).unwrap();
322 state.select(tree.root(), aa2);
323 let widget = TreeWidget::new(&tree);
325 assert_eq!(widget.calc_rows_to_skip(&state, 8), 2);
327 assert_eq!(widget.calc_rows_to_skip(&state, 6), 3);
329 }
330
331 #[test]
332 fn should_have_rows_to_skip_when_out_of_viewport() {
333 let tree = mock_tree();
334 let mut state = TreeState::default();
335 state.force_open(&["/", "a", "aA", "aB", "aC", "b", "bA", "bB"]);
337 let bb2 = tree.root().query(&String::from("bB2")).unwrap();
339 state.select(tree.root(), bb2);
340 let widget = TreeWidget::new(&tree);
342 assert_eq!(widget.calc_rows_to_skip(&state, 8), 17);
344 }
345
346 #[test]
347 fn should_not_panic_per_layout_direction() {
348 let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
349 let tree = mock_tree();
350 let constraints = [[50, 50], [100, 0]];
351 for direction in [LayoutDirection::Vertical, LayoutDirection::Horizontal] {
352 for constraint in constraints {
353 let widget = TreeWidget::new(&tree);
354 terminal
355 .draw(|frame| {
356 let layout = Layout::default()
357 .direction(direction)
358 .constraints(Constraint::from_percentages(constraint))
359 .split(frame.area());
360 frame.render_widget(widget, layout[1])
361 })
362 .unwrap();
363 }
364 }
365 }
366
367 #[test]
368 fn should_not_panic_when_layout_nested() {
369 let mut terminal = Terminal::new(TestBackend::new(80, 20)).unwrap();
370 let tree = mock_tree();
371 let constraints = [[50, 50], [100, 0]];
372 let directions = [LayoutDirection::Vertical, LayoutDirection::Horizontal];
373 for outer_direction in directions {
374 for inner_direction in directions {
375 for outer_constraint in constraints {
376 for inner_constraint in constraints {
377 let widget = TreeWidget::new(&tree);
378 terminal
379 .draw(|frame| {
380 let layout = Layout::default()
381 .direction(outer_direction)
382 .constraints(Constraint::from_percentages(outer_constraint))
383 .split(frame.area());
384 let nested_layout = Layout::default()
385 .direction(inner_direction)
386 .constraints(inner_constraint)
387 .split(layout[1]);
388 frame.render_widget(widget, nested_layout[1])
389 })
390 .unwrap();
391 }
392 }
393 }
394 }
395 }
396}