Compare commits
22 Commits
v0.20.0-al
...
v0.22.1-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
778c320008 | ||
|
|
268bbed17e | ||
|
|
f63ac72305 | ||
|
|
3293c6b80b | ||
|
|
149d48919d | ||
|
|
8c4a2e0fbf | ||
|
|
664fb4cffd | ||
|
|
6ad4bd4cf2 | ||
|
|
37fa6abe9d | ||
|
|
8b28672131 | ||
|
|
de9f52ff2c | ||
|
|
c8ddc164c7 | ||
|
|
e18393dbc6 | ||
|
|
aad164a531 | ||
|
|
3a37d2f6ed | ||
|
|
8cd3205d70 | ||
|
|
e82521ea79 | ||
|
|
9191ad60fd | ||
|
|
49a82e062f | ||
|
|
181706c564 | ||
|
|
554805d6cb | ||
|
|
1727fa5120 |
39
.github/workflows/cd.yml
vendored
39
.github/workflows/cd.yml
vendored
@@ -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 }}
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
33
Cargo.toml
33
Cargo.toml
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
64
benches/block.rs
Normal 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
73
benches/list.rs
Normal 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);
|
||||
@@ -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
45
benches/sparkline.rs
Normal 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
2
codecov.yml
Normal file
@@ -0,0 +1,2 @@
|
||||
ignore:
|
||||
- "examples"
|
||||
@@ -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
|
||||
|
||||
@@ -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(¶graph, Borders::ALL, frame, layout[0][0]);
|
||||
render_borders(¶graph, Borders::NONE, frame, layout[0][1]);
|
||||
render_borders(¶graph, Borders::LEFT, frame, layout[1][0]);
|
||||
render_borders(¶graph, Borders::RIGHT, frame, layout[1][1]);
|
||||
render_borders(¶graph, Borders::TOP, frame, layout[2][0]);
|
||||
render_borders(¶graph, Borders::BOTTOM, frame, layout[2][1]);
|
||||
|
||||
render_border_type(¶graph, BorderType::Plain, frame, layout[3][0]);
|
||||
render_border_type(¶graph, BorderType::Rounded, frame, layout[3][1]);
|
||||
render_border_type(¶graph, BorderType::Double, frame, layout[4][0]);
|
||||
render_border_type(¶graph, BorderType::Thick, frame, layout[4][1]);
|
||||
|
||||
render_styled_block(¶graph, frame, layout[5][0]);
|
||||
render_styled_borders(¶graph, frame, layout[5][1]);
|
||||
render_styled_title(¶graph, frame, layout[6][0]);
|
||||
render_styled_title_content(¶graph, frame, layout[6][1]);
|
||||
render_multiple_titles(¶graph, frame, layout[7][0]);
|
||||
render_multiple_title_positions(¶graph, frame, layout[7][1]);
|
||||
render_padding(¶graph, frame, layout[8][0]);
|
||||
render_nested_blocks(¶graph, 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);
|
||||
}
|
||||
|
||||
@@ -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
295
examples/colors.rs
Normal 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
18
examples/colors.tape
Normal 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
116
examples/modifiers.rs
Normal 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
12
examples/modifiers.tape
Normal 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
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,7 +28,7 @@ use crate::{
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TestBackend {
|
||||
width: u16,
|
||||
buffer: Buffer,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
59
src/style.rs
59
src/style.rs
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>);
|
||||
|
||||
|
||||
@@ -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>>,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user