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
50#[allow(dead_code)]
51impl<'a, Identifier> CheckTree<'a, Identifier>
52where
53 Identifier: Clone + PartialEq + Eq + core::hash::Hash,
54{
55 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} ", node_open_symbol: "\u{25bc} ", node_checked_symbol: "\u{2611} ", node_unchecked_symbol: "\u{2610} ", _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 #[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 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 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 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 y: area.y,
309 height: area.height,
310 x: full_area.x,
312 width: full_area.width,
313 };
314 scrollbar.render(scrollbar_area, buf, &mut scrollbar_state);
315 }
316
317 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 #[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 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 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 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 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 assert_eq!(state.scroll_down(1), false);
586
587 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 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 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 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 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 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 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 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 assert_eq!(state.rendered_at(Position::new(0, 0)), None);
713
714 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 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 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 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 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 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 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 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 assert_eq!(state.mouse_click(Position::new(0, 4)), false);
852
853 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 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}