From aef495604c52e563fbacfb1a6e730cd441a99129 Mon Sep 17 00:00:00 2001 From: Valentin271 <36198422+Valentin271@users.noreply.github.com> Date: Fri, 8 Dec 2023 23:20:49 +0100 Subject: [PATCH] feat(List)!: `List::new` now accepts `IntoIterator>` (#672) This allows to build list like ``` List::new(["Item 1", "Item 2"]) ``` BREAKING CHANGE: `List::new` parameter type changed from `Into>>` to `IntoIterator>>` --- BREAKING-CHANGES.md | 24 +++++- src/widgets/list.rs | 170 +++++++++++++++++++++++++++++++++++++----- tests/widgets_list.rs | 2 +- 3 files changed, 173 insertions(+), 23 deletions(-) diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index c6c29af1..156d2cd7 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -11,8 +11,9 @@ github with a [breaking change] label. This is a quick summary of the sections below: - Unreleased (0.24.1) + - `List::new()` now accepts `IntoIterator>>` - `Table::new()` now requires specifying the widths - -`Table::widths()` now accepts `IntoIterator>` + - `Table::widths()` now accepts `IntoIterator>` - Layout::new() now accepts direction and constraint parameters - The default `Tabs::highlight_style` is now `Style::new().reversed()` @@ -40,6 +41,21 @@ This is a quick summary of the sections below: ## Unreleased (v0.24.1) +### `List::new()` now accepts `IntoIterator>>` ([#672]) + +[#672]: https://github.com/ratatui-org/ratatui/pull/672 + +Previously `List::new()` took `Into>>`. This change will throw a compilation +error for `IntoIterator`s with an indeterminate item (e.g. empty vecs). + +E.g. + +```rust +let list = List::new(vec![]); +// becomes +let list = List::default(); +``` + ### The default `Tabs::highlight_style` is now `Style::new().reversed()` ([#635]) Previously the default highlight style for tabs was `Style::default()`, which meant that a `Tabs` @@ -53,10 +69,12 @@ Previously the default highlight style for tabs was `Style::default()`, which me widget in the default configuration would not show any indication of the selected tab. -### `Table::new()` now requires specifying the widths of the columrs (#664) +### `Table::new()` now requires specifying the widths of the columns (#664) + +[#664]: https://github.com/ratatui-org/ratatui/pull/664 Previously `Table`s could be constructed without widths. In almost all cases this is an error. -A new widths parameter is now manadatory on `Table::new()`. Existing code of the form: +A new widths parameter is now mandatory on `Table::new()`. Existing code of the form: ```rust Table::new(rows).widths(widths) diff --git a/src/widgets/list.rs b/src/widgets/list.rs index 081c9966..926ebdeb 100644 --- a/src/widgets/list.rs +++ b/src/widgets/list.rs @@ -34,7 +34,7 @@ use crate::{ /// # use ratatui::{prelude::*, widgets::*}; /// # fn ui(frame: &mut Frame) { /// # let area = Rect::default(); -/// # let items = vec![]; +/// # let items = vec!["Item 1"]; /// let list = List::new(items); /// /// // This should be stored outside of the function in your application state. @@ -173,12 +173,22 @@ impl ListState { /// # Examples /// /// You can create [`ListItem`]s from simple `&str` +/// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; /// let item = ListItem::new("Item 1"); /// ``` /// +/// Anything that can be converted to [`Text`] can be a [`ListItem`]. +/// +/// ```rust +/// # use ratatui::{prelude::*, widgets::*}; +/// let item1: ListItem = "Item 1".into(); +/// let item2: ListItem = Line::raw("Item 2").into(); +/// ``` +/// /// A [`ListItem`] styled with [`Stylize`] +/// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; /// let item = ListItem::new("Item 1").red().on_white(); @@ -186,6 +196,7 @@ impl ListState { /// /// If you need more control over the item's style, you can explicitly style the underlying /// [`Text`] +/// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; /// let mut text = Text::default(); @@ -208,12 +219,22 @@ impl<'a> ListItem<'a> { /// # Examples /// /// You can create [`ListItem`]s from simple `&str` + /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; /// let item = ListItem::new("Item 1"); /// ``` /// + /// Anything that can be converted to [`Text`] can be a [`ListItem`]. + /// + /// ```rust + /// # use ratatui::{prelude::*, widgets::*}; + /// let item1: ListItem = "Item 1".into(); + /// let item2: ListItem = Line::raw("Item 2").into(); + /// ``` + /// /// You can also create multilines item + /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; /// let item = ListItem::new("Multi-line\nitem"); @@ -264,6 +285,7 @@ impl<'a> ListItem<'a> { /// # Examples /// /// One line item + /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; /// let item = ListItem::new("Item 1"); @@ -271,6 +293,7 @@ impl<'a> ListItem<'a> { /// ``` /// /// Two lines item (note the `\n`) + /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; /// let item = ListItem::new("Multi-line\nitem"); @@ -300,6 +323,15 @@ impl<'a> ListItem<'a> { } } +impl<'a, T> From for ListItem<'a> +where + T: Into>, +{ + fn from(value: T) -> Self { + ListItem::new(value) + } +} + /// A widget to display several items among which one can be selected (optional) /// /// A list is a collection of [`ListItem`]s. @@ -336,7 +368,7 @@ impl<'a> ListItem<'a> { /// use ratatui::{prelude::*, widgets::*}; /// # fn ui(frame: &mut Frame) { /// # let area = Rect::default(); -/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")]; +/// let items = ["Item 1", "Item 2", "Item 3"]; /// let list = List::new(items) /// .block(Block::default().title("List").borders(Borders::ALL)) /// .style(Style::default().fg(Color::White)) @@ -357,7 +389,7 @@ impl<'a> ListItem<'a> { /// # let area = Rect::default(); /// // This should be stored outside of the function in your application state. /// let mut state = ListState::default(); -/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2"), ListItem::new("Item 3")]; +/// let items = ["Item 1", "Item 2", "Item 3"]; /// let list = List::new(items) /// .block(Block::default().title("List").borders(Borders::ALL)) /// .highlight_style(Style::new().add_modifier(Modifier::REVERSED)) @@ -366,7 +398,7 @@ impl<'a> ListItem<'a> { /// /// frame.render_stateful_widget(list, area, &mut state); /// # } -#[derive(Debug, Clone, Eq, PartialEq, Hash)] +#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)] pub struct List<'a> { block: Option>, items: Vec>, @@ -387,22 +419,45 @@ pub struct List<'a> { impl<'a> List<'a> { /// Creates a new list from [`ListItem`]s /// + /// The `items` parameter accepts any value that can be converted into an iterator of + /// [`Into`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`]. + /// /// # Example /// - /// From a slice of [`ListItem`] + /// From a slice of [`&str`] + /// /// ``` /// # use ratatui::{prelude::*, widgets::*}; - /// let items = [ListItem::new("Item 1"), ListItem::new("Item 2")]; - /// let list = List::new(items); + /// let list = List::new(["Item 1", "Item 2"]); + /// ``` + /// + /// From [`Text`] + /// + /// ``` + /// # use ratatui::{prelude::*, widgets::*}; + /// let list = List::new([ + /// Text::styled("Item 1", Style::default().red()), + /// Text::styled("Item 2", Style::default().red()) + /// ]); + /// ``` + /// + /// You can also create an empty list using the [`Default`] implementation and use the + /// [`List::items`] fluent setter. + /// + /// ```rust + /// # use ratatui::{prelude::*, widgets::*}; + /// let empty_list = List::default(); + /// let filled_list = empty_list.items(["Item 1"]); /// ``` pub fn new(items: T) -> List<'a> where - T: Into>>, + T: IntoIterator, + T::Item: Into>, { List { block: None, style: Style::default(), - items: items.into(), + items: items.into_iter().map(|i| i.into()).collect(), start_corner: Corner::TopLeft, highlight_style: Style::default(), highlight_symbol: None, @@ -411,6 +466,29 @@ impl<'a> List<'a> { } } + /// Set the items + /// + /// The `items` parameter accepts any value that can be converted into an iterator of + /// [`Into`]. This includes arrays of [`&str`] or [`Vec`]s of [`Text`]. + /// + /// This is a fluent setter method which must be chained or used as it consumes self. + /// + /// # Example + /// + /// ```rust + /// # use ratatui::{prelude::*, widgets::*}; + /// let list = List::default().items(["Item 1", "Item 2"]); + /// ``` + #[must_use = "method moves the value of self and returns the modified value"] + pub fn items(mut self, items: T) -> Self + where + T: IntoIterator, + T::Item: Into>, + { + self.items = items.into_iter().map(|i| i.into()).collect(); + self + } + /// Wraps the list with a custom [`Block`] widget. /// /// The `block` parameter holds the specified [`Block`] to be created around the [`List`] @@ -421,7 +499,7 @@ impl<'a> List<'a> { /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1")]; + /// # let items = vec!["Item 1"]; /// let block = Block::default().title("List").borders(Borders::ALL); /// let list = List::new(items).block(block); /// ``` @@ -442,7 +520,7 @@ impl<'a> List<'a> { /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1")]; + /// # let items = vec!["Item 1"]; /// let list = List::new(items).style(Style::new().red().italic()); /// ``` /// @@ -453,7 +531,7 @@ impl<'a> List<'a> { /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1")]; + /// # let items = vec!["Item 1"]; /// let list = List::new(items).red().italic(); /// ``` #[must_use = "method moves the value of self and returns the modified value"] @@ -472,7 +550,7 @@ impl<'a> List<'a> { /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1"), ListItem::new("Item 2")]; + /// # let items = vec!["Item 1", "Item 2"]; /// let list = List::new(items).highlight_symbol(">>"); /// ``` #[must_use = "method moves the value of self and returns the modified value"] @@ -493,7 +571,7 @@ impl<'a> List<'a> { /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1"), ListItem::new("Item 2")]; + /// # let items = vec!["Item 1", "Item 2"]; /// let list = List::new(items).highlight_style(Style::new().red().italic()); /// ``` #[must_use = "method moves the value of self and returns the modified value"] @@ -535,7 +613,7 @@ impl<'a> List<'a> { /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1")]; + /// # let items = vec!["Item 1"]; /// let list = List::new(items).highlight_spacing(HighlightSpacing::Always); /// ``` #[must_use = "method moves the value of self and returns the modified value"] @@ -561,16 +639,18 @@ impl<'a> List<'a> { /// # Example /// /// Same as default, i.e. *top to bottom*. Despite the name implying otherwise. + /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1")]; + /// # let items = vec!["Item 1"]; /// let list = List::new(items).start_corner(Corner::BottomRight); /// ``` /// /// Bottom to top + /// /// ```rust /// # use ratatui::{prelude::*, widgets::*}; - /// # let items = vec![ListItem::new("Item 1")]; + /// # let items = vec!["Item 1"]; /// let list = List::new(items).start_corner(Corner::BottomLeft); /// ``` #[must_use = "method moves the value of self and returns the modified value"] @@ -849,6 +929,38 @@ mod tests { assert_eq!(item.style, Style::default()); } + #[test] + fn test_str_into_list_item() { + let s = "Test item"; + let item: ListItem = s.into(); + assert_eq!(item.content, Text::from(s)); + assert_eq!(item.style, Style::default()); + } + + #[test] + fn test_string_into_list_item() { + let s = String::from("Test item"); + let item: ListItem = s.clone().into(); + assert_eq!(item.content, Text::from(s)); + assert_eq!(item.style, Style::default()); + } + + #[test] + fn test_span_into_list_item() { + let s = Span::from("Test item"); + let item: ListItem = s.clone().into(); + assert_eq!(item.content, Text::from(s)); + assert_eq!(item.style, Style::default()); + } + + #[test] + fn test_vec_lines_into_list_item() { + let lines = vec![Line::raw("l1"), Line::raw("l2")]; + let item: ListItem = lines.clone().into(); + assert_eq!(item.content, Text::from(lines)); + assert_eq!(item.style, Style::default()); + } + #[test] fn test_list_item_style() { let item = ListItem::new("Test item").style(Style::default().bg(Color::Red)); @@ -897,7 +1009,7 @@ mod tests { #[test] fn test_list_does_not_render_in_small_space() { - let items = list_items(vec!["Item 0", "Item 1", "Item 2"]); + let items = vec!["Item 0", "Item 1", "Item 2"]; let list = List::new(items.clone()).highlight_symbol(">>"); let mut buffer = Buffer::empty(Rect::new(0, 0, 15, 3)); @@ -1137,6 +1249,21 @@ mod tests { ); } + #[test] + fn test_list_items_setter() { + let list = List::default().items(["Item 0", "Item 1", "Item 2"]); + assert_buffer_eq!( + render_widget(list, 10, 5), + Buffer::with_lines(vec![ + "Item 0 ", + "Item 1 ", + "Item 2 ", + " ", + " ", + ]) + ); + } + #[test] fn test_list_with_empty_strings() { let items = list_items(vec!["Item 0", "", "", "Item 1", "Item 2"]); @@ -1472,7 +1599,12 @@ mod tests { #[test] fn list_can_be_stylized() { assert_eq!( - List::new(vec![]).black().on_white().bold().not_dim().style, + List::new::>(vec![]) + .black() + .on_white() + .bold() + .not_dim() + .style, Style::default() .fg(Color::Black) .bg(Color::White) diff --git a/tests/widgets_list.rs b/tests/widgets_list.rs index c918adef..00429994 100644 --- a/tests/widgets_list.rs +++ b/tests/widgets_list.rs @@ -20,7 +20,7 @@ fn list_should_shows_the_length() { assert_eq!(list.len(), 3); assert!(!list.is_empty()); - let empty_list = List::new(vec![]); + let empty_list = List::default(); assert_eq!(empty_list.len(), 0); assert!(empty_list.is_empty()); }