ratatui_widgets/
list.rs

1//! The [`List`] widget is used to display a list of items and allows selecting one or multiple
2//! items.
3
4use alloc::vec::Vec;
5
6use ratatui_core::style::{Style, Styled};
7use ratatui_core::text::Line;
8use strum::{Display, EnumString};
9
10pub use self::item::ListItem;
11pub use self::state::ListState;
12use crate::block::Block;
13use crate::table::HighlightSpacing;
14
15mod item;
16mod rendering;
17mod state;
18
19/// A widget to display several items among which one can be selected (optional)
20///
21/// A list is a collection of [`ListItem`]s.
22///
23/// This is different from a [`Table`] because it does not handle columns, headers or footers and
24/// the item's height is automatically determined. A `List` can also be put in reverse order (i.e.
25/// *bottom to top*) whereas a [`Table`] cannot.
26///
27/// [`Table`]: crate::table::Table
28///
29/// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`].
30///
31/// [`List`] is also a [`StatefulWidget`], which means you can use it with [`ListState`] to allow
32/// the user to [scroll] through items and [select] one of them.
33///
34/// See the list in the [Examples] directory for a more in depth example of the various
35/// configuration options and for how to handle state.
36///
37/// [Examples]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
38///
39/// # Fluent setters
40///
41/// - [`List::highlight_style`] sets the style of the selected item.
42/// - [`List::highlight_symbol`] sets the symbol to be displayed in front of the selected item.
43/// - [`List::repeat_highlight_symbol`] sets whether to repeat the symbol and style over selected
44///   multi-line items
45/// - [`List::direction`] sets the list direction
46///
47/// # Examples
48///
49/// ```
50/// use ratatui::Frame;
51/// use ratatui::layout::Rect;
52/// use ratatui::style::{Style, Stylize};
53/// use ratatui::widgets::{Block, List, ListDirection, ListItem};
54///
55/// # fn ui(frame: &mut Frame) {
56/// # let area = Rect::default();
57/// let items = ["Item 1", "Item 2", "Item 3"];
58/// let list = List::new(items)
59///     .block(Block::bordered().title("List"))
60///     .style(Style::new().white())
61///     .highlight_style(Style::new().italic())
62///     .highlight_symbol(">>")
63///     .repeat_highlight_symbol(true)
64///     .direction(ListDirection::BottomToTop);
65///
66/// frame.render_widget(list, area);
67/// # }
68/// ```
69///
70/// # Stateful example
71///
72/// ```rust
73/// use ratatui::Frame;
74/// use ratatui::layout::Rect;
75/// use ratatui::style::{Style, Stylize};
76/// use ratatui::widgets::{Block, List, ListState};
77///
78/// # fn ui(frame: &mut Frame) {
79/// # let area = Rect::default();
80/// // This should be stored outside of the function in your application state.
81/// let mut state = ListState::default();
82/// let items = ["Item 1", "Item 2", "Item 3"];
83/// let list = List::new(items)
84///     .block(Block::bordered().title("List"))
85///     .highlight_style(Style::new().reversed())
86///     .highlight_symbol(">>")
87///     .repeat_highlight_symbol(true);
88///
89/// frame.render_stateful_widget(list, area, &mut state);
90/// # }
91/// ```
92///
93/// In addition to `List::new`, any iterator whose element is convertible to `ListItem` can be
94/// collected into `List`.
95///
96/// ```
97/// use ratatui::widgets::List;
98///
99/// (0..5).map(|i| format!("Item{i}")).collect::<List>();
100/// ```
101///
102/// [`ListState`]: crate::list::ListState
103/// [scroll]: crate::list::ListState::offset
104/// [select]: crate::list::ListState::select
105/// [`Text::alignment`]: ratatui_core::text::Text::alignment
106/// [`StatefulWidget`]: ratatui_core::widgets::StatefulWidget
107/// [`Widget`]: ratatui_core::widgets::Widget
108#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
109pub struct List<'a> {
110    /// An optional block to wrap the widget in
111    pub(crate) block: Option<Block<'a>>,
112    /// The items in the list
113    pub(crate) items: Vec<ListItem<'a>>,
114    /// Style used as a base style for the widget
115    pub(crate) style: Style,
116    /// List display direction
117    pub(crate) direction: ListDirection,
118    /// Style used to render selected item
119    pub(crate) highlight_style: Style,
120    /// Symbol in front of the selected item (Shift all items to the right)
121    pub(crate) highlight_symbol: Option<Line<'a>>,
122    /// Whether to repeat the highlight symbol for each line of the selected item
123    pub(crate) repeat_highlight_symbol: bool,
124    /// Decides when to allocate spacing for the selection symbol
125    pub(crate) highlight_spacing: HighlightSpacing,
126    /// How many items to try to keep visible before and after the selected item
127    pub(crate) scroll_padding: usize,
128}
129
130/// Defines the direction in which the list will be rendered.
131///
132/// If there are too few items to fill the screen, the list will stick to the starting edge.
133///
134/// See [`List::direction`].
135#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
136#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
137pub enum ListDirection {
138    /// The first value is on the top, going to the bottom
139    #[default]
140    TopToBottom,
141    /// The first value is on the bottom, going to the top.
142    BottomToTop,
143}
144
145impl<'a> List<'a> {
146    /// Creates a new list from [`ListItem`]s
147    ///
148    /// The `items` parameter accepts any value that can be converted into an iterator of
149    /// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
150    ///
151    /// # Example
152    ///
153    /// From a slice of [`&str`]
154    ///
155    /// ```
156    /// use ratatui::widgets::List;
157    ///
158    /// let list = List::new(["Item 1", "Item 2"]);
159    /// ```
160    ///
161    /// From [`Text`]
162    ///
163    /// ```
164    /// use ratatui::style::{Style, Stylize};
165    /// use ratatui::text::Text;
166    /// use ratatui::widgets::List;
167    ///
168    /// let list = List::new([
169    ///     Text::styled("Item 1", Style::new().red()),
170    ///     Text::styled("Item 2", Style::new().red()),
171    /// ]);
172    /// ```
173    ///
174    /// You can also create an empty list using the [`Default`] implementation and use the
175    /// [`List::items`] fluent setter.
176    ///
177    /// ```rust
178    /// use ratatui::widgets::List;
179    ///
180    /// let empty_list = List::default();
181    /// let filled_list = empty_list.items(["Item 1"]);
182    /// ```
183    ///
184    /// [`Text`]: ratatui_core::text::Text
185    pub fn new<T>(items: T) -> Self
186    where
187        T: IntoIterator,
188        T::Item: Into<ListItem<'a>>,
189    {
190        Self {
191            block: None,
192            style: Style::default(),
193            items: items.into_iter().map(Into::into).collect(),
194            direction: ListDirection::default(),
195            ..Self::default()
196        }
197    }
198
199    /// Set the items
200    ///
201    /// The `items` parameter accepts any value that can be converted into an iterator of
202    /// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
203    ///
204    /// This is a fluent setter method which must be chained or used as it consumes self.
205    ///
206    /// # Example
207    ///
208    /// ```rust
209    /// use ratatui::widgets::List;
210    ///
211    /// let list = List::default().items(["Item 1", "Item 2"]);
212    /// ```
213    ///
214    /// [`Text`]: ratatui_core::text::Text
215    #[must_use = "method moves the value of self and returns the modified value"]
216    pub fn items<T>(mut self, items: T) -> Self
217    where
218        T: IntoIterator,
219        T::Item: Into<ListItem<'a>>,
220    {
221        self.items = items.into_iter().map(Into::into).collect();
222        self
223    }
224
225    /// Wraps the list with a custom [`Block`] widget.
226    ///
227    /// The `block` parameter holds the specified [`Block`] to be created around the [`List`]
228    ///
229    /// This is a fluent setter method which must be chained or used as it consumes self
230    ///
231    /// # Examples
232    ///
233    /// ```rust
234    /// use ratatui::widgets::{Block, List};
235    ///
236    /// let items = ["Item 1"];
237    /// let block = Block::bordered().title("List");
238    /// let list = List::new(items).block(block);
239    /// ```
240    #[must_use = "method moves the value of self and returns the modified value"]
241    pub fn block(mut self, block: Block<'a>) -> Self {
242        self.block = Some(block);
243        self
244    }
245
246    /// Sets the base style of the widget
247    ///
248    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
249    /// your own type that implements [`Into<Style>`]).
250    ///
251    /// All text rendered by the widget will use this style, unless overridden by [`Block::style`],
252    /// [`ListItem::style`], or the styles of the [`ListItem`]'s content.
253    ///
254    /// This is a fluent setter method which must be chained or used as it consumes self
255    ///
256    /// # Examples
257    ///
258    /// ```rust
259    /// use ratatui::style::{Style, Stylize};
260    /// use ratatui::widgets::List;
261    ///
262    /// let items = ["Item 1"];
263    /// let list = List::new(items).style(Style::new().red().italic());
264    /// ```
265    ///
266    /// `List` also implements the [`Styled`] trait, which means you can use style shorthands from
267    /// the [`Stylize`] trait to set the style of the widget more concisely.
268    ///
269    /// [`Stylize`]: ratatui_core::style::Stylize
270    ///
271    /// ```rust
272    /// use ratatui::style::Stylize;
273    /// use ratatui::widgets::List;
274    ///
275    /// let items = ["Item 1"];
276    /// let list = List::new(items).red().italic();
277    /// ```
278    ///
279    /// [`Color`]: ratatui_core::style::Color
280    #[must_use = "method moves the value of self and returns the modified value"]
281    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
282        self.style = style.into();
283        self
284    }
285
286    /// Set the symbol to be displayed in front of the selected item
287    ///
288    /// By default there are no highlight symbol.
289    ///
290    /// This is a fluent setter method which must be chained or used as it consumes self
291    ///
292    /// # Examples
293    ///
294    /// ```rust
295    /// use ratatui::widgets::List;
296    ///
297    /// let items = ["Item 1", "Item 2"];
298    /// let list = List::new(items).highlight_symbol(">>");
299    /// ```
300    #[must_use = "method moves the value of self and returns the modified value"]
301    pub fn highlight_symbol<L: Into<Line<'a>>>(mut self, highlight_symbol: L) -> Self {
302        self.highlight_symbol = Some(highlight_symbol.into());
303        self
304    }
305
306    /// Set the style of the selected item
307    ///
308    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
309    /// your own type that implements [`Into<Style>`]).
310    ///
311    /// This style will be applied to the entire item, including the
312    /// [highlight symbol](List::highlight_symbol) if it is displayed, and will override any style
313    /// set on the item or on the individual cells.
314    ///
315    /// This is a fluent setter method which must be chained or used as it consumes self
316    ///
317    /// # Examples
318    ///
319    /// ```rust
320    /// use ratatui::style::{Style, Stylize};
321    /// use ratatui::widgets::List;
322    ///
323    /// let items = ["Item 1", "Item 2"];
324    /// let list = List::new(items).highlight_style(Style::new().red().italic());
325    /// ```
326    ///
327    /// [`Color`]: ratatui_core::style::Color
328    #[must_use = "method moves the value of self and returns the modified value"]
329    pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
330        self.highlight_style = style.into();
331        self
332    }
333
334    /// Set whether to repeat the highlight symbol and style over selected multi-line items
335    ///
336    /// This is `false` by default.
337    ///
338    /// This is a fluent setter method which must be chained or used as it consumes self
339    #[must_use = "method moves the value of self and returns the modified value"]
340    pub const fn repeat_highlight_symbol(mut self, repeat: bool) -> Self {
341        self.repeat_highlight_symbol = repeat;
342        self
343    }
344
345    /// Set when to show the highlight spacing
346    ///
347    /// The highlight spacing is the spacing that is allocated for the selection symbol (if enabled)
348    /// and is used to shift the list when an item is selected. This method allows you to configure
349    /// when this spacing is allocated.
350    ///
351    /// - [`HighlightSpacing::Always`] will always allocate the spacing, regardless of whether an
352    ///   item is selected or not. This means that the table will never change size, regardless of
353    ///   if an item is selected or not.
354    /// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if an item is selected.
355    ///   This means that the table will shift when an item is selected. This is the default setting
356    ///   for backwards compatibility, but it is recommended to use `HighlightSpacing::Always` for a
357    ///   better user experience.
358    /// - [`HighlightSpacing::Never`] will never allocate the spacing, regardless of whether an item
359    ///   is selected or not. This means that the highlight symbol will never be drawn.
360    ///
361    /// This is a fluent setter method which must be chained or used as it consumes self
362    ///
363    /// # Examples
364    ///
365    /// ```rust
366    /// use ratatui::widgets::{HighlightSpacing, List};
367    ///
368    /// let items = ["Item 1"];
369    /// let list = List::new(items).highlight_spacing(HighlightSpacing::Always);
370    /// ```
371    #[must_use = "method moves the value of self and returns the modified value"]
372    pub const fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
373        self.highlight_spacing = value;
374        self
375    }
376
377    /// Defines the list direction (up or down)
378    ///
379    /// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*.
380    /// If there is too few items to fill the screen, the list will stick to the starting edge.
381    ///
382    /// This is a fluent setter method which must be chained or used as it consumes self
383    ///
384    /// # Example
385    ///
386    /// Bottom to top
387    ///
388    /// ```rust
389    /// use ratatui::widgets::{List, ListDirection};
390    ///
391    /// let items = ["Item 1"];
392    /// let list = List::new(items).direction(ListDirection::BottomToTop);
393    /// ```
394    #[must_use = "method moves the value of self and returns the modified value"]
395    pub const fn direction(mut self, direction: ListDirection) -> Self {
396        self.direction = direction;
397        self
398    }
399
400    /// Sets the number of items around the currently selected item that should be kept visible
401    ///
402    /// This is a fluent setter method which must be chained or used as it consumes self
403    ///
404    /// # Example
405    ///
406    /// A padding value of 1 will keep 1 item above and 1 item bellow visible if possible
407    ///
408    /// ```rust
409    /// use ratatui::widgets::List;
410    ///
411    /// let items = ["Item 1"];
412    /// let list = List::new(items).scroll_padding(1);
413    /// ```
414    #[must_use = "method moves the value of self and returns the modified value"]
415    pub const fn scroll_padding(mut self, padding: usize) -> Self {
416        self.scroll_padding = padding;
417        self
418    }
419
420    /// Returns the number of [`ListItem`]s in the list
421    pub fn len(&self) -> usize {
422        self.items.len()
423    }
424
425    /// Returns true if the list contains no elements.
426    pub fn is_empty(&self) -> bool {
427        self.items.is_empty()
428    }
429}
430
431impl Styled for List<'_> {
432    type Item = Self;
433
434    fn style(&self) -> Style {
435        self.style
436    }
437
438    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
439        self.style(style)
440    }
441}
442
443impl Styled for ListItem<'_> {
444    type Item = Self;
445
446    fn style(&self) -> Style {
447        self.style
448    }
449
450    fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
451        self.style(style)
452    }
453}
454
455impl<'a, Item> FromIterator<Item> for List<'a>
456where
457    Item: Into<ListItem<'a>>,
458{
459    fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
460        Self::new(iter)
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use alloc::{format, vec};
467
468    use pretty_assertions::assert_eq;
469    use ratatui_core::buffer::Buffer;
470    use ratatui_core::layout::Rect;
471    use ratatui_core::style::{Color, Modifier, Stylize};
472    use ratatui_core::text::{Text, ToSpan};
473    use ratatui_core::widgets::StatefulWidget;
474
475    use super::*;
476
477    #[test]
478    fn collect_list_from_iterator() {
479        let collected: List = (0..3).map(|i| format!("Item{i}")).collect();
480        let expected = List::new(["Item0", "Item1", "Item2"]);
481        assert_eq!(collected, expected);
482    }
483
484    #[test]
485    fn can_be_stylized() {
486        assert_eq!(
487            List::new::<Vec<&str>>(vec![])
488                .black()
489                .on_white()
490                .bold()
491                .not_dim()
492                .style,
493            Style::default()
494                .fg(Color::Black)
495                .bg(Color::White)
496                .add_modifier(Modifier::BOLD)
497                .remove_modifier(Modifier::DIM)
498        );
499    }
500
501    #[test]
502    fn no_style() {
503        let text = Text::from("Item 1");
504        let list = List::new([ListItem::new(text)])
505            .highlight_symbol(">>")
506            .highlight_spacing(HighlightSpacing::Always);
507        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
508
509        list.render(buffer.area, &mut buffer, &mut ListState::default());
510
511        assert_eq!(buffer, Buffer::with_lines(["  Item 1  "]));
512    }
513
514    #[test]
515    fn styled_text() {
516        let text = Text::from("Item 1").bold();
517        let list = List::new([ListItem::new(text)])
518            .highlight_symbol(">>")
519            .highlight_spacing(HighlightSpacing::Always);
520        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
521
522        list.render(buffer.area, &mut buffer, &mut ListState::default());
523
524        assert_eq!(
525            buffer,
526            Buffer::with_lines([Line::from(vec!["  ".to_span(), "Item 1  ".bold(),])])
527        );
528    }
529
530    #[test]
531    fn styled_list_item() {
532        let text = Text::from("Item 1");
533        // note this avoids using the `Stylize' methods as that gets then combines the style
534        // instead of setting it directly (which is not the same for some implementations)
535        let item = ListItem::new(text).style(Modifier::ITALIC);
536        let list = List::new([item])
537            .highlight_symbol(">>")
538            .highlight_spacing(HighlightSpacing::Always);
539        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
540
541        list.render(buffer.area, &mut buffer, &mut ListState::default());
542
543        assert_eq!(
544            buffer,
545            Buffer::with_lines([Line::from_iter(["  Item 1  ".italic()])])
546        );
547    }
548
549    #[test]
550    fn styled_text_and_list_item() {
551        let text = Text::from("Item 1").bold();
552        // note this avoids using the `Stylize' methods as that gets then combines the style
553        // instead of setting it directly (which is not the same for some implementations)
554        let item = ListItem::new(text).style(Modifier::ITALIC);
555        let list = List::new([item])
556            .highlight_symbol(">>")
557            .highlight_spacing(HighlightSpacing::Always);
558        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
559
560        list.render(buffer.area, &mut buffer, &mut ListState::default());
561
562        assert_eq!(
563            buffer,
564            Buffer::with_lines([Line::from(vec!["  ".italic(), "Item 1  ".bold().italic()])])
565        );
566    }
567
568    #[test]
569    fn styled_highlight() {
570        let text = Text::from("Item 1").bold();
571        // note this avoids using the `Stylize' methods as that gets then combines the style
572        // instead of setting it directly (which is not the same for some implementations)
573        let item = ListItem::new(text).style(Modifier::ITALIC);
574        let mut state = ListState::default().with_selected(Some(0));
575        let list = List::new([item])
576            .highlight_symbol(">>")
577            .highlight_style(Color::Red);
578
579        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 1));
580        list.render(buffer.area, &mut buffer, &mut state);
581
582        assert_eq!(
583            buffer,
584            Buffer::with_lines([Line::from(vec![
585                ">>".italic().red(),
586                "Item 1  ".bold().italic().red(),
587            ])])
588        );
589    }
590
591    #[test]
592    fn style_inheritance() {
593        let bold = Modifier::BOLD;
594        let italic = Modifier::ITALIC;
595        let items = [
596            ListItem::new(Text::raw("Item 1")),               // no style
597            ListItem::new(Text::styled("Item 2", bold)),      // affects only the text
598            ListItem::new(Text::raw("Item 3")).style(italic), // affects the entire line
599            ListItem::new(Text::styled("Item 4", bold)).style(italic), // bold text, italic line
600            ListItem::new(Text::styled("Item 5", bold)).style(italic), // same but highlighted
601        ];
602        let mut state = ListState::default().with_selected(Some(4));
603        let list = List::new(items)
604            .highlight_symbol(">>")
605            .highlight_style(Color::Red)
606            .style(Style::new().on_blue());
607
608        let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
609        list.render(buffer.area, &mut buffer, &mut state);
610
611        assert_eq!(
612            buffer,
613            Buffer::with_lines(vec![
614                vec!["  Item 1  ".on_blue()],
615                vec!["  ".on_blue(), "Item 2  ".bold().on_blue()],
616                vec!["  Item 3  ".italic().on_blue()],
617                vec![
618                    "  ".italic().on_blue(),
619                    "Item 4  ".bold().italic().on_blue(),
620                ],
621                vec![
622                    ">>".italic().red().on_blue(),
623                    "Item 5  ".bold().italic().red().on_blue(),
624                ],
625            ])
626        );
627    }
628
629    #[test]
630    fn render_in_minimal_buffer() {
631        let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
632        let mut state = ListState::default().with_selected(None);
633        let items = vec![
634            ListItem::new("Item 1"),
635            ListItem::new("Item 2"),
636            ListItem::new("Item 3"),
637        ];
638        let list = List::new(items);
639        // This should not panic, even if the buffer is too small to render the list.
640        list.render(buffer.area, &mut buffer, &mut state);
641        assert_eq!(buffer, Buffer::with_lines(["I"]));
642    }
643
644    #[test]
645    fn render_in_zero_size_buffer() {
646        let mut buffer = Buffer::empty(Rect::ZERO);
647        let mut state = ListState::default().with_selected(None);
648        let items = vec![
649            ListItem::new("Item 1"),
650            ListItem::new("Item 2"),
651            ListItem::new("Item 3"),
652        ];
653        let list = List::new(items);
654        // This should not panic, even if the buffer has zero size.
655        list.render(buffer.area, &mut buffer, &mut state);
656    }
657}