mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-24 21:53:21 +00:00
feat(tabs)!: allow tabs to be deselected (#1413)
`Tabs::select()` now accepts `Into<Option<usize>>` instead of `usize`. This allows tabs to be deselected by passing `None`. `Tabs::default()` is now also implemented manually instead of deriving `Default`, and a new method `Tabs::titles()` is added to set the titles of the tabs. Fixes: <https://github.com/ratatui/ratatui/pull/1412> BREAKING CHANGE: `Tabs::select()` now accepts `Into<Option<usize>>` which breaks any code already using parameter type inference: ```diff let selected = 1u8; - let tabs = Tabs::new(["A", "B"]).select(selected.into()) + let tabs = Tabs::new(["A", "B"]).select(selected as usize) ```
This commit is contained in:
parent
3a43274881
commit
ab6b1feaec
2 changed files with 146 additions and 11 deletions
|
@ -12,7 +12,7 @@ This is a quick summary of the sections below:
|
||||||
|
|
||||||
- [v0.29.0](#v0290)
|
- [v0.29.0](#v0290)
|
||||||
- `Table::highlight_style` is now `Table::row_highlight_style`
|
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||||
|
- `Tabs::select` now accepts `Into<Option<usize>>`
|
||||||
- [v0.28.0](#v0280)
|
- [v0.28.0](#v0280)
|
||||||
⁻ `Backend::size` returns `Size` instead of `Rect`
|
⁻ `Backend::size` returns `Size` instead of `Rect`
|
||||||
- `Backend` trait migrates to `get/set_cursor_position`
|
- `Backend` trait migrates to `get/set_cursor_position`
|
||||||
|
@ -70,6 +70,19 @@ This is a quick summary of the sections below:
|
||||||
|
|
||||||
## v0.29.0
|
## v0.29.0
|
||||||
|
|
||||||
|
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
|
||||||
|
|
||||||
|
[#1413]: https://github.com/ratatui/ratatui/pull/1413
|
||||||
|
|
||||||
|
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
|
||||||
|
any code already using parameter type inference:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
let selected = 1u8;
|
||||||
|
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
|
||||||
|
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
|
||||||
|
```
|
||||||
|
|
||||||
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
|
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
|
||||||
|
|
||||||
[#1331]: https://github.com/ratatui/ratatui/pull/1331
|
[#1331]: https://github.com/ratatui/ratatui/pull/1331
|
||||||
|
@ -152,7 +165,6 @@ This change simplifies the trait and makes it easier to implement.
|
||||||
|
|
||||||
### `Frame::size` is deprecated and renamed to `Frame::area`
|
### `Frame::size` is deprecated and renamed to `Frame::area`
|
||||||
|
|
||||||
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
|
||||||
|
|
||||||
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
|
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use itertools::Itertools;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::Rect,
|
layout::Rect,
|
||||||
|
@ -44,14 +46,14 @@ const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVER
|
||||||
///
|
///
|
||||||
/// (0..5).map(|i| format!("Tab{i}")).collect::<Tabs>();
|
/// (0..5).map(|i| format!("Tab{i}")).collect::<Tabs>();
|
||||||
/// ```
|
/// ```
|
||||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
pub struct Tabs<'a> {
|
pub struct Tabs<'a> {
|
||||||
/// A block to wrap this widget in if necessary
|
/// A block to wrap this widget in if necessary
|
||||||
block: Option<Block<'a>>,
|
block: Option<Block<'a>>,
|
||||||
/// One title for each tab
|
/// One title for each tab
|
||||||
titles: Vec<Line<'a>>,
|
titles: Vec<Line<'a>>,
|
||||||
/// The index of the selected tabs
|
/// The index of the selected tabs
|
||||||
selected: usize,
|
selected: Option<usize>,
|
||||||
/// The style used to draw the text
|
/// The style used to draw the text
|
||||||
style: Style,
|
style: Style,
|
||||||
/// Style to apply to the selected item
|
/// Style to apply to the selected item
|
||||||
|
@ -64,6 +66,30 @@ pub struct Tabs<'a> {
|
||||||
padding_right: Line<'a>,
|
padding_right: Line<'a>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for Tabs<'_> {
|
||||||
|
/// Returns a default `Tabs` widget.
|
||||||
|
///
|
||||||
|
/// The default widget has:
|
||||||
|
/// - No tabs
|
||||||
|
/// - No selected tab
|
||||||
|
/// - The highlight style is set to reversed.
|
||||||
|
/// - The divider is set to a pipe (`|`).
|
||||||
|
/// - The padding on the left and right is set to a space.
|
||||||
|
///
|
||||||
|
/// This is rarely useful on its own without calling [`Tabs::titles`].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ratatui::widgets::Tabs;
|
||||||
|
///
|
||||||
|
/// let tabs = Tabs::default().titles(["Tab 1", "Tab 2"]);
|
||||||
|
/// ```
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(Vec::<Line>::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> Tabs<'a> {
|
impl<'a> Tabs<'a> {
|
||||||
/// Creates new `Tabs` from their titles.
|
/// Creates new `Tabs` from their titles.
|
||||||
///
|
///
|
||||||
|
@ -102,10 +128,12 @@ impl<'a> Tabs<'a> {
|
||||||
Iter: IntoIterator,
|
Iter: IntoIterator,
|
||||||
Iter::Item: Into<Line<'a>>,
|
Iter::Item: Into<Line<'a>>,
|
||||||
{
|
{
|
||||||
|
let titles = titles.into_iter().map(Into::into).collect_vec();
|
||||||
|
let selected = if titles.is_empty() { None } else { Some(0) };
|
||||||
Self {
|
Self {
|
||||||
block: None,
|
block: None,
|
||||||
titles: titles.into_iter().map(Into::into).collect(),
|
titles,
|
||||||
selected: 0,
|
selected,
|
||||||
style: Style::default(),
|
style: Style::default(),
|
||||||
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
||||||
divider: Span::raw(symbols::line::VERTICAL),
|
divider: Span::raw(symbols::line::VERTICAL),
|
||||||
|
@ -114,6 +142,48 @@ impl<'a> Tabs<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the titles of the tabs.
|
||||||
|
///
|
||||||
|
/// `titles` is an iterator whose elements can be converted into `Line`.
|
||||||
|
///
|
||||||
|
/// The selected tab can be set with [`Tabs::select`]. The first tab has index 0 (this is also
|
||||||
|
/// the default index).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Basic titles.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ratatui::widgets::Tabs;
|
||||||
|
///
|
||||||
|
/// let tabs = Tabs::default().titles(vec!["Tab 1", "Tab 2"]);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Styled titles.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ratatui::{style::Stylize, widgets::Tabs};
|
||||||
|
///
|
||||||
|
/// let tabs = Tabs::default().titles(vec!["Tab 1".red(), "Tab 2".blue()]);
|
||||||
|
/// ```
|
||||||
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
|
pub fn titles<Iter>(mut self, titles: Iter) -> Self
|
||||||
|
where
|
||||||
|
Iter: IntoIterator,
|
||||||
|
Iter::Item: Into<Line<'a>>,
|
||||||
|
{
|
||||||
|
self.titles = titles.into_iter().map(Into::into).collect_vec();
|
||||||
|
self.selected = if self.titles.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// Ensure selected is within bounds, and default to 0 if no selected tab
|
||||||
|
self.selected
|
||||||
|
.map(|selected| selected.min(self.titles.len() - 1))
|
||||||
|
.or(Some(0))
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Surrounds the `Tabs` with a [`Block`].
|
/// Surrounds the `Tabs` with a [`Block`].
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||||
|
@ -125,9 +195,27 @@ impl<'a> Tabs<'a> {
|
||||||
///
|
///
|
||||||
/// The first tab has index 0 (this is also the default index).
|
/// The first tab has index 0 (this is also the default index).
|
||||||
/// The selected tab can have a different style with [`Tabs::highlight_style`].
|
/// The selected tab can have a different style with [`Tabs::highlight_style`].
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Select the second tab.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ratatui::widgets::Tabs;
|
||||||
|
///
|
||||||
|
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).select(1);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Deselect the selected tab.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ratatui::widgets::Tabs;
|
||||||
|
///
|
||||||
|
/// let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]).select(None);
|
||||||
|
/// ```
|
||||||
#[must_use = "method moves the value of self and returns the modified value"]
|
#[must_use = "method moves the value of self and returns the modified value"]
|
||||||
pub const fn select(mut self, selected: usize) -> Self {
|
pub fn select<T: Into<Option<usize>>>(mut self, selected: T) -> Self {
|
||||||
self.selected = selected;
|
self.selected = selected.into();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,7 +401,7 @@ impl Tabs<'_> {
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
let pos = buf.set_line(x, tabs_area.top(), title, remaining_width);
|
let pos = buf.set_line(x, tabs_area.top(), title, remaining_width);
|
||||||
if i == self.selected {
|
if Some(i) == self.selected {
|
||||||
buf.set_style(
|
buf.set_style(
|
||||||
Rect {
|
Rect {
|
||||||
x,
|
x,
|
||||||
|
@ -372,7 +460,7 @@ mod tests {
|
||||||
Line::from("Tab3"),
|
Line::from("Tab3"),
|
||||||
Line::from("Tab4"),
|
Line::from("Tab4"),
|
||||||
],
|
],
|
||||||
selected: 0,
|
selected: Some(0),
|
||||||
style: Style::default(),
|
style: Style::default(),
|
||||||
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
||||||
divider: Span::raw(symbols::line::VERTICAL),
|
divider: Span::raw(symbols::line::VERTICAL),
|
||||||
|
@ -382,6 +470,37 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default() {
|
||||||
|
assert_eq!(
|
||||||
|
Tabs::default(),
|
||||||
|
Tabs {
|
||||||
|
block: None,
|
||||||
|
titles: vec![],
|
||||||
|
selected: None,
|
||||||
|
style: Style::default(),
|
||||||
|
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
||||||
|
divider: Span::raw(symbols::line::VERTICAL),
|
||||||
|
padding_right: Line::from(" "),
|
||||||
|
padding_left: Line::from(" "),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_into() {
|
||||||
|
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
|
||||||
|
assert_eq!(tabs.clone().select(2).selected, Some(2));
|
||||||
|
assert_eq!(tabs.clone().select(None).selected, None);
|
||||||
|
assert_eq!(tabs.clone().select(1u8 as usize).selected, Some(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn select_before_titles() {
|
||||||
|
let tabs = Tabs::default().select(1).titles(["Tab1", "Tab2"]);
|
||||||
|
assert_eq!(tabs.selected, Some(1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn new_from_vec_of_str() {
|
fn new_from_vec_of_str() {
|
||||||
Tabs::new(vec!["a", "b"]);
|
Tabs::new(vec!["a", "b"]);
|
||||||
|
@ -410,7 +529,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_default() {
|
fn render_new() {
|
||||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
|
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
|
||||||
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||||
// first tab selected
|
// first tab selected
|
||||||
|
@ -490,6 +609,10 @@ mod tests {
|
||||||
// out of bounds selects no tab
|
// out of bounds selects no tab
|
||||||
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||||
test_case(tabs.clone().select(4), Rect::new(0, 0, 30, 1), &expected);
|
test_case(tabs.clone().select(4), Rect::new(0, 0, 30, 1), &expected);
|
||||||
|
|
||||||
|
// deselect
|
||||||
|
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||||
|
test_case(tabs.clone().select(None), Rect::new(0, 0, 30, 1), &expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in a new issue