mecomp_tui/ui/widgets/tree/
mod.rs

1pub mod flatten;
2pub mod item;
3pub mod state;
4
5use flatten::Flattened;
6use item::CheckTreeItem;
7use ratatui::{
8    buffer::Buffer,
9    layout::Rect,
10    style::Style,
11    widgets::{Block, Scrollbar, ScrollbarState, StatefulWidget, Widget},
12};
13use state::CheckTreeState;
14use unicode_width::UnicodeWidthStr;
15
16/// A `CheckTree` which can be rendered.
17///
18/// The generic argument `Identifier` is used to keep the state like the currently selected or opened [`CheckTreeItem`]s in the [`CheckTreeState`].
19/// For more information see [`CheckTreeItem`].
20///
21/// This differs from the `tui_tree_widget` crate's `Tree` in that it allows for checkboxes to be rendered next to each leaf item.
22/// This is useful for creating a tree of items that can be selected.
23#[derive(Debug, Clone)]
24#[allow(clippy::module_name_repetitions)]
25pub struct CheckTree<'a, Identifier> {
26    items: &'a [CheckTreeItem<'a, Identifier>],
27
28    block: Option<Block<'a>>,
29    scrollbar: Option<Scrollbar<'a>>,
30    /// Style used as a base style for the widget
31    style: Style,
32
33    /// Style used to render selected item
34    highlight_style: Style,
35    /// Symbol in front of the selected item (Shift all items to the right)
36    highlight_symbol: &'a str,
37
38    /// Symbol displayed in front of a closed node (As in the children are currently not visible)
39    node_closed_symbol: &'a str,
40    /// Symbol displayed in front of an open node. (As in the children are currently visible)
41    node_open_symbol: &'a str,
42    /// Symbol displayed in front of a node without children, that is checked
43    node_checked_symbol: &'a str,
44    /// Symbol displayed in front of a node without children, that is not checked
45    node_unchecked_symbol: &'a str,
46
47    _identifier: std::marker::PhantomData<Identifier>,
48}
49
50impl<'a, Identifier> CheckTree<'a, Identifier>
51where
52    Identifier: Clone + PartialEq + Eq + core::hash::Hash,
53{
54    /// Create a new `CheckTree`.
55    ///
56    /// # Errors
57    ///
58    /// Errors when there are duplicate identifiers in the children.
59    pub fn new(items: &'a [CheckTreeItem<'a, Identifier>]) -> Result<Self, std::io::Error> {
60        let identifiers = items
61            .iter()
62            .map(|item| &item.identifier)
63            .collect::<std::collections::HashSet<_>>();
64        if identifiers.len() != items.len() {
65            return Err(std::io::Error::new(
66                std::io::ErrorKind::InvalidInput,
67                "duplicate identifiers",
68            ));
69        }
70
71        Ok(Self {
72            items,
73            block: None,
74            scrollbar: None,
75            style: Style::new(),
76            highlight_style: Style::new(),
77            highlight_symbol: "",
78            node_closed_symbol: "\u{25b6} ", // ▸ Arrow to right (alt. ▸ U+25B8 BLACK RIGHT-POINTING SMALL TRIANGLE)
79            node_open_symbol: "\u{25bc} ", // ▼ Arrow down (alt. ▾ U+25BE BLACK DOWN-POINTING SMALL TRIANGLE)
80            node_checked_symbol: "\u{2611} ", // ☑ U+2611 BALLOT BOX WITH CHECK
81            node_unchecked_symbol: "\u{2610} ", // ☐ U+2610 BALLOT BOX
82            _identifier: std::marker::PhantomData,
83        })
84    }
85
86    #[allow(clippy::missing_const_for_fn)] // false positive
87    #[must_use]
88    pub fn block(mut self, block: Block<'a>) -> Self {
89        self.block = Some(block);
90        self
91    }
92
93    /// Show the scrollbar when rendering this widget.
94    ///
95    /// Experimental: Can change on any release without any additional notice.
96    /// Its there to test and experiment with whats possible with scrolling widgets.
97    /// Also see <https://github.com/ratatui-org/ratatui/issues/174>
98    #[must_use]
99    pub const fn experimental_scrollbar(mut self, scrollbar: Option<Scrollbar<'a>>) -> Self {
100        self.scrollbar = scrollbar;
101        self
102    }
103
104    #[must_use]
105    pub const fn style(mut self, style: Style) -> Self {
106        self.style = style;
107        self
108    }
109
110    #[must_use]
111    pub const fn highlight_style(mut self, style: Style) -> Self {
112        self.highlight_style = style;
113        self
114    }
115
116    #[must_use]
117    pub const fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
118        self.highlight_symbol = highlight_symbol;
119        self
120    }
121
122    #[must_use]
123    pub const fn node_closed_symbol(mut self, symbol: &'a str) -> Self {
124        self.node_closed_symbol = symbol;
125        self
126    }
127
128    #[must_use]
129    pub const fn node_open_symbol(mut self, symbol: &'a str) -> Self {
130        self.node_open_symbol = symbol;
131        self
132    }
133
134    #[must_use]
135    pub const fn node_checked_symbol(mut self, symbol: &'a str) -> Self {
136        self.node_checked_symbol = symbol;
137        self
138    }
139
140    #[must_use]
141    pub const fn node_unchecked_symbol(mut self, symbol: &'a str) -> Self {
142        self.node_unchecked_symbol = symbol;
143        self
144    }
145}
146
147impl<'a, Identifier: 'a + Clone + PartialEq + Eq + core::hash::Hash> StatefulWidget
148    for CheckTree<'a, Identifier>
149{
150    type State = CheckTreeState<Identifier>;
151
152    #[allow(clippy::too_many_lines)]
153    fn render(self, full_area: Rect, buf: &mut Buffer, state: &mut Self::State) {
154        buf.set_style(full_area, self.style);
155
156        // Get the inner area inside a possible block, otherwise use the full area
157        let area = self.block.map_or(full_area, |block| {
158            let inner_area = block.inner(full_area);
159            block.render(full_area, buf);
160            inner_area
161        });
162
163        state.last_area = area;
164        state.last_rendered_identifiers.clear();
165        if area.width < 1 || area.height < 1 {
166            return;
167        }
168
169        let visible = state.flatten(self.items);
170        state.last_biggest_index = visible.len().saturating_sub(1);
171        if visible.is_empty() {
172            return;
173        }
174        let available_height = area.height as usize;
175
176        let ensure_index_in_view =
177            if state.ensure_selected_in_view_on_next_render && !state.selected.is_empty() {
178                visible
179                    .iter()
180                    .position(|flattened| flattened.identifier == state.selected)
181            } else {
182                None
183            };
184
185        // Ensure last line is still visible
186        let mut start = state.offset.min(state.last_biggest_index);
187
188        if let Some(ensure_index_in_view) = ensure_index_in_view {
189            start = start.min(ensure_index_in_view);
190        }
191
192        let mut end = start;
193        let mut height = 0;
194        for item_height in visible
195            .iter()
196            .skip(start)
197            .map(|flattened| flattened.item.height())
198        {
199            if height + item_height > available_height {
200                break;
201            }
202            height += item_height;
203            end += 1;
204        }
205
206        if let Some(ensure_index_in_view) = ensure_index_in_view {
207            while ensure_index_in_view >= end {
208                height += visible[end].item.height();
209                end += 1;
210                while height > available_height {
211                    height = height.saturating_sub(visible[start].item.height());
212                    start += 1;
213                }
214            }
215        }
216
217        state.offset = start;
218        state.ensure_selected_in_view_on_next_render = false;
219
220        let blank_symbol = " ".repeat(self.highlight_symbol.width());
221
222        let mut current_height = 0;
223        let has_selection = !state.selected.is_empty();
224        #[allow(clippy::cast_possible_truncation)]
225        for flattened in visible.iter().skip(state.offset).take(end - start) {
226            let Flattened { identifier, item } = flattened;
227
228            let x = area.x;
229            let y = area.y + current_height;
230            let height = item.height() as u16;
231            current_height += height;
232
233            let area = Rect {
234                x,
235                y,
236                width: area.width,
237                height,
238            };
239
240            let text = &item.text;
241            let item_style = text.style;
242
243            let is_selected = state.selected == *identifier;
244            let after_highlight_symbol_x = if has_selection {
245                let symbol = if is_selected {
246                    self.highlight_symbol
247                } else {
248                    &blank_symbol
249                };
250                let (x, _) = buf.set_stringn(x, y, symbol, area.width as usize, item_style);
251                x
252            } else {
253                x
254            };
255
256            let after_depth_x = {
257                let indent_width = flattened.depth() * 2;
258                let (after_indent_x, _) = buf.set_stringn(
259                    after_highlight_symbol_x,
260                    y,
261                    " ".repeat(indent_width),
262                    indent_width,
263                    item_style,
264                );
265                let symbol = if text.width() == 0 {
266                    "  "
267                } else if item.children.is_empty() {
268                    if state.checked.contains(identifier) {
269                        self.node_checked_symbol
270                    } else {
271                        self.node_unchecked_symbol
272                    }
273                } else if state.opened.contains(identifier) {
274                    self.node_open_symbol
275                } else {
276                    self.node_closed_symbol
277                };
278                let max_width = area.width.saturating_sub(after_indent_x - x);
279                let (x, _) =
280                    buf.set_stringn(after_indent_x, y, symbol, max_width as usize, item_style);
281                x
282            };
283
284            let text_area = Rect {
285                x: after_depth_x,
286                width: area.width.saturating_sub(after_depth_x - x),
287                ..area
288            };
289            text.render(text_area, buf);
290
291            if is_selected {
292                buf.set_style(area, self.highlight_style);
293            }
294
295            state
296                .last_rendered_identifiers
297                .push((area.y, identifier.clone()));
298        }
299
300        // render scrollbar last so it's on top
301        if let Some(scrollbar) = self.scrollbar {
302            let mut scrollbar_state = ScrollbarState::new(visible.len().saturating_sub(height))
303                .position(start)
304                .viewport_content_length(height);
305            let scrollbar_area = Rect {
306                // Inner height to be exactly as the content
307                y: area.y,
308                height: area.height,
309                // Outer width to stay on the right border
310                x: full_area.x,
311                width: full_area.width,
312            };
313            scrollbar.render(scrollbar_area, buf, &mut scrollbar_state);
314        }
315
316        // update state
317        state.last_identifiers = visible
318            .into_iter()
319            .map(|flattened| flattened.identifier)
320            .collect();
321    }
322}
323
324#[cfg(test)]
325mod render_tests {
326    use super::*;
327    use pretty_assertions::assert_eq;
328    use ratatui::{layout::Position, widgets::ScrollbarOrientation};
329
330    #[must_use]
331    #[track_caller]
332    fn render(width: u16, height: u16, state: &mut CheckTreeState<&'static str>) -> Buffer {
333        let items = CheckTreeItem::example();
334        let tree = CheckTree::new(&items).unwrap();
335        let area = Rect::new(0, 0, width, height);
336        let mut buffer = Buffer::empty(area);
337        StatefulWidget::render(tree, area, &mut buffer, state);
338        buffer
339    }
340
341    #[test]
342    #[should_panic = "duplicate identifiers"]
343    fn tree_new_errors_with_duplicate_identifiers() {
344        let item = CheckTreeItem::new_leaf("same", "text");
345        let another = item.clone();
346        let items = [item, another];
347        let _: CheckTree<_> = CheckTree::new(&items).unwrap();
348    }
349
350    #[test]
351    fn does_not_panic() {
352        _ = render(0, 0, &mut CheckTreeState::default());
353        _ = render(10, 0, &mut CheckTreeState::default());
354        _ = render(0, 10, &mut CheckTreeState::default());
355        _ = render(10, 10, &mut CheckTreeState::default());
356    }
357
358    #[test]
359    fn scrollbar_renders_over_tree() {
360        let mut state = CheckTreeState::default();
361        state.open(vec!["b"]);
362        let items = CheckTreeItem::example();
363        let tree = CheckTree::new(&items)
364            .unwrap()
365            .experimental_scrollbar(Some(Scrollbar::new(ScrollbarOrientation::VerticalRight)));
366        let area = Rect::new(0, 0, 10, 4);
367        let mut buffer = Buffer::empty(area);
368        StatefulWidget::render(tree, area, &mut buffer, &mut state);
369
370        #[rustfmt::skip]
371        let expected = Buffer::with_lines([
372            "☐ Alfa   ▲",
373            "▼ Bravo  █",
374            "  ☐ Charl█",
375            "  ▶ Delta▼",
376        ]);
377        assert_eq!(buffer, expected);
378    }
379
380    #[test]
381    fn renders_border() {
382        let mut state = CheckTreeState::default();
383        let items = CheckTreeItem::example();
384        let tree = CheckTree::new(&items).unwrap().block(Block::bordered());
385        let area = Rect::new(0, 0, 10, 5);
386        let mut buffer = Buffer::empty(area);
387        StatefulWidget::render(tree, area, &mut buffer, &mut state);
388
389        let expected = Buffer::with_lines([
390            "┌────────┐",
391            "│☐ Alfa  │",
392            "│▶ Bravo │",
393            "│☐ Hotel │",
394            "└────────┘",
395        ]);
396        assert_eq!(buffer, expected);
397    }
398
399    #[test]
400    fn nothing_open() {
401        let buffer = render(10, 4, &mut CheckTreeState::default());
402        #[rustfmt::skip]
403        let expected = Buffer::with_lines([
404            "☐ Alfa    ",
405            "▶ Bravo   ",
406            "☐ Hotel   ",
407            "          ",
408        ]);
409        assert_eq!(buffer, expected);
410    }
411
412    #[test]
413    fn check_leaf_d1() {
414        let mut state = CheckTreeState::default();
415        state.check(vec!["a"]);
416        let buffer = render(10, 4, &mut state);
417        #[rustfmt::skip]
418        let expected = Buffer::with_lines([
419            "☑ Alfa    ",
420            "▶ Bravo   ",
421            "☐ Hotel   ",
422            "          ",
423        ]);
424        assert_eq!(buffer, expected);
425    }
426
427    #[test]
428    fn check_parent_d1() {
429        let mut state = CheckTreeState::default();
430        state.check(vec!["b"]);
431        let buffer = render(10, 4, &mut state);
432        #[rustfmt::skip]
433        let expected = Buffer::with_lines([
434            "☐ Alfa    ",
435            "▶ Bravo   ",
436            "☐ Hotel   ",
437            "          ",
438        ]);
439        assert_eq!(buffer, expected);
440    }
441
442    #[test]
443    fn check_leaf_d2() {
444        let mut state = CheckTreeState::default();
445        state.open(vec!["b"]);
446        state.check(vec!["b", "c"]);
447        state.check(vec!["b", "g"]);
448        let buffer = render(13, 7, &mut state);
449        #[rustfmt::skip]
450        let expected = Buffer::with_lines([
451            "☐ Alfa       ",
452            "▼ Bravo      ",
453            "  ☑ Charlie  ",
454            "  ▶ Delta    ",
455            "  ☑ Golf     ",
456            "☐ Hotel      ",
457            "             ",
458        ]);
459        assert_eq!(buffer, expected);
460    }
461
462    #[test]
463    fn depth_one() {
464        let mut state = CheckTreeState::default();
465        state.open(vec!["b"]);
466        let buffer = render(13, 7, &mut state);
467        let expected = Buffer::with_lines([
468            "☐ Alfa       ",
469            "▼ Bravo      ",
470            "  ☐ Charlie  ",
471            "  ▶ Delta    ",
472            "  ☐ Golf     ",
473            "☐ Hotel      ",
474            "             ",
475        ]);
476        assert_eq!(buffer, expected);
477    }
478
479    #[test]
480    fn depth_two() {
481        let mut state = CheckTreeState::default();
482        state.open(vec!["b"]);
483        state.open(vec!["b", "d"]);
484        let buffer = render(15, 9, &mut state);
485        let expected = Buffer::with_lines([
486            "☐ Alfa         ",
487            "▼ Bravo        ",
488            "  ☐ Charlie    ",
489            "  ▼ Delta      ",
490            "    ☐ Echo     ",
491            "    ☐ Foxtrot  ",
492            "  ☐ Golf       ",
493            "☐ Hotel        ",
494            "               ",
495        ]);
496        assert_eq!(buffer, expected);
497    }
498
499    // TODO: test CheckTreeState::select_relative, rendered_at
500    // key_up, key_down, key_left, key_right, and key_space
501
502    #[test]
503    fn test_select_first_last() {
504        let mut state = CheckTreeState::default();
505        let _ = render(15, 4, &mut state);
506        assert_eq!(state.select_first(), true);
507        assert_eq!(state.select_first(), false);
508        assert_eq!(state.selected(), &["a"]);
509        assert_eq!(state.select_last(), true);
510        assert_eq!(state.select_last(), false);
511        assert_eq!(state.selected(), &["h"]);
512    }
513
514    #[test]
515    fn test_scroll_selected_into_view() {
516        let mut state = CheckTreeState::default();
517        state.open(vec!["b"]);
518        state.open(vec!["b", "d"]);
519        let buffer = render(15, 4, &mut state);
520        let expected = Buffer::with_lines([
521            "☐ Alfa         ",
522            "▼ Bravo        ",
523            "  ☐ Charlie    ",
524            "  ▼ Delta      ",
525        ]);
526        assert_eq!(buffer, expected);
527
528        // selected is visible
529        state.select(vec!["b", "d"]);
530        state.scroll_selected_into_view();
531        let buffer = render(15, 4, &mut state);
532        assert_eq!(buffer, expected);
533
534        // selected is not visible
535        state.select(vec!["b", "g"]);
536        state.scroll_selected_into_view();
537        let buffer = render(15, 4, &mut state);
538        let expected = Buffer::with_lines([
539            "  ▼ Delta      ",
540            "    ☐ Echo     ",
541            "    ☐ Foxtrot  ",
542            "  ☐ Golf       ",
543        ]);
544        assert_eq!(buffer, expected);
545    }
546
547    #[test]
548    fn test_scroll() {
549        let mut state = CheckTreeState::default();
550        state.open(vec!["b"]);
551        state.open(vec!["b", "d"]);
552        let buffer = render(15, 4, &mut state);
553        let expected = Buffer::with_lines([
554            "☐ Alfa         ",
555            "▼ Bravo        ",
556            "  ☐ Charlie    ",
557            "  ▼ Delta      ",
558        ]);
559        assert_eq!(buffer, expected);
560
561        // scroll down works
562        assert_eq!(state.scroll_down(1), true);
563        let buffer = render(15, 4, &mut state);
564        let expected = Buffer::with_lines([
565            "▼ Bravo        ",
566            "  ☐ Charlie    ",
567            "  ▼ Delta      ",
568            "    ☐ Echo     ",
569        ]);
570        assert_eq!(buffer, expected);
571
572        // scroll down stops at boundary
573        assert_eq!(state.scroll_down(15), true);
574        let buffer = render(15, 4, &mut state);
575        let expected = Buffer::with_lines([
576            "☐ Hotel        ",
577            "               ",
578            "               ",
579            "               ",
580        ]);
581        assert_eq!(buffer, expected);
582
583        // scroll down stops at boundary
584        assert_eq!(state.scroll_down(1), false);
585
586        // scroll up works
587        assert_eq!(state.scroll_up(1), true);
588        let buffer = render(15, 4, &mut state);
589        let expected = Buffer::with_lines([
590            "  ☐ Golf       ",
591            "☐ Hotel        ",
592            "               ",
593            "               ",
594        ]);
595        assert_eq!(buffer, expected);
596
597        // scroll up stops at boundary
598        assert_eq!(state.scroll_up(15), true);
599        let buffer = render(15, 4, &mut state);
600        let expected = Buffer::with_lines([
601            "☐ Alfa         ",
602            "▼ Bravo        ",
603            "  ☐ Charlie    ",
604            "  ▼ Delta      ",
605        ]);
606        assert_eq!(buffer, expected);
607
608        // scroll up stops at boundary
609        assert_eq!(state.scroll_up(1), false);
610    }
611
612    #[test]
613    fn test_keys() {
614        let mut state = CheckTreeState::default();
615        state.open(vec!["b"]);
616        state.open(vec!["b", "d"]);
617        let buffer = render(15, 4, &mut state);
618        let expected = Buffer::with_lines([
619            "☐ Alfa         ",
620            "▼ Bravo        ",
621            "  ☐ Charlie    ",
622            "  ▼ Delta      ",
623        ]);
624        assert_eq!(buffer, expected);
625
626        // key_down works (if nothing selected, goes to top)
627        state.key_down();
628        let buffer = render(15, 4, &mut state);
629        let expected = Buffer::with_lines([
630            "☐ Alfa         ",
631            "▼ Bravo        ",
632            "  ☐ Charlie    ",
633            "  ▼ Delta      ",
634        ]);
635        assert_eq!(state.selected(), &["a"]);
636        assert_eq!(buffer, expected);
637
638        // key_up works (if nothing selected, goes to bottom)
639        state.key_left();
640        state.key_up();
641        let buffer = render(15, 4, &mut state);
642        let expected = Buffer::with_lines([
643            "    ☐ Echo     ",
644            "    ☐ Foxtrot  ",
645            "  ☐ Golf       ",
646            "☐ Hotel        ",
647        ]);
648        assert_eq!(state.selected(), &["h"]);
649        assert_eq!(buffer, expected);
650
651        // key_left works
652        state.select_first();
653        state.scroll_selected_into_view();
654        let buffer = render(15, 4, &mut state);
655        let expected = Buffer::with_lines([
656            "☐ Alfa         ",
657            "▼ Bravo        ",
658            "  ☐ Charlie    ",
659            "  ▼ Delta      ",
660        ]);
661        assert_eq!(state.selected(), &["a"]);
662        assert_eq!(buffer, expected);
663
664        state.key_down();
665        assert_eq!(state.selected(), &["b"]);
666        state.key_left();
667        assert_eq!(state.selected(), &["b"]);
668        let buffer = render(15, 4, &mut state);
669        #[rustfmt::skip]
670        let expected = Buffer::with_lines([
671            "☐ Alfa         ",
672            "▶ Bravo        ",
673            "☐ Hotel        ",
674            "               ",
675        ]);
676        assert_eq!(buffer, expected);
677
678        // key_right works
679        state.key_right();
680        assert_eq!(state.selected(), &["b"]);
681        let buffer = render(15, 4, &mut state);
682        let expected = Buffer::with_lines([
683            "☐ Alfa         ",
684            "▼ Bravo        ",
685            "  ☐ Charlie    ",
686            "  ▼ Delta      ",
687        ]);
688        assert_eq!(buffer, expected);
689
690        // key_space works
691        state.key_space();
692        assert_eq!(state.selected(), &["b"]);
693        state.key_down();
694        state.key_space();
695        assert_eq!(state.selected(), &["b", "c"]);
696        let buffer = render(15, 4, &mut state);
697        let expected = Buffer::with_lines([
698            "☐ Alfa         ",
699            "▼ Bravo        ",
700            "  ☑ Charlie    ",
701            "  ▼ Delta      ",
702        ]);
703        assert_eq!(buffer, expected);
704    }
705
706    #[test]
707    fn test_rendered_at() {
708        let mut state = CheckTreeState::default();
709
710        // nothing rendered
711        assert_eq!(state.rendered_at(Position::new(0, 0)), None);
712
713        // render the tree
714        let buffer = render(15, 4, &mut state);
715        let expected = Buffer::with_lines([
716            "☐ Alfa         ",
717            "▶ Bravo        ",
718            "☐ Hotel        ",
719            "               ",
720        ]);
721        assert_eq!(buffer, expected);
722
723        // check rendered at
724        assert_eq!(
725            state.rendered_at(Position::new(0, 0)),
726            Some(["a"].as_slice())
727        );
728        assert_eq!(
729            state.rendered_at(Position::new(0, 1)),
730            Some(["b"].as_slice())
731        );
732        assert_eq!(
733            state.rendered_at(Position::new(0, 2)),
734            Some(["h"].as_slice())
735        );
736        assert_eq!(state.rendered_at(Position::new(0, 3)), None);
737
738        // open branch, render, and check again
739        state.open(vec!["b"]);
740        let buffer = render(15, 4, &mut state);
741        let expected = Buffer::with_lines([
742            "☐ Alfa         ",
743            "▼ Bravo        ",
744            "  ☐ Charlie    ",
745            "  ▶ Delta      ",
746        ]);
747        assert_eq!(buffer, expected);
748
749        assert_eq!(
750            state.rendered_at(Position::new(0, 0)),
751            Some(["a"].as_slice())
752        );
753        assert_eq!(
754            state.rendered_at(Position::new(0, 1)),
755            Some(["b"].as_slice())
756        );
757        assert_eq!(
758            state.rendered_at(Position::new(0, 2)),
759            Some(["b", "c"].as_slice())
760        );
761        assert_eq!(
762            state.rendered_at(Position::new(0, 3)),
763            Some(["b", "d"].as_slice())
764        );
765
766        // close branch, render, and check again
767        state.close(["b"].as_slice());
768        let buffer = render(15, 4, &mut state);
769        let expected = Buffer::with_lines([
770            "☐ Alfa         ",
771            "▶ Bravo        ",
772            "☐ Hotel        ",
773            "               ",
774        ]);
775        assert_eq!(buffer, expected);
776
777        assert_eq!(
778            state.rendered_at(Position::new(0, 0)),
779            Some(["a"].as_slice())
780        );
781        assert_eq!(
782            state.rendered_at(Position::new(0, 1)),
783            Some(["b"].as_slice())
784        );
785        assert_eq!(
786            state.rendered_at(Position::new(0, 2)),
787            Some(["h"].as_slice())
788        );
789        assert_eq!(state.rendered_at(Position::new(0, 3)), None);
790    }
791
792    #[test]
793    fn test_mouse() {
794        let mut state = CheckTreeState::default();
795
796        // click before rendering
797        assert_eq!(state.mouse_click(Position::new(0, 0)), false);
798
799        state.open(vec!["b"]);
800        state.open(vec!["b", "d"]);
801        let buffer = render(15, 4, &mut state);
802        let expected = Buffer::with_lines([
803            "☐ Alfa         ",
804            "▼ Bravo        ",
805            "  ☐ Charlie    ",
806            "  ▼ Delta      ",
807        ]);
808        assert_eq!(buffer, expected);
809
810        // click on first item
811        // check that the item is checked
812        assert_eq!(state.mouse_click(Position::new(0, 0)), true);
813        let buffer = render(15, 4, &mut state);
814        let expected = Buffer::with_lines([
815            "☑ Alfa         ",
816            "▼ Bravo        ",
817            "  ☐ Charlie    ",
818            "  ▼ Delta      ",
819        ]);
820        assert_eq!(state.selected(), &["a"]);
821        assert_eq!(buffer, expected);
822
823        // click on second item
824        // check that the branch is closed
825        assert_eq!(state.mouse_click(Position::new(0, 1)), true);
826        let buffer = render(15, 4, &mut state);
827        let expected = Buffer::with_lines([
828            "☑ Alfa         ",
829            "▶ Bravo        ",
830            "☐ Hotel        ",
831            "               ",
832        ]);
833        assert_eq!(state.selected(), &["b"]);
834        assert_eq!(buffer, expected);
835
836        // click on the first item again
837        // check that the item is unchecked
838        assert_eq!(state.mouse_click(Position::new(0, 0)), true);
839        let buffer = render(15, 4, &mut state);
840        let expected = Buffer::with_lines([
841            "☐ Alfa         ",
842            "▶ Bravo        ",
843            "☐ Hotel        ",
844            "               ",
845        ]);
846        assert_eq!(state.selected(), &["a"]);
847        assert_eq!(buffer, expected);
848
849        // click on empty space
850        assert_eq!(state.mouse_click(Position::new(0, 4)), false);
851
852        // click on the second item again
853        // check that the branch is opened
854        assert_eq!(state.mouse_click(Position::new(0, 1)), true);
855        let buffer = render(15, 4, &mut state);
856        let expected = Buffer::with_lines([
857            "☐ Alfa         ",
858            "▼ Bravo        ",
859            "  ☐ Charlie    ",
860            "  ▼ Delta      ",
861        ]);
862        assert_eq!(state.selected(), &["b"]);
863        assert_eq!(buffer, expected);
864
865        // click on the third item
866        // check that the item is checked
867        assert_eq!(state.mouse_click(Position::new(0, 2)), true);
868        let buffer = render(15, 4, &mut state);
869        let expected = Buffer::with_lines([
870            "☐ Alfa         ",
871            "▼ Bravo        ",
872            "  ☑ Charlie    ",
873            "  ▼ Delta      ",
874        ]);
875        assert_eq!(state.selected(), &["b", "c"]);
876        assert_eq!(buffer, expected);
877    }
878}