Compare commits

..

18 Commits

Author SHA1 Message Date
dependabot[bot]
94f4547dcf chore(deps): bump orhun/git-cliff-action from 2 to 3 (#972) 2024-02-26 20:19:34 +01:00
dependabot[bot]
3a6b8808ed chore(deps): update derive_builder requirement from 0.13.0 to 0.20.0 (#960) 2024-02-26 01:23:20 -08:00
Josh McKinney
1cff511934 feat(line): impl Styled for Line (#968)
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.

```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
    .chain(iter::once(" world!".green()))
    .collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
    .chain(iter::once("The second line"))
    .collect();
```
2024-02-25 05:14:19 -08:00
Josh McKinney
b5bdde079e feat(text): add FromIterator impls for Line and Text (#967)
This adds `FromIterator` impls for `Line` and `Text` that allow creating
`Line` and `Text` instances from iterators of `Span` and `Line`
instances, respectively.

```rust
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
let line: Line = iter::once("Hello".blue())
    .chain(iter::once(" world!".green()))
    .collect();
let text = Text::from_iter(vec!["The first line", "The second line"]);
let text: Text = iter::once("The first line")
    .chain(iter::once("The second line"))
    .collect();
```
2024-02-25 05:13:32 -08:00
Cameron Barnes
654949bb00 feat(list): Add Scroll Padding to Lists (#958)
Introduces scroll padding, which allows the api user to request that a certain number of ListItems be kept visible above and below the currently selected item while scrolling.

```rust
let list = List::new(items).scroll_padding(1);
```

Fixes: https://github.com/ratatui-org/ratatui/pull/955
2024-02-24 19:11:29 -08:00
EdJoPaTo
943c0431d9 fix(scrollbar): dont render on 0 length track (#964)
Fixes a panic when `track_length - 1` is used. (clamp panics on `-1.0`
being smaller than `0.0`)
2024-02-24 19:21:24 +01:00
EdJoPaTo
65e7923753 perf(scrollbar): const creation (#963)
A bunch of `const fn` allow for more performance and `Default` now uses the `const` new implementations.
2024-02-24 18:29:42 +01:00
Orhun Parmaksız
d0067c8815 docs(license): update copyright years (#962) 2024-02-21 11:24:38 +01:00
ThomasMiz
35e971f7eb fix: scrollbar thumb not visible on long lists (#959)
When displaying somewhat-long lists, the `Scrollbar` widget sometimes did not display a thumb character, and only the track will be visible.
2024-02-20 20:24:33 +01:00
Valentin271
b0314c5731 chore: remove conventional commit check for PR (#950)
This removes conventional commit check for PRs.

Since we use the PR title and description this is useless. It fails a
lot of time and we ignore it.

IMPORTANT NOTE: This does **not** mean Ratatui abandons conventional
commits. This only relates to commits in PRs.
2024-02-14 19:08:36 +01:00
Dheepak Krishnamurthy
12f67e810f feat: impl Widget for &str and String (#952)
Currently, `f.render_widget("hello world".bold(), area)` works but
`f.render_widget("hello world", area)` doesn't. This PR changes that my
implementing `Widget` for `&str` and `String`. This makes it easier to
render strings with no styles as widgets.

Example usage:

```rust
terminal.draw(|f| f.render_widget("Hello World!", f.size()))?;
```

---------

Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
2024-02-14 06:26:08 -05:00
EdJoPaTo
11b452d56f feat(layout): mark various functions as const (#951) 2024-02-13 19:05:23 -08:00
Orhun Parmaksız
efd1e47642 chore(release): prepare for 0.26.1 (#945)
🐭
2024-02-12 12:35:48 +01:00
Orhun Parmaksız
410d08b2b5 docs: add link to FOSDEM 2024 talk (#944) 2024-02-12 10:54:05 +01:00
Orhun Parmaksız
a4892ad444 chore: fix typo in docsrs example (#946) 2024-02-12 10:53:56 +01:00
Orhun Parmaksız
18870ce990 chore: fix the method name for setting the Line style (#947) 2024-02-12 10:53:46 +01:00
Orhun Parmaksız
1f208ffd03 docs: add GitHub Sponsors badge (#943) 2024-02-11 10:54:42 +01:00
Josh McKinney
e51ca6e0d2 refactor: finish tidying up table (#942) 2024-02-11 10:54:08 +01:00
24 changed files with 912 additions and 274 deletions

View File

@@ -37,7 +37,7 @@ jobs:
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v2
uses: orhun/git-cliff-action@v3
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header

View File

@@ -29,13 +29,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
if: github.event_name != 'pull_request'
uses: actions/checkout@v4
- name: Checkout
if: github.event_name == 'pull_request'
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Rust nightly
uses: dtolnay/rust-toolchain@nightly
with:
@@ -48,11 +42,6 @@ jobs:
run: cargo make lint-format
- name: Check documentation
run: cargo make lint-docs
- name: Check conventional commits
uses: crate-ci/committed@master
with:
args: "-vv"
commits: HEAD
- name: Check typos
uses: crate-ci/typos@master
- name: Lint dependencies

View File

@@ -154,7 +154,7 @@ longer can be called from a constant context.
[#708]: https://github.com/ratatui-org/ratatui/pull/708
Previously the style of a `Line` was stored in the `Span`s that make up the line. Now the `Line`
itself has a `style` field, which can be set with the `Line::style` method. Any code that creates
itself has a `style` field, which can be set with the `Line::styled` method. Any code that creates
`Line`s using the struct initializer instead of constructors will fail to compile due to the added
field. This can be easily fixed by adding `..Default::default()` to the field list or by using a
constructor method (`Line::styled()`, `Line::raw()`) or conversion method (`Line::from()`).

View File

@@ -2,6 +2,165 @@
All notable changes to this project will be documented in this file.
## [0.26.1](https://github.com/ratatui-org/ratatui/releases/tag/0.26.1) - 2024-02-12
This is a patch release that fixes bugs and adds enhancements, including new iterators, title options for blocks, and various rendering improvements. ✨
### Features
- [74a0511](https://github.com/ratatui-org/ratatui/commit/74a051147a4059990c31e08d96a8469d8220537b)
*(rect)* Add Rect::positions iterator ([#928](https://github.com/ratatui-org/ratatui/issues/928))
````text
Useful for performing some action on all the cells in a particular area.
E.g.,
```rust
fn render(area: Rect, buf: &mut Buffer) {
for position in area.positions() {
buf.get_mut(position.x, position.y).set_symbol("x");
}
}
```
````
- [9182f47](https://github.com/ratatui-org/ratatui/commit/9182f47026d1630cb749163b6f8b8987474312ae)
*(uncategorized)* Add Block::title_top and Block::title_top_bottom ([#940](https://github.com/ratatui-org/ratatui/issues/940))
````text
This adds the ability to add titles to the top and bottom of a block
without having to use the `Title` struct (which will be removed in a
future release - likely v0.28.0).
Fixes a subtle bug if the title was created from a right aligned Line
and was also right aligned. The title would be rendered one cell too far
to the right.
```rust
Block::bordered()
.title_top(Line::raw("A").left_aligned())
.title_top(Line::raw("B").centered())
.title_top(Line::raw("C").right_aligned())
.title_bottom(Line::raw("D").left_aligned())
.title_bottom(Line::raw("E").centered())
.title_bottom(Line::raw("F").right_aligned())
.render(buffer.area, &mut buffer);
// renders
"┌A─────B─────C┐",
"│ │",
"└D─────E─────F┘",
```
Addresses part of https://github.com/ratatui-org/ratatui/issues/738
````
### Bug Fixes
- [2202059](https://github.com/ratatui-org/ratatui/commit/220205925911ed4377358d2a28ffca9373f11bda)
*(block)* Fix crash on empty right aligned title ([#933](https://github.com/ratatui-org/ratatui/issues/933))
````text
- Simplified implementation of the rendering for block.
- Introduces a subtle rendering change where centered titles that are
odd in length will now be rendered one character to the left compared
to before. This aligns with other places that we render centered text
and is a more consistent behavior. See
https://github.com/ratatui-org/ratatui/pull/807#discussion_r1455645954
for another example of this.
````
Fixes: https://github.com/ratatui-org/ratatui/pull/929
- [14c67fb](https://github.com/ratatui-org/ratatui/commit/14c67fbb52101d10b2d2e26898c408ab8dd3ec2d)
*(list)* Highlight symbol when using a multi-bytes char ([#924](https://github.com/ratatui-org/ratatui/issues/924))
````text
ratatui v0.26.0 brought a regression in the List widget, in which the
highlight symbol width was incorrectly calculated - specifically when
the highlight symbol was a multi-char character, e.g. ``.
````
- [0dcdbea](https://github.com/ratatui-org/ratatui/commit/0dcdbea083aace6d531c0d505837e0911f400675)
*(paragraph)* Render Line::styled correctly inside a paragraph ([#930](https://github.com/ratatui-org/ratatui/issues/930))
````text
Renders the styled graphemes of the line instead of the contained spans.
````
- [fae5862](https://github.com/ratatui-org/ratatui/commit/fae5862c6e0947ee1488a7e4775413dbead67c8b)
*(uncategorized)* Ensure that buffer::set_line sets the line style ([#926](https://github.com/ratatui-org/ratatui/issues/926))
````text
Fixes a regression in 0.26 where buffer::set_line was no longer setting
the style. This was due to the new style field on Line instead of being
stored only in the spans.
Also adds a configuration for just running unit tests to bacon.toml.
````
- [fbb5dfa](https://github.com/ratatui-org/ratatui/commit/fbb5dfaaa903efde0e63114c393dc3063d5f56fd)
*(uncategorized)* Scrollbar rendering when no track symbols are provided ([#911](https://github.com/ratatui-org/ratatui/issues/911))
### Refactor
- [c3fb258](https://github.com/ratatui-org/ratatui/commit/c3fb25898f3e3ffe485ee69631b680679874d2cb)
*(rect)* Move iters to module and add docs ([#927](https://github.com/ratatui-org/ratatui/issues/927))
- [e51ca6e](https://github.com/ratatui-org/ratatui/commit/e51ca6e0d2705e6e0a96aeee78f1e80fcaaf34fc)
*(uncategorized)* Finish tidying up table ([#942](https://github.com/ratatui-org/ratatui/issues/942))
- [91040c0](https://github.com/ratatui-org/ratatui/commit/91040c0865043b8d5e7387509523a41345ed5af3)
*(uncategorized)* Rearrange block structure ([#939](https://github.com/ratatui-org/ratatui/issues/939))
### Documentation
- [61a8278](https://github.com/ratatui-org/ratatui/commit/61a827821dff2bd733377cfc143266edce1dbeec)
*(canvas)* Add documentation to canvas module ([#913](https://github.com/ratatui-org/ratatui/issues/913))
````text
Document the whole `canvas` module. With this, the whole `widgets`
module is documented.
````
- [d2d91f7](https://github.com/ratatui-org/ratatui/commit/d2d91f754c87458c6d07863eca20f3ea8ae319ce)
*(changelog)* Add sponsors section ([#908](https://github.com/ratatui-org/ratatui/issues/908))
- [410d08b](https://github.com/ratatui-org/ratatui/commit/410d08b2b5812d7e29302adc0e8ddf18eb7d1d26)
*(uncategorized)* Add link to FOSDEM 2024 talk ([#944](https://github.com/ratatui-org/ratatui/issues/944))
- [1f208ff](https://github.com/ratatui-org/ratatui/commit/1f208ffd0368b4d269854dc0c550686dcd2d1de0)
*(uncategorized)* Add GitHub Sponsors badge ([#943](https://github.com/ratatui-org/ratatui/issues/943))
### Performance
- [0963463](https://github.com/ratatui-org/ratatui/commit/096346350e19c5de9a4d74bba64796997e9f40da)
*(uncategorized)* Use drain instead of remove in chart examples ([#922](https://github.com/ratatui-org/ratatui/issues/922))
### Miscellaneous Tasks
- [a4892ad](https://github.com/ratatui-org/ratatui/commit/a4892ad444739d7a760bc45bbd954e728c66b2d2)
*(uncategorized)* Fix typo in docsrs example ([#946](https://github.com/ratatui-org/ratatui/issues/946))
- [18870ce](https://github.com/ratatui-org/ratatui/commit/18870ce99063a492674de061441b2cce5dc54c60)
*(uncategorized)* Fix the method name for setting the Line style ([#947](https://github.com/ratatui-org/ratatui/issues/947))
- [8fb4630](https://github.com/ratatui-org/ratatui/commit/8fb46301a00b5d065f9b890496f914d3fdc17495)
*(uncategorized)* Remove github action bot that makes comments nudging commit signing ([#937](https://github.com/ratatui-org/ratatui/issues/937))
````text
We can consider reverting this commit once this PR is merged:
https://github.com/1Password/check-signed-commits-action/pull/9
````
### Contributors
Thank you so much to everyone that contributed to this release!
Here is the list of contributors who have contributed to `ratatui` for the first time!
* @mo8it
* @m4rch3n1ng
## [0.26.0](https://github.com/ratatui-org/ratatui/releases/tag/0.26.0) - 2024-02-02
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 🐭

View File

@@ -1,6 +1,6 @@
[package]
name = "ratatui"
version = "0.26.0" # crate version
version = "0.26.1" # 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/"
@@ -51,7 +51,7 @@ cargo-husky = { version = "1.5.0", default-features = false, features = [
] }
color-eyre = "0.6.2"
criterion = { version = "0.5.1", features = ["html_reports"] }
derive_builder = "0.13.0"
derive_builder = "0.20.0"
fakeit = "1.1"
font8x8 = "0.3.1"
palette = "0.7.3"

View File

@@ -1,7 +1,7 @@
The MIT License (MIT)
Copyright (c) 2016-2022 Florian Dehau
Copyright (c) 2023 The Ratatui Developers
Copyright (c) 2023-2024 The Ratatui Developers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -26,7 +26,7 @@
<div align="center">
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
Badge]](./LICENSE)<br>
Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
[![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
[![Matrix Badge]][Matrix]<br>
@@ -61,6 +61,9 @@ This is in contrast to the retained mode style of rendering where widgets are up
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
for more info.
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
## Other documentation
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
@@ -301,6 +304,7 @@ Running this example produces the following output:
[Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
[Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
[Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
[docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
[docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
[docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
@@ -322,6 +326,7 @@ Running this example produces the following output:
[Termion]: https://crates.io/crates/termion
[Termwiz]: https://crates.io/crates/termwiz
[tui-rs]: https://crates.io/crates/tui
[GitHub Sponsors]: https://github.com/sponsors/ratatui-org
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
[CI Badge]:
@@ -339,6 +344,7 @@ Running this example produces the following output:
[Matrix Badge]:
https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
<!-- cargo-rdme end -->

View File

@@ -21,7 +21,7 @@ use crossterm::{
};
use ratatui::{prelude::*, widgets::*};
/// Example code for libr.rs
/// Example code for lib.rs
///
/// When cargo-rdme supports doc comments that import from code, this will be imported
/// rather than copied to the lib.rs file.

View File

@@ -133,10 +133,7 @@ fn ui(f: &mut Frame, app: &mut App) {
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
Line::from("This is a line "),
Line::from("This is a line ".red()),
@@ -146,10 +143,7 @@ fn ui(f: &mut Frame, app: &mut App) {
Line::from("This is a line".reset()),
Line::from(vec![
Span::raw("Masked text: "),
Span::styled(
Masked::new("password", '*'),
Style::default().fg(Color::Red),
),
Span::styled(Masked::new("password", '*'), Style::new().fg(Color::Red)),
]),
];
app.vertical_scroll_state = app.vertical_scroll_state.content_length(text.len());
@@ -157,9 +151,9 @@ fn ui(f: &mut Frame, app: &mut App) {
let create_block = |title: &'static str| Block::bordered().gray().title(title.bold());
let title = Block::default()
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold())
.title_alignment(Alignment::Center);
let title = Block::new()
.title_alignment(Alignment::Center)
.title("Use h j k l or ◄ ▲ ▼ ► to scroll ".bold());
f.render_widget(title, chunks[0]);
let paragraph = Paragraph::new(text.clone())
@@ -168,8 +162,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[1]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some(""))
.end_symbol(Some("")),
chunks[1],
@@ -184,8 +177,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.track_symbol(None)
@@ -205,8 +197,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[3]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("🬋")
.end_symbol(None),
chunks[3].inner(&Margin {
@@ -224,8 +215,7 @@ fn ui(f: &mut Frame, app: &mut App) {
.scroll((0, app.horizontal_scroll as u16));
f.render_widget(paragraph, chunks[4]);
f.render_stateful_widget(
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(Some("")),
chunks[4].inner(&Margin {

View File

@@ -1,3 +1,5 @@
#![warn(clippy::missing_const_for_fn)]
mod alignment;
mod constraint;
mod corner;

View File

@@ -38,7 +38,7 @@ pub struct Position {
impl Position {
/// Create a new position
pub fn new(x: u16, y: u16) -> Self {
pub const fn new(x: u16, y: u16) -> Self {
Position { x, y }
}
}

View File

@@ -245,7 +245,7 @@ impl Rect {
/// }
/// }
/// ```
pub fn rows(self) -> Rows {
pub const fn rows(self) -> Rows {
Rows::new(self)
}
@@ -261,7 +261,7 @@ impl Rect {
/// }
/// }
/// ```
pub fn columns(self) -> Columns {
pub const fn columns(self) -> Columns {
Columns::new(self)
}
@@ -279,7 +279,7 @@ impl Rect {
/// }
/// }
/// ```
pub fn positions(self) -> Positions {
pub const fn positions(self) -> Positions {
Positions::new(self)
}
@@ -292,7 +292,7 @@ impl Rect {
/// let rect = Rect::new(1, 2, 3, 4);
/// let position = rect.as_position();
/// ````
pub fn as_position(self) -> Position {
pub const fn as_position(self) -> Position {
Position {
x: self.x,
y: self.y,
@@ -300,7 +300,7 @@ impl Rect {
}
/// Converts the rect into a size struct.
pub fn as_size(self) -> Size {
pub const fn as_size(self) -> Size {
Size {
width: self.width,
height: self.height,

View File

@@ -11,7 +11,7 @@ pub struct Rows {
impl Rows {
/// Creates a new `Rows` iterator.
pub fn new(rect: Rect) -> Self {
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_row: rect.y,
@@ -45,7 +45,7 @@ pub struct Columns {
impl Columns {
/// Creates a new `Columns` iterator.
pub fn new(rect: Rect) -> Self {
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_column: rect.x,
@@ -81,7 +81,7 @@ pub struct Positions {
impl Positions {
/// Creates a new `Positions` iterator.
pub fn new(rect: Rect) -> Self {
pub const fn new(rect: Rect) -> Self {
Self {
rect,
current_position: Position::new(rect.x, rect.y),

View File

@@ -15,7 +15,7 @@ pub struct Size {
impl Size {
/// Create a new `Size` struct
pub fn new(width: u16, height: u16) -> Self {
pub const fn new(width: u16, height: u16) -> Self {
Size { width, height }
}
}

View File

@@ -5,7 +5,7 @@
//! <div align="center">
//!
//! [![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![License
//! Badge]](./LICENSE)<br>
//! Badge]](./LICENSE) [![Sponsors Badge]][GitHub Sponsors]<br>
//! [![Codecov Badge]][Codecov] [![Deps.rs Badge]][Deps.rs] [![Discord Badge]][Discord Server]
//! [![Matrix Badge]][Matrix]<br>
//!
@@ -40,6 +40,9 @@
//! automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
//! for more info.
//!
//! You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
//! terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
//!
//! ## Other documentation
//!
//! - [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
@@ -299,6 +302,7 @@
//! [Changelog]: https://github.com/ratatui-org/ratatui/blob/main/CHANGELOG.md
//! [Contributing]: https://github.com/ratatui-org/ratatui/blob/main/CONTRIBUTING.md
//! [Breaking Changes]: https://github.com/ratatui-org/ratatui/blob/main/BREAKING-CHANGES.md
//! [FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
//! [docsrs-hello]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
//! [docsrs-layout]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
//! [docsrs-styling]: https://github.com/ratatui-org/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
@@ -320,6 +324,7 @@
//! [Termion]: https://crates.io/crates/termion
//! [Termwiz]: https://crates.io/crates/termwiz
//! [tui-rs]: https://crates.io/crates/tui
//! [GitHub Sponsors]: https://github.com/sponsors/ratatui-org
//! [Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square
//! [License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square
//! [CI Badge]:
@@ -337,6 +342,7 @@
//! [Matrix Badge]:
//! https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix
//! [Matrix]: https://matrix.to/#/#ratatui:matrix.org
//! [Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui-org?logo=github&style=flat-square
// show the feature flags in the generated documentation
#![cfg_attr(docsrs, feature(doc_auto_cfg))]

View File

@@ -2,7 +2,7 @@
use std::borrow::Cow;
use super::StyledGrapheme;
use crate::{prelude::*, widgets::Widget};
use crate::prelude::*;
/// A line of text, consisting of one or more [`Span`]s.
///
@@ -443,6 +443,15 @@ impl<'a> From<Line<'a>> for String {
}
}
impl<'a, T> FromIterator<T> for Line<'a>
where
T: Into<Span<'a>>,
{
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
Self::from(iter.into_iter().map(Into::into).collect::<Vec<_>>())
}
}
impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
@@ -486,8 +495,22 @@ impl std::fmt::Display for Line<'_> {
}
}
impl<'a> Styled for Line<'a> {
type Item = Line<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
#[cfg(test)]
mod tests {
use std::iter;
use rstest::{fixture, rstest};
use super::*;
@@ -601,6 +624,16 @@ mod tests {
assert_eq!(Style::reset(), line.style);
}
#[test]
fn stylize() {
assert_eq!(Line::default().green().style, Color::Green.into());
assert_eq!(
Line::default().on_green().style,
Style::new().bg(Color::Green)
);
assert_eq!(Line::default().italic().style, Modifier::ITALIC.into());
}
#[test]
fn from_string() {
let s = String::from("Hello, world!");
@@ -625,6 +658,32 @@ mod tests {
assert_eq!(spans, line.spans);
}
#[test]
fn from_iter() {
let line = Line::from_iter(vec!["Hello".blue(), " world!".green()]);
assert_eq!(
line.spans,
vec![
Span::styled("Hello", Style::new().blue()),
Span::styled(" world!", Style::new().green()),
]
);
}
#[test]
fn collect() {
let line: Line = iter::once("Hello".blue())
.chain(iter::once(" world!".green()))
.collect();
assert_eq!(
line.spans,
vec![
Span::styled("Hello", Style::new().blue()),
Span::styled(" world!", Style::new().green()),
]
);
}
#[test]
fn from_span() {
let span = Span::styled("Hello, world!", Style::default().fg(Color::Yellow));

View File

@@ -4,7 +4,7 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use super::StyledGrapheme;
use crate::{prelude::*, widgets::Widget};
use crate::prelude::*;
/// Represents a part of a line that is contiguous and where all characters share the same style.
///

View File

@@ -419,6 +419,19 @@ impl<'a> From<Vec<Line<'a>>> for Text<'a> {
}
}
impl<'a, T> FromIterator<T> for Text<'a>
where
T: Into<Line<'a>>,
{
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
let lines = iter.into_iter().map(Into::into).collect();
Text {
lines,
..Default::default()
}
}
}
impl<'a, T> Extend<T> for Text<'a>
where
T: Into<Line<'a>>,
@@ -486,6 +499,8 @@ impl<'a> Styled for Text<'a> {
#[cfg(test)]
mod tests {
use std::iter;
use rstest::{fixture, rstest};
use super::*;
@@ -601,6 +616,26 @@ mod tests {
);
}
#[test]
fn from_iterator() {
let text = Text::from_iter(vec!["The first line", "The second line"]);
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn collect() {
let text: Text = iter::once("The first line")
.chain(iter::once("The second line"))
.collect();
assert_eq!(
text.lines,
vec![Line::from("The first line"), Line::from("The second line")]
);
}
#[test]
fn into_iter() {
let text = Text::from("The first line\nThe second line");

View File

@@ -70,6 +70,7 @@ use crate::{buffer::Buffer, layout::Rect};
/// used where backwards compatibility is required (all the internal widgets use this approach).
///
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
/// Widget is also implemented for `&str` and `String` types.
///
/// # Examples
///
@@ -417,6 +418,51 @@ pub trait StatefulWidgetRef {
// }
// }
/// Renders a string slice as a widget.
///
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
/// drawing the text to the screen.
impl Widget for &str {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a string slice by reference.
///
/// This trait implementation ensures that a string slice, which is an immutable view over a
/// `String`, can be drawn on demand without requiring ownership of the string itself. It utilizes
/// the default text style when rendering onto the provided [`Buffer`] at the position defined by
/// [`Rect`].
impl WidgetRef for &str {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default())
}
}
/// Renders a `String` object as a widget.
///
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
/// on a [`Buffer`] within the bounds of a given [`Rect`].
impl Widget for String {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
/// Provides the ability to render a `String` by reference.
///
/// This trait allows for a `String` to be rendered onto the [`Buffer`], similarly using the default
/// style settings. It ensures that an owned `String` can be rendered efficiently by reference,
/// without the need to give up ownership of the underlying text.
impl WidgetRef for String {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_string(area.x, area.y, self, crate::style::Style::default())
}
}
#[cfg(test)]
mod tests {
use rstest::{fixture, rstest};
@@ -561,4 +607,52 @@ mod tests {
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" "]));
}
#[rstest]
fn str_render(mut buf: Buffer) {
"hello world".render(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 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 "]),);
}
}

View File

@@ -20,7 +20,7 @@ pub struct Line {
impl Line {
/// Create a new line from `(x1, y1)` to `(x2, y2)` with the given color
pub fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
pub const fn new(x1: f64, y1: f64, x2: f64, y2: f64, color: Color) -> Self {
Self {
x1,
y1,

View File

@@ -438,6 +438,8 @@ pub struct List<'a> {
repeat_highlight_symbol: bool,
/// Decides when to allocate spacing for the selection symbol
highlight_spacing: HighlightSpacing,
/// How many items to try to keep visible before and after the selected item
scroll_padding: usize,
}
/// Defines the direction in which the list will be rendered.
@@ -685,6 +687,25 @@ impl<'a> List<'a> {
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 = vec!["Item 1"];
/// let list = List::new(items).scroll_padding(1);
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn scroll_padding(mut self, padding: usize) -> List<'a> {
self.scroll_padding = padding;
self
}
/// Defines the list direction (up or down)
///
/// Defines if the `List` is displayed *top to bottom* (default) or *bottom to top*. Use
@@ -737,6 +758,52 @@ impl<'a> List<'a> {
self.items.is_empty()
}
/// Applies scroll padding to the selected index, reducing the padding value to keep the
/// selected item on screen even with items of inconsistent sizes
///
/// This function is sensitive to how the bounds checking function handles item height
fn apply_scroll_padding_to_selected_index(
&self,
selected: Option<usize>,
max_height: usize,
first_visible_index: usize,
last_visible_index: usize,
) -> Option<usize> {
let last_valid_index = self.items.len().saturating_sub(1);
let selected = selected?.min(last_valid_index);
// The bellow loop handles situations where the list item sizes may not be consistent,
// where the offset would have excluded some items that we want to include, or could
// cause the offset value to be set to an inconsistent value each time we render.
// The padding value will be reduced in case any of these issues would occur
let mut scroll_padding = self.scroll_padding;
while scroll_padding > 0 {
let mut height_around_selected = 0;
for index in selected.saturating_sub(scroll_padding)
..=selected
.saturating_add(scroll_padding)
.min(last_valid_index)
{
height_around_selected += self.items[index].height();
}
if height_around_selected <= max_height {
break;
}
scroll_padding -= 1;
}
Some(
if (selected + scroll_padding).min(last_valid_index) >= last_visible_index {
selected + scroll_padding
} else if selected.saturating_sub(scroll_padding) < first_visible_index {
selected.saturating_sub(scroll_padding)
} else {
selected
}
.min(last_valid_index),
)
}
/// Given an offset, calculate which items can fit in a given area
fn get_items_bounds(
&self,
@@ -765,9 +832,17 @@ impl<'a> List<'a> {
last_visible_index += 1;
}
// Get the selected index, but still honor the offset if nothing is selected
// This allows for the list to stay at a position after select()ing None.
let index_to_display = selected.unwrap_or(offset).min(self.items.len() - 1);
// Get the selected index and apply scroll_padding to it, but still honor the offset if
// nothing is selected. This allows for the list to stay at a position after select()ing
// None.
let index_to_display = self
.apply_scroll_padding_to_selected_index(
selected,
max_height,
first_visible_index,
last_visible_index,
)
.unwrap_or(offset);
// Recall that last_visible_index is the index of what we
// can render up to in the given space after the offset
@@ -970,6 +1045,9 @@ where
mod tests {
use std::borrow::Cow;
use pretty_assertions::assert_eq;
use rstest::rstest;
use super::*;
use crate::{
assert_buffer_eq,
@@ -1963,4 +2041,202 @@ mod tests {
let expected = Buffer::with_lines(vec!["Large", " ", " "]);
assert_buffer_eq!(buffer, expected);
}
#[rstest]
#[case::no_padding(
4,
2, // Offset
0, // Padding
Some(2), // Selected
Buffer::with_lines(vec![">> Item 2 ", " Item 3 ", " Item 4 ", " Item 5 "])
)]
#[case::one_before(
4,
2, // Offset
1, // Padding
Some(2), // Selected
Buffer::with_lines(vec![" Item 1 ", ">> Item 2 ", " Item 3 ", " Item 4 "])
)]
#[case::one_after(
4,
1, // Offset
1, // Padding
Some(4), // Selected
Buffer::with_lines(vec![" Item 2 ", " Item 3 ", ">> Item 4 ", " Item 5 "])
)]
#[case::check_padding_overflow(
4,
1, // Offset
2, // Padding
Some(4), // Selected
Buffer::with_lines(vec![" Item 2 ", " Item 3 ", ">> Item 4 ", " Item 5 "])
)]
#[case::no_padding_offset_behavior(
5, // Render Area Height
2, // Offset
0, // Padding
Some(3), // Selected
Buffer::with_lines(
vec![" Item 2 ", ">> Item 3 ", " Item 4 ", " Item 5 ", " "]
)
)]
#[case::two_before(
5, // Render Area Height
2, // Offset
2, // Padding
Some(3), // Selected
Buffer::with_lines(
vec![" Item 1 ", " Item 2 ", ">> Item 3 ", " Item 4 ", " Item 5 "]
)
)]
#[case::keep_selected_visible(
4,
0, // Offset
4, // Padding
Some(1), // Selected
Buffer::with_lines(vec![" Item 0 ", ">> Item 1 ", " Item 2 ", " Item 3 "])
)]
fn test_padding(
#[case] render_height: u16,
#[case] offset: usize,
#[case] padding: usize,
#[case] selected: Option<usize>,
#[case] expected: Buffer,
) {
let backend = backend::TestBackend::new(10, render_height);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = offset;
state.select(selected);
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4"),
ListItem::new("Item 5"),
];
let list = List::new(items)
.scroll_padding(padding)
.highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
}
/// If there isnt enough room for the selected item and the requested padding the list can jump
/// up and down every frame if something isnt done about it. This code tests to make sure that
/// isnt currently happening
#[test]
fn test_padding_flicker() {
let backend = backend::TestBackend::new(10, 5);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = 2;
state.select(Some(4));
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4"),
ListItem::new("Item 5"),
ListItem::new("Item 6"),
ListItem::new("Item 7"),
];
let list = List::new(items).scroll_padding(3).highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(&list, size, &mut state);
})
.unwrap();
let offset_after_render = state.offset();
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(&list, size, &mut state);
})
.unwrap();
// Offset after rendering twice should remain the same as after once
assert_eq!(offset_after_render, state.offset());
}
#[test]
fn test_padding_inconsistent_item_sizes() {
let backend = backend::TestBackend::new(10, 3);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default().with_offset(0).with_selected(Some(3));
let items = vec![
ListItem::new("Item 0"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
ListItem::new("Item 4\nTest\nTest"),
ListItem::new("Item 5"),
];
let list = List::new(items).scroll_padding(1).highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
terminal.backend().assert_buffer(&Buffer::with_lines(vec![
" Item 1 ",
" Item 2 ",
">> Item 3 ",
]));
}
// Tests to make sure when it's pushing back the first visible index value that it doesnt
// include an item that's too large
#[test]
fn test_padding_offset_pushback_break() {
let backend = backend::TestBackend::new(10, 4);
let mut terminal = Terminal::new(backend).unwrap();
let mut state = ListState::default();
*state.offset_mut() = 1;
state.select(Some(2));
let items = vec![
ListItem::new("Item 0\nTest\nTest"),
ListItem::new("Item 1"),
ListItem::new("Item 2"),
ListItem::new("Item 3"),
];
let list = List::new(items).scroll_padding(2).highlight_symbol(">> ");
terminal
.draw(|f| {
let size = f.size();
f.render_stateful_widget(list, size, &mut state);
})
.unwrap();
terminal.backend().assert_buffer(&Buffer::with_lines(vec![
" Item 1 ",
">> Item 2 ",
" Item 3 ",
" ",
]));
}
}

View File

@@ -119,7 +119,7 @@ pub enum ScrollbarOrientation {
///
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default and it'll use the track size as a `viewport_content_length`.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ScrollbarState {
/// The total length of the scrollable content.
@@ -148,27 +148,39 @@ pub enum ScrollDirection {
impl<'a> Default for Scrollbar<'a> {
fn default() -> Self {
Self {
orientation: ScrollbarOrientation::default(),
thumb_symbol: DOUBLE_VERTICAL.thumb,
thumb_style: Style::default(),
track_symbol: Some(DOUBLE_VERTICAL.track),
track_style: Style::default(),
begin_symbol: Some(DOUBLE_VERTICAL.begin),
begin_style: Style::default(),
end_symbol: Some(DOUBLE_VERTICAL.end),
end_style: Style::default(),
}
Self::new(ScrollbarOrientation::default())
}
}
impl<'a> Scrollbar<'a> {
/// Creates a new scrollbar with the given position.
/// Creates a new scrollbar with the given orientation.
///
/// Most of the time you'll want [`ScrollbarOrientation::VerticalLeft`] or
/// Most of the time you'll want [`ScrollbarOrientation::VerticalRight`] or
/// [`ScrollbarOrientation::HorizontalBottom`]. See [`ScrollbarOrientation`] for more options.
pub fn new(orientation: ScrollbarOrientation) -> Self {
Self::default().orientation(orientation)
#[must_use = "creates the Scrollbar"]
pub const fn new(orientation: ScrollbarOrientation) -> Self {
let symbols = if orientation.is_vertical() {
DOUBLE_VERTICAL
} else {
DOUBLE_HORIZONTAL
};
Self::new_with_symbols(orientation, &symbols)
}
/// Creates a new scrollbar with the given orientation and symbol set.
#[must_use = "creates the Scrollbar"]
const fn new_with_symbols(orientation: ScrollbarOrientation, symbols: &Set) -> Self {
Self {
orientation,
thumb_symbol: symbols.thumb,
thumb_style: Style::new(),
track_symbol: Some(symbols.track),
track_style: Style::new(),
begin_symbol: Some(symbols.begin),
begin_style: Style::new(),
end_symbol: Some(symbols.end),
end_style: Style::new(),
}
}
/// Sets the position of the scrollbar.
@@ -180,14 +192,14 @@ impl<'a> Scrollbar<'a> {
///
/// 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 fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
pub const fn orientation(mut self, orientation: ScrollbarOrientation) -> Self {
self.orientation = orientation;
let set = if self.orientation.is_vertical() {
let symbols = if self.orientation.is_vertical() {
DOUBLE_VERTICAL
} else {
DOUBLE_HORIZONTAL
};
self.symbols(set)
self.symbols(symbols)
}
/// Sets the orientation and symbols for the scrollbar from a [`Set`].
@@ -197,9 +209,13 @@ impl<'a> Scrollbar<'a> {
///
/// 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 fn orientation_and_symbol(mut self, orientation: ScrollbarOrientation, set: Set) -> Self {
pub const fn orientation_and_symbol(
mut self,
orientation: ScrollbarOrientation,
symbols: Set,
) -> Self {
self.orientation = orientation;
self.symbols(set)
self.symbols(symbols)
}
/// Sets the symbol that represents the thumb of the scrollbar.
@@ -209,7 +225,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
pub const fn thumb_symbol(mut self, thumb_symbol: &'a str) -> Self {
self.thumb_symbol = thumb_symbol;
self
}
@@ -235,7 +251,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
pub const fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
self.track_symbol = track_symbol;
self
}
@@ -260,7 +276,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
pub const fn begin_symbol(mut self, begin_symbol: Option<&'a str>) -> Self {
self.begin_symbol = begin_symbol;
self
}
@@ -285,7 +301,7 @@ impl<'a> Scrollbar<'a> {
///
/// 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 fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
pub const fn end_symbol(mut self, end_symbol: Option<&'a str>) -> Self {
self.end_symbol = end_symbol;
self
}
@@ -315,22 +331,23 @@ impl<'a> Scrollbar<'a> {
/// └─────────── begin
/// ```
///
/// Only sets begin_symbol, end_symbol and track_symbol if they already contain a value.
/// Only sets `begin_symbol`, `end_symbol` and `track_symbol` if they already contain a value.
/// If they were set to `None` explicitly, this function will respect that choice. Use their
/// respective setters to change their value.
///
/// This is a fluent setter method which must be chained or used as it consumes self
#[allow(clippy::needless_pass_by_value)] // Breaking change
#[must_use = "method moves the value of self and returns the modified value"]
pub fn symbols(mut self, symbol: Set) -> Self {
self.thumb_symbol = symbol.thumb;
pub const fn symbols(mut self, symbols: Set) -> Self {
self.thumb_symbol = symbols.thumb;
if self.track_symbol.is_some() {
self.track_symbol = Some(symbol.track);
self.track_symbol = Some(symbols.track);
}
if self.begin_symbol.is_some() {
self.begin_symbol = Some(symbol.begin);
self.begin_symbol = Some(symbols.begin);
}
if self.end_symbol.is_some() {
self.end_symbol = Some(symbol.end);
self.end_symbol = Some(symbols.end);
}
self
}
@@ -361,15 +378,23 @@ impl<'a> Scrollbar<'a> {
}
}
impl Default for ScrollbarState {
fn default() -> Self {
Self::new(0)
}
}
impl ScrollbarState {
/// Constructs a new ScrollbarState with the specified content length.
/// Constructs a new [`ScrollbarState`] with the specified content length.
///
/// `content_length` is the total number of element, that can be scrolled. See
/// [`ScrollbarState`] for more details.
pub fn new(content_length: usize) -> Self {
#[must_use = "creates the ScrollbarState"]
pub const fn new(content_length: usize) -> Self {
Self {
content_length,
..Default::default()
position: 0,
viewport_content_length: 0,
}
}
@@ -379,7 +404,7 @@ impl ScrollbarState {
///
/// 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 fn position(mut self, position: usize) -> Self {
pub const fn position(mut self, position: usize) -> Self {
self.position = position;
self
}
@@ -391,7 +416,7 @@ impl ScrollbarState {
///
/// 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 fn content_length(mut self, content_length: usize) -> Self {
pub const fn content_length(mut self, content_length: usize) -> Self {
self.content_length = content_length;
self
}
@@ -400,7 +425,7 @@ impl ScrollbarState {
///
/// 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 fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
pub const fn viewport_content_length(mut self, viewport_content_length: usize) -> Self {
self.viewport_content_length = viewport_content_length;
self
}
@@ -415,7 +440,7 @@ impl ScrollbarState {
self.position = self
.position
.saturating_add(1)
.min(self.content_length.saturating_sub(1))
.min(self.content_length.saturating_sub(1));
}
/// Sets the scroll position to the start of the scrollable content.
@@ -425,7 +450,7 @@ impl ScrollbarState {
/// Sets the scroll position to the end of the scrollable content.
pub fn last(&mut self) {
self.position = self.content_length.saturating_sub(1)
self.position = self.content_length.saturating_sub(1);
}
/// Changes the scroll position based on the provided [`ScrollDirection`].
@@ -445,7 +470,7 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
type State = ScrollbarState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if state.content_length == 0 || area.is_empty() {
if state.content_length == 0 || self.track_length_excluding_arrow_heads(area) == 0 {
return;
}
@@ -466,7 +491,7 @@ impl Scrollbar<'_> {
fn bar_symbols(
&self,
area: Rect,
state: &mut ScrollbarState,
state: &ScrollbarState,
) -> impl Iterator<Item = Option<(&str, Style)>> {
let (track_start_len, thumb_len, track_end_len) = self.part_lengths(area, state);
@@ -492,33 +517,38 @@ impl Scrollbar<'_> {
///
/// The scrollbar has 3 parts of note:
/// - `<═══█████═══════>`: full scrollbar
/// - ` ═══ `: track start part
/// - ` █████ `: thumb part part
/// - ` ═══════ `: track end part
/// - ` ═══ `: track start
/// - ` █████ `: thumb
/// - ` ═══════ `: track end
///
/// This method returns the length of the start, thumb, and end as a tuple.
fn part_lengths(&self, area: Rect, state: &mut ScrollbarState) -> (usize, usize, usize) {
let track_len = self.track_length_excluding_arrow_heads(area) as f64;
let viewport_len = self.viewport_length(state, area) as f64;
fn part_lengths(&self, area: Rect, state: &ScrollbarState) -> (usize, usize, usize) {
let track_length = self.track_length_excluding_arrow_heads(area) as f64;
let viewport_length = self.viewport_length(state, area) as f64;
let content_length = state.content_length as f64;
// Clamp the position to show at least one line of the content, even if the content is
let position = state.position.min(state.content_length - 1) as f64;
// Ensure that the position of the thumb is within the bounds of the content taking into
// account the content and viewport length. When the last line of the content is at the top
// of the viewport, the thumb should be at the bottom of the track.
let max_position = state.content_length.saturating_sub(1) as f64;
let start_position = (state.position as f64).clamp(0.0, max_position);
let max_viewport_position = max_position + viewport_length;
let end_position = start_position + viewport_length;
// vscode style scrolling behavior (allow scrolling past end of content)
let scrollable_content_len = content_length + viewport_len - 1.0;
let thumb_start = position * track_len / scrollable_content_len;
let thumb_end = (position + viewport_len) * track_len / scrollable_content_len;
// Calculate the start and end positions of the thumb. The size will be proportional to the
// viewport length compared to the total amount of possible visible rows.
let thumb_start = start_position * track_length / max_viewport_position;
let thumb_end = end_position * track_length / max_viewport_position;
// We round just the positions (instead of floor / ceil), and then calculate the sizes from
// those positions. Rounding the sizes instead causes subtle off by 1 errors.
let track_start_len = thumb_start.round() as usize;
let thumb_end = thumb_end.round() as usize;
// Make sure that the thumb is at least 1 cell long by ensuring that the start of the thumb
// is less than the track_len. We use the positions instead of the sizes and use nearest
// integer instead of floor / ceil to avoid problems caused by rounding errors.
let thumb_start = thumb_start.round().clamp(0.0, track_length - 1.0) as usize;
let thumb_end = thumb_end.round().clamp(0.0, track_length) as usize;
let thumb_len = thumb_end.saturating_sub(track_start_len);
let track_end_len = track_len as usize - track_start_len - thumb_len;
let thumb_length = thumb_end.saturating_sub(thumb_start).max(1);
let track_end_length = (track_length as usize).saturating_sub(thumb_start + thumb_length);
(track_start_len, thumb_len, track_end_len)
(thumb_start, thumb_length, track_end_length)
}
fn scollbar_area(&self, area: Rect) -> Rect {
@@ -539,9 +569,9 @@ impl Scrollbar<'_> {
/// <═══█████═══════>
/// ```
fn track_length_excluding_arrow_heads(&self, area: Rect) -> u16 {
let start_len = self.begin_symbol.map(|s| s.width() as u16).unwrap_or(0);
let end_len = self.end_symbol.map(|s| s.width() as u16).unwrap_or(0);
let arrows_len = start_len + end_len;
let start_len = self.begin_symbol.map_or(0, |s| s.width() as u16);
let end_len = self.end_symbol.map_or(0, |s| s.width() as u16);
let arrows_len = start_len.saturating_add(end_len);
if self.orientation.is_vertical() {
area.height.saturating_sub(arrows_len)
} else {
@@ -549,26 +579,27 @@ impl Scrollbar<'_> {
}
}
fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> u16 {
const fn viewport_length(&self, state: &ScrollbarState, area: Rect) -> usize {
if state.viewport_content_length != 0 {
return state.viewport_content_length as u16;
}
if self.orientation.is_vertical() {
area.height
state.viewport_content_length
} else if self.orientation.is_vertical() {
area.height as usize
} else {
area.width
area.width as usize
}
}
}
impl ScrollbarOrientation {
/// Returns `true` if the scrollbar is vertical.
pub fn is_vertical(&self) -> bool {
#[must_use = "returns the requested kind of the scrollbar"]
pub const fn is_vertical(&self) -> bool {
matches!(self, Self::VerticalRight | Self::VerticalLeft)
}
/// Returns `true` if the scrollbar is horizontal.
pub fn is_horizontal(&self) -> bool {
#[must_use = "returns the requested kind of the scrollbar"]
pub const fn is_horizontal(&self) -> bool {
matches!(self, Self::HorizontalBottom | Self::HorizontalTop)
}
}
@@ -623,8 +654,7 @@ mod tests {
#[fixture]
fn scrollbar_no_arrows() -> Scrollbar<'static> {
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalTop)
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(None)
.end_symbol(None)
.track_symbol(Some("-"))
@@ -642,9 +672,7 @@ mod tests {
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -668,9 +696,7 @@ mod tests {
scrollbar_no_arrows: Scrollbar,
) {
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.width() as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -686,9 +712,7 @@ mod tests {
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -706,9 +730,7 @@ mod tests {
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -725,9 +747,7 @@ mod tests {
) {
let size = expected.width();
let mut buffer = Buffer::empty(Rect::new(0, 0, size as u16, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
let mut state = ScrollbarState::new(content_length).position(position);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}",);
}
@@ -752,19 +772,15 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message
"{assertion_message}",
);
}
@@ -788,11 +804,8 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
@@ -800,8 +813,7 @@ mod tests {
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message
"{assertion_message}",
);
}
@@ -828,11 +840,8 @@ mod tests {
let width = buffer.area.width as usize;
let s = "";
Text::from(format!("{s:-^width$}")).render(buffer.area, &mut buffer);
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.begin_symbol(None)
.end_symbol(None)
@@ -840,8 +849,7 @@ mod tests {
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message
"{assertion_message}",
);
}
@@ -867,11 +875,8 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalTop)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
@@ -880,8 +885,7 @@ mod tests {
assert_eq!(
buffer,
Buffer::with_lines(vec![expected]),
"{}",
assertion_message,
"{assertion_message}",
);
}
@@ -905,15 +909,12 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalBottom)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string: String = " ".repeat(size as usize);
let empty_string = " ".repeat(size as usize);
assert_eq!(
buffer,
Buffer::with_lines(vec![&empty_string, expected]),
@@ -941,15 +942,12 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 2));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalTop)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::HorizontalTop)
.begin_symbol(None)
.end_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let empty_string: String = " ".repeat(size as usize);
let empty_string = " ".repeat(size as usize);
assert_eq!(
buffer,
Buffer::with_lines(vec![expected, &empty_string]),
@@ -977,11 +975,8 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalLeft)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::VerticalLeft)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
@@ -1011,11 +1006,8 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, 5, size));
let mut state = ScrollbarState::default()
.position(position)
.content_length(content_length);
Scrollbar::default()
.orientation(ScrollbarOrientation::VerticalRight)
let mut state = ScrollbarState::new(content_length).position(position);
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.begin_symbol(Some("<"))
.end_symbol(Some(">"))
.track_symbol(Some("-"))
@@ -1046,9 +1038,38 @@ mod tests {
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::default()
let mut state = ScrollbarState::new(content_length)
.position(position)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}");
}
/// Fixes <https://github.com/ratatui-org/ratatui/pull/959> which was a bug that would not
/// render a thumb when the viewport was very small in comparison to the content length.
#[rstest]
#[case("#----", 0, 100, "position_0")]
#[case("#----", 10, 100, "position_10")]
#[case("-#---", 20, 100, "position_20")]
#[case("-#---", 30, 100, "position_30")]
#[case("--#--", 40, 100, "position_40")]
#[case("--#--", 50, 100, "position_50")]
#[case("---#-", 60, 100, "position_60")]
#[case("---#-", 70, 100, "position_70")]
#[case("----#", 80, 100, "position_80")]
#[case("----#", 90, 100, "position_90")]
#[case("----#", 100, 100, "position_one_out_of_bounds")]
fn thumb_visible_on_very_small_track(
#[case] expected: &str,
#[case] position: usize,
#[case] content_length: usize,
#[case] description: &str,
scrollbar_no_arrows: Scrollbar,
) {
let size = expected.width() as u16;
let mut buffer = Buffer::empty(Rect::new(0, 0, size, 1));
let mut state = ScrollbarState::new(content_length)
.position(position)
.content_length(content_length)
.viewport_content_length(2);
scrollbar_no_arrows.render(buffer.area, &mut buffer, &mut state);
assert_eq!(buffer, Buffer::with_lines(vec![expected]), "{description}");

View File

@@ -1,85 +1,12 @@
use strum::{Display, EnumString};
mod cell;
mod highlight_spacing;
mod row;
#[allow(clippy::module_inception)]
mod table;
mod table_state;
pub use cell::Cell;
pub use row::Row;
pub use table::Table;
pub use table_state::TableState;
/// This option allows the user to configure the "highlight symbol" column width spacing
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
pub enum HighlightSpacing {
/// Always add spacing for the selection symbol column
///
/// With this variant, the column for the selection symbol will always be allocated, and so the
/// table will never change size, regardless of if a row is selected or not
Always,
/// Only add spacing for the selection symbol column if a row is selected
///
/// With this variant, the column for the selection symbol will only be allocated if there is a
/// selection, causing the table to shift if selected / unselected
#[default]
WhenSelected,
/// Never add spacing to the selection symbol column, regardless of whether something is
/// selected or not
///
/// This means that the highlight symbol will never be drawn
Never,
}
impl HighlightSpacing {
/// Determine if a selection column should be displayed
///
/// has_selection: true if a row is selected in the table
///
/// Returns true if a selection column should be displayed
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
match self {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => has_selection,
HighlightSpacing::Never => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn highlight_spacing_to_string() {
assert_eq!(HighlightSpacing::Always.to_string(), "Always".to_string());
assert_eq!(
HighlightSpacing::WhenSelected.to_string(),
"WhenSelected".to_string()
);
assert_eq!(HighlightSpacing::Never.to_string(), "Never".to_string());
}
#[test]
fn highlight_spacing_from_str() {
assert_eq!(
"Always".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Always)
);
assert_eq!(
"WhenSelected".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::WhenSelected)
);
assert_eq!(
"Never".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Never)
);
assert_eq!(
"".parse::<HighlightSpacing>(),
Err(strum::ParseError::VariantNotFound)
);
}
}
pub use cell::*;
pub use highlight_spacing::*;
pub use row::*;
pub use table::*;
pub use table_state::*;

View File

@@ -0,0 +1,74 @@
use strum::{Display, EnumString};
/// This option allows the user to configure the "highlight symbol" column width spacing
#[derive(Debug, Display, EnumString, PartialEq, Eq, Clone, Default, Hash)]
pub enum HighlightSpacing {
/// Always add spacing for the selection symbol column
///
/// With this variant, the column for the selection symbol will always be allocated, and so the
/// table will never change size, regardless of if a row is selected or not
Always,
/// Only add spacing for the selection symbol column if a row is selected
///
/// With this variant, the column for the selection symbol will only be allocated if there is a
/// selection, causing the table to shift if selected / unselected
#[default]
WhenSelected,
/// Never add spacing to the selection symbol column, regardless of whether something is
/// selected or not
///
/// This means that the highlight symbol will never be drawn
Never,
}
impl HighlightSpacing {
/// Determine if a selection column should be displayed
///
/// has_selection: true if a row is selected in the table
///
/// Returns true if a selection column should be displayed
pub(crate) fn should_add(&self, has_selection: bool) -> bool {
match self {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => has_selection,
HighlightSpacing::Never => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn to_string() {
assert_eq!(HighlightSpacing::Always.to_string(), "Always".to_string());
assert_eq!(
HighlightSpacing::WhenSelected.to_string(),
"WhenSelected".to_string()
);
assert_eq!(HighlightSpacing::Never.to_string(), "Never".to_string());
}
#[test]
fn from_str() {
assert_eq!(
"Always".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Always)
);
assert_eq!(
"WhenSelected".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::WhenSelected)
);
assert_eq!(
"Never".parse::<HighlightSpacing>(),
Ok(HighlightSpacing::Never)
);
assert_eq!(
"".parse::<HighlightSpacing>(),
Err(strum::ParseError::VariantNotFound)
);
}
}