mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-21 20:23:11 +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)
|
||||
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||
|
||||
- `Tabs::select` now accepts `Into<Option<usize>>`
|
||||
- [v0.28.0](#v0280)
|
||||
⁻ `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend` trait migrates to `get/set_cursor_position`
|
||||
|
@ -70,6 +70,19 @@ This is a quick summary of the sections below:
|
|||
|
||||
## 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])
|
||||
|
||||
[#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`
|
||||
|
||||
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
||||
|
||||
`Frame::size` is renamed to `Frame::area` as it's the more correct name.
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use itertools::Itertools;
|
||||
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
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>();
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Tabs<'a> {
|
||||
/// A block to wrap this widget in if necessary
|
||||
block: Option<Block<'a>>,
|
||||
/// One title for each tab
|
||||
titles: Vec<Line<'a>>,
|
||||
/// The index of the selected tabs
|
||||
selected: usize,
|
||||
selected: Option<usize>,
|
||||
/// The style used to draw the text
|
||||
style: Style,
|
||||
/// Style to apply to the selected item
|
||||
|
@ -64,6 +66,30 @@ pub struct Tabs<'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> {
|
||||
/// Creates new `Tabs` from their titles.
|
||||
///
|
||||
|
@ -102,10 +128,12 @@ impl<'a> Tabs<'a> {
|
|||
Iter: IntoIterator,
|
||||
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 {
|
||||
block: None,
|
||||
titles: titles.into_iter().map(Into::into).collect(),
|
||||
selected: 0,
|
||||
titles,
|
||||
selected,
|
||||
style: Style::default(),
|
||||
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
||||
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`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
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 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"]
|
||||
pub const fn select(mut self, selected: usize) -> Self {
|
||||
self.selected = selected;
|
||||
pub fn select<T: Into<Option<usize>>>(mut self, selected: T) -> Self {
|
||||
self.selected = selected.into();
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -313,7 +401,7 @@ impl Tabs<'_> {
|
|||
|
||||
// Title
|
||||
let pos = buf.set_line(x, tabs_area.top(), title, remaining_width);
|
||||
if i == self.selected {
|
||||
if Some(i) == self.selected {
|
||||
buf.set_style(
|
||||
Rect {
|
||||
x,
|
||||
|
@ -372,7 +460,7 @@ mod tests {
|
|||
Line::from("Tab3"),
|
||||
Line::from("Tab4"),
|
||||
],
|
||||
selected: 0,
|
||||
selected: Some(0),
|
||||
style: Style::default(),
|
||||
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
|
||||
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]
|
||||
fn new_from_vec_of_str() {
|
||||
Tabs::new(vec!["a", "b"]);
|
||||
|
@ -410,7 +529,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn render_default() {
|
||||
fn render_new() {
|
||||
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
|
||||
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
// first tab selected
|
||||
|
@ -490,6 +609,10 @@ mod tests {
|
|||
// out of bounds selects no tab
|
||||
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
|
||||
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]
|
||||
|
|
Loading…
Reference in a new issue