Compare commits

..

22 Commits

Author SHA1 Message Date
Orhun Parmaksız
778c320008 fix(release): set the correct permissions for creating alpha releases (#400) 2023-08-12 19:48:13 +00:00
Orhun Parmaksız
268bbed17e chore(make): add task descriptions to Makefile.toml (#398) 2023-08-12 19:48:00 +00:00
hasezoey
f63ac72305 feat(widgets::table): add option to always allocate the "selection" constraint (#375)
* feat(table): add option to configure selection layout changes

Before this option was available, selecting a row in the table when no row was selected
previously made the tables layout change (the same applies to unselecting) by adding the width
of the "highlight symbol" in the front of the first column, this option allows to configure this
behavior.

* refactor(table): refactor "get_columns_widths" to return (x, width)

and "render" to make use of that

* refactor(table): refactor "get_columns_widths" to take in a selection_width instead of a boolean

also refactor "render" to make use of this change

* fix(table): rename "highlight_set_selection_space" to "highlight_spacing"

* style(table): apply doc-comment suggestions from code review

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>

---------

Co-authored-by: Dheepak Krishnamurthy <me@kdheepak.com>
2023-08-11 14:15:46 +00:00
Valentin271
3293c6b80b test(sparkline): added benchmark (#384)
Added benchmark for the `sparkline` widget testing a basic render with different amout of data
2023-08-11 02:17:32 +00:00
Valentin271
149d48919d perf(bench): Used iter_batched to clone widgets in setup function (#383)
Replaced `Bencher::iter` by `Bencher::iter_batched` to clone the widget in the setup function instead of in the benchmark timing.
2023-08-11 02:17:14 +00:00
tieway59
8c4a2e0fbf chore: implement Hash common traits (#381)
Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

Hash trait won't be impl in this PR due to rust std design.
If we need hash trait for f64 related structs in the future,
we should consider wrap f64 into a new type.

see: https://github.com/ratatui-org/ratatui/issues/307
2023-08-11 02:16:48 +00:00
Valentin271
664fb4cffd test(list): Added benchmarks (#377)
Added benchmarks for the list widget (render and render half scrolled)
2023-08-11 02:11:41 +00:00
Josh McKinney
6ad4bd4cf2 docs(examples): Add color and modifiers examples (#345)
The intent of these examples is to show the available colors and
modifiers.

- added impl Display for Color

![colors](https://vhs.charm.sh/vhs-2ZCqYbTbXAaASncUeWkt1z.gif)
![modifiers](https://vhs.charm.sh/vhs-2ovGBz5l3tfRGdZ7FCw0am.gif)
2023-08-11 01:36:12 +00:00
a-kenji
37fa6abe9d build(deps): upgrade crossterm to 0.27 (#380) 2023-08-07 13:30:11 +00:00
a-kenji
8b28672131 chore(docs): add doc comment bump to release documentation (#382) 2023-08-07 13:30:03 +00:00
a-kenji
de9f52ff2c ci(coverage): exclude examples directory from coverage (#373) 2023-08-07 12:34:04 +00:00
hasezoey
c8ddc164c7 docs(layout::Constraint): add doc-comments for all variants (#371)
fixes #354
2023-08-05 22:40:25 +00:00
Valentin271
e18393dbc6 test(block): add benchmarks (#368)
Added benchmarks to the block widget to uncover eventual performance issues
2023-08-05 14:47:06 +00:00
Orhun Parmaksız
aad164a531 feat(release): add automated nightly releases (#359)
* feat(release): add automated nightly releases

* refactor(release): rename the alpha workflow

* refactor(release): simplify the release calculation
2023-08-05 14:41:07 +00:00
Valentin271
3a37d2f6ed docs(readme): use the correct version for MSRV (#369) 2023-08-05 10:20:30 +00:00
a-kenji
8cd3205d70 chore(toolchain)!: bump msrv to 1.67 (#361)
* chore(toolchain)!: bump msrv to 1.67

BREAKING_CHANGE: The msrv is now `1.67`

* docs(readme): update the MSRV notice

---------

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
2023-08-04 23:23:22 +00:00
Josh McKinney
e82521ea79 docs(examples): regen block.gif in readme (#365) 2023-08-04 10:12:13 +00:00
Josh McKinney
9191ad60fd ci: don't fail fast (#364)
Run all the tests rather than canceling when one test fails. This allows
us to see all the failures, rather than just the first one if there are
multiple. Specifically this is useful when we have an issue in one
toolchain or backend.
2023-08-04 09:56:05 +00:00
Valentin271
49a82e062f fix(block): Fixed title_style not rendered (#349) (#363)
Fixes #349
2023-08-04 09:47:55 +00:00
tieway59
181706c564 chore: implement Eq & PartialEq common traits (#357)
Reorder the derive fields to be more consistent:

    Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash

see: https://github.com/ratatui-org/ratatui/issues/307
2023-08-04 08:23:26 +00:00
Josh McKinney
554805d6cb docs(examples): Update block example (#351)
![Block example](https://vhs.charm.sh/vhs-5X6hpReuDBKjD6hLxmDQ6F.gif)
2023-08-04 07:46:37 +00:00
a-kenji
1727fa5120 feat(scrollbar)!: add optional track symbol (#360)
The track symbol is now optional, simplifying composition with other
widgets.

BREAKING_CHANGE: The `track_symbol` needs to be set in the following way
now:

```
let scrollbar = Scrollbar::default().track_symbol(Some("-"));
```
2023-08-03 15:42:54 +00:00
57 changed files with 1501 additions and 292 deletions

View File

@@ -15,9 +15,11 @@ defaults:
shell: bash
jobs:
publish-nightly:
name: Create a nightly release
publish-alpha:
name: Create an alpha release
runs-on: ubuntu-latest
permissions:
contents: write
if: ${{ !startsWith(github.event.ref, 'refs/tags/v') }}
steps:
- name: Checkout the repository
@@ -27,34 +29,47 @@ jobs:
- name: Calculate the next release
run: |
suffix="-alpha"
suffix="alpha"
last_tag="$(git describe --abbrev=0 --tags `git rev-list --tags --max-count=1`)"
if [[ "${last_tag}" = *"${suffix}"* ]]; then
if [[ "${last_tag}" = *"-${suffix}"* ]]; then
# increment the alpha version
alpha=$(echo "${last_tag}" | grep -oE '([0-9]+)$')
next_alpha=$((alpha + 1))
next_tag=$(echo "${last_tag}" | sed "s/\.[0-9]\+$/\.${next_alpha}/")
# e.g. v0.22.1-alpha.12 -> v0.22.1-alpha.13
alpha="${last_tag##*-${suffix}.}"
next_alpha="$((alpha + 1))"
next_tag="${last_tag/%${alpha}/${next_alpha}}"
else
# start the alpha version from 0
next_tag="${last_tag}${suffix}.0"
# increment the patch and start the alpha version from 0
# e.g. v0.22.0 -> v0.22.1-alpha.0
patch="${last_tag##*.}"
next_patch="$((patch + 1))"
next_tag="${last_tag/%${patch}/${next_patch}}-${suffix}.0"
fi
# update the crate version
msg="# crate version"
sed -E -i "s/^version = .* ${msg}$/version = \"${next_tag#v}\" ${msg}/" Cargo.toml
echo "NEXT_TAG=${next_tag}" >> $GITHUB_ENV
echo "Next nightly release: ${next_tag} 🐭"
echo "Next alpha release: ${next_tag} 🐭"
- name: Publish on crates.io
uses: actions-rs/cargo@v1
with:
command: publish
args: --dry-run --allow-dirty
args: --allow-dirty --token ${{ secrets.CARGO_TOKEN }}
- name: Generate a changelog
uses: orhun/git-cliff-action@v2
with:
config: cliff.toml
args: --unreleased --tag ${{ env.NEXT_TAG }} --strip header
env:
OUTPUT: BODY.md
- name: Publish on GitHub
uses: ncipollo/release-action@v1
with:
tag: ${{ env.NEXT_TAG }}
prerelease: true
bodyFile: BODY.md
publish-stable:
name: Create a stable release
@@ -68,4 +83,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: publish
args: --dry-run
args: --token ${{ secrets.CARGO_TOKEN }}

View File

@@ -91,9 +91,10 @@ jobs:
check:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
toolchain: [ "1.67.0", "stable" ]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
@@ -111,6 +112,7 @@ jobs:
test-doc:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
runs-on: ${{ matrix.os }}
@@ -128,9 +130,10 @@ jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-latest, windows-latest, macos-latest ]
toolchain: [ "1.65.0", "stable" ]
toolchain: [ "1.67.0", "stable" ]
backend: [ crossterm, termion, termwiz ]
exclude:
# termion is not supported on windows

View File

@@ -18,7 +18,7 @@ exclude = [
]
autoexamples = true
edition = "2021"
rust-version = "1.65.0"
rust-version = "1.67.0"
[badges]
@@ -38,7 +38,7 @@ rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
bitflags = "2.3"
cassowary = "0.3"
crossterm = { version = "0.26", optional = true }
crossterm = { version = "0.27", optional = true }
indoc = "2.0"
paste = "1.0.2"
serde = { version = "1", optional = true, features = ["derive"] }
@@ -51,16 +51,31 @@ unicode-width = "0.1"
[dev-dependencies]
anyhow = "1.0.71"
argh = "0.1"
cargo-husky = { version = "1.5.0", default-features = false, features = ["user-hooks"] }
cargo-husky = { version = "1.5.0", default-features = false, features = [
"user-hooks",
] }
criterion = { version = "0.5", features = ["html_reports"] }
fakeit = "1.1"
itertools = "0.10"
rand = "0.8"
[[bench]]
name = "block"
harness = false
[[bench]]
name = "paragraph"
harness = false
[[bench]]
name = "sparkline"
harness = false
[[bench]]
name = "list"
harness = false
[[example]]
name = "barchart"
required-features = ["crossterm"]
@@ -86,6 +101,12 @@ name = "chart"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "colors"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "custom_widget"
required-features = ["crossterm"]
@@ -116,6 +137,12 @@ name = "list"
required-features = ["crossterm"]
doc-scrape-examples = true
[[example]]
name = "modifiers"
required-features = ["crossterm"]
# this example is a bit verbose, so we don't want to include it in the docs
doc-scrape-examples = false
[[example]]
name = "panic"
required-features = ["crossterm"]

View File

@@ -11,6 +11,7 @@ ALL_FEATURES = "all-widgets,macros,serde"
alias = "ci"
[tasks.ci]
description = "Run continuous integration tasks"
dependencies = [
"style-check",
"clippy",
@@ -19,18 +20,22 @@ dependencies = [
]
[tasks.style-check]
description = "Check code style"
dependencies = ["fmt", "typos"]
[tasks.fmt]
description = "Format source code"
toolchain = "nightly"
command = "cargo"
args = ["fmt", "--all", "--check"]
[tasks.typos]
description = "Run typo checks"
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
command = "typos"
[tasks.check]
description = "Check code for errors and warnings"
command = "cargo"
args = [
"check",
@@ -46,6 +51,7 @@ args = [
]
[tasks.build]
description = "Compile the project"
command = "cargo"
args = [
"build",
@@ -61,6 +67,7 @@ args = [
]
[tasks.clippy]
description = "Run Clippy for linting"
command = "cargo"
args = [
"clippy",
@@ -86,6 +93,7 @@ args = [
]
[tasks.test]
description = "Run tests"
dependencies = [
"test-doc",
]
@@ -98,6 +106,7 @@ args = [
[tasks.test-windows]
description = "Run tests on Windows"
dependencies = [
"test-doc",
]
@@ -108,6 +117,7 @@ args = [
]
[tasks.test-doc]
description = "Run documentation tests"
command = "cargo"
args = [
"test", "--doc",
@@ -122,6 +132,7 @@ args = [
[tasks.test-backend]
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
description = "Run backend-specific tests"
command = "cargo"
args = [
"test",
@@ -131,6 +142,7 @@ args = [
[tasks.coverage]
description = "Generate code coverage report"
command = "cargo"
args = [
"llvm-cov",
@@ -156,10 +168,12 @@ command = "cargo"
args = ["run", "--release", "--example", "${TUI_EXAMPLE_NAME}", "--features", "all-widgets"]
[tasks.build-examples]
description = "Compile project examples"
command = "cargo"
args = ["build", "--examples", "--release", "--features", "all-widgets"]
[tasks.run-examples]
description = "Run project examples"
dependencies = ["build-examples"]
script = '''
#!@duckscript

View File

@@ -150,7 +150,7 @@ you are interested in working on a PR or issue opened in the previous repository
## Rust version requirements
Since version 0.21.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.65.0.
Since version 0.23.0, The Minimum Supported Rust Version (MSRV) of `ratatui` is 1.67.0.
## Documentation

View File

@@ -21,6 +21,7 @@ actions](.github/workflows/cd.yml) and triggered by pushing a tag.
repositories.
1. Bump the version in [Cargo.toml](Cargo.toml).
1. Bump versions in the doc comments of [lib.rs](src/lib.rs).
1. Ensure [CHANGELOG.md](CHANGELOG.md) is updated. [git-cliff](https://github.com/orhun/git-cliff)
can be used for generating the entries.
1. Commit and push the changes.

64
benches/block.rs Normal file
View File

@@ -0,0 +1,64 @@
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Alignment,
widgets::{
block::{Position, Title},
Block, Borders, Padding, Widget,
},
};
/// Benchmark for rendering a block.
pub fn block(c: &mut Criterion) {
let mut group = c.benchmark_group("block");
for buffer_size in &[
Rect::new(0, 0, 100, 50), // vertically split screen
Rect::new(0, 0, 200, 50), // 1080p fullscreen with medium font
Rect::new(0, 0, 256, 256), // Max sized area
] {
let buffer_area = buffer_size.area();
// Render an empty block
group.bench_with_input(
BenchmarkId::new("render_empty", buffer_area),
&Block::new(),
|b, block| render(b, block, buffer_size),
);
// Render with all features
group.bench_with_input(
BenchmarkId::new("render_all_feature", buffer_area),
&Block::new()
.borders(Borders::ALL)
.title("test title")
.title(
Title::from("bottom left title")
.alignment(Alignment::Right)
.position(Position::Bottom),
)
.padding(Padding::new(5, 5, 2, 2)),
|b, block| render(b, block, buffer_size),
);
}
group.finish();
}
/// render the block into a buffer of the given `size`
fn render(bencher: &mut Bencher, block: &Block, size: &Rect) {
let mut buffer = Buffer::empty(*size);
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| block.to_owned(),
|bench_block| {
bench_block.render(buffer.area, &mut buffer);
},
BatchSize::SmallInput,
)
}
criterion_group!(benches, block);
criterion_main!(benches);

73
benches/list.rs Normal file
View File

@@ -0,0 +1,73 @@
use criterion::{criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion};
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{List, ListItem, ListState, StatefulWidget, Widget},
};
/// Benchmark for rendering a list.
/// It only benchmarks the render with a different amount of items.
pub fn list(c: &mut Criterion) {
let mut group = c.benchmark_group("list");
for line_count in [64, 2048, 16384] {
let lines: Vec<ListItem> = (0..line_count)
.map(|_| ListItem::new(fakeit::words::sentence(10)))
.collect();
// Render default list
group.bench_with_input(
BenchmarkId::new("render", line_count),
&List::new(lines.clone()),
render,
);
// Render with an offset to the middle of the list and a selected item
group.bench_with_input(
BenchmarkId::new("render_scroll_half", line_count),
&List::new(lines.clone()).highlight_symbol(">>"),
|b, list| {
render_stateful(
b,
list,
ListState::default()
.with_offset(line_count / 2)
.with_selected(Some(line_count / 2)),
)
},
);
}
group.finish();
}
/// render the list into a common size buffer
fn render(bencher: &mut Bencher, list: &List) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
Widget::render(bench_list, buffer.area, &mut buffer);
},
BatchSize::LargeInput,
)
}
/// render the list into a common size buffer with a state
fn render_stateful(bencher: &mut Bencher, list: &List, mut state: ListState) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| list.to_owned(),
|bench_list| {
StatefulWidget::render(bench_list, buffer.area, &mut buffer, &mut state);
},
BatchSize::LargeInput,
)
}
criterion_group!(benches, list);
criterion_main!(benches);

View File

@@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use criterion::{
black_box, criterion_group, criterion_main, BatchSize, Bencher, BenchmarkId, Criterion,
};
use ratatui::{
buffer::Buffer,
layout::Rect,
@@ -69,9 +71,15 @@ pub fn paragraph(c: &mut Criterion) {
/// render the paragraph into a buffer with the given width
fn render(bencher: &mut Bencher, paragraph: &Paragraph, width: u16) {
let mut buffer = Buffer::empty(Rect::new(0, 0, width, 50));
bencher.iter(|| {
paragraph.clone().render(buffer.area, &mut buffer);
})
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| paragraph.to_owned(),
|bench_paragraph| {
bench_paragraph.render(buffer.area, &mut buffer);
},
BatchSize::LargeInput,
)
}
/// Create a string with the given number of lines filled with nonsense words

45
benches/sparkline.rs Normal file
View File

@@ -0,0 +1,45 @@
use criterion::{criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use rand::Rng;
use ratatui::{
buffer::Buffer,
layout::Rect,
widgets::{Sparkline, Widget},
};
/// Benchmark for rendering a sparkline.
pub fn sparkline(c: &mut Criterion) {
let mut group = c.benchmark_group("sparkline");
let mut rng = rand::thread_rng();
for data_count in [64, 256, 2048] {
let data: Vec<u64> = (0..data_count)
.map(|_| rng.gen_range(0..data_count))
.collect();
// Render a basic sparkline
group.bench_with_input(
BenchmarkId::new("render", data_count),
&Sparkline::default().data(&data),
render,
);
}
group.finish();
}
/// render the block into a buffer of the given `size`
fn render(bencher: &mut Bencher, sparkline: &Sparkline) {
let mut buffer = Buffer::empty(Rect::new(0, 0, 200, 50));
// We use `iter_batched` to clone the value in the setup function.
// See https://github.com/ratatui-org/ratatui/pull/377.
bencher.iter_batched(
|| sparkline.clone(),
|bench_sparkline| {
bench_sparkline.render(buffer.area, &mut buffer);
},
criterion::BatchSize::LargeInput,
)
}
criterion_group!(benches, sparkline);
criterion_main!(benches);

2
codecov.yml Normal file
View File

@@ -0,0 +1,2 @@
ignore:
- "examples"

View File

@@ -46,6 +46,14 @@ cargo run --example=chart --features=crossterm
![Chart][chart.gif]
## Colors ([colors.rs](./colors.rs))
```shell
cargo run --example=colors --features=crossterm
```
![Colors][colors.gif]
## Custom Widget ([custom_widget.rs](./custom_widget.rs))
```shell
@@ -103,6 +111,14 @@ cargo run --example=list --features=crossterm
![List][list.gif]
## Modifiers ([modifiers.rs](./modifiers.rs))
```shell
cargo run --example=modifiers --features=crossterm
```
![Modifiers][modifiers.gif]
## Panic ([panic.rs](./panic.rs))
```shell
@@ -189,16 +205,18 @@ done
```
-->
[barchart.gif]: https://vhs.charm.sh/vhs-6ioxdeRBVkVpyXcjIEVaJU.gif
[block.gif]: https://vhs.charm.sh/vhs-1sEo9vVkHRwFtu95MOXrTj.gif
[block.gif]: https://vhs.charm.sh/vhs-1TyeDa5GN7kewhNjKxJ4Br.gif
[calendar.gif]: https://vhs.charm.sh/vhs-1dBcpMSSP80WkBgm4lBhNo.gif
[canvas.gif]: https://vhs.charm.sh/vhs-4zeWEPF6bLEFSHuJrvaHlN.gif
[chart.gif]: https://vhs.charm.sh/vhs-zRzsE2AwRixQhcWMTAeF1.gif
[colors.gif]: https://vhs.charm.sh/vhs-2ZCqYbTbXAaASncUeWkt1z.gif
[custom_widget.gif]: https://vhs.charm.sh/vhs-32mW1TpkrovTcm79QXmBSu.gif
[gauge.gif]: https://vhs.charm.sh/vhs-2rvSeP5r4lRkGTzNCKpm9a.gif
[hello_world.gif]: https://vhs.charm.sh/vhs-3CKUwxFuQi8oKQMS5zkPfQ.gif
[inline.gif]: https://vhs.charm.sh/vhs-miRl1mosKFoJV7LjjvF4T.gif
[layout.gif]: https://vhs.charm.sh/vhs-5R8O3LQGQ5pQVWwlPVrdbQ.gif
[list.gif]: https://vhs.charm.sh/vhs-4goo9reeUM9r0nYb54R7SP.gif
[modifiers.gif]: https://vhs.charm.sh/vhs-2ovGBz5l3tfRGdZ7FCw0am.gif
[panic.gif]: https://vhs.charm.sh/vhs-HrvKCHV4yeN69fb1EadTH.gif
[paragraph.gif]: https://vhs.charm.sh/vhs-2qIPDi79DUmtmeNDEeHVEF.gif
[popup.gif]: https://vhs.charm.sh/vhs-2QnC682AUeNYNXcjNlKTyp.gif

View File

@@ -1,123 +1,253 @@
use std::{error::Error, io, time::Duration};
use std::{
error::Error,
io::{stdout, Stdout},
ops::ControlFlow,
time::Duration,
};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{prelude::*, widgets::*};
use itertools::Itertools;
use ratatui::{
prelude::*,
widgets::{
block::{Position, Title},
Block, BorderType, Borders, Padding, Paragraph, Wrap,
},
};
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// These type aliases are used to make the code more readable by reducing repetition of the generic
// types. They are not necessary for the functionality of the code.
type Frame<'a> = ratatui::Frame<'a, CrosstermBackend<Stdout>>;
type Terminal = ratatui::Terminal<CrosstermBackend<Stdout>>;
type Result<T> = std::result::Result<T, Box<dyn Error>>;
// create app and run it
let res = run_app(&mut terminal);
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let result = run(&mut terminal);
restore_terminal(terminal)?;
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.clear()?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
if let Err(err) = result {
eprintln!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
fn setup_terminal() -> Result<Terminal> {
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let terminal = Terminal::new(backend)?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
Ok(())
}
fn run(terminal: &mut Terminal) -> Result<()> {
loop {
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
if handle_events()?.is_break() {
return Ok(());
}
}
}
fn ui<B: Backend>(f: &mut Frame<B>) {
// Wrapping block for a group
// Just draw the block and the group on the same area and build the group
let outer = f.size();
let outer_block = Block::default()
.borders(Borders::ALL)
.title(block::Title::from("Main block with round corners").alignment(Alignment::Center))
.border_type(BorderType::Rounded);
let inner = outer_block.inner(outer);
let [top, bottom] = *Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(inner)
else {
return;
};
let [top_left, top_right] = *Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(top)
else {
return;
};
let [bottom_left, bottom_right] = *Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(bottom)
else {
return;
};
fn handle_events() -> Result<ControlFlow<()>> {
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(ControlFlow::Break(()));
}
}
}
Ok(ControlFlow::Continue(()))
}
let top_left_block = Block::default()
.title("With Green Background")
.borders(Borders::all())
.on_green();
let top_right_block = Block::default()
fn ui(frame: &mut Frame) {
let (title_area, layout) = calculate_layout(frame.size());
render_title(frame, title_area);
let paragraph = placeholder_paragraph();
render_borders(&paragraph, Borders::ALL, frame, layout[0][0]);
render_borders(&paragraph, Borders::NONE, frame, layout[0][1]);
render_borders(&paragraph, Borders::LEFT, frame, layout[1][0]);
render_borders(&paragraph, Borders::RIGHT, frame, layout[1][1]);
render_borders(&paragraph, Borders::TOP, frame, layout[2][0]);
render_borders(&paragraph, Borders::BOTTOM, frame, layout[2][1]);
render_border_type(&paragraph, BorderType::Plain, frame, layout[3][0]);
render_border_type(&paragraph, BorderType::Rounded, frame, layout[3][1]);
render_border_type(&paragraph, BorderType::Double, frame, layout[4][0]);
render_border_type(&paragraph, BorderType::Thick, frame, layout[4][1]);
render_styled_block(&paragraph, frame, layout[5][0]);
render_styled_borders(&paragraph, frame, layout[5][1]);
render_styled_title(&paragraph, frame, layout[6][0]);
render_styled_title_content(&paragraph, frame, layout[6][1]);
render_multiple_titles(&paragraph, frame, layout[7][0]);
render_multiple_title_positions(&paragraph, frame, layout[7][1]);
render_padding(&paragraph, frame, layout[8][0]);
render_nested_blocks(&paragraph, frame, layout[8][1]);
}
/// Calculate the layout of the UI elements.
///
/// Returns a tuple of the title area and the main areas.
fn calculate_layout(area: Rect) -> (Rect, Vec<Vec<Rect>>) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
.split(area);
let title_area = layout[0];
let main_areas = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Max(4); 9])
.split(layout[1])
.iter()
.map(|&area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area)
.to_vec()
})
.collect_vec();
(title_area, main_areas)
}
fn render_title(frame: &mut Frame, area: Rect) {
frame.render_widget(
Paragraph::new("Block example. Press q to quit")
.dark_gray()
.alignment(Alignment::Center),
area,
);
}
fn placeholder_paragraph() -> Paragraph<'static> {
let text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
Paragraph::new(text.dark_gray()).wrap(Wrap { trim: true })
}
fn render_borders(paragraph: &Paragraph, border: Borders, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(border)
.title(format!("Borders::{border:#?}", border = border));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_border_type(
paragraph: &Paragraph,
border_type: BorderType,
frame: &mut Frame,
area: Rect,
) {
let block = Block::new()
.borders(Borders::ALL)
.border_type(border_type)
.title(format!("BorderType::{border_type:#?}"));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_borders(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.border_style(Style::new().blue().on_white().bold().italic())
.title("Styled borders");
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_block(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.style(Style::new().blue().on_white().bold().italic())
.title("Styled block");
frame.render_widget(paragraph.clone().block(block), area);
}
// Note: this currently renders incorrectly, see https://github.com/ratatui-org/ratatui/issues/349
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Styled title")
.title_style(Style::new().blue().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_styled_title_content(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let title = Line::from(vec![
"Styled ".blue().on_white().bold().italic(),
"title content".red().on_white().bold().italic(),
]);
let block = Block::new().borders(Borders::ALL).title(title);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Multiple".blue().on_white().bold().italic())
.title("Titles".red().on_white().bold().italic());
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_multiple_title_positions(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title(
block::Title::from("With styled title".white().on_red().bold())
Title::from("top left")
.position(Position::Top)
.alignment(Alignment::Left),
)
.title(
Title::from("top center")
.position(Position::Top)
.alignment(Alignment::Center),
)
.title(
Title::from("top right")
.position(Position::Top)
.alignment(Alignment::Right),
)
.borders(Borders::ALL);
let bottom_left_block = Paragraph::new("Text inside padded block").block(
Block::default()
.title("With borders")
.borders(Borders::ALL)
.padding(Padding {
left: 4,
right: 4,
top: 2,
bottom: 2,
}),
);
let bottom_right_block = Block::default()
.title("With styled borders and doubled borders")
.border_style(Style::default().fg(Color::Cyan))
.borders(Borders::LEFT | Borders::RIGHT)
.border_type(BorderType::Double)
.padding(Padding::uniform(1));
let bottom_inner_block = Block::default()
.title("Block inside padded block")
.borders(Borders::ALL);
f.render_widget(outer_block, outer);
f.render_widget(Clear, top_left);
f.render_widget(top_left_block, top_left);
f.render_widget(top_right_block, top_right);
f.render_widget(bottom_left_block, bottom_left);
let bottom_right_inner = bottom_right_block.inner(bottom_right);
f.render_widget(bottom_right_block, bottom_right);
f.render_widget(bottom_inner_block, bottom_right_inner);
.title(
Title::from("bottom left")
.position(Position::Bottom)
.alignment(Alignment::Left),
)
.title(
Title::from("bottom center")
.position(Position::Bottom)
.alignment(Alignment::Center),
)
.title(
Title::from("bottom right")
.position(Position::Bottom)
.alignment(Alignment::Right),
);
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_padding(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let block = Block::new()
.borders(Borders::ALL)
.title("Padding")
.padding(Padding::new(5, 10, 1, 2));
frame.render_widget(paragraph.clone().block(block), area);
}
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
let outer_block = Block::new().borders(Borders::ALL).title("Outer block");
let inner_block = Block::new().borders(Borders::ALL).title("Inner block");
let inner = outer_block.inner(area);
frame.render_widget(outer_block, area);
frame.render_widget(paragraph.clone().block(inner_block), inner);
}

View File

@@ -1,11 +1,12 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/block.tape`
Output "target/block.gif"
Set Theme "Builtin Dark"
Set Width 1200
Set Height 800
Set Height 1200
Hide
Type "cargo run --example=block"
Enter
Sleep 1s
Sleep 2s
Show
Sleep 5s
Sleep 2s

295
examples/colors.rs Normal file
View File

@@ -0,0 +1,295 @@
/// This example shows all the colors supported by ratatui. It will render a grid of foreground
/// and background colors with their names and indexes.
use std::{
error::Error,
io::{self, Stdout},
result,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(30),
Constraint::Length(17),
Constraint::Length(2),
])
.split(frame.size());
render_named_colors(frame, layout[0]);
render_indexed_colors(frame, layout[1]);
render_indexed_grayscale(frame, layout[2]);
}
const NAMED_COLORS: [Color; 16] = [
Color::Black,
Color::Red,
Color::Green,
Color::Yellow,
Color::Blue,
Color::Magenta,
Color::Cyan,
Color::Gray,
Color::DarkGray,
Color::LightRed,
Color::LightGreen,
Color::LightYellow,
Color::LightBlue,
Color::LightMagenta,
Color::LightCyan,
Color::White,
];
fn render_named_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(3); 10])
.split(area);
render_fg_named_colors(frame, Color::Reset, layout[0]);
render_fg_named_colors(frame, Color::Black, layout[1]);
render_fg_named_colors(frame, Color::DarkGray, layout[2]);
render_fg_named_colors(frame, Color::Gray, layout[3]);
render_fg_named_colors(frame, Color::White, layout[4]);
render_bg_named_colors(frame, Color::Reset, layout[5]);
render_bg_named_colors(frame, Color::Black, layout[6]);
render_bg_named_colors(frame, Color::DarkGray, layout[7]);
render_bg_named_colors(frame, Color::Gray, layout[8]);
render_bg_named_colors(frame, Color::White, layout[9]);
}
fn render_fg_named_colors<B: Backend>(frame: &mut Frame<B>, bg: Color, area: Rect) {
let block = title_block(format!("Foreground colors on {bg} background"));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(13); 8])
.split(*area)
.to_vec()
})
.collect_vec();
for (i, &fg) in NAMED_COLORS.iter().enumerate() {
let color_name = fg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, layout[i]);
}
}
fn render_bg_named_colors<B: Backend>(frame: &mut Frame<B>, fg: Color, area: Rect) {
let block = title_block(format!("Background colors with {fg} foreground"));
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 2])
.split(inner)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(13); 8])
.split(*area)
.to_vec()
})
.collect_vec();
for (i, &bg) in NAMED_COLORS.iter().enumerate() {
let color_name = bg.to_string();
let paragraph = Paragraph::new(color_name).fg(fg).bg(bg);
frame.render_widget(paragraph, layout[i]);
}
}
fn render_indexed_colors<B: Backend>(frame: &mut Frame<B>, area: Rect) {
let block = title_block("Indexed colors".into());
let inner = block.inner(area);
frame.render_widget(block, area);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1), // 0 - 15
Constraint::Length(1), // blank
Constraint::Min(6), // 16 - 123
Constraint::Length(1), // blank
Constraint::Min(6), // 124 - 231
Constraint::Length(1), // blank
])
.split(inner);
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
let color_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(5); 16])
.split(layout[0]);
for i in 0..16 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>2}");
let bg = if i < 1 { Color::DarkGray } else { Color::Black };
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(bg),
"██".bg(color).fg(color),
]));
frame.render_widget(paragraph, color_layout[i as usize]);
}
// 16 17 18 19 20 21 52 53 54 55 56 57 88 89 90 91 92 93
// 22 23 24 25 26 27 58 59 60 61 62 63 94 95 96 97 98 99
// 28 29 30 31 32 33 64 65 66 67 68 69 100 101 102 103 104 105
// 34 35 36 37 38 39 70 71 72 73 74 75 106 107 108 109 110 111
// 40 41 42 43 44 45 76 77 78 79 80 81 112 113 114 115 116 117
// 46 47 48 49 50 51 82 83 84 85 86 87 118 119 120 121 122 123
//
// 124 125 126 127 128 129 160 161 162 163 164 165 196 197 198 199 200 201
// 130 131 132 133 134 135 166 167 168 169 170 171 202 203 204 205 206 207
// 136 137 138 139 140 141 172 173 174 175 176 177 208 209 210 211 212 213
// 142 143 144 145 146 147 178 179 180 181 182 183 214 215 216 217 218 219
// 148 149 150 151 152 153 184 185 186 187 188 189 220 221 222 223 224 225
// 154 155 156 157 158 159 190 191 192 193 194 195 226 227 228 229 230 231
// the above looks complex but it's so the colors are grouped into blocks that display nicely
let index_layout = [layout[2], layout[4]]
.iter()
// two rows of 3 columns
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(27); 3])
.split(*area)
.to_vec()
})
// each with 6 rows
.flat_map(|area| {
Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 6])
.split(area)
.to_vec()
})
// each with 6 columns
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Min(4); 6])
.split(area)
.to_vec()
})
.collect_vec();
for i in 16..=231 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>3}");
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(Color::Reset),
".".bg(color).fg(color),
// There's a bug in VHS that seems to bleed backgrounds into the next
// character. This is a workaround to make the bug less obvious.
"███".reversed(),
]));
frame.render_widget(paragraph, index_layout[i as usize - 16]);
}
}
fn title_block(title: String) -> Block<'static> {
Block::default()
.borders(Borders::TOP)
.border_style(Style::new().dark_gray())
.title(title)
.title_alignment(Alignment::Center)
.title_style(Style::new().reset())
}
fn render_indexed_grayscale<B: Backend>(frame: &mut Frame<B>, area: Rect) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![
Constraint::Length(1), // 232 - 243
Constraint::Length(1), // 244 - 255
])
.split(area)
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Length(6); 12])
.split(*area)
.to_vec()
})
.collect_vec();
for i in 232..=255 {
let color = Color::Indexed(i);
let color_index = format!("{i:0>3}");
// make the dark colors easier to read
let bg = if i < 244 { Color::Gray } else { Color::Black };
let paragraph = Paragraph::new(Line::from(vec![
color_index.fg(color).bg(bg),
"██".bg(color).fg(color),
// There's a bug in VHS that seems to bleed backgrounds into the next
// character. This is a workaround to make the bug less obvious.
"███████".reversed(),
]));
frame.render_widget(paragraph, layout[i as usize - 232]);
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

18
examples/colors.tape Normal file
View File

@@ -0,0 +1,18 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/colors.tape`
Output "target/colors.gif"
# The OceanicMaterial theme is a good choice for this example (Obsidian is almost as good) because:
# - Black is dark and distinct from the default background
# - White is light and distinct from the default foreground
# - Normal and bright colors are distinct
# - Black and DarkGray are distinct
# - White and Gray are distinct
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1410
Hide
Type "cargo run --example=colors --features=crossterm"
Enter
Sleep 2s
Show
Sleep 1s

116
examples/modifiers.rs Normal file
View File

@@ -0,0 +1,116 @@
/// This example is useful for testing how your terminal emulator handles different modifiers.
/// It will render a grid of combinations of foreground and background colors with all
/// modifiers applied to them.
use std::{
error::Error,
io::{self, Stdout},
iter::once,
result,
time::Duration,
};
use crossterm::{
event::{self, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use itertools::Itertools;
use ratatui::{prelude::*, widgets::*};
type Result<T> = result::Result<T, Box<dyn Error>>;
fn main() -> Result<()> {
let mut terminal = setup_terminal()?;
let res = run_app(&mut terminal);
restore_terminal(terminal)?;
if let Err(err) = res {
eprintln!("{err:?}");
}
Ok(())
}
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
loop {
terminal.draw(ui)?;
if event::poll(Duration::from_millis(250))? {
if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code {
return Ok(());
}
}
}
}
}
fn ui<B: Backend>(frame: &mut Frame<B>) {
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1), Constraint::Min(0)])
.split(frame.size());
frame.render_widget(
Paragraph::new("Note: not all terminals support all modifiers")
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)),
layout[0],
);
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Length(1); 50])
.split(layout[1])
.iter()
.flat_map(|area| {
Layout::default()
.direction(Direction::Horizontal)
.constraints(vec![Constraint::Percentage(20); 5])
.split(*area)
.to_vec()
})
.collect_vec();
let colors = [
Color::Black,
Color::DarkGray,
Color::Gray,
Color::White,
Color::Red,
];
let all_modifiers = once(Modifier::empty())
.chain(Modifier::all().iter())
.collect_vec();
let mut index = 0;
for bg in colors.iter() {
for fg in colors.iter() {
for modifier in &all_modifiers {
let modifier_name = format!("{modifier:11?}");
let padding = (" ").repeat(12 - modifier_name.len());
let paragraph = Paragraph::new(Line::from(vec![
modifier_name.fg(*fg).bg(*bg).add_modifier(*modifier),
padding.fg(*fg).bg(*bg).add_modifier(*modifier),
// This is a hack to work around a bug in VHS which is used for rendering the
// examples to gifs. The bug is that the background color of a paragraph seems
// to bleed into the next character.
".".black().on_black(),
]));
frame.render_widget(paragraph, layout[index]);
index += 1;
}
}
}
}
fn setup_terminal() -> Result<Terminal<CrosstermBackend<Stdout>>> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.hide_cursor()?;
Ok(terminal)
}
fn restore_terminal(mut terminal: Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
Ok(())
}

12
examples/modifiers.tape Normal file
View File

@@ -0,0 +1,12 @@
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
# To run this script, install vhs and run `vhs ./examples/modifiers.tape`
Output "target/modifiers.gif"
Set Theme "OceanicMaterial"
Set Width 1200
Set Height 1460
Hide
Type "cargo run --example=modifiers --features=crossterm"
Enter
Sleep 2s
Show
Sleep 1s

View File

@@ -188,7 +188,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
let paragraph = Paragraph::new(text.clone())
.gray()
.block(create_block(
"Vertical scrollbar without arrows and mirrored",
"Vertical scrollbar without arrows, without track symbol and mirrored",
))
.scroll((app.vertical_scroll as u16, 0));
f.render_widget(paragraph, chunks[2]);
@@ -197,6 +197,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
.orientation(ScrollbarOrientation::VerticalLeft)
.symbols(scrollbar::VERTICAL)
.begin_symbol(None)
.track_symbol(None)
.end_symbol(None),
chunks[2].inner(&Margin {
vertical: 1,
@@ -235,7 +236,7 @@ fn ui<B: Backend>(f: &mut Frame<B>, app: &mut App) {
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.thumb_symbol("")
.track_symbol(""),
.track_symbol(Some("")),
chunks[4].inner(&Margin {
vertical: 0,
horizontal: 1,

View File

@@ -42,7 +42,7 @@ use crate::{
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct CrosstermBackend<W: Write> {
buffer: W,
}
@@ -88,7 +88,7 @@ where
for (x, y, cell) in content {
// Move the cursor if the previous location was not (x - 1, y)
if !matches!(last_pos, Some(p) if x == p.0 + 1 && y == p.1) {
map_error(queue!(self.buffer, MoveTo(x, y)))?;
queue!(self.buffer, MoveTo(x, y))?;
}
last_pos = Some((x, y));
if cell.modifier != modifier {
@@ -101,38 +101,38 @@ where
}
if cell.fg != fg {
let color = CColor::from(cell.fg);
map_error(queue!(self.buffer, SetForegroundColor(color)))?;
queue!(self.buffer, SetForegroundColor(color))?;
fg = cell.fg;
}
if cell.bg != bg {
let color = CColor::from(cell.bg);
map_error(queue!(self.buffer, SetBackgroundColor(color)))?;
queue!(self.buffer, SetBackgroundColor(color))?;
bg = cell.bg;
}
if cell.underline_color != underline_color {
let color = CColor::from(cell.underline_color);
map_error(queue!(self.buffer, SetUnderlineColor(color)))?;
queue!(self.buffer, SetUnderlineColor(color))?;
underline_color = cell.underline_color;
}
map_error(queue!(self.buffer, Print(&cell.symbol)))?;
queue!(self.buffer, Print(&cell.symbol))?;
}
map_error(queue!(
queue!(
self.buffer,
SetForegroundColor(CColor::Reset),
SetBackgroundColor(CColor::Reset),
SetUnderlineColor(CColor::Reset),
SetAttribute(CAttribute::Reset)
))
)
}
fn hide_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Hide))
execute!(self.buffer, Hide)
}
fn show_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Show))
execute!(self.buffer, Show)
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@@ -141,7 +141,7 @@ where
}
fn set_cursor(&mut self, x: u16, y: u16) -> io::Result<()> {
map_error(execute!(self.buffer, MoveTo(x, y)))
execute!(self.buffer, MoveTo(x, y))
}
fn clear(&mut self) -> io::Result<()> {
@@ -149,7 +149,7 @@ where
}
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
map_error(execute!(
execute!(
self.buffer,
Clear(match clear_type {
ClearType::All => crossterm::terminal::ClearType::All,
@@ -158,12 +158,12 @@ where
ClearType::CurrentLine => crossterm::terminal::ClearType::CurrentLine,
ClearType::UntilNewLine => crossterm::terminal::ClearType::UntilNewLine,
})
))
)
}
fn append_lines(&mut self, n: u16) -> io::Result<()> {
for _ in 0..n {
map_error(queue!(self.buffer, Print("\n")))?;
queue!(self.buffer, Print("\n"))?;
}
self.buffer.flush()
}
@@ -180,10 +180,6 @@ where
}
}
fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {
@@ -213,7 +209,7 @@ impl From<Color> for CColor {
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct ModifierDiff {
pub from: Modifier,
pub to: Modifier,
@@ -227,54 +223,54 @@ impl ModifierDiff {
//use crossterm::Attribute;
let removed = self.from - self.to;
if removed.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::NoReverse)))?;
queue!(w, SetAttribute(CAttribute::NoReverse))?;
}
if removed.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
if self.to.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
queue!(w, SetAttribute(CAttribute::Dim))?;
}
}
if removed.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::NoItalic)))?;
queue!(w, SetAttribute(CAttribute::NoItalic))?;
}
if removed.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::NoUnderline)))?;
queue!(w, SetAttribute(CAttribute::NoUnderline))?;
}
if removed.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::NormalIntensity)))?;
queue!(w, SetAttribute(CAttribute::NormalIntensity))?;
}
if removed.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::NotCrossedOut)))?;
queue!(w, SetAttribute(CAttribute::NotCrossedOut))?;
}
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::NoBlink)))?;
queue!(w, SetAttribute(CAttribute::NoBlink))?;
}
let added = self.to - self.from;
if added.contains(Modifier::REVERSED) {
map_error(queue!(w, SetAttribute(CAttribute::Reverse)))?;
queue!(w, SetAttribute(CAttribute::Reverse))?;
}
if added.contains(Modifier::BOLD) {
map_error(queue!(w, SetAttribute(CAttribute::Bold)))?;
queue!(w, SetAttribute(CAttribute::Bold))?;
}
if added.contains(Modifier::ITALIC) {
map_error(queue!(w, SetAttribute(CAttribute::Italic)))?;
queue!(w, SetAttribute(CAttribute::Italic))?;
}
if added.contains(Modifier::UNDERLINED) {
map_error(queue!(w, SetAttribute(CAttribute::Underlined)))?;
queue!(w, SetAttribute(CAttribute::Underlined))?;
}
if added.contains(Modifier::DIM) {
map_error(queue!(w, SetAttribute(CAttribute::Dim)))?;
queue!(w, SetAttribute(CAttribute::Dim))?;
}
if added.contains(Modifier::CROSSED_OUT) {
map_error(queue!(w, SetAttribute(CAttribute::CrossedOut)))?;
queue!(w, SetAttribute(CAttribute::CrossedOut))?;
}
if added.contains(Modifier::SLOW_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::SlowBlink)))?;
queue!(w, SetAttribute(CAttribute::SlowBlink))?;
}
if added.contains(Modifier::RAPID_BLINK) {
map_error(queue!(w, SetAttribute(CAttribute::RapidBlink)))?;
queue!(w, SetAttribute(CAttribute::RapidBlink))?;
}
Ok(())

View File

@@ -49,7 +49,7 @@ pub use self::test::TestBackend;
/// Enum representing the different types of clearing operations that can be performed
/// on the terminal screen.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ClearType {
All,
AfterCursor,

View File

@@ -31,7 +31,7 @@ use crate::{
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TermionBackend<W>
where
W: Write,
@@ -164,16 +164,16 @@ where
self.stdout.flush()
}
}
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct Fg(Color);
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct Bg(Color);
/// The `ModifierDiff` struct is used to calculate the difference between two `Modifier`
/// values. This is useful when updating the terminal display, as it allows for more
/// efficient updates by only sending the necessary changes.
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
struct ModifierDiff {
from: Modifier,
to: Modifier,

View File

@@ -28,7 +28,7 @@ use crate::{
/// # Ok(())
/// # }
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct TestBackend {
width: u16,
buffer: Buffer,

View File

@@ -14,7 +14,7 @@ use crate::{
};
/// A buffer cell
#[derive(Debug, Clone, Eq, PartialEq)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Cell {
pub symbol: String,
pub fg: Color,
@@ -135,7 +135,7 @@ impl Default for Cell {
/// buf.get_mut(5, 0).set_char('x');
/// assert_eq!(buf.get(5, 0).symbol, "x");
/// ```
#[derive(Default, Clone, Eq, PartialEq)]
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,

View File

@@ -11,7 +11,7 @@ use cassowary::{
WeightedRelation::{EQ, GE, LE},
};
#[derive(Debug, Default, Hash, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Corner {
#[default]
TopLeft,
@@ -27,12 +27,64 @@ pub enum Direction {
Vertical,
}
/// Constraints to apply
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Constraint {
/// Apply a percentage to a given amount
/// Converts the given percentage to a f32, and then converts it back, trimming off the decimal
/// point (effectively rounding down)
/// ```
/// # use ratatui::prelude::Constraint;
/// assert_eq!(0, Constraint::Percentage(50).apply(0));
/// assert_eq!(2, Constraint::Percentage(50).apply(4));
/// assert_eq!(5, Constraint::Percentage(50).apply(10));
/// assert_eq!(5, Constraint::Percentage(50).apply(11));
/// ```
Percentage(u16),
/// Apply a ratio
/// Converts the given numbers to a f32, and then converts it back, trimming off the decimal
/// point (effectively rounding down)
/// ```
/// # use ratatui::prelude::Constraint;
/// assert_eq!(0, Constraint::Ratio(4, 3).apply(0));
/// assert_eq!(4, Constraint::Ratio(4, 3).apply(4));
/// assert_eq!(10, Constraint::Ratio(4, 3).apply(10));
/// assert_eq!(100, Constraint::Ratio(4, 3).apply(100));
///
/// assert_eq!(0, Constraint::Ratio(3, 4).apply(0));
/// assert_eq!(3, Constraint::Ratio(3, 4).apply(4));
/// assert_eq!(7, Constraint::Ratio(3, 4).apply(10));
/// assert_eq!(75, Constraint::Ratio(3, 4).apply(100));
/// ```
Ratio(u32, u32),
/// Apply no more than the given amount (currently roughly equal to [Constraint::Max], but less
/// consistent)
/// ```
/// # use ratatui::prelude::Constraint;
/// assert_eq!(0, Constraint::Length(4).apply(0));
/// assert_eq!(4, Constraint::Length(4).apply(4));
/// assert_eq!(4, Constraint::Length(4).apply(10));
/// ```
Length(u16),
/// Apply at most the given amount
///
/// also see [std::cmp::min]
/// ```
/// # use ratatui::prelude::Constraint;
/// assert_eq!(0, Constraint::Max(4).apply(0));
/// assert_eq!(4, Constraint::Max(4).apply(4));
/// assert_eq!(4, Constraint::Max(4).apply(10));
/// ```
Max(u16),
/// Apply at least the given amount
///
/// also see [std::cmp::max]
/// ```
/// # use ratatui::prelude::Constraint;
/// assert_eq!(4, Constraint::Min(4).apply(0));
/// assert_eq!(4, Constraint::Min(4).apply(4));
/// assert_eq!(10, Constraint::Min(4).apply(10));
/// ```
Min(u16),
}
@@ -70,7 +122,7 @@ pub struct Margin {
pub horizontal: u16,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Alignment {
#[default]
Left,
@@ -78,7 +130,7 @@ pub enum Alignment {
Right,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Layout {
direction: Direction,
margin: Margin,
@@ -368,7 +420,7 @@ fn split(area: Rect, layout: &Layout) -> Rc<[Rect]> {
}
/// A container used by the solver inside split
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
struct Element {
x: Variable,
y: Variable,

View File

@@ -12,8 +12,8 @@
//! Add the following to your `Cargo.toml`:
//! ```toml
//! [dependencies]
//! crossterm = "0.26"
//! ratatui = "0.20"
//! crossterm = "0.27"
//! ratatui = "0.22"
//! ```
//!
//! The crate is using the `crossterm` backend by default that works on most platforms. But if for
@@ -22,8 +22,8 @@
//!
//! ```toml
//! [dependencies]
//! termion = "1.5"
//! ratatui = { version = "0.20", default-features = false, features = ['termion'] }
//! termion = "2.0.1"
//! ratatui = { version = "0.22", default-features = false, features = ['termion'] }
//! ```
//!
//! The same logic applies for all other available backends.

View File

@@ -27,7 +27,7 @@
//! ```
use std::{
fmt::{self, Debug},
fmt::{self, Debug, Display},
str::FromStr,
};
@@ -89,7 +89,7 @@ pub use stylize::{Styled, Stylize};
/// assert_eq!("white".parse(), Ok(Color::White));
/// assert_eq!("bright white".parse(), Ok(Color::White));
/// ```
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
/// Resets the foreground or background color
@@ -151,7 +151,7 @@ bitflags! {
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Default, Clone, Copy, PartialEq, Eq)]
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
@@ -248,7 +248,7 @@ impl fmt::Debug for Modifier {
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
@@ -423,7 +423,7 @@ impl Style {
}
/// Error type indicating a failure to parse a color string.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ParseColorError;
impl std::fmt::Display for ParseColorError {
@@ -517,6 +517,32 @@ impl FromStr for Color {
}
}
impl Display for Color {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Color::Reset => write!(f, "Reset"),
Color::Black => write!(f, "Black"),
Color::Red => write!(f, "Red"),
Color::Green => write!(f, "Green"),
Color::Yellow => write!(f, "Yellow"),
Color::Blue => write!(f, "Blue"),
Color::Magenta => write!(f, "Magenta"),
Color::Cyan => write!(f, "Cyan"),
Color::Gray => write!(f, "Gray"),
Color::DarkGray => write!(f, "DarkGray"),
Color::LightRed => write!(f, "LightRed"),
Color::LightGreen => write!(f, "LightGreen"),
Color::LightYellow => write!(f, "LightYellow"),
Color::LightBlue => write!(f, "LightBlue"),
Color::LightMagenta => write!(f, "LightMagenta"),
Color::LightCyan => write!(f, "LightCyan"),
Color::White => write!(f, "White"),
Color::Rgb(r, g, b) => write!(f, "#{:02X}{:02X}{:02X}", r, g, b),
Color::Indexed(i) => write!(f, "{}", i),
}
}
}
#[cfg(test)]
mod tests {
use std::error::Error;
@@ -693,6 +719,29 @@ mod tests {
}
}
#[test]
fn display() {
assert_eq!(format!("{}", Color::Black), "Black");
assert_eq!(format!("{}", Color::Red), "Red");
assert_eq!(format!("{}", Color::Green), "Green");
assert_eq!(format!("{}", Color::Yellow), "Yellow");
assert_eq!(format!("{}", Color::Blue), "Blue");
assert_eq!(format!("{}", Color::Magenta), "Magenta");
assert_eq!(format!("{}", Color::Cyan), "Cyan");
assert_eq!(format!("{}", Color::Gray), "Gray");
assert_eq!(format!("{}", Color::DarkGray), "DarkGray");
assert_eq!(format!("{}", Color::LightRed), "LightRed");
assert_eq!(format!("{}", Color::LightGreen), "LightGreen");
assert_eq!(format!("{}", Color::LightYellow), "LightYellow");
assert_eq!(format!("{}", Color::LightBlue), "LightBlue");
assert_eq!(format!("{}", Color::LightMagenta), "LightMagenta");
assert_eq!(format!("{}", Color::LightCyan), "LightCyan");
assert_eq!(format!("{}", Color::White), "White");
assert_eq!(format!("{}", Color::Indexed(10)), "10");
assert_eq!(format!("{}", Color::Rgb(255, 0, 0)), "#FF0000");
assert_eq!(format!("{}", Color::Reset), "Reset");
}
#[test]
fn style_can_be_const() {
const RED: Color = Color::Red;

View File

@@ -8,7 +8,7 @@ pub mod block {
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
@@ -62,7 +62,7 @@ pub mod bar {
pub const ONE_QUARTER: &str = "";
pub const ONE_EIGHTH: &str = "";
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub full: &'static str,
pub seven_eighths: &'static str,
@@ -155,7 +155,7 @@ pub mod line {
pub const DOUBLE_CROSS: &str = "";
pub const THICK_CROSS: &str = "";
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub vertical: &'static str,
pub horizontal: &'static str,
@@ -240,7 +240,7 @@ pub mod braille {
}
/// Marker to use when plotting data points
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum Marker {
/// One point per cell in shape of dot
#[default]
@@ -265,7 +265,7 @@ pub mod scrollbar {
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Set {
pub track: &'static str,
pub thumb: &'static str,

View File

@@ -7,7 +7,7 @@ use crate::{
widgets::{StatefulWidget, Widget},
};
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum Viewport {
#[default]
Fullscreen,
@@ -16,14 +16,14 @@ pub enum Viewport {
}
/// Options to pass to [`Terminal::with_options`]
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TerminalOptions {
/// Viewport used to draw to the terminal
pub viewport: Viewport,
}
/// Interface to the terminal backed by Termion
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Terminal<B>
where
B: Backend,
@@ -47,7 +47,7 @@ where
}
/// Represents a consistent terminal interface for rendering.
#[derive(Debug)]
#[derive(Debug, Hash)]
pub struct Frame<'a, B: 'a>
where
B: Backend,
@@ -139,7 +139,7 @@ where
/// `CompletedFrame` represents the state of the terminal after all changes performed in the last
/// [`Terminal::draw`] call have been applied. Therefore, it is only valid until the next call to
/// [`Terminal::draw`].
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct CompletedFrame<'a> {
pub buffer: &'a Buffer,
pub area: Rect,

View File

@@ -5,7 +5,7 @@ use crate::style::{Style, Styled};
/// it actually is not a member of the text type hierarchy (`Text` -> `Line` -> `Span`).
/// It is a separate type used mostly for rendering purposes. A `Span` consists of components that
/// can be split into `StyledGrapheme`s, but it does not contain a collection of `StyledGrapheme`s.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct StyledGrapheme<'a> {
pub symbol: &'a str,
pub style: Style,

View File

@@ -4,7 +4,7 @@ use std::borrow::Cow;
use super::{Span, Spans, Style, StyledGrapheme};
use crate::layout::Alignment;
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Line<'a> {
pub spans: Vec<Span<'a>>,
pub alignment: Option<Alignment>,

View File

@@ -7,7 +7,7 @@ use super::StyledGrapheme;
use crate::style::{Style, Styled};
/// A string where all graphemes have the same style.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Span<'a> {
pub content: Cow<'a, str>,
pub style: Style,

View File

@@ -9,7 +9,7 @@ use crate::{layout::Alignment, text::Line};
/// future. All methods that accept Spans have been replaced with methods that
/// accept Into<Line<'a>> (which is implemented on `Spans`) to allow users of
/// this crate to gradually transition to Line.
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
#[deprecated(note = "Use `ratatui::text::Line` instead")]
pub struct Spans<'a>(pub Vec<Span<'a>>);

View File

@@ -28,7 +28,7 @@ use crate::style::Style;
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Text<'a> {
pub lines: Vec<Line<'a>>,
}

View File

@@ -1,6 +1,6 @@
use crate::{layout::Alignment, text::Line};
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Title<'a> {
pub content: Line<'a>,
/// Defaults to Left if unset

View File

@@ -15,7 +15,7 @@ use crate::{buffer::Buffer, style::Style, text::Line};
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
/// .text_value("10°C".to_string());
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Bar<'a> {
/// Value to display on the bar (computed when the data is passed to the widget)
pub(super) value: u64,

View File

@@ -10,7 +10,7 @@ use crate::text::Line;
/// .label("Group 1".into())
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct BarGroup<'a> {
/// label of the group. It will be printed centered under this group of bars
pub(super) label: Option<Line<'a>>,

View File

@@ -28,7 +28,7 @@ use super::{Block, Widget};
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]))
/// .max(4);
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct BarChart<'a> {
/// Block to wrap the widget in
block: Option<Block<'a>>,

View File

@@ -10,7 +10,7 @@ use crate::{
widgets::{Borders, Widget},
};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum BorderType {
#[default]
Plain,
@@ -113,7 +113,7 @@ impl Padding {
/// .border_type(BorderType::Rounded)
/// .style(Style::default().bg(Color::Black));
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Block<'a> {
/// List of titles
titles: Vec<Title<'a>>,
@@ -415,10 +415,16 @@ impl<'a> Block<'a> {
let title_x = current_offset;
current_offset += title.content.width() as u16 + 1;
// Clone the title's content, applying block title style then the title style
let mut content = title.content.clone();
for span in content.spans.iter_mut() {
span.style = self.titles_style.patch(span.style);
}
buf.set_line(
title_x + area.left(),
self.get_title_y(position, area),
&title.content,
&content,
title_area_width,
);
});
@@ -441,10 +447,16 @@ impl<'a> Block<'a> {
let title_x = current_offset;
current_offset += title.content.width() as u16 + 1;
// Clone the title's content, applying block title style then the title style
let mut content = title.content.clone();
for span in content.spans.iter_mut() {
span.style = self.titles_style.patch(span.style);
}
buf.set_line(
title_x + area.left(),
self.get_title_y(position, area),
&title.content,
&content,
title_area_width,
);
});
@@ -462,10 +474,16 @@ impl<'a> Block<'a> {
current_offset += title.content.width() as u16 + 1;
let title_x = current_offset - 1; // First element isn't spaced
// Clone the title's content, applying block title style then the title style
let mut content = title.content.clone();
for span in content.spans.iter_mut() {
span.style = self.titles_style.patch(span.style);
}
buf.set_line(
area.width.saturating_sub(title_x) + area.left(),
self.get_title_y(position, area),
&title.content,
&content,
title_area_width,
);
});
@@ -917,4 +935,54 @@ mod tests {
assert_buffer_eq!(buffer, Buffer::with_lines(vec![expected]));
}
}
#[test]
fn render_title_content_style() {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Block::default()
.title("test".yellow())
.title_alignment(alignment)
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec!["test"]);
expected_buffer.set_style(Rect::new(0, 0, 4, 1), Style::new().yellow());
assert_buffer_eq!(buffer, expected_buffer);
}
}
#[test]
fn render_block_title_style() {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Block::default()
.title("test")
.title_style(Style::new().yellow())
.title_alignment(alignment)
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec!["test"]);
expected_buffer.set_style(Rect::new(0, 0, 4, 1), Style::new().yellow());
assert_buffer_eq!(buffer, expected_buffer);
}
}
#[test]
fn title_style_overrides_block_title_style() {
for alignment in [Alignment::Left, Alignment::Center, Alignment::Right] {
let mut buffer = Buffer::empty(Rect::new(0, 0, 4, 1));
Block::default()
.title("test".yellow())
.title_style(Style::new().green().on_red())
.title_alignment(alignment)
.render(buffer.area, &mut buffer);
let mut expected_buffer = Buffer::with_lines(vec!["test"]);
expected_buffer.set_style(Rect::new(0, 0, 4, 1), Style::new().yellow().on_red());
assert_buffer_eq!(buffer, expected_buffer);
}
}
}

View File

@@ -21,7 +21,7 @@ use crate::{
};
/// Display a month calendar for the month containing `display_date`
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Monthly<'a, S: DateStyler> {
display_date: Date,
events: S,

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a circle with a given center and radius and with the given color
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Circle {
pub x: f64,
pub y: f64,

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a line from (x1, y1) to (x2, y2) with the given color
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Line {
pub x1: f64,
pub y1: f64,

View File

@@ -6,7 +6,7 @@ use crate::{
},
};
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum MapResolution {
#[default]
Low,
@@ -23,7 +23,7 @@ impl MapResolution {
}
/// Shape to draw a world map with the given resolution and color
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Map {
pub resolution: MapResolution,
pub color: Color,

View File

@@ -29,14 +29,14 @@ pub trait Shape {
}
/// Label to draw some text on the canvas
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Label<'a> {
x: f64,
y: f64,
line: TextLine<'a>,
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct Layer {
string: String,
colors: Vec<Color>,
@@ -51,7 +51,7 @@ trait Grid: Debug {
fn reset(&mut self);
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct BrailleGrid {
width: u16,
height: u16,
@@ -114,7 +114,7 @@ impl Grid for BrailleGrid {
}
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct CharGrid {
width: u16,
height: u16,
@@ -355,7 +355,7 @@ impl<'a> Context<'a> {
/// });
/// });
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Canvas<'a, F>
where
F: Fn(&mut Context),

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// A shape to draw a group of points with the given color
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Points<'a> {
pub coords: &'a [(f64, f64)],
pub color: Color,

View File

@@ -4,7 +4,7 @@ use crate::{
};
/// Shape to draw a rectangle from a `Rect` with the given color
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Rectangle {
pub x: f64,
pub y: f64,

View File

@@ -15,7 +15,7 @@ use crate::{
};
/// An X or Y axis for the chart widget
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Axis<'a> {
/// Title displayed next to axis end
title: Option<TextLine<'a>>,
@@ -76,7 +76,7 @@ impl<'a> Axis<'a> {
}
/// Used to determine which style of graphing to use
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum GraphType {
/// Draw each point
#[default]
@@ -86,7 +86,7 @@ pub enum GraphType {
}
/// A group of data points
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Dataset<'a> {
/// Name of the dataset (used in the legend if shown)
name: Cow<'a, str>,
@@ -132,7 +132,7 @@ impl<'a> Dataset<'a> {
/// A container that holds all the infos about where to display each elements of the chart (axis,
/// labels, legend, ...).
#[derive(Debug, Default, Clone, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
struct ChartLayout {
/// Location of the title of the x axis
title_x: Option<(u16, u16)>,
@@ -188,7 +188,7 @@ struct ChartLayout {
/// .bounds([0.0, 10.0])
/// .labels(["0.0", "5.0", "10.0"].iter().cloned().map(Span::from).collect()));
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct Chart<'a> {
/// A block to display around the widget eventually
block: Option<Block<'a>>,

View File

@@ -23,7 +23,7 @@ use crate::{buffer::Buffer, layout::Rect, widgets::Widget};
///
/// For a more complete example how to utilize `Clear` to realize popups see
/// the example `examples/popup.rs`
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Clear;
impl Widget for Clear {

View File

@@ -19,7 +19,7 @@ use crate::{
/// .gauge_style(Style::default().fg(Color::White).bg(Color::Black).add_modifier(Modifier::ITALIC))
/// .percent(20);
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Gauge<'a> {
block: Option<Block<'a>>,
ratio: f64,
@@ -179,7 +179,7 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
/// .line_set(symbols::line::THICK)
/// .ratio(0.4);
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, PartialEq)]
pub struct LineGauge<'a> {
block: Option<Block<'a>>,
ratio: f64,

View File

@@ -8,7 +8,7 @@ use crate::{
widgets::{Block, StatefulWidget, Widget},
};
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct ListState {
offset: usize,
selected: Option<usize>,
@@ -45,7 +45,7 @@ impl ListState {
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ListItem<'a> {
content: Text<'a>,
style: Style,
@@ -90,7 +90,7 @@ impl<'a> ListItem<'a> {
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct List<'a> {
block: Option<Block<'a>>,
items: Vec<ListItem<'a>>,

View File

@@ -46,14 +46,14 @@ pub use self::{
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline},
table::{Cell, Row, Table, TableState},
table::{Cell, HighlightSpacing, Row, Table, TableState},
tabs::Tabs,
};
use crate::{buffer::Buffer, layout::Rect};
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
#[derive(Default, Clone, Copy, PartialEq, Eq)]
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Borders: u8 {
/// Show no border (default)
const NONE = 0b0000;

View File

@@ -42,7 +42,7 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// .alignment(Alignment::Center)
/// .wrap(Wrap { trim: true });
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Paragraph<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -85,7 +85,7 @@ pub struct Paragraph<'a> {
/// // - Here is another point
/// // that is long enough to wrap
/// ```
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct Wrap {
/// Should leading whitespace be trimmed
pub trim: bool,

View File

@@ -7,7 +7,7 @@ use crate::{
};
/// An enum representing the direction of scrolling in a Scrollbar widget.
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum ScrollDirection {
/// Forward scroll direction, usually corresponds to scrolling downwards or rightwards.
#[default]
@@ -35,7 +35,7 @@ pub enum ScrollDirection {
///
/// If you don't have multi-line content, you can leave the `viewport_content_length` set to the
/// default of 0 and it'll use the track size as a `viewport_content_length`.
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub struct ScrollbarState {
// The current position within the scrollable content.
position: u16,
@@ -101,7 +101,7 @@ impl ScrollbarState {
}
/// Scrollbar Orientation
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub enum ScrollbarOrientation {
#[default]
VerticalRight,
@@ -122,13 +122,13 @@ pub enum ScrollbarOrientation {
/// │ └──────── thumb
/// └─────────── begin
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct Scrollbar<'a> {
orientation: ScrollbarOrientation,
thumb_style: Style,
thumb_symbol: &'a str,
track_style: Style,
track_symbol: &'a str,
track_symbol: Option<&'a str>,
begin_symbol: Option<&'a str>,
begin_style: Style,
end_symbol: Option<&'a str>,
@@ -141,7 +141,7 @@ impl<'a> Default for Scrollbar<'a> {
orientation: ScrollbarOrientation::default(),
thumb_symbol: DOUBLE_VERTICAL.thumb,
thumb_style: Style::default(),
track_symbol: DOUBLE_VERTICAL.track,
track_symbol: Some(DOUBLE_VERTICAL.track),
track_style: Style::default(),
begin_symbol: Some(DOUBLE_VERTICAL.begin),
begin_style: Style::default(),
@@ -187,7 +187,7 @@ impl<'a> Scrollbar<'a> {
}
/// Sets the symbol that represents the track of the scrollbar.
pub fn track_symbol(mut self, track_symbol: &'a str) -> Self {
pub fn track_symbol(mut self, track_symbol: Option<&'a str>) -> Self {
self.track_symbol = track_symbol;
self
}
@@ -233,12 +233,13 @@ impl<'a> Scrollbar<'a> {
/// └─────────── begin
/// ```
///
/// Only sets begin_symbol and end_symbol if they already contain a value.
/// If begin_symbol and/or end_symbol were set to `None` explicitly, this function will respect
/// that choice.
/// 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.
pub fn symbols(mut self, symbol: Set) -> Self {
self.track_symbol = symbol.track;
self.thumb_symbol = symbol.thumb;
if self.track_symbol.is_some() {
self.track_symbol = Some(symbol.track);
}
if self.begin_symbol.is_some() {
self.begin_symbol = Some(symbol.begin);
}
@@ -425,8 +426,10 @@ impl<'a> StatefulWidget for Scrollbar<'a> {
for i in track_start..track_end {
let (style, symbol) = if i >= thumb_start && i < thumb_end {
(self.thumb_style, self.thumb_symbol)
} else if let Some(track_symbol) = self.track_symbol {
(self.track_style, track_symbol)
} else {
(self.track_style, self.track_symbol)
continue;
};
if self.is_vertical() {
@@ -835,4 +838,28 @@ mod tests {
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
#[test]
fn test_rendering_without_track_horizontal_bottom() {
for i in 0..=16 {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 2));
let mut state = ScrollbarState::default().position(i).content_length(16);
Scrollbar::default()
.orientation(ScrollbarOrientation::HorizontalBottom)
.track_symbol(None)
.render(buffer.area, &mut buffer, &mut state);
let expected = if i <= 1 {
vec![" ", "◄██ ►"]
} else if i <= 5 {
vec![" ", "◄ ██ ►"]
} else if i <= 9 {
vec![" ", "◄ ██ ►"]
} else if i <= 13 {
vec![" ", "◄ ██ ►"]
} else {
vec![" ", "◄ ██►"]
};
assert_buffer_eq!(buffer, Buffer::with_lines(expected.clone()));
}
}
}

View File

@@ -21,7 +21,7 @@ use crate::{
/// .max(5)
/// .style(Style::default().fg(Color::Red).bg(Color::White));
/// ```
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Sparkline<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -38,7 +38,7 @@ pub struct Sparkline<'a> {
direction: RenderDirection,
}
#[derive(Debug, Default, Clone, Copy)]
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
pub enum RenderDirection {
#[default]
LeftToRight,

View File

@@ -32,7 +32,7 @@ use crate::{
///
/// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling
/// capabilities of [`Text`].
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Cell<'a> {
content: Text<'a>,
style: Style,
@@ -99,7 +99,7 @@ impl<'a> Styled for Cell<'a> {
/// ```
///
/// By default, a row has a height of 1 but you can change this using [`Row::height`].
#[derive(Debug, Default, Clone, PartialEq, Eq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Row<'a> {
cells: Vec<Cell<'a>>,
height: u16,
@@ -160,6 +160,39 @@ impl<'a> Styled for Row<'a> {
}
}
/// This option allows the user to configure the "highlight symbol" column width spacing
#[derive(Debug, 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 should be done, based on variant
/// Input "selection_state" should be similar to `state.selected.is_some()`
pub fn should_add(&self, selection_state: bool) -> bool {
match self {
HighlightSpacing::Always => true,
HighlightSpacing::WhenSelected => selection_state,
HighlightSpacing::Never => false,
}
}
}
/// A widget to display data in formatted columns.
///
/// It is a collection of [`Row`]s, themselves composed of [`Cell`]s:
@@ -211,7 +244,7 @@ impl<'a> Styled for Row<'a> {
/// // ...and potentially show a symbol in front of the selection.
/// .highlight_symbol(">>");
/// ```
#[derive(Debug, Default, Clone, Eq, PartialEq)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Table<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
@@ -229,6 +262,8 @@ pub struct Table<'a> {
header: Option<Row<'a>>,
/// Data to display in each row
rows: Vec<Row<'a>>,
/// Decides when to allocate spacing for the row selection
highlight_spacing: HighlightSpacing,
}
impl<'a> Table<'a> {
@@ -245,6 +280,7 @@ impl<'a> Table<'a> {
highlight_symbol: None,
header: None,
rows: rows.into_iter().collect(),
highlight_spacing: HighlightSpacing::default(),
}
}
@@ -286,17 +322,24 @@ impl<'a> Table<'a> {
self
}
/// Set when to show the highlight spacing
///
/// See [HighlightSpacing] about which variant affects spacing in which way
pub fn highlight_spacing(mut self, value: HighlightSpacing) -> Self {
self.highlight_spacing = value;
self
}
pub fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
fn get_columns_widths(&self, max_width: u16, has_selection: bool) -> Vec<u16> {
/// Get all offsets and widths of all user specified columns
/// Returns (x, width)
fn get_columns_widths(&self, max_width: u16, selection_width: u16) -> Vec<(u16, u16)> {
let mut constraints = Vec::with_capacity(self.widths.len() * 2 + 1);
if has_selection {
let highlight_symbol_width = self.highlight_symbol.map_or(0, |s| s.width() as u16);
constraints.push(Constraint::Length(highlight_symbol_width));
}
constraints.push(Constraint::Length(selection_width));
for constraint in self.widths {
constraints.push(*constraint);
constraints.push(Constraint::Length(self.column_spacing));
@@ -314,11 +357,12 @@ impl<'a> Table<'a> {
width: max_width,
height: 1,
});
let mut chunks = &chunks[..];
if has_selection {
chunks = &chunks[1..];
}
chunks.iter().step_by(2).map(|c| c.width).collect()
chunks
.iter()
.skip(1)
.step_by(2)
.map(|c| (c.x, c.width))
.collect()
}
fn get_row_bounds(
@@ -372,7 +416,7 @@ impl<'a> Styled for Table<'a> {
}
}
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct TableState {
offset: usize,
selected: Option<usize>,
@@ -426,10 +470,13 @@ impl<'a> StatefulWidget for Table<'a> {
None => area,
};
let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let selection_width = if self.highlight_spacing.should_add(state.selected.is_some()) {
self.highlight_symbol.map_or(0, |s| s.width() as u16)
} else {
0
};
let columns_widths = self.get_columns_widths(table_area.width, selection_width);
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let mut rows_height = table_area.height;
@@ -445,22 +492,18 @@ impl<'a> StatefulWidget for Table<'a> {
},
header.style,
);
let mut col = table_area.left();
if has_selection {
col += (highlight_symbol.width() as u16).min(table_area.width);
}
for (width, cell) in columns_widths.iter().zip(header.cells.iter()) {
let inner_offset = table_area.left();
for ((x, width), cell) in columns_widths.iter().zip(header.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: col,
x: inner_offset + x,
y: table_area.top(),
width: *width,
height: max_header_height,
},
);
col += *width + self.column_spacing;
}
current_height += max_header_height;
rows_height = rows_height.saturating_sub(max_header_height);
@@ -479,41 +522,39 @@ impl<'a> StatefulWidget for Table<'a> {
.skip(state.offset)
.take(end - start)
{
let (row, col) = (table_area.top() + current_height, table_area.left());
let (row, inner_offset) = (table_area.top() + current_height, table_area.left());
current_height += table_row.total_height();
let table_row_area = Rect {
x: col,
x: inner_offset,
y: row,
width: table_area.width,
height: table_row.height,
};
buf.set_style(table_row_area, table_row.style);
let is_selected = state.selected.map_or(false, |s| s == i);
let table_row_start_col = if has_selection {
let symbol = if is_selected {
highlight_symbol
} else {
&blank_symbol
};
let (col, _) =
buf.set_stringn(col, row, symbol, table_area.width as usize, table_row.style);
col
} else {
col
if selection_width > 0 && is_selected {
// this should in normal cases be safe, because "get_columns_widths" allocates
// "highlight_symbol.width()" space but "get_columns_widths"
// currently does not bind it to max table.width()
buf.set_stringn(
inner_offset,
row,
highlight_symbol,
table_area.width as usize,
table_row.style,
);
};
let mut col = table_row_start_col;
for (width, cell) in columns_widths.iter().zip(table_row.cells.iter()) {
for ((x, width), cell) in columns_widths.iter().zip(table_row.cells.iter()) {
render_cell(
buf,
cell,
Rect {
x: col,
x: inner_offset + x,
y: row,
width: *width,
height: table_row.height,
},
);
col += *width + self.column_spacing;
}
if is_selected {
buf.set_style(table_row_area, self.highlight_style);

View File

@@ -23,7 +23,7 @@ use crate::{
/// .highlight_style(Style::default().fg(Color::Yellow))
/// .divider(DOT);
/// ```
#[derive(Debug, Default, Clone)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Tabs<'a> {
/// A block to wrap this widget in if necessary
block: Option<Block<'a>>,

View File

@@ -6,7 +6,7 @@ use ratatui::{
layout::Constraint,
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Cell, Row, Table, TableState},
widgets::{Block, Borders, Cell, HighlightSpacing, Row, Table, TableState},
Terminal,
};
@@ -611,6 +611,139 @@ fn widgets_table_can_have_rows_with_multi_lines() {
);
}
#[test]
fn widgets_table_enable_always_highlight_spacing() {
let test_case = |state: &mut TableState, space: HighlightSpacing, expected: Buffer| {
let backend = TestBackend::new(30, 8);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|f| {
let size = f.size();
let table = Table::new(vec![
Row::new(vec!["Row11", "Row12", "Row13"]),
Row::new(vec!["Row21", "Row22", "Row23"]).height(2),
Row::new(vec!["Row31", "Row32", "Row33"]),
Row::new(vec!["Row41", "Row42", "Row43"]).height(2),
])
.header(Row::new(vec!["Head1", "Head2", "Head3"]).bottom_margin(1))
.block(Block::default().borders(Borders::ALL))
.highlight_symbol(">> ")
.highlight_spacing(space)
.widths(&[
Constraint::Length(5),
Constraint::Length(5),
Constraint::Length(5),
])
.column_spacing(1);
f.render_stateful_widget(table, size, state);
})
.unwrap();
terminal.backend().assert_buffer(&expected);
};
assert_eq!(HighlightSpacing::default(), HighlightSpacing::WhenSelected);
let mut state = TableState::default();
// no selection, "WhenSelected" should only allocate if selected
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// no selection, "Always" should allocate regardless if selected or not
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│ Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// no selection, "Never" should never allocate regadless if selected or not
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first, "WhenSelected" should only allocate if selected
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::default(),
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first, "Always" should allocate regardless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Always,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│ Head1 Head2 Head3 │",
"│ │",
"│>> Row11 Row12 Row13 │",
"│ Row21 Row22 Row23 │",
"│ │",
"│ Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
// select first, "Never" should never allocate regadless if selected or not
state.select(Some(0));
test_case(
&mut state,
HighlightSpacing::Never,
Buffer::with_lines(vec![
"┌────────────────────────────┐",
"│Head1 Head2 Head3 │",
"│ │",
"│Row11 Row12 Row13 │",
"│Row21 Row22 Row23 │",
"│ │",
"│Row31 Row32 Row33 │",
"└────────────────────────────┘",
]),
);
}
#[test]
fn widgets_table_can_have_elements_styled_individually() {
let backend = TestBackend::new(30, 4);