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