ratatui/widgets/list/
rendering.rs

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
33// Note: remove this when StatefulWidgetRef is stabilized and replace with the blanket impl
34impl 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 the selected index is out of bounds, set it to the last item
59        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        // Important: this changes the state's offset to be the beginning of the now viewable items
69        state.offset = first_visible_index;
70
71        // Get our set highlighted symbol (if one was set)
72        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                // if the item is selected, we need to display the highlight symbol:
119                // - either for the first line of the item only,
120                // - or for each line of the item if the appropriate option is set
121                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    /// Given an offset, calculate which items can fit in a given area
146    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        // Note: visible here implies visible in the given area
155        let mut first_visible_index = offset;
156        let mut last_visible_index = offset;
157
158        // Current height of all items in the list to render, beginning at the offset
159        let mut height_from_offset = 0;
160
161        // Calculate the last visible index and total height of the items
162        // that will fit in the available space
163        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        // Get the selected index and apply scroll_padding to it, but still honor the offset if
174        // nothing is selected. This allows for the list to stay at a position after select()ing
175        // None.
176        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        // Recall that last_visible_index is the index of what we
186        // can render up to in the given space after the offset
187        // If we have an item selected that is out of the viewable area (or
188        // the offset is still set), we still need to show this item
189        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            // Now we need to hide previous items since we didn't have space
196            // for the selected/offset item
197            while height_from_offset > max_height {
198                height_from_offset =
199                    height_from_offset.saturating_sub(self.items[first_visible_index].height());
200
201                // Remove this item to view by starting at the next item index
202                first_visible_index += 1;
203            }
204        }
205
206        // Here we're doing something similar to what we just did above
207        // If the selected item index is not in the viewable area, let's try to show the item
208        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            // Don't show an item if it is beyond our viewable height
215            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    /// Applies scroll padding to the selected index, reducing the padding value to keep the
227    /// selected item on screen even with items of inconsistent sizes
228    ///
229    /// This function is sensitive to how the bounds checking function handles item height
230    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        // The bellow loop handles situations where the list item sizes may not be consistent,
241        // where the offset would have excluded some items that we want to include, or could
242        // cause the offset value to be set to an inconsistent value each time we render.
243        // The padding value will be reduced in case any of these issues would occur
244        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    /// helper method to render a widget to an empty buffer with the default state
348    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    /// helper method to render a widget to an empty buffer with a given state
355    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        // attempt to render into an area of the buffer with 0 width
368        Widget::render(list.clone(), Rect::new(0, 0, 0, 3), &mut buffer);
369        assert_eq!(&buffer, &Buffer::empty(buffer.area));
370
371        // attempt to render into an area of the buffer with 0 height
372        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        // attempt to render into an area of the buffer with zero height after
379        // setting the block borders
380        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        // empty list
427        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        // single item
461        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        // multiple items
506        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        // multi line items
562        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        // when not selected
689        {
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        // when selected
704        {
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        // when not selected
723        {
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        // when selected
740        {
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        // when not selected
761        {
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        // when selected
778        {
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        // Set the initial visible range to items 3, 4, and 5
897        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        // Set the initial visible range to items 3, 4, and 5
922        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, // Offset
1058        0, // Padding
1059        Some(2), // Selected
1060        [
1061            ">> Item 2 ",
1062            "   Item 3 ",
1063            "   Item 4 ",
1064            "   Item 5 ",
1065        ]
1066    )]
1067    #[case::one_before(
1068        4,
1069        2, // Offset
1070        1, // Padding
1071        Some(2), // Selected
1072        [
1073            "   Item 1 ",
1074            ">> Item 2 ",
1075            "   Item 3 ",
1076            "   Item 4 ",
1077        ]
1078    )]
1079    #[case::one_after(
1080        4,
1081        1, // Offset
1082        1, // Padding
1083        Some(4), // Selected
1084        [
1085            "   Item 2 ",
1086            "   Item 3 ",
1087            ">> Item 4 ",
1088            "   Item 5 ",
1089        ]
1090    )]
1091    #[case::check_padding_overflow(
1092        4,
1093        1, // Offset
1094        2, // Padding
1095        Some(4), // Selected
1096        [
1097            "   Item 2 ",
1098            "   Item 3 ",
1099            ">> Item 4 ",
1100            "   Item 5 ",
1101        ]
1102    )]
1103    #[case::no_padding_offset_behavior(
1104        5, // Render Area Height
1105        2, // Offset
1106        0, // Padding
1107        Some(3), // Selected
1108        [
1109            "   Item 2 ",
1110            ">> Item 3 ",
1111            "   Item 4 ",
1112            "   Item 5 ",
1113            "          ",
1114        ]
1115    )]
1116    #[case::two_before(
1117        5, // Render Area Height
1118        2, // Offset
1119        2, // Padding
1120        Some(3), // Selected
1121        [
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, // Offset
1132        4, // Padding
1133        Some(1), // Selected
1134        [
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    /// If there isn't enough room for the selected item and the requested padding the list can jump
1168    /// up and down every frame if something isn't done about it. This code tests to make sure that
1169    /// isn't currently happening
1170    #[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        // Offset after rendering twice should remain the same as after once
1195        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    // Tests to make sure when it's pushing back the first visible index value that it doesnt
1228    // include an item that's too large
1229    #[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    /// Regression test for a bug where highlight symbol being greater than width caused a panic due
1259    /// to subtraction with underflow.
1260    ///
1261    /// See [#949](https://github.com/ratatui/ratatui/pull/949) for details
1262    #[rstest]
1263    #[case::under(">>>>", "Item1", ">>>>Item1 ")] // enough space to render the highlight symbol
1264    #[case::exact(">>>>>", "Item1", ">>>>>Item1")] // exact space to render the highlight symbol
1265    #[case::overflow(">>>>>>", "Item1", ">>>>>>Item")] // not enough space
1266    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}