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