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#[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: Style,
32
33 highlight_style: Style,
35 highlight_symbol: &'a str,
37
38 node_closed_symbol: &'a str,
40 node_open_symbol: &'a str,
42 node_checked_symbol: &'a str,
44 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 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} ", node_open_symbol: "\u{25bc} ", node_checked_symbol: "\u{2611} ", node_unchecked_symbol: "\u{2610} ", _identifier: std::marker::PhantomData,
83 })
84 }
85
86 #[allow(clippy::missing_const_for_fn)] #[must_use]
88 pub fn block(mut self, block: Block<'a>) -> Self {
89 self.block = Some(block);
90 self
91 }
92
93 #[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 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 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 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 y: area.y,
308 height: area.height,
309 x: full_area.x,
311 width: full_area.width,
312 };
313 scrollbar.render(scrollbar_area, buf, &mut scrollbar_state);
314 }
315
316 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 #[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 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 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 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 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 assert_eq!(state.scroll_down(1), false);
585
586 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 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 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 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 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 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 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 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 assert_eq!(state.rendered_at(Position::new(0, 0)), None);
712
713 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 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 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 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 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 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 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 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 assert_eq!(state.mouse_click(Position::new(0, 4)), false);
851
852 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 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}