mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-22 04:33:13 +00:00
refactor(list): split up list.rs into smaller modules (#1204)
This commit is contained in:
parent
7e1bab049b
commit
e81663bec0
11 changed files with 2360 additions and 2296 deletions
|
@ -78,6 +78,10 @@ missing_panics_doc = "allow"
|
|||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
//! A module for the [`Buffer`] and [`Cell`] types.
|
||||
|
||||
mod assert;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod buffer;
|
||||
mod cell;
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ mod alignment;
|
|||
mod constraint;
|
||||
mod direction;
|
||||
mod flex;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod layout;
|
||||
mod margin;
|
||||
mod position;
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
//! [`Buffer`]: crate::buffer::Buffer
|
||||
|
||||
mod frame;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod terminal;
|
||||
mod viewport;
|
||||
|
||||
|
|
|
@ -56,6 +56,5 @@ pub use masked::Masked;
|
|||
mod span;
|
||||
pub use span::{Span, ToSpan};
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod text;
|
||||
pub use text::{Text, ToText};
|
||||
|
|
2299
src/widgets/list.rs
2299
src/widgets/list.rs
File diff suppressed because it is too large
Load diff
324
src/widgets/list/item.rs
Normal file
324
src/widgets/list/item.rs
Normal file
|
@ -0,0 +1,324 @@
|
|||
use crate::prelude::*;
|
||||
|
||||
/// A single item in a [`List`]
|
||||
///
|
||||
/// The item's height is defined by the number of lines it contains. This can be queried using
|
||||
/// [`ListItem::height`]. Similarly, [`ListItem::width`] will return the maximum width of all
|
||||
/// lines.
|
||||
///
|
||||
/// You can set the style of an item with [`ListItem::style`] or using the [`Stylize`] trait.
|
||||
/// This [`Style`] will be combined with the [`Style`] of the inner [`Text`]. The [`Style`]
|
||||
/// of the [`Text`] will be added to the [`Style`] of the [`ListItem`].
|
||||
///
|
||||
/// You can also align a `ListItem` by aligning its underlying [`Text`] and [`Line`]s. For that,
|
||||
/// see [`Text::alignment`] and [`Line::alignment`]. On a multiline `Text`, one `Line` can override
|
||||
/// the alignment by setting it explicitly.
|
||||
///
|
||||
/// # 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();
|
||||
/// text.extend(["Item".blue(), Span::raw(" "), "1".bold().red()]);
|
||||
/// let item = ListItem::new(text);
|
||||
/// ```
|
||||
///
|
||||
/// A right-aligned `ListItem`
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// ListItem::new(Text::from("foo").alignment(Alignment::Right));
|
||||
/// ```
|
||||
///
|
||||
/// [`List`]: crate::widgets::List
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct ListItem<'a> {
|
||||
pub(crate) content: Text<'a>,
|
||||
pub(crate) style: Style,
|
||||
}
|
||||
|
||||
impl<'a> ListItem<'a> {
|
||||
/// Creates a new [`ListItem`]
|
||||
///
|
||||
/// The `content` parameter accepts any value that can be converted into [`Text`].
|
||||
///
|
||||
/// # 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");
|
||||
/// ```
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - [`List::new`](crate::widgets::List::new) to create a list of items that can be converted
|
||||
/// to [`ListItem`]
|
||||
pub fn new<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
Self {
|
||||
content: content.into(),
|
||||
style: Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the item style
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This [`Style`] can be overridden by the [`Style`] of the [`Text`] content.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let item = ListItem::new("Item 1").style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `ListItem` also implements the [`Styled`] trait, which means you can use style shorthands
|
||||
/// from the [`Stylize`](crate::style::Stylize) trait to set the style of the widget more
|
||||
/// concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let item = ListItem::new("Item 1").red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Styled`]: crate::style::Styled
|
||||
/// [`ListState`]: crate::widgets::list::ListState
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the item height
|
||||
///
|
||||
/// # 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");
|
||||
/// assert_eq!(item.height(), 2);
|
||||
/// ```
|
||||
pub fn height(&self) -> usize {
|
||||
self.content.height()
|
||||
}
|
||||
|
||||
/// Returns the max width of all the lines
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let item = ListItem::new("12345");
|
||||
/// assert_eq!(item.width(), 5);
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let item = ListItem::new("12345\n1234567");
|
||||
/// assert_eq!(item.width(), 7);
|
||||
/// ```
|
||||
pub fn width(&self) -> usize {
|
||||
self.content.width()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<T> for ListItem<'a>
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
{
|
||||
fn from(value: T) -> Self {
|
||||
Self::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_from_str() {
|
||||
let item = ListItem::new("Test item");
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_from_string() {
|
||||
let item = ListItem::new("Test item".to_string());
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_from_cow_str() {
|
||||
let item = ListItem::new(Cow::Borrowed("Test item"));
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_from_span() {
|
||||
let span = Span::styled("Test item", Style::default().fg(Color::Blue));
|
||||
let item = ListItem::new(span.clone());
|
||||
assert_eq!(item.content, Text::from(span));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_from_spans() {
|
||||
let spans = Line::from(vec![
|
||||
Span::styled("Test ", Style::default().fg(Color::Blue)),
|
||||
Span::styled("item", Style::default().fg(Color::Red)),
|
||||
]);
|
||||
let item = ListItem::new(spans.clone());
|
||||
assert_eq!(item.content, Text::from(spans));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_from_vec_spans() {
|
||||
let lines = vec![
|
||||
Line::from(vec![
|
||||
Span::styled("Test ", Style::default().fg(Color::Blue)),
|
||||
Span::styled("item", Style::default().fg(Color::Red)),
|
||||
]),
|
||||
Line::from(vec![
|
||||
Span::styled("Second ", Style::default().fg(Color::Green)),
|
||||
Span::styled("line", Style::default().fg(Color::Yellow)),
|
||||
]),
|
||||
];
|
||||
let item = ListItem::new(lines.clone());
|
||||
assert_eq!(item.content, Text::from(lines));
|
||||
assert_eq!(item.style, Style::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn 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 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 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 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 style() {
|
||||
let item = ListItem::new("Test item").style(Style::default().bg(Color::Red));
|
||||
assert_eq!(item.content, Text::from("Test item"));
|
||||
assert_eq!(item.style, Style::default().bg(Color::Red));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn height() {
|
||||
let item = ListItem::new("Test item");
|
||||
assert_eq!(item.height(), 1);
|
||||
|
||||
let item = ListItem::new("Test item\nSecond line");
|
||||
assert_eq!(item.height(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn width() {
|
||||
let item = ListItem::new("Test item");
|
||||
assert_eq!(item.width(), 9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_be_stylized() {
|
||||
assert_eq!(
|
||||
ListItem::new("").black().on_white().bold().not_dim().style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
}
|
450
src/widgets/list/list.rs
Normal file
450
src/widgets/list/list.rs
Normal file
|
@ -0,0 +1,450 @@
|
|||
use strum::{Display, EnumString};
|
||||
|
||||
use super::ListItem;
|
||||
use crate::{
|
||||
prelude::*,
|
||||
style::Styled,
|
||||
widgets::{Block, HighlightSpacing},
|
||||
};
|
||||
|
||||
/// A widget to display several items among which one can be selected (optional)
|
||||
///
|
||||
/// A list is a collection of [`ListItem`]s.
|
||||
///
|
||||
/// This is different from a [`Table`] because it does not handle columns, headers or footers and
|
||||
/// the item's height is automatically determined. A `List` can also be put in reverse order (i.e.
|
||||
/// *bottom to top*) whereas a [`Table`] cannot.
|
||||
///
|
||||
/// [`Table`]: crate::widgets::Table
|
||||
///
|
||||
/// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`].
|
||||
///
|
||||
/// [`List`] implements [`Widget`] and so it can be drawn using
|
||||
/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
|
||||
///
|
||||
/// [`List`] is also a [`StatefulWidget`], which means you can use it with [`ListState`] to allow
|
||||
/// the user to [scroll] through items and [select] one of them.
|
||||
///
|
||||
/// See the list in the [Examples] directory for a more in depth example of the various
|
||||
/// configuration options and for how to handle state.
|
||||
///
|
||||
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
///
|
||||
/// # Fluent setters
|
||||
///
|
||||
/// - [`List::highlight_style`] sets the style of the selected item.
|
||||
/// - [`List::highlight_symbol`] sets the symbol to be displayed in front of the selected item.
|
||||
/// - [`List::repeat_highlight_symbol`] sets whether to repeat the symbol and style over selected
|
||||
/// multi-line items
|
||||
/// - [`List::direction`] sets the list direction
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// let items = ["Item 1", "Item 2", "Item 3"];
|
||||
/// let list = List::new(items)
|
||||
/// .block(Block::bordered().title("List"))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
/// .highlight_symbol(">>")
|
||||
/// .repeat_highlight_symbol(true)
|
||||
/// .direction(ListDirection::BottomToTop);
|
||||
///
|
||||
/// frame.render_widget(list, area);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// # Stateful example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// // This should be stored outside of the function in your application state.
|
||||
/// let mut state = ListState::default();
|
||||
/// let items = ["Item 1", "Item 2", "Item 3"];
|
||||
/// let list = List::new(items)
|
||||
/// .block(Block::bordered().title("List"))
|
||||
/// .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
|
||||
/// .highlight_symbol(">>")
|
||||
/// .repeat_highlight_symbol(true);
|
||||
///
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// In addition to `List::new`, any iterator whose element is convertible to `ListItem` can be
|
||||
/// collected into `List`.
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::List;
|
||||
///
|
||||
/// (0..5).map(|i| format!("Item{i}")).collect::<List>();
|
||||
/// ```
|
||||
///
|
||||
/// [`ListState`]: crate::widgets::list::ListState
|
||||
/// [scroll]: crate::widgets::list::ListState::offset
|
||||
/// [select]: crate::widgets::list::ListState::select
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct List<'a> {
|
||||
/// An optional block to wrap the widget in
|
||||
pub(crate) block: Option<Block<'a>>,
|
||||
/// The items in the list
|
||||
pub(crate) items: Vec<ListItem<'a>>,
|
||||
/// Style used as a base style for the widget
|
||||
pub(crate) style: Style,
|
||||
/// List display direction
|
||||
pub(crate) direction: ListDirection,
|
||||
/// Style used to render selected item
|
||||
pub(crate) highlight_style: Style,
|
||||
/// Symbol in front of the selected item (Shift all items to the right)
|
||||
pub(crate) highlight_symbol: Option<&'a str>,
|
||||
/// Whether to repeat the highlight symbol for each line of the selected item
|
||||
pub(crate) repeat_highlight_symbol: bool,
|
||||
/// Decides when to allocate spacing for the selection symbol
|
||||
pub(crate) highlight_spacing: HighlightSpacing,
|
||||
/// How many items to try to keep visible before and after the selected item
|
||||
pub(crate) scroll_padding: usize,
|
||||
}
|
||||
|
||||
/// Defines the direction in which the list will be rendered.
|
||||
///
|
||||
/// If there are too few items to fill the screen, the list will stick to the starting edge.
|
||||
///
|
||||
/// See [`List::direction`].
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum ListDirection {
|
||||
/// The first value is on the top, going to the bottom
|
||||
#[default]
|
||||
TopToBottom,
|
||||
/// The first value is on the bottom, going to the top.
|
||||
BottomToTop,
|
||||
}
|
||||
|
||||
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 [`&str`]
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// 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<T>(items: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<ListItem<'a>>,
|
||||
{
|
||||
Self {
|
||||
block: None,
|
||||
style: Style::default(),
|
||||
items: items.into_iter().map(Into::into).collect(),
|
||||
direction: ListDirection::default(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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"]);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn items<T>(mut self, items: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<ListItem<'a>>,
|
||||
{
|
||||
self.items = items.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Wraps the list with a custom [`Block`] widget.
|
||||
///
|
||||
/// The `block` parameter holds the specified [`Block`] to be created around the [`List`]
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// let block = Block::bordered().title("List");
|
||||
/// let list = List::new(items).block(block);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the base style of the widget
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// All text rendered by the widget will use this style, unless overridden by [`Block::style`],
|
||||
/// [`ListItem::style`], or the styles of the [`ListItem`]'s content.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// let list = List::new(items).style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `List` also implements the [`Styled`] trait, which means you can use style shorthands from
|
||||
/// the [`Stylize`] trait to set the style of the widget more concisely.
|
||||
///
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// let list = List::new(items).red().italic();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the symbol to be displayed in front of the selected item
|
||||
///
|
||||
/// By default there are no highlight symbol.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1", "Item 2"];
|
||||
/// let list = List::new(items).highlight_symbol(">>");
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn highlight_symbol(mut self, highlight_symbol: &'a str) -> Self {
|
||||
self.highlight_symbol = Some(highlight_symbol);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style of the selected item
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This style will be applied to the entire item, including the
|
||||
/// [highlight symbol](List::highlight_symbol) if it is displayed, and will override any style
|
||||
/// set on the item or on the individual cells.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["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"]
|
||||
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.highlight_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether to repeat the highlight symbol and style over selected multi-line items
|
||||
///
|
||||
/// This is `false` by default.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn repeat_highlight_symbol(mut self, repeat: bool) -> Self {
|
||||
self.repeat_highlight_symbol = repeat;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set when to show the highlight spacing
|
||||
///
|
||||
/// The highlight spacing is the spacing that is allocated for the selection symbol (if enabled)
|
||||
/// and is used to shift the list when an item is selected. This method allows you to configure
|
||||
/// when this spacing is allocated.
|
||||
///
|
||||
/// - [`HighlightSpacing::Always`] will always allocate the spacing, regardless of whether an
|
||||
/// item is selected or not. This means that the table will never change size, regardless of
|
||||
/// if an item is selected or not.
|
||||
/// - [`HighlightSpacing::WhenSelected`] will only allocate the spacing if an item is selected.
|
||||
/// This means that the table will shift when an item is selected. This is the default setting
|
||||
/// for backwards compatibility, but it is recommended to use `HighlightSpacing::Always` for a
|
||||
/// better user experience.
|
||||
/// - [`HighlightSpacing::Never`] will never allocate the spacing, regardless of whether an item
|
||||
/// is selected or not. This means that the highlight symbol will never be drawn.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// let list = List::new(items).highlight_spacing(HighlightSpacing::Always);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
|
||||
self.highlight_spacing = value;
|
||||
self
|
||||
}
|
||||
|
||||
/// Defines the list direction (up or down)
|
||||
///
|
||||
/// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*.
|
||||
/// If there is too few items to fill the screen, the list will stick to the starting edge.
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Bottom to top
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// let list = List::new(items).direction(ListDirection::BottomToTop);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn direction(mut self, direction: ListDirection) -> Self {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the number of items around the currently selected item that should be kept visible
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// A padding value of 1 will keep 1 item above and 1 item bellow visible if possible
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// let list = List::new(items).scroll_padding(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn scroll_padding(mut self, padding: usize) -> Self {
|
||||
self.scroll_padding = padding;
|
||||
self
|
||||
}
|
||||
|
||||
/// Returns the number of [`ListItem`]s in the list
|
||||
pub fn len(&self) -> usize {
|
||||
self.items.len()
|
||||
}
|
||||
|
||||
/// Returns true if the list contains no elements.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.items.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for List<'a> {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for ListItem<'a> {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Item> FromIterator<Item> for List<'a>
|
||||
where
|
||||
Item: Into<ListItem<'a>>,
|
||||
{
|
||||
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
|
||||
Self::new(iter)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn collect_list_from_iterator() {
|
||||
let collected: List = (0..3).map(|i| format!("Item{i}")).collect();
|
||||
let expected = List::new(["Item0", "Item1", "Item2"]);
|
||||
assert_eq!(collected, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_be_stylized() {
|
||||
assert_eq!(
|
||||
List::new::<Vec<&str>>(vec![])
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
.not_dim()
|
||||
.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
}
|
1285
src/widgets/list/rendering.rs
Normal file
1285
src/widgets/list/rendering.rs
Normal file
File diff suppressed because it is too large
Load diff
289
src/widgets/list/state.rs
Normal file
289
src/widgets/list/state.rs
Normal file
|
@ -0,0 +1,289 @@
|
|||
/// State of the [`List`] widget
|
||||
///
|
||||
/// This state can be used to scroll through items and select one. When the list is rendered as a
|
||||
/// stateful widget, the selected item will be highlighted and the list will be shifted to ensure
|
||||
/// that the selected item is visible. This will modify the [`ListState`] object passed to the
|
||||
/// [`Frame::render_stateful_widget`](crate::terminal::Frame::render_stateful_widget) method.
|
||||
///
|
||||
/// The state consists of two fields:
|
||||
/// - [`offset`]: the index of the first item to be displayed
|
||||
/// - [`selected`]: the index of the selected item, which can be `None` if no item is selected
|
||||
///
|
||||
/// [`offset`]: ListState::offset()
|
||||
/// [`selected`]: ListState::selected()
|
||||
///
|
||||
/// See the list in the [Examples] directory for a more in depth example of the various
|
||||
/// configuration options and for how to handle state.
|
||||
///
|
||||
/// [Examples]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// # let items = ["Item 1"];
|
||||
/// let list = List::new(items);
|
||||
///
|
||||
/// // This should be stored outside of the function in your application state.
|
||||
/// let mut state = ListState::default();
|
||||
///
|
||||
/// *state.offset_mut() = 1; // display the second item and onwards
|
||||
/// state.select(Some(3)); // select the forth item (0-indexed)
|
||||
///
|
||||
/// frame.render_stateful_widget(list, area, &mut state);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`List`]: crate::widgets::List
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ListState {
|
||||
pub(crate) offset: usize,
|
||||
pub(crate) selected: Option<usize>,
|
||||
}
|
||||
|
||||
impl ListState {
|
||||
/// Sets the index of the first item to be displayed
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = ListState::default().with_offset(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn with_offset(mut self, offset: usize) -> Self {
|
||||
self.offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the index of the selected item
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = ListState::default().with_selected(Some(1));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn with_selected(mut self, selected: Option<usize>) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
/// Index of the first item to be displayed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = ListState::default();
|
||||
/// assert_eq!(state.offset(), 0);
|
||||
/// ```
|
||||
pub const fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the first item to be displayed
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = ListState::default();
|
||||
/// *state.offset_mut() = 1;
|
||||
/// ```
|
||||
pub fn offset_mut(&mut self) -> &mut usize {
|
||||
&mut self.offset
|
||||
}
|
||||
|
||||
/// Index of the selected item
|
||||
///
|
||||
/// Returns `None` if no item is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::default();
|
||||
/// assert_eq!(state.selected(), None);
|
||||
/// ```
|
||||
pub const fn selected(&self) -> Option<usize> {
|
||||
self.selected
|
||||
}
|
||||
|
||||
/// Mutable reference to the index of the selected item
|
||||
///
|
||||
/// Returns `None` if no item is selected
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = ListState::default();
|
||||
/// *state.selected_mut() = Some(1);
|
||||
/// ```
|
||||
pub fn selected_mut(&mut self) -> &mut Option<usize> {
|
||||
&mut self.selected
|
||||
}
|
||||
|
||||
/// Sets the index of the selected item
|
||||
///
|
||||
/// Set to `None` if no item is selected. This will also reset the offset to `0`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
/// ```
|
||||
pub fn select(&mut self, index: Option<usize>) {
|
||||
self.selected = index;
|
||||
if index.is_none() {
|
||||
self.offset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Selects the next item or the first one if no item is selected
|
||||
///
|
||||
/// Note: until the list is rendered, the number of items is not known, so the index is set to
|
||||
/// `0` and will be corrected when the list is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_next();
|
||||
/// ```
|
||||
pub fn select_next(&mut self) {
|
||||
let next = self.selected.map_or(0, |i| i.saturating_add(1));
|
||||
self.select(Some(next));
|
||||
}
|
||||
|
||||
/// Selects the previous item or the last one if no item is selected
|
||||
///
|
||||
/// Note: until the list is rendered, the number of items is not known, so the index is set to
|
||||
/// `usize::MAX` and will be corrected when the list is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_previous();
|
||||
/// ```
|
||||
pub fn select_previous(&mut self) {
|
||||
let previous = self.selected.map_or(usize::MAX, |i| i.saturating_sub(1));
|
||||
self.select(Some(previous));
|
||||
}
|
||||
|
||||
/// Selects the first item
|
||||
///
|
||||
/// Note: until the list is rendered, the number of items is not known, so the index is set to
|
||||
/// `0` and will be corrected when the list is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_first();
|
||||
/// ```
|
||||
pub fn select_first(&mut self) {
|
||||
self.select(Some(0));
|
||||
}
|
||||
|
||||
/// Selects the last item
|
||||
///
|
||||
/// Note: until the list is rendered, the number of items is not known, so the index is set to
|
||||
/// `usize::MAX` and will be corrected when the list is rendered
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_last();
|
||||
/// ```
|
||||
pub fn select_last(&mut self) {
|
||||
self.select(Some(usize::MAX));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::widgets::ListState;
|
||||
|
||||
#[test]
|
||||
fn selected() {
|
||||
let mut state = ListState::default();
|
||||
assert_eq!(state.selected(), None);
|
||||
|
||||
state.select(Some(1));
|
||||
assert_eq!(state.selected(), Some(1));
|
||||
|
||||
state.select(None);
|
||||
assert_eq!(state.selected(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select() {
|
||||
let mut state = ListState::default();
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.offset, 0);
|
||||
|
||||
state.select(Some(2));
|
||||
assert_eq!(state.selected, Some(2));
|
||||
assert_eq!(state.offset, 0);
|
||||
|
||||
state.select(None);
|
||||
assert_eq!(state.selected, None);
|
||||
assert_eq!(state.offset, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn state_navigation() {
|
||||
let mut state = ListState::default();
|
||||
state.select_first();
|
||||
assert_eq!(state.selected, Some(0));
|
||||
|
||||
state.select_previous(); // should not go below 0
|
||||
assert_eq!(state.selected, Some(0));
|
||||
|
||||
state.select_next();
|
||||
assert_eq!(state.selected, Some(1));
|
||||
|
||||
state.select_previous();
|
||||
assert_eq!(state.selected, Some(0));
|
||||
|
||||
state.select_last();
|
||||
assert_eq!(state.selected, Some(usize::MAX));
|
||||
|
||||
state.select_next(); // should not go above usize::MAX
|
||||
assert_eq!(state.selected, Some(usize::MAX));
|
||||
|
||||
state.select_previous();
|
||||
assert_eq!(state.selected, Some(usize::MAX - 1));
|
||||
|
||||
state.select_next();
|
||||
assert_eq!(state.selected, Some(usize::MAX));
|
||||
|
||||
let mut state = ListState::default();
|
||||
state.select_next();
|
||||
assert_eq!(state.selected, Some(0));
|
||||
|
||||
let mut state = ListState::default();
|
||||
state.select_previous();
|
||||
assert_eq!(state.selected, Some(usize::MAX));
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
mod cell;
|
||||
mod highlight_spacing;
|
||||
mod row;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod table;
|
||||
mod table_state;
|
||||
|
||||
|
|
Loading…
Reference in a new issue