Compare commits
18 Commits
v0.26.1-al
...
v0.26.2-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94f4547dcf | ||
|
|
3a6b8808ed | ||
|
|
1cff511934 | ||
|
|
b5bdde079e | ||
|
|
654949bb00 | ||
|
|
943c0431d9 | ||
|
|
65e7923753 | ||
|
|
d0067c8815 | ||
|
|
35e971f7eb | ||
|
|
b0314c5731 | ||
|
|
12f67e810f | ||
|
|
11b452d56f | ||
|
|
efd1e47642 | ||
|
|
410d08b2b5 | ||
|
|
a4892ad444 | ||
|
|
18870ce990 | ||
|
|
1f208ffd03 | ||
|
|
e51ca6e0d2 |
2
.github/workflows/cd.yml
vendored
2
.github/workflows/cd.yml
vendored
@@ -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
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
@@ -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()`).
|
||||
|
||||
159
CHANGELOG.md
159
CHANGELOG.md
@@ -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 🐭
|
||||
|
||||
@@ -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"
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -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
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![warn(clippy::missing_const_for_fn)]
|
||||
|
||||
mod alignment;
|
||||
mod constraint;
|
||||
mod corner;
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 "]),);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 ",
|
||||
" ",
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
74
src/widgets/table/highlight_spacing.rs
Normal file
74
src/widgets/table/highlight_spacing.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user