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}