Compare commits

..

16 Commits

Author SHA1 Message Date
Tayfun Bocek
be3eb75ea5 perf(table): avoid extra allocations when rendering Table (#1242)
When rendering a `Table` the `Text` stored inside of a `Cell` gets
cloned before rendering. This removes the clone and uses `WidgetRef`
instead, saving us from allocating a `Vec<Line<'_>>` inside `Text`. Also
avoids an allocation when rendering the highlight symbol if it contains
an owned value.
2024-07-26 21:48:29 +03:00
Dheepak Krishnamurthy
efef0d0dc0 chore(ci): change label from breaking change to Type: Breaking Change (#1243)
This PR changes the label that is auto attached to a PR with a breaking
change per the conventional commits specification.
2024-07-26 21:48:05 +03:00
Tayfun Bocek
663486f1e8 perf(list): avoid extra allocations when rendering List (#1244)
When rendering a `List`, each `ListItem` would be cloned. Removing the
clone, and replacing `Widget::render` with `WidgetRef::render_ref` saves
us allocations caused by the clone of the `Text<'_>` stored inside of
`ListItem`.

Based on the results of running the "list" benchmark locally;
Performance is improved by %1-3 for all `render` benchmarks for `List`.
2024-07-26 21:47:30 +03:00
Alex Saveau
7ddfbc0010 fix: unnecessary allocations when creating Lines (#1237)
Signed-off-by: Alex Saveau <saveau.alexandre@gmail.com>
2024-07-26 01:55:04 -07:00
Josh McKinney
3725262ca3 feat(text): Add Add and AddAssign implementations for Line, Span, and Text (#1236)
This enables:

```rust
let line = Span::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Span::raw("blue").blue();
let line = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Line::raw("Red").red() + Line::raw("Blue").blue();
let text = Text::raw("Red").red() + Line::raw("Blue").blue();

let mut line = Line::raw("Red").red();
line += Span::raw("Blue").blue();

let mut text = Text::raw("Red").red();
text += Line::raw("Blue").blue();

line.extend(vec![Span::raw("1"), Span::raw("2"), Span::raw("3")]);
```
2024-07-26 01:37:49 -07:00
Josh McKinney
84f334163b fix: clippy lints from rust 1.80.0 (#1238) 2024-07-26 00:55:07 -07:00
airblast
03f3124c1d fix(paragraph): line_width, and line_count include block borders (#1235)
The `line_width`, and `line_count` methods for `Paragraph` would not
take into account the `Block` if one was set. This will now correctly
calculate the values including the `Block`'s width/height.

Fixes: #1233
2024-07-23 13:13:50 -07:00
Josh McKinney
c34fb77818 feat(text)!: remove unnecessary lifetime from ToText trait (#1234)
BREAKING CHANGE: The ToText trait no longer has a lifetime parameter.
This change simplifies the trait and makes it easier implement.
2024-07-22 04:24:30 -07:00
Josh McKinney
6ce447c4f3 docs(block): add docs about style inheritance (#1190)
Fixes: https://github.com/ratatui-org/ratatui/issues/1129
2024-07-19 20:02:08 -07:00
Josh McKinney
272d0591a7 docs(paragraph): update main docs (#1202) 2024-07-18 15:59:48 -07:00
Josh McKinney
e81663bec0 refactor(list): split up list.rs into smaller modules (#1204) 2024-07-18 15:59:35 -07:00
EdJoPaTo
7e1bab049b fix(buffer): dont render control characters (#1226) 2024-07-17 13:22:00 +02:00
EdJoPaTo
379dab9cdb build: cleanup dev dependencies (#1231) 2024-07-16 04:35:54 -07:00
Josh McKinney
5b51018501 feat(chart): add GraphType::Bar (#1205)
![Demo](https://vhs.charm.sh/vhs-50v7I5n7lQF7tHCb1VCmFc.gif)
2024-07-15 20:47:50 -07:00
Josh McKinney
7bab9f0d80 chore: add more CompactString::const_new instead of new (#1230) 2024-07-15 20:29:32 -07:00
dependabot[bot]
6d210b3b6b chore(deps): update compact_str requirement from 0.7.1 to 0.8.0 (#1229)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-07-15 14:02:44 -07:00
23 changed files with 3076 additions and 2359 deletions

View File

@@ -71,7 +71,7 @@ jobs:
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
labels: ['breaking change']
labels: ['Type: Breaking Change']
})
do-not-merge:

View File

@@ -27,7 +27,7 @@ rust-version = "1.74.0"
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
compact_str = "0.7.1"
compact_str = "0.8.0"
crossterm = { version = "0.27", optional = true }
document-features = { version = "0.2.7", optional = true }
instability = "0.3.1"
@@ -48,14 +48,12 @@ unicode-width = "0.1.13"
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1.12"
better-panic = "0.3.0"
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
indoc = "2"
palette = "0.7.3"
pretty_assertions = "1.4.0"
rand = "0.8.5"
rand_chacha = "0.3.1"
@@ -80,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"
@@ -231,7 +233,7 @@ doc-scrape-examples = false
[[example]]
name = "colors_rgb"
required-features = ["crossterm"]
required-features = ["crossterm", "palette"]
doc-scrape-examples = true
[[example]]
@@ -256,7 +258,7 @@ doc-scrape-examples = false
[[example]]
name = "demo2"
required-features = ["crossterm", "widget-calendar"]
required-features = ["crossterm", "palette", "widget-calendar"]
doc-scrape-examples = true
[[example]]

View File

@@ -153,17 +153,18 @@ fn run_app<B: Backend>(
fn ui(frame: &mut Frame, app: &App) {
let area = frame.size();
let vertical = Layout::vertical([Constraint::Percentage(40), Constraint::Percentage(60)]);
let horizontal = Layout::horizontal([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]);
let [chart1, bottom] = vertical.areas(area);
let [line_chart, scatter] = horizontal.areas(bottom);
let [top, bottom] = Layout::vertical([Constraint::Fill(1); 2]).areas(area);
let [animated_chart, bar_chart] =
Layout::horizontal([Constraint::Fill(1), Constraint::Length(29)]).areas(top);
let [line_chart, scatter] = Layout::horizontal([Constraint::Fill(1); 2]).areas(bottom);
render_chart1(frame, chart1, app);
render_animated_chart(frame, animated_chart, app);
render_barchart(frame, bar_chart);
render_line_chart(frame, line_chart);
render_scatter(frame, scatter);
}
fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
fn render_animated_chart(f: &mut Frame, area: Rect, app: &App) {
let x_labels = vec![
Span::styled(
format!("{}", app.window[0]),
@@ -189,7 +190,7 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
];
let chart = Chart::new(datasets)
.block(Block::bordered().title("Chart 1".cyan().bold()))
.block(Block::bordered())
.x_axis(
Axis::default()
.title("X Axis")
@@ -208,6 +209,51 @@ fn render_chart1(f: &mut Frame, area: Rect, app: &App) {
f.render_widget(chart, area);
}
fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
let dataset = Dataset::default()
.marker(symbols::Marker::HalfBlock)
.style(Style::new().fg(Color::Blue))
.graph_type(GraphType::Bar)
// a bell curve
.data(&[
(0., 0.4),
(10., 2.9),
(20., 13.5),
(30., 41.1),
(40., 80.1),
(50., 100.0),
(60., 80.1),
(70., 41.1),
(80., 13.5),
(90., 2.9),
(100., 0.4),
]);
let chart = Chart::new(vec![dataset])
.block(
Block::bordered().title(
Title::default()
.content("Bar chart".cyan().bold())
.alignment(Alignment::Center),
),
)
.x_axis(
Axis::default()
.style(Style::default().gray())
.bounds([0.0, 100.0])
.labels(vec!["0".bold(), "50".into(), "100.0".bold()]),
)
.y_axis(
Axis::default()
.style(Style::default().gray())
.bounds([0.0, 100.0])
.labels(vec!["0".bold(), "50".into(), "100.0".bold()]),
)
.hidden_legend_constraints((Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)));
frame.render_widget(chart, bar_chart);
}
fn render_line_chart(f: &mut Frame, area: Rect) {
let datasets = vec![Dataset::default()
.name("Line from only 2 points".italic())

View File

@@ -2,7 +2,6 @@
//! A module for the [`Buffer`] and [`Cell`] types.
mod assert;
#[allow(clippy::module_inception)]
mod buffer;
mod cell;

View File

@@ -192,7 +192,7 @@ impl Buffer {
}
/// Print at most the first n characters of a string if enough space is available
/// until the end of the line.
/// until the end of the line. Skips zero-width graphemes and control characters.
///
/// Use [`Buffer::set_string`] when the maximum amount of characters can be printed.
pub fn set_stringn<T, S>(
@@ -210,6 +210,7 @@ impl Buffer {
let max_width = max_width.try_into().unwrap_or(u16::MAX);
let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
.filter(|symbol| !symbol.contains(|char: char| char.is_control()))
.map(|symbol| (symbol, symbol.width() as u16))
.filter(|(_symbol, width)| *width > 0)
.map_while(|(symbol, width)| {
@@ -931,4 +932,35 @@ mod tests {
buf.set_string(0, 1, "bar", Style::new().blue());
assert_eq!(buf, Buffer::with_lines(["foo".red(), "bar".blue()]));
}
#[test]
fn control_sequence_rendered_full() {
let text = "I \x1b[0;36mwas\x1b[0m here!";
let mut buffer = Buffer::filled(Rect::new(0, 0, 25, 3), Cell::new("x"));
buffer.set_string(1, 1, text, Style::new());
let expected = Buffer::with_lines([
"xxxxxxxxxxxxxxxxxxxxxxxxx",
"xI [0;36mwas[0m here!xxxx",
"xxxxxxxxxxxxxxxxxxxxxxxxx",
]);
assert_eq!(buffer, expected);
}
#[test]
fn control_sequence_rendered_partially() {
let text = "I \x1b[0;36mwas\x1b[0m here!";
let mut buffer = Buffer::filled(Rect::new(0, 0, 11, 3), Cell::new("x"));
buffer.set_string(1, 1, text, Style::new());
#[rustfmt::skip]
let expected = Buffer::with_lines([
"xxxxxxxxxxx",
"xI [0;36mwa",
"xxxxxxxxxxx",
]);
assert_eq!(buffer, expected);
}
}

View File

@@ -41,11 +41,11 @@ impl Cell {
///
/// This works at compile time and puts the symbol onto the stack. Fails to build when the
/// symbol doesnt fit onto the stack and requires to be placed on the heap. Use
/// `Self::default().set_symbol()` in that case. See [`CompactString::new_inline`] for more
/// `Self::default().set_symbol()` in that case. See [`CompactString::const_new`] for more
/// details on this.
pub const fn new(symbol: &str) -> Self {
pub const fn new(symbol: &'static str) -> Self {
Self {
symbol: CompactString::new_inline(symbol),
symbol: CompactString::const_new(symbol),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]
@@ -139,7 +139,7 @@ impl Cell {
/// Resets the cell to the empty state.
pub fn reset(&mut self) {
self.symbol = CompactString::new_inline(" ");
self.symbol = CompactString::const_new(" ");
self.fg = Color::Reset;
self.bg = Color::Reset;
#[cfg(feature = "underline-color")]
@@ -167,7 +167,7 @@ mod tests {
assert_eq!(
cell,
Cell {
symbol: CompactString::new_inline(""),
symbol: CompactString::const_new(""),
fg: Color::Reset,
bg: Color::Reset,
#[cfg(feature = "underline-color")]

View File

@@ -4,7 +4,6 @@ mod alignment;
mod constraint;
mod direction;
mod flex;
#[allow(clippy::module_inception)]
mod layout;
mod margin;
mod position;

View File

@@ -32,7 +32,6 @@
//! [`Buffer`]: crate::buffer::Buffer
mod frame;
#[allow(clippy::module_inception)]
mod terminal;
mod viewport;

View File

@@ -5,7 +5,7 @@
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
//! - A multiple line string where each grapheme may have its own style is represented by a
//! [`Text`].
//! [`Text`].
//!
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
//! is a [`Line`].
@@ -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};

View File

@@ -161,6 +161,13 @@ pub struct Line<'a> {
pub alignment: Option<Alignment>,
}
fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
match content.into() {
Cow::Borrowed(s) => s.lines().map(Span::raw).collect(),
Cow::Owned(s) => s.lines().map(|v| Span::raw(v.to_string())).collect(),
}
}
impl<'a> Line<'a> {
/// Create a line with the default style.
///
@@ -186,17 +193,14 @@ impl<'a> Line<'a> {
T: Into<Cow<'a, str>>,
{
Self {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
spans: cow_to_spans(content),
..Default::default()
}
}
/// Create a line with the given style.
// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
///
/// `content` can be any type that is convertible to [`Cow<str>`] (e.g. [`&str`], [`String`],
/// [`Cow<str>`], or your own type that implements [`Into<Cow<str>>`]).
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
@@ -220,11 +224,7 @@ impl<'a> Line<'a> {
S: Into<Style>,
{
Self {
spans: content
.into()
.lines()
.map(|v| Span::raw(v.to_string()))
.collect(),
spans: cow_to_spans(content),
style: style.into(),
..Default::default()
}
@@ -548,6 +548,37 @@ where
}
}
/// Adds a `Span` to a `Line`, returning a new `Line` with the `Span` added.
impl<'a> std::ops::Add<Span<'a>> for Line<'a> {
type Output = Self;
fn add(mut self, rhs: Span<'a>) -> Self::Output {
self.spans.push(rhs);
self
}
}
/// Adds two `Line`s together, returning a new `Text` with the contents of the two `Line`s.
impl<'a> std::ops::Add<Self> for Line<'a> {
type Output = Text<'a>;
fn add(self, rhs: Self) -> Self::Output {
Text::from(vec![self, rhs])
}
}
impl<'a> std::ops::AddAssign<Span<'a>> for Line<'a> {
fn add_assign(&mut self, rhs: Span<'a>) {
self.spans.push(rhs);
}
}
impl<'a> Extend<Span<'a>> for Line<'a> {
fn extend<T: IntoIterator<Item = Span<'a>>>(&mut self, iter: T) {
self.spans.extend(iter);
}
}
impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
@@ -896,6 +927,62 @@ mod tests {
assert_eq!(line.spans, vec![span],);
}
#[test]
fn add_span() {
assert_eq!(
Line::raw("Red").red() + Span::raw("blue").blue(),
Line {
spans: vec![Span::raw("Red"), Span::raw("blue").blue()],
style: Style::new().red(),
alignment: None,
},
);
}
#[test]
fn add_line() {
assert_eq!(
Line::raw("Red").red() + Line::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red").red(), Line::raw("Blue").blue()],
style: Style::default(),
alignment: None,
}
);
}
#[test]
fn add_assign_span() {
let mut line = Line::raw("Red").red();
line += Span::raw("Blue").blue();
assert_eq!(
line,
Line {
spans: vec![Span::raw("Red"), Span::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
},
);
}
#[test]
fn extend() {
let mut line = Line::from("Hello, ");
line.extend(vec![Span::raw("world!")]);
assert_eq!(line.spans, vec![Span::raw("Hello, "), Span::raw("world!")]);
let mut line = Line::from("Hello, ");
line.extend(vec![Span::raw("world! "), Span::raw("How are you?")]);
assert_eq!(
line.spans,
vec![
Span::raw("Hello, "),
Span::raw("world! "),
Span::raw("How are you?")
]
);
}
#[test]
fn into_string() {
let line = Line::from(vec![

View File

@@ -342,6 +342,14 @@ where
}
}
impl<'a> std::ops::Add<Self> for Span<'a> {
type Output = Line<'a>;
fn add(self, rhs: Self) -> Self::Output {
Line::from_iter([self, rhs])
}
}
impl<'a> Styled for Span<'a> {
type Item = Self;
@@ -773,4 +781,27 @@ mod tests {
]
);
}
#[test]
fn add() {
assert_eq!(
Span::default() + Span::default(),
Line::from(vec![Span::default(), Span::default()])
);
assert_eq!(
Span::default() + Span::raw("test"),
Line::from(vec![Span::default(), Span::raw("test")])
);
assert_eq!(
Span::raw("test") + Span::default(),
Line::from(vec![Span::raw("test"), Span::default()])
);
assert_eq!(
Span::raw("test") + Span::raw("content"),
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
}

View File

@@ -570,6 +570,33 @@ where
}
}
impl<'a> std::ops::Add<Line<'a>> for Text<'a> {
type Output = Self;
fn add(mut self, line: Line<'a>) -> Self::Output {
self.push_line(line);
self
}
}
/// Adds two `Text` together.
///
/// This ignores the style and alignment of the second `Text`.
impl<'a> std::ops::Add<Self> for Text<'a> {
type Output = Self;
fn add(mut self, text: Self) -> Self::Output {
self.lines.extend(text.lines);
self
}
}
impl<'a> std::ops::AddAssign<Line<'a>> for Text<'a> {
fn add_assign(&mut self, line: Line<'a>) {
self.push_line(line);
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
@@ -587,9 +614,9 @@ where
/// you get the `ToText` implementation for free.
///
/// [`Display`]: std::fmt::Display
pub trait ToText<'a> {
pub trait ToText {
/// Converts the value to a [`Text`].
fn to_text(&self) -> Text<'a>;
fn to_text(&self) -> Text<'_>;
}
/// # Panics
@@ -597,8 +624,8 @@ pub trait ToText<'a> {
/// In this implementation, the `to_text` method panics if the `Display` implementation returns an
/// error. This indicates an incorrect `Display` implementation since `fmt::Write for String` never
/// returns an error itself.
impl<'a, T: fmt::Display> ToText<'a> for T {
fn to_text(&self) -> Text<'a> {
impl<T: fmt::Display> ToText for T {
fn to_text(&self) -> Text {
Text::raw(self.to_string())
}
}
@@ -829,6 +856,44 @@ mod tests {
assert_eq!(iter.next(), None);
}
#[test]
fn add_line() {
assert_eq!(
Text::raw("Red").red() + Line::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn add_text() {
assert_eq!(
Text::raw("Red").red() + Text::raw("Blue").blue(),
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue")],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn add_assign_line() {
let mut text = Text::raw("Red").red();
text += Line::raw("Blue").blue();
assert_eq!(
text,
Text {
lines: vec![Line::raw("Red"), Line::raw("Blue").blue()],
style: Style::new().red(),
alignment: None,
}
);
}
#[test]
fn extend() {
let mut text = Text::from("The first line\nThe second line");

View File

@@ -27,8 +27,8 @@ pub use title::{Position, Title};
/// both centered and non-centered titles are rendered, the centered space is calculated based on
/// the full width of the block, rather than the leftover width.
///
/// Titles are not rendered in the corners of the block unless there is no border on that edge.
/// If the block is too small and multiple titles overlap, the border may get cut off at a corner.
/// Titles are not rendered in the corners of the block unless there is no border on that edge. If
/// the block is too small and multiple titles overlap, the border may get cut off at a corner.
///
/// ```plain
/// ┌With at least a left border───
@@ -60,6 +60,10 @@ pub use title::{Position, Title};
/// # Other Methods
/// - [`Block::inner`] Compute the inner area of a block based on its border visibility rules.
///
/// [`Style`]s are applied first to the entire block, then to the borders, and finally to the
/// titles. If the block is used as a container for another widget, the inner widget can also be
/// styled. See [`Style`] for more information on how merging styles works.
///
/// # Examples
///
/// ```
@@ -252,12 +256,15 @@ impl<'a> Block<'a> {
/// Note: If the block is too small and multiple titles overlap, the border might get cut off at
/// a corner.
///
/// # Example
/// # Examples
///
/// See the [Block example] for a visual representation of how the various borders and styles
/// look when rendered.
///
/// The following example demonstrates:
/// - Default title alignment
/// - Multiple titles (notice "Center" is centered according to the full with of the block, not
/// the leftover space)
/// the leftover space)
/// - Two titles with the same alignment (notice the left titles are separated)
/// ```
/// use ratatui::{
@@ -280,6 +287,8 @@ impl<'a> Block<'a> {
/// - [`Block::title_style`]
/// - [`Block::title_alignment`]
/// - [`Block::title_position`]
///
/// [Block example]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md#block
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title<T>(mut self, title: T) -> Self
where
@@ -347,10 +356,14 @@ impl<'a> Block<'a> {
/// Applies the style to all titles.
///
/// This style will be applied to all titles of the block. If a title has a style set, it will
/// be applied after this style. This style will be applied after any [`Block::style`] or
/// [`Block::border_style`] is applied.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// If a [`Title`] already has a style, the title's style will add on top of this one.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
self.titles_style = style.into();
@@ -416,7 +429,10 @@ impl<'a> Block<'a> {
/// Defines the style of the borders.
///
/// If a [`Block::style`] is defined, `border_style` will be applied on top of it.
/// This style is applied only to the areas covered by borders, and is applied to the block
/// after any [`Block::style`] is applied.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
@@ -434,16 +450,39 @@ impl<'a> Block<'a> {
self
}
/// Defines the block style.
/// Defines the style of the entire block.
///
/// This is the most generic [`Style`] a block can receive, it will be merged with any other
/// more specific style. Elements can be styled further with [`Block::title_style`] and
/// [`Block::border_style`].
/// more specific styles. Elements can be styled further with [`Block::title_style`] and
/// [`Block::border_style`], which will be applied on top of this style. If the block is used as
/// a container for another widget (e.g. a [`Paragraph`]), then the style of the widget is
/// generally applied before this style.
///
/// See [`Style`] for more information on how merging styles works.
///
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
/// your own type that implements [`Into<Style>`]).
///
/// This will also apply to the widget inside that block, unless the inner widget is styled.
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// let block = Block::new().style(Style::new().red().on_black());
///
/// // For border and title you can additionally apply styles on top of the block level style.
/// let block = Block::new()
/// .style(Style::new().red().bold().italic())
/// .border_style(Style::new().not_italic()) // will be red and bold
/// .title_style(Style::new().not_bold()) // will be red and italic
/// .title("Title");
///
/// // To style the inner widget, you can style the widget itself.
/// let paragraph = Paragraph::new("Content")
/// .block(block)
/// .style(Style::new().white().not_bold()); // will be white, and italic
/// ```
///
/// [`Paragraph`]: crate::widgets::Paragraph
#[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();
@@ -871,6 +910,35 @@ impl Block<'_> {
height: 1,
}
}
/// Calculate the left, and right space the [`Block`] will take up.
///
/// The result takes the [`Block`]'s, [`Borders`], and [`Padding`] into account.
pub(crate) fn horizontal_space(&self) -> (u16, u16) {
let left = self
.padding
.left
.saturating_add(u16::from(self.borders.contains(Borders::LEFT)));
let right = self
.padding
.right
.saturating_add(u16::from(self.borders.contains(Borders::RIGHT)));
(left, right)
}
/// Calculate the top, and bottom space that the [`Block`] will take up.
///
/// Takes the [`Padding`], [`Title`]'s position, and the [`Borders`] that are selected into
/// account when calculating the result.
pub(crate) fn vertical_space(&self) -> (u16, u16) {
let has_top =
self.borders.contains(Borders::TOP) || self.has_title_at_position(Position::Top);
let top = self.padding.top + u16::from(has_top);
let has_bottom =
self.borders.contains(Borders::BOTTOM) || self.has_title_at_position(Position::Bottom);
let bottom = self.padding.bottom + u16::from(has_bottom);
(top, bottom)
}
}
/// An extension trait for [`Block`] that provides some convenience methods.
@@ -1023,6 +1091,126 @@ mod tests {
assert!(block.has_title_at_position(Position::Bottom));
}
#[rstest]
#[case::none(Borders::NONE, (0, 0))]
#[case::top(Borders::TOP, (1, 0))]
#[case::right(Borders::RIGHT, (0, 0))]
#[case::bottom(Borders::BOTTOM, (0, 1))]
#[case::left(Borders::LEFT, (0, 0))]
#[case::top_right(Borders::TOP | Borders::RIGHT, (1, 0))]
#[case::top_bottom(Borders::TOP | Borders::BOTTOM, (1, 1))]
#[case::top_left(Borders::TOP | Borders::LEFT, (1, 0))]
#[case::bottom_right(Borders::BOTTOM | Borders::RIGHT, (0, 1))]
#[case::bottom_left(Borders::BOTTOM | Borders::LEFT, (0, 1))]
#[case::left_right(Borders::LEFT | Borders::RIGHT, (0, 0))]
fn vertical_space_takes_into_account_borders(
#[case] borders: Borders,
#[case] vertical_space: (u16, u16),
) {
let block = Block::new().borders(borders);
assert_eq!(block.vertical_space(), vertical_space);
}
#[rstest]
#[case::top_border_top_p1(Borders::TOP, Padding::new(0, 0, 1, 0), (2, 0))]
#[case::right_border_top_p1(Borders::RIGHT, Padding::new(0, 0, 1, 0), (1, 0))]
#[case::bottom_border_top_p1(Borders::BOTTOM, Padding::new(0, 0, 1, 0), (1, 1))]
#[case::left_border_top_p1(Borders::LEFT, Padding::new(0, 0, 1, 0), (1, 0))]
#[case::top_bottom_border_all_p3(Borders::TOP | Borders::BOTTOM, Padding::new(100, 100, 4, 5), (5, 6))]
#[case::no_border(Borders::NONE, Padding::new(100, 100, 10, 13), (10, 13))]
#[case::all(Borders::ALL, Padding::new(100, 100, 1, 3), (2, 4))]
fn vertical_space_takes_into_account_padding(
#[case] borders: Borders,
#[case] padding: Padding,
#[case] vertical_space: (u16, u16),
) {
let block = Block::new().borders(borders).padding(padding);
assert_eq!(block.vertical_space(), vertical_space);
}
#[test]
fn vertical_space_takes_into_account_titles() {
let block = Block::new()
.title_position(Position::Top)
.title(Title::from("Test"));
assert_eq!(block.vertical_space(), (1, 0));
let block = Block::new()
.title_position(Position::Bottom)
.title(Title::from("Test"));
assert_eq!(block.vertical_space(), (0, 1));
}
#[rstest]
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Top, (1, 0))]
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Top, (1, 0))]
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Top, (1, 1))]
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Top, (1, 0))]
#[case::top_border_top_title(Block::new(), Borders::TOP, Position::Bottom, (1, 1))]
#[case::right_border_top_title(Block::new(), Borders::RIGHT, Position::Bottom, (0, 1))]
#[case::bottom_border_top_title(Block::new(), Borders::BOTTOM, Position::Bottom, (0, 1))]
#[case::left_border_top_title(Block::new(), Borders::LEFT, Position::Bottom, (0, 1))]
fn vertical_space_takes_into_account_borders_and_title(
#[case] block: Block,
#[case] borders: Borders,
#[case] pos: Position,
#[case] vertical_space: (u16, u16),
) {
let block = block
.borders(borders)
.title_position(pos)
.title(Title::from("Test"));
assert_eq!(block.vertical_space(), vertical_space);
}
#[test]
fn horizontal_space_takes_into_account_borders() {
let block = Block::bordered();
assert_eq!(block.horizontal_space(), (1, 1));
let block = Block::new().borders(Borders::LEFT);
assert_eq!(block.horizontal_space(), (1, 0));
let block = Block::new().borders(Borders::RIGHT);
assert_eq!(block.horizontal_space(), (0, 1));
}
#[test]
fn horizontal_space_takes_into_account_padding() {
let block = Block::new().padding(Padding::new(1, 1, 100, 100));
assert_eq!(block.horizontal_space(), (1, 1));
let block = Block::new().padding(Padding::new(3, 5, 0, 0));
assert_eq!(block.horizontal_space(), (3, 5));
let block = Block::new().padding(Padding::new(0, 1, 100, 100));
assert_eq!(block.horizontal_space(), (0, 1));
let block = Block::new().padding(Padding::new(1, 0, 100, 100));
assert_eq!(block.horizontal_space(), (1, 0));
}
#[rstest]
#[case::all_bordered_all_padded(Block::bordered(), Padding::new(1, 1, 1, 1), (2, 2))]
#[case::all_bordered_left_padded(Block::bordered(), Padding::new(1, 0, 0, 0), (2, 1))]
#[case::all_bordered_right_padded(Block::bordered(), Padding::new(0, 1, 0, 0), (1, 2))]
#[case::all_bordered_top_padded(Block::bordered(), Padding::new(0, 0, 1, 0), (1, 1))]
#[case::all_bordered_bottom_padded(Block::bordered(), Padding::new(0, 0, 0, 1), (1, 1))]
#[case::left_bordered_left_padded(Block::new().borders(Borders::LEFT), Padding::new(1, 0, 0, 0), (2, 0))]
#[case::left_bordered_right_padded(Block::new().borders(Borders::LEFT), Padding::new(0, 1, 0, 0), (1, 1))]
#[case::right_bordered_right_padded(Block::new().borders(Borders::RIGHT), Padding::new(0, 1, 0, 0), (0, 2))]
#[case::right_bordered_left_padded(Block::new().borders(Borders::RIGHT), Padding::new(1, 0, 0, 0), (1, 1))]
fn horizontal_space_takes_into_account_borders_and_padding(
#[case] block: Block,
#[case] padding: Padding,
#[case] horizontal_space: (u16, u16),
) {
let block = block.padding(padding);
assert_eq!(block.horizontal_space(), horizontal_space);
}
#[test]
const fn border_type_can_be_const() {
const _PLAIN: border::Set = BorderType::border_symbols(BorderType::Plain);

View File

@@ -144,11 +144,15 @@ pub enum GraphType {
/// Draw each point. This is the default.
#[default]
Scatter,
/// Draw a line between each following point.
///
/// The order of the lines will be the same as the order of the points in the dataset, which
/// allows this widget to draw lines both left-to-right and right-to-left
Line,
/// Draw a bar chart. This will draw a bar for each point in the dataset.
Bar,
}
/// Allow users to specify the position of a legend in a [`Chart`]
@@ -362,9 +366,10 @@ impl<'a> Dataset<'a> {
/// Sets how the dataset should be drawn
///
/// [`Chart`] can draw either a [scatter](GraphType::Scatter) or [line](GraphType::Line) charts.
/// A scatter will draw only the points in the dataset while a line will also draw a line
/// between them. See [`GraphType`] for more details
/// [`Chart`] can draw [scatter](GraphType::Scatter), [line](GraphType::Line) or
/// [bar](GraphType::Bar) charts. A scatter chart draws only the points in the dataset, a line
/// char draws a line between each point, and a bar chart draws a line from the x axis to the
/// point. See [`GraphType`] for more details
///
/// 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"]
@@ -998,16 +1003,30 @@ impl WidgetRef for Chart<'_> {
coords: dataset.data,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
if dataset.graph_type == GraphType::Line {
for data in dataset.data.windows(2) {
ctx.draw(&CanvasLine {
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
match dataset.graph_type {
GraphType::Line => {
for data in dataset.data.windows(2) {
ctx.draw(&CanvasLine {
x1: data[0].0,
y1: data[0].1,
x2: data[1].0,
y2: data[1].1,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
}
}
GraphType::Bar => {
for (x, y) in dataset.data {
ctx.draw(&CanvasLine {
x1: *x,
y1: 0.0,
x2: *x,
y2: *y,
color: dataset.style.fg.unwrap_or(Color::Reset),
});
}
}
GraphType::Scatter => {}
}
})
.render(graph_area, buf);
@@ -1194,12 +1213,14 @@ mod tests {
fn graph_type_to_string() {
assert_eq!(GraphType::Scatter.to_string(), "Scatter");
assert_eq!(GraphType::Line.to_string(), "Line");
assert_eq!(GraphType::Bar.to_string(), "Bar");
}
#[test]
fn graph_type_from_str() {
assert_eq!("Scatter".parse::<GraphType>(), Ok(GraphType::Scatter));
assert_eq!("Line".parse::<GraphType>(), Ok(GraphType::Line));
assert_eq!("Bar".parse::<GraphType>(), Ok(GraphType::Bar));
assert_eq!("".parse::<GraphType>(), Err(ParseError::VariantNotFound));
}
@@ -1460,4 +1481,39 @@ mod tests {
chart.render(buffer.area, &mut buffer);
assert_eq!(buffer, Buffer::with_lines(expected));
}
#[test]
fn bar_chart() {
let data = [
(0.0, 0.0),
(2.0, 1.0),
(4.0, 4.0),
(6.0, 8.0),
(8.0, 9.0),
(10.0, 10.0),
];
let chart = Chart::new(vec![Dataset::default()
.data(&data)
.marker(symbols::Marker::Dot)
.graph_type(GraphType::Bar)])
.x_axis(Axis::default().bounds([0.0, 10.0]))
.y_axis(Axis::default().bounds([0.0, 10.0]));
let area = Rect::new(0, 0, 11, 11);
let mut buffer = Buffer::empty(area);
chart.render(buffer.area, &mut buffer);
let expected = Buffer::with_lines([
"",
" • •",
" • • •",
" • • •",
" • • •",
" • • •",
" • • • •",
" • • • •",
" • • • •",
" • • • • •",
"• • • • • •",
]);
assert_eq!(buffer, expected);
}
}

File diff suppressed because it is too large Load Diff

324
src/widgets/list/item.rs Normal file
View 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
View 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)
);
}
}

File diff suppressed because it is too large Load Diff

289
src/widgets/list/state.rs Normal file
View 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));
}
}

View File

@@ -20,6 +20,42 @@ const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Align
/// A widget to display some text.
///
/// It is used to display a block of text. The text can be styled and aligned. It can also be
/// wrapped to the next line if it is too long to fit in the given area.
///
/// The text can be any type that can be converted into a [`Text`]. By default, the text is styled
/// with [`Style::default()`], not wrapped, and aligned to the left.
///
/// The text can be wrapped to the next line if it is too long to fit in the given area. The
/// wrapping can be configured with the [`wrap`] method. For more complex wrapping, consider using
/// the [Textwrap crate].
///
/// The text can be aligned to the left, right, or center. The alignment can be configured with the
/// [`alignment`] method or with the [`left_aligned`], [`right_aligned`], and [`centered`] methods.
///
/// The text can be scrolled to show a specific part of the text. The scroll offset can be set with
/// the [`scroll`] method.
///
/// The text can be surrounded by a [`Block`] with a title and borders. The block can be configured
/// with the [`block`] method.
///
/// The style of the text can be set with the [`style`] method. This style will be applied to the
/// entire widget, including the block if one is present. Any style set on the block or text will be
/// added to this style. See the [`Style`] type for more information on how styles are combined.
///
/// Note: If neither wrapping or a block is needed, consider rendering the [`Text`], [`Line`], or
/// [`Span`] widgets directly.
///
/// [Textwrap crate]: https://crates.io/crates/textwrap
/// [`wrap`]: Self::wrap
/// [`alignment`]: Self::alignment
/// [`left_aligned`]: Self::left_aligned
/// [`right_aligned`]: Self::right_aligned
/// [`centered`]: Self::centered
/// [`scroll`]: Self::scroll
/// [`block`]: Self::block
/// [`style`]: Self::style
///
/// # Example
///
/// ```
@@ -259,6 +295,8 @@ impl<'a> Paragraph<'a> {
/// need in order to be fully rendered. For paragraphs that do not use wrapping, this count is
/// simply the number of lines present in the paragraph.
///
/// This method will also account for the [`Block`] if one is set through [`Self::block`].
///
/// Note: The design for text wrapping is not stable and might affect this API.
///
/// # Example
@@ -279,7 +317,13 @@ impl<'a> Paragraph<'a> {
return 0;
}
if let Some(Wrap { trim }) = self.wrap {
let (top, bottom) = self
.block
.as_ref()
.map(Block::vertical_space)
.unwrap_or_default();
let count = if let Some(Wrap { trim }) = self.wrap {
let styled = self.text.iter().map(|line| {
let graphemes = line
.spans
@@ -296,11 +340,17 @@ impl<'a> Paragraph<'a> {
count
} else {
self.text.height()
}
};
count
.saturating_add(top as usize)
.saturating_add(bottom as usize)
}
/// Calculates the shortest line width needed to avoid any word being wrapped or truncated.
///
/// Accounts for the [`Block`] if a block is set through [`Self::block`].
///
/// Note: The design for text wrapping is not stable and might affect this API.
///
/// # Example
@@ -318,7 +368,16 @@ impl<'a> Paragraph<'a> {
issue = "https://github.com/ratatui-org/ratatui/issues/293"
)]
pub fn line_width(&self) -> usize {
self.text.iter().map(Line::width).max().unwrap_or_default()
let width = self.text.iter().map(Line::width).max().unwrap_or_default();
let (left, right) = self
.block
.as_ref()
.map(Block::horizontal_space)
.unwrap_or_default();
width
.saturating_add(left as usize)
.saturating_add(right as usize)
}
}
@@ -947,6 +1006,69 @@ mod test {
assert_eq!(paragraph.line_count(6), 200);
}
#[test]
fn widgets_paragraph_rendered_line_count_accounts_block() {
let block = Block::new();
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_count(20), 1);
assert_eq!(paragraph.line_count(10), 1);
let block = Block::new().borders(Borders::TOP);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 2);
assert_eq!(paragraph.line_count(10), 2);
let block = Block::new().borders(Borders::BOTTOM);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 2);
assert_eq!(paragraph.line_count(10), 2);
let block = Block::new().borders(Borders::TOP | Borders::BOTTOM);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 3);
let block = Block::bordered();
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 3);
let block = Block::bordered();
let paragraph = paragraph.block(block).wrap(Wrap { trim: true });
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 4);
let block = Block::bordered();
let paragraph = paragraph.block(block).wrap(Wrap { trim: false });
assert_eq!(paragraph.line_count(20), 3);
assert_eq!(paragraph.line_count(10), 4);
let text = "Hello World ".repeat(100);
let block = Block::new();
let paragraph = Paragraph::new(text.trim()).block(block);
assert_eq!(paragraph.line_count(11), 1);
let block = Block::bordered();
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 3);
assert_eq!(paragraph.line_count(6), 3);
let block = Block::new().borders(Borders::TOP);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 2);
assert_eq!(paragraph.line_count(6), 2);
let block = Block::new().borders(Borders::BOTTOM);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 2);
assert_eq!(paragraph.line_count(6), 2);
let block = Block::new().borders(Borders::LEFT | Borders::RIGHT);
let paragraph = paragraph.block(block);
assert_eq!(paragraph.line_count(11), 1);
assert_eq!(paragraph.line_count(6), 1);
}
#[test]
fn widgets_paragraph_line_width() {
let paragraph = Paragraph::new("Hello World");
@@ -965,6 +1087,29 @@ mod test {
assert_eq!(paragraph.line_width(), 1200);
}
#[test]
fn widgets_paragraph_line_width_accounts_for_block() {
let block = Block::bordered();
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_width(), 13);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World").block(block);
assert_eq!(paragraph.line_width(), 12);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World")
.block(block)
.wrap(Wrap { trim: true });
assert_eq!(paragraph.line_width(), 12);
let block = Block::new().borders(Borders::LEFT);
let paragraph = Paragraph::new("Hello World")
.block(block)
.wrap(Wrap { trim: false });
assert_eq!(paragraph.line_width(), 12);
}
#[test]
fn left_aligned() {
let p = Paragraph::new("Hello, world!").left_aligned();

View File

@@ -1,7 +1,6 @@
mod cell;
mod highlight_spacing;
mod row;
#[allow(clippy::module_inception)]
mod table;
mod table_state;

View File

@@ -134,7 +134,7 @@ impl<'a> Cell<'a> {
impl Cell<'_> {
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.content.clone().render(area, buf);
self.content.render_ref(area, buf);
}
}

View File

@@ -722,7 +722,7 @@ impl Table<'_> {
..row_area
};
buf.set_style(selection_area, row.style);
highlight_symbol.clone().render(selection_area, buf);
highlight_symbol.render_ref(selection_area, buf);
};
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
cell.render(
@@ -927,6 +927,8 @@ mod tests {
let table = Table::default().widths([Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
// ensure that code that uses &[] continues to work as there is a large amount of code that
// uses this pattern
#[allow(clippy::needless_borrows_for_generic_args)]
let table = Table::default().widths(&[Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
@@ -934,6 +936,9 @@ mod tests {
let table = Table::default().widths(vec![Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);
// ensure that code that uses &some_vec continues to work as there is a large amount of code
// that uses this pattern
#[allow(clippy::needless_borrows_for_generic_args)]
let table = Table::default().widths(&vec![Constraint::Length(100)]);
assert_eq!(table.widths, [Constraint::Length(100)]);