ratatui/widgets/list/
list.rs

1use strum::{Display, EnumString};
2
3use crate::{
4    style::{Style, Styled},
5    widgets::{Block, HighlightSpacing, ListItem},
6};
7
8/// A widget to display several items among which one can be selected (optional)
9///
10/// A list is a collection of [`ListItem`]s.
11///
12/// This is different from a [`Table`] because it does not handle columns, headers or footers and
13/// the item's height is automatically determined. A `List` can also be put in reverse order (i.e.
14/// *bottom to top*) whereas a [`Table`] cannot.
15///
16/// [`Table`]: crate::widgets::Table
17///
18/// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`].
19///
20/// [`List`] implements [`Widget`] and so it can be drawn using
21/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
22///
23/// [`List`] is also a [`StatefulWidget`], which means you can use it with [`ListState`] to allow
24/// the user to [scroll] through items and [select] one of them.
25///
26/// See the list in the [Examples] directory for a more in depth example of the various
27/// configuration options and for how to handle state.
28///
29/// [Examples]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
30///
31/// # Fluent setters
32///
33/// - [`List::highlight_style`] sets the style of the selected item.
34/// - [`List::highlight_symbol`] sets the symbol to be displayed in front of the selected item.
35/// - [`List::repeat_highlight_symbol`] sets whether to repeat the symbol and style over selected
36///   multi-line items
37/// - [`List::direction`] sets the list direction
38///
39/// # Examples
40///
41/// ```
42/// use ratatui::{
43///     layout::Rect,
44///     style::{Style, Stylize},
45///     widgets::{Block, List, ListDirection, ListItem},
46///     Frame,
47/// };
48///
49/// # fn ui(frame: &mut Frame) {
50/// # let area = Rect::default();
51/// let items = ["Item 1", "Item 2", "Item 3"];
52/// let list = List::new(items)
53///     .block(Block::bordered().title("List"))
54///     .style(Style::new().white())
55///     .highlight_style(Style::new().italic())
56///     .highlight_symbol(">>")
57///     .repeat_highlight_symbol(true)
58///     .direction(ListDirection::BottomToTop);
59///
60/// frame.render_widget(list, area);
61/// # }
62/// ```
63///
64/// # Stateful example
65///
66/// ```rust
67/// use ratatui::{
68///     layout::Rect,
69///     style::{Style, Stylize},
70///     widgets::{Block, List, ListState},
71///     Frame,
72/// };
73///
74/// # fn ui(frame: &mut Frame) {
75/// # let area = Rect::default();
76/// // This should be stored outside of the function in your application state.
77/// let mut state = ListState::default();
78/// let items = ["Item 1", "Item 2", "Item 3"];
79/// let list = List::new(items)
80///     .block(Block::bordered().title("List"))
81///     .highlight_style(Style::new().reversed())
82///     .highlight_symbol(">>")
83///     .repeat_highlight_symbol(true);
84///
85/// frame.render_stateful_widget(list, area, &mut state);
86/// # }
87/// ```
88///
89/// In addition to `List::new`, any iterator whose element is convertible to `ListItem` can be
90/// collected into `List`.
91///
92/// ```
93/// use ratatui::widgets::List;
94///
95/// (0..5).map(|i| format!("Item{i}")).collect::<List>();
96/// ```
97///
98/// [`ListState`]: crate::widgets::list::ListState
99/// [scroll]: crate::widgets::list::ListState::offset
100/// [select]: crate::widgets::list::ListState::select
101/// [`Text::alignment`]: crate::text::Text::alignment
102/// [`StatefulWidget`]: crate::widgets::StatefulWidget
103/// [`Widget`]: crate::widgets::Widget
104#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
105pub struct List<'a> {
106    /// An optional block to wrap the widget in
107    pub(crate) block: Option<Block<'a>>,
108    /// The items in the list
109    pub(crate) items: Vec<ListItem<'a>>,
110    /// Style used as a base style for the widget
111    pub(crate) style: Style,
112    /// List display direction
113    pub(crate) direction: ListDirection,
114    /// Style used to render selected item
115    pub(crate) highlight_style: Style,
116    /// Symbol in front of the selected item (Shift all items to the right)
117    pub(crate) highlight_symbol: Option<&'a str>,
118    /// Whether to repeat the highlight symbol for each line of the selected item
119    pub(crate) repeat_highlight_symbol: bool,
120    /// Decides when to allocate spacing for the selection symbol
121    pub(crate) highlight_spacing: HighlightSpacing,
122    /// How many items to try to keep visible before and after the selected item
123    pub(crate) scroll_padding: usize,
124}
125
126/// Defines the direction in which the list will be rendered.
127///
128/// If there are too few items to fill the screen, the list will stick to the starting edge.
129///
130/// See [`List::direction`].
131#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
132pub enum ListDirection {
133    /// The first value is on the top, going to the bottom
134    #[default]
135    TopToBottom,
136    /// The first value is on the bottom, going to the top.
137    BottomToTop,
138}
139
140impl<'a> List<'a> {
141    /// Creates a new list from [`ListItem`]s
142    ///
143    /// The `items` parameter accepts any value that can be converted into an iterator of
144    /// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
145    ///
146    /// # Example
147    ///
148    /// From a slice of [`&str`]
149    ///
150    /// ```
151    /// use ratatui::widgets::List;
152    ///
153    /// let list = List::new(["Item 1", "Item 2"]);
154    /// ```
155    ///
156    /// From [`Text`]
157    ///
158    /// ```
159    /// use ratatui::{
160    ///     style::{Style, Stylize},
161    ///     text::Text,
162    ///     widgets::List,
163    /// };
164    ///
165    /// let list = List::new([
166    ///     Text::styled("Item 1", Style::new().red()),
167    ///     Text::styled("Item 2", Style::new().red()),
168    /// ]);
169    /// ```
170    ///
171    /// You can also create an empty list using the [`Default`] implementation and use the
172    /// [`List::items`] fluent setter.
173    ///
174    /// ```rust
175    /// use ratatui::widgets::List;
176    ///
177    /// let empty_list = List::default();
178    /// let filled_list = empty_list.items(["Item 1"]);
179    /// ```
180    ///
181    /// [`Text`]: crate::text::Text
182    pub fn new<T>(items: T) -> Self
183    where
184        T: IntoIterator,
185        T::Item: Into<ListItem<'a>>,
186    {
187        Self {
188            block: None,
189            style: Style::default(),
190            items: items.into_iter().map(Into::into).collect(),
191            direction: ListDirection::default(),
192            ..Self::default()
193        }
194    }
195
196    /// Set the items
197    ///
198    /// The `items` parameter accepts any value that can be converted into an iterator of
199    /// [`Into<ListItem>`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`].
200    ///
201    /// This is a fluent setter method which must be chained or used as it consumes self.
202    ///
203    /// # Example
204    ///
205    /// ```rust
206    /// use ratatui::widgets::List;
207    ///
208    /// let list = List::default().items(["Item 1", "Item 2"]);
209    /// ```
210    ///
211    /// [`Text`]: crate::text::Text
212    #[must_use = "method moves the value of self and returns the modified value"]
213    pub fn items<T>(mut self, items: T) -> Self
214    where
215        T: IntoIterator,
216        T::Item: Into<ListItem<'a>>,
217    {
218        self.items = items.into_iter().map(Into::into).collect();
219        self
220    }
221
222    /// Wraps the list with a custom [`Block`] widget.
223    ///
224    /// The `block` parameter holds the specified [`Block`] to be created around the [`List`]
225    ///
226    /// This is a fluent setter method which must be chained or used as it consumes self
227    ///
228    /// # Examples
229    ///
230    /// ```rust
231    /// use ratatui::widgets::{Block, List};
232    ///
233    /// let items = ["Item 1"];
234    /// let block = Block::bordered().title("List");
235    /// let list = List::new(items).block(block);
236    /// ```
237    #[must_use = "method moves the value of self and returns the modified value"]
238    pub fn block(mut self, block: Block<'a>) -> Self {
239        self.block = Some(block);
240        self
241    }
242
243    /// Sets the base style of the widget
244    ///
245    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
246    /// your own type that implements [`Into<Style>`]).
247    ///
248    /// All text rendered by the widget will use this style, unless overridden by [`Block::style`],
249    /// [`ListItem::style`], or the styles of the [`ListItem`]'s content.
250    ///
251    /// This is a fluent setter method which must be chained or used as it consumes self
252    ///
253    /// # Examples
254    ///
255    /// ```rust
256    /// use ratatui::{
257    ///     style::{Style, Stylize},
258    ///     widgets::List,
259    /// };
260    ///
261    /// let items = ["Item 1"];
262    /// let list = List::new(items).style(Style::new().red().italic());
263    /// ```
264    ///
265    /// `List` also implements the [`Styled`] trait, which means you can use style shorthands from
266    /// the [`Stylize`] trait to set the style of the widget more concisely.
267    ///
268    /// [`Stylize`]: crate::style::Stylize
269    ///
270    /// ```rust
271    /// use ratatui::{style::Stylize, widgets::List};
272    ///
273    /// let items = ["Item 1"];
274    /// let list = List::new(items).red().italic();
275    /// ```
276    ///
277    /// [`Color`]: crate::style::Color
278    #[must_use = "method moves the value of self and returns the modified value"]
279    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
280        self.style = style.into();
281        self
282    }
283
284    /// Set the symbol to be displayed in front of the selected item
285    ///
286    /// By default there are no highlight symbol.
287    ///
288    /// This is a fluent setter method which must be chained or used as it consumes self
289    ///
290    /// # Examples
291    ///
292    /// ```rust
293    /// use ratatui::widgets::List;
294    ///
295    /// let items = ["Item 1", "Item 2"];
296    /// let list = List::new(items).highlight_symbol(">>");
297    /// ```
298    #[must_use = "method moves the value of self and returns the modified value"]
299    pub const fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
300        self.highlight_symbol = Some(highlight_symbol);
301        self
302    }
303
304    /// Set the style of the selected item
305    ///
306    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
307    /// your own type that implements [`Into<Style>`]).
308    ///
309    /// This style will be applied to the entire item, including the
310    /// [highlight symbol](List::highlight_symbol) if it is displayed, and will override any style
311    /// set on the item or on the individual cells.
312    ///
313    /// This is a fluent setter method which must be chained or used as it consumes self
314    ///
315    /// # Examples
316    ///
317    /// ```rust
318    /// use ratatui::{
319    ///     style::{Style, Stylize},
320    ///     widgets::List,
321    /// };
322    ///
323    /// let items = ["Item 1", "Item 2"];
324    /// let list = List::new(items).highlight_style(Style::new().red().italic());
325    /// ```
326    ///
327    /// [`Color`]: crate::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<'a> Styled for List<'a> {
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<'a> Styled for ListItem<'a> {
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 pretty_assertions::assert_eq;
467
468    use super::*;
469    use crate::style::{Color, Modifier, Stylize};
470
471    #[test]
472    fn collect_list_from_iterator() {
473        let collected: List = (0..3).map(|i| format!("Item{i}")).collect();
474        let expected = List::new(["Item0", "Item1", "Item2"]);
475        assert_eq!(collected, expected);
476    }
477
478    #[test]
479    fn can_be_stylized() {
480        assert_eq!(
481            List::new::<Vec<&str>>(vec![])
482                .black()
483                .on_white()
484                .bold()
485                .not_dim()
486                .style,
487            Style::default()
488                .fg(Color::Black)
489                .bg(Color::White)
490                .add_modifier(Modifier::BOLD)
491                .remove_modifier(Modifier::DIM)
492        );
493    }
494}