ratatui_widgets/list/
rendering.rs

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 the selected index is out of bounds, set it to the last item
48        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        // Important: this changes the state's offset to be the beginning of the now viewable items
58        state.offset = first_visible_index;
59
60        // Get our set highlighted symbol (if one was set)
61        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                    // if the item is selected, we need to display the highlight symbol:
112                    // - either for the first line of the item only,
113                    // - or for each line of the item if the appropriate option is set
114                    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    /// Given an offset, calculate which items can fit in a given area
129    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        // Note: visible here implies visible in the given area
138        let mut first_visible_index = offset;
139        let mut last_visible_index = offset;
140
141        // Current height of all items in the list to render, beginning at the offset
142        let mut height_from_offset = 0;
143
144        // Calculate the last visible index and total height of the items
145        // that will fit in the available space
146        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        // Get the selected index and apply scroll_padding to it, but still honor the offset if
157        // nothing is selected. This allows for the list to stay at a position after select()ing
158        // None.
159        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        // Recall that last_visible_index is the index of what we
169        // can render up to in the given space after the offset
170        // If we have an item selected that is out of the viewable area (or
171        // the offset is still set), we still need to show this item
172        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            // Now we need to hide previous items since we didn't have space
179            // for the selected/offset item
180            while height_from_offset > max_height {
181                height_from_offset =
182                    height_from_offset.saturating_sub(self.items[first_visible_index].height());
183
184                // Remove this item to view by starting at the next item index
185                first_visible_index += 1;
186            }
187        }
188
189        // Here we're doing something similar to what we just did above
190        // If the selected item index is not in the viewable area, let's try to show the item
191        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            // Don't show an item if it is beyond our viewable height
198            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    /// Applies scroll padding to the selected index, reducing the padding value to keep the
210    /// selected item on screen even with items of inconsistent sizes
211    ///
212    /// This function is sensitive to how the bounds checking function handles item height
213    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        // The bellow loop handles situations where the list item sizes may not be consistent,
224        // where the offset would have excluded some items that we want to include, or could
225        // cause the offset value to be set to an inconsistent value each time we render.
226        // The padding value will be reduced in case any of these issues would occur
227        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    /// helper method to render a widget to an empty buffer with the default state
334    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    /// helper method to render a widget to an empty buffer with a given state
341    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        // attempt to render into an area of the buffer with 0 width
354        Widget::render(list.clone(), Rect::new(0, 0, 0, 3), &mut buffer);
355        assert_eq!(&buffer, &Buffer::empty(buffer.area));
356
357        // attempt to render into an area of the buffer with 0 height
358        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        // attempt to render into an area of the buffer with zero height after
365        // setting the block borders
366        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        // empty list
413        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        // single item
447        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        // multiple items
492        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        // multi line items
548        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        // when not selected
694        {
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        // when selected
709        {
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        // when not selected
728        {
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        // when selected
745        {
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        // when not selected
766        {
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        // when selected
783        {
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        // Set the initial visible range to items 3, 4, and 5
903        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        // Set the initial visible range to items 3, 4, and 5
928        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, // Offset
1064        0, // Padding
1065        Some(2), // Selected
1066        [
1067            ">> Item 2 ",
1068            "   Item 3 ",
1069            "   Item 4 ",
1070            "   Item 5 ",
1071        ]
1072    )]
1073    #[case::one_before(
1074        4,
1075        2, // Offset
1076        1, // Padding
1077        Some(2), // Selected
1078        [
1079            "   Item 1 ",
1080            ">> Item 2 ",
1081            "   Item 3 ",
1082            "   Item 4 ",
1083        ]
1084    )]
1085    #[case::one_after(
1086        4,
1087        1, // Offset
1088        1, // Padding
1089        Some(4), // Selected
1090        [
1091            "   Item 2 ",
1092            "   Item 3 ",
1093            ">> Item 4 ",
1094            "   Item 5 ",
1095        ]
1096    )]
1097    #[case::check_padding_overflow(
1098        4,
1099        1, // Offset
1100        2, // Padding
1101        Some(4), // Selected
1102        [
1103            "   Item 2 ",
1104            "   Item 3 ",
1105            ">> Item 4 ",
1106            "   Item 5 ",
1107        ]
1108    )]
1109    #[case::no_padding_offset_behavior(
1110        5, // Render Area Height
1111        2, // Offset
1112        0, // Padding
1113        Some(3), // Selected
1114        [
1115            "   Item 2 ",
1116            ">> Item 3 ",
1117            "   Item 4 ",
1118            "   Item 5 ",
1119            "          ",
1120        ]
1121    )]
1122    #[case::two_before(
1123        5, // Render Area Height
1124        2, // Offset
1125        2, // Padding
1126        Some(3), // Selected
1127        [
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, // Offset
1138        4, // Padding
1139        Some(1), // Selected
1140        [
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    /// If there isn't enough room for the selected item and the requested padding the list can jump
1171    /// up and down every frame if something isn't done about it. This code tests to make sure that
1172    /// isn't currently happening
1173    #[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        // Offset after rendering twice should remain the same as after once
1193        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    // Tests to make sure when it's pushing back the first visible index value that it doesnt
1223    // include an item that's too large
1224    #[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    /// Regression test for a bug where highlight symbol being greater than width caused a panic due
1253    /// to subtraction with underflow.
1254    ///
1255    /// See [#949](https://github.com/ratatui/ratatui/pull/949) for details
1256    #[rstest]
1257    #[case::under(">>>>", "Item1", ">>>>Item1 ")] // enough space to render the highlight symbol
1258    #[case::exact(">>>>>", "Item1", ">>>>>Item1")] // exact space to render the highlight symbol
1259    #[case::overflow(">>>>>>", "Item1", ">>>>>>Item")] // not enough space
1260    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}