1use unicode_width::UnicodeWidthStr;
2
3use crate::{
4 buffer::Buffer,
5 layout::Rect,
6 widgets::{
7 block::BlockExt, List, ListDirection, ListState, StatefulWidget, StatefulWidgetRef, Widget,
8 WidgetRef,
9 },
10};
11
12impl Widget for List<'_> {
13 fn render(self, area: Rect, buf: &mut Buffer) {
14 WidgetRef::render_ref(&self, area, buf);
15 }
16}
17
18impl WidgetRef for List<'_> {
19 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
20 let mut state = ListState::default();
21 StatefulWidgetRef::render_ref(self, area, buf, &mut state);
22 }
23}
24
25impl StatefulWidget for List<'_> {
26 type State = ListState;
27
28 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
29 StatefulWidgetRef::render_ref(&self, area, buf, state);
30 }
31}
32
33impl StatefulWidget for &List<'_> {
35 type State = ListState;
36 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
37 StatefulWidgetRef::render_ref(self, area, buf, state);
38 }
39}
40
41impl StatefulWidgetRef for List<'_> {
42 type State = ListState;
43
44 fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
45 buf.set_style(area, self.style);
46 self.block.render_ref(area, buf);
47 let list_area = self.block.inner_if_some(area);
48
49 if list_area.is_empty() {
50 return;
51 }
52
53 if self.items.is_empty() {
54 state.select(None);
55 return;
56 }
57
58 if state.selected.is_some_and(|s| s >= self.items.len()) {
60 state.select(Some(self.items.len().saturating_sub(1)));
61 }
62
63 let list_height = list_area.height as usize;
64
65 let (first_visible_index, last_visible_index) =
66 self.get_items_bounds(state.selected, state.offset, list_height);
67
68 state.offset = first_visible_index;
70
71 let highlight_symbol = self.highlight_symbol.unwrap_or("");
73 let blank_symbol = " ".repeat(highlight_symbol.width());
74
75 let mut current_height = 0;
76 let selection_spacing = self.highlight_spacing.should_add(state.selected.is_some());
77 for (i, item) in self
78 .items
79 .iter()
80 .enumerate()
81 .skip(state.offset)
82 .take(last_visible_index - first_visible_index)
83 {
84 let (x, y) = if self.direction == ListDirection::BottomToTop {
85 current_height += item.height() as u16;
86 (list_area.left(), list_area.bottom() - current_height)
87 } else {
88 let pos = (list_area.left(), list_area.top() + current_height);
89 current_height += item.height() as u16;
90 pos
91 };
92
93 let row_area = Rect {
94 x,
95 y,
96 width: list_area.width,
97 height: item.height() as u16,
98 };
99
100 let item_style = self.style.patch(item.style);
101 buf.set_style(row_area, item_style);
102
103 let is_selected = state.selected.map_or(false, |s| s == i);
104
105 let item_area = if selection_spacing {
106 let highlight_symbol_width = self.highlight_symbol.unwrap_or("").width() as u16;
107 Rect {
108 x: row_area.x + highlight_symbol_width,
109 width: row_area.width.saturating_sub(highlight_symbol_width),
110 ..row_area
111 }
112 } else {
113 row_area
114 };
115 item.content.render_ref(item_area, buf);
116
117 for j in 0..item.content.height() {
118 let symbol = if is_selected && (j == 0 || self.repeat_highlight_symbol) {
122 highlight_symbol
123 } else {
124 &blank_symbol
125 };
126 if selection_spacing {
127 buf.set_stringn(
128 x,
129 y + j as u16,
130 symbol,
131 list_area.width as usize,
132 item_style,
133 );
134 }
135 }
136
137 if is_selected {
138 buf.set_style(row_area, self.highlight_style);
139 }
140 }
141 }
142}
143
144impl List<'_> {
145 fn get_items_bounds(
147 &self,
148 selected: Option<usize>,
149 offset: usize,
150 max_height: usize,
151 ) -> (usize, usize) {
152 let offset = offset.min(self.items.len().saturating_sub(1));
153
154 let mut first_visible_index = offset;
156 let mut last_visible_index = offset;
157
158 let mut height_from_offset = 0;
160
161 for item in self.items.iter().skip(offset) {
164 if height_from_offset + item.height() > max_height {
165 break;
166 }
167
168 height_from_offset += item.height();
169
170 last_visible_index += 1;
171 }
172
173 let index_to_display = self
177 .apply_scroll_padding_to_selected_index(
178 selected,
179 max_height,
180 first_visible_index,
181 last_visible_index,
182 )
183 .unwrap_or(offset);
184
185 while index_to_display >= last_visible_index {
190 height_from_offset =
191 height_from_offset.saturating_add(self.items[last_visible_index].height());
192
193 last_visible_index += 1;
194
195 while height_from_offset > max_height {
198 height_from_offset =
199 height_from_offset.saturating_sub(self.items[first_visible_index].height());
200
201 first_visible_index += 1;
203 }
204 }
205
206 while index_to_display < first_visible_index {
209 first_visible_index -= 1;
210
211 height_from_offset =
212 height_from_offset.saturating_add(self.items[first_visible_index].height());
213
214 while height_from_offset > max_height {
216 last_visible_index -= 1;
217
218 height_from_offset =
219 height_from_offset.saturating_sub(self.items[last_visible_index].height());
220 }
221 }
222
223 (first_visible_index, last_visible_index)
224 }
225
226 fn apply_scroll_padding_to_selected_index(
231 &self,
232 selected: Option<usize>,
233 max_height: usize,
234 first_visible_index: usize,
235 last_visible_index: usize,
236 ) -> Option<usize> {
237 let last_valid_index = self.items.len().saturating_sub(1);
238 let selected = selected?.min(last_valid_index);
239
240 let mut scroll_padding = self.scroll_padding;
245 while scroll_padding > 0 {
246 let mut height_around_selected = 0;
247 for index in selected.saturating_sub(scroll_padding)
248 ..=selected
249 .saturating_add(scroll_padding)
250 .min(last_valid_index)
251 {
252 height_around_selected += self.items[index].height();
253 }
254 if height_around_selected <= max_height {
255 break;
256 }
257 scroll_padding -= 1;
258 }
259
260 Some(
261 if (selected + scroll_padding).min(last_valid_index) >= last_visible_index {
262 selected + scroll_padding
263 } else if selected.saturating_sub(scroll_padding) < first_visible_index {
264 selected.saturating_sub(scroll_padding)
265 } else {
266 selected
267 }
268 .min(last_valid_index),
269 )
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use pretty_assertions::assert_eq;
276 use rstest::{fixture, rstest};
277
278 use super::*;
279 use crate::{
280 backend,
281 layout::{Alignment, Rect},
282 style::{Color, Modifier, Style, Stylize},
283 text::Line,
284 widgets::{Block, HighlightSpacing, ListItem, StatefulWidget, Widget},
285 Terminal,
286 };
287
288 #[fixture]
289 fn single_line_buf() -> Buffer {
290 Buffer::empty(Rect::new(0, 0, 10, 1))
291 }
292
293 #[rstest]
294 fn empty_list(mut single_line_buf: Buffer) {
295 let mut state = ListState::default();
296
297 let items: Vec<ListItem> = Vec::new();
298 let list = List::new(items);
299 state.select_first();
300 StatefulWidget::render(list, single_line_buf.area, &mut single_line_buf, &mut state);
301 assert_eq!(state.selected, None);
302 }
303
304 #[rstest]
305 fn single_item(mut single_line_buf: Buffer) {
306 let mut state = ListState::default();
307
308 let items = vec![ListItem::new("Item 1")];
309 let list = List::new(items);
310 state.select_first();
311 StatefulWidget::render(
312 &list,
313 single_line_buf.area,
314 &mut single_line_buf,
315 &mut state,
316 );
317 assert_eq!(state.selected, Some(0));
318
319 state.select_last();
320 StatefulWidget::render(
321 &list,
322 single_line_buf.area,
323 &mut single_line_buf,
324 &mut state,
325 );
326 assert_eq!(state.selected, Some(0));
327
328 state.select_previous();
329 StatefulWidget::render(
330 &list,
331 single_line_buf.area,
332 &mut single_line_buf,
333 &mut state,
334 );
335 assert_eq!(state.selected, Some(0));
336
337 state.select_next();
338 StatefulWidget::render(
339 &list,
340 single_line_buf.area,
341 &mut single_line_buf,
342 &mut state,
343 );
344 assert_eq!(state.selected, Some(0));
345 }
346
347 fn widget(widget: List<'_>, width: u16, height: u16) -> Buffer {
349 let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
350 Widget::render(widget, buffer.area, &mut buffer);
351 buffer
352 }
353
354 fn stateful_widget(widget: List<'_>, state: &mut ListState, width: u16, height: u16) -> Buffer {
356 let mut buffer = Buffer::empty(Rect::new(0, 0, width, height));
357 StatefulWidget::render(widget, buffer.area, &mut buffer, state);
358 buffer
359 }
360
361 #[test]
362 fn does_not_render_in_small_space() {
363 let items = vec!["Item 0", "Item 1", "Item 2"];
364 let list = List::new(items.clone()).highlight_symbol(">>");
365 let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3));
366
367 Widget::render(list.clone(), Rect::new(0, 0, 0, 3), &mut buffer);
369 assert_eq!(&buffer, &Buffer::empty(buffer.area));
370
371 Widget::render(list.clone(), Rect::new(0, 0, 15, 0), &mut buffer);
373 assert_eq!(&buffer, &Buffer::empty(buffer.area));
374
375 let list = List::new(items)
376 .highlight_symbol(">>")
377 .block(Block::bordered());
378 Widget::render(list, Rect::new(0, 0, 15, 2), &mut buffer);
381 #[rustfmt::skip]
382 let expected = Buffer::with_lines([
383 "┌─────────────┐",
384 "└─────────────┘",
385 " ",
386 ]);
387 assert_eq!(buffer, expected,);
388 }
389
390 #[allow(clippy::too_many_lines)]
391 #[test]
392 fn combinations() {
393 #[track_caller]
394 fn test_case_render<'line, Lines>(items: &[ListItem], expected: Lines)
395 where
396 Lines: IntoIterator,
397 Lines::Item: Into<Line<'line>>,
398 {
399 let list = List::new(items.to_owned()).highlight_symbol(">>");
400 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
401 Widget::render(list, buffer.area, &mut buffer);
402 assert_eq!(buffer, Buffer::with_lines(expected));
403 }
404
405 #[track_caller]
406 fn test_case_render_stateful<'line, Lines>(
407 items: &[ListItem],
408 selected: Option<usize>,
409 expected: Lines,
410 ) where
411 Lines: IntoIterator,
412 Lines::Item: Into<Line<'line>>,
413 {
414 let list = List::new(items.to_owned()).highlight_symbol(">>");
415 let mut state = ListState::default().with_selected(selected);
416 let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
417 StatefulWidget::render(list, buffer.area, &mut buffer, &mut state);
418 assert_eq!(buffer, Buffer::with_lines(expected));
419 }
420
421 let empty_items = Vec::new();
422 let single_item = vec!["Item 0".into()];
423 let multiple_items = vec!["Item 0".into(), "Item 1".into(), "Item 2".into()];
424 let multi_line_items = vec!["Item 0\nLine 2".into(), "Item 1".into(), "Item 2".into()];
425
426 test_case_render(
428 &empty_items,
429 [
430 " ",
431 " ",
432 " ",
433 " ",
434 " ",
435 ],
436 );
437 test_case_render_stateful(
438 &empty_items,
439 None,
440 [
441 " ",
442 " ",
443 " ",
444 " ",
445 " ",
446 ],
447 );
448 test_case_render_stateful(
449 &empty_items,
450 Some(0),
451 [
452 " ",
453 " ",
454 " ",
455 " ",
456 " ",
457 ],
458 );
459
460 test_case_render(
462 &single_item,
463 [
464 "Item 0 ",
465 " ",
466 " ",
467 " ",
468 " ",
469 ],
470 );
471 test_case_render_stateful(
472 &single_item,
473 None,
474 [
475 "Item 0 ",
476 " ",
477 " ",
478 " ",
479 " ",
480 ],
481 );
482 test_case_render_stateful(
483 &single_item,
484 Some(0),
485 [
486 ">>Item 0 ",
487 " ",
488 " ",
489 " ",
490 " ",
491 ],
492 );
493 test_case_render_stateful(
494 &single_item,
495 Some(1),
496 [
497 ">>Item 0 ",
498 " ",
499 " ",
500 " ",
501 " ",
502 ],
503 );
504
505 test_case_render(
507 &multiple_items,
508 [
509 "Item 0 ",
510 "Item 1 ",
511 "Item 2 ",
512 " ",
513 " ",
514 ],
515 );
516 test_case_render_stateful(
517 &multiple_items,
518 None,
519 [
520 "Item 0 ",
521 "Item 1 ",
522 "Item 2 ",
523 " ",
524 " ",
525 ],
526 );
527 test_case_render_stateful(
528 &multiple_items,
529 Some(0),
530 [
531 ">>Item 0 ",
532 " Item 1 ",
533 " Item 2 ",
534 " ",
535 " ",
536 ],
537 );
538 test_case_render_stateful(
539 &multiple_items,
540 Some(1),
541 [
542 " Item 0 ",
543 ">>Item 1 ",
544 " Item 2 ",
545 " ",
546 " ",
547 ],
548 );
549 test_case_render_stateful(
550 &multiple_items,
551 Some(3),
552 [
553 " Item 0 ",
554 " Item 1 ",
555 ">>Item 2 ",
556 " ",
557 " ",
558 ],
559 );
560
561 test_case_render(
563 &multi_line_items,
564 [
565 "Item 0 ",
566 "Line 2 ",
567 "Item 1 ",
568 "Item 2 ",
569 " ",
570 ],
571 );
572 test_case_render_stateful(
573 &multi_line_items,
574 None,
575 [
576 "Item 0 ",
577 "Line 2 ",
578 "Item 1 ",
579 "Item 2 ",
580 " ",
581 ],
582 );
583 test_case_render_stateful(
584 &multi_line_items,
585 Some(0),
586 [
587 ">>Item 0 ",
588 " Line 2 ",
589 " Item 1 ",
590 " Item 2 ",
591 " ",
592 ],
593 );
594 test_case_render_stateful(
595 &multi_line_items,
596 Some(1),
597 [
598 " Item 0 ",
599 " Line 2 ",
600 ">>Item 1 ",
601 " Item 2 ",
602 " ",
603 ],
604 );
605 }
606
607 #[test]
608 fn items() {
609 let list = List::default().items(["Item 0", "Item 1", "Item 2"]);
610 let buffer = widget(list, 10, 5);
611 let expected = Buffer::with_lines([
612 "Item 0 ",
613 "Item 1 ",
614 "Item 2 ",
615 " ",
616 " ",
617 ]);
618 assert_eq!(buffer, expected);
619 }
620
621 #[test]
622 fn empty_strings() {
623 let list = List::new(["Item 0", "", "", "Item 1", "Item 2"])
624 .block(Block::bordered().title("List"));
625 let buffer = widget(list, 10, 7);
626 let expected = Buffer::with_lines([
627 "┌List────┐",
628 "│Item 0 │",
629 "│ │",
630 "│ │",
631 "│Item 1 │",
632 "│Item 2 │",
633 "└────────┘",
634 ]);
635 assert_eq!(buffer, expected);
636 }
637
638 #[test]
639 fn block() {
640 let list = List::new(["Item 0", "Item 1", "Item 2"]).block(Block::bordered().title("List"));
641 let buffer = widget(list, 10, 7);
642 let expected = Buffer::with_lines([
643 "┌List────┐",
644 "│Item 0 │",
645 "│Item 1 │",
646 "│Item 2 │",
647 "│ │",
648 "│ │",
649 "└────────┘",
650 ]);
651 assert_eq!(buffer, expected);
652 }
653
654 #[test]
655 fn style() {
656 let list = List::new(["Item 0", "Item 1", "Item 2"]).style(Style::default().fg(Color::Red));
657 let buffer = widget(list, 10, 5);
658 let expected = Buffer::with_lines([
659 "Item 0 ".red(),
660 "Item 1 ".red(),
661 "Item 2 ".red(),
662 " ".red(),
663 " ".red(),
664 ]);
665 assert_eq!(buffer, expected);
666 }
667
668 #[test]
669 fn highlight_symbol_and_style() {
670 let list = List::new(["Item 0", "Item 1", "Item 2"])
671 .highlight_symbol(">>")
672 .highlight_style(Style::default().fg(Color::Yellow));
673 let mut state = ListState::default();
674 state.select(Some(1));
675 let buffer = stateful_widget(list, &mut state, 10, 5);
676 let expected = Buffer::with_lines([
677 " Item 0 ".into(),
678 ">>Item 1 ".yellow(),
679 " Item 2 ".into(),
680 " ".into(),
681 " ".into(),
682 ]);
683 assert_eq!(buffer, expected);
684 }
685
686 #[test]
687 fn highlight_spacing_default_when_selected() {
688 {
690 let list = List::new(["Item 0", "Item 1", "Item 2"]).highlight_symbol(">>");
691 let mut state = ListState::default();
692 let buffer = stateful_widget(list, &mut state, 10, 5);
693 let expected = Buffer::with_lines([
694 "Item 0 ",
695 "Item 1 ",
696 "Item 2 ",
697 " ",
698 " ",
699 ]);
700 assert_eq!(buffer, expected);
701 }
702
703 {
705 let list = List::new(["Item 0", "Item 1", "Item 2"]).highlight_symbol(">>");
706 let mut state = ListState::default();
707 state.select(Some(1));
708 let buffer = stateful_widget(list, &mut state, 10, 5);
709 let expected = Buffer::with_lines([
710 " Item 0 ",
711 ">>Item 1 ",
712 " Item 2 ",
713 " ",
714 " ",
715 ]);
716 assert_eq!(buffer, expected);
717 }
718 }
719
720 #[test]
721 fn highlight_spacing_default_always() {
722 {
724 let list = List::new(["Item 0", "Item 1", "Item 2"])
725 .highlight_symbol(">>")
726 .highlight_spacing(HighlightSpacing::Always);
727 let mut state = ListState::default();
728 let buffer = stateful_widget(list, &mut state, 10, 5);
729 let expected = Buffer::with_lines([
730 " Item 0 ",
731 " Item 1 ",
732 " Item 2 ",
733 " ",
734 " ",
735 ]);
736 assert_eq!(buffer, expected);
737 }
738
739 {
741 let list = List::new(["Item 0", "Item 1", "Item 2"])
742 .highlight_symbol(">>")
743 .highlight_spacing(HighlightSpacing::Always);
744 let mut state = ListState::default();
745 state.select(Some(1));
746 let buffer = stateful_widget(list, &mut state, 10, 5);
747 let expected = Buffer::with_lines([
748 " Item 0 ",
749 ">>Item 1 ",
750 " Item 2 ",
751 " ",
752 " ",
753 ]);
754 assert_eq!(buffer, expected);
755 }
756 }
757
758 #[test]
759 fn highlight_spacing_default_never() {
760 {
762 let list = List::new(["Item 0", "Item 1", "Item 2"])
763 .highlight_symbol(">>")
764 .highlight_spacing(HighlightSpacing::Never);
765 let mut state = ListState::default();
766 let buffer = stateful_widget(list, &mut state, 10, 5);
767 let expected = Buffer::with_lines([
768 "Item 0 ",
769 "Item 1 ",
770 "Item 2 ",
771 " ",
772 " ",
773 ]);
774 assert_eq!(buffer, expected);
775 }
776
777 {
779 let list = List::new(["Item 0", "Item 1", "Item 2"])
780 .highlight_symbol(">>")
781 .highlight_spacing(HighlightSpacing::Never);
782 let mut state = ListState::default();
783 state.select(Some(1));
784 let buffer = stateful_widget(list, &mut state, 10, 5);
785 let expected = Buffer::with_lines([
786 "Item 0 ",
787 "Item 1 ",
788 "Item 2 ",
789 " ",
790 " ",
791 ]);
792 assert_eq!(buffer, expected);
793 }
794 }
795
796 #[test]
797 fn repeat_highlight_symbol() {
798 let list = List::new(["Item 0\nLine 2", "Item 1", "Item 2"])
799 .highlight_symbol(">>")
800 .highlight_style(Style::default().fg(Color::Yellow))
801 .repeat_highlight_symbol(true);
802 let mut state = ListState::default();
803 state.select(Some(0));
804 let buffer = stateful_widget(list, &mut state, 10, 5);
805 let expected = Buffer::with_lines([
806 ">>Item 0 ".yellow(),
807 ">>Line 2 ".yellow(),
808 " Item 1 ".into(),
809 " Item 2 ".into(),
810 " ".into(),
811 ]);
812 assert_eq!(buffer, expected);
813 }
814
815 #[rstest]
816 #[case::top_to_bottom(ListDirection::TopToBottom, [
817 "Item 0 ",
818 "Item 1 ",
819 "Item 2 ",
820 " ",
821 ])]
822 #[case::top_to_bottom(ListDirection::BottomToTop, [
823 " ",
824 "Item 2 ",
825 "Item 1 ",
826 "Item 0 ",
827 ])]
828 fn list_direction<'line, Lines>(#[case] direction: ListDirection, #[case] expected: Lines)
829 where
830 Lines: IntoIterator,
831 Lines::Item: Into<Line<'line>>,
832 {
833 let list = List::new(["Item 0", "Item 1", "Item 2"]).direction(direction);
834 let buffer = widget(list, 10, 4);
835 assert_eq!(buffer, Buffer::with_lines(expected));
836 }
837
838 #[test]
839 fn truncate_items() {
840 let list = List::new(["Item 0", "Item 1", "Item 2", "Item 3", "Item 4"]);
841 let buffer = widget(list, 10, 3);
842 #[rustfmt::skip]
843 let expected = Buffer::with_lines([
844 "Item 0 ",
845 "Item 1 ",
846 "Item 2 ",
847 ]);
848 assert_eq!(buffer, expected);
849 }
850
851 #[test]
852 fn offset_renders_shifted() {
853 let list = List::new([
854 "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
855 ]);
856 let mut state = ListState::default().with_offset(3);
857 let buffer = stateful_widget(list, &mut state, 6, 3);
858
859 let expected = Buffer::with_lines(["Item 3", "Item 4", "Item 5"]);
860 assert_eq!(buffer, expected);
861 }
862
863 #[rstest]
864 #[case(None, [
865 "Item 0 with a v",
866 "Item 1 ",
867 "Item 2 ",
868 ])]
869 #[case(Some(0), [
870 ">>Item 0 with a",
871 " Item 1 ",
872 " Item 2 ",
873 ])]
874 fn long_lines<'line, Lines>(#[case] selected: Option<usize>, #[case] expected: Lines)
875 where
876 Lines: IntoIterator,
877 Lines::Item: Into<Line<'line>>,
878 {
879 let items = [
880 "Item 0 with a very long line that will be truncated",
881 "Item 1",
882 "Item 2",
883 ];
884 let list = List::new(items).highlight_symbol(">>");
885 let mut state = ListState::default().with_selected(selected);
886 let buffer = stateful_widget(list, &mut state, 15, 3);
887 assert_eq!(buffer, Buffer::with_lines(expected));
888 }
889
890 #[test]
891 fn selected_item_ensures_selected_item_is_visible_when_offset_is_before_visible_range() {
892 let items = [
893 "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
894 ];
895 let list = List::new(items).highlight_symbol(">>");
896 let mut state = ListState::default().with_selected(Some(1)).with_offset(3);
898 let buffer = stateful_widget(list, &mut state, 10, 3);
899
900 #[rustfmt::skip]
901 let expected = Buffer::with_lines([
902 ">>Item 1 ",
903 " Item 2 ",
904 " Item 3 ",
905 ]);
906
907 assert_eq!(buffer, expected);
908 assert_eq!(state.selected, Some(1));
909 assert_eq!(
910 state.offset, 1,
911 "did not scroll the selected item into view"
912 );
913 }
914
915 #[test]
916 fn selected_item_ensures_selected_item_is_visible_when_offset_is_after_visible_range() {
917 let items = [
918 "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6",
919 ];
920 let list = List::new(items).highlight_symbol(">>");
921 let mut state = ListState::default().with_selected(Some(6)).with_offset(3);
923 let buffer = stateful_widget(list, &mut state, 10, 3);
924
925 #[rustfmt::skip]
926 let expected = Buffer::with_lines([
927 " Item 4 ",
928 " Item 5 ",
929 ">>Item 6 ",
930 ]);
931
932 assert_eq!(buffer, expected);
933 assert_eq!(state.selected, Some(6));
934 assert_eq!(
935 state.offset, 4,
936 "did not scroll the selected item into view"
937 );
938 }
939
940 #[test]
941 fn can_be_stylized() {
942 assert_eq!(
943 List::new::<Vec<&str>>(vec![])
944 .black()
945 .on_white()
946 .bold()
947 .not_dim()
948 .style,
949 Style::default()
950 .fg(Color::Black)
951 .bg(Color::White)
952 .add_modifier(Modifier::BOLD)
953 .remove_modifier(Modifier::DIM)
954 );
955 }
956
957 #[test]
958 fn with_alignment() {
959 let list = List::new([
960 Line::from("Left").alignment(Alignment::Left),
961 Line::from("Center").alignment(Alignment::Center),
962 Line::from("Right").alignment(Alignment::Right),
963 ]);
964 let buffer = widget(list, 10, 4);
965 let expected = Buffer::with_lines(["Left ", " Center ", " Right", ""]);
966 assert_eq!(buffer, expected);
967 }
968
969 #[test]
970 fn alignment_odd_line_odd_area() {
971 let list = List::new([
972 Line::from("Odd").alignment(Alignment::Left),
973 Line::from("Even").alignment(Alignment::Center),
974 Line::from("Width").alignment(Alignment::Right),
975 ]);
976 let buffer = widget(list, 7, 4);
977 let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
978 assert_eq!(buffer, expected);
979 }
980
981 #[test]
982 fn alignment_even_line_even_area() {
983 let list = List::new([
984 Line::from("Odd").alignment(Alignment::Left),
985 Line::from("Even").alignment(Alignment::Center),
986 Line::from("Width").alignment(Alignment::Right),
987 ]);
988 let buffer = widget(list, 6, 4);
989 let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
990 assert_eq!(buffer, expected);
991 }
992
993 #[test]
994 fn alignment_odd_line_even_area() {
995 let list = List::new([
996 Line::from("Odd").alignment(Alignment::Left),
997 Line::from("Even").alignment(Alignment::Center),
998 Line::from("Width").alignment(Alignment::Right),
999 ]);
1000 let buffer = widget(list, 8, 4);
1001 let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
1002 assert_eq!(buffer, expected);
1003 }
1004
1005 #[test]
1006 fn alignment_even_line_odd_area() {
1007 let list = List::new([
1008 Line::from("Odd").alignment(Alignment::Left),
1009 Line::from("Even").alignment(Alignment::Center),
1010 Line::from("Width").alignment(Alignment::Right),
1011 ]);
1012 let buffer = widget(list, 6, 4);
1013 let expected = Buffer::with_lines(["Odd ", " Even ", " Width", ""]);
1014 assert_eq!(buffer, expected);
1015 }
1016
1017 #[test]
1018 fn alignment_zero_line_width() {
1019 let list = List::new([Line::from("This line has zero width").alignment(Alignment::Center)]);
1020 let buffer = widget(list, 0, 2);
1021 assert_eq!(buffer, Buffer::with_lines([""; 2]));
1022 }
1023
1024 #[test]
1025 fn alignment_zero_area_width() {
1026 let list = List::new([Line::from("Text").alignment(Alignment::Left)]);
1027 let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
1028 Widget::render(list, Rect::new(0, 0, 4, 0), &mut buffer);
1029 assert_eq!(buffer, Buffer::with_lines([" "]));
1030 }
1031
1032 #[test]
1033 fn alignment_line_less_than_width() {
1034 let list = List::new([Line::from("Small").alignment(Alignment::Center)]);
1035 let buffer = widget(list, 10, 2);
1036 let expected = Buffer::with_lines([" Small ", ""]);
1037 assert_eq!(buffer, expected);
1038 }
1039
1040 #[test]
1041 fn alignment_line_equal_to_width() {
1042 let list = List::new([Line::from("Exact").alignment(Alignment::Left)]);
1043 let buffer = widget(list, 5, 2);
1044 assert_eq!(buffer, Buffer::with_lines(["Exact", ""]));
1045 }
1046
1047 #[test]
1048 fn alignment_line_greater_than_width() {
1049 let list = List::new([Line::from("Large line").alignment(Alignment::Left)]);
1050 let buffer = widget(list, 5, 2);
1051 assert_eq!(buffer, Buffer::with_lines(["Large", ""]));
1052 }
1053
1054 #[rstest]
1055 #[case::no_padding(
1056 4,
1057 2, 0, Some(2), [
1061 ">> Item 2 ",
1062 " Item 3 ",
1063 " Item 4 ",
1064 " Item 5 ",
1065 ]
1066 )]
1067 #[case::one_before(
1068 4,
1069 2, 1, Some(2), [
1073 " Item 1 ",
1074 ">> Item 2 ",
1075 " Item 3 ",
1076 " Item 4 ",
1077 ]
1078 )]
1079 #[case::one_after(
1080 4,
1081 1, 1, Some(4), [
1085 " Item 2 ",
1086 " Item 3 ",
1087 ">> Item 4 ",
1088 " Item 5 ",
1089 ]
1090 )]
1091 #[case::check_padding_overflow(
1092 4,
1093 1, 2, Some(4), [
1097 " Item 2 ",
1098 " Item 3 ",
1099 ">> Item 4 ",
1100 " Item 5 ",
1101 ]
1102 )]
1103 #[case::no_padding_offset_behavior(
1104 5, 2, 0, Some(3), [
1109 " Item 2 ",
1110 ">> Item 3 ",
1111 " Item 4 ",
1112 " Item 5 ",
1113 " ",
1114 ]
1115 )]
1116 #[case::two_before(
1117 5, 2, 2, Some(3), [
1122 " Item 1 ",
1123 " Item 2 ",
1124 ">> Item 3 ",
1125 " Item 4 ",
1126 " Item 5 ",
1127 ]
1128 )]
1129 #[case::keep_selected_visible(
1130 4,
1131 0, 4, Some(1), [
1135 " Item 0 ",
1136 ">> Item 1 ",
1137 " Item 2 ",
1138 " Item 3 ",
1139 ]
1140 )]
1141 fn with_padding<'line, Lines>(
1142 #[case] render_height: u16,
1143 #[case] offset: usize,
1144 #[case] padding: usize,
1145 #[case] selected: Option<usize>,
1146 #[case] expected: Lines,
1147 ) where
1148 Lines: IntoIterator,
1149 Lines::Item: Into<Line<'line>>,
1150 {
1151 let backend = backend::TestBackend::new(10, render_height);
1152 let mut terminal = Terminal::new(backend).unwrap();
1153 let mut state = ListState::default();
1154
1155 *state.offset_mut() = offset;
1156 state.select(selected);
1157
1158 let list = List::new(["Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5"])
1159 .scroll_padding(padding)
1160 .highlight_symbol(">> ");
1161 terminal
1162 .draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
1163 .unwrap();
1164 terminal.backend().assert_buffer_lines(expected);
1165 }
1166
1167 #[test]
1171 fn padding_flicker() {
1172 let backend = backend::TestBackend::new(10, 5);
1173 let mut terminal = Terminal::new(backend).unwrap();
1174 let mut state = ListState::default();
1175
1176 *state.offset_mut() = 2;
1177 state.select(Some(4));
1178
1179 let items = [
1180 "Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7",
1181 ];
1182 let list = List::new(items).scroll_padding(3).highlight_symbol(">> ");
1183
1184 terminal
1185 .draw(|f| f.render_stateful_widget(&list, f.area(), &mut state))
1186 .unwrap();
1187
1188 let offset_after_render = state.offset();
1189
1190 terminal
1191 .draw(|f| f.render_stateful_widget(&list, f.area(), &mut state))
1192 .unwrap();
1193
1194 assert_eq!(offset_after_render, state.offset());
1196 }
1197
1198 #[test]
1199 fn padding_inconsistent_item_sizes() {
1200 let backend = backend::TestBackend::new(10, 3);
1201 let mut terminal = Terminal::new(backend).unwrap();
1202 let mut state = ListState::default().with_offset(0).with_selected(Some(3));
1203
1204 let items = [
1205 ListItem::new("Item 0"),
1206 ListItem::new("Item 1"),
1207 ListItem::new("Item 2"),
1208 ListItem::new("Item 3"),
1209 ListItem::new("Item 4\nTest\nTest"),
1210 ListItem::new("Item 5"),
1211 ];
1212 let list = List::new(items).scroll_padding(1).highlight_symbol(">> ");
1213
1214 terminal
1215 .draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
1216 .unwrap();
1217
1218 #[rustfmt::skip]
1219 let expected = [
1220 " Item 1 ",
1221 " Item 2 ",
1222 ">> Item 3 ",
1223 ];
1224 terminal.backend().assert_buffer_lines(expected);
1225 }
1226
1227 #[test]
1230 fn padding_offset_pushback_break() {
1231 let backend = backend::TestBackend::new(10, 4);
1232 let mut terminal = Terminal::new(backend).unwrap();
1233 let mut state = ListState::default();
1234
1235 *state.offset_mut() = 1;
1236 state.select(Some(2));
1237
1238 let items = [
1239 ListItem::new("Item 0\nTest\nTest"),
1240 ListItem::new("Item 1"),
1241 ListItem::new("Item 2"),
1242 ListItem::new("Item 3"),
1243 ];
1244 let list = List::new(items).scroll_padding(2).highlight_symbol(">> ");
1245
1246 terminal
1247 .draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
1248 .unwrap();
1249
1250 terminal.backend().assert_buffer_lines([
1251 " Item 1 ",
1252 ">> Item 2 ",
1253 " Item 3 ",
1254 " ",
1255 ]);
1256 }
1257
1258 #[rstest]
1263 #[case::under(">>>>", "Item1", ">>>>Item1 ")] #[case::exact(">>>>>", "Item1", ">>>>>Item1")] #[case::overflow(">>>>>>", "Item1", ">>>>>>Item")] fn highlight_symbol_overflow(
1267 #[case] highlight_symbol: &str,
1268 #[case] item: &str,
1269 #[case] expected: &str,
1270 mut single_line_buf: Buffer,
1271 ) {
1272 let list = List::new([item]).highlight_symbol(highlight_symbol);
1273 let mut state = ListState::default();
1274 state.select(Some(0));
1275 StatefulWidget::render(list, single_line_buf.area, &mut single_line_buf, &mut state);
1276 assert_eq!(single_line_buf, Buffer::with_lines([expected]));
1277 }
1278}