Compare commits

..

4 Commits

Author SHA1 Message Date
Orhun Parmaksız
fadc73d62e chore(release): prepare for 0.26.3 (#1118)
🧀

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-05-20 15:20:29 +03:00
Josh McKinney
fcb5d589bb fix: make cargo test --doc work with unstable-widget-ref examples (#1117) 2024-05-19 16:18:45 -07:00
Josh McKinney
4955380932 build: remove pre-push hooks (#1115) 2024-05-18 22:09:15 -07:00
Josh McKinney
828d17a3f5 docs: add minimal example (#1114) 2024-05-18 21:18:59 -07:00
10 changed files with 5712 additions and 5818 deletions

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env bash
if !(command cargo-make >/dev/null 2>&1); then # Check if cargo-make is installed
echo Attempting to run cargo-make as part of the pre-push hook but it\'s not installed.
echo Please install it by running the following command:
echo
echo " cargo install --force cargo-make"
echo
echo If you don\'t want to run cargo-make as part of the pre-push hook, you can run
echo the following command instead of git push:
echo
echo " git push --no-verify"
exit 1
fi
cargo make ci

10551
CHANGELOG.md

File diff suppressed because it is too large Load Diff

View File

@@ -56,11 +56,9 @@ documented.
### Run CI tests before pushing a PR
We're using [cargo-husky](https://github.com/rhysd/cargo-husky) to automatically run git hooks,
which will run `cargo make ci` before each push. To initialize the hook run `cargo test`. If
`cargo-make` is not installed, it will provide instructions to install it for you. This will ensure
that your code is formatted, compiles and passes all tests before you push. If you need to skip this
check, you can use `git push --no-verify`.
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process.
It's not mandatory to do this before pushing, however it may save you time to do so instead of
waiting for GitHub to run the checks.
### Sign your commits

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.26.2" # crate version
version = "0.26.3" # crate version
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
description = "A library that's all about cooking up terminal user interfaces"
documentation = "https://docs.rs/ratatui/latest/ratatui/"
@@ -47,9 +47,6 @@ unicode-width = "0.1"
anyhow = "1.0.71"
argh = "0.1.12"
better-panic = "0.3.0"
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.20.0"
@@ -274,6 +271,12 @@ name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "minimal"
required-features = ["crossterm"]
# prefer to show the more featureful examples in the docs
doc-scrape-examples = false
[[example]]
name = "modifiers"
required-features = ["crossterm"]

44
examples/minimal.rs Normal file
View File

@@ -0,0 +1,44 @@
//! # [Ratatui] Minimal example
//!
//! The latest version of this example is available in the [examples] folder in the repository.
//!
//! Please note that the examples are designed to be run against the `main` branch of the Github
//! repository. This means that you may not be able to compile with the latest release version on
//! crates.io, or the one that you have installed locally.
//!
//! See the [examples readme] for more information on finding examples that match the version of the
//! library you are using.
//!
//! [Ratatui]: https://github.com/ratatui-org/ratatui
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, text::Text, Terminal};
/// This is a bare minimum example. There are many approaches to running an application loop, so
/// this is not meant to be prescriptive. See the [examples] folder for more complete examples.
/// In particular, the [hello-world] example is a good starting point.
///
/// [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
/// [hello-world]: https://github.com/ratatui-org/ratatui/blob/main/examples/hello_world.rs
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut terminal = Terminal::new(CrosstermBackend::new(std::io::stdout()))?;
enable_raw_mode()?;
execute!(terminal.backend_mut(), EnterAlternateScreen)?;
loop {
terminal.draw(|frame| frame.render_widget(Text::raw("Hello World!"), frame.size()))?;
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}

View File

@@ -83,6 +83,7 @@ impl Frame<'_> {
/// # Example
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::Block};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
@@ -90,6 +91,7 @@ impl Frame<'_> {
/// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget_ref(block, area);
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]
@@ -138,6 +140,7 @@ impl Frame<'_> {
/// # Example
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// # use ratatui::{backend::TestBackend, prelude::*, widgets::*};
/// # let backend = TestBackend::new(5, 5);
/// # let mut terminal = Terminal::new(backend).unwrap();
@@ -146,6 +149,7 @@ impl Frame<'_> {
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget_ref(list, area, &mut state);
/// # }
/// ```
#[allow(clippy::needless_pass_by_value)]
#[stability::unstable(feature = "widget-ref")]

View File

@@ -248,6 +248,7 @@ pub trait StatefulWidget {
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
///
/// struct Greeting;
@@ -294,6 +295,7 @@ pub trait StatefulWidget {
/// widget.render_ref(area, buf);
/// }
/// # }
/// # }
/// ```
#[stability::unstable(feature = "widget-ref")]
pub trait WidgetRef {
@@ -321,6 +323,7 @@ impl<W: WidgetRef> Widget for &W {
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
///
/// struct Parent {
@@ -340,6 +343,7 @@ impl<W: WidgetRef> Widget for &W {
/// self.child.render_ref(area, buf);
/// }
/// }
/// # }
/// ```
impl<W: WidgetRef> WidgetRef for Option<W> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
@@ -368,6 +372,7 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// # Examples
///
/// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::{prelude::*, widgets::*};
///
/// struct PersonalGreeting;
@@ -386,10 +391,11 @@ impl<W: WidgetRef> WidgetRef for Option<W> {
/// }
/// }
///
/// # fn render(area: Rect, buf: &mut Buffer) {
/// let widget = PersonalGreeting;
/// let mut state = "world".to_string();
/// widget.render(area, buf, &mut state);
/// fn render(area: Rect, buf: &mut Buffer) {
/// let widget = PersonalGreeting;
/// let mut state = "world".to_string();
/// widget.render(area, buf, &mut state);
/// }
/// # }
/// ```
#[stability::unstable(feature = "widget-ref")]
@@ -470,90 +476,79 @@ mod tests {
use super::*;
use crate::prelude::*;
struct Greeting;
struct Farewell;
struct PersonalGreeting;
impl Widget for Greeting {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
impl Widget for Farewell {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
impl WidgetRef for Farewell {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Goodbye").right_aligned().render(area, buf);
}
}
impl StatefulWidget for PersonalGreeting {
type State = String;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
self.render_ref(area, buf, state);
}
}
impl StatefulWidgetRef for PersonalGreeting {
type State = String;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[fixture]
fn buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 20, 1))
}
#[rstest]
fn widget_render(mut buf: Buffer) {
let widget = Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
mod widget {
use super::*;
#[rstest]
fn widget_ref_render(mut buf: Buffer) {
let widget = Greeting;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
struct Greeting;
/// This test is to ensure that the blanket implementation of `Widget` for `&W` where `W`
/// implements `WidgetRef` works as expected.
#[rstest]
fn widget_blanket_render(mut buf: Buffer) {
let widget = &Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn widget_box_render_ref(mut buf: Buffer) {
let widget: Box<dyn WidgetRef> = Box::new(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn widget_vec_box_render(mut buf: Buffer) {
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
for widget in widgets {
widget.render_ref(buf.area, &mut buf);
impl Widget for Greeting {
fn render(self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer) {
let widget = Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
}
mod widget_ref {
use super::*;
struct Greeting;
struct Farewell;
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
impl WidgetRef for Farewell {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Goodbye").right_aligned().render(area, buf);
}
}
#[rstest]
fn render_ref(mut buf: Buffer) {
let widget = Greeting;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
/// Ensure that the blanket implementation of `Widget` for `&W` where `W` implements
/// `WidgetRef` works as expected.
#[rstest]
fn blanket_render(mut buf: Buffer) {
let widget = &Greeting;
widget.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn box_render_ref(mut buf: Buffer) {
let widget: Box<dyn WidgetRef> = Box::new(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn vec_box_render(mut buf: Buffer) {
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)];
for widget in widgets {
widget.render_ref(buf.area, &mut buf);
}
assert_eq!(buf, Buffer::with_lines(["Hello Goodbye"]));
}
assert_eq!(buf, Buffer::with_lines(["Hello Goodbye"]));
}
#[fixture]
@@ -561,98 +556,143 @@ mod tests {
"world".to_string()
}
#[rstest]
fn stateful_widget_render(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
mod stateful_widget {
use super::*;
struct PersonalGreeting;
impl StatefulWidget for PersonalGreeting {
type State = String;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[rstest]
fn render(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
}
#[rstest]
fn stateful_widget_ref_render(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
mod stateful_widget_ref {
use super::*;
struct PersonalGreeting;
impl StatefulWidgetRef for PersonalGreeting {
type State = String;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf);
}
}
#[rstest]
fn render_ref(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting;
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W
// where W implements StatefulWidgetRef is added. (see the comment in the blanket
// implementation for more).
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W`
// where /// `W` implements `StatefulWidgetRef` works as expected.
// #[rstest]
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
// let widget = &PersonalGreeting;
// widget.render(buf.area, &mut buf, &mut state);
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
// }
#[rstest]
fn box_render_render(mut buf: Buffer, mut state: String) {
let widget = Box::new(PersonalGreeting);
widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
}
}
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W where W
// implements StatefulWidgetRef is added. (see the comment in the blanket implementation for
// more).
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W` where
// /// `W` implements `StatefulWidgetRef` works as expected.
// #[rstest]
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
// let widget = &PersonalGreeting;
// widget.render(buf.area, &mut buf, &mut state);
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
// }
mod option_widget_ref {
use super::*;
#[rstest]
fn stateful_widget_box_render(mut buf: Buffer, mut state: String) {
let widget = Box::new(PersonalGreeting);
widget.render(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
struct Greeting;
impl WidgetRef for Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf);
}
}
#[rstest]
fn render_ref_some(mut buf: Buffer) {
let widget = Some(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
#[rstest]
fn render_ref_none(mut buf: Buffer) {
let widget: Option<Greeting> = None;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" "]));
}
}
#[rstest]
fn widget_option_render_ref_some(mut buf: Buffer) {
let widget = Some(Greeting);
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
mod str {
use super::*;
#[rstest]
fn render(mut buf: Buffer) {
"hello world".render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render(mut buf: Buffer) {
Some("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render_ref(mut buf: Buffer) {
Some("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
#[rstest]
fn widget_option_render_ref_none(mut buf: Buffer) {
let widget: Option<Greeting> = None;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" "]));
}
mod string {
use super::*;
#[rstest]
fn render(mut buf: Buffer) {
String::from("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn str_render(mut buf: Buffer) {
"hello world".render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn str_render_ref(mut buf: Buffer) {
"hello world".render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn option_render(mut buf: Buffer) {
Some(String::from("hello world")).render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn str_option_render(mut buf: Buffer) {
Some("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn str_option_render_ref(mut buf: Buffer) {
Some("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_render(mut buf: Buffer) {
String::from("hello world").render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_render_ref(mut buf: Buffer) {
String::from("hello world").render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_option_render(mut buf: Buffer) {
Some(String::from("hello world")).render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
#[rstest]
fn string_option_render_ref(mut buf: Buffer) {
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
#[rstest]
fn option_render_ref(mut buf: Buffer) {
Some(String::from("hello world")).render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["hello world "]));
}
}
}

View File

@@ -213,15 +213,6 @@ pub struct Table<'a> {
/// Symbol in front of the selected row
highlight_symbol: Text<'a>,
/// Symbol in front of the marked row
mark_symbol: Text<'a>,
/// Symbol in front of the unmarked row
unmark_symbol: Text<'a>,
/// Symbol in front of the marked and selected row
mark_highlight_symbol: Text<'a>,
/// Decides when to allocate spacing for the row selection
highlight_spacing: HighlightSpacing,
@@ -241,9 +232,6 @@ impl<'a> Default for Table<'a> {
style: Style::new(),
highlight_style: Style::new(),
highlight_symbol: Text::default(),
mark_symbol: Text::default(),
unmark_symbol: Text::default(),
mark_highlight_symbol: Text::default(),
highlight_spacing: HighlightSpacing::default(),
flex: Flex::Start,
}
@@ -515,60 +503,6 @@ impl<'a> Table<'a> {
self
}
/// Set the symbol to be displayed in front of the marked row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).mark_symbol("\u{2714}");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn mark_symbol<T: Into<Text<'a>>>(mut self, mark_symbol: T) -> Self {
self.mark_symbol = mark_symbol.into();
self
}
/// Set the symbol to be displayed in front of the unmarked row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).unmark_symbol(" ");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn unmark_symbol<T: Into<Text<'a>>>(mut self, unmark_symbol: T) -> Self {
self.unmark_symbol = unmark_symbol.into();
self
}
/// Set the symbol to be displayed in front of the marked and selected row
///
/// This is a fluent setter method which must be chained or used as it consumes self
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # let rows = [Row::new(vec!["Cell1", "Cell2"])];
/// # let widths = [Constraint::Length(5), Constraint::Length(5)];
/// let table = Table::new(rows, widths).mark_highlight_symbol("\u{29bf}");
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn mark_highlight_symbol<T: Into<Text<'a>>>(mut self, mark_highlight_symbol: T) -> Self {
self.mark_highlight_symbol = mark_highlight_symbol.into();
self
}
/// Set when to show the highlight spacing
///
/// The highlight spacing is the spacing that is allocated for the selection symbol column (if
@@ -670,8 +604,8 @@ impl StatefulWidgetRef for Table<'_> {
return;
}
let highlight_column_width = self.highlight_column_width(state);
let columns_widths = self.get_columns_widths(table_area.width, highlight_column_width);
let selection_width = self.selection_width(state);
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let (header_area, rows_area, footer_area) = self.layout(table_area);
self.render_header(header_area, buf, &columns_widths);
@@ -680,13 +614,8 @@ impl StatefulWidgetRef for Table<'_> {
rows_area,
buf,
state,
highlight_column_width,
(
&self.highlight_symbol,
&self.mark_symbol,
&self.unmark_symbol,
&self.mark_highlight_symbol,
),
selection_width,
&self.highlight_symbol,
&columns_widths,
);
@@ -741,16 +670,14 @@ impl Table<'_> {
area: Rect,
buf: &mut Buffer,
state: &mut TableState,
highlight_column_width: u16,
symbols: (&Text<'_>, &Text<'_>, &Text<'_>, &Text<'_>),
selection_width: u16,
highlight_symbol: &Text<'_>,
columns_widths: &[(u16, u16)],
) {
if self.rows.is_empty() {
return;
}
let (highlight_symbol, mark_symbol, unmark_symbol, mark_highlight_symbol) = symbols;
let (start_index, end_index) =
self.get_row_bounds(state.selected, state.offset, area.height);
state.offset = start_index;
@@ -771,37 +698,22 @@ impl Table<'_> {
);
buf.set_style(row_area, row.style);
let is_marked = state.marked().contains(&(i + state.offset));
let is_highlighted = state.selected().is_some_and(|index| index == i);
if highlight_column_width > 0 {
let area = Rect {
width: highlight_column_width,
let is_selected = state.selected().is_some_and(|index| index == i);
if selection_width > 0 && is_selected {
let selection_area = Rect {
width: selection_width,
..row_area
};
buf.set_style(area, row.style);
match (is_marked, is_highlighted) {
(true, true) => {
mark_highlight_symbol.render(area, buf);
}
(true, false) => {
mark_symbol.render(area, buf);
}
(false, true) => {
highlight_symbol.render(area, buf);
}
(false, false) => {
unmark_symbol.render(area, buf);
}
};
}
buf.set_style(selection_area, row.style);
highlight_symbol.clone().render(selection_area, buf);
};
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
cell.render(
Rect::new(row_area.x + x, row_area.y, *width, row_area.height),
buf,
);
}
if is_highlighted {
if is_selected {
buf.set_style(row_area, self.highlight_style);
}
y_offset += row.height_with_margin();
@@ -876,35 +788,15 @@ impl Table<'_> {
(start, end)
}
/// Returns the width of the indicator column if a row is selected, rows are marked,
/// or the `highlight_spacing` is set to show the column always, otherwise 0.
fn highlight_column_width(&self, state: &TableState) -> u16 {
let has_highlight = state.selected().is_some() || state.marked().len() > 0;
let highlight_column_width = if self.highlight_spacing.should_add(has_highlight) {
/// Returns the width of the selection column if a row is selected, or the `highlight_spacing`
/// is set to show the column always, otherwise 0.
fn selection_width(&self, state: &TableState) -> u16 {
let has_selection = state.selected().is_some();
if self.highlight_spacing.should_add(has_selection) {
self.highlight_symbol.width() as u16
} else {
0
};
let mark_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.mark_symbol.width() as u16
} else {
0
};
let mark_highlight_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.mark_highlight_symbol.width() as u16
} else {
0
};
let unmark_column_width = if self.highlight_spacing.should_add(has_highlight) {
self.unmark_symbol.width() as u16
} else {
0
};
highlight_column_width
.max(mark_column_width)
.max(mark_highlight_column_width)
.max(unmark_column_width)
}
}
}
@@ -1298,100 +1190,6 @@ mod tests {
]);
assert_eq!(buf, expected);
}
#[test]
fn render_with_selected_marked_unmarked() {
let rows = vec![
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
Row::new(vec!["Cell", "Cell"]),
];
let table = Table::new(rows, [Constraint::Length(5); 2])
.highlight_symbol("")
.mark_symbol("")
.unmark_symbol(" ")
.mark_highlight_symbol("⦿");
let mut state = TableState::new().with_selected(0);
state.mark(1);
state.mark(3);
state.mark(5);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"• Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.mark(0);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"⦿ Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.select(Some(1));
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
"⦾ Cell Cell ".into(),
"⦿ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
state.unmark(0);
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 10));
StatefulWidget::render(table.clone(), Rect::new(0, 0, 15, 10), &mut buf, &mut state);
let expected = Buffer::with_lines(Text::from(vec![
" Cell Cell ".into(),
"⦿ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
"⦾ Cell Cell ".into(),
" Cell Cell ".into(),
" ".into(),
" ".into(),
" ".into(),
]));
assert_eq!(buf, expected);
}
}
// test how constraints interact with table column width allocation
@@ -1590,214 +1388,6 @@ mod tests {
assert_eq!(table.get_columns_widths(10, 0), [(0, 5), (5, 5)]);
}
#[track_caller]
fn test_table_with_selection_and_marks<'line, Lines, Marks>(
highlight_spacing: HighlightSpacing,
columns: u16,
spacing: u16,
selection: Option<usize>,
marks: Marks,
expected: Lines,
) where
Lines: IntoIterator,
Lines::Item: Into<Line<'line>>,
Marks: IntoIterator<Item = usize>,
{
let table = Table::default()
.rows(vec![Row::new(vec!["ABCDE", "12345"])])
.highlight_spacing(highlight_spacing)
.highlight_symbol(">>>")
.mark_symbol(" MMM ")
.mark_highlight_symbol(" >M> ")
.column_spacing(spacing);
let area = Rect::new(0, 0, columns, 3);
let mut buf = Buffer::empty(area);
let mut state = TableState::default().with_selected(selection);
for mark in marks {
state.mark(mark);
}
StatefulWidget::render(table, area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(expected));
}
#[test]
#[allow(clippy::too_many_lines)]
fn highlight_symbol_mark_symbol_and_column_spacing_with_highlight_spacing() {
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[0], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered ever
test_table_with_selection_and_marks(
HighlightSpacing::Never,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
None, // selection
[], // marks
[
"ABCDE 12345 ", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
None, // selection
[0], // marks
[
" MMM ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// highlight symbol rendered with mark symbol width
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
Some(0), // selection
[], // marks
[
">>> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark highlight symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::WhenSelected,
15, // width
0, // spacing
Some(0), // selection
[0], // marks
[
" >M> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// no highlight_symbol or mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
None, // selection
[], // marks
[
" ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark_symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
None, // selection
[0], // marks
[
" MMM ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// highlight symbol rendered with mark symbol width
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
Some(0), // selection
[], // marks
[
">>> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
// mark highlight symbol rendered
test_table_with_selection_and_marks(
HighlightSpacing::Always,
15, // width
0, // spacing
Some(0), // selection
[0], // marks
[
" >M> ABCDE12345", /* default layout is Flex::Start but columns length
* constraints are calculated as `max_area / n_columns`,
* i.e. they are distributed amongst available space */
" ", // row 2
" ", // row 3
],
);
}
#[track_caller]
fn test_table_with_selection<'line, Lines>(
highlight_spacing: HighlightSpacing,

View File

@@ -49,7 +49,6 @@
pub struct TableState {
pub(crate) offset: usize,
pub(crate) selected: Option<usize>,
pub(crate) marked: Vec<usize>,
}
impl TableState {
@@ -65,7 +64,6 @@ impl TableState {
Self {
offset: 0,
selected: None,
marked: vec![],
}
}
@@ -177,78 +175,6 @@ impl TableState {
self.offset = 0;
}
}
/// Sets the index of the row as marked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.mark(1);
/// ```
pub fn mark(&mut self, index: usize) {
if !self.marked.contains(&index) {
self.marked.push(index);
}
}
/// Sets the index of the row as unmarked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.unmark(1);
/// ```
pub fn unmark(&mut self, index: usize) {
self.marked.retain(|i| *i != index);
}
/// Toggles the index of the row as marked or unmarked
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.toggle_mark(1);
/// ```
pub fn toggle_mark(&mut self, index: usize) {
if self.marked.contains(&index) {
self.unmark(index);
} else {
self.mark(index);
}
}
/// Returns a iterator of all marked rows
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// # use itertools::Itertools;
/// let mut state = TableState::default();
/// state.marked().contains(&1);
/// ```
pub fn marked(&self) -> std::slice::Iter<'_, usize> {
self.marked.iter()
}
/// Clears all marks from all rows
///
/// # Examples
///
/// ```rust
/// # use ratatui::{prelude::*, widgets::*};
/// let mut state = TableState::default();
/// state.clear_marks();
/// ```
pub fn clear_marks(&mut self) {
self.marked.drain(..);
}
}
#[cfg(test)]

View File

@@ -14,7 +14,6 @@
// not too happy about the redundancy in these tests,
// but if that helps readability then it's ok i guess /shrug
use pretty_assertions::assert_eq;
use ratatui::{backend::TestBackend, prelude::*, widgets::*};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
@@ -99,8 +98,7 @@ const DEFAULT_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": null,
"marked": []
"selected": null
},
"scrollbar": {
"content_length": 10,
@@ -137,8 +135,7 @@ const SELECTED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 0,
"selected": 1,
"marked": []
"selected": 1
},
"scrollbar": {
"content_length": 10,
@@ -177,8 +174,7 @@ const SCROLLED_STATE_REPR: &str = r#"{
},
"table": {
"offset": 4,
"selected": 8,
"marked": []
"selected": 8
},
"scrollbar": {
"content_length": 10,