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}