Skip to content

Commit

Permalink
feat(List)!: List::new now accepts `IntoIterator<Item = Into<ListIt…
Browse files Browse the repository at this point in the history
…em>>`

This allows to build list like

```
List::new(["Item 1", "Item 2"])
```

BREAKING CHANGE: `List::new` parameter type changed from `Into<Vec<ListItem<'a>>>`
to `IntoIterator<Item = Into<ListItem<'a>>>`
  • Loading branch information
Valentin271 committed Dec 8, 2023
1 parent 8bfd666 commit aff61c9
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 23 deletions.
22 changes: 19 additions & 3 deletions BREAKING-CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Item = Into<ListItem<'a>>>`
- `Table::new()` now requires specifying the widths
-`Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
- `Table::widths()` now accepts `IntoIterator<Item = AsRef<Constraint>>`
- Layout::new() now accepts direction and constraint parameters
- The default `Tabs::highlight_style` is now `Style::new().reversed()`

Expand Down Expand Up @@ -40,6 +41,21 @@ This is a quick summary of the sections below:

## Unreleased (v0.24.1)

### `List::new()` now accepts `IntoIterator<Item = Into<ListItem<'a>>>` ([#672])

[#672]: https://github.com/ratatui-org/ratatui/pull/672

Previously `List::new()` took `Into<Vec<ListItem<'a>>>`. 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`
Expand All @@ -53,10 +69,10 @@ 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)

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)
Expand Down
160 changes: 141 additions & 19 deletions src/widgets/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -173,19 +173,30 @@ 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();
/// ```
///
/// 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();
Expand All @@ -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");
Expand Down Expand Up @@ -264,13 +285,15 @@ impl<'a> ListItem<'a> {
/// # Examples
///
/// One line item
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Item 1");
/// assert_eq!(item.height(), 1);
/// ```
///
/// Two lines item (note the `\n`)
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let item = ListItem::new("Multi-line\nitem");
Expand Down Expand Up @@ -300,6 +323,15 @@ impl<'a> ListItem<'a> {
}
}

impl<'a, T> From<T> for ListItem<'a>
where
T: Into<Text<'a>>,
{
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.
Expand Down Expand Up @@ -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))
Expand All @@ -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))
Expand All @@ -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<Block<'a>>,
items: Vec<ListItem<'a>>,
Expand All @@ -387,22 +419,36 @@ 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<ListItem>`]. 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 list = List::new(["Item 1", "Item 2"]);
/// ```
///
/// From [`Text`]
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let items = [ListItem::new("Item 1"), ListItem::new("Item 2")];
/// let list = List::new(items);
/// let list = List::new([
/// Text::styled("Item 1", Style::default().red()),
/// Text::styled("Item 2", Style::default().red())
/// ]);
/// ```
pub fn new<T>(items: T) -> List<'a>
where
T: Into<Vec<ListItem<'a>>>,
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
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,
Expand All @@ -411,6 +457,28 @@ impl<'a> List<'a> {
}
}

/// Set the items
///
/// The `items` parameter accepts any value that can be converted into an iterator of
/// [`Into<ListItem>`]. 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"]);
/// ```
pub fn items<T>(mut self, items: T) -> Self
where
T: IntoIterator,
T::Item: Into<ListItem<'a>>,
{
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`]
Expand All @@ -421,7 +489,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);
/// ```
Expand All @@ -442,7 +510,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());
/// ```
///
Expand All @@ -453,7 +521,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"]
Expand All @@ -472,7 +540,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"]
Expand All @@ -493,7 +561,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"]
Expand Down Expand Up @@ -535,7 +603,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"]
Expand All @@ -561,16 +629,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"]
Expand Down Expand Up @@ -849,6 +919,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));
Expand Down Expand Up @@ -897,7 +999,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));

Expand Down Expand Up @@ -1137,6 +1239,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 ",
" ",
" ",
])
);

Check warning on line 1254 in src/widgets/list.rs

View check run for this annotation

Codecov / codecov/patch

src/widgets/list.rs#L1246-L1254

Added lines #L1246 - L1254 were not covered by tests
}

#[test]
fn test_list_with_empty_strings() {
let items = list_items(vec!["Item 0", "", "", "Item 1", "Item 2"]);
Expand Down Expand Up @@ -1472,7 +1589,12 @@ mod tests {
#[test]
fn list_can_be_stylized() {
assert_eq!(
List::new(vec![]).black().on_white().bold().not_dim().style,
List::new::<Vec<&str>>(vec![])
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
Expand Down

0 comments on commit aff61c9

Please sign in to comment.