Compare commits
97 Commits
v0.28.2-al
...
v0.30.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5431a70c0c | ||
|
|
65666905e6 | ||
|
|
9703083288 | ||
|
|
c079cafe4d | ||
|
|
421adbe5dd | ||
|
|
35e790cb0d | ||
|
|
2b7ec5cb7f | ||
|
|
d291042e69 | ||
|
|
5c1c97d5a2 | ||
|
|
f51d1ccc07 | ||
|
|
d137456ca1 | ||
|
|
881fe3eff1 | ||
|
|
99ac005b06 | ||
|
|
f132fa1715 | ||
|
|
369b18eef2 | ||
|
|
2ce958e38c | ||
|
|
217c57cd60 | ||
|
|
3ae6bf1d6f | ||
|
|
ec30390446 | ||
|
|
56d5e05762 | ||
|
|
b76ad3b02e | ||
|
|
afd1ce179b | ||
|
|
8f282473b2 | ||
|
|
36e2d1bda1 | ||
|
|
9d5aba69e9 | ||
|
|
1b0d6b473b | ||
|
|
c8339494a8 | ||
|
|
3ef1face9a | ||
|
|
f4cbab4101 | ||
|
|
ae6a8501ee | ||
|
|
1bb41e7165 | ||
|
|
4d7704fba5 | ||
|
|
e4e95bcecf | ||
|
|
a41c97b413 | ||
|
|
46902f5587 | ||
|
|
e7085e3a3e | ||
|
|
9f90f7495f | ||
|
|
260af68a34 | ||
|
|
e461b724a6 | ||
|
|
02c8c9373e | ||
|
|
f40fa787d1 | ||
|
|
7b875091e1 | ||
|
|
17316ec5d0 | ||
|
|
eaa403856e | ||
|
|
e5e2316451 | ||
|
|
98df774d7f | ||
|
|
0a47ebd94b | ||
|
|
abe2f27328 | ||
|
|
fcde9cb9c3 | ||
|
|
2ef3583eff | ||
|
|
a6b579223f | ||
|
|
f1d0a18375 | ||
|
|
55fb2d2e56 | ||
|
|
836634734f | ||
|
|
860e48b0f0 | ||
|
|
04e1b32cd2 | ||
|
|
28732176e1 | ||
|
|
6515097434 | ||
|
|
4c4851ca3d | ||
|
|
4f5503dbf6 | ||
|
|
611086eba4 | ||
|
|
514d273875 | ||
|
|
60cc15bbb0 | ||
|
|
a52ee82fc7 | ||
|
|
381ec75329 | ||
|
|
f6f7794dd7 | ||
|
|
453a308b46 | ||
|
|
9fd1beedb2 | ||
|
|
8db7a9a44a | ||
|
|
b7e488507d | ||
|
|
4728f0e68b | ||
|
|
6db16d67fc | ||
|
|
cc7497532a | ||
|
|
d72968d86b | ||
|
|
7bdccce3d5 | ||
|
|
3df685e114 | ||
|
|
4069aa8274 | ||
|
|
e5a7609588 | ||
|
|
69e0cd2fc4 | ||
|
|
ab6b1feaec | ||
|
|
3a43274881 | ||
|
|
dc8d0587ec | ||
|
|
23c0d52c29 | ||
|
|
c32baa7cd8 | ||
|
|
1153a9ebaf | ||
|
|
2805dddf05 | ||
|
|
baf047f556 | ||
|
|
6745a10508 | ||
|
|
7799f4ff5b | ||
|
|
edcdc8a814 | ||
|
|
5ad623c29b | ||
|
|
bc10af5931 | ||
|
|
784f67a912 | ||
|
|
f4880b40cc | ||
|
|
67c0ea243b | ||
|
|
b9653ba05a | ||
|
|
9875d9facc |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
github: ratatui
|
||||
open_collective: ratatui
|
||||
51
.github/workflows/bench_track_fork_pr.yml
vendored
51
.github/workflows/bench_track_fork_pr.yml
vendored
@@ -18,43 +18,22 @@ jobs:
|
||||
PR_EVENT: event.json
|
||||
steps:
|
||||
- name: Download Benchmark Results
|
||||
uses: actions/github-script@v7
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
script: |
|
||||
async function downloadArtifact(artifactName) {
|
||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
});
|
||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
||||
return artifact.name == artifactName
|
||||
})[0];
|
||||
if (!matchArtifact) {
|
||||
core.setFailed(`Failed to find artifact: ${artifactName}`);
|
||||
}
|
||||
let download = await github.rest.actions.downloadArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: matchArtifact.id,
|
||||
archive_format: 'zip',
|
||||
});
|
||||
let fs = require('fs');
|
||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/${artifactName}.zip`, Buffer.from(download.data));
|
||||
}
|
||||
await downloadArtifact(process.env.BENCHMARK_RESULTS);
|
||||
await downloadArtifact(process.env.PR_EVENT);
|
||||
- name: Unzip Benchmark Results
|
||||
run: |
|
||||
unzip $BENCHMARK_RESULTS.zip
|
||||
unzip $PR_EVENT.zip
|
||||
name: ${{ env.BENCHMARK_RESULTS }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
- name: Download PR Event
|
||||
uses: dawidd6/action-download-artifact@v6
|
||||
with:
|
||||
name: ${{ env.PR_EVENT }}
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
- name: Export PR Event Data
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
let fs = require('fs');
|
||||
let prEvent = JSON.parse(fs.readFileSync(process.env.PR_EVENT, {encoding: 'utf8'}));
|
||||
core.exportVariable("PR_HEAD", `${prEvent.number}/merge`);
|
||||
core.exportVariable("PR_HEAD", prEvent.pull_request.head.ref);
|
||||
core.exportVariable("PR_BASE", prEvent.pull_request.base.ref);
|
||||
core.exportVariable("PR_BASE_SHA", prEvent.pull_request.base.sha);
|
||||
core.exportVariable("PR_NUMBER", prEvent.number);
|
||||
@@ -64,12 +43,14 @@ jobs:
|
||||
bencher run \
|
||||
--project ratatui-org \
|
||||
--token '${{ secrets.BENCHER_API_TOKEN }}' \
|
||||
--branch '${{ env.PR_HEAD }}' \
|
||||
--branch-start-point '${{ env.PR_BASE }}' \
|
||||
--branch-start-point-hash '${{ env.PR_BASE_SHA }}' \
|
||||
--branch "$PR_HEAD" \
|
||||
--start-point "$PR_BASE" \
|
||||
--start-point-hash "$PR_BASE_SHA" \
|
||||
--start-point-clone-thresholds \
|
||||
--start-point-reset \
|
||||
--testbed ubuntu-latest \
|
||||
--adapter rust_criterion \
|
||||
--err \
|
||||
--github-actions '${{ secrets.GITHUB_TOKEN }}' \
|
||||
--ci-number '${{ env.PR_NUMBER }}' \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
--ci-number "$PR_NUMBER" \
|
||||
--file "$BENCHMARK_RESULTS"
|
||||
123
.github/workflows/ci.yml
vendored
123
.github/workflows/ci.yml
vendored
@@ -9,7 +9,6 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
merge_group:
|
||||
|
||||
# ensure that the workflow is only triggered once per PR, subsequent pushes to the PR will cancel
|
||||
# and restart the workflow. See https://docs.github.com/en/actions/using-jobs/using-concurrency
|
||||
@@ -21,73 +20,88 @@ concurrency:
|
||||
# typos, and missing tests as early as possible. This allows us to fix these and resubmit the PR
|
||||
# without having to wait for the comprehensive matrix of tests to complete.
|
||||
jobs:
|
||||
rustfmt:
|
||||
# Lint the formatting of the codebase.
|
||||
lint-formatting:
|
||||
name: Check Formatting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo +nightly fmt --all --check
|
||||
with: { components: rustfmt }
|
||||
- run: cargo xtask lint-formatting
|
||||
|
||||
typos:
|
||||
# Check for typos in the codebase.
|
||||
# See <https://github.com/crate-ci/typos/>
|
||||
lint-typos:
|
||||
name: Check Typos
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: crate-ci/typos@master
|
||||
|
||||
# Check for any disallowed dependencies in the codebase due to license / security issues.
|
||||
# See <https://github.com/EmbarkStudios/cargo-deny>
|
||||
dependencies:
|
||||
name: Check Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
|
||||
# Check for any unused dependencies in the codebase.
|
||||
# See <https://github.com/bnjbvr/cargo-machete/>
|
||||
cargo-machete:
|
||||
name: Check Unused Dependencies
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: bnjbvr/cargo-machete@v0.6.2
|
||||
- uses: bnjbvr/cargo-machete@v0.7.0
|
||||
|
||||
clippy:
|
||||
# Run cargo clippy.
|
||||
lint-clippy:
|
||||
name: Check Clippy
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
with: { components: clippy }
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make clippy
|
||||
- run: cargo xtask lint-clippy
|
||||
|
||||
markdownlint:
|
||||
# Run markdownlint on all markdown files in the repository.
|
||||
lint-markdown:
|
||||
name: Check Markdown
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v16
|
||||
- uses: DavidAnson/markdownlint-cli2-action@v18
|
||||
with:
|
||||
globs: |
|
||||
'**/*.md'
|
||||
'!target'
|
||||
|
||||
# Run cargo coverage. This will generate a coverage report and upload it to codecov.
|
||||
# <https://app.codecov.io/gh/ratatui/ratatui>
|
||||
coverage:
|
||||
name: Coverage Report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: llvm-tools
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-llvm-cov,cargo-make
|
||||
- uses: taiki-e/install-action@cargo-llvm-cov
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make coverage
|
||||
- uses: codecov/codecov-action@v4
|
||||
- run: cargo xtask coverage
|
||||
- uses: codecov/codecov-action@v5
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
|
||||
# Run cargo check. This is a fast way to catch any obvious errors in the code.
|
||||
check:
|
||||
name: Check ${{ matrix.os }} ${{ matrix.toolchain }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -99,13 +113,23 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make check
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- run: cargo xtask check
|
||||
|
||||
# Check if README.md is up-to-date with the crate's documentation.
|
||||
check-readme:
|
||||
name: Check README
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: taiki-e/install-action@cargo-rdme
|
||||
- run: cargo xtask check-readme
|
||||
|
||||
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account the
|
||||
# package.metadata.docs.rs configured in Cargo.toml. https://github.com/dtolnay/cargo-docs-rs
|
||||
lint-docs:
|
||||
name: Check Docs
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTDOCFLAGS: -Dwarnings
|
||||
@@ -114,47 +138,48 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: dtolnay/install@cargo-docs-rs
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
# Run cargo rustdoc with the same options that would be used by docs.rs, taking into account
|
||||
# the package.metadata.docs.rs configured in Cargo.toml.
|
||||
# https://github.com/dtolnay/cargo-docs-rs
|
||||
- run: cargo +nightly docs-rs
|
||||
- run: cargo xtask lint-docs
|
||||
|
||||
test-doc:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
runs-on: ${{ matrix.os }}
|
||||
# Run cargo test on the documentation of the crate. This will catch any code examples that don't
|
||||
# compile, or any other issues in the documentation.
|
||||
test-docs:
|
||||
name: Test Docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: taiki-e/install-action@cargo-make
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-doc
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- run: cargo xtask test-docs
|
||||
|
||||
test:
|
||||
# Run cargo test on the libraries of the crate.
|
||||
test-libs:
|
||||
name: Test Libs ${{ matrix.toolchain }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo xtask test-libs
|
||||
|
||||
# Run cargo test on all the backends.
|
||||
test-backends:
|
||||
name: Test ${{matrix.backend}} on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
toolchain: ["1.74.0", "stable"]
|
||||
backend: [crossterm, termion, termwiz]
|
||||
exclude:
|
||||
# termion is not supported on windows
|
||||
- os: windows-latest
|
||||
backend: termion
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@master
|
||||
with:
|
||||
toolchain: ${{ matrix.toolchain }}
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make,nextest
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- run: cargo make test-backend ${{ matrix.backend }}
|
||||
env:
|
||||
RUST_BACKTRACE: full
|
||||
- run: cargo xtask test-backend ${{ matrix.backend }}
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
||||
target
|
||||
Cargo.lock
|
||||
*.log
|
||||
*.rs.rustfmt
|
||||
.gdb_history
|
||||
|
||||
@@ -10,8 +10,17 @@ GitHub with a [breaking change] label.
|
||||
|
||||
This is a quick summary of the sections below:
|
||||
|
||||
- [Unreleased](#unreleased)
|
||||
- The `From` impls for backend types are now replaced with more specific traits
|
||||
- [v0.29.0](#v0290)
|
||||
- `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const
|
||||
- Removed public fields from `Rect` iterators
|
||||
- `Line` now implements `From<Cow<str>`
|
||||
- `Table::highlight_style` is now `Table::row_highlight_style`
|
||||
- `Tabs::select` now accepts `Into<Option<usize>>`
|
||||
- `Color::from_hsl` is now behind the `palette` feature
|
||||
- [v0.28.0](#v0280)
|
||||
⁻ `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend::size` returns `Size` instead of `Rect`
|
||||
- `Backend` trait migrates to `get/set_cursor_position`
|
||||
- Ratatui now requires Crossterm 0.28.0
|
||||
- `Axis::labels` now accepts `IntoIterator<Into<Line>>`
|
||||
@@ -65,7 +74,189 @@ This is a quick summary of the sections below:
|
||||
- MSRV is now 1.63.0
|
||||
- `List` no longer ignores empty strings
|
||||
|
||||
## v0.28.0
|
||||
## Unreleased (0.30.0)
|
||||
|
||||
### `WidgetRef` no longer has a blanket implementation of Widget
|
||||
|
||||
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
|
||||
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
|
||||
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
|
||||
type.
|
||||
|
||||
```diff
|
||||
-impl WidgetRef for Foo {
|
||||
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
|
||||
+impl Widget for &Foo {
|
||||
+ fn render(self, area: Rect, buf: &mut Buffer)
|
||||
}
|
||||
```
|
||||
|
||||
### The `From` impls for backend types are now replaced with more specific traits [#1464]
|
||||
|
||||
[#1464]: https://github.com/ratatui/ratatui/pull/1464
|
||||
|
||||
Crossterm gains `ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm}`
|
||||
Termwiz gains `ratatui::backend::termwiz::{FromTermwiz, IntoTermwiz}`
|
||||
|
||||
This is necessary in order to avoid the orphan rule when implementing `From` for crossterm types
|
||||
once the crossterm types are moved to a separate crate.
|
||||
|
||||
```diff
|
||||
+ use ratatui::backend::crossterm::{FromCrossterm, IntoCrossterm};
|
||||
|
||||
let crossterm_color = crossterm::style::Color::Black;
|
||||
- let ratatui_color = crossterm_color.into();
|
||||
- let ratatui_color = ratatui::style::Color::from(crossterm_color);
|
||||
+ let ratatui_color = ratatui::style::Color::from_crossterm(crossterm_color);
|
||||
- let crossterm_color = ratatui_color.into();
|
||||
- let crossterm_color = crossterm::style::Color::from(ratatui_color);
|
||||
+ let crossterm_color = ratatui_color.into_crossterm();
|
||||
|
||||
let crossterm_attribute = crossterm::style::types::Attribute::Bold;
|
||||
- let ratatui_modifier = crossterm_attribute.into();
|
||||
- let ratatui_modifier = ratatui::style::Modifier::from(crossterm_attribute);
|
||||
+ let ratatui_modifier = ratatui::style::Modifier::from_crossterm(crossterm_attribute);
|
||||
- let crossterm_attribute = ratatui_modifier.into();
|
||||
- let crossterm_attribute = crossterm::style::types::Attribute::from(ratatui_modifier);
|
||||
+ let crossterm_attribute = ratatui_modifier.into_crossterm();
|
||||
```
|
||||
|
||||
Similar conversions for `ContentStyle` -> `Style` and `Attributes` -> `Modifier` exist for
|
||||
Crossterm and the various Termion and Termwiz types as well.
|
||||
|
||||
### `Bar::label()` and `BarGroup::label()` now accepts `Into<Line<'a>>`. ([#1471])
|
||||
|
||||
[#1471]: https://github.com/ratatui/ratatui/pull/1471
|
||||
|
||||
Previously `Bar::label()` and `BarGroup::label()` accepted `Line<'a>`, but they now accepts `Into<Line<'a>>`.
|
||||
|
||||
for `Bar::label()`:
|
||||
|
||||
```diff
|
||||
- Bar::default().label("foo".into());
|
||||
+ Bar::default().label("foo");
|
||||
```
|
||||
|
||||
for `BarGroup::label()`:
|
||||
|
||||
```diff
|
||||
- BarGroup::default().label("bar".into());
|
||||
+ BarGroup::default().label("bar");
|
||||
```
|
||||
|
||||
### `Bar::text_value` now accepts `Into<String>` ([#1471])
|
||||
|
||||
Previously `Bar::text_value` accepted `String`, but now it accepts `Into<String>`.
|
||||
|
||||
for `Bar::text_value()`:
|
||||
|
||||
```diff
|
||||
- Bar::default().text_value("foobar".into());
|
||||
+ Bar::default().text_value("foobar");
|
||||
```
|
||||
|
||||
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0)
|
||||
|
||||
### `Sparkline::data` takes `IntoIterator<Item = SparklineBar>` instead of `&[u64]` and is no longer const ([#1326])
|
||||
|
||||
[#1326]: https://github.com/ratatui/ratatui/pull/1326
|
||||
|
||||
The `Sparkline::data` method has been modified to accept `IntoIterator<Item = SparklineBar>`
|
||||
instead of `&[u64]`.
|
||||
|
||||
`SparklineBar` is a struct that contains an `Option<u64>` value, which represents an possible
|
||||
_absent_ value, as distinct from a `0` value. This change allows the `Sparkline` to style
|
||||
data points differently, depending on whether they are present or absent.
|
||||
|
||||
`SparklineBar` also contains an `Option<Style>` that will be used to apply a style the bar in
|
||||
addition to any other styling applied to the `Sparkline`.
|
||||
|
||||
Several `From` implementations have been added to `SparklineBar` to support existing callers who
|
||||
provide `&[u64]` and other types that can be converted to `SparklineBar`, such as `Option<u64>`.
|
||||
|
||||
If you encounter any type inference issues, you may need to provide an explicit type for the data
|
||||
passed to `Sparkline::data`. For example, if you are passing a single value, you may need to use
|
||||
`into()` to convert it to form that can be used as a `SparklineBar`:
|
||||
|
||||
```diff
|
||||
let value = 1u8;
|
||||
- Sparkline::default().data(&[value.into()]);
|
||||
+ Sparkline::default().data(&[u64::from(value)]);
|
||||
```
|
||||
|
||||
As a consequence of this change, the `data` method is no longer a `const fn`.
|
||||
|
||||
### `Color::from_hsl` is now behind the `palette` feature and accepts `palette::Hsl` ([#1418])
|
||||
|
||||
[#1418]: https://github.com/ratatui/ratatui/pull/1418
|
||||
|
||||
Previously `Color::from_hsl` accepted components as individual f64 parameters. It now accepts a
|
||||
single `palette::Hsl` value and is gated behind a `palette` feature flag.
|
||||
|
||||
```diff
|
||||
- Color::from_hsl(360.0, 100.0, 100.0)
|
||||
+ Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
|
||||
```
|
||||
|
||||
### Removed public fields from `Rect` iterators ([#1358], [#1424])
|
||||
|
||||
[#1358]: https://github.com/ratatui/ratatui/pull/1358
|
||||
[#1424]: https://github.com/ratatui/ratatui/pull/1424
|
||||
|
||||
The `pub` modifier has been removed from fields on the `Columns`,`Rows`, and `Positions` iterators.
|
||||
These fields were not intended to be public and should not have been accessed directly.
|
||||
|
||||
### `Rect::area()` now returns u32 instead of u16 ([#1378])
|
||||
|
||||
[#1378]: https://github.com/ratatui/ratatui/pull/1378
|
||||
|
||||
This is likely to impact anything which relies on `Rect::area` maxing out at u16::MAX. It can now
|
||||
return up to u16::MAX * u16::MAX (2^32 - 2^17 + 1).
|
||||
|
||||
### `Line` now implements `From<Cow<str>` ([#1373])
|
||||
|
||||
[#1373]: https://github.com/ratatui/ratatui/pull/1373
|
||||
|
||||
As this adds an extra conversion, ambiguous inferred expressions may no longer compile.
|
||||
|
||||
```rust
|
||||
// given:
|
||||
struct Foo { ... }
|
||||
impl From<Foo> for String { ... }
|
||||
impl From<Foo> for Cow<str> { ... }
|
||||
|
||||
let foo = Foo { ... };
|
||||
let line = Line::from(foo); // now fails due to now ambiguous inferred type
|
||||
// replace with e.g.
|
||||
let line = Line::from(String::from(foo));
|
||||
```
|
||||
|
||||
### `Tabs::select()` now accepts `Into<Option<usize>>` ([#1413])
|
||||
|
||||
[#1413]: https://github.com/ratatui/ratatui/pull/1413
|
||||
|
||||
Previously `Tabs::select()` accepted `usize`, but it now accepts `Into<Option<usize>>`. This breaks
|
||||
any code already using parameter type inference:
|
||||
|
||||
```diff
|
||||
let selected = 1u8;
|
||||
- let tabs = Tabs::new(["A", "B"]).select(selected.into())
|
||||
+ let tabs = Tabs::new(["A", "B"]).select(selected as usize)
|
||||
```
|
||||
|
||||
### `Table::highlight_style` is now `Table::row_highlight_style` ([#1331])
|
||||
|
||||
[#1331]: https://github.com/ratatui/ratatui/pull/1331
|
||||
|
||||
The `Table::highlight_style` is now deprecated in favor of `Table::row_highlight_style`.
|
||||
|
||||
Also, the serialized output of the `TableState` will now include the "selected_column" field.
|
||||
Software that manually parse the serialized the output (with anything other than the `Serialize`
|
||||
implementation on `TableState`) may have to be refactored if the "selected_column" field is not
|
||||
accounted for. This does not affect users who rely on the `Deserialize`, or `Serialize`
|
||||
implementation on the state.
|
||||
|
||||
## [v0.28.0](https://github.com/ratatui/ratatui/releases/tag/v0.28.0)
|
||||
|
||||
### `Backend::size` returns `Size` instead of `Rect` ([#1254])
|
||||
|
||||
@@ -134,7 +325,7 @@ are also named terminal, and confusion about module exports for newer Rust users
|
||||
|
||||
This change simplifies the trait and makes it easier to implement.
|
||||
|
||||
### `Frame::size` is deprecated and renamed to `Frame::area`
|
||||
### `Frame::size` is deprecated and renamed to `Frame::area` ([#1293])
|
||||
|
||||
[#1293]: https://github.com/ratatui/ratatui/pull/1293
|
||||
|
||||
|
||||
552
CHANGELOG.md
552
CHANGELOG.md
@@ -2,6 +2,556 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
_"Food will come, Remy. Food always comes to those who love to cook." – Gusteau_
|
||||
|
||||
We are excited to announce the new version of `ratatui` - a Rust library that's all about cooking up TUIs 👨🍳🐀
|
||||
|
||||
✨ **Release highlights**: <https://ratatui.rs/highlights/v029/>
|
||||
|
||||
⚠️ List of breaking changes can be found [here](https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md).
|
||||
|
||||
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0) - 2024-10-21
|
||||
|
||||
### Features
|
||||
|
||||
- [3a43274](https://github.com/ratatui/ratatui/commit/3a43274881a79b4e593536c2ca915b509e557215) *(color)* Add hsluv support by @du-ob in [#1333](https://github.com/ratatui/ratatui/pull/1333)
|
||||
|
||||
- [4c4851c](https://github.com/ratatui/ratatui/commit/4c4851ca3d1437a50ed1f146c0849b58716b89a2) *(example)* Add drawing feature to the canvas example by @orhun in [#1429](https://github.com/ratatui/ratatui/pull/1429)
|
||||
|
||||
> 
|
||||
>
|
||||
>
|
||||
> fun fact: I had to do [35
|
||||
> pushups](https://www.youtube.com/watch?v=eS92stzBYXA) for this...
|
||||
>
|
||||
> ---------
|
||||
|
||||
- [e5a7609](https://github.com/ratatui/ratatui/commit/e5a76095884a4ce792846289f56d04a4acaaa6fa) *(line)* Impl From<Cow<str>> for Line by @joshka in [#1373](https://github.com/ratatui/ratatui/pull/1373) [**breaking**]
|
||||
>
|
||||
> BREAKING-CHANGES:`Line` now implements `From<Cow<str>`
|
||||
>
|
||||
> As this adds an extra conversion, ambiguous inferred values may no longer
|
||||
> compile.
|
||||
>
|
||||
> ```rust
|
||||
> // given:
|
||||
> struct Foo { ... }
|
||||
> impl From<Foo> for String { ... }
|
||||
> impl From<Foo> for Cow<str> { ... }
|
||||
>
|
||||
> let foo = Foo { ... };
|
||||
> let line = Line::from(foo); // now fails due to ambiguous type inference
|
||||
> // replace with
|
||||
> let line = Line::from(String::from(foo));
|
||||
> ```
|
||||
>
|
||||
> Fixes:https://github.com/ratatui/ratatui/issues/1367
|
||||
>
|
||||
> ---------
|
||||
|
||||
- [2805ddd](https://github.com/ratatui/ratatui/commit/2805dddf0527584da9c7865ff6a78a9c74731187) *(logo)* Add a Ratatui logo widget by @joshka in [#1307](https://github.com/ratatui/ratatui/pull/1307)
|
||||
|
||||
> This is a simple logo widget that can be used to render the Ratatui logo
|
||||
> in the terminal. It is used in the `examples/ratatui-logo.rs` example,
|
||||
> and may be used in your applications' help or about screens.
|
||||
>
|
||||
> ```rust
|
||||
> use ratatui::{Frame, widgets::RatatuiLogo};
|
||||
>
|
||||
> fn draw(frame: &mut Frame) {
|
||||
> frame.render_widget(RatatuiLogo::tiny(), frame.area());
|
||||
> }
|
||||
> ```
|
||||
|
||||
- [d72968d](https://github.com/ratatui/ratatui/commit/d72968d86b94100579feba80c5cd207c2e7e13e7) *(scrolling-regions)* Use terminal scrolling regions to stop Terminal::insert_before from flickering by @nfachan in [#1341](https://github.com/ratatui/ratatui/pull/1341) [**breaking**]
|
||||
|
||||
> The current implementation of Terminal::insert_before causes the
|
||||
> viewport to flicker. This is described in #584 .
|
||||
>
|
||||
> This PR removes that flickering by using terminal scrolling regions
|
||||
> (sometimes called "scroll regions"). A terminal can have its scrolling
|
||||
> region set to something other than the whole screen. When a scroll ANSI
|
||||
> sequence is sent to the terminal and it has a non-default scrolling
|
||||
> region, the terminal will scroll just inside of that region.
|
||||
>
|
||||
> We use scrolling regions to implement insert_before. We create a region
|
||||
> on the screen above the viewport, scroll that up to make room for the
|
||||
> newly inserted lines, and then draw the new lines. We may need to repeat
|
||||
> this process depending on how much space there is and how many lines we
|
||||
> need to draw.
|
||||
>
|
||||
> When the viewport takes up the entire screen, we take a modified
|
||||
> approach. We create a scrolling region of just the top line (could be
|
||||
> more) of the viewport, then use that to draw the lines we want to
|
||||
> output. When we're done, we scroll it up by one line, into the
|
||||
> scrollback history, and then redraw the top line from the viewport.
|
||||
>
|
||||
> A final edge case is when the viewport hasn't yet reached the bottom of
|
||||
> the screen. This case, we set up a different scrolling region, where the
|
||||
> top is the top of the viewport, and the bottom is the viewport's bottom
|
||||
> plus the number of lines we want to scroll by. We then scroll this
|
||||
> region down to open up space above the viewport for drawing the inserted
|
||||
> lines.
|
||||
>
|
||||
> Regardless of what we do, we need to reset the scrolling region. This PR
|
||||
> takes the approach of always resetting the scrolling region after every
|
||||
> operation. So the Backend gets new scroll_region_up and
|
||||
> scroll_region_down methods instead of set_scrolling_region, scroll_up,
|
||||
> scroll_down, and reset_scrolling_region methods. We chose that approach
|
||||
> for two reasons. First, we don't want Ratatui to have to remember that
|
||||
> state and then reset the scrolling region when tearing down. Second, the
|
||||
> pre-Windows-10 console code doesn't support scrolling region
|
||||
>
|
||||
> This PR:
|
||||
> - Adds a new scrolling-regions feature.
|
||||
> - Adds two new Backend methods: scroll_region_up and scroll_region_down.
|
||||
> - Implements those Backend methods on all backends in the codebase.
|
||||
> - The crossterm and termion implementations use raw ANSI escape
|
||||
> sequences. I'm trying to merge changes into those two projects
|
||||
> separately to support these functions.
|
||||
> - Adds code to Terminal::insert_before to choose between
|
||||
> insert_before_scrolling_regions and insert_before_no_scrolling_regions.
|
||||
> The latter is the old implementation.
|
||||
> - Adds lots of tests to the TestBackend to for the
|
||||
> scrolling-region-related Backend methods.
|
||||
> - Adds versions of terminal tests that show that insert_before doesn't
|
||||
> clobber the viewport. This is a change in behavior from before.
|
||||
|
||||
- [dc8d058](https://github.com/ratatui/ratatui/commit/dc8d0587ecfd46cde86c9e33a6fd385e2d4810a9) *(table)* Add support for selecting column and cell by @airblast-dev in [#1331](https://github.com/ratatui/ratatui/pull/1331) [**breaking**]
|
||||
|
||||
> Fixes https://github.com/ratatui-org/ratatui/issues/1250
|
||||
>
|
||||
> Adds support for selecting a column and cell in `TableState`. The
|
||||
> selected column, and cells style can be set by
|
||||
> `Table::column_highlight_style` and `Table::cell_highlight_style`
|
||||
> respectively.
|
||||
>
|
||||
> The table example has also been updated to display the new
|
||||
> functionality:
|
||||
>
|
||||
> https://github.com/user-attachments/assets/e5fd2858-4931-4ce1-a2f6-a5ea1eacbecc
|
||||
>
|
||||
> BREAKING CHANGE:The Serialized output of the state will now include the
|
||||
> "selected_column" field. Software that manually parse the serialized the
|
||||
> output (with anything other than the `Serialize` implementation on
|
||||
> `TableState`) may have to be refactored if the "selected_column" field
|
||||
> is not accounted for. This does not affect users who rely on the
|
||||
> `Deserialize`, or `Serialize` implementation on the state.
|
||||
>
|
||||
> BREAKING CHANGE:The `Table::highlight_style` is now deprecated in favor
|
||||
> of `Table::row_highlight_style`.
|
||||
>
|
||||
> ---------
|
||||
|
||||
- [ab6b1fe](https://github.com/ratatui/ratatui/commit/ab6b1feaec3ef0cf23bcfac219b95ec946180fa8) *(tabs)* Allow tabs to be deselected by @joshka in [#1413](https://github.com/ratatui/ratatui/pull/1413) [**breaking**]
|
||||
>
|
||||
> `Tabs::select()` now accepts `Into<Option<usize>>` instead of `usize`.
|
||||
> This allows tabs to be deselected by passing `None`.
|
||||
>
|
||||
> `Tabs::default()` is now also implemented manually instead of deriving
|
||||
> `Default`, and a new method `Tabs::titles()` is added to set the titles
|
||||
> of the tabs.
|
||||
>
|
||||
> Fixes:<https://github.com/ratatui/ratatui/pull/1412>
|
||||
>
|
||||
> BREAKING CHANGE:`Tabs::select()` now accepts `Into<Option<usize>>`
|
||||
> which breaks any code already using parameter type inference:
|
||||
>
|
||||
> ```diff
|
||||
> let selected = 1u8;
|
||||
> - let tabs = Tabs::new(["A", "B"]).select(selected.into())
|
||||
> + let tabs = Tabs::new(["A", "B"]).select(selected as usize)
|
||||
> ```
|
||||
|
||||
- [23c0d52](https://github.com/ratatui/ratatui/commit/23c0d52c29f27547d94448be44aa46e85f49fbb0) *(text)* Improve concise debug view for Span,Line,Text,Style by @joshka in [#1410](https://github.com/ratatui/ratatui/pull/1410)
|
||||
|
||||
> Improves https://github.com/ratatui/ratatui/pull/1383
|
||||
>
|
||||
> The following now round trips when formatted for debug.
|
||||
> This will make it easier to use insta when testing text related views of
|
||||
> widgets.
|
||||
>
|
||||
> ```rust
|
||||
> Text::from_iter([
|
||||
> Line::from("Hello, world!"),
|
||||
> Line::from("How are you?").bold().left_aligned(),
|
||||
> Line::from_iter([
|
||||
> Span::from("I'm "),
|
||||
> Span::from("doing ").italic(),
|
||||
> Span::from("great!").bold(),
|
||||
> ]),
|
||||
> ]).on_blue().italic().centered()
|
||||
> ```
|
||||
|
||||
- [60cc15b](https://github.com/ratatui/ratatui/commit/60cc15bbb064faa704f78ca51ae60584b5f7ca31) *(uncategorized)* Add support for empty bar style to `Sparkline` by @fujiapple852 in [#1326](https://github.com/ratatui/ratatui/pull/1326) [**breaking**]
|
||||
|
||||
> - distinguish between empty bars and bars with a value of 0
|
||||
> - provide custom styling for empty bars
|
||||
> - provide custom styling for individual bars
|
||||
> - inverts the rendering algorithm to be item first
|
||||
>
|
||||
> Closes:#1325
|
||||
>
|
||||
> BREAKING CHANGE:`Sparkline::data` takes `IntoIterator<Item = SparklineBar>`
|
||||
> instead of `&[u64]` and is no longer const
|
||||
|
||||
- [453a308](https://github.com/ratatui/ratatui/commit/453a308b46bbacba2ee7cba849cf0c19c88a1a27) *(uncategorized)* Add overlap to layout by @kdheepak in [#1398](https://github.com/ratatui/ratatui/pull/1398) [**breaking**]
|
||||
|
||||
> This PR adds a new feature for the existing `Layout::spacing` method,
|
||||
> and introducing a `Spacing` enum.
|
||||
>
|
||||
> Now `Layout::spacing` is generic and can take
|
||||
>
|
||||
> - zero or positive numbers, e.g. `Layout::spacing(1)` (current
|
||||
> functionality)
|
||||
> - negative number, e.g. `Layout::spacing(-1)` (new)
|
||||
> - variant of the `Spacing` (new)
|
||||
>
|
||||
> This allows creating layouts with a shared pixel for segments. When
|
||||
> `spacing(negative_value)` is used, spacing is ignored and all segments
|
||||
> will be adjacent and have pixels overlapping.
|
||||
> `spacing(zero_or_positive_value)` behaves the same as before. These are
|
||||
> internally converted to `Spacing::Overlap` or `Spacing::Space`.
|
||||
>
|
||||
> Here's an example output to illustrate the layout solve from this PR:
|
||||
>
|
||||
> ```rust
|
||||
> #[test]
|
||||
> fn test_layout() {
|
||||
> use crate::layout::Constraint::*;
|
||||
> let mut terminal = crate::Terminal::new(crate::backend::TestBackend::new(50, 4)).unwrap();
|
||||
> terminal
|
||||
> .draw(|frame| {
|
||||
> let [upper, lower] = Layout::vertical([Fill(1), Fill(1)]).areas(frame.area());
|
||||
>
|
||||
> let (segments, spacers) = Layout::horizontal([Length(10), Length(10), Length(10)])
|
||||
> .flex(Flex::Center)
|
||||
> .split_with_spacers(upper);
|
||||
>
|
||||
> for segment in segments.iter() {
|
||||
> frame.render_widget(
|
||||
> crate::widgets::Block::bordered()
|
||||
> .border_set(crate::symbols::border::DOUBLE),
|
||||
> *segment,
|
||||
> );
|
||||
> }
|
||||
> for spacer in spacers.iter() {
|
||||
> frame.render_widget(crate::widgets::Block::bordered(), *spacer);
|
||||
> }
|
||||
>
|
||||
> let (segments, spacers) = Layout::horizontal([Length(10), Length(10), Length(10)])
|
||||
> .flex(Flex::Center)
|
||||
> .spacing(-1) // new feature
|
||||
> .split_with_spacers(lower);
|
||||
>
|
||||
> for segment in segments.iter() {
|
||||
> frame.render_widget(
|
||||
> crate::widgets::Block::bordered()
|
||||
> .border_set(crate::symbols::border::DOUBLE),
|
||||
> *segment,
|
||||
> );
|
||||
> }
|
||||
> for spacer in spacers.iter() {
|
||||
> frame.render_widget(crate::widgets::Block::bordered(), *spacer);
|
||||
> }
|
||||
> })
|
||||
> .unwrap();
|
||||
> dbg!(terminal.backend());
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
>
|
||||
> ```plain
|
||||
> ┌────────┐╔════════╗╔════════╗╔════════╗┌────────┐
|
||||
> └────────┘╚════════╝╚════════╝╚════════╝└────────┘
|
||||
> ┌─────────┐╔════════╔════════╔════════╗┌─────────┐
|
||||
> └─────────┘╚════════╚════════╚════════╝└─────────┘
|
||||
> ```
|
||||
>
|
||||
> Currently drawing a border on top of an existing border overwrites it.
|
||||
> Future PRs will allow for making the border drawing handle overlaps
|
||||
> better.
|
||||
>
|
||||
> ---------
|
||||
|
||||
- [7bdccce](https://github.com/ratatui/ratatui/commit/7bdccce3d56052306eb4121afe6b1ff56b198796) *(uncategorized)* Add an impl of `DoubleEndedIterator` for `Columns` and `Rows` by @fujiapple852 [**breaking**]
|
||||
>
|
||||
> BREAKING-CHANGE:The `pub` modifier has been removed from fields on the
|
||||
>
|
||||
> `layout::rect::Columns` and `layout::rect::Rows` iterators. These fields
|
||||
> were not intended to be public and should not have been accessed
|
||||
> directly.
|
||||
>
|
||||
> Fixes:#1357
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [4f5503d](https://github.com/ratatui/ratatui/commit/4f5503dbf610290904a759a3f169a15111f11392) *(color)* Hsl and hsluv are now clamped before conversion by @joshka in [#1436](https://github.com/ratatui/ratatui/pull/1436) [**breaking**]
|
||||
|
||||
> The `from_hsl` and `from_hsluv` functions now clamp the HSL and HSLuv
|
||||
> values before converting them to RGB. This ensures that the input values
|
||||
> are within the expected range before conversion.
|
||||
>
|
||||
> Also note that the ranges of Saturation and Lightness values have been
|
||||
> aligned to be consistent with the palette crate. Saturation and Lightness
|
||||
> for `from_hsl` are now in the range [0.0..1.0] while `from_hsluv` are
|
||||
> in the range [0.0..100.0].
|
||||
>
|
||||
> Refs:- <https://github.com/Ogeon/palette/discussions/253>
|
||||
> - <https://docs.rs/palette/latest/palette/struct.Hsl.html>
|
||||
> - <https://docs.rs/palette/latest/palette/struct.Hsluv.html>
|
||||
>
|
||||
> Fixes:<https://github.com/ratatui/ratatui/issues/1433>
|
||||
|
||||
- [b7e4885](https://github.com/ratatui/ratatui/commit/b7e488507d23cbc91ac63d5249088ad0f4852205) *(color)* Fix doc test for from_hsl by @joshka in [#1421](https://github.com/ratatui/ratatui/pull/1421)
|
||||
|
||||
- [3df685e](https://github.com/ratatui/ratatui/commit/3df685e1144340935db2b1d929e2546f83c5e65f) *(rect)* Rect::area now returns u32 and Rect::new() no longer clamps area to u16::MAX by @joshka in [#1378](https://github.com/ratatui/ratatui/pull/1378) [**breaking**]
|
||||
|
||||
> This change fixes the unexpected behavior of the Rect::new() function to
|
||||
> be more intuitive. The Rect::new() function now clamps the width and
|
||||
> height of the rectangle to keep each bound within u16::MAX. The
|
||||
> Rect::area() function now returns a u32 instead of a u16 to allow for
|
||||
> larger areas to be calculated.
|
||||
>
|
||||
> Previously, the Rect::new() function would clamp the total area of the
|
||||
> rectangle to u16::MAX, by preserving the aspect ratio of the rectangle.
|
||||
>
|
||||
> BREAKING CHANGE:Rect::area() now returns a u32 instead of a u16.
|
||||
>
|
||||
> Fixes:<https://github.com/ratatui/ratatui/issues/1375>
|
||||
|
||||
- [514d273](https://github.com/ratatui/ratatui/commit/514d2738750d792a75fde6cc7666f9220bcf6b3a) *(terminal)* Use the latest, resized area when clearing by @roberth in [#1427](https://github.com/ratatui/ratatui/pull/1427)
|
||||
|
||||
- [0f48239](https://github.com/ratatui/ratatui/commit/0f4823977894cef51d5ffafe6ae35ca7ad56e1ac) *(terminal)* Resize() now resizes fixed viewports by @Patryk27 in [#1353](https://github.com/ratatui/ratatui/pull/1353)
|
||||
>
|
||||
> `Terminal::resize()` on a fixed viewport used to do nothing due to
|
||||
> an accidentally shadowed variable. This now works as intended.
|
||||
|
||||
- [a52ee82](https://github.com/ratatui/ratatui/commit/a52ee82fc716fafb2652b83a331c36f844104dda) *(text)* Truncate based on alignment by @Lunderberg in [#1432](https://github.com/ratatui/ratatui/pull/1432)
|
||||
|
||||
> This is a follow-up PR to https://github.com/ratatui/ratatui/pull/987,
|
||||
> which implemented alignment-aware truncation for the `Line` widget.
|
||||
> However, the truncation only checked the `Line::alignment` field, and
|
||||
> any alignment inherited from a parent's `Text::alignment` field would
|
||||
> not be used.
|
||||
>
|
||||
> This commit updates the truncation of `Line` to depend both on the
|
||||
> individual `Line::alignment`, and on any alignment inherited from the
|
||||
> parent's `Text::alignment`.
|
||||
|
||||
- [611086e](https://github.com/ratatui/ratatui/commit/611086eba4dc07dcef89502a3bedfc28015b879f) *(uncategorized)* Sparkline docs / doc tests by @joshka in [#1437](https://github.com/ratatui/ratatui/pull/1437)
|
||||
|
||||
- [b9653ba](https://github.com/ratatui/ratatui/commit/b9653ba05a468d3843499d8abd243158df823f82) *(uncategorized)* Prevent calender render panic when terminal height is small by @adrodgers in [#1380](https://github.com/ratatui/ratatui/pull/1380)
|
||||
>
|
||||
> Fixes:#1379
|
||||
|
||||
- [da821b4](https://github.com/ratatui/ratatui/commit/da821b431edd656973b4480d3d4f22e7eea6d369) *(uncategorized)* Clippy lints from rust 1.81.0 by @fujiapple852 in [#1356](https://github.com/ratatui/ratatui/pull/1356)
|
||||
|
||||
- [68886d1](https://github.com/ratatui/ratatui/commit/68886d1787b8e07d307dda4f36342d51d650345b) *(uncategorized)* Add `unstable-backend-writer` feature by @Patryk27 in [#1352](https://github.com/ratatui/ratatui/pull/1352)
|
||||
>
|
||||
> https://github.com/ratatui/ratatui/pull/991 created a new unstable
|
||||
> feature, but forgot to add it to Cargo.toml, making it impossible to use
|
||||
> on newer versions of rustc - this commit fixes it.
|
||||
|
||||
### Refactor
|
||||
|
||||
- [6db16d6](https://github.com/ratatui/ratatui/commit/6db16d67fc3cc97f1e5bd4b7df02ce9f00756a55) *(color)* Use palette types for Hsl/Hsluv conversions by @orhun in [#1418](https://github.com/ratatui/ratatui/pull/1418) [**breaking**]
|
||||
>
|
||||
> BREAKING-CHANGE:Previously `Color::from_hsl` accepted components
|
||||
> as individual f64 parameters. It now accepts a single `palette::Hsl`
|
||||
> value
|
||||
> and is gated behind a `palette` feature flag.
|
||||
>
|
||||
> ```diff
|
||||
> - Color::from_hsl(360.0, 100.0, 100.0)
|
||||
> + Color::from_hsl(Hsl::new(360.0, 100.0, 100.0))
|
||||
> ```
|
||||
>
|
||||
> Fixes:<https://github.com/ratatui/ratatui/issues/1414>
|
||||
>
|
||||
> ---------
|
||||
|
||||
- [edcdc8a](https://github.com/ratatui/ratatui/commit/edcdc8a8147a2f450d2c871b19da6d6383fd5497) *(layout)* Rename element to segment in layout by @kdheepak in [#1397](https://github.com/ratatui/ratatui/pull/1397)
|
||||
|
||||
> This PR renames `element` to `segment` in a couple of functions in the
|
||||
> layout calculations for clarity. `element` can refer to `segment`s or
|
||||
> `spacer`s and functions that take only `segment`s should use `segment`
|
||||
> as the variable names.
|
||||
|
||||
- [1153a9e](https://github.com/ratatui/ratatui/commit/1153a9ebaf0b98c45982002a659cb718e3c1d137) *(uncategorized)* Consistent result expected in layout tests by @farmeroy in [#1406](https://github.com/ratatui/ratatui/pull/1406)
|
||||
>
|
||||
> Fixes #1399
|
||||
> I've looked through all the `assert_eq` and made sure that they follow
|
||||
> the `expected, result` pattern. I wasn't sure if it was desired to
|
||||
> actually pass result and expected as variables to the assert_eq
|
||||
> statements, so I've left everything that seems to have followed the
|
||||
> pattern as is.
|
||||
|
||||
- [20c88aa](https://github.com/ratatui/ratatui/commit/20c88aaa5b9eb011a52240eab5edc1a8db23157a) *(uncategorized)* Avoid unneeded allocations by @mo8it in [#1345](https://github.com/ratatui/ratatui/pull/1345)
|
||||
|
||||
### Documentation
|
||||
|
||||
- [b13e2f9](https://github.com/ratatui/ratatui/commit/b13e2f94733afccfe02275fca263bde1dc532d2f) *(backend)* Added link to stdio FAQ by @Valentin271 in [#1349](https://github.com/ratatui/ratatui/pull/1349)
|
||||
|
||||
- [b88717b](https://github.com/ratatui/ratatui/commit/b88717b65f7f89276edd855c4a3f9da2eda44361) *(constraint)* Add note about percentages by @joshka in [#1368](https://github.com/ratatui/ratatui/pull/1368)
|
||||
|
||||
- [381ec75](https://github.com/ratatui/ratatui/commit/381ec75329866b3c1256113d1cb7716206b79fb7) *(readme)* Reduce the length by @joshka in [#1431](https://github.com/ratatui/ratatui/pull/1431)
|
||||
|
||||
> Motivation for this is that there's a bunch of stuff at the bottom of the Readme that we don't really keep up to date. Instead it's better to link to the places that we do keep this info.
|
||||
|
||||
- [4728f0e](https://github.com/ratatui/ratatui/commit/4728f0e68b41eabb7d4ebd041fd5a85a0e794287) *(uncategorized)* Tweak readme by @joshka in [#1419](https://github.com/ratatui/ratatui/pull/1419)
|
||||
>
|
||||
> Fixes:<https://github.com/ratatui/ratatui/issues/1417>
|
||||
|
||||
- [4069aa8](https://github.com/ratatui/ratatui/commit/4069aa82745585f53b4b3376af589bb1b6108427) *(uncategorized)* Fix missing breaking changes link by @joshka in [#1416](https://github.com/ratatui/ratatui/pull/1416)
|
||||
|
||||
- [870bc6a](https://github.com/ratatui/ratatui/commit/870bc6a64a680e9209d30e67e2e1f4e50a10a4bb) *(uncategorized)* Use `Frame::area()` instead of `size()` in examples by @hosseinnedaee in [#1361](https://github.com/ratatui/ratatui/pull/1361)
|
||||
>
|
||||
> `Frame::size()` is deprecated
|
||||
|
||||
### Performance
|
||||
|
||||
- [8db7a9a](https://github.com/ratatui/ratatui/commit/8db7a9a44a2358315dedaee3e7a2cb1a44ae1e58) *(uncategorized)* Implement size hints for `Rect` iterators by @airblast-dev in [#1420](https://github.com/ratatui/ratatui/pull/1420)
|
||||
|
||||
### Styling
|
||||
|
||||
- [e02947b](https://github.com/ratatui/ratatui/commit/e02947be6185643f906a97c453540676eade3f38) *(example)* Update panic message in minimal template by @orhun in [#1344](https://github.com/ratatui/ratatui/pull/1344)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [67c0ea2](https://github.com/ratatui/ratatui/commit/67c0ea243b5eb08159e41f922067247984902c1a) *(block)* Deprecate block::Title by @joshka in [#1372](https://github.com/ratatui/ratatui/pull/1372)
|
||||
>
|
||||
> `ratatui::widgets::block::Title` is deprecated in favor of using `Line`
|
||||
> to represent titles.
|
||||
> This removes an unnecessary layer of wrapping (string -> Span -> Line ->
|
||||
> Title).
|
||||
>
|
||||
> This struct will be removed in a future release of Ratatui (likely
|
||||
> 0.31).
|
||||
> For more information see:
|
||||
>
|
||||
> <https://github.com/ratatui/ratatui/issues/738>
|
||||
>
|
||||
> To update your code:
|
||||
> ```rust
|
||||
>
|
||||
> Block::new().title(Title::from("foo"));
|
||||
> // becomes any of
|
||||
>
|
||||
> Block::new().title("foo");
|
||||
>
|
||||
> Block::new().title(Line::from("foo"));
|
||||
>
|
||||
> Block::new().title(Title::from("foo").position(Position::TOP));
|
||||
> // becomes any of
|
||||
>
|
||||
> Block::new().title_top("foo");
|
||||
>
|
||||
> Block::new().title_top(Line::from("foo"));
|
||||
>
|
||||
> Block::new().title(Title::from("foo").position(Position::BOTTOM));
|
||||
> // becomes any of
|
||||
>
|
||||
> Block::new().title_bottom("foo");
|
||||
>
|
||||
> Block::new().title_bottom(Line::from("foo"));
|
||||
> ```
|
||||
|
||||
- [6515097](https://github.com/ratatui/ratatui/commit/6515097434a10c08276b58f0cd10b9301b44e9fe) *(cargo)* Check in Cargo.lock by @joshka in [#1434](https://github.com/ratatui/ratatui/pull/1434)
|
||||
|
||||
> When kept up to date, this makes it possible to build any git version
|
||||
> with the same versions of crates that were used for any version, without
|
||||
> it, you can only use the current versions. This makes bugs in semver
|
||||
> compatible code difficult to detect.
|
||||
>
|
||||
> The Cargo.lock file is not used by downstream consumers of the crate, so
|
||||
> it is safe to include it in the repository (and recommended by the Rust
|
||||
> docs).
|
||||
>
|
||||
> See:- https://doc.rust-lang.org/cargo/faq.html#why-have-cargolock-in-version-control
|
||||
> - https://blog.rust-lang.org/2023/08/29/committing-lockfiles.html
|
||||
> - https://github.com/rust-lang/cargo/issues/8728
|
||||
|
||||
- [c777beb](https://github.com/ratatui/ratatui/commit/c777beb658ebab26890b52cbda8df5d945525221) *(ci)* Bump git-cliff-action to v4 by @orhun in [#1350](https://github.com/ratatui/ratatui/pull/1350)
|
||||
>
|
||||
> See:https://github.com/orhun/git-cliff-action/releases/tag/v4.0.0
|
||||
|
||||
- [69e0cd2](https://github.com/ratatui/ratatui/commit/69e0cd2fc4b126870b3381704260271904996c8f) *(deny)* Allow Zlib license in cargo-deny configuration by @orhun in [#1411](https://github.com/ratatui/ratatui/pull/1411)
|
||||
|
||||
- [bc10af5](https://github.com/ratatui/ratatui/commit/bc10af5931d1c1ec58a4181c01807ed3c52051c6) *(style)* Make Debug output for Text/Line/Span/Style more concise by @joshka in [#1383](https://github.com/ratatui/ratatui/pull/1383)
|
||||
>
|
||||
> Given:```rust
|
||||
>
|
||||
> Text::from_iter([
|
||||
> Line::from("without line fields"),
|
||||
> Line::from("with line fields").bold().centered(),
|
||||
> Line::from_iter([
|
||||
> Span::from("without span fields"),
|
||||
> Span::from("with span fields")
|
||||
> .green()
|
||||
> .on_black()
|
||||
> .italic()
|
||||
> .not_dim(),
|
||||
> ]),
|
||||
> ])
|
||||
> ```
|
||||
>
|
||||
> Debug:```
|
||||
> Text [Line [Span("without line fields")], Line { style: Style::new().add_modifier(Modifier::BOLD), alignment: Some(Center), spans: [Span("with line fields")] }, Line [Span("without span fields"), Span { style: Style::new().green().on_black().add_modifier(Modifier::ITALIC).remove_modifier(Modifier::DIM), content: "with span fields" }]]
|
||||
> ```
|
||||
>
|
||||
> Fixes: https://github.com/ratatui/ratatui/issues/1382
|
||||
>
|
||||
> ---------
|
||||
|
||||
- [f6f7794](https://github.com/ratatui/ratatui/commit/f6f7794dd782d20cd41875c0578ffc4331692c1e) *(uncategorized)* Remove leftover prelude refs / glob imports from example code by @joshka in [#1430](https://github.com/ratatui/ratatui/pull/1430)
|
||||
>
|
||||
> Fixes:<https://github.com/ratatui/ratatui/issues/1150>
|
||||
|
||||
- [9fd1bee](https://github.com/ratatui/ratatui/commit/9fd1beedb25938bcc9565a52f1104ed45636c2dd) *(uncategorized)* Make Positions iterator fields private by @joshka in [#1424](https://github.com/ratatui/ratatui/pull/1424) [**breaking**]
|
||||
>
|
||||
> BREAKING CHANGE:The Rect Positions iterator no longer has public
|
||||
> fields. The `rect` and `current_position` fields have been made private
|
||||
> as they were not intended to be accessed directly.
|
||||
|
||||
- [c32baa7](https://github.com/ratatui/ratatui/commit/c32baa7cd8a29a370a71da07ee02cf32125c9bcf) *(uncategorized)* Add benchmark for `Table` by @airblast-dev in [#1408](https://github.com/ratatui/ratatui/pull/1408)
|
||||
|
||||
- [5ad623c](https://github.com/ratatui/ratatui/commit/5ad623c29b8f0b50fad742448902245f353ef19e) *(uncategorized)* Remove usage of prelude by @joshka in [#1390](https://github.com/ratatui/ratatui/pull/1390)
|
||||
|
||||
> This helps make the doc examples more explicit about what is being used.
|
||||
> It will also makes it a bit easier to do future refactoring of Ratatui,
|
||||
> into several crates, as the ambiguity of where types are coming from
|
||||
> will be reduced.
|
||||
>
|
||||
> Additionally, several doc examples have been simplified to use Stylize,
|
||||
> and necessary imports are no longer hidden.
|
||||
>
|
||||
> This doesn't remove the prelude. Only the internal usages.
|
||||
|
||||
- [f4880b4](https://github.com/ratatui/ratatui/commit/cc7497532ac50e7e15e8ee8ff506f4689c396f50) *(deps)* Pin unicode-width to 0.2.0 by @orhun in [#1403](https://github.com/ratatui/ratatui/pull/1403) [**breaking**]
|
||||
|
||||
> We pin unicode-width to avoid breaking applications when there are breaking changes in the library.
|
||||
>
|
||||
> Discussion in [#1271](https://github.com/ratatui/ratatui/pull/1271)
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- [5635b93](https://github.com/ratatui/ratatui/commit/5635b930c7196ef8f12824341a7bd8b7323aabcd) *(uncategorized)* Add cargo-machete and remove unused deps by @Veetaha in [#1362](https://github.com/ratatui/ratatui/pull/1362)
|
||||
>
|
||||
> https://github.com/bnjbvr/cargo-machete
|
||||
|
||||
### New Contributors
|
||||
|
||||
* @roberth made their first contribution in [#1427](https://github.com/ratatui/ratatui/pull/1427)
|
||||
* @du-ob made their first contribution in [#1333](https://github.com/ratatui/ratatui/pull/1333)
|
||||
* @farmeroy made their first contribution in [#1406](https://github.com/ratatui/ratatui/pull/1406)
|
||||
* @adrodgers made their first contribution in [#1380](https://github.com/ratatui/ratatui/pull/1380)
|
||||
* @Veetaha made their first contribution in [#1362](https://github.com/ratatui/ratatui/pull/1362)
|
||||
* @hosseinnedaee made their first contribution in [#1361](https://github.com/ratatui/ratatui/pull/1361)
|
||||
* @Patryk27 made their first contribution in [#1352](https://github.com/ratatui/ratatui/pull/1352)
|
||||
|
||||
**Full Changelog**: https://github.com/ratatui/ratatui/compare/v0.28.1...v0.29.0
|
||||
|
||||
## [v0.28.1](https://github.com/ratatui/ratatui/releases/tag/v0.28.1) - 2024-08-25
|
||||
|
||||
### Features
|
||||
@@ -4625,7 +5175,7 @@ Also, we created various tutorials and walkthroughs in [Ratatui Book](https://gi
|
||||
|
||||
```text
|
||||
The `Spans` type (plural, not singular) was replaced with a more ergonomic `Line` type
|
||||
in Ratatui v0.21.0 and marked deprecated byt left for backwards compatibility. This is now
|
||||
in Ratatui v0.21.0 and marked deprecated but left for backwards compatibility. This is now
|
||||
removed.
|
||||
|
||||
- `Line` replaces `Spans`
|
||||
|
||||
@@ -31,7 +31,7 @@ guarantee that the behavior is unchanged.
|
||||
|
||||
### Code formatting
|
||||
|
||||
Run `cargo make format` before committing to ensure that code is consistently formatted with
|
||||
Run `cargo xtask format` before committing to ensure that code is consistently formatted with
|
||||
rustfmt. Configuration is in [`rustfmt.toml`](./rustfmt.toml).
|
||||
|
||||
### Search `tui-rs` for similar work
|
||||
@@ -56,7 +56,7 @@ documented.
|
||||
|
||||
### Run CI tests before pushing a PR
|
||||
|
||||
Running `cargo make ci` before pushing will perform the same checks that we do in the CI process.
|
||||
Running `cargo xtask ci` before pushing will perform the same checks that we do in the CI process.
|
||||
It's not mandatory to do this before pushing, however it may save you time to do so instead of
|
||||
waiting for GitHub to run the checks.
|
||||
|
||||
@@ -71,17 +71,17 @@ in GitHub docs.
|
||||
|
||||
### Setup
|
||||
|
||||
Clone the repo and build it using [cargo-make](https://sagiegurari.github.io/cargo-make/)
|
||||
TL;DR: Clone the repo and build it using `cargo xtask`.
|
||||
|
||||
Ratatui is an ordinary Rust project where common tasks are managed with
|
||||
[cargo-make](https://github.com/sagiegurari/cargo-make/). It wraps common `cargo` commands with sane
|
||||
[cargo-xtask](https://github.com/matklad/cargo-xtask). It wraps common `cargo` commands with sane
|
||||
defaults depending on your platform of choice. Building the project should be as easy as running
|
||||
`cargo make build`.
|
||||
`cargo xtask build`.
|
||||
|
||||
```shell
|
||||
git clone https://github.com/ratatui/ratatui.git
|
||||
cd ratatui
|
||||
cargo make build
|
||||
cargo xtask build
|
||||
```
|
||||
|
||||
### Tests
|
||||
@@ -182,7 +182,7 @@ We use GitHub Actions for the CI where we perform the following checks:
|
||||
- The code should conform to the default format enforced by `rustfmt`.
|
||||
- The code should not contain common style issues `clippy`.
|
||||
|
||||
You can also check most of those things yourself locally using `cargo make ci` which will offer you
|
||||
You can also check most of those things yourself locally using `cargo xtask ci` which will offer you
|
||||
a shorter feedback loop than pushing to github.
|
||||
|
||||
## Relationship with `tui-rs`
|
||||
|
||||
3953
Cargo.lock
generated
Normal file
3953
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
401
Cargo.toml
401
Cargo.toml
@@ -1,8 +1,18 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.28.1" # crate version
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui", "ratatui-*", "xtask"]
|
||||
default-members = [
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
# this is not included as it doesn't compile on windows
|
||||
# "ratatui-termion",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Florian Dehau <work@fdehau.com>", "The Ratatui Developers"]
|
||||
description = "A library that's all about cooking up terminal user interfaces"
|
||||
documentation = "https://docs.rs/ratatui/latest/ratatui/"
|
||||
repository = "https://github.com/ratatui/ratatui"
|
||||
homepage = "https://ratatui.rs"
|
||||
@@ -10,371 +20,36 @@ keywords = ["tui", "terminal", "dashboard"]
|
||||
categories = ["command-line-interface"]
|
||||
readme = "README.md"
|
||||
license = "MIT"
|
||||
exclude = [
|
||||
"assets/*",
|
||||
".github",
|
||||
"Makefile.toml",
|
||||
"CONTRIBUTING.md",
|
||||
"*.log",
|
||||
"tags",
|
||||
]
|
||||
exclude = ["assets/*", ".github", "Makefile.toml", "CONTRIBUTING.md", "*.log", "tags"]
|
||||
edition = "2021"
|
||||
rust-version = "1.74.0"
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.8.0"
|
||||
crossterm = { version = "0.28.1", optional = true }
|
||||
document-features = { version = "0.2.7", optional = true }
|
||||
instability = "0.3.1"
|
||||
itertools = "0.13"
|
||||
lru = "0.12.0"
|
||||
paste = "1.0.2"
|
||||
palette = { version = "0.7.6", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
[workspace.dependencies]
|
||||
bitflags = "2.6.0"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
document-features = "0.2.7"
|
||||
indoc = "2.0.5"
|
||||
instability = "0.3.3"
|
||||
itertools = "0.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.0" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.0" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.0" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.0" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.0" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.0" }
|
||||
rstest = "0.23.0"
|
||||
serde = { version = "1.0.215", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
termwiz = { version = "0.22.0", optional = true }
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation = "1.10"
|
||||
unicode-truncate = "1"
|
||||
unicode-width = "0.1.13"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
# termion is not supported on Windows
|
||||
termion = { version = "4.0.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
argh = "0.1.12"
|
||||
color-eyre = "0.6.2"
|
||||
criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
crossterm = { version = "0.28.1", features = ["event-stream"] }
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
octocrab = "0.39.0"
|
||||
pretty_assertions = "1.4.0"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
rstest = "0.22.0"
|
||||
serde_json = "1.0.109"
|
||||
tokio = { version = "1.39.2", features = [
|
||||
"rt",
|
||||
"macros",
|
||||
"time",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
or_fun_call = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
[features]
|
||||
#! The crate provides a set of optional features that can be enabled in your `cargo.toml` file.
|
||||
#!
|
||||
## By default, we enable the crossterm backend as this is a reasonable choice for most applications
|
||||
## as it is supported on Linux/Mac/Windows systems. We also enable the `underline-color` feature
|
||||
## which allows you to set the underline color of text.
|
||||
default = ["crossterm", "underline-color"]
|
||||
#! Generally an application will only use one backend, so you should only enable one of the following features:
|
||||
## enables the [`CrosstermBackend`](backend::CrosstermBackend) backend and adds a dependency on [`crossterm`].
|
||||
crossterm = ["dep:crossterm"]
|
||||
## enables the [`TermionBackend`](backend::TermionBackend) backend and adds a dependency on [`termion`].
|
||||
termion = ["dep:termion"]
|
||||
## enables the [`TermwizBackend`](backend::TermwizBackend) backend and adds a dependency on [`termwiz`].
|
||||
termwiz = ["dep:termwiz"]
|
||||
|
||||
#! The following optional features are available for all backends:
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
## enables the [`border!`] macro.
|
||||
macros = []
|
||||
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["dep:palette"]
|
||||
|
||||
## enables all widgets.
|
||||
all-widgets = ["widget-calendar"]
|
||||
|
||||
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
|
||||
#! dependencies. The available features are:
|
||||
## enables the [`calendar`](widgets::calendar) widget module and adds a dependency on [`time`].
|
||||
widget-calendar = ["dep:time"]
|
||||
|
||||
#! The following optional features are only available for some backends:
|
||||
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
|
||||
## and is not supported on Windows 7.
|
||||
underline-color = ["dep:crossterm"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = [
|
||||
"unstable-rendered-line-info",
|
||||
"unstable-widget-ref",
|
||||
"unstable-backend-writer",
|
||||
]
|
||||
|
||||
## Enables the [`Paragraph::line_count`](widgets::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](widgets::Paragraph::line_width) methods
|
||||
## which are experimental and may change in the future.
|
||||
## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
## Enables the [`WidgetRef`](widgets::WidgetRef) and [`StatefulWidgetRef`](widgets::StatefulWidgetRef) traits which are experimental and may change in
|
||||
## the future.
|
||||
unstable-widget-ref = []
|
||||
|
||||
## Enables getting access to backends' writers.
|
||||
unstable-backend-writer = []
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
# see https://doc.rust-lang.org/nightly/rustdoc/scraped-examples.html
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
termwiz = { version = "0.22.0" }
|
||||
unicode-segmentation = "1.12.0"
|
||||
# See <https://github.com/ratatui/ratatui/issues/1271> for information about why we pin unicode-width
|
||||
unicode-width = "=0.2.0"
|
||||
termion = "4.0.0"
|
||||
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
||||
[[bench]]
|
||||
name = "main"
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "async"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "barchart-grouped"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "calendar"
|
||||
required-features = ["crossterm", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "canvas"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
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 = "colors_rgb"
|
||||
required-features = ["crossterm", "palette"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraint-explorer"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "constraints"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "custom_widget"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "demo"
|
||||
# this runs for all of the terminal backends, so it can't be built using --all-features or scraped
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "demo2"
|
||||
required-features = ["crossterm", "palette", "widget-calendar"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "docsrs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[example]]
|
||||
name = "flex"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "inline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "layout"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "line_gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hyperlink"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "list"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "minimal"
|
||||
required-features = ["crossterm"]
|
||||
# prefer to show the more featureful examples in the docs
|
||||
doc-scrape-examples = false
|
||||
|
||||
[[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"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "paragraph"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "popup"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "ratatui-logo"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbar"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "sparkline"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "table"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tabs"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "tracing"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "user_input"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "widget_impl"
|
||||
required-features = ["crossterm", "unstable-widget-ref"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[test]]
|
||||
name = "state_serde"
|
||||
required-features = ["serde"]
|
||||
|
||||
154
Makefile.toml
154
Makefile.toml
@@ -1,154 +0,0 @@
|
||||
# configuration for https://github.com/sagiegurari/cargo-make
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
# all features except the backend ones
|
||||
NON_BACKEND_FEATURES = "all-widgets,macros,serde"
|
||||
|
||||
[tasks.default]
|
||||
alias = "ci"
|
||||
|
||||
[tasks.ci]
|
||||
description = "Run continuous integration tasks"
|
||||
dependencies = ["lint", "clippy", "check", "test"]
|
||||
|
||||
[tasks.lint]
|
||||
description = "Lint code style (formatting, typos, docs, markdown)"
|
||||
dependencies = ["lint-format", "lint-typos", "lint-docs"]
|
||||
|
||||
[tasks.lint-format]
|
||||
description = "Lint code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all", "--check"]
|
||||
|
||||
[tasks.format]
|
||||
description = "Fix code formatting"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = ["fmt", "--all"]
|
||||
|
||||
[tasks.lint-typos]
|
||||
description = "Run typo checks"
|
||||
install_crate = { crate_name = "typos-cli", binary = "typos", test_arg = "--version" }
|
||||
command = "typos"
|
||||
|
||||
[tasks.lint-docs]
|
||||
description = "Check documentation for errors and warnings"
|
||||
toolchain = "nightly"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"rustdoc",
|
||||
"--all-features",
|
||||
"--",
|
||||
"-Zunstable-options",
|
||||
"--check",
|
||||
"-Dwarnings",
|
||||
]
|
||||
|
||||
[tasks.lint-markdown]
|
||||
description = "Check markdown files for errors and warnings"
|
||||
command = "markdownlint-cli2"
|
||||
args = ["**/*.md", "!target"]
|
||||
|
||||
[tasks.check]
|
||||
description = "Check code for errors and warnings"
|
||||
command = "cargo"
|
||||
args = ["check", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.build]
|
||||
description = "Compile the project"
|
||||
command = "cargo"
|
||||
args = ["build", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.clippy]
|
||||
description = "Run Clippy for linting"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"clippy",
|
||||
"--all-targets",
|
||||
"--all-features",
|
||||
"--tests",
|
||||
"--benches",
|
||||
"--",
|
||||
"-D",
|
||||
"warnings",
|
||||
]
|
||||
|
||||
[tasks.install-nextest]
|
||||
description = "Install cargo-nextest"
|
||||
install_crate = { crate_name = "cargo-nextest", binary = "cargo-nextest", test_arg = "--help" }
|
||||
|
||||
[tasks.test]
|
||||
description = "Run tests"
|
||||
run_task = { name = ["test-lib", "test-doc"] }
|
||||
|
||||
[tasks.test-lib]
|
||||
description = "Run default tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = ["nextest", "run", "--all-targets", "--all-features"]
|
||||
|
||||
[tasks.test-doc]
|
||||
description = "Run documentation tests"
|
||||
command = "cargo"
|
||||
args = ["test", "--doc", "--all-features"]
|
||||
|
||||
[tasks.test-backend]
|
||||
# takes a command line parameter to specify the backend to test (e.g. "crossterm")
|
||||
description = "Run backend-specific tests"
|
||||
dependencies = ["install-nextest"]
|
||||
command = "cargo"
|
||||
args = [
|
||||
"nextest",
|
||||
"run",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"${NON_BACKEND_FEATURES},${@}",
|
||||
]
|
||||
|
||||
[tasks.coverage]
|
||||
description = "Generate code coverage report"
|
||||
command = "cargo"
|
||||
args = [
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--all-features",
|
||||
]
|
||||
|
||||
[tasks.run-example]
|
||||
private = true
|
||||
condition = { env_set = ["TUI_EXAMPLE_NAME"] }
|
||||
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
|
||||
files = glob_array ./examples/*.rs
|
||||
for file in ${files}
|
||||
name = basename ${file}
|
||||
name = substring ${name} -3
|
||||
set_env TUI_EXAMPLE_NAME ${name}
|
||||
cm_run_task run-example
|
||||
end
|
||||
'''
|
||||
319
README.md
319
README.md
@@ -2,14 +2,13 @@
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [Ratatui](#ratatui)
|
||||
- [Installation](#installation)
|
||||
- [Introduction](#introduction)
|
||||
- [Quick Start](#quickstart)
|
||||
- [Other documentation](#other-documentation)
|
||||
- [Introduction](#introduction)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Initialize and restore the terminal](#initialize-and-restore-the-terminal)
|
||||
- [Drawing the UI](#drawing-the-ui)
|
||||
- [Handling events](#handling-events)
|
||||
- [Example](#example)
|
||||
- [Layout](#layout)
|
||||
- [Text and styling](#text-and-styling)
|
||||
- [Status of this fork](#status-of-this-fork)
|
||||
@@ -45,28 +44,42 @@ Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]
|
||||
lightweight library that provides a set of widgets and utilities to build complex Rust TUIs.
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development.
|
||||
|
||||
## Installation
|
||||
## Quickstart
|
||||
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
Add `ratatui` and `crossterm` as dependencies to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo add ratatui
|
||||
cargo add ratatui crossterm
|
||||
```
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
Then you can create a simple "Hello World" application:
|
||||
|
||||
## Introduction
|
||||
```rust
|
||||
use crossterm::event::{self, Event};
|
||||
use ratatui::{text::Text, Frame};
|
||||
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
fn main() {
|
||||
let mut terminal = ratatui::init();
|
||||
loop {
|
||||
terminal.draw(draw).expect("failed to draw frame");
|
||||
if matches!(event::read().expect("failed to read event"), Event::Key(_)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
ratatui::restore();
|
||||
}
|
||||
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
fn draw(frame: &mut Frame) {
|
||||
let text = Text::raw("Hello World!");
|
||||
frame.render_widget(text, frame.area());
|
||||
}
|
||||
```
|
||||
|
||||
The full code for this example which contains a little more detail is in the [Examples]
|
||||
directory. For more guidance on different ways to structure your application see the
|
||||
[Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
various [Examples]. There are also several starter templates available in the [templates]
|
||||
repository.
|
||||
|
||||
## Other documentation
|
||||
|
||||
@@ -78,46 +91,82 @@ terminal user interfaces and showcases the features of Ratatui, along with a hel
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
|
||||
## Quickstart
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
The following example demonstrates the minimal amount of code necessary to setup a terminal and
|
||||
render "Hello World!". The full code for this example which contains a little more detail is in
|
||||
the [Examples] directory. For more guidance on different ways to structure your application see
|
||||
the [Application Patterns] and [Hello World tutorial] sections in the [Ratatui Website] and the
|
||||
various [Examples]. There are also several starter templates available in the [templates]
|
||||
repository.
|
||||
## Introduction
|
||||
|
||||
Ratatui is based on the principle of immediate rendering with intermediate buffers. This means
|
||||
that for each frame, your app must render all widgets that are supposed to be part of the UI.
|
||||
This is in contrast to the retained mode style of rendering where widgets are updated and then
|
||||
automatically redrawn on the next frame. See the [Rendering] section of the [Ratatui Website]
|
||||
for more info.
|
||||
|
||||
Ratatui uses [Crossterm] by default as it works on most platforms. See the [Installation]
|
||||
section of the [Ratatui Website] for more details on how to use other backends ([Termion] /
|
||||
[Termwiz]).
|
||||
|
||||
Every application built with `ratatui` needs to implement the following steps:
|
||||
|
||||
- Initialize the terminal
|
||||
- A main loop to:
|
||||
- Handle input events
|
||||
- Draw the UI
|
||||
- A main loop that:
|
||||
- Draws the UI
|
||||
- Handles input events
|
||||
- Restore the terminal state
|
||||
|
||||
The library contains a [`prelude`] module that re-exports the most commonly used traits and
|
||||
types for convenience. Most examples in the documentation will use this instead of showing the
|
||||
full path of each type.
|
||||
|
||||
### Initialize and restore the terminal
|
||||
|
||||
The [`Terminal`] type is the main entry point for any Ratatui application. It is a light
|
||||
abstraction over a choice of [`Backend`] implementations that provides functionality to draw
|
||||
each frame, clear the screen, hide the cursor, etc. It is parametrized over any type that
|
||||
implements the [`Backend`] trait which has implementations for [Crossterm], [Termion] and
|
||||
[Termwiz].
|
||||
The [`Terminal`] type is the main entry point for any Ratatui application. It is generic over a
|
||||
a choice of [`Backend`] implementations that each provide functionality to draw frames, clear
|
||||
the screen, hide the cursor, etc. There are backend implementations for [Crossterm], [Termion]
|
||||
and [Termwiz].
|
||||
|
||||
Most applications should enter the Alternate Screen when starting and leave it when exiting and
|
||||
also enable raw mode to disable line buffering and enable reading key events. See the [`backend`
|
||||
module] and the [Backends] section of the [Ratatui Website] for more info.
|
||||
The simplest way to initialize the terminal is to use the [`init`] function which returns a
|
||||
[`DefaultTerminal`] instance with the default options, enters the Alternate Screen and Raw mode
|
||||
and sets up a panic hook that restores the terminal in case of panic. This instance can then be
|
||||
used to draw frames and interact with the terminal state. (The [`DefaultTerminal`] instance is a
|
||||
type alias for a terminal with the [`crossterm`] backend.) The [`restore`] function restores the
|
||||
terminal to its original state.
|
||||
|
||||
```rust
|
||||
fn main() -> std::io::Result<()> {
|
||||
let mut terminal = ratatui::init();
|
||||
let result = run(&mut terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
See the [`backend` module] and the [Backends] section of the [Ratatui Website] for more info on
|
||||
the alternate screen and raw mode.
|
||||
|
||||
### Drawing the UI
|
||||
|
||||
The drawing logic is delegated to a closure that takes a [`Frame`] instance as argument. The
|
||||
[`Frame`] provides the size of the area to draw to and allows the app to render any [`Widget`]
|
||||
using the provided [`render_widget`] method. After this closure returns, a diff is performed and
|
||||
only the changes are drawn to the terminal. See the [Widgets] section of the [Ratatui Website]
|
||||
for more info.
|
||||
Drawing the UI is done by calling the [`Terminal::draw`] method on the terminal instance. This
|
||||
method takes a closure that is called with a [`Frame`] instance. The [`Frame`] provides the size
|
||||
of the area to draw to and allows the app to render any [`Widget`] using the provided
|
||||
[`render_widget`] method. After this closure returns, a diff is performed and only the changes
|
||||
are drawn to the terminal. See the [Widgets] section of the [Ratatui Website] for more info.
|
||||
|
||||
The closure passed to the [`Terminal::draw`] method should handle the rendering of a full frame.
|
||||
|
||||
```rust
|
||||
use ratatui::{widgets::Paragraph, Frame};
|
||||
|
||||
fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
|
||||
loop {
|
||||
terminal.draw(|frame| draw(frame))?;
|
||||
if handle_events()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(frame: &mut Frame) {
|
||||
let text = Paragraph::new("Hello World!");
|
||||
frame.render_widget(text, frame.area());
|
||||
}
|
||||
```
|
||||
|
||||
### Handling events
|
||||
|
||||
@@ -126,63 +175,23 @@ calling backend library methods directly. See the [Handling Events] section of t
|
||||
Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
[`crossterm::event`] module to handle events.
|
||||
|
||||
### Example
|
||||
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
fn handle_events() -> std::io::Result<bool> {
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => match key.code {
|
||||
KeyCode::Char('q') => return Ok(true),
|
||||
// handle other key events
|
||||
_ => {}
|
||||
},
|
||||
ExecutableCommand,
|
||||
},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame, Terminal,
|
||||
};
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
|
||||
|
||||
let mut should_quit = false;
|
||||
while !should_quit {
|
||||
terminal.draw(ui)?;
|
||||
should_quit = handle_events()?;
|
||||
}
|
||||
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events() -> io::Result<bool> {
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == event::KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
// handle other events
|
||||
_ => {}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.area(),
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-hello]
|
||||
|
||||
## Layout
|
||||
|
||||
The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
@@ -197,16 +206,13 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let [title_area, main_area, status_area] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(frame.area());
|
||||
let [left_area, right_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(main_area);
|
||||
fn draw(frame: &mut Frame) {
|
||||
use Constraint::{Fill, Length, Min};
|
||||
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [title_area, main_area, status_area] = vertical.areas(frame.area());
|
||||
let horizontal = Layout::horizontal([Fill(1); 2]);
|
||||
let [left_area, right_area] = horizontal.areas(main_area);
|
||||
|
||||
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
@@ -217,7 +223,13 @@ fn ui(frame: &mut Frame) {
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-layout]
|
||||
```text
|
||||
Title Bar───────────────────────────────────
|
||||
┌Left────────────────┐┌Right───────────────┐
|
||||
│ ││ │
|
||||
└────────────────────┘└────────────────────┘
|
||||
Status Bar──────────────────────────────────
|
||||
```
|
||||
|
||||
## Text and styling
|
||||
|
||||
@@ -240,7 +252,7 @@ use ratatui::{
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
fn draw(frame: &mut Frame) {
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.area());
|
||||
|
||||
let line = Line::from(vec![
|
||||
@@ -270,10 +282,6 @@ fn ui(frame: &mut Frame) {
|
||||
}
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
|
||||
![docsrs-styling]
|
||||
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Installation]: https://ratatui.rs/installation/
|
||||
[Rendering]: https://ratatui.rs/concepts/rendering/
|
||||
@@ -285,7 +293,7 @@ Running this example produces the following output:
|
||||
[Layout]: https://ratatui.rs/how-to/layout/
|
||||
[Styling Text]: https://ratatui.rs/how-to/render/style-text/
|
||||
[templates]: https://github.com/ratatui/templates/
|
||||
[Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
[Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
[Report a bug]: https://github.com/ratatui/ratatui/issues/new?labels=bug&projects=&template=bug_report.md
|
||||
[Request a Feature]: https://github.com/ratatui/ratatui/issues/new?labels=enhancement&projects=&template=feature_request.md
|
||||
[Create a Pull Request]: https://github.com/ratatui/ratatui/compare
|
||||
@@ -296,9 +304,6 @@ Running this example produces the following output:
|
||||
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[FOSDEM 2024 talk]: https://www.youtube.com/watch?v=NU0q6NOLJ20
|
||||
[docsrs-hello]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-hello.png?raw=true
|
||||
[docsrs-layout]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-layout.png?raw=true
|
||||
[docsrs-styling]: https://github.com/ratatui/ratatui/blob/c3c3c289b1eb8d562afb1931adb4dc719cd48490/examples/docsrs-styling.png?raw=true
|
||||
[`Frame`]: terminal::Frame
|
||||
[`render_widget`]: terminal::Frame::render_widget
|
||||
[`Widget`]: widgets::Widget
|
||||
@@ -337,94 +342,36 @@ Running this example produces the following output:
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
## Status of this fork
|
||||
|
||||
In response to the original maintainer [**Florian Dehau**](https://github.com/fdehau)'s issue
|
||||
regarding the [future of `tui-rs`](https://github.com/fdehau/tui-rs/issues/654), several members of
|
||||
the community forked the project and created this crate. We look forward to continuing the work
|
||||
started by Florian 🚀
|
||||
## Contributing
|
||||
|
||||
In order to organize ourselves, we currently use a [Discord server](https://discord.gg/pMCEU9hNEj),
|
||||
feel free to join and come chat! There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org).
|
||||
|
||||
While we do utilize Discord for coordinating, it's not essential for contributing. We have recently
|
||||
launched the [Ratatui Forum][Forum], and our primary open-source workflow is centered around GitHub.
|
||||
For bugs and features, we rely on GitHub. Please [Report a bug], [Request a Feature] or [Create a
|
||||
Pull Request].
|
||||
We have also recently launched the [Ratatui Forum][Forum], For bugs and features, we rely on GitHub.
|
||||
Please [Report a bug], [Request a Feature] or [Create a Pull Request].
|
||||
|
||||
Please make sure you read the updated [contributing](./CONTRIBUTING.md) guidelines, especially if
|
||||
you are interested in working on a PR or issue opened in the previous repository.
|
||||
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines, especially if you are
|
||||
interested in working on a PR or issue opened in the previous repository.
|
||||
|
||||
## Widgets
|
||||
## Built with Ratatui
|
||||
|
||||
### Built in
|
||||
|
||||
The library comes with the following
|
||||
[widgets](https://docs.rs/ratatui/latest/ratatui/widgets/index.html):
|
||||
|
||||
- [BarChart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
- [Block](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
- [Calendar](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
- [Canvas](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/struct.Canvas.html) which allows
|
||||
rendering [points, lines, shapes and a world
|
||||
map](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html)
|
||||
- [Chart](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html)
|
||||
- [Clear](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html)
|
||||
- [Gauge](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html)
|
||||
- [List](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html)
|
||||
- [Paragraph](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
- [Scrollbar](https://docs.rs/ratatui/latest/ratatui/widgets/scrollbar/struct.Scrollbar.html)
|
||||
- [Sparkline](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
- [Table](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html)
|
||||
- [Tabs](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html)
|
||||
|
||||
Each widget has an associated example which can be found in the [Examples] folder. Run each example
|
||||
with cargo (e.g. to run the gauge example `cargo run --example gauge`), and quit by pressing `q`.
|
||||
|
||||
You can also run all examples by running `cargo make run-examples` (requires `cargo-make` that can
|
||||
be installed with `cargo install cargo-make`).
|
||||
|
||||
### Third-party libraries, bootstrapping templates and widgets
|
||||
|
||||
- [ansi-to-tui](https://github.com/uttarayan21/ansi-to-tui) — Convert ansi colored text to
|
||||
`ratatui::text::Text`
|
||||
- [color-to-tui](https://github.com/uttarayan21/color-to-tui) — Parse hex colors to
|
||||
`ratatui::style::Color`
|
||||
- [templates](https://github.com/ratatui/templates) — Starter templates for
|
||||
bootstrapping a Rust TUI application with Ratatui & crossterm
|
||||
- [tui-builder](https://github.com/jkelleyrtp/tui-builder) — Batteries-included MVC framework for
|
||||
Tui-rs + Crossterm apps
|
||||
- [tui-clap](https://github.com/kegesch/tui-clap-rs) — Use clap-rs together with Tui-rs
|
||||
- [tui-log](https://github.com/kegesch/tui-log-rs) — Example of how to use logging with Tui-rs
|
||||
- [tui-logger](https://github.com/gin66/tui-logger) — Logger and Widget for Tui-rs
|
||||
- [tui-realm](https://github.com/veeso/tui-realm) — Tui-rs framework to build stateful applications
|
||||
with a React/Elm inspired approach
|
||||
- [tui-realm-treeview](https://github.com/veeso/tui-realm-treeview) — Treeview component for
|
||||
Tui-realm
|
||||
- [tui-rs-tree-widgets](https://github.com/EdJoPaTo/tui-rs-tree-widget) — Widget for tree data
|
||||
structures.
|
||||
- [tui-windows](https://github.com/markatk/tui-windows-rs) — Tui-rs abstraction to handle multiple
|
||||
windows and their rendering
|
||||
- [tui-textarea](https://github.com/rhysd/tui-textarea) — Simple yet powerful multi-line text editor
|
||||
widget supporting several key shortcuts, undo/redo, text search, etc.
|
||||
- [tui-input](https://github.com/sayanarijit/tui-input) — TUI input library supporting multiple
|
||||
backends and tui-rs.
|
||||
- [tui-term](https://github.com/a-kenji/tui-term) — A pseudoterminal widget library
|
||||
that enables the rendering of terminal applications as ratatui widgets.
|
||||
|
||||
## Apps
|
||||
|
||||
Check out [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) for a curated list of
|
||||
awesome apps/libraries built with `ratatui`!
|
||||
Ratatui has a number of built-in [widgets](https://docs.rs/ratatui/latest/ratatui/widgets/), as well
|
||||
as many contributed by external contributors. Check out the [Showcase](https://ratatui.rs/showcase/)
|
||||
section of the website, or the [awesome-ratatui](https://github.com/ratatui/awesome-ratatui) repo
|
||||
for a curated list of awesome apps/libraries built with `ratatui`!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) or
|
||||
[iocraft](https://github.com/ccbrown/iocraft/) for an alternative solutions
|
||||
to build text user interfaces in Rust.
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
None of this could be possible without [**Florian Dehau**](https://github.com/fdehau) who originally
|
||||
created [tui-rs] which inspired many Rust TUIs.
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and ratatui organization.
|
||||
|
||||
|
||||
97
bacon.toml
97
bacon.toml
@@ -8,58 +8,66 @@
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
command = ["cargo", "check", "--all-features"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
command = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"crossterm",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
command = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"termion",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
command = [
|
||||
"cargo",
|
||||
"check",
|
||||
"--all-targets",
|
||||
"--no-default-features",
|
||||
"--features",
|
||||
"termwiz",
|
||||
]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
command = ["cargo", "clippy", "--all-targets"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
command = ["cargo", "test", "--all-features"]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.test-unit]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
command = ["cargo", "test", "--lib", "--all-features"]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"cargo",
|
||||
"+nightly",
|
||||
"doc",
|
||||
"-Zunstable-options",
|
||||
"-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
@@ -69,10 +77,12 @@ need_stdout = false
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"cargo",
|
||||
"+nightly",
|
||||
"doc",
|
||||
"-Zunstable-options",
|
||||
"-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
"--open",
|
||||
]
|
||||
@@ -82,19 +92,34 @@ on_success = "job:doc" # so that we don't open the browser at each change
|
||||
|
||||
[jobs.coverage]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"cargo",
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"cargo",
|
||||
"llvm-cov",
|
||||
"--lcov",
|
||||
"--output-path",
|
||||
"target/lcov.info",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
|
||||
[jobs.hack]
|
||||
command = [
|
||||
"cargo",
|
||||
"hack",
|
||||
"test",
|
||||
"--lib",
|
||||
"--each-feature",
|
||||
# "--all-targets",
|
||||
"--workspace",
|
||||
]
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
@@ -102,7 +127,7 @@ command = [
|
||||
# Shortcuts to internal functions (scrolling, toggling, etc.)
|
||||
# should go in your personal global prefs.toml file instead.
|
||||
[keybindings]
|
||||
# alt-m = "job:my-job"
|
||||
ctrl-h = "job:hack"
|
||||
ctrl-c = "job:check-crossterm"
|
||||
ctrl-t = "job:check-termion"
|
||||
ctrl-w = "job:check-termwiz"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
use criterion::{black_box, criterion_group, BenchmarkId, Criterion};
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
fn rect_rows_benchmark(c: &mut Criterion) {
|
||||
let rect_sizes = vec![
|
||||
Rect::new(0, 0, 1, 16),
|
||||
Rect::new(0, 0, 1, 1024),
|
||||
Rect::new(0, 0, 1, 65535),
|
||||
];
|
||||
let mut group = c.benchmark_group("rect_rows");
|
||||
for rect in rect_sizes {
|
||||
group.bench_with_input(BenchmarkId::new("rows", rect.height), &rect, |b, rect| {
|
||||
b.iter(|| {
|
||||
for row in rect.rows() {
|
||||
// Perform any necessary operations on each row
|
||||
black_box(row);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, rect_rows_benchmark);
|
||||
14
cliff.toml
14
cliff.toml
@@ -30,19 +30,9 @@ body = """
|
||||
{% macro commit(commit) -%}
|
||||
- [{{ commit.id | truncate(length=7, end="") }}]({{ "https://github.com/ratatui/ratatui/commit/" ~ commit.id }}) \
|
||||
*({{commit.scope | default(value = "uncategorized") | lower }})* {{ commit.message | upper_first | trim }}\
|
||||
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}\
|
||||
{% if commit.github.pr_number %} in [#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}){%- endif %}\
|
||||
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}\
|
||||
{% if commit.remote.pr_number %} in [#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}){%- endif %}\
|
||||
{%- if commit.breaking %} [**breaking**]{% endif %}
|
||||
{%- if commit.body %}\n\n{{ commit.body | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- for footer in commit.footers %}\n
|
||||
{%- if footer.token != "Signed-off-by" and footer.token != "Co-authored-by" %}
|
||||
>
|
||||
{{ footer.token | indent(prefix=" > ", first=true, blank=true) }}
|
||||
{{- footer.separator }}
|
||||
{{- footer.value| indent(prefix=" > ", first=false, blank=true) }}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
{% endmacro -%}
|
||||
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
|
||||
@@ -3,9 +3,15 @@ avoid-breaking-exported-api = false
|
||||
# https://rust-lang.github.io/rust-clippy/master/index.html#/multiple_crate_versions
|
||||
# ratatui -> bitflags v2.3
|
||||
# termwiz -> wezterm-blob-leases -> mac_address -> nix -> bitflags v1.3.2
|
||||
# (also, memoffset, syn, nix, strsim, windows-sys
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"memoffset",
|
||||
"nix",
|
||||
"strsim",
|
||||
"syn",
|
||||
"windows-sys",
|
||||
"windows-targets",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
@@ -14,4 +20,5 @@ allowed-duplicate-crates = [
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
@@ -11,6 +11,7 @@ allow = [
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
//! # [Ratatui] Block example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{
|
||||
block::{Position, Title},
|
||||
Block, BorderType, Borders, Padding, Paragraph, Wrap,
|
||||
},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(draw)?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(frame: &mut Frame) {
|
||||
let (title_area, layout) = calculate_layout(frame.area());
|
||||
|
||||
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 main_layout = Layout::vertical([Constraint::Length(1), Constraint::Min(0)]);
|
||||
let block_layout = Layout::vertical([Constraint::Max(4); 9]);
|
||||
let [title_area, main_area] = main_layout.areas(area);
|
||||
let main_areas = block_layout
|
||||
.split(main_area)
|
||||
.iter()
|
||||
.map(|&area| {
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.split(area)
|
||||
.to_vec()
|
||||
})
|
||||
.collect();
|
||||
(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:#?}"));
|
||||
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::bordered()
|
||||
.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::bordered()
|
||||
.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::bordered()
|
||||
.style(Style::new().blue().on_white().bold().italic())
|
||||
.title("Styled block");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_styled_title(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered()
|
||||
.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::bordered().title(title);
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_multiple_titles(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered()
|
||||
.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::bordered()
|
||||
.title(
|
||||
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),
|
||||
)
|
||||
.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::bordered()
|
||||
.padding(Padding::new(5, 10, 1, 2))
|
||||
.title("Padding");
|
||||
frame.render_widget(paragraph.clone().block(block), area);
|
||||
}
|
||||
|
||||
fn render_nested_blocks(paragraph: &Paragraph, frame: &mut Frame, area: Rect) {
|
||||
let outer_block = Block::bordered().title("Outer block");
|
||||
let inner_block = Block::bordered().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,69 +0,0 @@
|
||||
//! # [Ratatui] Logo example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [examples]: https://github.com/ratatui/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{
|
||||
io::{self},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use indoc::indoc;
|
||||
use itertools::izip;
|
||||
use ratatui::{widgets::Paragraph, TerminalOptions, Viewport};
|
||||
|
||||
/// A fun example of using half block characters to draw a logo
|
||||
#[allow(clippy::many_single_char_names)]
|
||||
fn logo() -> String {
|
||||
let r = indoc! {"
|
||||
▄▄▄
|
||||
█▄▄▀
|
||||
█ █
|
||||
"};
|
||||
let a = indoc! {"
|
||||
▄▄
|
||||
█▄▄█
|
||||
█ █
|
||||
"};
|
||||
let t = indoc! {"
|
||||
▄▄▄
|
||||
█
|
||||
█
|
||||
"};
|
||||
let u = indoc! {"
|
||||
▄ ▄
|
||||
█ █
|
||||
▀▄▄▀
|
||||
"};
|
||||
let i = indoc! {"
|
||||
▄
|
||||
█
|
||||
█
|
||||
"};
|
||||
izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines())
|
||||
.map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
let mut terminal = ratatui::init_with_options(TerminalOptions {
|
||||
viewport: Viewport::Inline(3),
|
||||
});
|
||||
terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?;
|
||||
sleep(Duration::from_secs(5));
|
||||
ratatui::restore();
|
||||
println!();
|
||||
Ok(())
|
||||
}
|
||||
70
ratatui-core/Cargo.toml
Normal file
70
ratatui-core/Cargo.toml
Normal file
@@ -0,0 +1,70 @@
|
||||
[package]
|
||||
name = "ratatui-core"
|
||||
description = """
|
||||
Core types and traits for the Ratatui Terminal UI library.
|
||||
Widget libraries should use this crate. Applications should use the main Ratatui crate.
|
||||
"""
|
||||
version = "0.1.0-alpha.0"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## enables conversions from colors in the [`palette`] crate to [`Color`](crate::style::Color).
|
||||
palette = ["dep:palette"]
|
||||
|
||||
## enables the backend code that sets the underline color. Underline color is only supported by
|
||||
## the Crossterm backend, and is not supported on Windows 7.
|
||||
underline-color = []
|
||||
|
||||
## Use terminal scrolling regions to make some operations less prone to
|
||||
## flickering. (i.e. Terminal::insert_before).
|
||||
scrolling-regions = []
|
||||
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
|
||||
|
||||
[dependencies]
|
||||
bitflags = "2.3"
|
||||
cassowary = "0.3"
|
||||
compact_str = "0.8.0"
|
||||
document-features = { workspace = true, optional = true }
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
lru = "0.12.0"
|
||||
palette = { version = "0.7.6", optional = true }
|
||||
paste = "1.0.2"
|
||||
serde = { workspace = true, optional = true }
|
||||
strum.workspace = true
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-truncate = "2"
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
ratatui = { workspace = true, features = ["crossterm", "termwiz"] }
|
||||
rstest.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[target.'cfg(not(windows))'.dev-dependencies]
|
||||
ratatui = { workspace = true, features = ["termion"] }
|
||||
|
||||
[lints.clippy]
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
41
ratatui-core/README.md
Normal file
41
ratatui-core/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Ratatui Core
|
||||
|
||||
[](https://crates.io/crates/ratatui-core)
|
||||
[](https://docs.rs/ratatui-core)
|
||||
[](../LICENSE)
|
||||
|
||||
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
**ratatui-core** is the core library of the [ratatui] project,
|
||||
providing the essential building blocks for creating rich terminal user interfaces in Rust.
|
||||
|
||||
[ratatui]: https://github.com/ratatui/ratatui
|
||||
|
||||
### Why `ratatui-core`?
|
||||
|
||||
The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
|
||||
to offer better stability for widget library authors. Widget libraries should generally depend
|
||||
on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
|
||||
|
||||
Applications, on the other hand, should depend on the main `ratatui` crate, which includes
|
||||
built-in widgets and additional features.
|
||||
|
||||
## Installation
|
||||
|
||||
Add `ratatui-core` to your `Cargo.toml`:
|
||||
|
||||
```shell
|
||||
cargo add ratatui-core
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
|
||||
guide for more details on how to get involved.
|
||||
|
||||
### License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
@@ -27,7 +27,7 @@
|
||||
//! ```rust,no_run
|
||||
//! use std::io::stdout;
|
||||
//!
|
||||
//! use ratatui::prelude::*;
|
||||
//! use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
//!
|
||||
//! let backend = CrosstermBackend::new(stdout());
|
||||
//! let mut terminal = Terminal::new(backend)?;
|
||||
@@ -56,7 +56,7 @@
|
||||
//! Each backend handles raw mode differently, so the behavior may vary depending on the backend
|
||||
//! being used. Be sure to consult the backend's specific documentation for exact details on how it
|
||||
//! implements raw mode.
|
||||
|
||||
//!
|
||||
//! # Alternate Screen
|
||||
//!
|
||||
//! The alternate screen is a separate buffer that some terminals provide, distinct from the main
|
||||
@@ -90,15 +90,15 @@
|
||||
//! backend being used, and developers should consult the specific backend's documentation to
|
||||
//! understand how it implements mouse capture.
|
||||
//!
|
||||
//! [`TermionBackend`]: termion/struct.TermionBackend.html
|
||||
//! [`Terminal`]: crate::terminal::Terminal
|
||||
//! [`TermionBackend`]: termion/struct.TermionBackend.html
|
||||
//! [`CrosstermBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.CrosstermBackend.html
|
||||
//! [`TermionBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermionBackend.html
|
||||
//! [`TermwizBackend`]: https://docs.rs/ratatui/latest/ratatui/backend/struct.TermwizBackend.html
|
||||
//! [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
//! [Termion]: https://crates.io/crates/termion
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/examples/README.md
|
||||
//! [Backend Comparison]:
|
||||
//! https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
//! [Backend Comparison]: https://ratatui.rs/concepts/backends/comparison/
|
||||
//! [Ratatui Website]: https://ratatui.rs
|
||||
use std::io;
|
||||
|
||||
@@ -109,21 +109,6 @@ use crate::{
|
||||
layout::{Position, Size},
|
||||
};
|
||||
|
||||
#[cfg(all(not(windows), feature = "termion"))]
|
||||
mod termion;
|
||||
#[cfg(all(not(windows), feature = "termion"))]
|
||||
pub use self::termion::TermionBackend;
|
||||
|
||||
#[cfg(feature = "crossterm")]
|
||||
mod crossterm;
|
||||
#[cfg(feature = "crossterm")]
|
||||
pub use self::crossterm::CrosstermBackend;
|
||||
|
||||
#[cfg(feature = "termwiz")]
|
||||
mod termwiz;
|
||||
#[cfg(feature = "termwiz")]
|
||||
pub use self::termwiz::TermwizBackend;
|
||||
|
||||
mod test;
|
||||
pub use self::test::TestBackend;
|
||||
|
||||
@@ -162,7 +147,7 @@ pub struct WindowSize {
|
||||
/// Most applications should not need to interact with the `Backend` trait directly as the
|
||||
/// [`Terminal`] struct provides a higher level interface for interacting with the terminal.
|
||||
///
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
pub trait Backend {
|
||||
/// Draw the given content to the terminal screen.
|
||||
///
|
||||
@@ -187,8 +172,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::Backend;
|
||||
///
|
||||
/// backend.hide_cursor()?;
|
||||
/// // do something with hidden cursor
|
||||
/// backend.show_cursor()?;
|
||||
@@ -222,9 +209,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::layout::Position;
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::{backend::Backend, layout::Position};
|
||||
///
|
||||
/// backend.set_cursor_position(Position { x: 10, y: 20 })?;
|
||||
/// assert_eq!(backend.get_cursor_position()?, Position { x: 10, y: 20 });
|
||||
/// # std::io::Result::Ok(())
|
||||
@@ -254,8 +242,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::Backend;
|
||||
///
|
||||
/// backend.clear()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -270,8 +260,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
|
||||
/// # use ratatui::{backend::{TestBackend}};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::{Backend, ClearType};
|
||||
///
|
||||
/// backend.clear_region(ClearType::All)?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -302,8 +294,10 @@ pub trait Backend {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// # use ratatui::{backend::{TestBackend}};
|
||||
/// # let backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::{backend::Backend, layout::Size};
|
||||
///
|
||||
/// assert_eq!(backend.size()?, Size::new(80, 25));
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -318,6 +312,64 @@ pub trait Backend {
|
||||
|
||||
/// Flush any buffered content to the terminal screen.
|
||||
fn flush(&mut self) -> io::Result<()>;
|
||||
|
||||
/// Scroll a region of the screen upwards, where a region is specified by a (half-open) range
|
||||
/// of rows.
|
||||
///
|
||||
/// Each row in the region is replaced by the row `line_count` rows below it, except the bottom
|
||||
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
|
||||
/// than the number of rows in the region, then all rows are replaced with empty rows.
|
||||
///
|
||||
/// If the region includes row 0, then `line_count` rows are copied into the bottom of the
|
||||
/// scrollback buffer. These rows are first taken from the old contents of the region, starting
|
||||
/// from the top. If there aren't sufficient rows in the region, then the remainder are empty
|
||||
/// rows.
|
||||
///
|
||||
/// The position of the cursor afterwards is undefined.
|
||||
///
|
||||
/// The behavior is designed to match what ANSI terminals do when scrolling regions are
|
||||
/// established. With ANSI terminals, a scrolling region can be established with the "^[[X;Yr"
|
||||
/// sequence, where X and Y define the lines of the region. The scrolling region can be reset
|
||||
/// to be the whole screen with the "^[[r" sequence.
|
||||
///
|
||||
/// When a scrolling region is established in an ANSI terminal, various operations' behaviors
|
||||
/// are changed in such a way that the scrolling region acts like a "virtual screen". In
|
||||
/// particular, the scrolling sequence "^[[NS", which scrolls lines up by a count of N.
|
||||
///
|
||||
/// On an ANSI terminal, this method will probably translate to something like:
|
||||
/// "^[[X;Yr^[[NS^[[r". That is, set the scrolling region, scroll up, then reset the scrolling
|
||||
/// region.
|
||||
///
|
||||
/// For examples of how this function is expected to work, refer to the tests for
|
||||
/// [`TestBackend::scroll_region_up`].
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, line_count: u16)
|
||||
-> io::Result<()>;
|
||||
|
||||
/// Scroll a region of the screen downwards, where a region is specified by a (half-open) range
|
||||
/// of rows.
|
||||
///
|
||||
/// Each row in the region is replaced by the row `line_count` rows above it, except the top
|
||||
/// `line_count` rows, which are replaced by empty rows. If `line_count` is equal to or larger
|
||||
/// than the number of rows in the region, then all rows are replaced with empty rows.
|
||||
///
|
||||
/// The position of the cursor afterwards is undefined.
|
||||
///
|
||||
/// See the documentation for [`Self::scroll_region_down`] for more information about how this
|
||||
/// is expected to be implemented for ANSI terminals. All of that applies, except the ANSI
|
||||
/// sequence to scroll down is "^[[NT".
|
||||
///
|
||||
/// This function is asymmetrical with regards to the scrollback buffer. The reason is that
|
||||
/// this how terminals seem to implement things.
|
||||
///
|
||||
/// For examples of how this function is expected to work, refer to the tests for
|
||||
/// [`TestBackend::scroll_region_down`].
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(
|
||||
&mut self,
|
||||
region: std::ops::Range<u16>,
|
||||
line_count: u16,
|
||||
) -> io::Result<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -24,7 +24,7 @@ use crate::{
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::TestBackend, prelude::*};
|
||||
/// use ratatui::backend::{Backend, TestBackend};
|
||||
///
|
||||
/// let mut backend = TestBackend::new(10, 2);
|
||||
/// backend.clear()?;
|
||||
@@ -80,6 +80,28 @@ impl TestBackend {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `TestBackend` with the specified lines as the initial screen state.
|
||||
///
|
||||
/// The backend's screen size is determined from the initial lines.
|
||||
#[must_use]
|
||||
pub fn with_lines<'line, Lines>(lines: Lines) -> Self
|
||||
where
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<crate::text::Line<'line>>,
|
||||
{
|
||||
let buffer = Buffer::with_lines(lines);
|
||||
let scrollback = Buffer::empty(Rect {
|
||||
width: buffer.area.width,
|
||||
..Rect::ZERO
|
||||
});
|
||||
Self {
|
||||
buffer,
|
||||
scrollback,
|
||||
cursor: false,
|
||||
pos: (0, 0),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the internal buffer of the `TestBackend`.
|
||||
pub const fn buffer(&self) -> &Buffer {
|
||||
&self.buffer
|
||||
@@ -247,7 +269,7 @@ impl Backend for TestBackend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: super::ClearType) -> io::Result<()> {
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
let region = match clear_type {
|
||||
ClearType::All => return self.clear(),
|
||||
ClearType::AfterCursor => {
|
||||
@@ -343,6 +365,77 @@ impl Backend for TestBackend {
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, scroll_by: u16) -> io::Result<()> {
|
||||
let width: usize = self.buffer.area.width.into();
|
||||
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
|
||||
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
|
||||
let cell_region_len = cell_region_end - cell_region_start;
|
||||
let cells_to_scroll_by = width * scroll_by as usize;
|
||||
|
||||
// Deal with the simple case where nothing needs to be copied into scrollback.
|
||||
if cell_region_start > 0 {
|
||||
if cells_to_scroll_by >= cell_region_len {
|
||||
// The scroll amount is large enough to clear the whole region.
|
||||
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
|
||||
} else {
|
||||
// Scroll up by rotating, then filling in the bottom with empty cells.
|
||||
self.buffer.content[cell_region_start..cell_region_end]
|
||||
.rotate_left(cells_to_scroll_by);
|
||||
self.buffer.content[cell_region_end - cells_to_scroll_by..cell_region_end]
|
||||
.fill_with(Default::default);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// The rows inserted into the scrollback will first come from the buffer, and if that is
|
||||
// insufficient, will then be blank rows.
|
||||
let cells_from_region = cell_region_len.min(cells_to_scroll_by);
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
self.buffer.content.splice(
|
||||
0..cells_from_region,
|
||||
iter::repeat_with(Default::default).take(cells_from_region),
|
||||
),
|
||||
);
|
||||
if cells_to_scroll_by < cell_region_len {
|
||||
// Rotate the remaining cells to the front of the region.
|
||||
self.buffer.content[cell_region_start..cell_region_end].rotate_left(cells_from_region);
|
||||
} else {
|
||||
// Splice cleared out the region. Insert empty rows in scrollback.
|
||||
append_to_scrollback(
|
||||
&mut self.scrollback,
|
||||
iter::repeat_with(Default::default).take(cells_to_scroll_by - cell_region_len),
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(
|
||||
&mut self,
|
||||
region: std::ops::Range<u16>,
|
||||
scroll_by: u16,
|
||||
) -> io::Result<()> {
|
||||
let width: usize = self.buffer.area.width.into();
|
||||
let cell_region_start = width * region.start.min(self.buffer.area.height) as usize;
|
||||
let cell_region_end = width * region.end.min(self.buffer.area.height) as usize;
|
||||
let cell_region_len = cell_region_end - cell_region_start;
|
||||
let cells_to_scroll_by = width * scroll_by as usize;
|
||||
|
||||
if cells_to_scroll_by >= cell_region_len {
|
||||
// The scroll amount is large enough to clear the whole region.
|
||||
self.buffer.content[cell_region_start..cell_region_end].fill_with(Default::default);
|
||||
} else {
|
||||
// Scroll up by rotating, then filling in the top with empty cells.
|
||||
self.buffer.content[cell_region_start..cell_region_end]
|
||||
.rotate_right(cells_to_scroll_by);
|
||||
self.buffer.content[cell_region_start..cell_region_start + cells_to_scroll_by]
|
||||
.fill_with(Default::default);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Append the provided cells to the bottom of a scrollback buffer. The number of cells must be a
|
||||
@@ -386,12 +479,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn buffer_view_with_overwrites() {
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 8 wide
|
||||
let multi_byte_char = "👨👩👧👦"; // renders 2 wide
|
||||
let buffer = Buffer::with_lines([multi_byte_char]);
|
||||
assert_eq!(
|
||||
buffer_view(&buffer),
|
||||
format!(
|
||||
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " "), (2, " "), (3, " "), (4, " "), (5, " "), (6, " "), (7, " ")]
|
||||
r#""{multi_byte_char}" Hidden by multi-width symbols: [(1, " ")]
|
||||
"#,
|
||||
)
|
||||
);
|
||||
@@ -492,8 +585,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_all() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -513,8 +605,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_after_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -537,8 +628,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_before_cursor() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -561,8 +651,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_current_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -585,8 +674,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn clear_region_until_new_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
"aaaaaaaaaa",
|
||||
@@ -609,8 +697,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -648,8 +735,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_lines_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -681,8 +767,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_not_at_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -711,8 +796,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_past_last_line() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -739,8 +823,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -773,8 +856,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_appends_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -799,8 +881,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn append_multiple_lines_where_cursor_at_end_appends_more_than_height_lines() {
|
||||
let mut backend = TestBackend::new(10, 5);
|
||||
backend.buffer = Buffer::with_lines([
|
||||
let mut backend = TestBackend::with_lines([
|
||||
"aaaaaaaaaa",
|
||||
"bbbbbbbbbb",
|
||||
"cccccccccc",
|
||||
@@ -916,4 +997,81 @@ mod tests {
|
||||
let mut backend = TestBackend::new(10, 2);
|
||||
backend.flush().unwrap();
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
mod scrolling_regions {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
const A: &str = "aaaa";
|
||||
const B: &str = "bbbb";
|
||||
const C: &str = "cccc";
|
||||
const D: &str = "dddd";
|
||||
const E: &str = "eeee";
|
||||
const S: &str = " ";
|
||||
|
||||
#[rstest]
|
||||
#[case([A, B, C, D, E], 0..5, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..5, 2, [A, B], [C, D, E, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 5, [A, B, C, D, E], [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 7, [A, B, C, D, E, S, S], [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..3, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 2, [A, B], [C, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 3, [A, B, C], [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 4, [A, B, C, S], [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 2, [], [A, D, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 3, [], [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 4, [], [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 2, [S, S], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 0, [], [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 2, [], [A, B, C, D, E])]
|
||||
fn scroll_region_up<const L: usize, const M: usize, const N: usize>(
|
||||
#[case] initial_screen: [&'static str; L],
|
||||
#[case] range: std::ops::Range<u16>,
|
||||
#[case] scroll_by: u16,
|
||||
#[case] expected_scrollback: [&'static str; M],
|
||||
#[case] expected_buffer: [&'static str; N],
|
||||
) {
|
||||
let mut backend = TestBackend::with_lines(initial_screen);
|
||||
backend.scroll_region_up(range, scroll_by).unwrap();
|
||||
if expected_scrollback.is_empty() {
|
||||
backend.assert_scrollback_empty();
|
||||
} else {
|
||||
backend.assert_scrollback_lines(expected_scrollback);
|
||||
}
|
||||
backend.assert_buffer_lines(expected_buffer);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case([A, B, C, D, E], 0..5, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..5, 2, [S, S, A, B, C])]
|
||||
#[case([A, B, C, D, E], 0..5, 5, [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..5, 7, [S, S, S, S, S])]
|
||||
#[case([A, B, C, D, E], 0..3, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 2, [S, S, A, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 3, [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 0..3, 4, [S, S, S, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 2, [A, S, S, B, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 3, [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 1..4, 4, [A, S, S, S, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 0..0, 2, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 0, [A, B, C, D, E])]
|
||||
#[case([A, B, C, D, E], 2..2, 2, [A, B, C, D, E])]
|
||||
fn scroll_region_down<const M: usize, const N: usize>(
|
||||
#[case] initial_screen: [&'static str; M],
|
||||
#[case] range: std::ops::Range<u16>,
|
||||
#[case] scroll_by: u16,
|
||||
#[case] expected_buffer: [&'static str; N],
|
||||
) {
|
||||
let mut backend = TestBackend::with_lines(initial_screen);
|
||||
backend.scroll_region_down(range, scroll_by).unwrap();
|
||||
backend.assert_scrollback_empty();
|
||||
backend.assert_buffer_lines(expected_buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,11 @@ macro_rules! assert_buffer_eq {
|
||||
#[allow(deprecated)]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn assert_buffer_eq_does_not_panic_on_equal_buffers() {
|
||||
@@ -6,7 +6,12 @@ use std::{
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{buffer::Cell, layout::Position, prelude::*};
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
layout::{Position, Rect},
|
||||
style::Style,
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
/// A buffer that maps to the desired content of the terminal after the draw call
|
||||
///
|
||||
@@ -18,7 +23,7 @@ use crate::{buffer::Cell, layout::Position, prelude::*};
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// style::{Color, Style},
|
||||
@@ -163,7 +168,11 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// };
|
||||
///
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
///
|
||||
/// assert_eq!(buffer.cell(Position::new(0, 0)), Some(&Cell::default()));
|
||||
@@ -190,7 +199,11 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// style::{Color, Style},
|
||||
/// };
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
///
|
||||
/// if let Some(cell) = buffer.cell_mut(Position::new(0, 0)) {
|
||||
@@ -214,7 +227,8 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
|
||||
/// // Global coordinates to the top corner of this buffer's area
|
||||
/// assert_eq!(buffer.index_of(200, 100), 0);
|
||||
@@ -225,7 +239,8 @@ impl Buffer {
|
||||
/// Panics when given an coordinate that is outside of this Buffer's area.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let buffer = Buffer::empty(Rect::new(200, 100, 10, 10));
|
||||
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
|
||||
/// // starts at (200, 100).
|
||||
@@ -254,9 +269,10 @@ impl Buffer {
|
||||
return None;
|
||||
}
|
||||
// remove offset
|
||||
let y = position.y - self.area.y;
|
||||
let x = position.x - self.area.x;
|
||||
Some((y * self.area.width + x) as usize)
|
||||
let y = (position.y - self.area.y) as usize;
|
||||
let x = (position.x - self.area.x) as usize;
|
||||
let width = self.area.width as usize;
|
||||
Some(y * width + x)
|
||||
}
|
||||
|
||||
/// Returns the (global) coordinates of a cell given its index
|
||||
@@ -266,7 +282,8 @@ impl Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let rect = Rect::new(200, 100, 10, 10);
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// assert_eq!(buffer.pos_of(0), (200, 100));
|
||||
@@ -278,22 +295,25 @@ impl Buffer {
|
||||
/// Panics when given an index that is outside the Buffer's content.
|
||||
///
|
||||
/// ```should_panic
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
|
||||
/// let buffer = Buffer::empty(rect);
|
||||
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
|
||||
/// buffer.pos_of(100); // Panics
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub fn pos_of(&self, i: usize) -> (u16, u16) {
|
||||
pub fn pos_of(&self, index: usize) -> (u16, u16) {
|
||||
debug_assert!(
|
||||
i < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={i} len={}",
|
||||
index < self.content.len(),
|
||||
"Trying to get the coords of a cell outside the buffer: i={index} len={}",
|
||||
self.content.len()
|
||||
);
|
||||
let x = index % self.area.width as usize + self.area.x as usize;
|
||||
let y = index / self.area.width as usize + self.area.y as usize;
|
||||
(
|
||||
self.area.x + (i as u16) % self.area.width,
|
||||
self.area.y + (i as u16) / self.area.width,
|
||||
u16::try_from(x).expect("x overflow. This should never happen as area.width is u16"),
|
||||
u16::try_from(y).expect("y overflow. This should never happen as area.height is u16"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -377,6 +397,8 @@ impl Buffer {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn set_style<S: Into<Style>>(&mut self, area: Rect, style: S) {
|
||||
let style = style.into();
|
||||
let area = self.area.intersection(area);
|
||||
@@ -504,7 +526,11 @@ impl<P: Into<Position>> Index<P> for Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// };
|
||||
///
|
||||
/// let buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
/// let cell = &buf[(0, 0)];
|
||||
/// let cell = &buf[Position::new(0, 0)];
|
||||
@@ -530,7 +556,11 @@ impl<P: Into<Position>> IndexMut<P> for Buffer {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, buffer::Cell, layout::Position};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// };
|
||||
///
|
||||
/// let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
/// buf[(0, 0)].set_symbol("A");
|
||||
/// buf[Position::new(0, 0)].set_symbol("B");
|
||||
@@ -622,6 +652,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
|
||||
#[test]
|
||||
fn debug_empty_buffer() {
|
||||
@@ -1214,11 +1245,12 @@ mod tests {
|
||||
#[case::shrug("🤷", "🤷xxxxx")]
|
||||
// Technically this is a (brown) bear, a zero-width joiner and a snowflake
|
||||
// As it is joined its a single emoji and should therefore have a width of 2.
|
||||
// It's correctly detected as a single grapheme but it's width is 4 for some reason
|
||||
#[case::polarbear("🐻❄️", "🐻❄️xxx")]
|
||||
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
|
||||
#[case::polarbear("🐻❄️", "🐻❄️xxxxx")]
|
||||
// Technically this is an eye, a zero-width joiner and a speech bubble
|
||||
// Both eye and speech bubble include a 'display as emoji' variation selector
|
||||
#[case::eye_speechbubble("👁️🗨️", "👁️🗨️xxx")]
|
||||
// Prior to unicode-width 0.2, this was incorrectly detected as width 4 for some reason
|
||||
#[case::eye_speechbubble("👁️🗨️", "👁️🗨️xxxxx")]
|
||||
fn renders_emoji(#[case] input: &str, #[case] expected: &str) {
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
@@ -1244,4 +1276,24 @@ mod tests {
|
||||
let expected = Buffer::with_lines([expected]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
/// Regression test for <https://github.com/ratatui/ratatui/issues/1441>
|
||||
///
|
||||
/// Previously the `pos_of` function would incorrectly cast the index to a u16 value instead of
|
||||
/// using the index as is. This caused incorrect rendering of any buffer with an length > 65535.
|
||||
#[test]
|
||||
fn index_pos_of_u16_max() {
|
||||
let buffer = Buffer::empty(Rect::new(0, 0, 256, 256 + 1));
|
||||
assert_eq!(buffer.index_of(255, 255), 65535);
|
||||
assert_eq!(buffer.pos_of(65535), (255, 255));
|
||||
|
||||
assert_eq!(buffer.index_of(0, 256), 65536);
|
||||
assert_eq!(buffer.pos_of(65536), (0, 256)); // previously (0, 0)
|
||||
|
||||
assert_eq!(buffer.index_of(1, 256), 65537);
|
||||
assert_eq!(buffer.pos_of(65537), (1, 256)); // previously (1, 0)
|
||||
|
||||
assert_eq!(buffer.index_of(255, 256), 65791);
|
||||
assert_eq!(buffer.pos_of(65791), (255, 256)); // previously (255, 0)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use compact_str::CompactString;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::style::{Color, Modifier, Style};
|
||||
|
||||
/// A buffer cell
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -1,4 +1,5 @@
|
||||
#![warn(clippy::missing_const_for_fn)]
|
||||
//! Provides types and traits for working with layout and positioning in the terminal.
|
||||
|
||||
mod alignment;
|
||||
mod constraint;
|
||||
@@ -14,8 +15,8 @@ pub use alignment::Alignment;
|
||||
pub use constraint::Constraint;
|
||||
pub use direction::Direction;
|
||||
pub use flex::Flex;
|
||||
pub use layout::Layout;
|
||||
pub use layout::{Layout, Spacing};
|
||||
pub use margin::Margin;
|
||||
pub use position::Position;
|
||||
pub use rect::*;
|
||||
pub use rect::{Columns, Offset, Positions, Rect, Rows};
|
||||
pub use size::Size;
|
||||
@@ -26,7 +26,8 @@ use strum::EnumIs;
|
||||
/// `Constraint` provides helper methods to create lists of constraints from various input formats.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::Constraint;
|
||||
///
|
||||
/// // Create a layout with specified lengths for each element
|
||||
/// let constraints = Constraint::from_lengths([10, 20, 10]);
|
||||
///
|
||||
@@ -223,7 +224,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_lengths([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -240,7 +242,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_ratios([(1, 4), (1, 2), (1, 4)]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -257,7 +260,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_percentages([25, 50, 25]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -274,7 +278,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_maxes([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -291,7 +296,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_mins([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -308,7 +314,8 @@ impl Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let constraints = Constraint::from_fills([1, 2, 3]);
|
||||
/// let layout = Layout::default().constraints(constraints).split(area);
|
||||
@@ -330,7 +337,8 @@ impl From<u16> for Constraint {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::{Constraint, Direction, Layout, Rect};
|
||||
///
|
||||
/// # let area = Rect::default();
|
||||
/// let layout = Layout::new(Direction::Vertical, [1, 2, 3]).split(area);
|
||||
/// let layout = Layout::horizontal([1, 2, 3]).split(area);
|
||||
@@ -1,7 +1,7 @@
|
||||
use strum::{Display, EnumIs, EnumString};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use super::constraint::Constraint;
|
||||
use crate::layout::Constraint;
|
||||
|
||||
/// Defines the options for layout flex justify content in a container.
|
||||
///
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ use crate::layout::Rect;
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::layout::{Position, Rect};
|
||||
/// use ratatui_core::layout::{Position, Rect};
|
||||
///
|
||||
/// // the following are all equivalent
|
||||
/// let position = Position { x: 1, y: 2 };
|
||||
@@ -4,8 +4,7 @@ use std::{
|
||||
fmt,
|
||||
};
|
||||
|
||||
use super::{Position, Size};
|
||||
use crate::prelude::*;
|
||||
use crate::layout::{Margin, Position, Size};
|
||||
|
||||
mod iter;
|
||||
pub use iter::*;
|
||||
@@ -27,7 +26,7 @@ pub struct Rect {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Amounts by which to move a [`Rect`](super::Rect).
|
||||
/// Amounts by which to move a [`Rect`](crate::layout::Rect).
|
||||
///
|
||||
/// Positive numbers move to the right/bottom and negative to the left/top.
|
||||
///
|
||||
@@ -56,32 +55,41 @@ impl Rect {
|
||||
height: 0,
|
||||
};
|
||||
|
||||
/// Creates a new `Rect`, with width and height limited to keep the area under max `u16`. If
|
||||
/// clipped, aspect ratio will be preserved.
|
||||
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
let max_area = u16::MAX;
|
||||
let (clipped_width, clipped_height) =
|
||||
if u32::from(width) * u32::from(height) > u32::from(max_area) {
|
||||
let aspect_ratio = f64::from(width) / f64::from(height);
|
||||
let max_area_f = f64::from(max_area);
|
||||
let height_f = (max_area_f / aspect_ratio).sqrt();
|
||||
let width_f = height_f * aspect_ratio;
|
||||
(width_f as u16, height_f as u16)
|
||||
} else {
|
||||
(width, height)
|
||||
};
|
||||
/// Creates a new `Rect`, with width and height limited to keep both bounds within `u16`.
|
||||
///
|
||||
/// If the width or height would cause the right or bottom coordinate to be larger than the
|
||||
/// maximum value of `u16`, the width or height will be clamped to keep the right or bottom
|
||||
/// coordinate within `u16`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui_core::layout::Rect;
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// ```
|
||||
pub const fn new(x: u16, y: u16, width: u16, height: u16) -> Self {
|
||||
// these calculations avoid using min so that this function can be const
|
||||
let max_width = u16::MAX - x;
|
||||
let max_height = u16::MAX - y;
|
||||
let width = if width > max_width { max_width } else { width };
|
||||
let height = if height > max_height {
|
||||
max_height
|
||||
} else {
|
||||
height
|
||||
};
|
||||
Self {
|
||||
x,
|
||||
y,
|
||||
width: clipped_width,
|
||||
height: clipped_height,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
/// The area of the `Rect`. If the area is larger than the maximum value of `u16`, it will be
|
||||
/// clamped to `u16::MAX`.
|
||||
pub const fn area(self) -> u16 {
|
||||
self.width.saturating_mul(self.height)
|
||||
pub const fn area(self) -> u32 {
|
||||
(self.width as u32) * (self.height as u32)
|
||||
}
|
||||
|
||||
/// Returns true if the `Rect` has no area.
|
||||
@@ -205,7 +213,8 @@ impl Rect {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, layout::Position};
|
||||
/// use ratatui_core::layout::{Position, Rect};
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// assert!(rect.contains(Position { x: 1, y: 2 }));
|
||||
/// ````
|
||||
@@ -234,11 +243,11 @@ impl Rect {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.area();
|
||||
/// let rect = Rect::new(0, 0, 100, 100).clamp(area);
|
||||
/// # }
|
||||
/// use ratatui_core::layout::Rect;
|
||||
///
|
||||
/// let area = Rect::new(0, 0, 100, 100);
|
||||
/// let rect = Rect::new(80, 80, 30, 30).clamp(area);
|
||||
/// assert_eq!(rect, Rect::new(70, 70, 30, 30));
|
||||
/// ```
|
||||
#[must_use = "method returns the modified value"]
|
||||
pub fn clamp(self, other: Self) -> Self {
|
||||
@@ -254,7 +263,8 @@ impl Rect {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for row in area.rows() {
|
||||
/// Line::raw("Hello, world!").render(row, buf);
|
||||
@@ -270,10 +280,11 @@ impl Rect {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// if let Some(left) = area.columns().next() {
|
||||
/// Block::new().borders(Borders::LEFT).render(left, buf);
|
||||
/// for (i, column) in area.columns().enumerate() {
|
||||
/// Text::from(format!("{}", i)).render(column, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
@@ -288,7 +299,8 @@ impl Rect {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
///
|
||||
/// fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// for position in area.positions() {
|
||||
/// buf[(position.x, position.y)].set_symbol("x");
|
||||
@@ -304,7 +316,8 @@ impl Rect {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::layout::Rect;
|
||||
///
|
||||
/// let rect = Rect::new(1, 2, 3, 4);
|
||||
/// let position = rect.as_position();
|
||||
/// ````
|
||||
@@ -352,6 +365,7 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::layout::{Constraint, Layout};
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
@@ -496,46 +510,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn size_truncation() {
|
||||
for width in 256u16..300u16 {
|
||||
for height in 256u16..300u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
// The target dimensions are rounded down so the math will not be too precise
|
||||
// but let's make sure the ratios don't diverge crazily.
|
||||
assert!(
|
||||
(f64::from(rect.width) / f64::from(rect.height)
|
||||
- f64::from(width) / f64::from(height))
|
||||
.abs()
|
||||
< 1.0
|
||||
);
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 100, u16::MAX - 1000, 200, 2000),
|
||||
Rect {
|
||||
x: u16::MAX - 100,
|
||||
y: u16::MAX - 1000,
|
||||
width: 100,
|
||||
height: 1000
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area above max u16.
|
||||
let width = 900;
|
||||
let height = 100;
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
assert_ne!(rect.width, 900);
|
||||
assert_ne!(rect.height, 100);
|
||||
assert!(rect.width < width || rect.height < height);
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_preservation() {
|
||||
for width in 0..256u16 {
|
||||
for height in 0..256u16 {
|
||||
let rect = Rect::new(0, 0, width, height);
|
||||
rect.area(); // Should not panic.
|
||||
assert_eq!(rect.width, width);
|
||||
assert_eq!(rect.height, height);
|
||||
assert_eq!(
|
||||
Rect::new(u16::MAX - 100, u16::MAX - 1000, 100, 1000),
|
||||
Rect {
|
||||
x: u16::MAX - 100,
|
||||
y: u16::MAX - 1000,
|
||||
width: 100,
|
||||
height: 1000
|
||||
}
|
||||
}
|
||||
|
||||
// One dimension below 255, one above. Area below max u16.
|
||||
let rect = Rect::new(0, 0, 300, 100);
|
||||
assert_eq!(rect.width, 300);
|
||||
assert_eq!(rect.height, 100);
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -546,7 +542,7 @@ mod tests {
|
||||
width: 10,
|
||||
height: 10,
|
||||
};
|
||||
const _AREA: u16 = RECT.area();
|
||||
const _AREA: u32 = RECT.area();
|
||||
const _LEFT: u16 = RECT.left();
|
||||
const _RIGHT: u16 = RECT.right();
|
||||
const _TOP: u16 = RECT.top();
|
||||
329
ratatui-core/src/layout/rect/iter.rs
Normal file
329
ratatui-core/src/layout/rect/iter.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
use crate::layout::{Position, Rect};
|
||||
|
||||
/// An iterator over rows within a `Rect`.
|
||||
pub struct Rows {
|
||||
/// The `Rect` associated with the rows.
|
||||
rect: Rect,
|
||||
/// The y coordinate of the row within the `Rect` when iterating forwards.
|
||||
current_row_fwd: u16,
|
||||
/// The y coordinate of the row within the `Rect` when iterating backwards.
|
||||
current_row_back: u16,
|
||||
}
|
||||
|
||||
impl Rows {
|
||||
/// Creates a new `Rows` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_row_fwd: rect.y,
|
||||
current_row_back: rect.bottom(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Rows {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row_fwd >= self.current_row_back {
|
||||
return None;
|
||||
}
|
||||
let row = Rect::new(self.rect.x, self.current_row_fwd, self.rect.width, 1);
|
||||
self.current_row_fwd += 1;
|
||||
Some(row)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let start_count = self.current_row_fwd.saturating_sub(self.rect.top());
|
||||
let end_count = self.rect.bottom().saturating_sub(self.current_row_back);
|
||||
let count = self
|
||||
.rect
|
||||
.height
|
||||
.saturating_sub(start_count)
|
||||
.saturating_sub(end_count) as usize;
|
||||
(count, Some(count))
|
||||
}
|
||||
}
|
||||
|
||||
impl DoubleEndedIterator for Rows {
|
||||
/// Retrieves the previous row within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more rows to iterate through.
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.current_row_back <= self.current_row_fwd {
|
||||
return None;
|
||||
}
|
||||
self.current_row_back -= 1;
|
||||
let row = Rect::new(self.rect.x, self.current_row_back, self.rect.width, 1);
|
||||
Some(row)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over columns within a `Rect`.
|
||||
pub struct Columns {
|
||||
/// The `Rect` associated with the columns.
|
||||
rect: Rect,
|
||||
/// The x coordinate of the column within the `Rect` when iterating forwards.
|
||||
current_column_fwd: u16,
|
||||
/// The x coordinate of the column within the `Rect` when iterating backwards.
|
||||
current_column_back: u16,
|
||||
}
|
||||
|
||||
impl Columns {
|
||||
/// Creates a new `Columns` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_column_fwd: rect.x,
|
||||
current_column_back: rect.right(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Columns {
|
||||
type Item = Rect;
|
||||
|
||||
/// Retrieves the next column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column_fwd >= self.current_column_back {
|
||||
return None;
|
||||
}
|
||||
let column = Rect::new(self.current_column_fwd, self.rect.y, 1, self.rect.height);
|
||||
self.current_column_fwd += 1;
|
||||
Some(column)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let start_count = self.current_column_fwd.saturating_sub(self.rect.left());
|
||||
let end_count = self.rect.right().saturating_sub(self.current_column_back);
|
||||
let count = self
|
||||
.rect
|
||||
.width
|
||||
.saturating_sub(start_count)
|
||||
.saturating_sub(end_count) as usize;
|
||||
(count, Some(count))
|
||||
}
|
||||
}
|
||||
|
||||
impl DoubleEndedIterator for Columns {
|
||||
/// Retrieves the previous column within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more columns to iterate through.
|
||||
fn next_back(&mut self) -> Option<Self::Item> {
|
||||
if self.current_column_back <= self.current_column_fwd {
|
||||
return None;
|
||||
}
|
||||
self.current_column_back -= 1;
|
||||
let column = Rect::new(self.current_column_back, self.rect.y, 1, self.rect.height);
|
||||
Some(column)
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator over positions within a `Rect`.
|
||||
///
|
||||
/// The iterator will yield all positions within the `Rect` in a row-major order.
|
||||
pub struct Positions {
|
||||
/// The `Rect` associated with the positions.
|
||||
rect: Rect,
|
||||
/// The current position within the `Rect`.
|
||||
current_position: Position,
|
||||
}
|
||||
|
||||
impl Positions {
|
||||
/// Creates a new `Positions` iterator.
|
||||
pub const fn new(rect: Rect) -> Self {
|
||||
Self {
|
||||
rect,
|
||||
current_position: Position::new(rect.x, rect.y),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Positions {
|
||||
type Item = Position;
|
||||
|
||||
/// Retrieves the next position within the `Rect`.
|
||||
///
|
||||
/// Returns `None` when there are no more positions to iterate through.
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_position.y >= self.rect.bottom() {
|
||||
return None;
|
||||
}
|
||||
let position = self.current_position;
|
||||
self.current_position.x += 1;
|
||||
if self.current_position.x >= self.rect.right() {
|
||||
self.current_position.x = self.rect.x;
|
||||
self.current_position.y += 1;
|
||||
}
|
||||
Some(position)
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
let row_count = self.rect.bottom().saturating_sub(self.current_position.y);
|
||||
if row_count == 0 {
|
||||
return (0, Some(0));
|
||||
}
|
||||
let column_count = self.rect.right().saturating_sub(self.current_position.x);
|
||||
// subtract 1 from the row count to account for the current row
|
||||
let count = (row_count - 1)
|
||||
.saturating_mul(self.rect.width)
|
||||
.saturating_add(column_count) as usize;
|
||||
(count, Some(count))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rows() {
|
||||
let rect = Rect::new(0, 0, 2, 3);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.size_hint(), (3, Some(3)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (2, Some(2)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (1, Some(1)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 2, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next_back(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_back() {
|
||||
let rect = Rect::new(0, 0, 2, 3);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.size_hint(), (3, Some(3)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (2, Some(2)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (1, Some(1)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next_back(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rows_meet_in_the_middle() {
|
||||
let rect = Rect::new(0, 0, 2, 4);
|
||||
let mut rows = Rows::new(rect);
|
||||
assert_eq!(rows.size_hint(), (4, Some(4)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 0, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (3, Some(3)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 3, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (2, Some(2)));
|
||||
assert_eq!(rows.next(), Some(Rect::new(0, 1, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (1, Some(1)));
|
||||
assert_eq!(rows.next_back(), Some(Rect::new(0, 2, 2, 1)));
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
assert_eq!(rows.next_back(), None);
|
||||
assert_eq!(rows.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns() {
|
||||
let rect = Rect::new(0, 0, 3, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.size_hint(), (3, Some(3)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (2, Some(2)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (1, Some(1)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(2, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next_back(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns_back() {
|
||||
let rect = Rect::new(0, 0, 3, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.size_hint(), (3, Some(3)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (2, Some(2)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (1, Some(1)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next_back(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn columns_meet_in_the_middle() {
|
||||
let rect = Rect::new(0, 0, 4, 2);
|
||||
let mut columns = Columns::new(rect);
|
||||
assert_eq!(columns.size_hint(), (4, Some(4)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (3, Some(3)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(3, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (2, Some(2)));
|
||||
assert_eq!(columns.next(), Some(Rect::new(1, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (1, Some(1)));
|
||||
assert_eq!(columns.next_back(), Some(Rect::new(2, 0, 1, 2)));
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
assert_eq!(columns.next_back(), None);
|
||||
assert_eq!(columns.size_hint(), (0, Some(0)));
|
||||
}
|
||||
|
||||
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
|
||||
/// forward and skip the first `65534` columns, and expect the next column to be `65535` and
|
||||
/// the subsequent columns to be `None`.
|
||||
#[test]
|
||||
fn columns_max() {
|
||||
let rect = Rect::new(0, 0, u16::MAX, 1);
|
||||
let mut columns = Columns::new(rect).skip(usize::from(u16::MAX - 1));
|
||||
assert_eq!(columns.next(), Some(Rect::new(u16::MAX - 1, 0, 1, 1)));
|
||||
assert_eq!(columns.next(), None);
|
||||
}
|
||||
|
||||
/// We allow a total of `65536` columns in the range `(0..=65535)`. In this test we iterate
|
||||
/// backward and skip the last `65534` columns, and expect the next column to be `0` and the
|
||||
/// subsequent columns to be `None`.
|
||||
#[test]
|
||||
fn columns_min() {
|
||||
let rect = Rect::new(0, 0, u16::MAX, 1);
|
||||
let mut columns = Columns::new(rect).rev().skip(usize::from(u16::MAX - 1));
|
||||
assert_eq!(columns.next(), Some(Rect::new(0, 0, 1, 1)));
|
||||
assert_eq!(columns.next(), None);
|
||||
assert_eq!(columns.next(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positions() {
|
||||
let rect = Rect::new(0, 0, 2, 2);
|
||||
let mut positions = Positions::new(rect);
|
||||
assert_eq!(positions.size_hint(), (4, Some(4)));
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 0)));
|
||||
assert_eq!(positions.size_hint(), (3, Some(3)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 0)));
|
||||
assert_eq!(positions.size_hint(), (2, Some(2)));
|
||||
assert_eq!(positions.next(), Some(Position::new(0, 1)));
|
||||
assert_eq!(positions.size_hint(), (1, Some(1)));
|
||||
assert_eq!(positions.next(), Some(Position::new(1, 1)));
|
||||
assert_eq!(positions.size_hint(), (0, Some(0)));
|
||||
assert_eq!(positions.next(), None);
|
||||
assert_eq!(positions.size_hint(), (0, Some(0)));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::fmt;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::layout::Rect;
|
||||
|
||||
/// A simple size struct
|
||||
///
|
||||
47
ratatui-core/src/lib.rs
Normal file
47
ratatui-core/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
//! **ratatui-core** is the core library of the [ratatui] project,
|
||||
//! providing the essential building blocks for creating rich terminal user interfaces in Rust.
|
||||
//!
|
||||
//! [ratatui]: https://github.com/ratatui/ratatui
|
||||
//!
|
||||
//! ## Why `ratatui-core`?
|
||||
//!
|
||||
//! The `ratatui-core` crate is split from the main [`ratatui`](https://crates.io/crates/ratatui) crate
|
||||
//! to offer better stability for widget library authors. Widget libraries should generally depend
|
||||
//! on `ratatui-core`, benefiting from a stable API and reducing the need for frequent updates.
|
||||
//!
|
||||
//! Applications, on the other hand, should depend on the main `ratatui` crate, which includes
|
||||
//! built-in widgets and additional features.
|
||||
//!
|
||||
//! # Installation
|
||||
//!
|
||||
//! Add `ratatui-core` to your `Cargo.toml`:
|
||||
//!
|
||||
//! ```shell
|
||||
//! cargo add ratatui-core
|
||||
//! ```
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
//! # Contributing
|
||||
//!
|
||||
//! We welcome contributions from the community! Please see our [CONTRIBUTING](../CONTRIBUTING.md)
|
||||
//! guide for more details on how to get involved.
|
||||
//!
|
||||
//! ## License
|
||||
//!
|
||||
//! This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
pub mod backend;
|
||||
pub mod buffer;
|
||||
pub mod layout;
|
||||
pub mod style;
|
||||
pub mod symbols;
|
||||
pub mod text;
|
||||
pub mod widgets;
|
||||
@@ -13,7 +13,10 @@
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::prelude::*;
|
||||
//! use ratatui_core::{
|
||||
//! style::{Color, Modifier, Style},
|
||||
//! text::Span,
|
||||
//! };
|
||||
//!
|
||||
//! let heading_style = Style::new()
|
||||
//! .fg(Color::Black)
|
||||
@@ -35,13 +38,15 @@
|
||||
//! - [`Span`]s can be styled again, which will merge the styles.
|
||||
//! - Many widget types can be styled directly rather than calling their `style()` method.
|
||||
//!
|
||||
//! See the [`Stylize`] and [`Styled`] traits for more information. These traits are re-exported in
|
||||
//! the [`prelude`] module for convenience.
|
||||
//! See the [`Stylize`] and [`Styled`] traits for more information.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! use ratatui_core::{
|
||||
//! style::{Color, Modifier, Style, Stylize},
|
||||
//! text::{Span, Text},
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! "hello".red().on_blue().bold(),
|
||||
@@ -55,8 +60,8 @@
|
||||
//! );
|
||||
//!
|
||||
//! assert_eq!(
|
||||
//! Paragraph::new("hello").red().on_blue().bold(),
|
||||
//! Paragraph::new("hello").style(
|
||||
//! Text::from("hello").red().on_blue().bold(),
|
||||
//! Text::from("hello").style(
|
||||
//! Style::default()
|
||||
//! .fg(Color::Red)
|
||||
//! .bg(Color::Blue)
|
||||
@@ -65,13 +70,13 @@
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! [`prelude`]: crate::prelude
|
||||
//! [`Span`]: crate::text::Span
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
pub use color::{Color, ParseColorError};
|
||||
use stylize::ColorDebugKind;
|
||||
pub use stylize::{Styled, Stylize};
|
||||
|
||||
mod color;
|
||||
@@ -91,7 +96,7 @@ bitflags! {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui_core::style::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
@@ -127,7 +132,7 @@ impl fmt::Debug for Modifier {
|
||||
/// Style lets you control the main characteristics of the displayed elements.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// Style::default()
|
||||
/// .fg(Color::Black)
|
||||
@@ -138,7 +143,8 @@ impl fmt::Debug for Modifier {
|
||||
/// Styles can also be created with a [shorthand notation](crate::style#using-style-shorthands).
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Style, Stylize};
|
||||
///
|
||||
/// Style::new().black().on_green().italic().bold();
|
||||
/// ```
|
||||
///
|
||||
@@ -148,7 +154,11 @@ impl fmt::Debug for Modifier {
|
||||
/// anywhere that accepts `Into<Style>`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// Line::styled("hello", Style::new().fg(Color::Red));
|
||||
/// // simplifies to
|
||||
/// Line::styled("hello", Color::Red);
|
||||
@@ -163,7 +173,11 @@ impl fmt::Debug for Modifier {
|
||||
/// just S3.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// };
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default()
|
||||
@@ -199,7 +213,11 @@ impl fmt::Debug for Modifier {
|
||||
/// reset all properties until that point use [`Style::reset`].
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// };
|
||||
///
|
||||
/// let styles = [
|
||||
/// Style::default()
|
||||
@@ -223,17 +241,32 @@ impl fmt::Debug for Modifier {
|
||||
/// buffer[(0, 0)].style(),
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[derive(Default, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Style {
|
||||
/// The foreground color.
|
||||
pub fg: Option<Color>,
|
||||
/// The background color.
|
||||
pub bg: Option<Color>,
|
||||
/// The underline color.
|
||||
#[cfg(feature = "underline-color")]
|
||||
pub underline_color: Option<Color>,
|
||||
/// The modifiers to add.
|
||||
pub add_modifier: Modifier,
|
||||
/// The modifiers to remove.
|
||||
pub sub_modifier: Modifier,
|
||||
}
|
||||
|
||||
/// A custom debug implementation that prints only the fields that are not the default, and unwraps
|
||||
/// the `Option`s.
|
||||
impl fmt::Debug for Style {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str("Style::new()")?;
|
||||
self.fmt_stylize(f)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Style {
|
||||
type Item = Self;
|
||||
|
||||
@@ -247,6 +280,7 @@ impl Styled for Style {
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` with default properties.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
fg: None,
|
||||
@@ -275,7 +309,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// let style = Style::default().fg(Color::Blue);
|
||||
/// let diff = Style::default().fg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
|
||||
@@ -291,7 +326,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// let style = Style::default().bg(Color::Blue);
|
||||
/// let diff = Style::default().bg(Color::Red);
|
||||
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
|
||||
@@ -315,7 +351,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// let style = Style::default()
|
||||
/// .underline_color(Color::Blue)
|
||||
/// .add_modifier(Modifier::UNDERLINED);
|
||||
@@ -343,7 +380,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Modifier, Style};
|
||||
///
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD);
|
||||
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -364,7 +402,8 @@ impl Style {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Modifier, Style};
|
||||
///
|
||||
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
|
||||
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
|
||||
/// let patched = style.patch(diff);
|
||||
@@ -386,7 +425,8 @@ impl Style {
|
||||
///
|
||||
/// ## Examples
|
||||
/// ```
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// let style_1 = Style::default().fg(Color::Yellow);
|
||||
/// let style_2 = Style::default().bg(Color::Red);
|
||||
/// let combined = style_1.patch(style_2);
|
||||
@@ -413,6 +453,54 @@ impl Style {
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Formats the style in a way that can be copy-pasted into code using the style shorthands.
|
||||
///
|
||||
/// This is useful for debugging and for generating code snippets.
|
||||
pub(crate) fn fmt_stylize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use fmt::Debug;
|
||||
if let Some(fg) = self.fg {
|
||||
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
|
||||
}
|
||||
if let Some(bg) = self.bg {
|
||||
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if let Some(underline_color) = self.underline_color {
|
||||
underline_color
|
||||
.stylize_debug(ColorDebugKind::Underline)
|
||||
.fmt(f)?;
|
||||
}
|
||||
for modifier in self.add_modifier.iter() {
|
||||
match modifier {
|
||||
Modifier::BOLD => f.write_str(".bold()")?,
|
||||
Modifier::DIM => f.write_str(".dim()")?,
|
||||
Modifier::ITALIC => f.write_str(".italic()")?,
|
||||
Modifier::UNDERLINED => f.write_str(".underlined()")?,
|
||||
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
|
||||
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
|
||||
Modifier::REVERSED => f.write_str(".reversed()")?,
|
||||
Modifier::HIDDEN => f.write_str(".hidden()")?,
|
||||
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
|
||||
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
|
||||
}
|
||||
}
|
||||
for modifier in self.sub_modifier.iter() {
|
||||
match modifier {
|
||||
Modifier::BOLD => f.write_str(".not_bold()")?,
|
||||
Modifier::DIM => f.write_str(".not_dim()")?,
|
||||
Modifier::ITALIC => f.write_str(".not_italic()")?,
|
||||
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
|
||||
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
|
||||
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
|
||||
Modifier::REVERSED => f.write_str(".not_reversed()")?,
|
||||
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
|
||||
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
|
||||
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for Style {
|
||||
@@ -423,7 +511,8 @@ impl From<Color> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// let style = Style::from(Color::Red);
|
||||
/// ```
|
||||
fn from(color: Color) -> Self {
|
||||
@@ -437,7 +526,8 @@ impl From<(Color, Color)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Style};
|
||||
///
|
||||
/// // red foreground, blue background
|
||||
/// let style = Style::from((Color::Red, Color::Blue));
|
||||
/// // default foreground, blue background
|
||||
@@ -459,7 +549,8 @@ impl From<Modifier> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Style, Modifier};
|
||||
///
|
||||
/// // add bold and italic
|
||||
/// let style = Style::from(Modifier::BOLD|Modifier::ITALIC);
|
||||
fn from(modifier: Modifier) -> Self {
|
||||
@@ -473,7 +564,8 @@ impl From<(Modifier, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Modifier, Style};
|
||||
///
|
||||
/// // add bold and italic, remove dim
|
||||
/// let style = Style::from((Modifier::BOLD | Modifier::ITALIC, Modifier::DIM));
|
||||
/// ```
|
||||
@@ -492,7 +584,8 @@ impl From<(Color, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// // red foreground, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
@@ -509,7 +602,8 @@ impl From<(Color, Color, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// // red foreground, blue background, add bold and italic
|
||||
/// let style = Style::from((Color::Red, Color::Blue, Modifier::BOLD | Modifier::ITALIC));
|
||||
/// ```
|
||||
@@ -525,7 +619,8 @@ impl From<(Color, Color, Modifier, Modifier)> for Style {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::{Color, Modifier, Style};
|
||||
///
|
||||
/// // red foreground, blue background, add bold and italic, remove dim
|
||||
/// let style = Style::from((
|
||||
/// Color::Red,
|
||||
@@ -549,6 +644,20 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(Style::new(), "Style::new()")]
|
||||
#[case(Style::new().red(), "Style::new().red()")]
|
||||
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
|
||||
#[case(Style::new().bold(), "Style::new().bold()")]
|
||||
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
|
||||
#[case(
|
||||
Style::new().red().on_blue().bold().italic().not_dim().not_hidden(),
|
||||
"Style::new().red().on_blue().bold().italic().not_dim().not_hidden()"
|
||||
)]
|
||||
fn debug(#[case] style: Style, #[case] expected: &'static str) {
|
||||
assert_eq!(format!("{style:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_patch_gives_same_result_as_individual_patch() {
|
||||
let styles = [
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use crate::style::stylize::{ColorDebug, ColorDebugKind};
|
||||
|
||||
/// ANSI Color
|
||||
///
|
||||
/// All colors from the [ANSI color table] are supported (though some names are not exactly the
|
||||
@@ -42,7 +44,7 @@ use std::{fmt, str::FromStr};
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// assert_eq!(Color::from_str("red"), Ok(Color::Red));
|
||||
/// assert_eq!("red".parse(), Ok(Color::Red));
|
||||
@@ -110,14 +112,12 @@ pub enum Color {
|
||||
/// Notably versions of Windows Terminal prior to Windows 10 and macOS Terminal.app do not
|
||||
/// support this.
|
||||
///
|
||||
/// If the terminal does not support true color, code using the [`TermwizBackend`] will
|
||||
/// If the terminal does not support true color, code using the `TermwizBackend` will
|
||||
/// fallback to the default text color. Crossterm and Termion do not have this capability and
|
||||
/// the display will be unpredictable (e.g. Terminal.app may display glitched blinking text).
|
||||
/// See <https://github.com/ratatui/ratatui/issues/475> for an example of this problem.
|
||||
///
|
||||
/// See also: <https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit>
|
||||
///
|
||||
/// [`TermwizBackend`]: crate::backend::TermwizBackend
|
||||
Rgb(u8, u8, u8),
|
||||
/// An 8-bit 256 color.
|
||||
///
|
||||
@@ -166,7 +166,9 @@ impl<'de> serde::Deserialize<'de> for Color {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// #[derive(Debug, serde::Deserialize)]
|
||||
/// struct Theme {
|
||||
@@ -261,7 +263,7 @@ impl std::error::Error for ParseColorError {}
|
||||
/// ```
|
||||
/// use std::str::FromStr;
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color: Color = Color::from_str("blue").unwrap();
|
||||
/// assert_eq!(color, Color::Blue);
|
||||
@@ -361,111 +363,114 @@ impl fmt::Display for Color {
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub(crate) const fn stylize_debug(self, kind: ColorDebugKind) -> ColorDebug {
|
||||
ColorDebug { kind, color: self }
|
||||
}
|
||||
|
||||
/// Converts a HSL representation to a `Color::Rgb` instance.
|
||||
///
|
||||
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a
|
||||
/// corresponding `Color` RGB equivalent.
|
||||
/// The `from_hsl` function converts the Hue, Saturation and Lightness values to a corresponding
|
||||
/// `Color` RGB equivalent.
|
||||
///
|
||||
/// Hue values should be in the range [0, 360].
|
||||
/// Saturation and L values should be in the range [0, 100].
|
||||
/// Values that are not in the range are clamped to be within the range.
|
||||
/// Hue values should be in the range [-180..180]. Values outside this range are normalized by
|
||||
/// wrapping.
|
||||
///
|
||||
/// Saturation and L values should be in the range [0.0..1.0]. Values outside this range are
|
||||
/// clamped.
|
||||
///
|
||||
/// Clamping to valid ranges happens before conversion to RGB.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::prelude::*;
|
||||
/// use palette::Hsl;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color: Color = Color::from_hsl(360.0, 100.0, 100.0);
|
||||
/// // Minimum Lightness is black
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
///
|
||||
/// // Maximum Lightness is white
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 1.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
///
|
||||
/// let color: Color = Color::from_hsl(0.0, 0.0, 0.0);
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
/// // Minimum Saturation is fully desaturated red = gray
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 0.0, 0.5));
|
||||
/// assert_eq!(color, Color::Rgb(128, 128, 128));
|
||||
///
|
||||
/// // Bright red
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(0.0, 1.0, 0.5));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// // Bright blue
|
||||
/// let color: Color = Color::from_hsl(Hsl::new(-120.0, 1.0, 0.5));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 255));
|
||||
/// ```
|
||||
pub fn from_hsl(h: f64, s: f64, l: f64) -> Self {
|
||||
// Clamp input values to valid ranges
|
||||
let h = h.clamp(0.0, 360.0);
|
||||
let s = s.clamp(0.0, 100.0);
|
||||
let l = l.clamp(0.0, 100.0);
|
||||
#[cfg(feature = "palette")]
|
||||
pub fn from_hsl(hsl: palette::Hsl) -> Self {
|
||||
use palette::{Clamp, FromColor, Srgb};
|
||||
let hsl = hsl.clamp();
|
||||
let Srgb {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
standard: _,
|
||||
}: Srgb<u8> = Srgb::from_color(hsl).into();
|
||||
|
||||
// Delegate to the function for normalized HSL to RGB conversion
|
||||
normalized_hsl_to_rgb(h / 360.0, s / 100.0, l / 100.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts normalized HSL (Hue, Saturation, Lightness) values to RGB (Red, Green, Blue) color
|
||||
/// representation. H, S, and L values should be in the range [0, 1].
|
||||
///
|
||||
/// Based on <https://github.com/killercup/hsl-rs/blob/b8a30e11afd75f262e0550725333293805f4ead0/src/lib.rs>
|
||||
fn normalized_hsl_to_rgb(hue: f64, saturation: f64, lightness: f64) -> Color {
|
||||
// This function can be made into `const` in the future.
|
||||
// This comment contains the relevant information for making it `const`.
|
||||
//
|
||||
// If it is `const` and made public, users can write the following:
|
||||
//
|
||||
// ```rust
|
||||
// const SLATE_50: Color = normalized_hsl_to_rgb(0.210, 0.40, 0.98);
|
||||
// ```
|
||||
//
|
||||
// For it to be const now, we need `#![feature(const_fn_floating_point_arithmetic)]`
|
||||
// Tracking issue: https://github.com/rust-lang/rust/issues/57241
|
||||
//
|
||||
// We would also need to remove the use of `.round()` in this function, i.e.:
|
||||
//
|
||||
// ```rust
|
||||
// Color::Rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)
|
||||
// ```
|
||||
|
||||
// Initialize RGB components
|
||||
let red: f64;
|
||||
let green: f64;
|
||||
let blue: f64;
|
||||
|
||||
// Check if the color is achromatic (grayscale)
|
||||
if saturation == 0.0 {
|
||||
red = lightness;
|
||||
green = lightness;
|
||||
blue = lightness;
|
||||
} else {
|
||||
// Calculate RGB components for colored cases
|
||||
let q = if lightness < 0.5 {
|
||||
lightness * (1.0 + saturation)
|
||||
} else {
|
||||
lightness + saturation - lightness * saturation
|
||||
};
|
||||
let p = 2.0 * lightness - q;
|
||||
red = hue_to_rgb(p, q, hue + 1.0 / 3.0);
|
||||
green = hue_to_rgb(p, q, hue);
|
||||
blue = hue_to_rgb(p, q, hue - 1.0 / 3.0);
|
||||
Self::Rgb(red, green, blue)
|
||||
}
|
||||
|
||||
// Scale RGB components to the range [0, 255] and create a Color::Rgb instance
|
||||
Color::Rgb(
|
||||
(red * 255.0).round() as u8,
|
||||
(green * 255.0).round() as u8,
|
||||
(blue * 255.0).round() as u8,
|
||||
)
|
||||
}
|
||||
/// Converts a `HSLuv` representation to a `Color::Rgb` instance.
|
||||
///
|
||||
/// The `from_hsluv` function converts the Hue, Saturation and Lightness values to a
|
||||
/// corresponding `Color` RGB equivalent.
|
||||
///
|
||||
/// Hue values should be in the range [-180.0..180.0]. Values outside this range are normalized
|
||||
/// by wrapping.
|
||||
///
|
||||
/// Saturation and L values should be in the range [0.0..100.0]. Values outside this range are
|
||||
/// clamped.
|
||||
///
|
||||
/// Clamping to valid ranges happens before conversion to RGB.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use palette::Hsluv;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// // Minimum Lightness is black
|
||||
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 100.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
///
|
||||
/// // Maximum Lightness is white
|
||||
/// let color: Color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 100.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
///
|
||||
/// // Minimum Saturation is fully desaturated red = gray
|
||||
/// let color = Color::from_hsluv(Hsluv::new(0.0, 0.0, 50.0));
|
||||
/// assert_eq!(color, Color::Rgb(119, 119, 119));
|
||||
///
|
||||
/// // Bright Red
|
||||
/// let color = Color::from_hsluv(Hsluv::new(12.18, 100.0, 53.2));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
///
|
||||
/// // Bright Blue
|
||||
/// let color = Color::from_hsluv(Hsluv::new(-94.13, 100.0, 32.3));
|
||||
/// assert_eq!(color, Color::Rgb(0, 0, 255));
|
||||
/// ```
|
||||
#[cfg(feature = "palette")]
|
||||
pub fn from_hsluv(hsluv: palette::Hsluv) -> Self {
|
||||
use palette::{Clamp, FromColor, Srgb};
|
||||
let hsluv = hsluv.clamp();
|
||||
let Srgb {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
standard: _,
|
||||
}: Srgb<u8> = Srgb::from_color(hsluv).into();
|
||||
|
||||
/// Helper function to calculate RGB component for a specific hue value.
|
||||
fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
|
||||
// Adjust the hue value to be within the valid range [0, 1]
|
||||
let mut t = t;
|
||||
if t < 0.0 {
|
||||
t += 1.0;
|
||||
}
|
||||
if t > 1.0 {
|
||||
t -= 1.0;
|
||||
}
|
||||
|
||||
// Calculate the RGB component based on the hue value
|
||||
if t < 1.0 / 6.0 {
|
||||
p + (q - p) * 6.0 * t
|
||||
} else if t < 1.0 / 2.0 {
|
||||
q
|
||||
} else if t < 2.0 / 3.0 {
|
||||
p + (q - p) * (2.0 / 3.0 - t) * 6.0
|
||||
} else {
|
||||
p
|
||||
Self::Rgb(red, green, blue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,36 +478,62 @@ fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 {
|
||||
mod tests {
|
||||
use std::error::Error;
|
||||
|
||||
#[cfg(feature = "palette")]
|
||||
use palette::{Hsl, Hsluv};
|
||||
#[cfg(feature = "palette")]
|
||||
use rstest::rstest;
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::de::{Deserialize, IntoDeserializer};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_hsl_to_rgb() {
|
||||
// Test with valid HSL values
|
||||
let color = Color::from_hsl(120.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(159, 223, 159));
|
||||
#[cfg(feature = "palette")]
|
||||
#[rstest]
|
||||
#[case::black(Hsl::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::white(Hsl::new(0.0, 0.0, 1.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::valid(Hsl::new(120.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
|
||||
#[case::min_hue(Hsl::new(-180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
|
||||
#[case::max_hue(Hsl::new(180.0, 0.5, 0.75), Color::Rgb(159, 223, 223))]
|
||||
#[case::min_saturation(Hsl::new(0.0, 0.0, 0.5), Color::Rgb(128, 128, 128))]
|
||||
#[case::max_saturation(Hsl::new(0.0, 1.0, 0.5), Color::Rgb(255, 0, 0))]
|
||||
#[case::min_lightness(Hsl::new(0.0, 0.5, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::max_lightness(Hsl::new(0.0, 0.5, 1.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_hue_wraps(Hsl::new(-240.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
|
||||
#[case::over_hue_wraps(Hsl::new(480.0, 0.5, 0.75), Color::Rgb(159, 223, 159))]
|
||||
#[case::under_saturation_clamps(Hsl::new(0.0, -0.5, 0.75), Color::Rgb(191, 191, 191))]
|
||||
#[case::over_saturation_clamps(Hsl::new(0.0, 1.2, 0.75), Color::Rgb(255, 128, 128))]
|
||||
#[case::under_lightness_clamps(Hsl::new(0.0, 0.5, -0.20), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_lightness_clamps(Hsl::new(0.0, 0.5, 1.5), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_saturation_lightness_clamps(Hsl::new(0.0, -0.5, -0.20), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_saturation_lightness_clamps(Hsl::new(0.0, 1.2, 1.5), Color::Rgb(255, 255, 255))]
|
||||
fn test_hsl_to_rgb(#[case] hsl: palette::Hsl, #[case] expected: Color) {
|
||||
assert_eq!(Color::from_hsl(hsl), expected);
|
||||
}
|
||||
|
||||
// Test with H value at upper bound
|
||||
let color = Color::from_hsl(360.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(223, 159, 159));
|
||||
|
||||
// Test with H value exceeding the upper bound
|
||||
let color = Color::from_hsl(400.0, 50.0, 75.0);
|
||||
assert_eq!(color, Color::Rgb(223, 159, 159));
|
||||
|
||||
// Test with S and L values exceeding the upper bound
|
||||
let color = Color::from_hsl(240.0, 120.0, 150.0);
|
||||
assert_eq!(color, Color::Rgb(255, 255, 255));
|
||||
|
||||
// Test with H, S, and L values below the lower bound
|
||||
let color = Color::from_hsl(-20.0, -50.0, -20.0);
|
||||
assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
|
||||
// Test with S and L values below the lower bound
|
||||
let color = Color::from_hsl(60.0, -20.0, -10.0);
|
||||
assert_eq!(color, Color::Rgb(0, 0, 0));
|
||||
#[cfg(feature = "palette")]
|
||||
#[rstest]
|
||||
#[case::black(Hsluv::new(0.0, 0.0, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::white(Hsluv::new(0.0, 0.0, 100.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::valid(Hsluv::new(120.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
|
||||
#[case::min_hue(Hsluv::new(-180.0, 50.0, 75.0), Color::Rgb(135,196, 188))]
|
||||
#[case::max_hue(Hsluv::new(180.0, 50.0, 75.0), Color::Rgb(135, 196, 188))]
|
||||
#[case::min_saturation(Hsluv::new(0.0, 0.0, 75.0), Color::Rgb(185, 185, 185))]
|
||||
#[case::max_saturation(Hsluv::new(0.0, 100.0, 75.0), Color::Rgb(255, 156, 177))]
|
||||
#[case::min_lightness(Hsluv::new(0.0, 50.0, 0.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::max_lightness(Hsluv::new(0.0, 50.0, 100.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_hue_wraps(Hsluv::new(-240.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
|
||||
#[case::over_hue_wraps(Hsluv::new(480.0, 50.0, 75.0), Color::Rgb(147, 198, 129))]
|
||||
#[case::under_saturation_clamps(Hsluv::new(0.0, -50.0, 75.0), Color::Rgb(185, 185, 185))]
|
||||
#[case::over_saturation_clamps(Hsluv::new(0.0, 150.0, 75.0), Color::Rgb(255, 156, 177))]
|
||||
#[case::under_lightness_clamps(Hsluv::new(0.0, 50.0, -20.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_lightness_clamps(Hsluv::new(0.0, 50.0, 150.0), Color::Rgb(255, 255, 255))]
|
||||
#[case::under_saturation_lightness_clamps(Hsluv::new(0.0, -50.0, -20.0), Color::Rgb(0, 0, 0))]
|
||||
#[case::over_saturation_lightness_clamps(
|
||||
Hsluv::new(0.0, 150.0, 150.0),
|
||||
Color::Rgb(255, 255, 255)
|
||||
)]
|
||||
fn test_hsluv_to_rgb(#[case] hsluv: palette::Hsluv, #[case] expected: Color) {
|
||||
assert_eq!(Color::from_hsluv(hsluv), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -403,8 +403,10 @@
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::material::{BLUE, RED};
|
||||
//! use ratatui_core::style::{
|
||||
//! palette::material::{BLUE, RED},
|
||||
//! Color,
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(244, 67, 54));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(33, 150, 243));
|
||||
@@ -412,7 +414,7 @@
|
||||
//!
|
||||
//! [`matdesign-color` crate]: https://crates.io/crates/matdesign-color
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::style::Color;
|
||||
|
||||
/// A palette of colors for use in Material design with accent colors
|
||||
///
|
||||
@@ -268,14 +268,16 @@
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use ratatui::prelude::*;
|
||||
//! use ratatui::style::palette::tailwind::{BLUE, RED};
|
||||
//! use ratatui_core::style::{
|
||||
//! palette::tailwind::{BLUE, RED},
|
||||
//! Color,
|
||||
//! };
|
||||
//!
|
||||
//! assert_eq!(RED.c500, Color::Rgb(239, 68, 68));
|
||||
//! assert_eq!(BLUE.c500, Color::Rgb(59, 130, 246));
|
||||
//! ```
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::style::Color;
|
||||
|
||||
pub struct Palette {
|
||||
pub c50: Color,
|
||||
@@ -7,7 +7,7 @@ use ::palette::{
|
||||
};
|
||||
use palette::{stimulus::IntoStimulus, Srgb};
|
||||
|
||||
use super::Color;
|
||||
use crate::style::Color;
|
||||
|
||||
/// Convert an [`palette::Srgb`] color to a [`Color`].
|
||||
///
|
||||
@@ -15,7 +15,7 @@ use super::Color;
|
||||
///
|
||||
/// ```
|
||||
/// use palette::Srgb;
|
||||
/// use ratatui::style::Color;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color = Color::from(Srgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
@@ -36,7 +36,7 @@ impl<T: IntoStimulus<u8>> From<Srgb<T>> for Color {
|
||||
///
|
||||
/// ```
|
||||
/// use palette::LinSrgb;
|
||||
/// use ratatui::style::Color;
|
||||
/// use ratatui_core::style::Color;
|
||||
///
|
||||
/// let color = Color::from(LinSrgb::new(1.0f32, 0.0, 0.0));
|
||||
/// assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::fmt;
|
||||
|
||||
use paste::paste;
|
||||
|
||||
use crate::{
|
||||
@@ -23,6 +25,75 @@ pub trait Styled {
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item;
|
||||
}
|
||||
|
||||
/// A helper struct to make it easy to debug using the `Stylize` method names
|
||||
pub(crate) struct ColorDebug {
|
||||
pub kind: ColorDebugKind,
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub(crate) enum ColorDebugKind {
|
||||
Foreground,
|
||||
Background,
|
||||
#[cfg(feature = "underline-color")]
|
||||
Underline,
|
||||
}
|
||||
|
||||
impl fmt::Debug for ColorDebug {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
#[cfg(feature = "underline-color")]
|
||||
let is_underline = self.kind == ColorDebugKind::Underline;
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
let is_underline = false;
|
||||
if is_underline
|
||||
|| matches!(
|
||||
self.color,
|
||||
Color::Reset | Color::Indexed(_) | Color::Rgb(_, _, _)
|
||||
)
|
||||
{
|
||||
match self.kind {
|
||||
ColorDebugKind::Foreground => write!(f, ".fg(")?,
|
||||
ColorDebugKind::Background => write!(f, ".bg(")?,
|
||||
#[cfg(feature = "underline-color")]
|
||||
ColorDebugKind::Underline => write!(f, ".underline_color(")?,
|
||||
}
|
||||
write!(f, "Color::{:?}", self.color)?;
|
||||
write!(f, ")")?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match self.kind {
|
||||
ColorDebugKind::Foreground => write!(f, ".")?,
|
||||
ColorDebugKind::Background => write!(f, ".on_")?,
|
||||
// TODO: .underline_color_xxx is not implemented on Stylize yet, but it should be
|
||||
#[cfg(feature = "underline-color")]
|
||||
ColorDebugKind::Underline => {
|
||||
unreachable!("covered by the first part of the if statement")
|
||||
}
|
||||
}
|
||||
match self.color {
|
||||
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, "dark_gray")?,
|
||||
Color::LightRed => write!(f, "light_red")?,
|
||||
Color::LightGreen => write!(f, "light_green")?,
|
||||
Color::LightYellow => write!(f, "light_yellow")?,
|
||||
Color::LightBlue => write!(f, "light_blue")?,
|
||||
Color::LightMagenta => write!(f, "light_magenta")?,
|
||||
Color::LightCyan => write!(f, "light_cyan")?,
|
||||
Color::White => write!(f, "white")?,
|
||||
_ => unreachable!("covered by the first part of the if statement"),
|
||||
}
|
||||
write!(f, "()")
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates two methods for each color, one for setting the foreground color (`red()`, `blue()`,
|
||||
/// etc) and one for setting the background color (`on_red()`, `on_blue()`, etc.). Each method sets
|
||||
/// the color of the style to the corresponding color.
|
||||
@@ -124,8 +195,12 @@ macro_rules! modifier {
|
||||
/// by `not_`). The `reset()` method is also provided to reset the style.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// ```ignore
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::Line,
|
||||
/// widgets::{Block, Paragraph},
|
||||
/// };
|
||||
///
|
||||
/// let span = "hello".red().on_blue().bold();
|
||||
/// let line = Line::from(vec![
|
||||
@@ -231,6 +306,7 @@ impl Styled for String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::Itertools;
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -423,4 +499,77 @@ mod tests {
|
||||
Span::styled("hello", all_modifier_black)
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Color::Black, ".black()")]
|
||||
#[case(Color::Red, ".red()")]
|
||||
#[case(Color::Green, ".green()")]
|
||||
#[case(Color::Yellow, ".yellow()")]
|
||||
#[case(Color::Blue, ".blue()")]
|
||||
#[case(Color::Magenta, ".magenta()")]
|
||||
#[case(Color::Cyan, ".cyan()")]
|
||||
#[case(Color::Gray, ".gray()")]
|
||||
#[case(Color::DarkGray, ".dark_gray()")]
|
||||
#[case(Color::LightRed, ".light_red()")]
|
||||
#[case(Color::LightGreen, ".light_green()")]
|
||||
#[case(Color::LightYellow, ".light_yellow()")]
|
||||
#[case(Color::LightBlue, ".light_blue()")]
|
||||
#[case(Color::LightMagenta, ".light_magenta()")]
|
||||
#[case(Color::LightCyan, ".light_cyan()")]
|
||||
#[case(Color::White, ".white()")]
|
||||
#[case(Color::Indexed(10), ".fg(Color::Indexed(10))")]
|
||||
#[case(Color::Rgb(255, 0, 0), ".fg(Color::Rgb(255, 0, 0))")]
|
||||
fn stylize_debug_foreground(#[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(ColorDebugKind::Foreground);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(Color::Black, ".on_black()")]
|
||||
#[case(Color::Red, ".on_red()")]
|
||||
#[case(Color::Green, ".on_green()")]
|
||||
#[case(Color::Yellow, ".on_yellow()")]
|
||||
#[case(Color::Blue, ".on_blue()")]
|
||||
#[case(Color::Magenta, ".on_magenta()")]
|
||||
#[case(Color::Cyan, ".on_cyan()")]
|
||||
#[case(Color::Gray, ".on_gray()")]
|
||||
#[case(Color::DarkGray, ".on_dark_gray()")]
|
||||
#[case(Color::LightRed, ".on_light_red()")]
|
||||
#[case(Color::LightGreen, ".on_light_green()")]
|
||||
#[case(Color::LightYellow, ".on_light_yellow()")]
|
||||
#[case(Color::LightBlue, ".on_light_blue()")]
|
||||
#[case(Color::LightMagenta, ".on_light_magenta()")]
|
||||
#[case(Color::LightCyan, ".on_light_cyan()")]
|
||||
#[case(Color::White, ".on_white()")]
|
||||
#[case(Color::Indexed(10), ".bg(Color::Indexed(10))")]
|
||||
#[case(Color::Rgb(255, 0, 0), ".bg(Color::Rgb(255, 0, 0))")]
|
||||
fn stylize_debug_background(#[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(ColorDebugKind::Background);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
#[rstest]
|
||||
#[case(Color::Black, ".underline_color(Color::Black)")]
|
||||
#[case(Color::Red, ".underline_color(Color::Red)")]
|
||||
#[case(Color::Green, ".underline_color(Color::Green)")]
|
||||
#[case(Color::Yellow, ".underline_color(Color::Yellow)")]
|
||||
#[case(Color::Blue, ".underline_color(Color::Blue)")]
|
||||
#[case(Color::Magenta, ".underline_color(Color::Magenta)")]
|
||||
#[case(Color::Cyan, ".underline_color(Color::Cyan)")]
|
||||
#[case(Color::Gray, ".underline_color(Color::Gray)")]
|
||||
#[case(Color::DarkGray, ".underline_color(Color::DarkGray)")]
|
||||
#[case(Color::LightRed, ".underline_color(Color::LightRed)")]
|
||||
#[case(Color::LightGreen, ".underline_color(Color::LightGreen)")]
|
||||
#[case(Color::LightYellow, ".underline_color(Color::LightYellow)")]
|
||||
#[case(Color::LightBlue, ".underline_color(Color::LightBlue)")]
|
||||
#[case(Color::LightMagenta, ".underline_color(Color::LightMagenta)")]
|
||||
#[case(Color::LightCyan, ".underline_color(Color::LightCyan)")]
|
||||
#[case(Color::White, ".underline_color(Color::White)")]
|
||||
#[case(Color::Indexed(10), ".underline_color(Color::Indexed(10))")]
|
||||
#[case(Color::Rgb(255, 0, 0), ".underline_color(Color::Rgb(255, 0, 0))")]
|
||||
fn stylize_debug_underline(#[case] color: Color, #[case] expected: &str) {
|
||||
let debug = color.stylize_debug(ColorDebugKind::Underline);
|
||||
assert_eq!(format!("{debug:?}"), expected);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//! Symbols and markers for drawing various widgets.
|
||||
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
pub mod border;
|
||||
@@ -155,7 +157,7 @@ pub enum Marker {
|
||||
}
|
||||
|
||||
pub mod scrollbar {
|
||||
use super::{block, line};
|
||||
use crate::symbols::{block, line};
|
||||
|
||||
/// Scrollbar Set
|
||||
/// ```text
|
||||
@@ -203,6 +205,14 @@ pub mod scrollbar {
|
||||
};
|
||||
}
|
||||
|
||||
pub mod shade {
|
||||
pub const EMPTY: &str = " ";
|
||||
pub const LIGHT: &str = "░";
|
||||
pub const MEDIUM: &str = "▒";
|
||||
pub const DARK: &str = "▓";
|
||||
pub const FULL: &str = "█";
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use strum::ParseError;
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::{block, line};
|
||||
use crate::symbols::{block, line};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub struct Set {
|
||||
@@ -1,25 +1,29 @@
|
||||
//! Primitives for styled text.
|
||||
//!
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish,
|
||||
//! those strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! A terminal UI is at its root a lot of strings. In order to make it accessible and stylish, those
|
||||
//! strings may be associated to a set of styles. `ratatui` has three ways to represent them:
|
||||
//! - A single line string where all graphemes have the same style is represented by a [`Span`].
|
||||
//! - A single line string where each grapheme may have its own style is represented by [`Line`].
|
||||
//! - A multiple line string where each grapheme may have its own style is represented by a
|
||||
//! [`Text`].
|
||||
//!
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`]
|
||||
//! is a [`Line`].
|
||||
//! These types form a hierarchy: [`Line`] is a collection of [`Span`] and each line of [`Text`] is
|
||||
//! a [`Line`].
|
||||
//!
|
||||
//! Keep it mind that a lot of widgets will use those types to advertise what kind of string is
|
||||
//! supported for their properties. Moreover, `ratatui` provides convenient `From` implementations
|
||||
//! so that you can start by using simple `String` or `&str` and then promote them to the previous
|
||||
//! primitives when you need additional styling capabilities.
|
||||
//!
|
||||
//! For example, for the [`crate::widgets::Block`] widget, all the following calls are valid to set
|
||||
//! its `title` property (which is a [`Line`] under the hood):
|
||||
//! For example, for the `Block` widget, all the following calls are valid to set its `title`
|
||||
//! property (which is a [`Line`] under the hood):
|
||||
//!
|
||||
//! ```rust
|
||||
//! use ratatui::{prelude::*, widgets::*};
|
||||
//! ```rust,ignore
|
||||
//! use ratatui_core::{
|
||||
//! style::{Color, Style},
|
||||
//! text::{Line, Span},
|
||||
//! widgets::Block,
|
||||
//! };
|
||||
//!
|
||||
//! // A simple string with no styling.
|
||||
//! // Converted to Line(vec![
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{prelude::*, style::Styled};
|
||||
use crate::style::{Style, Styled};
|
||||
|
||||
const NBSP: &str = "\u{00a0}";
|
||||
const ZWSP: &str = "\u{200b}";
|
||||
@@ -19,6 +19,8 @@ impl<'a> StyledGrapheme<'a> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn new<S: Into<Style>>(symbol: &'a str, style: S) -> Self {
|
||||
Self {
|
||||
symbol,
|
||||
@@ -26,7 +28,7 @@ impl<'a> StyledGrapheme<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_whitespace(&self) -> bool {
|
||||
pub fn is_whitespace(&self) -> bool {
|
||||
let symbol = self.symbol;
|
||||
symbol == ZWSP || symbol.chars().all(char::is_whitespace) && symbol != NBSP
|
||||
}
|
||||
@@ -48,6 +50,7 @@ impl<'a> Styled for StyledGrapheme<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn new() {
|
||||
251
src/text/line.rs → ratatui-core/src/text/line.rs
Normal file → Executable file
251
src/text/line.rs → ratatui-core/src/text/line.rs
Normal file → Executable file
@@ -4,7 +4,13 @@ use std::{borrow::Cow, fmt};
|
||||
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
|
||||
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{Span, StyledGrapheme, Text},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
/// A line of text, consisting of one or more [`Span`]s.
|
||||
///
|
||||
@@ -69,7 +75,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// [`Style`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::{Line, Span},
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow();
|
||||
/// let line = Line::raw("Hello, world!").style(style);
|
||||
@@ -93,7 +102,11 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// methods of the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// let line = Line::from("Hello world!").style(Color::Yellow);
|
||||
/// let line = Line::from("Hello world!").style((Color::Yellow, Color::Black));
|
||||
@@ -108,7 +121,8 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// ignored and the line is truncated.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{layout::Alignment, text::Line};
|
||||
///
|
||||
/// let line = Line::from("Hello world!").alignment(Alignment::Right);
|
||||
/// let line = Line::from("Hello world!").centered();
|
||||
/// let line = Line::from("Hello world!").left_aligned();
|
||||
@@ -120,26 +134,44 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// `Line` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// widgets::Widget,
|
||||
/// };
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// // in another widget's render method
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// line.render(area, buf);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Or you can use the `render_widget` method on the `Frame` in a `Terminal::draw` closure.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::{Frame, layout::Rect, text::Line};
|
||||
/// # fn draw(frame: &mut Frame, area: Rect) {
|
||||
/// // in a terminal.draw closure
|
||||
/// let line = Line::from("Hello world!").style(Style::new().yellow().italic());
|
||||
/// let line = Line::from("Hello world!");
|
||||
/// frame.render_widget(line, area);
|
||||
/// # }
|
||||
/// ```
|
||||
/// ## Rendering Lines with a Paragraph widget
|
||||
///
|
||||
/// Usually apps will use the [`Paragraph`] widget instead of rendering a [`Line`] directly as it
|
||||
/// Usually apps will use the `Paragraph` widget instead of rendering a [`Line`] directly as it
|
||||
/// provides more functionality.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// style::Stylize,
|
||||
/// text::Line,
|
||||
/// widgets::{Paragraph, Widget, Wrap},
|
||||
/// };
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let line = Line::from("Hello world!").yellow().italic();
|
||||
/// Paragraph::new(line)
|
||||
@@ -148,17 +180,44 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Line<'a> {
|
||||
/// The spans that make up this line of text.
|
||||
pub spans: Vec<Span<'a>>,
|
||||
|
||||
/// The style of this line of text.
|
||||
pub style: Style,
|
||||
|
||||
/// The alignment of this line of text.
|
||||
pub alignment: Option<Alignment>,
|
||||
|
||||
/// The spans that make up this line of text.
|
||||
pub spans: Vec<Span<'a>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Line<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.spans.is_empty() {
|
||||
f.write_str("Line::default()")?;
|
||||
} else if self.spans.len() == 1 && self.spans[0].style == Style::default() {
|
||||
f.write_str(r#"Line::from(""#)?;
|
||||
f.write_str(&self.spans[0].content)?;
|
||||
f.write_str(r#"")"#)?;
|
||||
} else if self.spans.len() == 1 {
|
||||
f.write_str("Line::from(")?;
|
||||
self.spans[0].fmt(f)?;
|
||||
f.write_str(")")?;
|
||||
} else {
|
||||
f.write_str("Line::from_iter(")?;
|
||||
f.debug_list().entries(&self.spans).finish()?;
|
||||
f.write_str(")")?;
|
||||
}
|
||||
self.style.fmt_stylize(f)?;
|
||||
match self.alignment {
|
||||
Some(Alignment::Left) => write!(f, ".left_aligned()"),
|
||||
Some(Alignment::Center) => write!(f, ".centered()"),
|
||||
Some(Alignment::Right) => write!(f, ".right_aligned()"),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cow_to_spans<'a>(content: impl Into<Cow<'a, str>>) -> Vec<Span<'a>> {
|
||||
@@ -182,8 +241,10 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// Line::raw("test content");
|
||||
/// Line::raw(String::from("test content"));
|
||||
/// Line::raw(Cow::from("test content"));
|
||||
@@ -211,13 +272,20 @@ impl<'a> Line<'a> {
|
||||
/// Any newlines in the content are removed.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use std::borrow::Cow;
|
||||
/// use std::borrow::Cow;
|
||||
///
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// Line::styled("My text", style);
|
||||
/// Line::styled(String::from("My text"), style);
|
||||
/// Line::styled(Cow::from("test content"), style);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
@@ -238,7 +306,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{style::Stylize, text::Line};
|
||||
///
|
||||
/// let line = Line::default().spans(vec!["Hello".blue(), " world!".green()]);
|
||||
/// let line = Line::default().spans([1, 2, 3].iter().map(|i| format!("Item {}", i)));
|
||||
/// ```
|
||||
@@ -265,9 +334,15 @@ impl<'a> Line<'a> {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let mut line = Line::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -283,7 +358,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{layout::Alignment, text::Line};
|
||||
///
|
||||
/// let mut line = Line::from("Hi, what's up?");
|
||||
/// assert_eq!(None, line.alignment);
|
||||
/// assert_eq!(
|
||||
@@ -308,7 +384,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// let line = Line::from("Hi, what's up?").left_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -325,7 +402,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// let line = Line::from("Hi, what's up?").centered();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -342,7 +420,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Line;
|
||||
///
|
||||
/// let line = Line::from("Hi, what's up?").right_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -355,7 +434,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{style::Stylize, text::Line};
|
||||
///
|
||||
/// let line = Line::from(vec!["Hello".blue(), " world!".green()]);
|
||||
/// assert_eq!(12, line.width());
|
||||
/// ```
|
||||
@@ -376,7 +456,10 @@ impl<'a> Line<'a> {
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Style},
|
||||
/// text::{Line, StyledGrapheme},
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::styled("Text", Style::default().fg(Color::Yellow));
|
||||
/// let style = Style::default().fg(Color::Green).bg(Color::Black);
|
||||
@@ -391,6 +474,8 @@ impl<'a> Line<'a> {
|
||||
/// ]
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: S,
|
||||
@@ -415,13 +500,19 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::styled("My text", Modifier::ITALIC);
|
||||
///
|
||||
/// let styled_line = Line::styled("My text", (Color::Yellow, Modifier::ITALIC));
|
||||
///
|
||||
/// assert_eq!(styled_line, line.patch_style(Color::Yellow));
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
@@ -437,8 +528,12 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # let style = Style::default().yellow();
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Line,
|
||||
/// };
|
||||
///
|
||||
/// let line = Line::styled("My text", style);
|
||||
///
|
||||
/// assert_eq!(Style::reset(), line.reset_style().style);
|
||||
@@ -466,7 +561,8 @@ impl<'a> Line<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::{Line, Span};
|
||||
///
|
||||
/// let mut line = Line::from("Hello, ");
|
||||
/// line.push_span(Span::raw("world!"));
|
||||
/// line.push_span(" How are you?");
|
||||
@@ -515,6 +611,12 @@ impl<'a> From<&'a str> for Line<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, str>> for Line<'a> {
|
||||
fn from(s: Cow<'a, str>) -> Self {
|
||||
Self::raw(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Vec<Span<'a>>> for Line<'a> {
|
||||
fn from(spans: Vec<Span<'a>>) -> Self {
|
||||
Self {
|
||||
@@ -581,12 +683,25 @@ impl<'a> Extend<Span<'a>> for Line<'a> {
|
||||
|
||||
impl Widget for Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Line<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Line<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_with_alignment(area, buf, None);
|
||||
}
|
||||
}
|
||||
|
||||
impl Line<'_> {
|
||||
/// An internal implementation method for `Widget::render` that allows the parent widget to
|
||||
/// define a default alignment, to be used if `Line::alignment` is `None`.
|
||||
pub(crate) fn render_with_alignment(
|
||||
&self,
|
||||
area: Rect,
|
||||
buf: &mut Buffer,
|
||||
parent_alignment: Option<Alignment>,
|
||||
) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
@@ -599,10 +714,12 @@ impl WidgetRef for Line<'_> {
|
||||
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
let alignment = self.alignment.or(parent_alignment);
|
||||
|
||||
let area_width = usize::from(area.width);
|
||||
let can_render_complete_line = line_width <= area_width;
|
||||
if can_render_complete_line {
|
||||
let indent_width = match self.alignment {
|
||||
let indent_width = match alignment {
|
||||
Some(Alignment::Center) => (area_width.saturating_sub(line_width)) / 2,
|
||||
Some(Alignment::Right) => area_width.saturating_sub(line_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
@@ -613,7 +730,7 @@ impl WidgetRef for Line<'_> {
|
||||
} else {
|
||||
// There is not enough space to render the whole line. As the right side is truncated by
|
||||
// the area width, only truncate the left.
|
||||
let skip_width = match self.alignment {
|
||||
let skip_width = match alignment {
|
||||
Some(Alignment::Center) => (line_width.saturating_sub(area_width)) / 2,
|
||||
Some(Alignment::Right) => line_width.saturating_sub(area_width),
|
||||
Some(Alignment::Left) | None => 0,
|
||||
@@ -630,7 +747,7 @@ fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_widt
|
||||
if area.is_empty() {
|
||||
break;
|
||||
}
|
||||
span.render_ref(area, buf);
|
||||
span.render(area, buf);
|
||||
let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
|
||||
area = area.indent_x(span_width);
|
||||
}
|
||||
@@ -732,6 +849,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
@@ -1201,7 +1319,7 @@ mod tests {
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
|
||||
);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([
|
||||
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
|
||||
]));
|
||||
@@ -1238,7 +1356,7 @@ mod tests {
|
||||
) {
|
||||
let line = Line::from("1234🦀7890").alignment(alignment);
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1291,7 +1409,7 @@ mod tests {
|
||||
};
|
||||
let line = Line::from(value).centered();
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1309,7 +1427,7 @@ mod tests {
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X"));
|
||||
let area = Rect::new(2, 0, 6, 1);
|
||||
line.render_ref(area, &mut buf);
|
||||
line.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1327,7 +1445,7 @@ mod tests {
|
||||
let area = Rect::new(0, 0, buf_width, 1);
|
||||
// Fill buffer with stuff to ensure the output is indeed padded
|
||||
let mut buf = Buffer::filled(area, Cell::new("X"));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1358,7 +1476,7 @@ mod tests {
|
||||
fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
|
||||
let line = Line::from("🇺🇸1234");
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1382,7 +1500,7 @@ mod tests {
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1406,7 +1524,7 @@ mod tests {
|
||||
assert!(line.width() >= min_width);
|
||||
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
|
||||
line.render_ref(buf.area, &mut buf);
|
||||
line.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines([expected]));
|
||||
}
|
||||
|
||||
@@ -1505,4 +1623,49 @@ mod tests {
|
||||
assert_eq!(result, "Hello world!");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::empty(Line::default(), "Line::default()")]
|
||||
#[case::raw(Line::raw("Hello, world!"), r#"Line::from("Hello, world!")"#)]
|
||||
#[case::styled(
|
||||
Line::styled("Hello, world!", Color::Yellow),
|
||||
r#"Line::from("Hello, world!").yellow()"#
|
||||
)]
|
||||
#[case::styled_complex(
|
||||
Line::from(String::from("Hello, world!")).green().on_blue().bold().italic().not_dim(),
|
||||
r#"Line::from("Hello, world!").green().on_blue().bold().italic().not_dim()"#
|
||||
)]
|
||||
#[case::styled_span(
|
||||
Line::from(Span::styled("Hello, world!", Color::Yellow)),
|
||||
r#"Line::from(Span::from("Hello, world!").yellow())"#
|
||||
)]
|
||||
#[case::styled_line_and_span(
|
||||
Line::from(vec![
|
||||
Span::styled("Hello", Color::Yellow),
|
||||
Span::styled(" world!", Color::Green),
|
||||
]).italic(),
|
||||
r#"Line::from_iter([Span::from("Hello").yellow(), Span::from(" world!").green()]).italic()"#
|
||||
)]
|
||||
#[case::spans_vec(
|
||||
Line::from(vec![
|
||||
Span::styled("Hello", Color::Blue),
|
||||
Span::styled(" world!", Color::Green),
|
||||
]),
|
||||
r#"Line::from_iter([Span::from("Hello").blue(), Span::from(" world!").green()])"#,
|
||||
)]
|
||||
#[case::left_aligned(
|
||||
Line::from("Hello, world!").left_aligned(),
|
||||
r#"Line::from("Hello, world!").left_aligned()"#
|
||||
)]
|
||||
#[case::centered(
|
||||
Line::from("Hello, world!").centered(),
|
||||
r#"Line::from("Hello, world!").centered()"#
|
||||
)]
|
||||
#[case::right_aligned(
|
||||
Line::from("Hello, world!").right_aligned(),
|
||||
r#"Line::from("Hello, world!").right_aligned()"#
|
||||
)]
|
||||
fn debug(#[case] line: Line, #[case] expected: &str) {
|
||||
assert_eq!(format!("{line:?}"), expected);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use super::Text;
|
||||
use crate::text::Text;
|
||||
|
||||
/// A wrapper around a string that is masked when displayed.
|
||||
///
|
||||
/// The masked string is displayed as a series of the same character.
|
||||
/// This might be used to display a password field or similar secure data.
|
||||
/// The masked string is displayed as a series of the same character. This might be used to display
|
||||
/// a password field or similar secure data.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui_core::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// text::{Masked, Text},
|
||||
/// widgets::Widget,
|
||||
/// };
|
||||
///
|
||||
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 5, 1));
|
||||
/// let password = Masked::new("12345", 'x');
|
||||
///
|
||||
/// Paragraph::new(password).render(buffer.area, &mut buffer);
|
||||
/// Text::from(password).render(buffer.area, &mut buffer);
|
||||
/// assert_eq!(buffer, Buffer::with_lines(["xxxxx"]));
|
||||
/// ```
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -3,7 +3,13 @@ use std::{borrow::Cow, fmt};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
text::{Line, StyledGrapheme},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
/// Represents a part of a line that is contiguous and where all characters share the same style.
|
||||
///
|
||||
@@ -36,7 +42,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// any type convertible to [`Cow<str>`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Span;
|
||||
///
|
||||
/// let span = Span::raw("test content");
|
||||
/// let span = Span::raw(String::from("test content"));
|
||||
@@ -50,7 +56,10 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled("test content", Style::new().green());
|
||||
/// let span = Span::styled(String::from("test content"), Style::new().green());
|
||||
@@ -64,7 +73,7 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// defined in the [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{style::Stylize, text::Span};
|
||||
///
|
||||
/// let span = Span::raw("test content").green().on_yellow().italic();
|
||||
/// let span = Span::raw(String::from("test content"))
|
||||
@@ -73,27 +82,40 @@ use crate::{prelude::*, style::Styled, text::StyledGrapheme};
|
||||
/// .italic();
|
||||
/// ```
|
||||
///
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Usually
|
||||
/// apps will use the [`Paragraph`] widget instead of rendering `Span` directly, as it handles text
|
||||
/// `Span` implements the [`Widget`] trait, which allows it to be rendered to a [`Buffer`]. Often
|
||||
/// apps will use the `Paragraph` widget instead of rendering `Span` directly, as it handles text
|
||||
/// wrapping and alignment for you.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::prelude::*;
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{style::Stylize, Frame};
|
||||
///
|
||||
/// # fn render_frame(frame: &mut Frame) {
|
||||
/// frame.render_widget("test content".green().on_yellow().italic(), frame.area());
|
||||
/// # }
|
||||
/// ```
|
||||
/// [`Line`]: crate::text::Line
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Cow<str>`]: std::borrow::Cow
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Span<'a> {
|
||||
/// The content of the span as a Clone-on-write string.
|
||||
pub content: Cow<'a, str>,
|
||||
/// The style of the span.
|
||||
pub style: Style,
|
||||
/// The content of the span as a Clone-on-write string.
|
||||
pub content: Cow<'a, str>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Span<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.content.is_empty() {
|
||||
write!(f, "Span::default()")?;
|
||||
} else {
|
||||
write!(f, "Span::from({:?})", self.content)?;
|
||||
}
|
||||
if self.style != Style::default() {
|
||||
self.style.fmt_stylize(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Span<'a> {
|
||||
@@ -102,7 +124,8 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Span;
|
||||
///
|
||||
/// Span::raw("test content");
|
||||
/// Span::raw(String::from("test content"));
|
||||
/// ```
|
||||
@@ -127,11 +150,17 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow().on_green().italic();
|
||||
/// Span::styled("test content", style);
|
||||
/// Span::styled(String::from("test content"), style);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
@@ -153,7 +182,8 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Span;
|
||||
///
|
||||
/// let mut span = Span::default().content("content");
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -178,9 +208,15 @@ impl<'a> Span<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let mut span = Span::default().style(Style::new().green());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -197,11 +233,17 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled("test content", Style::new().green().italic())
|
||||
/// .patch_style(Style::new().red().on_yellow().bold());
|
||||
/// assert_eq!(span.style, Style::new().red().on_yellow().italic().bold());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
@@ -217,7 +259,11 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Span,
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled(
|
||||
/// "Test Content",
|
||||
/// Style::new().dark_gray().on_yellow().italic(),
|
||||
@@ -248,7 +294,10 @@ impl<'a> Span<'a> {
|
||||
/// ```rust
|
||||
/// use std::iter::Iterator;
|
||||
///
|
||||
/// use ratatui::{prelude::*, text::StyledGrapheme};
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::{Span, StyledGrapheme},
|
||||
/// };
|
||||
///
|
||||
/// let span = Span::styled("Test", Style::new().green().italic());
|
||||
/// let style = Style::new().red().on_yellow();
|
||||
@@ -263,6 +312,8 @@ impl<'a> Span<'a> {
|
||||
/// ],
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled_graphemes<S: Into<Style>>(
|
||||
&'a self,
|
||||
base_style: S,
|
||||
@@ -280,7 +331,8 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Stylize;
|
||||
///
|
||||
/// let line = "Test Content".green().italic().into_left_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -299,7 +351,8 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Stylize;
|
||||
///
|
||||
/// let line = "Test Content".green().italic().into_centered_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -318,7 +371,8 @@ impl<'a> Span<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::style::Stylize;
|
||||
///
|
||||
/// let line = "Test Content".green().italic().into_right_aligned_line();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -364,12 +418,12 @@ impl<'a> Styled for Span<'a> {
|
||||
|
||||
impl Widget for Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Span<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Span<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
@@ -452,10 +506,10 @@ impl fmt::Display for Span<'_> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use buffer::Cell;
|
||||
use rstest::fixture;
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
@@ -831,4 +885,16 @@ mod tests {
|
||||
Line::from(vec![Span::raw("test"), Span::raw("content")])
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default(Span::default(), "Span::default()")]
|
||||
#[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
|
||||
#[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
|
||||
#[case::styled_italic(
|
||||
Span::styled("test", Style::new().green().italic()),
|
||||
r#"Span::from("test").green().italic()"#
|
||||
)]
|
||||
fn debug(#[case] span: Span, #[case] expected: &str) {
|
||||
assert_eq!(format!("{span:?}"), expected);
|
||||
}
|
||||
}
|
||||
264
src/text/text.rs → ratatui-core/src/text/text.rs
Normal file → Executable file
264
src/text/text.rs → ratatui-core/src/text/text.rs
Normal file → Executable file
@@ -1,7 +1,13 @@
|
||||
#![warn(missing_docs)]
|
||||
use std::{borrow::Cow, fmt};
|
||||
|
||||
use crate::{prelude::*, style::Styled};
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{Line, Span},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
/// A string split over one or more lines.
|
||||
///
|
||||
@@ -62,7 +68,10 @@ use crate::{prelude::*, style::Styled};
|
||||
/// ```rust
|
||||
/// use std::{borrow::Cow, iter};
|
||||
///
|
||||
/// use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::{Line, Span, Text},
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::new().yellow().italic();
|
||||
/// let text = Text::raw("The first line\nThe second line").style(style);
|
||||
@@ -99,7 +108,11 @@ use crate::{prelude::*, style::Styled};
|
||||
/// [`Stylize`] trait.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style, Stylize},
|
||||
/// text::{Line, Text},
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line").style(Style::new().yellow().italic());
|
||||
/// let text = Text::from("The first line\nThe second line")
|
||||
/// .yellow()
|
||||
@@ -116,7 +129,11 @@ use crate::{prelude::*, style::Styled};
|
||||
/// Lines composing the text can also be individually aligned with [`Line::alignment`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// layout::Alignment,
|
||||
/// text::{Line, Text},
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line").alignment(Alignment::Right);
|
||||
/// let text = Text::from("The first line\nThe second line").right_aligned();
|
||||
/// let text = Text::from(vec![
|
||||
@@ -129,17 +146,23 @@ use crate::{prelude::*, style::Styled};
|
||||
///
|
||||
/// ## Rendering Text
|
||||
/// `Text` implements the [`Widget`] trait, which means it can be rendered to a [`Buffer`] or to a
|
||||
/// [`Frame`].
|
||||
/// `Frame`.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # use ratatui_core::{buffer::Buffer, layout::Rect};
|
||||
/// use ratatui_core::{text::Text, widgets::Widget};
|
||||
///
|
||||
/// // within another widget's `render` method:
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// text.render(area, buf);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// // within a terminal.draw closure:
|
||||
/// Or you can use the `render_widget` method on a `Frame` within a `Terminal::draw` closure.
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::{Frame, layout::Rect, text::Text};
|
||||
/// # fn draw(frame: &mut Frame, area: Rect) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// frame.render_widget(text, area);
|
||||
@@ -148,11 +171,17 @@ use crate::{prelude::*, style::Styled};
|
||||
///
|
||||
/// ## Rendering Text with a Paragraph Widget
|
||||
///
|
||||
/// Usually apps will use the [`Paragraph`] widget instead of rendering a `Text` directly as it
|
||||
/// Usually apps will use the `Paragraph` widget instead of rendering a `Text` directly as it
|
||||
/// provides more functionality.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{
|
||||
/// buffer::Buffer,
|
||||
/// layout::Rect,
|
||||
/// text::Text,
|
||||
/// widgets::{Paragraph, Widget, Wrap},
|
||||
/// };
|
||||
///
|
||||
/// # fn render(area: Rect, buf: &mut Buffer) {
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// let paragraph = Paragraph::new(text)
|
||||
@@ -163,14 +192,37 @@ use crate::{prelude::*, style::Styled};
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Text<'a> {
|
||||
/// The lines that make up this piece of text.
|
||||
pub lines: Vec<Line<'a>>,
|
||||
/// The style of this text.
|
||||
pub style: Style,
|
||||
/// The alignment of this text.
|
||||
pub alignment: Option<Alignment>,
|
||||
/// The style of this text.
|
||||
pub style: Style,
|
||||
/// The lines that make up this piece of text.
|
||||
pub lines: Vec<Line<'a>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for Text<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
if self.lines.is_empty() {
|
||||
f.write_str("Text::default()")?;
|
||||
} else if self.lines.len() == 1 {
|
||||
write!(f, "Text::from({:?})", self.lines[0])?;
|
||||
} else {
|
||||
f.write_str("Text::from_iter(")?;
|
||||
f.debug_list().entries(self.lines.iter()).finish()?;
|
||||
f.write_str(")")?;
|
||||
}
|
||||
self.style.fmt_stylize(f)?;
|
||||
match self.alignment {
|
||||
Some(Alignment::Left) => f.write_str(".left_aligned()")?,
|
||||
Some(Alignment::Center) => f.write_str(".centered()")?,
|
||||
Some(Alignment::Right) => f.write_str(".right_aligned()")?,
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Text<'a> {
|
||||
@@ -179,7 +231,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// Text::raw("The first line\nThe second line");
|
||||
/// Text::raw(String::from("The first line\nThe second line"));
|
||||
/// ```
|
||||
@@ -204,13 +257,19 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let style = Style::default()
|
||||
/// .fg(Color::Yellow)
|
||||
/// .add_modifier(Modifier::ITALIC);
|
||||
/// Text::styled("The first line\nThe second line", style);
|
||||
/// Text::styled(String::from("The first line\nThe second line"), style);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
pub fn styled<T, S>(content: T, style: S) -> Self
|
||||
where
|
||||
T: Into<Cow<'a, str>>,
|
||||
@@ -224,7 +283,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(15, text.width());
|
||||
/// ```
|
||||
@@ -237,7 +297,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("The first line\nThe second line");
|
||||
/// assert_eq!(2, text.height());
|
||||
/// ```
|
||||
@@ -258,9 +319,15 @@ impl<'a> Text<'a> {
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let mut line = Text::from("foo").style(Style::new().red());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -284,7 +351,11 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let raw_text = Text::styled("The first line\nThe second line", Modifier::ITALIC);
|
||||
/// let styled_text = Text::styled(
|
||||
/// String::from("The first line\nThe second line"),
|
||||
@@ -295,6 +366,9 @@ impl<'a> Text<'a> {
|
||||
/// let raw_text = raw_text.patch_style(Color::Yellow);
|
||||
/// assert_eq!(raw_text, styled_text);
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: crate::style::Color
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn patch_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = self.style.patch(style);
|
||||
@@ -310,7 +384,11 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// style::{Color, Modifier, Style},
|
||||
/// text::Text,
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::styled(
|
||||
/// "The first line\nThe second line",
|
||||
/// (Color::Yellow, Modifier::ITALIC),
|
||||
@@ -337,7 +415,8 @@ impl<'a> Text<'a> {
|
||||
/// Set alignment to the whole text.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{layout::Alignment, text::Text};
|
||||
///
|
||||
/// let mut text = Text::from("Hi, what's up?");
|
||||
/// assert_eq!(None, text.alignment);
|
||||
/// assert_eq!(
|
||||
@@ -349,7 +428,11 @@ impl<'a> Text<'a> {
|
||||
/// Set a default alignment and override it on a per line basis.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::{
|
||||
/// layout::Alignment,
|
||||
/// text::{Line, Text},
|
||||
/// };
|
||||
///
|
||||
/// let text = Text::from(vec![
|
||||
/// Line::from("left").alignment(Alignment::Left),
|
||||
/// Line::from("default"),
|
||||
@@ -386,7 +469,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("Hi, what's up?").left_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -405,7 +489,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("Hi, what's up?").centered();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -424,7 +509,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::Text;
|
||||
///
|
||||
/// let text = Text::from("Hi, what's up?").right_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -450,7 +536,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::{Line, Span, Text};
|
||||
///
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_line(Line::from("How are you?"));
|
||||
/// text.push_line(Span::from("How are you?"));
|
||||
@@ -468,7 +555,8 @@ impl<'a> Text<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use ratatui_core::text::{Span, Text};
|
||||
///
|
||||
/// let mut text = Text::from("Hello, world!");
|
||||
/// text.push_span(Span::from("How are you?"));
|
||||
/// text.push_span("How are you?");
|
||||
@@ -642,31 +730,16 @@ impl fmt::Display for Text<'_> {
|
||||
|
||||
impl Widget for Text<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Text<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Text<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
buf.set_style(area, self.style);
|
||||
for (line, row) in self.iter().zip(area.rows()) {
|
||||
let line_width = line.width() as u16;
|
||||
|
||||
let x_offset = match (self.alignment, line.alignment) {
|
||||
(Some(Alignment::Center), None) => area.width.saturating_sub(line_width) / 2,
|
||||
(Some(Alignment::Right), None) => area.width.saturating_sub(line_width),
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let line_area = Rect {
|
||||
x: area.x + x_offset,
|
||||
y: row.y,
|
||||
width: area.width - x_offset,
|
||||
height: 1,
|
||||
};
|
||||
|
||||
line.render(line_area, buf);
|
||||
for (line, line_area) in self.iter().zip(area.rows()) {
|
||||
line.render_with_alignment(line_area, buf, self.alignment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,6 +763,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
|
||||
#[fixture]
|
||||
fn small_buf() -> Buffer {
|
||||
@@ -1110,6 +1184,33 @@ mod tests {
|
||||
assert_eq!(buf, Buffer::with_lines([" foo "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_right_aligned_with_truncation() {
|
||||
let text = Text::from("123456789").alignment(Alignment::Right);
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["56789"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_odd_with_truncation() {
|
||||
let text = Text::from("123456789").alignment(Alignment::Center);
|
||||
let area = Rect::new(0, 0, 5, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["34567"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_centered_even_with_truncation() {
|
||||
let text = Text::from("123456789").alignment(Alignment::Center);
|
||||
let area = Rect::new(0, 0, 6, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
text.render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["234567"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_one_line_right() {
|
||||
let text = Text::from(vec![
|
||||
@@ -1232,4 +1333,69 @@ mod tests {
|
||||
assert_eq!(result, "Hello world!");
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::default(Text::default(), "Text::default()")]
|
||||
// TODO jm: these could be improved to inspect the line / span if there's only one. e.g.
|
||||
// Text::from("Hello, world!") and Text::from("Hello, world!".blue()) but the current
|
||||
// implementation is good enough for now.
|
||||
#[case::raw(
|
||||
Text::raw("Hello, world!"),
|
||||
r#"Text::from(Line::from("Hello, world!"))"#
|
||||
)]
|
||||
#[case::styled(
|
||||
Text::styled("Hello, world!", Color::Yellow),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow()"#
|
||||
)]
|
||||
#[case::complex_styled(
|
||||
Text::from("Hello, world!").yellow().on_blue().bold().italic().not_dim().not_hidden(),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow().on_blue().bold().italic().not_dim().not_hidden()"#
|
||||
)]
|
||||
#[case::alignment(
|
||||
Text::from("Hello, world!").centered(),
|
||||
r#"Text::from(Line::from("Hello, world!")).centered()"#
|
||||
)]
|
||||
#[case::styled_alignment(
|
||||
Text::styled("Hello, world!", Color::Yellow).centered(),
|
||||
r#"Text::from(Line::from("Hello, world!")).yellow().centered()"#
|
||||
)]
|
||||
#[case::multiple_lines(
|
||||
Text::from(vec![
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?")
|
||||
]),
|
||||
r#"Text::from_iter([Line::from("Hello, world!"), Line::from("How are you?")])"#
|
||||
)]
|
||||
fn debug(#[case] text: Text, #[case] expected: &str) {
|
||||
assert_eq!(format!("{text:?}"), expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn debug_alternate() {
|
||||
let text = Text::from_iter([
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?").bold().left_aligned(),
|
||||
Line::from_iter([
|
||||
Span::from("I'm "),
|
||||
Span::from("doing ").italic(),
|
||||
Span::from("great!").bold(),
|
||||
]),
|
||||
])
|
||||
.on_blue()
|
||||
.italic()
|
||||
.centered();
|
||||
assert_eq!(
|
||||
format!("{text:#?}"),
|
||||
indoc::indoc! {r#"
|
||||
Text::from_iter([
|
||||
Line::from("Hello, world!"),
|
||||
Line::from("How are you?").bold().left_aligned(),
|
||||
Line::from_iter([
|
||||
Span::from("I'm "),
|
||||
Span::from("doing ").italic(),
|
||||
Span::from("great!").bold(),
|
||||
]),
|
||||
]).on_blue().italic().centered()"#}
|
||||
);
|
||||
}
|
||||
}
|
||||
8
ratatui-core/src/widgets.rs
Normal file
8
ratatui-core/src/widgets.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
#![warn(missing_docs)]
|
||||
//! The `widgets` module contains the `Widget` and `StatefulWidget` traits, which are used to
|
||||
//! render UI elements on the screen.
|
||||
|
||||
pub use self::{stateful_widget::StatefulWidget, widget::Widget};
|
||||
|
||||
mod stateful_widget;
|
||||
mod widget;
|
||||
181
ratatui-core/src/widgets/stateful_widget.rs
Normal file
181
ratatui-core/src/widgets/stateful_widget.rs
Normal file
@@ -0,0 +1,181 @@
|
||||
use crate::{buffer::Buffer, layout::Rect};
|
||||
|
||||
/// A `StatefulWidget` is a widget that can take advantage of some local state to remember things
|
||||
/// between two draw calls.
|
||||
///
|
||||
/// Most widgets can be drawn directly based on the input parameters. However, some features may
|
||||
/// require some kind of associated state to be implemented.
|
||||
///
|
||||
/// For example, the `List` widget can highlight the item currently selected. This can be translated
|
||||
/// in an offset, which is the number of elements to skip in order to have the selected item within
|
||||
/// the viewport currently allocated to this widget. The widget can therefore only provide the
|
||||
/// following behavior: whenever the selected item is out of the viewport scroll to a predefined
|
||||
/// position (making the selected item the last viewable item or the one in the middle for example).
|
||||
/// Nonetheless, if the widget has access to the last computed offset then it can implement a
|
||||
/// natural scrolling experience where the last offset is reused until the selected item is out of
|
||||
/// the viewport.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use std::io;
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// backend::TestBackend,
|
||||
/// widgets::{List, ListItem, ListState, StatefulWidget, Widget},
|
||||
/// Terminal,
|
||||
/// };
|
||||
///
|
||||
/// // Let's say we have some events to display.
|
||||
/// struct Events {
|
||||
/// // `items` is the state managed by your application.
|
||||
/// items: Vec<String>,
|
||||
/// // `state` is the state that can be modified by the UI. It stores the index of the selected
|
||||
/// // item as well as the offset computed during the previous draw call (used to implement
|
||||
/// // natural scrolling).
|
||||
/// state: ListState,
|
||||
/// }
|
||||
///
|
||||
/// impl Events {
|
||||
/// fn new(items: Vec<String>) -> Events {
|
||||
/// Events {
|
||||
/// items,
|
||||
/// state: ListState::default(),
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// pub fn set_items(&mut self, items: Vec<String>) {
|
||||
/// self.items = items;
|
||||
/// // We reset the state as the associated items have changed. This effectively reset
|
||||
/// // the selection as well as the stored offset.
|
||||
/// self.state = ListState::default();
|
||||
/// }
|
||||
///
|
||||
/// // Select the next item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn next(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i >= self.items.len() - 1 {
|
||||
/// 0
|
||||
/// } else {
|
||||
/// i + 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Select the previous item. This will not be reflected until the widget is drawn in the
|
||||
/// // `Terminal::draw` callback using `Frame::render_stateful_widget`.
|
||||
/// pub fn previous(&mut self) {
|
||||
/// let i = match self.state.selected() {
|
||||
/// Some(i) => {
|
||||
/// if i == 0 {
|
||||
/// self.items.len() - 1
|
||||
/// } else {
|
||||
/// i - 1
|
||||
/// }
|
||||
/// }
|
||||
/// None => 0,
|
||||
/// };
|
||||
/// self.state.select(Some(i));
|
||||
/// }
|
||||
///
|
||||
/// // Unselect the currently selected item if any. The implementation of `ListState` makes
|
||||
/// // sure that the stored offset is also reset.
|
||||
/// pub fn unselect(&mut self) {
|
||||
/// self.state.select(None);
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// let mut events = Events::new(vec![String::from("Item 1"), String::from("Item 2")]);
|
||||
///
|
||||
/// loop {
|
||||
/// terminal.draw(|f| {
|
||||
/// // The items managed by the application are transformed to something
|
||||
/// // that is understood by ratatui.
|
||||
/// let items: Vec<ListItem> = events
|
||||
/// .items
|
||||
/// .iter()
|
||||
/// .map(|i| ListItem::new(i.as_str()))
|
||||
/// .collect();
|
||||
/// // The `List` widget is then built with those items.
|
||||
/// let list = List::new(items);
|
||||
/// // Finally the widget is rendered using the associated state. `events.state` is
|
||||
/// // effectively the only thing that we will "remember" from this draw call.
|
||||
/// f.render_stateful_widget(list, f.size(), &mut events.state);
|
||||
/// });
|
||||
///
|
||||
/// // In response to some input events or an external http request or whatever:
|
||||
/// events.next();
|
||||
/// }
|
||||
/// ```
|
||||
pub trait StatefulWidget {
|
||||
/// State associated with the stateful widget.
|
||||
///
|
||||
/// If you don't need this then you probably want to implement [`Widget`] instead.
|
||||
///
|
||||
/// [`Widget`]: super::Widget
|
||||
type State: ?Sized;
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom stateful widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
|
||||
#[fixture]
|
||||
fn buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 20, 1))
|
||||
}
|
||||
|
||||
#[fixture]
|
||||
fn state() -> String {
|
||||
"world".to_string()
|
||||
}
|
||||
|
||||
struct PersonalGreeting;
|
||||
|
||||
impl StatefulWidget for PersonalGreeting {
|
||||
type State = String;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
Line::from(format!("Hello {state}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer, mut state: String) {
|
||||
let widget = PersonalGreeting;
|
||||
widget.render(buf.area, &mut buf, &mut state);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello world "]));
|
||||
}
|
||||
|
||||
struct Bytes;
|
||||
|
||||
/// A widget with an unsized state type.
|
||||
impl StatefulWidget for Bytes {
|
||||
type State = [u8];
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
let slice = std::str::from_utf8(state).unwrap();
|
||||
Line::from(format!("Bytes: {slice}")).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_unsized_state_type(mut buf: Buffer) {
|
||||
let widget = Bytes;
|
||||
let state = b"hello";
|
||||
widget.render(buf.area, &mut buf, &mut state.clone());
|
||||
assert_eq!(buf, Buffer::with_lines(["Bytes: hello "]));
|
||||
}
|
||||
}
|
||||
160
ratatui-core/src/widgets/widget.rs
Normal file
160
ratatui-core/src/widgets/widget.rs
Normal file
@@ -0,0 +1,160 @@
|
||||
use crate::{buffer::Buffer, layout::Rect, style::Style};
|
||||
|
||||
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
|
||||
///
|
||||
/// Prior to Ratatui 0.26.0, widgets generally were created for each frame as they were consumed
|
||||
/// during rendering. This meant that they were not meant to be stored but used as *commands* to
|
||||
/// draw common figures in the UI.
|
||||
///
|
||||
/// Starting with Ratatui 0.26.0, all the internal widgets implement Widget for a reference to
|
||||
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates
|
||||
/// should consider also doing this to allow for more flexibility in how widgets are used.
|
||||
///
|
||||
/// In Ratatui 0.26.0, we also added an unstable `WidgetRef` trait and implemented this on all the
|
||||
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also
|
||||
/// allows you to render boxed widgets. This is useful when you want to store a collection of
|
||||
/// widgets with different types. You can then iterate over the collection and render each widget.
|
||||
/// See <https://github.com/ratatui/ratatui/issues/1287> for more information.
|
||||
///
|
||||
/// In general where you expect a widget to immutably work on its data, we recommended to implement
|
||||
/// `Widget` for a reference to the widget (`impl Widget for &MyWidget`). If you need to store state
|
||||
/// between draw calls, implement `StatefulWidget` if you want the Widget to be immutable, or
|
||||
/// implement `Widget` for a mutable reference to the widget (`impl Widget for &mut MyWidget`) if
|
||||
/// you want the widget to be mutable. The mutable widget pattern is used infrequently in apps, but
|
||||
/// can be quite useful.
|
||||
///
|
||||
/// A blanket implementation of `Widget` for `&W` where `W` implements `WidgetRef` is provided.
|
||||
/// Widget is also implemented for `&str` and `String` types.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use ratatui::{
|
||||
/// backend::TestBackend,
|
||||
/// widgets::{Clear, Widget},
|
||||
/// Terminal,
|
||||
/// };
|
||||
/// # let backend = TestBackend::new(5, 5);
|
||||
/// # let mut terminal = Terminal::new(backend).unwrap();
|
||||
///
|
||||
/// terminal.draw(|frame| {
|
||||
/// frame.render_widget(Clear, frame.area());
|
||||
/// });
|
||||
/// ```
|
||||
///
|
||||
/// It's common to render widgets inside other widgets:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
|
||||
///
|
||||
/// struct MyWidget;
|
||||
///
|
||||
/// impl Widget for MyWidget {
|
||||
/// fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
/// Line::raw("Hello").render(area, buf);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub trait Widget {
|
||||
/// Draws the current state of the widget in the given buffer. That is the only method required
|
||||
/// to implement a custom widget.
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Renders a string slice as a widget.
|
||||
///
|
||||
/// This implementation allows a string slice (`&str`) to act as a widget, meaning it can be drawn
|
||||
/// onto a [`Buffer`] in a specified [`Rect`]. The slice represents a static string which can be
|
||||
/// rendered by reference, thereby avoiding the need for string cloning or ownership transfer when
|
||||
/// drawing the text to the screen.
|
||||
impl Widget for &str {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a `String` object as a widget.
|
||||
///
|
||||
/// This implementation enables an owned `String` to be treated as a widget, which can be rendered
|
||||
/// on a [`Buffer`] within the bounds of a given [`Rect`].
|
||||
impl Widget for String {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl<W: Widget> Widget for Option<W> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
if let Some(widget) = self {
|
||||
widget.render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{buffer::Buffer, layout::Rect, text::Line};
|
||||
|
||||
#[fixture]
|
||||
fn buf() -> Buffer {
|
||||
Buffer::empty(Rect::new(0, 0, 20, 1))
|
||||
}
|
||||
|
||||
struct Greeting;
|
||||
|
||||
impl Widget for Greeting {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Line::from("Hello").render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render(mut buf: Buffer) {
|
||||
let widget = Greeting;
|
||||
widget.render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["Hello "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_str(mut buf: Buffer) {
|
||||
"hello world".render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_str_truncate(mut buf: Buffer) {
|
||||
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
|
||||
"hello world, just hello".render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_option_str(mut buf: Buffer) {
|
||||
Some("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_string(mut buf: Buffer) {
|
||||
String::from("hello world").render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_string_truncate(mut buf: Buffer) {
|
||||
let area = Rect::new(buf.area.x, buf.area.y, 11, buf.area.height);
|
||||
String::from("hello world, just hello").render(area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn render_option_string(mut buf: Buffer) {
|
||||
Some(String::from("hello world")).render(buf.area, &mut buf);
|
||||
assert_eq!(buf, Buffer::with_lines(["hello world "]));
|
||||
}
|
||||
}
|
||||
45
ratatui-crossterm/Cargo.toml
Normal file
45
ratatui-crossterm/Cargo.toml
Normal file
@@ -0,0 +1,45 @@
|
||||
[package]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0-alpha.0"
|
||||
description = "Crossterm backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-crossterm/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["underline-color"]
|
||||
|
||||
## enables the backend code that sets the underline color.
|
||||
## Underline color is only supported by the [`CrosstermBackend`](backend::CrosstermBackend) backend,
|
||||
## and is not supported on Windows 7.
|
||||
underline-color = ["ratatui-core/underline-color"]
|
||||
|
||||
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
|
||||
scrolling-regions = ["ratatui-core/scrolling-regions"]
|
||||
|
||||
#! The following features are unstable and may change in the future:
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-backend-writer"]
|
||||
|
||||
## Enables getting access to backends' writers.
|
||||
unstable-backend-writer = []
|
||||
|
||||
|
||||
[dependencies]
|
||||
crossterm.workspace = true
|
||||
document-features = { workspace = true, optional = true }
|
||||
instability.workspace = true
|
||||
ratatui-core = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
ratatui = { path = "../ratatui", features = ["crossterm"] }
|
||||
rstest.workspace = true
|
||||
10
ratatui-crossterm/README.md
Normal file
10
ratatui-crossterm/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# Ratatui-crossterm
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
the [Crossterm] crate to interact with the terminal.
|
||||
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
812
ratatui-crossterm/src/lib.rs
Normal file
812
ratatui-crossterm/src/lib.rs
Normal file
@@ -0,0 +1,812 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! This module provides the [`CrosstermBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Crossterm] crate to interact with the terminal.
|
||||
//!
|
||||
//! [Crossterm]: https://crates.io/crates/crossterm
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
||||
pub use crossterm;
|
||||
#[cfg(feature = "underline-color")]
|
||||
use crossterm::style::SetUnderlineColor;
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
execute, queue,
|
||||
style::{
|
||||
Attribute as CrosstermAttribute, Attributes as CrosstermAttributes,
|
||||
Color as CrosstermColor, Colors as CrosstermColors, ContentStyle, Print, SetAttribute,
|
||||
SetBackgroundColor, SetColors, SetForegroundColor,
|
||||
},
|
||||
terminal::{self, Clear},
|
||||
};
|
||||
use ratatui_core::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Crossterm] to render to the terminal.
|
||||
///
|
||||
/// The `CrosstermBackend` struct is a wrapper around a writer implementing [`Write`], which is
|
||||
/// used to send commands to the terminal. It provides methods for drawing content, manipulating
|
||||
/// the cursor, and clearing the terminal screen.
|
||||
///
|
||||
/// Most applications should not call the methods on `CrosstermBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// Usually applications will enable raw mode and switch to alternate screen mode after creating
|
||||
/// a `CrosstermBackend`. This is done by calling [`crossterm::terminal::enable_raw_mode`] and
|
||||
/// [`crossterm::terminal::EnterAlternateScreen`] (and the corresponding disable/leave functions
|
||||
/// when the application exits). This is not done automatically by the backend because it is
|
||||
/// possible that the application may want to use the terminal for other purposes (like showing
|
||||
/// help text) before entering alternate screen mode.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use crossterm::{
|
||||
/// terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
/// ExecutableCommand,
|
||||
/// };
|
||||
/// use ratatui::{backend::CrosstermBackend, Terminal};
|
||||
///
|
||||
/// let mut backend = CrosstermBackend::new(stdout());
|
||||
/// // or
|
||||
/// let backend = CrosstermBackend::new(stderr());
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// enable_raw_mode()?;
|
||||
/// stdout().execute(EnterAlternateScreen)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
///
|
||||
/// stdout().execute(LeaveAlternateScreen)?;
|
||||
/// disable_raw_mode()?;
|
||||
///
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`Write`]: std::io::Write
|
||||
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
/// [`backend`]: ratatui_core::backend
|
||||
/// [Crossterm]: https://crates.io/crates/crossterm
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct CrosstermBackend<W: Write> {
|
||||
/// The writer used to send commands to the terminal.
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<W> CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Creates a new `CrosstermBackend` with the given writer.
|
||||
///
|
||||
/// Most applications will use either [`stdout`](std::io::stdout) or
|
||||
/// [`stderr`](std::io::stderr) as writer. See the [FAQ] to determine which one to use.
|
||||
///
|
||||
/// [FAQ]: https://ratatui.rs/faq/#should-i-use-stdout-or-stderr
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::backend::CrosstermBackend;
|
||||
///
|
||||
/// let backend = CrosstermBackend::new(stdout());
|
||||
/// ```
|
||||
pub const fn new(writer: W) -> Self {
|
||||
Self { writer }
|
||||
}
|
||||
|
||||
/// Gets the writer.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub const fn writer(&self) -> &W {
|
||||
&self.writer
|
||||
}
|
||||
|
||||
/// Gets the writer as a mutable reference.
|
||||
///
|
||||
/// Note: writing to the writer may cause incorrect output after the write. This is due to the
|
||||
/// way that the Terminal implements diffing Buffers.
|
||||
#[instability::unstable(
|
||||
feature = "backend-writer",
|
||||
issue = "https://github.com/ratatui/ratatui/pull/991"
|
||||
)]
|
||||
pub fn writer_mut(&mut self) -> &mut W {
|
||||
&mut self.writer
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Write for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
/// Writes a buffer of bytes to the underlying buffer.
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.writer.write(buf)
|
||||
}
|
||||
|
||||
/// Flushes the underlying buffer.
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl<W> Backend for CrosstermBackend<W>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
let mut fg = Color::Reset;
|
||||
let mut bg = Color::Reset;
|
||||
#[cfg(feature = "underline-color")]
|
||||
let mut underline_color = Color::Reset;
|
||||
let mut modifier = Modifier::empty();
|
||||
let mut last_pos: Option<Position> = None;
|
||||
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.x + 1 && y == p.y) {
|
||||
queue!(self.writer, MoveTo(x, y))?;
|
||||
}
|
||||
last_pos = Some(Position { x, y });
|
||||
if cell.modifier != modifier {
|
||||
let diff = ModifierDiff {
|
||||
from: modifier,
|
||||
to: cell.modifier,
|
||||
};
|
||||
diff.queue(&mut self.writer)?;
|
||||
modifier = cell.modifier;
|
||||
}
|
||||
if cell.fg != fg || cell.bg != bg {
|
||||
queue!(
|
||||
self.writer,
|
||||
SetColors(CrosstermColors::new(
|
||||
cell.fg.into_crossterm(),
|
||||
cell.bg.into_crossterm(),
|
||||
))
|
||||
)?;
|
||||
fg = cell.fg;
|
||||
bg = cell.bg;
|
||||
}
|
||||
#[cfg(feature = "underline-color")]
|
||||
if cell.underline_color != underline_color {
|
||||
let color = cell.underline_color.into_crossterm();
|
||||
queue!(self.writer, SetUnderlineColor(color))?;
|
||||
underline_color = cell.underline_color;
|
||||
}
|
||||
|
||||
queue!(self.writer, Print(cell.symbol()))?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CrosstermColor::Reset),
|
||||
SetBackgroundColor(CrosstermColor::Reset),
|
||||
SetUnderlineColor(CrosstermColor::Reset),
|
||||
SetAttribute(CrosstermAttribute::Reset),
|
||||
);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
return queue!(
|
||||
self.writer,
|
||||
SetForegroundColor(CrosstermColor::Reset),
|
||||
SetBackgroundColor(CrosstermColor::Reset),
|
||||
SetAttribute(CrosstermAttribute::Reset),
|
||||
);
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Hide)
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
execute!(self.writer, Show)
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
crossterm::cursor::position()
|
||||
.map(|(x, y)| Position { x, y })
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
execute!(self.writer, MoveTo(x, y))
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.clear_region(ClearType::All)
|
||||
}
|
||||
|
||||
fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> {
|
||||
execute!(
|
||||
self.writer,
|
||||
Clear(match clear_type {
|
||||
ClearType::All => crossterm::terminal::ClearType::All,
|
||||
ClearType::AfterCursor => crossterm::terminal::ClearType::FromCursorDown,
|
||||
ClearType::BeforeCursor => crossterm::terminal::ClearType::FromCursorUp,
|
||||
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 {
|
||||
queue!(self.writer, Print("\n"))?;
|
||||
}
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let (width, height) = terminal::size()?;
|
||||
Ok(Size { width, height })
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let crossterm::terminal::WindowSize {
|
||||
columns,
|
||||
rows,
|
||||
width,
|
||||
height,
|
||||
} = terminal::window_size()?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: columns,
|
||||
height: rows,
|
||||
},
|
||||
pixels: Size { width, height },
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
queue!(
|
||||
self.writer,
|
||||
ScrollUpInRegion {
|
||||
first_row: region.start,
|
||||
last_row: region.end.saturating_sub(1),
|
||||
lines_to_scroll: amount,
|
||||
}
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
queue!(
|
||||
self.writer,
|
||||
ScrollDownInRegion {
|
||||
first_row: region.start,
|
||||
last_row: region.end.saturating_sub(1),
|
||||
lines_to_scroll: amount,
|
||||
}
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a Ratatui type to a Crossterm type.
|
||||
///
|
||||
/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
|
||||
/// once these are moved to a separate crate.
|
||||
pub trait IntoCrossterm<C> {
|
||||
/// Converts the ratatui type to a crossterm type.
|
||||
fn into_crossterm(self) -> C;
|
||||
}
|
||||
|
||||
/// A trait for converting a Crossterm type to a Ratatui type.
|
||||
///
|
||||
/// This trait is needed for avoiding the orphan rule when implementing `From` for crossterm types
|
||||
/// once these are moved to a separate crate.
|
||||
pub trait FromCrossterm<C> {
|
||||
/// Converts the crossterm type to a ratatui type.
|
||||
fn from_crossterm(value: C) -> Self;
|
||||
}
|
||||
|
||||
impl IntoCrossterm<CrosstermColor> for Color {
|
||||
fn into_crossterm(self) -> CrosstermColor {
|
||||
match self {
|
||||
Self::Reset => CrosstermColor::Reset,
|
||||
Self::Black => CrosstermColor::Black,
|
||||
Self::Red => CrosstermColor::DarkRed,
|
||||
Self::Green => CrosstermColor::DarkGreen,
|
||||
Self::Yellow => CrosstermColor::DarkYellow,
|
||||
Self::Blue => CrosstermColor::DarkBlue,
|
||||
Self::Magenta => CrosstermColor::DarkMagenta,
|
||||
Self::Cyan => CrosstermColor::DarkCyan,
|
||||
Self::Gray => CrosstermColor::Grey,
|
||||
Self::DarkGray => CrosstermColor::DarkGrey,
|
||||
Self::LightRed => CrosstermColor::Red,
|
||||
Self::LightGreen => CrosstermColor::Green,
|
||||
Self::LightBlue => CrosstermColor::Blue,
|
||||
Self::LightYellow => CrosstermColor::Yellow,
|
||||
Self::LightMagenta => CrosstermColor::Magenta,
|
||||
Self::LightCyan => CrosstermColor::Cyan,
|
||||
Self::White => CrosstermColor::White,
|
||||
Self::Indexed(i) => CrosstermColor::AnsiValue(i),
|
||||
Self::Rgb(r, g, b) => CrosstermColor::Rgb { r, g, b },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<CrosstermColor> for Color {
|
||||
fn from_crossterm(value: CrosstermColor) -> Self {
|
||||
match value {
|
||||
CrosstermColor::Reset => Self::Reset,
|
||||
CrosstermColor::Black => Self::Black,
|
||||
CrosstermColor::DarkRed => Self::Red,
|
||||
CrosstermColor::DarkGreen => Self::Green,
|
||||
CrosstermColor::DarkYellow => Self::Yellow,
|
||||
CrosstermColor::DarkBlue => Self::Blue,
|
||||
CrosstermColor::DarkMagenta => Self::Magenta,
|
||||
CrosstermColor::DarkCyan => Self::Cyan,
|
||||
CrosstermColor::Grey => Self::Gray,
|
||||
CrosstermColor::DarkGrey => Self::DarkGray,
|
||||
CrosstermColor::Red => Self::LightRed,
|
||||
CrosstermColor::Green => Self::LightGreen,
|
||||
CrosstermColor::Blue => Self::LightBlue,
|
||||
CrosstermColor::Yellow => Self::LightYellow,
|
||||
CrosstermColor::Magenta => Self::LightMagenta,
|
||||
CrosstermColor::Cyan => Self::LightCyan,
|
||||
CrosstermColor::White => Self::White,
|
||||
CrosstermColor::Rgb { r, g, b } => Self::Rgb(r, g, b),
|
||||
CrosstermColor::AnsiValue(v) => Self::Indexed(v),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
struct ModifierDiff {
|
||||
pub from: Modifier,
|
||||
pub to: Modifier,
|
||||
}
|
||||
|
||||
impl ModifierDiff {
|
||||
fn queue<W>(self, mut w: W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
//use crossterm::Attribute;
|
||||
let removed = self.from - self.to;
|
||||
if removed.contains(Modifier::REVERSED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoReverse))?;
|
||||
}
|
||||
if removed.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
|
||||
if self.to.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
|
||||
}
|
||||
}
|
||||
if removed.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoItalic))?;
|
||||
}
|
||||
if removed.contains(Modifier::UNDERLINED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoUnderline))?;
|
||||
}
|
||||
if removed.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NormalIntensity))?;
|
||||
}
|
||||
if removed.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NotCrossedOut))?;
|
||||
}
|
||||
if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::NoBlink))?;
|
||||
}
|
||||
|
||||
let added = self.to - self.from;
|
||||
if added.contains(Modifier::REVERSED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Reverse))?;
|
||||
}
|
||||
if added.contains(Modifier::BOLD) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Bold))?;
|
||||
}
|
||||
if added.contains(Modifier::ITALIC) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Italic))?;
|
||||
}
|
||||
if added.contains(Modifier::UNDERLINED) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Underlined))?;
|
||||
}
|
||||
if added.contains(Modifier::DIM) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::Dim))?;
|
||||
}
|
||||
if added.contains(Modifier::CROSSED_OUT) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::CrossedOut))?;
|
||||
}
|
||||
if added.contains(Modifier::SLOW_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::SlowBlink))?;
|
||||
}
|
||||
if added.contains(Modifier::RAPID_BLINK) {
|
||||
queue!(w, SetAttribute(CrosstermAttribute::RapidBlink))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<CrosstermAttribute> for Modifier {
|
||||
fn from_crossterm(value: CrosstermAttribute) -> Self {
|
||||
// `Attribute*s*` (note the *s*) contains multiple `Attribute` We convert `Attribute` to
|
||||
// `Attribute*s*` (containing only 1 value) to avoid implementing the conversion again
|
||||
Self::from_crossterm(CrosstermAttributes::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<CrosstermAttributes> for Modifier {
|
||||
fn from_crossterm(value: CrosstermAttributes) -> Self {
|
||||
let mut res = Self::empty();
|
||||
if value.has(CrosstermAttribute::Bold) {
|
||||
res |= Self::BOLD;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Dim) {
|
||||
res |= Self::DIM;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Italic) {
|
||||
res |= Self::ITALIC;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Underlined)
|
||||
|| value.has(CrosstermAttribute::DoubleUnderlined)
|
||||
|| value.has(CrosstermAttribute::Undercurled)
|
||||
|| value.has(CrosstermAttribute::Underdotted)
|
||||
|| value.has(CrosstermAttribute::Underdashed)
|
||||
{
|
||||
res |= Self::UNDERLINED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::SlowBlink) {
|
||||
res |= Self::SLOW_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::RapidBlink) {
|
||||
res |= Self::RAPID_BLINK;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Reverse) {
|
||||
res |= Self::REVERSED;
|
||||
}
|
||||
if value.has(CrosstermAttribute::Hidden) {
|
||||
res |= Self::HIDDEN;
|
||||
}
|
||||
if value.has(CrosstermAttribute::CrossedOut) {
|
||||
res |= Self::CROSSED_OUT;
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl FromCrossterm<ContentStyle> for Style {
|
||||
fn from_crossterm(value: ContentStyle) -> Self {
|
||||
let mut sub_modifier = Modifier::empty();
|
||||
if value.attributes.has(CrosstermAttribute::NoBold) {
|
||||
sub_modifier |= Modifier::BOLD;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoItalic) {
|
||||
sub_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NotCrossedOut) {
|
||||
sub_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoUnderline) {
|
||||
sub_modifier |= Modifier::UNDERLINED;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoHidden) {
|
||||
sub_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoBlink) {
|
||||
sub_modifier |= Modifier::RAPID_BLINK | Modifier::SLOW_BLINK;
|
||||
}
|
||||
if value.attributes.has(CrosstermAttribute::NoReverse) {
|
||||
sub_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
|
||||
Self {
|
||||
fg: value.foreground_color.map(FromCrossterm::from_crossterm),
|
||||
bg: value.background_color.map(FromCrossterm::from_crossterm),
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: value.underline_color.map(FromCrossterm::from_crossterm),
|
||||
add_modifier: Modifier::from_crossterm(value.attributes),
|
||||
sub_modifier,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that scrolls the terminal screen a given number of rows up in a specific scrolling
|
||||
/// region.
|
||||
///
|
||||
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
|
||||
/// crossterm PRs that will address this:
|
||||
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
|
||||
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ScrollUpInRegion {
|
||||
/// The first row of the scrolling region.
|
||||
pub first_row: u16,
|
||||
|
||||
/// The last row of the scrolling region.
|
||||
pub last_row: u16,
|
||||
|
||||
/// The number of lines to scroll up by.
|
||||
pub lines_to_scroll: u16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
impl crate::crossterm::Command for ScrollUpInRegion {
|
||||
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
if self.lines_to_scroll != 0 {
|
||||
// Set a scrolling region that contains just the desired lines.
|
||||
write!(
|
||||
f,
|
||||
crate::crossterm::csi!("{};{}r"),
|
||||
self.first_row.saturating_add(1),
|
||||
self.last_row.saturating_add(1)
|
||||
)?;
|
||||
// Scroll the region by the desired count.
|
||||
write!(f, crate::crossterm::csi!("{}S"), self.lines_to_scroll)?;
|
||||
// Reset the scrolling region to be the whole screen.
|
||||
write!(f, crate::crossterm::csi!("r"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"ScrollUpInRegion command not supported for winapi",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that scrolls the terminal screen a given number of rows down in a specific scrolling
|
||||
/// region.
|
||||
///
|
||||
/// This will hopefully be replaced by a struct in crossterm proper. There are two outstanding
|
||||
/// crossterm PRs that will address this:
|
||||
/// - [918](https://github.com/crossterm-rs/crossterm/pull/918)
|
||||
/// - [923](https://github.com/crossterm-rs/crossterm/pull/923)
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ScrollDownInRegion {
|
||||
/// The first row of the scrolling region.
|
||||
pub first_row: u16,
|
||||
|
||||
/// The last row of the scrolling region.
|
||||
pub last_row: u16,
|
||||
|
||||
/// The number of lines to scroll down by.
|
||||
pub lines_to_scroll: u16,
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
impl crate::crossterm::Command for ScrollDownInRegion {
|
||||
fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result {
|
||||
if self.lines_to_scroll != 0 {
|
||||
// Set a scrolling region that contains just the desired lines.
|
||||
write!(
|
||||
f,
|
||||
crate::crossterm::csi!("{};{}r"),
|
||||
self.first_row.saturating_add(1),
|
||||
self.last_row.saturating_add(1)
|
||||
)?;
|
||||
// Scroll the region by the desired count.
|
||||
write!(f, crate::crossterm::csi!("{}T"), self.lines_to_scroll)?;
|
||||
// Reset the scrolling region to be the whole screen.
|
||||
write!(f, crate::crossterm::csi!("r"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"ScrollDownInRegion command not supported for winapi",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(CrosstermColor::Reset, Color::Reset)]
|
||||
#[case(CrosstermColor::Black, Color::Black)]
|
||||
#[case(CrosstermColor::DarkGrey, Color::DarkGray)]
|
||||
#[case(CrosstermColor::Red, Color::LightRed)]
|
||||
#[case(CrosstermColor::DarkRed, Color::Red)]
|
||||
#[case(CrosstermColor::Green, Color::LightGreen)]
|
||||
#[case(CrosstermColor::DarkGreen, Color::Green)]
|
||||
#[case(CrosstermColor::Yellow, Color::LightYellow)]
|
||||
#[case(CrosstermColor::DarkYellow, Color::Yellow)]
|
||||
#[case(CrosstermColor::Blue, Color::LightBlue)]
|
||||
#[case(CrosstermColor::DarkBlue, Color::Blue)]
|
||||
#[case(CrosstermColor::Magenta, Color::LightMagenta)]
|
||||
#[case(CrosstermColor::DarkMagenta, Color::Magenta)]
|
||||
#[case(CrosstermColor::Cyan, Color::LightCyan)]
|
||||
#[case(CrosstermColor::DarkCyan, Color::Cyan)]
|
||||
#[case(CrosstermColor::White, Color::White)]
|
||||
#[case(CrosstermColor::Grey, Color::Gray)]
|
||||
#[case(CrosstermColor::Rgb { r: 0, g: 0, b: 0 }, Color::Rgb(0, 0, 0) )]
|
||||
#[case(CrosstermColor::Rgb { r: 10, g: 20, b: 30 }, Color::Rgb(10, 20, 30) )]
|
||||
#[case(CrosstermColor::AnsiValue(32), Color::Indexed(32))]
|
||||
#[case(CrosstermColor::AnsiValue(37), Color::Indexed(37))]
|
||||
fn from_crossterm_color(#[case] crossterm_color: CrosstermColor, #[case] color: Color) {
|
||||
assert_eq!(Color::from_crossterm(crossterm_color), color);
|
||||
}
|
||||
|
||||
mod modifier {
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case(CrosstermAttribute::Reset, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Bold, Modifier::BOLD)]
|
||||
#[case(CrosstermAttribute::NoBold, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Italic, Modifier::ITALIC)]
|
||||
#[case(CrosstermAttribute::NoItalic, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Underlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::OverLined, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::NotOverLined, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::DoubleUnderlined, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Undercurled, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Underdotted, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Underdashed, Modifier::UNDERLINED)]
|
||||
#[case(CrosstermAttribute::Dim, Modifier::DIM)]
|
||||
#[case(CrosstermAttribute::NormalIntensity, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::CrossedOut, Modifier::CROSSED_OUT)]
|
||||
#[case(CrosstermAttribute::NotCrossedOut, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::NoUnderline, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::SlowBlink, Modifier::SLOW_BLINK)]
|
||||
#[case(CrosstermAttribute::RapidBlink, Modifier::RAPID_BLINK)]
|
||||
#[case(CrosstermAttribute::Hidden, Modifier::HIDDEN)]
|
||||
#[case(CrosstermAttribute::NoHidden, Modifier::empty())]
|
||||
#[case(CrosstermAttribute::Reverse, Modifier::REVERSED)]
|
||||
#[case(CrosstermAttribute::NoReverse, Modifier::empty())]
|
||||
fn from_crossterm_attribute(
|
||||
#[case] crossterm_attribute: CrosstermAttribute,
|
||||
#[case] ratatui_modifier: Modifier,
|
||||
) {
|
||||
assert_eq!(
|
||||
Modifier::from_crossterm(crossterm_attribute),
|
||||
ratatui_modifier
|
||||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(&[CrosstermAttribute::Bold], Modifier::BOLD)]
|
||||
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::Italic], Modifier::BOLD | Modifier::ITALIC)]
|
||||
#[case(&[CrosstermAttribute::Bold, CrosstermAttribute::NotCrossedOut], Modifier::BOLD)]
|
||||
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::Underdotted], Modifier::DIM | Modifier::UNDERLINED)]
|
||||
#[case(&[CrosstermAttribute::Dim, CrosstermAttribute::SlowBlink, CrosstermAttribute::Italic], Modifier::DIM | Modifier::SLOW_BLINK | Modifier::ITALIC)]
|
||||
#[case(&[CrosstermAttribute::Hidden, CrosstermAttribute::NoUnderline, CrosstermAttribute::NotCrossedOut], Modifier::HIDDEN)]
|
||||
#[case(&[CrosstermAttribute::Reverse], Modifier::REVERSED)]
|
||||
#[case(&[CrosstermAttribute::Reset], Modifier::empty())]
|
||||
#[case(&[CrosstermAttribute::RapidBlink, CrosstermAttribute::CrossedOut], Modifier::RAPID_BLINK | Modifier::CROSSED_OUT)]
|
||||
fn from_crossterm_attributes(
|
||||
#[case] crossterm_attributes: &[CrosstermAttribute],
|
||||
#[case] ratatui_modifier: Modifier,
|
||||
) {
|
||||
assert_eq!(
|
||||
Modifier::from_crossterm(CrosstermAttributes::from(crossterm_attributes)),
|
||||
ratatui_modifier
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case(ContentStyle::default(), Style::default())]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
foreground_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().fg(Color::Yellow)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
background_color: Some(CrosstermColor::DarkYellow),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().bg(Color::Yellow)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Bold),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().add_modifier(Modifier::BOLD)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoBold),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().remove_modifier(Modifier::BOLD)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().add_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(CrosstermAttribute::NoItalic),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default().remove_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::Bold, CrosstermAttribute::Italic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
#[case(
|
||||
ContentStyle {
|
||||
attributes: CrosstermAttributes::from(
|
||||
[CrosstermAttribute::NoBold, CrosstermAttribute::NoItalic].as_ref()
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
Style::default()
|
||||
.remove_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::ITALIC)
|
||||
)]
|
||||
fn from_crossterm_content_style(#[case] content_style: ContentStyle, #[case] style: Style) {
|
||||
assert_eq!(Style::from_crossterm(content_style), style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "underline-color")]
|
||||
fn from_crossterm_content_style_underline() {
|
||||
let content_style = ContentStyle {
|
||||
underline_color: Some(CrosstermColor::DarkRed),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
Style::from_crossterm(content_style),
|
||||
Style::default().underline_color(Color::Red)
|
||||
);
|
||||
}
|
||||
}
|
||||
35
ratatui-termion/Cargo.toml
Normal file
35
ratatui-termion/Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "ratatui-termion"
|
||||
version = "0.1.0-alpha.0"
|
||||
description = "Termion backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-termion/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
|
||||
scrolling-regions = ["ratatui-core/scrolling-regions"]
|
||||
|
||||
[dependencies]
|
||||
document-features = { workspace = true, optional = true }
|
||||
ratatui-core = { workspace = true }
|
||||
termion.workspace = true
|
||||
instability.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
rstest.workspace = true
|
||||
11
ratatui-termion/README.md
Normal file
11
ratatui-termion/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Ratatui-termion
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
|
||||
the [Termion] crate to interact with the terminal.
|
||||
|
||||
[`Backend`]: ratatui_core::backend::Backend
|
||||
[Termion]: https://docs.rs/termion
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
@@ -1,21 +1,32 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! This module provides the [`TermionBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Termion] crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: crate::backend::Backend
|
||||
//! [`TermionBackend`]: crate::backend::TermionBackend
|
||||
//! [`Backend`]: ratatui_core::backend::Backend
|
||||
//! [Termion]: https://docs.rs/termion
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{self, Write},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
use ratatui_core::{
|
||||
backend::{Backend, ClearType, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
termion::{self, color as tcolor, color::Color as _, style as tstyle},
|
||||
};
|
||||
pub use termion;
|
||||
use termion::{color as tcolor, color::Color as _, style as tstyle};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termion] to render to the terminal.
|
||||
///
|
||||
@@ -40,8 +51,9 @@ use crate::{
|
||||
/// use std::io::{stderr, stdout};
|
||||
///
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// backend::TermionBackend,
|
||||
/// termion::{raw::IntoRawMode, screen::IntoAlternateScreen},
|
||||
/// Terminal,
|
||||
/// };
|
||||
///
|
||||
/// let writer = stdout().into_raw_mode()?.into_alternate_screen()?;
|
||||
@@ -60,7 +72,7 @@ use crate::{
|
||||
///
|
||||
/// [`IntoRawMode::into_raw_mode()`]: termion::raw::IntoRawMode
|
||||
/// [`IntoAlternateScreen::into_alternate_screen()`]: termion::screen::IntoAlternateScreen
|
||||
/// [`Terminal`]: crate::terminal::Terminal
|
||||
/// [`Terminal`]: ratatui::terminal::Terminal
|
||||
/// [Termion]: https://docs.rs/termion
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct TermionBackend<W>
|
||||
@@ -84,8 +96,10 @@ where
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use std::io::stdout;
|
||||
/// # use ratatui::prelude::*;
|
||||
/// use std::io::stdout;
|
||||
///
|
||||
/// use ratatui::backend::TermionBackend;
|
||||
///
|
||||
/// let backend = TermionBackend::new(stdout());
|
||||
/// ```
|
||||
pub const fn new(writer: W) -> Self {
|
||||
@@ -236,6 +250,30 @@ where
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
write!(
|
||||
self.writer,
|
||||
"{}{}{}",
|
||||
SetRegion(region.start.saturating_add(1), region.end),
|
||||
termion::scroll::Up(amount),
|
||||
ResetRegion,
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
write!(
|
||||
self.writer,
|
||||
"{}{}{}",
|
||||
SetRegion(region.start.saturating_add(1), region.end),
|
||||
termion::scroll::Down(amount),
|
||||
ResetRegion,
|
||||
)?;
|
||||
self.writer.flush()
|
||||
}
|
||||
}
|
||||
struct Fg(Color);
|
||||
|
||||
@@ -300,22 +338,40 @@ impl fmt::Display for Bg {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting a Termion type to a Ratatui type.
|
||||
///
|
||||
/// This trait is necessary to avoid the orphan rule, as we cannot implement a trait for a type
|
||||
/// defined in another crate.
|
||||
pub trait FromTermion<T> {
|
||||
/// Convert the Termion type to the Ratatui type.
|
||||
fn from_termion(termion: T) -> Self;
|
||||
}
|
||||
|
||||
/// A trait for converting a Ratatui type to a Termion type.
|
||||
///
|
||||
/// This trait is necessary to avoid the orphan rule, as we cannot implement a trait for a type
|
||||
/// defined in another crate.
|
||||
pub trait IntoTermion<T> {
|
||||
/// Convert the Ratatui type to the Termion type.
|
||||
fn into_termion(self) -> T;
|
||||
}
|
||||
|
||||
macro_rules! from_termion_for_color {
|
||||
($termion_color:ident, $color:ident) => {
|
||||
impl From<tcolor::$termion_color> for Color {
|
||||
fn from(_: tcolor::$termion_color) -> Self {
|
||||
impl FromTermion<tcolor::$termion_color> for Color {
|
||||
fn from_termion(_: tcolor::$termion_color) -> Self {
|
||||
Color::$color
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
|
||||
impl FromTermion<tcolor::Bg<tcolor::$termion_color>> for Style {
|
||||
fn from_termion(_: tcolor::Bg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().bg(Color::$color)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::$termion_color>> for Style {
|
||||
fn from(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
|
||||
impl FromTermion<tcolor::Fg<tcolor::$termion_color>> for Style {
|
||||
fn from_termion(_: tcolor::Fg<tcolor::$termion_color>) -> Self {
|
||||
Style::default().fg(Color::$color)
|
||||
}
|
||||
}
|
||||
@@ -340,38 +396,38 @@ from_termion_for_color!(LightMagenta, LightMagenta);
|
||||
from_termion_for_color!(LightCyan, LightCyan);
|
||||
from_termion_for_color!(LightWhite, White);
|
||||
|
||||
impl From<tcolor::AnsiValue> for Color {
|
||||
fn from(value: tcolor::AnsiValue) -> Self {
|
||||
impl FromTermion<tcolor::AnsiValue> for Color {
|
||||
fn from_termion(value: tcolor::AnsiValue) -> Self {
|
||||
Self::Indexed(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
impl FromTermion<tcolor::Bg<tcolor::AnsiValue>> for Style {
|
||||
fn from_termion(value: tcolor::Bg<tcolor::AnsiValue>) -> Self {
|
||||
Self::default().bg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
impl FromTermion<tcolor::Fg<tcolor::AnsiValue>> for Style {
|
||||
fn from_termion(value: tcolor::Fg<tcolor::AnsiValue>) -> Self {
|
||||
Self::default().fg(Color::Indexed(value.0 .0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Rgb> for Color {
|
||||
fn from(value: tcolor::Rgb) -> Self {
|
||||
impl FromTermion<tcolor::Rgb> for Color {
|
||||
fn from_termion(value: tcolor::Rgb) -> Self {
|
||||
Self::Rgb(value.0, value.1, value.2)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
impl FromTermion<tcolor::Bg<tcolor::Rgb>> for Style {
|
||||
fn from_termion(value: tcolor::Bg<tcolor::Rgb>) -> Self {
|
||||
Self::default().bg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
impl FromTermion<tcolor::Fg<tcolor::Rgb>> for Style {
|
||||
fn from_termion(value: tcolor::Fg<tcolor::Rgb>) -> Self {
|
||||
Self::default().fg(Color::Rgb(value.0 .0, value.0 .1, value.0 .2))
|
||||
}
|
||||
}
|
||||
@@ -443,8 +499,8 @@ impl fmt::Display for ModifierDiff {
|
||||
|
||||
macro_rules! from_termion_for_modifier {
|
||||
($termion_modifier:ident, $modifier:ident) => {
|
||||
impl From<tstyle::$termion_modifier> for Modifier {
|
||||
fn from(_: tstyle::$termion_modifier) -> Self {
|
||||
impl FromTermion<tstyle::$termion_modifier> for Modifier {
|
||||
fn from_termion(_: tstyle::$termion_modifier) -> Self {
|
||||
Modifier::$modifier
|
||||
}
|
||||
}
|
||||
@@ -459,38 +515,68 @@ from_termion_for_modifier!(Faint, DIM);
|
||||
from_termion_for_modifier!(CrossedOut, CROSSED_OUT);
|
||||
from_termion_for_modifier!(Blink, SLOW_BLINK);
|
||||
|
||||
impl From<termion::style::Reset> for Modifier {
|
||||
fn from(_: termion::style::Reset) -> Self {
|
||||
impl FromTermion<termion::style::Reset> for Modifier {
|
||||
fn from_termion(_: termion::style::Reset) -> Self {
|
||||
Self::empty()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set scrolling region.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct SetRegion(pub u16, pub u16);
|
||||
|
||||
impl fmt::Display for SetRegion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "\x1B[{};{}r", self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Reset scrolling region.
|
||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
||||
pub struct ResetRegion;
|
||||
|
||||
impl fmt::Display for ResetRegion {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "\x1B[r")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::Stylize;
|
||||
|
||||
use super::*;
|
||||
use crate::style::Stylize;
|
||||
|
||||
#[test]
|
||||
fn from_termion_color() {
|
||||
assert_eq!(Color::from(tcolor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from(tcolor::Black), Color::Black);
|
||||
assert_eq!(Color::from(tcolor::Red), Color::Red);
|
||||
assert_eq!(Color::from(tcolor::Green), Color::Green);
|
||||
assert_eq!(Color::from(tcolor::Yellow), Color::Yellow);
|
||||
assert_eq!(Color::from(tcolor::Blue), Color::Blue);
|
||||
assert_eq!(Color::from(tcolor::Magenta), Color::Magenta);
|
||||
assert_eq!(Color::from(tcolor::Cyan), Color::Cyan);
|
||||
assert_eq!(Color::from(tcolor::White), Color::Gray);
|
||||
assert_eq!(Color::from(tcolor::LightBlack), Color::DarkGray);
|
||||
assert_eq!(Color::from(tcolor::LightRed), Color::LightRed);
|
||||
assert_eq!(Color::from(tcolor::LightGreen), Color::LightGreen);
|
||||
assert_eq!(Color::from(tcolor::LightBlue), Color::LightBlue);
|
||||
assert_eq!(Color::from(tcolor::LightYellow), Color::LightYellow);
|
||||
assert_eq!(Color::from(tcolor::LightMagenta), Color::LightMagenta);
|
||||
assert_eq!(Color::from(tcolor::LightCyan), Color::LightCyan);
|
||||
assert_eq!(Color::from(tcolor::LightWhite), Color::White);
|
||||
assert_eq!(Color::from(tcolor::AnsiValue(31)), Color::Indexed(31));
|
||||
assert_eq!(Color::from(tcolor::Rgb(1, 2, 3)), Color::Rgb(1, 2, 3));
|
||||
assert_eq!(Color::from_termion(tcolor::Reset), Color::Reset);
|
||||
assert_eq!(Color::from_termion(tcolor::Black), Color::Black);
|
||||
assert_eq!(Color::from_termion(tcolor::Red), Color::Red);
|
||||
assert_eq!(Color::from_termion(tcolor::Green), Color::Green);
|
||||
assert_eq!(Color::from_termion(tcolor::Yellow), Color::Yellow);
|
||||
assert_eq!(Color::from_termion(tcolor::Blue), Color::Blue);
|
||||
assert_eq!(Color::from_termion(tcolor::Magenta), Color::Magenta);
|
||||
assert_eq!(Color::from_termion(tcolor::Cyan), Color::Cyan);
|
||||
assert_eq!(Color::from_termion(tcolor::White), Color::Gray);
|
||||
assert_eq!(Color::from_termion(tcolor::LightBlack), Color::DarkGray);
|
||||
assert_eq!(Color::from_termion(tcolor::LightRed), Color::LightRed);
|
||||
assert_eq!(Color::from_termion(tcolor::LightGreen), Color::LightGreen);
|
||||
assert_eq!(Color::from_termion(tcolor::LightBlue), Color::LightBlue);
|
||||
assert_eq!(Color::from_termion(tcolor::LightYellow), Color::LightYellow);
|
||||
assert_eq!(
|
||||
Color::from_termion(tcolor::LightMagenta),
|
||||
Color::LightMagenta
|
||||
);
|
||||
assert_eq!(Color::from_termion(tcolor::LightCyan), Color::LightCyan);
|
||||
assert_eq!(Color::from_termion(tcolor::LightWhite), Color::White);
|
||||
assert_eq!(
|
||||
Color::from_termion(tcolor::AnsiValue(31)),
|
||||
Color::Indexed(31)
|
||||
);
|
||||
assert_eq!(
|
||||
Color::from_termion(tcolor::Rgb(1, 2, 3)),
|
||||
Color::Rgb(1, 2, 3)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -498,38 +584,62 @@ mod tests {
|
||||
use tc::Bg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Bg(tc::Reset)), Style::new().bg(Color::Reset));
|
||||
assert_eq!(Style::from(Bg(tc::Black)), Style::new().on_black());
|
||||
assert_eq!(Style::from(Bg(tc::Red)), Style::new().on_red());
|
||||
assert_eq!(Style::from(Bg(tc::Green)), Style::new().on_green());
|
||||
assert_eq!(Style::from(Bg(tc::Yellow)), Style::new().on_yellow());
|
||||
assert_eq!(Style::from(Bg(tc::Blue)), Style::new().on_blue());
|
||||
assert_eq!(Style::from(Bg(tc::Magenta)), Style::new().on_magenta());
|
||||
assert_eq!(Style::from(Bg(tc::Cyan)), Style::new().on_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::White)), Style::new().on_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightBlack)), Style::new().on_dark_gray());
|
||||
assert_eq!(Style::from(Bg(tc::LightRed)), Style::new().on_light_red());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightGreen)),
|
||||
Style::from_termion(Bg(tc::Reset)),
|
||||
Style::new().bg(Color::Reset)
|
||||
);
|
||||
assert_eq!(Style::from_termion(Bg(tc::Black)), Style::new().on_black());
|
||||
assert_eq!(Style::from_termion(Bg(tc::Red)), Style::new().on_red());
|
||||
assert_eq!(Style::from_termion(Bg(tc::Green)), Style::new().on_green());
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::Yellow)),
|
||||
Style::new().on_yellow()
|
||||
);
|
||||
assert_eq!(Style::from_termion(Bg(tc::Blue)), Style::new().on_blue());
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::Magenta)),
|
||||
Style::new().on_magenta()
|
||||
);
|
||||
assert_eq!(Style::from_termion(Bg(tc::Cyan)), Style::new().on_cyan());
|
||||
assert_eq!(Style::from_termion(Bg(tc::White)), Style::new().on_gray());
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightBlack)),
|
||||
Style::new().on_dark_gray()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightRed)),
|
||||
Style::new().on_light_red()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightGreen)),
|
||||
Style::new().on_light_green()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightBlue)), Style::new().on_light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightYellow)),
|
||||
Style::from_termion(Bg(tc::LightBlue)),
|
||||
Style::new().on_light_blue()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightYellow)),
|
||||
Style::new().on_light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::LightMagenta)),
|
||||
Style::from_termion(Bg(tc::LightMagenta)),
|
||||
Style::new().on_light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Bg(tc::LightCyan)), Style::new().on_light_cyan());
|
||||
assert_eq!(Style::from(Bg(tc::LightWhite)), Style::new().on_white());
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::AnsiValue(31))),
|
||||
Style::from_termion(Bg(tc::LightCyan)),
|
||||
Style::new().on_light_cyan()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::LightWhite)),
|
||||
Style::new().on_white()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Bg(tc::AnsiValue(31))),
|
||||
Style::new().bg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Bg(tc::Rgb(1, 2, 3))),
|
||||
Style::from_termion(Bg(tc::Rgb(1, 2, 3))),
|
||||
Style::new().bg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
@@ -539,48 +649,78 @@ mod tests {
|
||||
use tc::Fg;
|
||||
use tcolor as tc;
|
||||
|
||||
assert_eq!(Style::from(Fg(tc::Reset)), Style::new().fg(Color::Reset));
|
||||
assert_eq!(Style::from(Fg(tc::Black)), Style::new().black());
|
||||
assert_eq!(Style::from(Fg(tc::Red)), Style::new().red());
|
||||
assert_eq!(Style::from(Fg(tc::Green)), Style::new().green());
|
||||
assert_eq!(Style::from(Fg(tc::Yellow)), Style::new().yellow());
|
||||
assert_eq!(Style::from(Fg(tc::Blue)), Style::default().blue());
|
||||
assert_eq!(Style::from(Fg(tc::Magenta)), Style::default().magenta());
|
||||
assert_eq!(Style::from(Fg(tc::Cyan)), Style::default().cyan());
|
||||
assert_eq!(Style::from(Fg(tc::White)), Style::default().gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlack)), Style::new().dark_gray());
|
||||
assert_eq!(Style::from(Fg(tc::LightRed)), Style::new().light_red());
|
||||
assert_eq!(Style::from(Fg(tc::LightGreen)), Style::new().light_green());
|
||||
assert_eq!(Style::from(Fg(tc::LightBlue)), Style::new().light_blue());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightYellow)),
|
||||
Style::from_termion(Fg(tc::Reset)),
|
||||
Style::new().fg(Color::Reset)
|
||||
);
|
||||
assert_eq!(Style::from_termion(Fg(tc::Black)), Style::new().black());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Red)), Style::new().red());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Green)), Style::new().green());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Yellow)), Style::new().yellow());
|
||||
assert_eq!(Style::from_termion(Fg(tc::Blue)), Style::default().blue());
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::Magenta)),
|
||||
Style::default().magenta()
|
||||
);
|
||||
assert_eq!(Style::from_termion(Fg(tc::Cyan)), Style::default().cyan());
|
||||
assert_eq!(Style::from_termion(Fg(tc::White)), Style::default().gray());
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightBlack)),
|
||||
Style::new().dark_gray()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightRed)),
|
||||
Style::new().light_red()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightGreen)),
|
||||
Style::new().light_green()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightBlue)),
|
||||
Style::new().light_blue()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightYellow)),
|
||||
Style::new().light_yellow()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::LightMagenta)),
|
||||
Style::from_termion(Fg(tc::LightMagenta)),
|
||||
Style::new().light_magenta()
|
||||
);
|
||||
assert_eq!(Style::from(Fg(tc::LightCyan)), Style::new().light_cyan());
|
||||
assert_eq!(Style::from(Fg(tc::LightWhite)), Style::new().white());
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::AnsiValue(31))),
|
||||
Style::from_termion(Fg(tc::LightCyan)),
|
||||
Style::new().light_cyan()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::LightWhite)),
|
||||
Style::new().white()
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from_termion(Fg(tc::AnsiValue(31))),
|
||||
Style::default().fg(Color::Indexed(31))
|
||||
);
|
||||
assert_eq!(
|
||||
Style::from(Fg(tc::Rgb(1, 2, 3))),
|
||||
Style::from_termion(Fg(tc::Rgb(1, 2, 3))),
|
||||
Style::default().fg(Color::Rgb(1, 2, 3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_termion_style() {
|
||||
assert_eq!(Modifier::from(tstyle::Invert), Modifier::REVERSED);
|
||||
assert_eq!(Modifier::from(tstyle::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from(tstyle::Italic), Modifier::ITALIC);
|
||||
assert_eq!(Modifier::from(tstyle::Underline), Modifier::UNDERLINED);
|
||||
assert_eq!(Modifier::from(tstyle::Faint), Modifier::DIM);
|
||||
assert_eq!(Modifier::from(tstyle::CrossedOut), Modifier::CROSSED_OUT);
|
||||
assert_eq!(Modifier::from(tstyle::Blink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from(tstyle::Reset), Modifier::empty());
|
||||
assert_eq!(Modifier::from_termion(tstyle::Invert), Modifier::REVERSED);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Italic), Modifier::ITALIC);
|
||||
assert_eq!(
|
||||
Modifier::from_termion(tstyle::Underline),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Faint), Modifier::DIM);
|
||||
assert_eq!(
|
||||
Modifier::from_termion(tstyle::CrossedOut),
|
||||
Modifier::CROSSED_OUT
|
||||
);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Blink), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from_termion(tstyle::Reset), Modifier::empty());
|
||||
}
|
||||
}
|
||||
39
ratatui-termwiz/Cargo.toml
Normal file
39
ratatui-termwiz/Cargo.toml
Normal file
@@ -0,0 +1,39 @@
|
||||
[package]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.0-alpha.0"
|
||||
description = "Termwiz backend for the Ratatui Terminal UI library."
|
||||
documentation = "https://docs.rs/ratatui-termwiz/"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
## Enables the backend code that sets the underline color.
|
||||
## Underline color is not supported on Windows 7.
|
||||
underline-color = []
|
||||
|
||||
## Use terminal scrolling regions to make Terminal::insert_before less prone to flickering.
|
||||
scrolling-regions = ["ratatui-core/scrolling-regions"]
|
||||
|
||||
[dependencies]
|
||||
document-features = { workspace = true, optional = true }
|
||||
ratatui-core = { workspace = true }
|
||||
termwiz.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
ratatui = { path = "../ratatui", features = ["termwiz"] }
|
||||
rstest.workspace = true
|
||||
11
ratatui-termwiz/README.md
Normal file
11
ratatui-termwiz/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Ratatui-termwiz
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
This module provides the [`TermwizBackend`] implementation for the [`Backend`] trait. It uses
|
||||
the [Termwiz] crate to interact with the terminal.
|
||||
|
||||
[`Backend`]: trait.Backend.html
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
858
ratatui-termwiz/src/lib.rs
Normal file
858
ratatui-termwiz/src/lib.rs
Normal file
@@ -0,0 +1,858 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! This module provides the [`TermwizBackend`] implementation for the [`Backend`] trait. It uses
|
||||
//! the [Termwiz] crate to interact with the terminal.
|
||||
//!
|
||||
//! [`Backend`]: trait.Backend.html
|
||||
//! [Termwiz]: https://crates.io/crates/termwiz
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
|
||||
use std::{error::Error, io};
|
||||
|
||||
use ratatui_core::{
|
||||
backend::{Backend, WindowSize},
|
||||
buffer::Cell,
|
||||
layout::{Position, Size},
|
||||
style::{Color, Modifier, Style},
|
||||
};
|
||||
pub use termwiz;
|
||||
use termwiz::{
|
||||
caps::Capabilities,
|
||||
cell::{AttributeChange, Blink, CellAttributes, Intensity, Underline},
|
||||
color::{AnsiColor, ColorAttribute, ColorSpec, LinearRgba, RgbColor, SrgbaTuple},
|
||||
surface::{Change, CursorVisibility, Position as TermwizPosition},
|
||||
terminal::{buffered::BufferedTerminal, ScreenSize, SystemTerminal, Terminal},
|
||||
};
|
||||
|
||||
/// A [`Backend`] implementation that uses [Termwiz] to render to the terminal.
|
||||
///
|
||||
/// The `TermwizBackend` struct is a wrapper around a [`BufferedTerminal`], which is used to send
|
||||
/// commands to the terminal. It provides methods for drawing content, manipulating the cursor, and
|
||||
/// clearing the terminal screen.
|
||||
///
|
||||
/// Most applications should not call the methods on `TermwizBackend` directly, but will instead
|
||||
/// use the [`Terminal`] struct, which provides a more ergonomic interface.
|
||||
///
|
||||
/// This backend automatically enables raw mode and switches to the alternate screen when it is
|
||||
/// created using the [`TermwizBackend::new`] method (and disables raw mode and returns to the main
|
||||
/// screen when dropped). Use the [`TermwizBackend::with_buffered_terminal`] to create a new
|
||||
/// instance with a custom [`BufferedTerminal`] if this is not desired.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::{backend::TermwizBackend, Terminal};
|
||||
///
|
||||
/// let backend = TermwizBackend::new()?;
|
||||
/// let mut terminal = Terminal::new(backend)?;
|
||||
///
|
||||
/// terminal.clear()?;
|
||||
/// terminal.draw(|frame| {
|
||||
/// // -- snip --
|
||||
/// })?;
|
||||
/// # std::result::Result::Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// See the the [Examples] directory for more examples. See the [`backend`] module documentation
|
||||
/// for more details on raw mode and alternate screen.
|
||||
///
|
||||
/// [`backend`]: ratatui_core::backend
|
||||
/// [`Terminal`]: https://docs.rs/ratatui/latest/ratatui/struct.Terminal.html
|
||||
/// [`BufferedTerminal`]: termwiz::terminal::buffered::BufferedTerminal
|
||||
/// [Termwiz]: https://crates.io/crates/termwiz
|
||||
/// [Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui/examples/README.md
|
||||
pub struct TermwizBackend {
|
||||
buffered_terminal: BufferedTerminal<SystemTerminal>,
|
||||
}
|
||||
|
||||
impl TermwizBackend {
|
||||
/// Creates a new Termwiz backend instance.
|
||||
///
|
||||
/// The backend will automatically enable raw mode and enter the alternate screen.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if unable to do any of the following:
|
||||
/// - query the terminal capabilities.
|
||||
/// - enter raw mode.
|
||||
/// - enter the alternate screen.
|
||||
/// - create the system or buffered terminal.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// use ratatui::backend::TermwizBackend;
|
||||
///
|
||||
/// let backend = TermwizBackend::new()?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub fn new() -> Result<Self, Box<dyn Error>> {
|
||||
let mut buffered_terminal =
|
||||
BufferedTerminal::new(SystemTerminal::new(Capabilities::new_from_env()?)?)?;
|
||||
buffered_terminal.terminal().set_raw_mode()?;
|
||||
buffered_terminal.terminal().enter_alternate_screen()?;
|
||||
Ok(Self { buffered_terminal })
|
||||
}
|
||||
|
||||
/// Creates a new Termwiz backend instance with the given buffered terminal.
|
||||
pub const fn with_buffered_terminal(instance: BufferedTerminal<SystemTerminal>) -> Self {
|
||||
Self {
|
||||
buffered_terminal: instance,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the buffered terminal used by the backend.
|
||||
pub const fn buffered_terminal(&self) -> &BufferedTerminal<SystemTerminal> {
|
||||
&self.buffered_terminal
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the buffered terminal used by the backend.
|
||||
pub fn buffered_terminal_mut(&mut self) -> &mut BufferedTerminal<SystemTerminal> {
|
||||
&mut self.buffered_terminal
|
||||
}
|
||||
}
|
||||
|
||||
impl Backend for TermwizBackend {
|
||||
fn draw<'a, I>(&mut self, content: I) -> io::Result<()>
|
||||
where
|
||||
I: Iterator<Item = (u16, u16, &'a Cell)>,
|
||||
{
|
||||
for (x, y, cell) in content {
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::CursorPosition {
|
||||
x: TermwizPosition::Absolute(x as usize),
|
||||
y: TermwizPosition::Absolute(y as usize),
|
||||
},
|
||||
Change::Attribute(AttributeChange::Foreground(cell.fg.into_termwiz())),
|
||||
Change::Attribute(AttributeChange::Background(cell.bg.into_termwiz())),
|
||||
]);
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Intensity(
|
||||
if cell.modifier.contains(Modifier::BOLD) {
|
||||
Intensity::Bold
|
||||
} else if cell.modifier.contains(Modifier::DIM) {
|
||||
Intensity::Half
|
||||
} else {
|
||||
Intensity::Normal
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Italic(
|
||||
cell.modifier.contains(Modifier::ITALIC),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Underline(
|
||||
if cell.modifier.contains(Modifier::UNDERLINED) {
|
||||
Underline::Single
|
||||
} else {
|
||||
Underline::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Reverse(
|
||||
cell.modifier.contains(Modifier::REVERSED),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Invisible(
|
||||
cell.modifier.contains(Modifier::HIDDEN),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::StrikeThrough(
|
||||
cell.modifier.contains(Modifier::CROSSED_OUT),
|
||||
)));
|
||||
|
||||
self.buffered_terminal
|
||||
.add_change(Change::Attribute(AttributeChange::Blink(
|
||||
if cell.modifier.contains(Modifier::SLOW_BLINK) {
|
||||
Blink::Slow
|
||||
} else if cell.modifier.contains(Modifier::RAPID_BLINK) {
|
||||
Blink::Rapid
|
||||
} else {
|
||||
Blink::None
|
||||
},
|
||||
)));
|
||||
|
||||
self.buffered_terminal.add_change(cell.symbol());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hide_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Hidden));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_cursor(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::CursorVisibility(CursorVisibility::Visible));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_cursor_position(&mut self) -> io::Result<Position> {
|
||||
let (x, y) = self.buffered_terminal.cursor_position();
|
||||
Ok(Position::new(x as u16, y as u16))
|
||||
}
|
||||
|
||||
fn set_cursor_position<P: Into<Position>>(&mut self, position: P) -> io::Result<()> {
|
||||
let Position { x, y } = position.into();
|
||||
self.buffered_terminal.add_change(Change::CursorPosition {
|
||||
x: TermwizPosition::Absolute(x as usize),
|
||||
y: TermwizPosition::Absolute(y as usize),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.add_change(Change::ClearScreen(termwiz::color::ColorAttribute::Default));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn size(&self) -> io::Result<Size> {
|
||||
let (cols, rows) = self.buffered_terminal.dimensions();
|
||||
Ok(Size::new(u16_max(cols), u16_max(rows)))
|
||||
}
|
||||
|
||||
fn window_size(&mut self) -> io::Result<WindowSize> {
|
||||
let ScreenSize {
|
||||
cols,
|
||||
rows,
|
||||
xpixel,
|
||||
ypixel,
|
||||
} = self
|
||||
.buffered_terminal
|
||||
.terminal()
|
||||
.get_screen_size()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(WindowSize {
|
||||
columns_rows: Size {
|
||||
width: u16_max(cols),
|
||||
height: u16_max(rows),
|
||||
},
|
||||
pixels: Size {
|
||||
width: u16_max(xpixel),
|
||||
height: u16_max(ypixel),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.buffered_terminal
|
||||
.flush()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_up(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
|
||||
// scrolling region and scrolling are combined. However, this has the side-effect of
|
||||
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
|
||||
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
|
||||
// lines. See [`Change::ScrollRegionUp`] for more details.
|
||||
let (_, rows) = self.buffered_terminal.dimensions();
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::ScrollRegionUp {
|
||||
first_row: region.start as usize,
|
||||
region_size: region.len(),
|
||||
scroll_count: amount as usize,
|
||||
},
|
||||
Change::ScrollRegionUp {
|
||||
first_row: 0,
|
||||
region_size: rows,
|
||||
scroll_count: 0,
|
||||
},
|
||||
]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "scrolling-regions")]
|
||||
fn scroll_region_down(&mut self, region: std::ops::Range<u16>, amount: u16) -> io::Result<()> {
|
||||
// termwiz doesn't have a command to just set the scrolling region. Instead, setting the
|
||||
// scrolling region and scrolling are combined. However, this has the side-effect of
|
||||
// leaving the scrolling region set. To reset the scrolling region, termwiz advises one to
|
||||
// make a scrolling-region scroll command that contains the entire screen, but scrolls by 0
|
||||
// lines. See [`Change::ScrollRegionDown`] for more details.
|
||||
let (_, rows) = self.buffered_terminal.dimensions();
|
||||
self.buffered_terminal.add_changes(vec![
|
||||
Change::ScrollRegionDown {
|
||||
first_row: region.start as usize,
|
||||
region_size: region.len(),
|
||||
scroll_count: amount as usize,
|
||||
},
|
||||
Change::ScrollRegionDown {
|
||||
first_row: 0,
|
||||
region_size: rows,
|
||||
scroll_count: 0,
|
||||
},
|
||||
]);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting types from Termwiz to Ratatui.
|
||||
///
|
||||
/// This trait replaces the `From` trait for converting types from Termwiz to Ratatui. It is
|
||||
/// necessary because the `From` trait is not implemented for types defined in external crates.
|
||||
pub trait FromTermwiz<T> {
|
||||
/// Converts the given Termwiz type to the Ratatui type.
|
||||
fn from_termwiz(termwiz: T) -> Self;
|
||||
}
|
||||
|
||||
/// A trait for converting types from Ratatui to Termwiz.
|
||||
///
|
||||
/// This trait replaces the `Into` trait for converting types from Ratatui to Termwiz. It is
|
||||
/// necessary because the `Into` trait is not implemented for types defined in external crates.
|
||||
pub trait IntoTermwiz<T> {
|
||||
/// Converts the given Ratatui type to the Termwiz type.
|
||||
fn into_termwiz(self) -> T;
|
||||
}
|
||||
|
||||
/// A replacement for the `Into` trait for converting types from Ratatui to Termwiz.
|
||||
///
|
||||
/// This trait is necessary because the `Into` trait is not implemented for types defined in
|
||||
/// external crates.
|
||||
///
|
||||
/// A blanket implementation is provided for all types that implement `FromTermwiz`.
|
||||
///
|
||||
/// This trait is private to the module as it would otherwise conflict with the other backend
|
||||
/// modules. It is mainly used to avoid rewriting all the `.into()` calls in this module.
|
||||
trait IntoRatatui<R> {
|
||||
fn into_ratatui(self) -> R;
|
||||
}
|
||||
|
||||
impl<C, R: FromTermwiz<C>> IntoRatatui<R> for C {
|
||||
fn into_ratatui(self) -> R {
|
||||
R::from_termwiz(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<CellAttributes> for Style {
|
||||
fn from_termwiz(value: CellAttributes) -> Self {
|
||||
let mut style = Self::new()
|
||||
.add_modifier(value.intensity().into_ratatui())
|
||||
.add_modifier(value.underline().into_ratatui())
|
||||
.add_modifier(value.blink().into_ratatui());
|
||||
|
||||
if value.italic() {
|
||||
style.add_modifier |= Modifier::ITALIC;
|
||||
}
|
||||
if value.reverse() {
|
||||
style.add_modifier |= Modifier::REVERSED;
|
||||
}
|
||||
if value.strikethrough() {
|
||||
style.add_modifier |= Modifier::CROSSED_OUT;
|
||||
}
|
||||
if value.invisible() {
|
||||
style.add_modifier |= Modifier::HIDDEN;
|
||||
}
|
||||
|
||||
style.fg = Some(value.foreground().into_ratatui());
|
||||
style.bg = Some(value.background().into_ratatui());
|
||||
#[cfg(feature = "underline-color")]
|
||||
{
|
||||
style.underline_color = Some(value.underline_color().into_ratatui());
|
||||
}
|
||||
|
||||
style
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<Intensity> for Modifier {
|
||||
fn from_termwiz(value: Intensity) -> Self {
|
||||
match value {
|
||||
Intensity::Normal => Self::empty(),
|
||||
Intensity::Bold => Self::BOLD,
|
||||
Intensity::Half => Self::DIM,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<Underline> for Modifier {
|
||||
fn from_termwiz(value: Underline) -> Self {
|
||||
match value {
|
||||
Underline::None => Self::empty(),
|
||||
_ => Self::UNDERLINED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<Blink> for Modifier {
|
||||
fn from_termwiz(value: Blink) -> Self {
|
||||
match value {
|
||||
Blink::None => Self::empty(),
|
||||
Blink::Slow => Self::SLOW_BLINK,
|
||||
Blink::Rapid => Self::RAPID_BLINK,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoTermwiz<ColorAttribute> for Color {
|
||||
fn into_termwiz(self) -> ColorAttribute {
|
||||
match self {
|
||||
Self::Reset => ColorAttribute::Default,
|
||||
Self::Black => AnsiColor::Black.into(),
|
||||
Self::DarkGray => AnsiColor::Grey.into(),
|
||||
Self::Gray => AnsiColor::Silver.into(),
|
||||
Self::Red => AnsiColor::Maroon.into(),
|
||||
Self::LightRed => AnsiColor::Red.into(),
|
||||
Self::Green => AnsiColor::Green.into(),
|
||||
Self::LightGreen => AnsiColor::Lime.into(),
|
||||
Self::Yellow => AnsiColor::Olive.into(),
|
||||
Self::LightYellow => AnsiColor::Yellow.into(),
|
||||
Self::Magenta => AnsiColor::Purple.into(),
|
||||
Self::LightMagenta => AnsiColor::Fuchsia.into(),
|
||||
Self::Cyan => AnsiColor::Teal.into(),
|
||||
Self::LightCyan => AnsiColor::Aqua.into(),
|
||||
Self::White => AnsiColor::White.into(),
|
||||
Self::Blue => AnsiColor::Navy.into(),
|
||||
Self::LightBlue => AnsiColor::Blue.into(),
|
||||
Self::Indexed(i) => ColorAttribute::PaletteIndex(i),
|
||||
Self::Rgb(r, g, b) => {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple::from((r, g, b)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<AnsiColor> for Color {
|
||||
fn from_termwiz(value: AnsiColor) -> Self {
|
||||
match value {
|
||||
AnsiColor::Black => Self::Black,
|
||||
AnsiColor::Grey => Self::DarkGray,
|
||||
AnsiColor::Silver => Self::Gray,
|
||||
AnsiColor::Maroon => Self::Red,
|
||||
AnsiColor::Red => Self::LightRed,
|
||||
AnsiColor::Green => Self::Green,
|
||||
AnsiColor::Lime => Self::LightGreen,
|
||||
AnsiColor::Olive => Self::Yellow,
|
||||
AnsiColor::Yellow => Self::LightYellow,
|
||||
AnsiColor::Purple => Self::Magenta,
|
||||
AnsiColor::Fuchsia => Self::LightMagenta,
|
||||
AnsiColor::Teal => Self::Cyan,
|
||||
AnsiColor::Aqua => Self::LightCyan,
|
||||
AnsiColor::White => Self::White,
|
||||
AnsiColor::Navy => Self::Blue,
|
||||
AnsiColor::Blue => Self::LightBlue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<ColorAttribute> for Color {
|
||||
fn from_termwiz(value: ColorAttribute) -> Self {
|
||||
match value {
|
||||
ColorAttribute::TrueColorWithDefaultFallback(srgba)
|
||||
| ColorAttribute::TrueColorWithPaletteFallback(srgba, _) => srgba.into_ratatui(),
|
||||
ColorAttribute::PaletteIndex(i) => Self::Indexed(i),
|
||||
ColorAttribute::Default => Self::Reset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<ColorSpec> for Color {
|
||||
fn from_termwiz(value: ColorSpec) -> Self {
|
||||
match value {
|
||||
ColorSpec::Default => Self::Reset,
|
||||
ColorSpec::PaletteIndex(i) => Self::Indexed(i),
|
||||
ColorSpec::TrueColor(srgba) => srgba.into_ratatui(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<SrgbaTuple> for Color {
|
||||
fn from_termwiz(value: SrgbaTuple) -> Self {
|
||||
let (r, g, b, _) = value.to_srgb_u8();
|
||||
Self::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<RgbColor> for Color {
|
||||
fn from_termwiz(value: RgbColor) -> Self {
|
||||
let (r, g, b) = value.to_tuple_rgb8();
|
||||
Self::Rgb(r, g, b)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromTermwiz<LinearRgba> for Color {
|
||||
fn from_termwiz(value: LinearRgba) -> Self {
|
||||
value.to_srgb().into_ratatui()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn u16_max(i: usize) -> u16 {
|
||||
u16::try_from(i).unwrap_or(u16::MAX)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
mod into_color {
|
||||
use Color as C;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_linear_rgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 0., 1.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
// full black + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 0., 0.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(1., 1., 1., 1.)),
|
||||
C::Rgb(254, 254, 254)
|
||||
);
|
||||
// full white + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(1., 1., 1., 0.)),
|
||||
C::Rgb(254, 254, 254)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(1., 0., 0., 1.)),
|
||||
C::Rgb(254, 0, 0)
|
||||
);
|
||||
// full green
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 1., 0., 1.)),
|
||||
C::Rgb(0, 254, 0)
|
||||
);
|
||||
// full blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 1., 1.)),
|
||||
C::Rgb(0, 0, 254)
|
||||
);
|
||||
|
||||
// See https://stackoverflow.com/questions/12524623/what-are-the-practical-differences-when-working-with-colors-in-a-linear-vs-a-no
|
||||
// for an explanation
|
||||
|
||||
// half red
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0.214, 0., 0., 1.)),
|
||||
C::Rgb(127, 0, 0)
|
||||
);
|
||||
// half green
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0.214, 0., 1.)),
|
||||
C::Rgb(0, 127, 0)
|
||||
);
|
||||
// half blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(LinearRgba(0., 0., 0.214, 1.)),
|
||||
C::Rgb(0, 0, 127)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_srgba() {
|
||||
// full black + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 0., 1.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
// full black + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 0., 0.)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
|
||||
// full white + opaque
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(1., 1., 1., 1.)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
// full white + transparent
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(1., 1., 1., 0.)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(1., 0., 0., 1.)),
|
||||
C::Rgb(255, 0, 0)
|
||||
);
|
||||
// full green
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 1., 0., 1.)),
|
||||
C::Rgb(0, 255, 0)
|
||||
);
|
||||
// full blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 1., 1.)),
|
||||
C::Rgb(0, 0, 255)
|
||||
);
|
||||
|
||||
// half red
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0.5, 0., 0., 1.)),
|
||||
C::Rgb(127, 0, 0)
|
||||
);
|
||||
// half green
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0.5, 0., 1.)),
|
||||
C::Rgb(0, 127, 0)
|
||||
);
|
||||
// half blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(SrgbaTuple(0., 0., 0.5, 1.)),
|
||||
C::Rgb(0, 0, 127)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_rgbcolor() {
|
||||
// full black
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 0, 0)),
|
||||
Color::Rgb(0, 0, 0)
|
||||
);
|
||||
// full white
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(255, 255, 255)),
|
||||
C::Rgb(255, 255, 255)
|
||||
);
|
||||
|
||||
// full red
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(255, 0, 0)),
|
||||
C::Rgb(255, 0, 0)
|
||||
);
|
||||
// full green
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 255, 0)),
|
||||
C::Rgb(0, 255, 0)
|
||||
);
|
||||
// full blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 0, 255)),
|
||||
C::Rgb(0, 0, 255)
|
||||
);
|
||||
|
||||
// half red
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(127, 0, 0)),
|
||||
C::Rgb(127, 0, 0)
|
||||
);
|
||||
// half green
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 127, 0)),
|
||||
C::Rgb(0, 127, 0)
|
||||
);
|
||||
// half blue
|
||||
assert_eq!(
|
||||
C::from_termwiz(RgbColor::new_8bpc(0, 0, 127)),
|
||||
C::Rgb(0, 0, 127)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorspec() {
|
||||
assert_eq!(C::from_termwiz(ColorSpec::Default), C::Reset);
|
||||
assert_eq!(C::from_termwiz(ColorSpec::PaletteIndex(33)), C::Indexed(33));
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorSpec::TrueColor(SrgbaTuple(0., 0., 0., 1.))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_colorattribute() {
|
||||
assert_eq!(C::from_termwiz(ColorAttribute::Default), C::Reset);
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorAttribute::PaletteIndex(32)),
|
||||
C::Indexed(32)
|
||||
);
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorAttribute::TrueColorWithDefaultFallback(SrgbaTuple(
|
||||
0., 0., 0., 1.
|
||||
))),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
assert_eq!(
|
||||
C::from_termwiz(ColorAttribute::TrueColorWithPaletteFallback(
|
||||
SrgbaTuple(0., 0., 0., 1.),
|
||||
31
|
||||
)),
|
||||
C::Rgb(0, 0, 0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_ansicolor() {
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Black), Color::Black);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Grey), Color::DarkGray);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Silver), Color::Gray);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Maroon), Color::Red);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Red), Color::LightRed);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Green), Color::Green);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Lime), Color::LightGreen);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Olive), Color::Yellow);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Yellow), Color::LightYellow);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Purple), Color::Magenta);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Fuchsia), Color::LightMagenta);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Teal), Color::Cyan);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Aqua), Color::LightCyan);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::White), Color::White);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Navy), Color::Blue);
|
||||
assert_eq!(C::from_termwiz(AnsiColor::Blue), Color::LightBlue);
|
||||
}
|
||||
}
|
||||
|
||||
mod into_modifier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn from_intensity() {
|
||||
assert_eq!(Modifier::from_termwiz(Intensity::Normal), Modifier::empty());
|
||||
assert_eq!(Modifier::from_termwiz(Intensity::Bold), Modifier::BOLD);
|
||||
assert_eq!(Modifier::from_termwiz(Intensity::Half), Modifier::DIM);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_underline() {
|
||||
assert_eq!(Modifier::from_termwiz(Underline::None), Modifier::empty());
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Single),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Double),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Curly),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Dashed),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
assert_eq!(
|
||||
Modifier::from_termwiz(Underline::Dotted),
|
||||
Modifier::UNDERLINED
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_blink() {
|
||||
assert_eq!(Modifier::from_termwiz(Blink::None), Modifier::empty());
|
||||
assert_eq!(Modifier::from_termwiz(Blink::Slow), Modifier::SLOW_BLINK);
|
||||
assert_eq!(Modifier::from_termwiz(Blink::Rapid), Modifier::RAPID_BLINK);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_cell_attribute_for_style() {
|
||||
use ratatui_core::style::Stylize;
|
||||
|
||||
#[cfg(feature = "underline-color")]
|
||||
const STYLE: Style = Style::new()
|
||||
.underline_color(Color::Reset)
|
||||
.fg(Color::Reset)
|
||||
.bg(Color::Reset);
|
||||
#[cfg(not(feature = "underline-color"))]
|
||||
const STYLE: Style = Style::new().fg(Color::Reset).bg(Color::Reset);
|
||||
|
||||
// default
|
||||
assert_eq!(Style::from_termwiz(CellAttributes::default()), STYLE);
|
||||
|
||||
// foreground color
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_foreground(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.fg(Color::Indexed(31))
|
||||
);
|
||||
// background color
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_background(ColorAttribute::PaletteIndex(31))
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.bg(Color::Indexed(31))
|
||||
);
|
||||
// underlined
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_underline(Underline::Single)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.underlined()
|
||||
);
|
||||
// blink
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_blink(Blink::Slow).to_owned()),
|
||||
STYLE.slow_blink()
|
||||
);
|
||||
// intensity
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_intensity(Intensity::Bold)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.bold()
|
||||
);
|
||||
// italic
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_italic(true).to_owned()),
|
||||
STYLE.italic()
|
||||
);
|
||||
// reversed
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_reverse(true).to_owned()),
|
||||
STYLE.reversed()
|
||||
);
|
||||
// strikethrough
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_strikethrough(true).to_owned()),
|
||||
STYLE.crossed_out()
|
||||
);
|
||||
// hidden
|
||||
assert_eq!(
|
||||
Style::from_termwiz(CellAttributes::default().set_invisible(true).to_owned()),
|
||||
STYLE.hidden()
|
||||
);
|
||||
|
||||
// underline color
|
||||
#[cfg(feature = "underline-color")]
|
||||
assert_eq!(
|
||||
Style::from_termwiz(
|
||||
CellAttributes::default()
|
||||
.set_underline_color(AnsiColor::Red)
|
||||
.to_owned()
|
||||
),
|
||||
STYLE.underline_color(Color::Indexed(9))
|
||||
);
|
||||
}
|
||||
}
|
||||
117
ratatui-widgets/Cargo.toml
Normal file
117
ratatui-widgets/Cargo.toml
Normal file
@@ -0,0 +1,117 @@
|
||||
[package]
|
||||
name = "ratatui-widgets"
|
||||
description = "A collection of Ratatui widgets for building terminal user interfaces using Ratatui."
|
||||
# Note that this started at 0.3.0 as there was a previous crate using the name `ratatui-widgets`.
|
||||
# <https://github.com/joshka/ratatui-widgets/issues/46>
|
||||
version = "0.3.0-alpha.0"
|
||||
readme = "README.md"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[features]
|
||||
default = ["all-widgets"]
|
||||
|
||||
## enables serialization and deserialization of style and color types using the [`serde`] crate.
|
||||
## This is useful if you want to save themes to a file.
|
||||
serde = ["dep:serde", "ratatui-core/serde"]
|
||||
|
||||
#! Widgets that add dependencies are gated behind feature flags to prevent unused transitive
|
||||
#! dependencies. The available features are:
|
||||
|
||||
## enables all widgets.
|
||||
all-widgets = ["calendar"]
|
||||
|
||||
## enables the [`calendar`](calendar) widget module and adds a dependency on [`time`].
|
||||
calendar = ["dep:time"]
|
||||
|
||||
## Enable all unstable features.
|
||||
unstable = ["unstable-rendered-line-info"]
|
||||
|
||||
## Enables the [`Paragraph::line_count`](paragraph::Paragraph::line_count)
|
||||
## [`Paragraph::line_width`](paragraph::Paragraph::line_width) methods
|
||||
## which are experimental and may change in the future.
|
||||
## See [Issue 293](https://github.com/ratatui/ratatui/issues/293) for more details.
|
||||
unstable-rendered-line-info = []
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
itertools.workspace = true
|
||||
indoc.workspace = true
|
||||
instability.workspace = true
|
||||
ratatui-core = { workspace = true }
|
||||
strum.workspace = true
|
||||
time = { version = "0.3.11", optional = true, features = ["local-offset"] }
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-width.workspace = true
|
||||
serde = { workspace = true, optional = true }
|
||||
document-features = { workspace = true, optional = true }
|
||||
line-clipping = "0.2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
color-eyre.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
ratatui = { path = "../ratatui" }
|
||||
rstest.workspace = true
|
||||
|
||||
[lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
must_use_candidate = "allow"
|
||||
|
||||
# we often split up a module into multiple files with the main type in a file named after the
|
||||
# module, so we want to allow this pattern
|
||||
module_inception = "allow"
|
||||
|
||||
# nursery or restricted
|
||||
as_underscore = "warn"
|
||||
deref_by_slicing = "warn"
|
||||
else_if_without_else = "warn"
|
||||
empty_line_after_doc_comments = "warn"
|
||||
equatable_if_let = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
format_push_string = "warn"
|
||||
map_err_ignore = "warn"
|
||||
missing_const_for_fn = "warn"
|
||||
mixed_read_write_in_expression = "warn"
|
||||
mod_module_files = "warn"
|
||||
needless_pass_by_ref_mut = "warn"
|
||||
needless_raw_strings = "warn"
|
||||
or_fun_call = "warn"
|
||||
redundant_type_annotations = "warn"
|
||||
rest_pat_in_fully_bound_structs = "warn"
|
||||
string_lit_chars_any = "warn"
|
||||
string_slice = "warn"
|
||||
string_to_string = "warn"
|
||||
unnecessary_self_imports = "warn"
|
||||
use_self = "warn"
|
||||
|
||||
[[example]]
|
||||
name = "barchart"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "block"
|
||||
doc-scrape-examples = true
|
||||
80
ratatui-widgets/README.md
Normal file
80
ratatui-widgets/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Ratatui Widgets
|
||||
|
||||
[](https://crates.io/crates/ratatui-widgets)
|
||||
[](https://docs.rs/ratatui-widgets)
|
||||
[](../LICENSE)
|
||||
|
||||
<!-- ⚠️ DO NOT EDIT THIS FILE DIRECTLY, EDIT lib.rs AND THEN RUN `cargo rdme` to update this file. -->
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
**ratatui-widgets** contains all the widgets that were previously part of the [Ratatui] crate.
|
||||
It is meant to be used in conjunction with `ratatui`, which provides the core functionality
|
||||
for building terminal user interfaces.
|
||||
|
||||
[Ratatui]: https://crates.io/crates/ratatui
|
||||
|
||||
Most applications shouldn't need to depend directly on `ratatui-widgets`, `ratatui` crate
|
||||
re-exports all the widgets from this crate. However, if you are building a widget library that
|
||||
internally uses these widgets, or if you prefer finer grained dependencies, you may want to
|
||||
depend on this crate rather than transitively through the `ratatui` crate.
|
||||
|
||||
Previously, a crate named `ratatui-widgets` was published with some formative ideas about an
|
||||
eventual Ratatui framework. That crate is now move to [tui-framework-experiment], pending a new
|
||||
name.
|
||||
|
||||
[tui-framework-experiment]: https://crates.io/crates/tui-framework-experiment
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following command to add this crate to your project:
|
||||
|
||||
```sh
|
||||
cargo add ratatui-widgets
|
||||
```
|
||||
|
||||
## Available Widgets
|
||||
|
||||
- [`BarChart`]: displays multiple datasets as bars with optional grouping.
|
||||
- [`Block`]: a basic widget that draws a block with optional borders, titles, and styles.
|
||||
- [`calendar::Monthly`]: displays a single month.
|
||||
- [`Canvas`]: draws arbitrary shapes using drawing characters.
|
||||
- [`Chart`]: displays multiple datasets as lines or scatter graphs.
|
||||
- [`Clear`]: clears the area it occupies. Useful to render over previously drawn widgets.
|
||||
- [`Gauge`]: displays progress percentage using block characters.
|
||||
- [`LineGauge`]: displays progress as a line.
|
||||
- [`List`]: displays a list of items and allows selection.
|
||||
- [`RatatuiLogo`]: displays the Ratatui logo.
|
||||
- [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
|
||||
- [`Scrollbar`]: displays a scrollbar.
|
||||
- [`Sparkline`]: displays a single dataset as a sparkline.
|
||||
- [`Table`]: displays multiple rows and columns in a grid and allows selection.
|
||||
- [`Tabs`]: displays a tab bar and allows selection.
|
||||
|
||||
[`BarChart`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/barchart/struct.BarChart.html
|
||||
[`Block`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/block/struct.Block.html
|
||||
[`calendar::Monthly`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/calendar/struct.Monthly.html
|
||||
[`Canvas`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/canvas/struct.Canvas.html
|
||||
[`Chart`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/chart/struct.Chart.html
|
||||
[`Clear`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/clear/struct.Clear.html
|
||||
[`Gauge`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/gauge/struct.Gauge.html
|
||||
[`LineGauge`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/gauge/struct.LineGauge.html
|
||||
[`List`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/list/struct.List.html
|
||||
[`RatatuiLogo`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/logo/struct.RatatuiLogo.html
|
||||
[`Paragraph`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/paragraph/struct.Paragraph.html
|
||||
[`Scrollbar`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/scrollbar/struct.Scrollbar.html
|
||||
[`Sparkline`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/sparkline/struct.Sparkline.html
|
||||
[`Table`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/table/struct.Table.html
|
||||
[`Tabs`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/tabs/struct.Tabs.html
|
||||
|
||||
All these widgets are re-exported directly under `ratatui::widgets` in the `ratatui` crate.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please open an issue or submit a pull request on GitHub. For more
|
||||
details on contributing, please see the [CONTRIBUTING](CONTRIBUTING.md) document.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
91
ratatui-widgets/examples/barchart.rs
Normal file
91
ratatui-widgets/examples/barchart.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! # [Ratatui] `BarChart` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [widget examples] folder in the
|
||||
//! repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [widget examples]: https://github.com/ratatui/ratatui/blob/main/ratatui-widgets/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
text::{Line, Span},
|
||||
widgets::{Bar, BarChart},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(draw)?;
|
||||
if quit_key_pressed()? {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the UI with a title and two barcharts.
|
||||
fn draw(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
|
||||
let horizontal = Layout::horizontal([Constraint::Length(28), Constraint::Fill(1)]).spacing(1);
|
||||
let [top, main] = vertical.areas(frame.area());
|
||||
let [left, right] = horizontal.areas(main);
|
||||
|
||||
let title = Line::from_iter([
|
||||
Span::from("BarChart Widget").bold(),
|
||||
Span::from(" (Press 'q' to quit)"),
|
||||
]);
|
||||
frame.render_widget(title.centered(), top);
|
||||
render_vertical_barchart(frame, left);
|
||||
render_horizontal_barchart(frame, right);
|
||||
}
|
||||
|
||||
/// Render a horizontal barchart with some sample data.
|
||||
fn render_horizontal_barchart(frame: &mut Frame, area: Rect) {
|
||||
let bars = vec![
|
||||
Bar::with_label("Red", 30).red(),
|
||||
Bar::with_label("Blue", 20).blue(),
|
||||
Bar::with_label("Green", 15).green(),
|
||||
Bar::with_label("Yellow", 10).yellow(),
|
||||
];
|
||||
let chart = BarChart::horizontal(bars).bar_width(3);
|
||||
frame.render_widget(chart, area);
|
||||
}
|
||||
|
||||
/// Render a vertical barchart with some sample data.
|
||||
fn render_vertical_barchart(frame: &mut Frame, area: Rect) {
|
||||
let bars = vec![
|
||||
Bar::with_label("Red", 30).red(),
|
||||
Bar::with_label("Blue", 20).blue(),
|
||||
Bar::with_label("Green", 15).green(),
|
||||
Bar::with_label("Yellow", 10).yellow(),
|
||||
];
|
||||
let chart = BarChart::vertical(bars).bar_width(6);
|
||||
frame.render_widget(chart, area);
|
||||
}
|
||||
|
||||
/// Wait for an event and return `true` if the Esc or 'q' key is pressed.
|
||||
fn quit_key_pressed() -> Result<bool> {
|
||||
use ratatui::crossterm::event::{self, Event, KeyCode};
|
||||
match event::read()? {
|
||||
Event::Key(event) if matches!(event.code, KeyCode::Esc | KeyCode::Char('q')) => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
84
ratatui-widgets/examples/block.rs
Normal file
84
ratatui-widgets/examples/block.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
//! # [Ratatui] `Block` example
|
||||
//!
|
||||
//! The latest version of this example is available in the [widget examples] folder in the
|
||||
//! repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui/ratatui
|
||||
//! [widget examples]: https://github.com/ratatui/ratatui/blob/main/ratatui-widgets/examples
|
||||
//! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
/// Run the application.
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(draw)?;
|
||||
if matches!(event::read()?, Event::Key(_)) {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the UI with various blocks.
|
||||
fn draw(frame: &mut Frame) {
|
||||
let vertical = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).spacing(1);
|
||||
let horizontal = Layout::horizontal([Constraint::Percentage(33); 3]).spacing(1);
|
||||
let [top, main] = vertical.areas(frame.area());
|
||||
let [left, middle, right] = horizontal.areas(main);
|
||||
|
||||
let title = Line::from_iter([
|
||||
Span::from("Block Widget").bold(),
|
||||
Span::from(" (Press 'q' to quit)"),
|
||||
]);
|
||||
frame.render_widget(title.centered(), top);
|
||||
|
||||
render_bordered_block(frame, left);
|
||||
render_styled_block(frame, middle);
|
||||
render_custom_bordered_block(frame, right);
|
||||
}
|
||||
|
||||
/// Render a block with borders.
|
||||
pub fn render_bordered_block(frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered().title("Bordered block");
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
|
||||
/// Render a styled block.
|
||||
pub fn render_styled_block(frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered()
|
||||
.style(Style::new().blue().on_black().bold().italic())
|
||||
.title("Styled block");
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
|
||||
/// Render a block with custom borders.
|
||||
pub fn render_custom_bordered_block(frame: &mut Frame, area: Rect) {
|
||||
let block = Block::bordered()
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::new().red())
|
||||
.title("Custom borders");
|
||||
frame.render_widget(block, area);
|
||||
}
|
||||
@@ -1,11 +1,20 @@
|
||||
use crate::{prelude::*, style::Styled, widgets::Block};
|
||||
//! The [`BarChart`] widget and its related types (e.g. [`Bar`], [`BarGroup`]).
|
||||
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Direction, Rect},
|
||||
style::{Style, Styled},
|
||||
symbols::{self},
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
pub use self::{bar::Bar, bar_group::BarGroup};
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
mod bar;
|
||||
mod bar_group;
|
||||
|
||||
pub use bar::Bar;
|
||||
pub use bar_group::BarGroup;
|
||||
|
||||
/// A chart showing values as [bars](Bar).
|
||||
///
|
||||
/// Here is a possible `BarChart` output.
|
||||
@@ -42,7 +51,10 @@ pub use bar_group::BarGroup;
|
||||
/// The first group is added by an array slice (`&[(&str, u64)]`).
|
||||
/// The second group is added by a [`BarGroup`] instance.
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::{Bar, BarChart, BarGroup, Block},
|
||||
/// };
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .block(Block::bordered().title("BarChart"))
|
||||
@@ -52,10 +64,21 @@ pub use bar_group::BarGroup;
|
||||
/// .bar_style(Style::new().yellow().on_red())
|
||||
/// .value_style(Style::new().red().bold())
|
||||
/// .label_style(Style::new().white())
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]))
|
||||
/// .data(&[("A0", 0), ("A1", 2), ("A2", 4), ("A3", 3)])
|
||||
/// .data(BarGroup::new([
|
||||
/// Bar::with_label("B0", 10),
|
||||
/// Bar::with_label("B2", 20),
|
||||
/// ]))
|
||||
/// .max(4);
|
||||
/// ```
|
||||
///
|
||||
/// For simpler usages, you can also create a `BarChart` simply by
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::{Bar, BarChart};
|
||||
///
|
||||
/// BarChart::new([Bar::with_label("A", 10), Bar::with_label("B", 20)]);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct BarChart<'a> {
|
||||
/// Block to wrap the widget in
|
||||
@@ -105,6 +128,52 @@ impl<'a> Default for BarChart<'a> {
|
||||
}
|
||||
|
||||
impl<'a> BarChart<'a> {
|
||||
/// Creates a new vertical `BarChart` widget with the given bars.
|
||||
///
|
||||
/// The `bars` parameter accepts any type that can be converted into a `Vec<Bar>`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{
|
||||
/// layout::Direction,
|
||||
/// widgets::{Bar, BarChart},
|
||||
/// };
|
||||
///
|
||||
/// BarChart::new(vec![Bar::with_label("A", 10), Bar::with_label("B", 10)]);
|
||||
/// ```
|
||||
pub fn new<T: Into<Vec<Bar<'a>>>>(bars: T) -> Self {
|
||||
Self {
|
||||
data: vec![BarGroup::new(bars.into())],
|
||||
direction: Direction::Vertical,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `BarChart` widget with a vertical direction.
|
||||
///
|
||||
/// This function is equivalent to `BarChart::new()`.
|
||||
pub fn vertical(bars: impl Into<Vec<Bar<'a>>>) -> Self {
|
||||
Self::new(bars)
|
||||
}
|
||||
|
||||
/// Creates a new `BarChart` widget with a horizontal direction.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::{Bar, BarChart};
|
||||
///
|
||||
/// BarChart::horizontal(vec![Bar::with_label("A", 10), Bar::with_label("B", 20)]);
|
||||
/// ```
|
||||
pub fn horizontal(bars: impl Into<Vec<Bar<'a>>>) -> Self {
|
||||
Self {
|
||||
data: vec![BarGroup::new(bars.into())],
|
||||
direction: Direction::Horizontal,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Add group of bars to the `BarChart`
|
||||
///
|
||||
/// # Examples
|
||||
@@ -113,10 +182,14 @@ impl<'a> BarChart<'a> {
|
||||
/// The first group is added by an array slice (`&[(&str, u64)]`).
|
||||
/// The second group is added by a [`BarGroup`] instance.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Bar, BarChart, BarGroup};
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .data(&[("B0", 0), ("B1", 2), ("B2", 4), ("B3", 3)])
|
||||
/// .data(BarGroup::default().bars(&[Bar::default().value(10), Bar::default().value(20)]));
|
||||
/// .data(BarGroup::new([
|
||||
/// Bar::with_label("A", 10),
|
||||
/// Bar::with_label("B", 20),
|
||||
/// ]));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn data(mut self, data: impl Into<BarGroup<'a>>) -> Self {
|
||||
@@ -143,7 +216,7 @@ impl<'a> BarChart<'a> {
|
||||
/// This example shows the default behavior when `max` is not set.
|
||||
/// The maximum value in the dataset is taken (here, `100`).
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::BarChart;
|
||||
/// BarChart::default().data(&[("foo", 1), ("bar", 2), ("baz", 100)]);
|
||||
/// // Renders
|
||||
/// // █
|
||||
@@ -154,7 +227,8 @@ impl<'a> BarChart<'a> {
|
||||
/// This example shows a custom max value.
|
||||
/// The maximum height being `2`, `bar` & `baz` render as the max.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::BarChart;
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .data(&[("foo", 1), ("bar", 2), ("baz", 100)])
|
||||
/// .max(2);
|
||||
@@ -176,6 +250,8 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// It is also possible to set individually the style of each [`Bar`].
|
||||
/// In this case the default style will be patched by the individual style
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn bar_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.bar_style = style.into();
|
||||
@@ -184,8 +260,8 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the width of the displayed bars.
|
||||
///
|
||||
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars this becomes the height of
|
||||
/// the bar.
|
||||
/// For [`Horizontal`](ratatui_core::layout::Direction::Horizontal) bars this becomes the height
|
||||
/// of the bar.
|
||||
///
|
||||
/// If not set, this defaults to `1`.
|
||||
/// The bar label also uses this value as its width.
|
||||
@@ -204,7 +280,8 @@ impl<'a> BarChart<'a> {
|
||||
///
|
||||
/// This shows two bars with a gap of `3`. Notice the labels will always stay under the bar.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::BarChart;
|
||||
///
|
||||
/// BarChart::default()
|
||||
/// .data(&[("foo", 1), ("bar", 2)])
|
||||
/// .bar_gap(3);
|
||||
@@ -219,9 +296,9 @@ impl<'a> BarChart<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// The [`bar::Set`](crate::symbols::bar::Set) to use for displaying the bars.
|
||||
/// The [`bar::Set`](ratatui_core::symbols::bar::Set) to use for displaying the bars.
|
||||
///
|
||||
/// If not set, the default is [`bar::NINE_LEVELS`](crate::symbols::bar::NINE_LEVELS).
|
||||
/// If not set, the default is [`bar::NINE_LEVELS`](ratatui_core::symbols::bar::NINE_LEVELS).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn bar_set(mut self, bar_set: symbols::bar::Set) -> Self {
|
||||
self.bar_set = bar_set;
|
||||
@@ -239,6 +316,8 @@ impl<'a> BarChart<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value_style`] to set the value style individually.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.value_style = style.into();
|
||||
@@ -256,6 +335,8 @@ impl<'a> BarChart<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::label`] to set the label style individually.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.label_style = style.into();
|
||||
@@ -275,6 +356,8 @@ impl<'a> BarChart<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The style will be applied to everything that isn't styled (borders, bars, labels, ...).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -283,7 +366,7 @@ impl<'a> BarChart<'a> {
|
||||
|
||||
/// Set the direction of the bars.
|
||||
///
|
||||
/// [`Vertical`](crate::layout::Direction::Vertical) bars are the default.
|
||||
/// [`Vertical`](ratatui_core::layout::Direction::Vertical) bars are the default.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -577,15 +660,15 @@ impl BarChart<'_> {
|
||||
|
||||
impl Widget for BarChart<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for BarChart<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &BarChart<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
self.block.render_ref(area, buf);
|
||||
self.block.as_ref().render(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
|
||||
if inner.is_empty() || self.data.is_empty() || self.bar_width == 0 {
|
||||
@@ -613,9 +696,14 @@ impl<'a> Styled for BarChart<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use itertools::iproduct;
|
||||
use ratatui_core::{
|
||||
layout::Alignment,
|
||||
style::{Color, Modifier, Stylize},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::widgets::BorderType;
|
||||
use crate::borders::BorderType;
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
@@ -846,10 +934,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_empty_group() {
|
||||
let chart = BarChart::default()
|
||||
.data(BarGroup::default().label("invisible".into()))
|
||||
.data(BarGroup::default().label("invisible"))
|
||||
.data(
|
||||
BarGroup::default()
|
||||
.label("G".into())
|
||||
.label("G")
|
||||
.bars(&[Bar::default().value(1), Bar::default().value(2)]),
|
||||
);
|
||||
|
||||
@@ -866,12 +954,12 @@ mod tests {
|
||||
|
||||
fn build_test_barchart<'a>() -> BarChart<'a> {
|
||||
BarChart::default()
|
||||
.data(BarGroup::default().label("G1".into()).bars(&[
|
||||
.data(BarGroup::default().label("G1").bars(&[
|
||||
Bar::default().value(2),
|
||||
Bar::default().value(3),
|
||||
Bar::default().value(4),
|
||||
]))
|
||||
.data(BarGroup::default().label("G2".into()).bars(&[
|
||||
.data(BarGroup::default().label("G2").bars(&[
|
||||
Bar::default().value(3),
|
||||
Bar::default().value(4),
|
||||
Bar::default().value(5),
|
||||
@@ -938,7 +1026,7 @@ mod tests {
|
||||
fn test_horizontal_bars_label_width_greater_than_bar(bar_color: Option<Color>) {
|
||||
let mut bar = Bar::default()
|
||||
.value(2)
|
||||
.text_value("label".into())
|
||||
.text_value("label")
|
||||
.value_style(Style::default().red());
|
||||
|
||||
if let Some(color) = bar_color {
|
||||
@@ -1013,7 +1101,7 @@ mod tests {
|
||||
let chart: BarChart<'_> = BarChart::default()
|
||||
.data(
|
||||
BarGroup::default()
|
||||
.label(Span::from("G1").red().into())
|
||||
.label(Span::from("G1").red())
|
||||
.bars(&[Bar::default().value(2)]),
|
||||
)
|
||||
.group_gap(1)
|
||||
@@ -1081,18 +1169,9 @@ mod tests {
|
||||
#[test]
|
||||
fn test_unicode_as_value() {
|
||||
let group = BarGroup::default().bars(&[
|
||||
Bar::default()
|
||||
.value(123)
|
||||
.label("B1".into())
|
||||
.text_value("写".into()),
|
||||
Bar::default()
|
||||
.value(321)
|
||||
.label("B2".into())
|
||||
.text_value("写".into()),
|
||||
Bar::default()
|
||||
.value(333)
|
||||
.label("B2".into())
|
||||
.text_value("写".into()),
|
||||
Bar::default().value(123).label("B1").text_value("写"),
|
||||
Bar::default().value(321).label("B2").text_value("写"),
|
||||
Bar::default().value(333).label("B2").text_value("写"),
|
||||
]);
|
||||
let chart = BarChart::default().data(group).bar_width(3).bar_gap(1);
|
||||
|
||||
@@ -1134,7 +1213,7 @@ mod tests {
|
||||
("i", 8),
|
||||
])
|
||||
.into();
|
||||
group = group.label("Group".into());
|
||||
group = group.label("Group");
|
||||
|
||||
let chart = BarChart::default()
|
||||
.data(group)
|
||||
@@ -1159,7 +1238,7 @@ mod tests {
|
||||
("i", 8),
|
||||
])
|
||||
.into();
|
||||
group = group.label("Group".into());
|
||||
group = group.label("Group");
|
||||
|
||||
let chart = BarChart::default()
|
||||
.data(group)
|
||||
@@ -1326,4 +1405,19 @@ mod tests {
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_barchart_new() {
|
||||
let bars = [Bar::with_label("Red", 1), Bar::with_label("Green", 2)];
|
||||
|
||||
let chart = BarChart::new(bars.clone());
|
||||
assert_eq!(chart.data.len(), 1);
|
||||
assert_eq!(chart.data[0].bars, bars);
|
||||
|
||||
let bars2 = [("Blue", 3)];
|
||||
|
||||
let updated_chart = chart.data(&bars2);
|
||||
assert_eq!(updated_chart.data.len(), 2);
|
||||
assert_eq!(updated_chart.data[1].bars, [Bar::with_label("Blue", 3)]);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A bar to be shown by the [`BarChart`](crate::widgets::BarChart) widget.
|
||||
/// A bar to be shown by the [`BarChart`](super::BarChart) widget.
|
||||
///
|
||||
/// Here is an explanation of a `Bar`'s components.
|
||||
/// ```plain
|
||||
@@ -17,14 +22,15 @@ use crate::prelude::*;
|
||||
/// The following example creates a bar with the label "Bar 1", a value "10",
|
||||
/// red background and a white value foreground.
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::Bar,
|
||||
/// };
|
||||
///
|
||||
/// Bar::default()
|
||||
/// .label("Bar 1".into())
|
||||
/// .value(10)
|
||||
/// .style(Style::default().fg(Color::Red))
|
||||
/// .value_style(Style::default().bg(Color::Red).fg(Color::White))
|
||||
/// .text_value("10°C".to_string());
|
||||
/// Bar::with_label("Bar 1", 10)
|
||||
/// .red()
|
||||
/// .value_style(Style::new().red().on_white())
|
||||
/// .text_value("10°C");
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Bar<'a> {
|
||||
@@ -41,14 +47,54 @@ pub struct Bar<'a> {
|
||||
}
|
||||
|
||||
impl<'a> Bar<'a> {
|
||||
/// Creates a new `Bar` with the given value.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// let bar = Bar::new(42);
|
||||
/// ```
|
||||
pub const fn new(value: u64) -> Self {
|
||||
Self {
|
||||
value,
|
||||
label: None,
|
||||
style: Style::new(),
|
||||
value_style: Style::new(),
|
||||
text_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a new `Bar` with the given `label` and value.
|
||||
///
|
||||
/// a `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// let bar = Bar::with_label("Label", 42);
|
||||
/// ```
|
||||
pub fn with_label<T: Into<Line<'a>>>(label: T, value: u64) -> Self {
|
||||
Self {
|
||||
value,
|
||||
label: Some(label.into()),
|
||||
style: Style::new(),
|
||||
value_style: Style::new(),
|
||||
text_value: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the value of this bar.
|
||||
///
|
||||
/// The value will be displayed inside the bar.
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value_style`] to style the value.
|
||||
/// [`Bar::text_value`] to set the displayed value.
|
||||
/// - [`Bar::value_style`] to style the value.
|
||||
/// - [`Bar::text_value`] to set the displayed value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn value(mut self, value: u64) -> Self {
|
||||
self.value = value;
|
||||
@@ -57,14 +103,35 @@ impl<'a> Bar<'a> {
|
||||
|
||||
/// Set the label of the bar.
|
||||
///
|
||||
/// For [`Vertical`](crate::layout::Direction::Vertical) bars,
|
||||
/// `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From [`&str`] and [`String`]:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// Bar::default().label("label");
|
||||
/// Bar::default().label(String::from("label"));
|
||||
/// ```
|
||||
///
|
||||
/// From a [`Line`] with red foreground color:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{style::Stylize, text::Line, widgets::Bar};
|
||||
///
|
||||
/// Bar::default().label(Line::from("Line").red());
|
||||
/// ```
|
||||
///
|
||||
/// For [`Vertical`](ratatui_core::layout::Direction::Vertical) bars,
|
||||
/// display the label **under** the bar.
|
||||
/// For [`Horizontal`](crate::layout::Direction::Horizontal) bars,
|
||||
/// For [`Horizontal`](ratatui_core::layout::Direction::Horizontal) bars,
|
||||
/// display the label **in** the bar.
|
||||
/// See [`BarChart::direction`](crate::widgets::BarChart::direction) to set the direction.
|
||||
/// See [`BarChart::direction`](crate::barchart::BarChart::direction) to set the direction.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label(mut self, label: Line<'a>) -> Self {
|
||||
self.label = Some(label);
|
||||
pub fn label<T: Into<Line<'a>>>(mut self, label: T) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -74,6 +141,8 @@ impl<'a> Bar<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This will apply to every non-styled element. It can be seen and used as a default value.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -88,6 +157,8 @@ impl<'a> Bar<'a> {
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.value_style = style.into();
|
||||
@@ -96,15 +167,28 @@ impl<'a> Bar<'a> {
|
||||
|
||||
/// Set the text value printed in the bar.
|
||||
///
|
||||
/// `text_value` can be a [`&str`], `Number` or anything that can be converted into [`String`].
|
||||
///
|
||||
/// If `text_value` is not set, then the [`ToString`] representation of `value` will be shown on
|
||||
/// the bar.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From [`&str`] and [`String`]:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::Bar;
|
||||
///
|
||||
/// Bar::default().text_value("label");
|
||||
/// Bar::default().text_value(String::from("label"));
|
||||
/// ```
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// [`Bar::value`] to set the value.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn text_value(mut self, text_value: String) -> Self {
|
||||
self.text_value = Some(text_value);
|
||||
pub fn text_value<T: Into<String>>(mut self, text_value: T) -> Self {
|
||||
self.text_value = Some(text_value.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -200,3 +284,46 @@ impl<'a> Bar<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Bar<'a> {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::{Color, Modifier, Style, Stylize};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bar_new() {
|
||||
let bar = Bar::new(42).label(Line::from("Label"));
|
||||
assert_eq!(bar.label, Some(Line::from("Label")));
|
||||
assert_eq!(bar.value, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_with_label() {
|
||||
let bar = Bar::with_label("Label", 42);
|
||||
assert_eq!(bar.label, Some(Line::from("Label")));
|
||||
assert_eq!(bar.value, 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bar_stylized() {
|
||||
let bar = Bar::default().red().bold();
|
||||
assert_eq!(
|
||||
bar.style,
|
||||
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
use super::Bar;
|
||||
use crate::prelude::*;
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::Style,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::barchart::Bar;
|
||||
|
||||
/// A group of bars to be shown by the Barchart.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Bar, BarGroup};
|
||||
///
|
||||
/// BarGroup::default()
|
||||
/// .label("Group 1".into())
|
||||
/// .bars(&[Bar::default().value(200), Bar::default().value(150)]);
|
||||
/// let group = BarGroup::new([Bar::with_label("Red", 20), Bar::with_label("Blue", 15)]);
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct BarGroup<'a> {
|
||||
@@ -21,10 +26,50 @@ pub struct BarGroup<'a> {
|
||||
}
|
||||
|
||||
impl<'a> BarGroup<'a> {
|
||||
/// Creates a new `BarGroup` with the given bars.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::{Bar, BarGroup},
|
||||
/// };
|
||||
///
|
||||
/// let group = BarGroup::new(vec![Bar::with_label("A", 10), Bar::with_label("B", 20)]);
|
||||
/// ```
|
||||
pub fn new<T: Into<Vec<Bar<'a>>>>(bars: T) -> Self {
|
||||
Self {
|
||||
bars: bars.into(),
|
||||
..Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the group label
|
||||
///
|
||||
/// `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// From [`&str`] and [`String`].
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::widgets::BarGroup;
|
||||
///
|
||||
/// BarGroup::default().label("label");
|
||||
/// BarGroup::default().label(String::from("label"));
|
||||
/// ```
|
||||
///
|
||||
/// From a [`Line`] with red foreground color:
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{style::Stylize, text::Line, widgets::BarGroup};
|
||||
///
|
||||
/// BarGroup::default().label(Line::from("Line").red());
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn label(mut self, label: Line<'a>) -> Self {
|
||||
self.label = Some(label);
|
||||
pub fn label<T: Into<Line<'a>>>(mut self, label: T) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -70,7 +115,7 @@ impl<'a> From<&[(&'a str, u64)]> for BarGroup<'a> {
|
||||
label: None,
|
||||
bars: value
|
||||
.iter()
|
||||
.map(|&(text, v)| Bar::default().value(v).label(text.into()))
|
||||
.map(|&(text, v)| Bar::with_label(text, v))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
@@ -89,3 +134,16 @@ impl<'a> From<&Vec<(&'a str, u64)>> for BarGroup<'a> {
|
||||
Self::from(array)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bargroup_new() {
|
||||
let group = BarGroup::new([Bar::with_label("Label1", 1), Bar::with_label("Label2", 2)])
|
||||
.label(Line::from("Group1"));
|
||||
assert_eq!(group.label, Some(Line::from("Group1")));
|
||||
assert_eq!(group.bars.len(), 2);
|
||||
}
|
||||
}
|
||||
@@ -6,17 +6,25 @@
|
||||
//! [title](Block::title) and [padding](Block::padding).
|
||||
|
||||
use itertools::Itertools;
|
||||
use strum::{Display, EnumString};
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Style, Styled},
|
||||
symbols::border,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::{prelude::*, style::Styled, symbols::border, widgets::Borders};
|
||||
pub use self::{
|
||||
padding::Padding,
|
||||
title::{Position, Title},
|
||||
};
|
||||
use crate::borders::{BorderType, Borders};
|
||||
|
||||
mod padding;
|
||||
pub mod title;
|
||||
|
||||
pub use padding::Padding;
|
||||
pub use title::{Position, Title};
|
||||
|
||||
/// Base widget to be used to display a box border around all [upper level ones](crate::widgets).
|
||||
/// Base widget to be used to display a box border around all other built-in widgets.
|
||||
///
|
||||
/// The borders can be configured with [`Block::borders`] and others. A block can have multiple
|
||||
/// [`Title`] using [`Block::title`]. It can also be [styled](Block::style) and
|
||||
@@ -67,7 +75,10 @@ pub use title::{Position, Title};
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Style},
|
||||
/// widgets::{Block, BorderType, Borders},
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
/// .border_type(BorderType::Rounded)
|
||||
@@ -79,12 +90,9 @@ pub use title::{Position, Title};
|
||||
///
|
||||
/// You may also use multiple titles like in the following:
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// },
|
||||
/// use ratatui::widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
@@ -94,10 +102,7 @@ pub use title::{Position, Title};
|
||||
///
|
||||
/// You can also pass it as parameters of another widget so that the block surrounds them:
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{Block, Borders, List},
|
||||
/// };
|
||||
/// use ratatui::widgets::{Block, Borders, List};
|
||||
///
|
||||
/// let surrounding_block = Block::default()
|
||||
/// .borders(Borders::ALL)
|
||||
@@ -108,7 +113,7 @@ pub use title::{Position, Title};
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Block<'a> {
|
||||
/// List of titles
|
||||
titles: Vec<Title<'a>>,
|
||||
titles: Vec<(Option<Position>, Line<'a>)>,
|
||||
/// The style to be patched to all titles of the block
|
||||
titles_style: Style,
|
||||
/// The default alignment of the titles that don't have one
|
||||
@@ -128,79 +133,6 @@ pub struct Block<'a> {
|
||||
padding: Padding,
|
||||
}
|
||||
|
||||
/// The type of border of a [`Block`].
|
||||
///
|
||||
/// See the [`borders`](Block::borders) method of `Block` to configure its borders.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum BorderType {
|
||||
/// A plain, simple border.
|
||||
///
|
||||
/// This is the default
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────┐
|
||||
/// │ │
|
||||
/// └───────┘
|
||||
/// ```
|
||||
#[default]
|
||||
Plain,
|
||||
/// A plain border with rounded corners.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╭───────╮
|
||||
/// │ │
|
||||
/// ╰───────╯
|
||||
/// ```
|
||||
Rounded,
|
||||
/// A doubled border.
|
||||
///
|
||||
/// Note this uses one character that draws two lines.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╔═══════╗
|
||||
/// ║ ║
|
||||
/// ╚═══════╝
|
||||
/// ```
|
||||
Double,
|
||||
/// A thick border.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┏━━━━━━━┓
|
||||
/// ┃ ┃
|
||||
/// ┗━━━━━━━┛
|
||||
/// ```
|
||||
Thick,
|
||||
/// A border with a single line on the inside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▗▄▄▄▄▄▄▄▖
|
||||
/// ▐ ▌
|
||||
/// ▐ ▌
|
||||
/// ▝▀▀▀▀▀▀▀▘
|
||||
QuadrantInside,
|
||||
|
||||
/// A border with a single line on the outside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▛▀▀▀▀▀▀▀▜
|
||||
/// ▌ ▐
|
||||
/// ▌ ▐
|
||||
/// ▙▄▄▄▄▄▄▄▟
|
||||
QuadrantOutside,
|
||||
}
|
||||
|
||||
impl<'a> Block<'a> {
|
||||
/// Creates a new block with no [`Borders`] or [`Padding`].
|
||||
pub const fn new() -> Self {
|
||||
@@ -220,7 +152,8 @@ impl<'a> Block<'a> {
|
||||
/// Create a new block with [all borders](Borders::ALL) shown
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Block, Borders};
|
||||
/// use ratatui::widgets::{Block, Borders};
|
||||
///
|
||||
/// assert_eq!(Block::bordered(), Block::new().borders(Borders::ALL));
|
||||
/// ```
|
||||
pub const fn bordered() -> Self {
|
||||
@@ -239,13 +172,13 @@ impl<'a> Block<'a> {
|
||||
/// space is calculated based on the full width of the block, rather than the leftover width.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Title`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
|
||||
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// By default, the titles will avoid being rendered in the corners of the block but will align
|
||||
/// against the left or right edge of the block if there is no border on that edge.
|
||||
/// The following demonstrates this behavior, notice the second title is one character off to
|
||||
/// the left.
|
||||
/// against the left or right edge of the block if there is no border on that edge. The
|
||||
/// following demonstrates this behavior, notice the second title is one character off to the
|
||||
/// left.
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌With at least a left border───
|
||||
@@ -268,15 +201,15 @@ impl<'a> Block<'a> {
|
||||
/// - Two titles with the same alignment (notice the left titles are separated)
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// text::Line,
|
||||
/// widgets::{Block, Borders},
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title("Title") // By default in the top left corner
|
||||
/// .title(Title::from("Left").alignment(Alignment::Left)) // also on the left
|
||||
/// .title(Title::from("Right").alignment(Alignment::Right))
|
||||
/// .title(Title::from("Center").alignment(Alignment::Center));
|
||||
/// .title(Line::from("Left").left_aligned()) // also on the left
|
||||
/// .title(Line::from("Right").right_aligned())
|
||||
/// .title(Line::from("Center").centered());
|
||||
/// // Renders
|
||||
/// // ┌Title─Left────Center─────────Right┐
|
||||
/// ```
|
||||
@@ -288,26 +221,40 @@ impl<'a> Block<'a> {
|
||||
/// - [`Block::title_alignment`]
|
||||
/// - [`Block::title_position`]
|
||||
///
|
||||
/// # Future improvements
|
||||
///
|
||||
/// In a future release of Ratatui this method will be changed to accept `Into<Line>` instead of
|
||||
/// `Into<Title>`. This allows us to remove the unnecessary `Title` struct and store the
|
||||
/// position in the block itself. For more information see
|
||||
/// <https://github.com/ratatui/ratatui/issues/738>.
|
||||
///
|
||||
/// [Block example]: https://github.com/ratatui/ratatui/blob/main/examples/README.md#block
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title<T>(mut self, title: T) -> Self
|
||||
where
|
||||
T: Into<Title<'a>>,
|
||||
{
|
||||
self.titles.push(title.into());
|
||||
let title = title.into();
|
||||
let position = title.position;
|
||||
let mut content = title.content;
|
||||
if let Some(alignment) = title.alignment {
|
||||
content = content.alignment(alignment);
|
||||
}
|
||||
self.titles.push((position, content));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a title to the top of the block.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Line`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
|
||||
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{ prelude::*, widgets::* };
|
||||
/// use ratatui::{ widgets::Block, text::Line };
|
||||
///
|
||||
/// Block::bordered()
|
||||
/// .title_top("Left1") // By default in the top left corner
|
||||
/// .title_top(Line::from("Left2").left_aligned())
|
||||
@@ -321,21 +268,22 @@ impl<'a> Block<'a> {
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_top<T: Into<Line<'a>>>(mut self, title: T) -> Self {
|
||||
let title = Title::from(title).position(Position::Top);
|
||||
self.titles.push(title);
|
||||
let line = title.into();
|
||||
self.titles.push((Some(Position::Top), line));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a title to the bottom of the block.
|
||||
///
|
||||
/// You can provide any type that can be converted into [`Line`] including: strings, string
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](crate::text::Span), or vectors of
|
||||
/// [spans](crate::text::Span) (`Vec<Span>`).
|
||||
/// slices (`&str`), borrowed strings (`Cow<str>`), [spans](ratatui_core::text::Span), or
|
||||
/// vectors of [spans](ratatui_core::text::Span) (`Vec<Span>`).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{ prelude::*, widgets::* };
|
||||
/// use ratatui::{ widgets::Block, text::Line };
|
||||
///
|
||||
/// Block::bordered()
|
||||
/// .title_bottom("Left1") // By default in the top left corner
|
||||
/// .title_bottom(Line::from("Left2").left_aligned())
|
||||
@@ -349,8 +297,8 @@ impl<'a> Block<'a> {
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_bottom<T: Into<Line<'a>>>(mut self, title: T) -> Self {
|
||||
let title = Title::from(title).position(Position::Bottom);
|
||||
self.titles.push(title);
|
||||
let line = title.into();
|
||||
self.titles.push((Some(Position::Bottom), line));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -364,6 +312,8 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn title_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.titles_style = style.into();
|
||||
@@ -379,15 +329,12 @@ impl<'a> Block<'a> {
|
||||
/// This example aligns all titles in the center except the "right" title which explicitly sets
|
||||
/// [`Alignment::Right`].
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{block::*, *},
|
||||
/// };
|
||||
/// use ratatui::{layout::Alignment, text::Line, widgets::Block};
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title_alignment(Alignment::Center)
|
||||
/// // This title won't be aligned in the center
|
||||
/// .title(Title::from("right").alignment(Alignment::Right))
|
||||
/// .title(Line::from("right").right_aligned())
|
||||
/// .title("foo")
|
||||
/// .title("bar");
|
||||
/// ```
|
||||
@@ -406,18 +353,12 @@ impl<'a> Block<'a> {
|
||||
/// This example positions all titles on the bottom except the "top" title which explicitly sets
|
||||
/// [`Position::Top`].
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// },
|
||||
/// };
|
||||
/// use ratatui::widgets::{block::Position, Block};
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title_position(Position::Bottom)
|
||||
/// // This title won't be aligned in the center
|
||||
/// .title(Title::from("top").position(Position::Top))
|
||||
/// .title_top("top")
|
||||
/// .title("foo")
|
||||
/// .title("bar");
|
||||
/// ```
|
||||
@@ -441,9 +382,14 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// This example shows a `Block` with blue borders.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::Block,
|
||||
/// };
|
||||
/// Block::bordered().border_style(Style::new().blue());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn border_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.border_style = style.into();
|
||||
@@ -466,7 +412,11 @@ impl<'a> Block<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Style, Stylize},
|
||||
/// widgets::{Block, Paragraph},
|
||||
/// };
|
||||
///
|
||||
/// let block = Block::new().style(Style::new().red().on_black());
|
||||
///
|
||||
/// // For border and title you can additionally apply styles on top of the block level style.
|
||||
@@ -482,7 +432,8 @@ impl<'a> Block<'a> {
|
||||
/// .style(Style::new().white().not_bold()); // will be white, and italic
|
||||
/// ```
|
||||
///
|
||||
/// [`Paragraph`]: crate::widgets::Paragraph
|
||||
/// [`Paragraph`]: crate::paragraph::Paragraph
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -497,7 +448,7 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// Display left and right borders.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, Borders};
|
||||
/// Block::new().borders(Borders::LEFT | Borders::RIGHT);
|
||||
/// ```
|
||||
///
|
||||
@@ -518,7 +469,7 @@ impl<'a> Block<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, BorderType};
|
||||
/// Block::bordered()
|
||||
/// .border_type(BorderType::Rounded)
|
||||
/// .title("Block");
|
||||
@@ -533,14 +484,15 @@ impl<'a> Block<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbols used to display the border as a [`crate::symbols::border::Set`].
|
||||
/// Sets the symbols used to display the border as a [`ratatui_core::symbols::border::Set`].
|
||||
///
|
||||
/// Setting this overwrites any [`border_type`](Block::border_type) that was set.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{widgets::Block, symbols};
|
||||
///
|
||||
/// Block::bordered().border_set(symbols::border::DOUBLE).title("Block");
|
||||
/// // Renders
|
||||
/// // ╔Block╗
|
||||
@@ -560,7 +512,8 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// This renders a `Block` with no padding (the default).
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, Padding};
|
||||
///
|
||||
/// Block::bordered().padding(Padding::ZERO);
|
||||
/// // Renders
|
||||
/// // ┌───────┐
|
||||
@@ -571,7 +524,8 @@ impl<'a> Block<'a> {
|
||||
/// This example shows a `Block` with padding left and right ([`Padding::horizontal`]).
|
||||
/// Notice the two spaces before and after the content.
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, Padding};
|
||||
///
|
||||
/// Block::bordered().padding(Padding::horizontal(2));
|
||||
/// // Renders
|
||||
/// // ┌───────────┐
|
||||
@@ -590,7 +544,8 @@ impl<'a> Block<'a> {
|
||||
///
|
||||
/// Draw a block nested within another block
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{widgets::Block, Frame};
|
||||
///
|
||||
/// # fn render_nested_block(frame: &mut Frame) {
|
||||
/// let outer_block = Block::bordered().title("Outer");
|
||||
/// let inner_block = Block::bordered().title("Inner");
|
||||
@@ -642,37 +597,18 @@ impl<'a> Block<'a> {
|
||||
fn has_title_at_position(&self, position: Position) -> bool {
|
||||
self.titles
|
||||
.iter()
|
||||
.any(|title| title.position.unwrap_or(self.titles_position) == position)
|
||||
}
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn border_symbols(border_type: Self) -> border::Set {
|
||||
match border_type {
|
||||
Self::Plain => border::PLAIN,
|
||||
Self::Rounded => border::ROUNDED,
|
||||
Self::Double => border::DOUBLE,
|
||||
Self::Thick => border::THICK,
|
||||
Self::QuadrantInside => border::QUADRANT_INSIDE,
|
||||
Self::QuadrantOutside => border::QUADRANT_OUTSIDE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn to_border_set(self) -> border::Set {
|
||||
Self::border_symbols(self)
|
||||
.any(|(pos, _)| pos.unwrap_or(self.titles_position) == position)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Block<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Block<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Block<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let area = area.intersection(buf.area);
|
||||
if area.is_empty() {
|
||||
return;
|
||||
@@ -798,7 +734,7 @@ impl Block<'_> {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_width = title.width() as u16;
|
||||
let title_area = Rect {
|
||||
x: titles_area
|
||||
.right()
|
||||
@@ -808,7 +744,7 @@ impl Block<'_> {
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
title.render(title_area, buf);
|
||||
|
||||
// bump the width of the titles area to the left
|
||||
titles_area.width = titles_area
|
||||
@@ -830,7 +766,7 @@ impl Block<'_> {
|
||||
.collect_vec();
|
||||
let total_width = titles
|
||||
.iter()
|
||||
.map(|title| title.content.width() as u16 + 1) // space between titles
|
||||
.map(|title| title.width() as u16 + 1) // space between titles
|
||||
.sum::<u16>()
|
||||
.saturating_sub(1); // no space for the last title
|
||||
|
||||
@@ -843,13 +779,13 @@ impl Block<'_> {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_width = title.width() as u16;
|
||||
let title_area = Rect {
|
||||
width: title_width.min(titles_area.width),
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
title.render(title_area, buf);
|
||||
|
||||
// bump the titles area to the right and reduce its width
|
||||
titles_area.x = titles_area.x.saturating_add(title_width + 1);
|
||||
@@ -866,13 +802,13 @@ impl Block<'_> {
|
||||
if titles_area.is_empty() {
|
||||
break;
|
||||
}
|
||||
let title_width = title.content.width() as u16;
|
||||
let title_width = title.width() as u16;
|
||||
let title_area = Rect {
|
||||
width: title_width.min(titles_area.width),
|
||||
..titles_area
|
||||
};
|
||||
buf.set_style(title_area, self.titles_style);
|
||||
title.content.render_ref(title_area, buf);
|
||||
title.render(title_area, buf);
|
||||
|
||||
// bump the titles area to the right and reduce its width
|
||||
titles_area.x = titles_area.x.saturating_add(title_width + 1);
|
||||
@@ -885,11 +821,12 @@ impl Block<'_> {
|
||||
&self,
|
||||
position: Position,
|
||||
alignment: Alignment,
|
||||
) -> impl DoubleEndedIterator<Item = &Title> {
|
||||
self.titles.iter().filter(move |title| {
|
||||
title.position.unwrap_or(self.titles_position) == position
|
||||
&& title.alignment.unwrap_or(self.titles_alignment) == alignment
|
||||
})
|
||||
) -> impl DoubleEndedIterator<Item = &Line> {
|
||||
self.titles
|
||||
.iter()
|
||||
.filter(move |(pos, _)| pos.unwrap_or(self.titles_position) == position)
|
||||
.filter(move |(_, line)| line.alignment.unwrap_or(self.titles_alignment) == alignment)
|
||||
.map(|(_, line)| line)
|
||||
}
|
||||
|
||||
/// An area that is one line tall and spans the width of the block excluding the borders and
|
||||
@@ -972,6 +909,7 @@ impl<'a> Styled for Block<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::{Color, Modifier, Stylize};
|
||||
use rstest::rstest;
|
||||
use strum::ParseError;
|
||||
|
||||
@@ -1023,24 +961,17 @@ mod tests {
|
||||
let area = Rect::new(0, 0, 0, 1);
|
||||
let expected = Rect::new(0, 1, 0, 0);
|
||||
|
||||
let block = Block::new().title(Title::from("Test").alignment(alignment));
|
||||
let block = Block::new().title(Line::from("Test").alignment(alignment));
|
||||
assert_eq!(block.inner(area), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::top_top(Borders::TOP, Position::Top, Rect::new(0, 1, 0, 1))]
|
||||
#[case::top_bot(Borders::BOTTOM, Position::Top, Rect::new(0, 1, 0, 0))]
|
||||
#[case::bot_top(Borders::TOP, Position::Bottom, Rect::new(0, 1, 0, 0))]
|
||||
#[case::top_top(Borders::BOTTOM, Position::Bottom, Rect::new(0, 0, 0, 1))]
|
||||
fn inner_takes_into_account_border_and_title(
|
||||
#[case] borders: Borders,
|
||||
#[case] position: Position,
|
||||
#[case] expected: Rect,
|
||||
) {
|
||||
#[case::top_top(Block::new().title_top("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 1))]
|
||||
#[case::top_bot(Block::new().title_top("Test").borders(Borders::BOTTOM), Rect::new(0, 1, 0, 0))]
|
||||
#[case::bot_top(Block::new().title_bottom("Test").borders(Borders::TOP), Rect::new(0, 1, 0, 0))]
|
||||
#[case::bot_bot(Block::new().title_bottom("Test").borders(Borders::BOTTOM), Rect::new(0, 0, 0, 1))]
|
||||
fn inner_takes_into_account_border_and_title(#[case] block: Block, #[case] expected: Rect) {
|
||||
let area = Rect::new(0, 0, 0, 2);
|
||||
let block = Block::new()
|
||||
.borders(borders)
|
||||
.title(Title::from("Test").position(position));
|
||||
assert_eq!(block.inner(area), expected);
|
||||
}
|
||||
|
||||
@@ -1050,32 +981,33 @@ mod tests {
|
||||
assert!(!block.has_title_at_position(Position::Top));
|
||||
assert!(!block.has_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::new().title(Title::from("Test").position(Position::Top));
|
||||
let block = Block::new().title_top("test");
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(!block.has_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::new().title(Title::from("Test").position(Position::Bottom));
|
||||
let block = Block::new().title_bottom("test");
|
||||
assert!(!block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title_position(Position::Bottom);
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(!block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
.title_position(Position::Top);
|
||||
assert!(!block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title(Title::from("Test").position(Position::Bottom));
|
||||
let block = Block::new().title_top("test").title_bottom("test");
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test").position(Position::Top))
|
||||
.title(Title::from("Test"))
|
||||
@@ -1083,6 +1015,7 @@ mod tests {
|
||||
assert!(block.has_title_at_position(Position::Top));
|
||||
assert!(block.has_title_at_position(Position::Bottom));
|
||||
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
let block = Block::new()
|
||||
.title(Title::from("Test"))
|
||||
.title(Title::from("Test").position(Position::Bottom))
|
||||
@@ -1130,16 +1063,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn vertical_space_takes_into_account_titles() {
|
||||
let block = Block::new()
|
||||
.title_position(Position::Top)
|
||||
.title(Title::from("Test"));
|
||||
|
||||
let block = Block::new().title_top("Test");
|
||||
assert_eq!(block.vertical_space(), (1, 0));
|
||||
|
||||
let block = Block::new()
|
||||
.title_position(Position::Bottom)
|
||||
.title(Title::from("Test"));
|
||||
|
||||
let block = Block::new().title_bottom("Test");
|
||||
assert_eq!(block.vertical_space(), (0, 1));
|
||||
}
|
||||
|
||||
@@ -1158,10 +1085,7 @@ mod tests {
|
||||
#[case] pos: Position,
|
||||
#[case] vertical_space: (u16, u16),
|
||||
) {
|
||||
let block = block
|
||||
.borders(borders)
|
||||
.title_position(pos)
|
||||
.title(Title::from("Test"));
|
||||
let block = block.borders(borders).title_position(pos).title("Test");
|
||||
assert_eq!(block.vertical_space(), vertical_space);
|
||||
}
|
||||
|
||||
@@ -1310,6 +1234,7 @@ mod tests {
|
||||
use Alignment::*;
|
||||
use Position::*;
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 11, 3));
|
||||
#[allow(deprecated)] // until Title is removed
|
||||
Block::bordered()
|
||||
.title(Title::from("A").position(Top).alignment(Left))
|
||||
.title(Title::from("B").position(Top).alignment(Center))
|
||||
@@ -1375,7 +1300,7 @@ mod tests {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
|
||||
Block::new()
|
||||
.title_alignment(block_title_alignment)
|
||||
.title(Title::from("test").alignment(alignment))
|
||||
.title(Line::from("test").alignment(alignment))
|
||||
.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([expected]));
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::Padding;
|
||||
///
|
||||
/// Padding::uniform(1);
|
||||
/// Padding::horizontal(2);
|
||||
@@ -19,8 +19,8 @@
|
||||
/// Padding::symmetric(5, 6);
|
||||
/// ```
|
||||
///
|
||||
/// [`Block`]: crate::widgets::Block
|
||||
/// [`padding`]: crate::widgets::Block::padding
|
||||
/// [`Block`]: crate::block::Block
|
||||
/// [`padding`]: crate::block::Block::padding
|
||||
/// [CSS padding]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
|
||||
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||
pub struct Padding {
|
||||
@@ -1,14 +1,24 @@
|
||||
//! This module holds the [`Title`] element and its related configuration types.
|
||||
//! A title is a piece of [`Block`](crate::widgets::Block) configuration.
|
||||
//! A title is a piece of [`Block`](crate::block::Block) configuration.
|
||||
|
||||
use ratatui_core::{layout::Alignment, text::Line};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{layout::Alignment, text::Line};
|
||||
|
||||
/// A [`Block`](crate::widgets::Block) title.
|
||||
/// A [`Block`](crate::block::Block) title.
|
||||
///
|
||||
/// It can be aligned (see [`Alignment`]) and positioned (see [`Position`]).
|
||||
///
|
||||
/// # Future Deprecation
|
||||
///
|
||||
/// This type is deprecated and will be removed in a future release. The reason for this is that the
|
||||
/// position of the title should be stored in the block itself, not in the title. The `Line` type
|
||||
/// has an alignment method that can be used to align the title. For more information see
|
||||
/// <https://github.com/ratatui/ratatui/issues/738>.
|
||||
///
|
||||
/// Use [`Line`] instead, when the position is not defined as part of the title. When a specific
|
||||
/// position is needed, use [`Block::title_top`](crate::block::Block::title_top) or
|
||||
/// [`Block::title_bottom`](crate::block::Block::title_bottom) instead.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// Title with no style.
|
||||
@@ -18,16 +28,16 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// Title::from("Title");
|
||||
/// ```
|
||||
///
|
||||
/// Blue title on a white background (via [`Stylize`](crate::style::Stylize) trait).
|
||||
/// Blue title on a white background (via [`Stylize`](ratatui_core::style::Stylize) trait).
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::block::*};
|
||||
/// use ratatui::{style::Stylize, widgets::block::Title};
|
||||
///
|
||||
/// Title::from("Title".blue().on_white());
|
||||
/// ```
|
||||
///
|
||||
/// Title with multiple styles (see [`Line`] and [`Stylize`](crate::style::Stylize)).
|
||||
/// Title with multiple styles (see [`Line`] and [`Stylize`](ratatui_core::style::Stylize)).
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::block::*};
|
||||
/// use ratatui::{style::Stylize, text::Line, widgets::block::Title};
|
||||
///
|
||||
/// Title::from(Line::from(vec!["Q".white().underlined(), "uit".gray()]));
|
||||
/// ```
|
||||
@@ -35,7 +45,7 @@ use crate::{layout::Alignment, text::Line};
|
||||
/// Complete example
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// prelude::*,
|
||||
/// layout::Alignment,
|
||||
/// widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
@@ -53,19 +63,19 @@ pub struct Title<'a> {
|
||||
/// Title alignment
|
||||
///
|
||||
/// If [`None`], defaults to the alignment defined with
|
||||
/// [`Block::title_alignment`](crate::widgets::Block::title_alignment) in the associated
|
||||
/// [`Block`](crate::widgets::Block).
|
||||
/// [`Block::title_alignment`](crate::block::Block::title_alignment) in the associated
|
||||
/// [`Block`](crate::block::Block).
|
||||
pub alignment: Option<Alignment>,
|
||||
|
||||
/// Title position
|
||||
///
|
||||
/// If [`None`], defaults to the position defined with
|
||||
/// [`Block::title_position`](crate::widgets::Block::title_position) in the associated
|
||||
/// [`Block`](crate::widgets::Block).
|
||||
/// [`Block::title_position`](crate::block::Block::title_position) in the associated
|
||||
/// [`Block`](crate::block::Block).
|
||||
pub position: Option<Position>,
|
||||
}
|
||||
|
||||
/// Defines the [title](crate::widgets::block::Title) position.
|
||||
/// Defines the [title](crate::block::Title) position.
|
||||
///
|
||||
/// The title can be positioned on top or at the bottom of the block.
|
||||
/// Defaults to [`Position::Top`].
|
||||
@@ -73,7 +83,10 @@ pub struct Title<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::{block::*, *};
|
||||
/// use ratatui::widgets::{
|
||||
/// block::{Position, Title},
|
||||
/// Block,
|
||||
/// };
|
||||
///
|
||||
/// Block::new().title(Title::from("title").position(Position::Bottom));
|
||||
/// ```
|
||||
@@ -88,6 +101,7 @@ pub enum Position {
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[deprecated = "use Block::title_top() or Block::title_bottom() instead. This will be removed in a future release."]
|
||||
impl<'a> Title<'a> {
|
||||
/// Set the title content.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -1,6 +1,9 @@
|
||||
//! Border related types ([`Borders`], [`BorderType`]) and a macro to create borders ([`border`]).
|
||||
use std::fmt;
|
||||
|
||||
use bitflags::bitflags;
|
||||
use ratatui_core::symbols::border;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
bitflags! {
|
||||
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
|
||||
@@ -21,6 +24,98 @@ bitflags! {
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of border of a [`Block`](crate::block::Block).
|
||||
///
|
||||
/// See the [`borders`](crate::block::Block::borders) method of `Block` to configure its borders.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum BorderType {
|
||||
/// A plain, simple border.
|
||||
///
|
||||
/// This is the default
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┌───────┐
|
||||
/// │ │
|
||||
/// └───────┘
|
||||
/// ```
|
||||
#[default]
|
||||
Plain,
|
||||
/// A plain border with rounded corners.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╭───────╮
|
||||
/// │ │
|
||||
/// ╰───────╯
|
||||
/// ```
|
||||
Rounded,
|
||||
/// A doubled border.
|
||||
///
|
||||
/// Note this uses one character that draws two lines.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ╔═══════╗
|
||||
/// ║ ║
|
||||
/// ╚═══════╝
|
||||
/// ```
|
||||
Double,
|
||||
/// A thick border.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ┏━━━━━━━┓
|
||||
/// ┃ ┃
|
||||
/// ┗━━━━━━━┛
|
||||
/// ```
|
||||
Thick,
|
||||
/// A border with a single line on the inside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▗▄▄▄▄▄▄▄▖
|
||||
/// ▐ ▌
|
||||
/// ▐ ▌
|
||||
/// ▝▀▀▀▀▀▀▀▘
|
||||
QuadrantInside,
|
||||
|
||||
/// A border with a single line on the outside of a half block.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```plain
|
||||
/// ▛▀▀▀▀▀▀▀▜
|
||||
/// ▌ ▐
|
||||
/// ▌ ▐
|
||||
/// ▙▄▄▄▄▄▄▄▟
|
||||
QuadrantOutside,
|
||||
}
|
||||
|
||||
impl BorderType {
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn border_symbols(border_type: Self) -> border::Set {
|
||||
match border_type {
|
||||
Self::Plain => border::PLAIN,
|
||||
Self::Rounded => border::ROUNDED,
|
||||
Self::Double => border::DOUBLE,
|
||||
Self::Thick => border::THICK,
|
||||
Self::QuadrantInside => border::QUADRANT_INSIDE,
|
||||
Self::QuadrantOutside => border::QUADRANT_OUTSIDE,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert this `BorderType` into the corresponding [`Set`](border::Set) of border symbols.
|
||||
pub const fn to_border_set(self) -> border::Set {
|
||||
Self::border_symbols(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Implement the `Debug` trait for the `Borders` bitflags. This is a manual implementation to
|
||||
/// display the flags in a more readable way. The default implementation would display the
|
||||
/// flags as 'Border(0x0)' for `Borders::NONE` for example.
|
||||
@@ -55,12 +150,16 @@ impl fmt::Debug for Borders {
|
||||
/// and RIGHT.
|
||||
///
|
||||
/// When used with NONE you should consider omitting this completely. For ALL you should consider
|
||||
/// [`Block::bordered()`](crate::widgets::Block::bordered) instead.
|
||||
/// [`Block::bordered()`](crate::block::Block::bordered) instead.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{border, prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// border,
|
||||
/// widgets::{Block, Borders},
|
||||
/// };
|
||||
///
|
||||
/// Block::new()
|
||||
/// .title("Construct Borders and use them in place")
|
||||
/// .borders(border!(TOP, BOTTOM));
|
||||
@@ -69,7 +168,7 @@ impl fmt::Debug for Borders {
|
||||
/// `border!` can be called with any number of individual sides:
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{border, prelude::*, widgets::*};
|
||||
/// use ratatui::{border, widgets::Borders};
|
||||
/// let right_open = border!(TOP, LEFT, BOTTOM);
|
||||
/// assert_eq!(right_open, Borders::TOP | Borders::LEFT | Borders::BOTTOM);
|
||||
/// ```
|
||||
@@ -77,12 +176,12 @@ impl fmt::Debug for Borders {
|
||||
/// Single borders work but using `Borders::` directly would be simpler.
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{border, prelude::*, widgets::*};
|
||||
/// use ratatui::{border, widgets::Borders};
|
||||
///
|
||||
/// assert_eq!(border!(TOP), Borders::TOP);
|
||||
/// assert_eq!(border!(ALL), Borders::ALL);
|
||||
/// assert_eq!(border!(), Borders::NONE);
|
||||
/// ```
|
||||
#[cfg(feature = "macros")]
|
||||
#[macro_export]
|
||||
macro_rules! border {
|
||||
() => {
|
||||
@@ -119,11 +218,6 @@ mod tests {
|
||||
"TOP | BOTTOM"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "macros"))]
|
||||
mod macro_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn can_be_const() {
|
||||
@@ -10,9 +10,16 @@
|
||||
//! [`Monthly`] has several controls for what should be displayed
|
||||
use std::collections::HashMap;
|
||||
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::Style,
|
||||
text::{Line, Span},
|
||||
widgets::Widget,
|
||||
};
|
||||
use time::{Date, Duration, OffsetDateTime};
|
||||
|
||||
use crate::{prelude::*, widgets::Block};
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
/// Display a month calendar for the month containing `display_date`
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -46,6 +53,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn show_surrounding<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_surrounding = Some(style.into());
|
||||
@@ -56,6 +65,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn show_weekdays_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_weekday = Some(style.into());
|
||||
@@ -66,6 +77,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn show_month_header<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.show_month = Some(style.into());
|
||||
@@ -76,6 +89,8 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn default_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.default_style = style.into();
|
||||
@@ -121,13 +136,13 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
|
||||
|
||||
impl<DS: DateStyler> Widget for Monthly<'_, DS> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<DS: DateStyler> WidgetRef for Monthly<'_, DS> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.render_ref(area, buf);
|
||||
impl<DS: DateStyler> Widget for &Monthly<'_, DS> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.as_ref().render(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
self.render_monthly(inner, buf);
|
||||
}
|
||||
@@ -177,7 +192,9 @@ impl<DS: DateStyler> Monthly<'_, DS> {
|
||||
spans.push(self.format_date(curr_day));
|
||||
curr_day += Duration::DAY;
|
||||
}
|
||||
buf.set_line(days_area.x, y, &spans.into(), area.width);
|
||||
if buf.area.height > y {
|
||||
buf.set_line(days_area.x, y, &spans.into(), area.width);
|
||||
}
|
||||
y += 1;
|
||||
}
|
||||
}
|
||||
@@ -199,6 +216,8 @@ impl CalendarEventStore {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
pub fn today<S: Into<Style>>(style: S) -> Self {
|
||||
let mut res = Self::default();
|
||||
res.add(
|
||||
@@ -214,6 +233,8 @@ impl CalendarEventStore {
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
pub fn add<S: Into<Style>>(&mut self, date: Date, style: S) {
|
||||
// to simplify style nonsense, last write wins
|
||||
let _ = self.0.insert(date, style.into());
|
||||
@@ -245,6 +266,7 @@ impl Default for CalendarEventStore {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::Color;
|
||||
use time::Month;
|
||||
|
||||
use super::*;
|
||||
@@ -12,16 +12,18 @@
|
||||
//! - [`Rectangle`]: A basic rectangle
|
||||
//!
|
||||
//! You can also implement your own custom [`Shape`]s.
|
||||
mod circle;
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
use std::{fmt, iter::zip};
|
||||
|
||||
use itertools::Itertools;
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style},
|
||||
symbols::{self, Marker},
|
||||
text::Line as TextLine,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
pub use self::{
|
||||
circle::Circle,
|
||||
@@ -30,7 +32,14 @@ pub use self::{
|
||||
points::Points,
|
||||
rectangle::Rectangle,
|
||||
};
|
||||
use crate::{prelude::*, symbols::Marker, text::Line as TextLine, widgets::Block};
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
mod circle;
|
||||
mod line;
|
||||
mod map;
|
||||
mod points;
|
||||
mod rectangle;
|
||||
mod world;
|
||||
|
||||
/// Something that can be drawn on a [`Canvas`].
|
||||
///
|
||||
@@ -353,10 +362,16 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// and `[0, height - 1]` respectively. The resolution of the grid is used to convert the
|
||||
/// `(x, y)` coordinates to the location of a point on the grid.
|
||||
///
|
||||
/// Points are rounded to the nearest grid cell (with points exactly in the center of a cell
|
||||
/// rounding up).
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{
|
||||
/// symbols,
|
||||
/// widgets::canvas::{Context, Painter},
|
||||
/// };
|
||||
///
|
||||
/// let mut ctx = Context::new(2, 2, [1.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
@@ -365,7 +380,7 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// assert_eq!(point, Some((0, 7)));
|
||||
///
|
||||
/// let point = painter.get_point(1.5, 1.0);
|
||||
/// assert_eq!(point, Some((1, 3)));
|
||||
/// assert_eq!(point, Some((2, 4)));
|
||||
///
|
||||
/// let point = painter.get_point(0.0, 0.0);
|
||||
/// assert_eq!(point, None);
|
||||
@@ -377,20 +392,18 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// assert_eq!(point, Some((0, 0)));
|
||||
/// ```
|
||||
pub fn get_point(&self, x: f64, y: f64) -> Option<(usize, usize)> {
|
||||
let left = self.context.x_bounds[0];
|
||||
let right = self.context.x_bounds[1];
|
||||
let top = self.context.y_bounds[1];
|
||||
let bottom = self.context.y_bounds[0];
|
||||
let [left, right] = self.context.x_bounds;
|
||||
let [bottom, top] = self.context.y_bounds;
|
||||
if x < left || x > right || y < bottom || y > top {
|
||||
return None;
|
||||
}
|
||||
let width = (self.context.x_bounds[1] - self.context.x_bounds[0]).abs();
|
||||
let height = (self.context.y_bounds[1] - self.context.y_bounds[0]).abs();
|
||||
if width == 0.0 || height == 0.0 {
|
||||
let width = right - left;
|
||||
let height = top - bottom;
|
||||
if width <= 0.0 || height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let x = ((x - left) * (self.resolution.0 - 1.0) / width) as usize;
|
||||
let y = ((top - y) * (self.resolution.1 - 1.0) / height) as usize;
|
||||
let x = ((x - left) * (self.resolution.0 - 1.0) / width).round() as usize;
|
||||
let y = ((top - y) * (self.resolution.1 - 1.0) / height).round() as usize;
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
@@ -399,7 +412,11 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{
|
||||
/// style::Color,
|
||||
/// symbols,
|
||||
/// widgets::canvas::{Context, Painter},
|
||||
/// };
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
@@ -408,6 +425,25 @@ impl<'a, 'b> Painter<'a, 'b> {
|
||||
pub fn paint(&mut self, x: usize, y: usize, color: Color) {
|
||||
self.context.grid.paint(x, y, color);
|
||||
}
|
||||
|
||||
/// Canvas context bounds by axis.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::Color,
|
||||
/// symbols,
|
||||
/// widgets::canvas::{Context, Painter},
|
||||
/// };
|
||||
///
|
||||
/// let mut ctx = Context::new(1, 1, [0.0, 2.0], [0.0, 2.0], symbols::Marker::Braille);
|
||||
/// let mut painter = Painter::from(&mut ctx);
|
||||
/// assert_eq!(painter.bounds(), (&[0.0, 2.0], &[0.0, 2.0]));
|
||||
/// ```
|
||||
pub fn bounds(&self) -> (&[f64; 2], &[f64; 2]) {
|
||||
(&self.context.x_bounds, &self.context.y_bounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
@@ -423,9 +459,7 @@ impl<'a, 'b> From<&'a mut Context<'b>> for Painter<'a, 'b> {
|
||||
/// Holds the state of the [`Canvas`] when painting to it.
|
||||
///
|
||||
/// This is used by the [`Canvas`] widget to draw shapes on the grid. It can be useful to think of
|
||||
/// this as similar to the [`Frame`] struct that is used to draw widgets on the terminal.
|
||||
///
|
||||
/// [`Frame`]: crate::prelude::Frame
|
||||
/// this as similar to the `Frame` struct that is used to draw widgets on the terminal.
|
||||
#[derive(Debug)]
|
||||
pub struct Context<'a> {
|
||||
x_bounds: [f64; 2],
|
||||
@@ -449,7 +483,7 @@ impl<'a> Context<'a> {
|
||||
/// example, if you want to draw a map of the world, you might want to use the following bounds:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{symbols, widgets::canvas::Context};
|
||||
///
|
||||
/// let ctx = Context::new(
|
||||
/// 100,
|
||||
@@ -513,6 +547,8 @@ impl<'a> Context<'a> {
|
||||
///
|
||||
/// Note that the text is always printed on top of the canvas and is **not** affected by the
|
||||
/// layers.
|
||||
///
|
||||
/// [`Text`]: ratatui_core::text::Text
|
||||
pub fn print<T>(&mut self, x: f64, y: f64, line: T)
|
||||
where
|
||||
T: Into<TextLine<'a>>,
|
||||
@@ -563,7 +599,10 @@ impl<'a> Context<'a> {
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::Color,
|
||||
/// widgets::{canvas::*, *},
|
||||
/// widgets::{
|
||||
/// canvas::{Canvas, Line, Map, MapResolution, Rectangle},
|
||||
/// Block,
|
||||
/// },
|
||||
/// };
|
||||
///
|
||||
/// Canvas::default()
|
||||
@@ -690,15 +729,15 @@ where
|
||||
/// cell. This allows for more flexibility than the `BrailleGrid` which only supports a single
|
||||
/// foreground color for each 2x4 dots cell.
|
||||
///
|
||||
/// [`Braille`]: crate::symbols::Marker::Braille
|
||||
/// [`HalfBlock`]: crate::symbols::Marker::HalfBlock
|
||||
/// [`Dot`]: crate::symbols::Marker::Dot
|
||||
/// [`Block`]: crate::symbols::Marker::Block
|
||||
/// [`Braille`]: ratatui_core::symbols::Marker::Braille
|
||||
/// [`HalfBlock`]: ratatui_core::symbols::Marker::HalfBlock
|
||||
/// [`Dot`]: ratatui_core::symbols::Marker::Dot
|
||||
/// [`Block`]: ratatui_core::symbols::Marker::Block
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::canvas::*};
|
||||
/// use ratatui::{symbols, widgets::canvas::Canvas};
|
||||
///
|
||||
/// Canvas::default()
|
||||
/// .marker(symbols::Marker::Braille)
|
||||
@@ -728,16 +767,16 @@ where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> WidgetRef for Canvas<'_, F>
|
||||
impl<F> Widget for &Canvas<'_, F>
|
||||
where
|
||||
F: Fn(&mut Context),
|
||||
{
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.render_ref(area, buf);
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.as_ref().render(area, buf);
|
||||
let canvas_area = self.block.inner_if_some(area);
|
||||
if canvas_area.is_empty() {
|
||||
return;
|
||||
@@ -809,9 +848,9 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use indoc::indoc;
|
||||
use ratatui_core::buffer::Cell;
|
||||
|
||||
use super::*;
|
||||
use crate::buffer::Cell;
|
||||
|
||||
// helper to test the canvas checks that drawing a vertical and horizontal line
|
||||
// results in the expected output
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Painter, Shape};
|
||||
|
||||
/// A circle with a given center and radius and with a given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
@@ -31,17 +30,12 @@ impl Shape for Circle {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Color,
|
||||
symbols::Marker,
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle},
|
||||
Widget,
|
||||
},
|
||||
use ratatui_core::{
|
||||
buffer::Buffer, layout::Rect, style::Color, symbols::Marker, widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::canvas::{Canvas, Circle};
|
||||
|
||||
#[test]
|
||||
fn test_it_draws_a_circle() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
@@ -59,10 +53,10 @@ mod tests {
|
||||
.y_bounds([-10.0, 10.0]);
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines([
|
||||
" ⢀⣠⢤⣀ ",
|
||||
" ⢰⠋ ⠈⣇",
|
||||
" ⠘⣆⡀ ⣠⠇",
|
||||
" ⠉⠉⠁ ",
|
||||
" ⣀⣀⣀ ",
|
||||
" ⡞⠁ ⠈⢣",
|
||||
" ⢇⡀ ⢀⡼",
|
||||
" ⠉⠉⠉ ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
use line_clipping::{cohen_sutherland, LineSegment, Point, Window};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Painter, Shape};
|
||||
|
||||
/// A line from `(x1, y1)` to `(x2, y2)` with the given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
@@ -32,13 +32,21 @@ impl Line {
|
||||
}
|
||||
|
||||
impl Shape for Line {
|
||||
#[allow(clippy::similar_names)]
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
let Some((x1, y1)) = painter.get_point(self.x1, self.y1) else {
|
||||
let (x_bounds, y_bounds) = painter.bounds();
|
||||
let Some((world_x1, world_y1, world_x2, world_y2)) =
|
||||
clip_line(x_bounds, y_bounds, self.x1, self.y1, self.x2, self.y2)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some((x2, y2)) = painter.get_point(self.x2, self.y2) else {
|
||||
let Some((x1, y1)) = painter.get_point(world_x1, world_y1) else {
|
||||
return;
|
||||
};
|
||||
let Some((x2, y2)) = painter.get_point(world_x2, world_y2) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (dx, x_range) = if x2 >= x1 {
|
||||
(x2 - x1, x1..=x2)
|
||||
} else {
|
||||
@@ -72,6 +80,27 @@ impl Shape for Line {
|
||||
}
|
||||
}
|
||||
|
||||
fn clip_line(
|
||||
&[xmin, xmax]: &[f64; 2],
|
||||
&[ymin, ymax]: &[f64; 2],
|
||||
x1: f64,
|
||||
y1: f64,
|
||||
x2: f64,
|
||||
y2: f64,
|
||||
) -> Option<(f64, f64, f64, f64)> {
|
||||
if let Some(LineSegment {
|
||||
p1: Point { x: x1, y: y1 },
|
||||
p2: Point { x: x2, y: y2 },
|
||||
}) = cohen_sutherland::clip_line(
|
||||
LineSegment::new(Point::new(x1, y1), Point::new(x2, y2)),
|
||||
Window::new(xmin, xmax, ymin, ymax),
|
||||
) {
|
||||
Some((x1, y1, x2, y2))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_line_low(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: usize, color: Color) {
|
||||
let dx = (x2 - x1) as isize;
|
||||
let dy = (y2 as isize - y1 as isize).abs();
|
||||
@@ -112,21 +141,24 @@ fn draw_line_high(painter: &mut Painter, x1: usize, y1: usize, x2: usize, y2: us
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::{canvas::Canvas, Widget},
|
||||
widgets::Widget,
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[rstest]
|
||||
#[case::off_grid(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [" "; 10])]
|
||||
#[case::horizontal(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
||||
#[case::off_grid1(&Line::new(-1.0, 0.0, -1.0, 10.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid2(&Line::new(0.0, -1.0, 10.0, -1.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid3(&Line::new(-10.0, 5.0, -1.0, 5.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid4(&Line::new(5.0, 11.0, 5.0, 20.0, Color::Red), [" "; 10])]
|
||||
#[case::off_grid5(&Line::new(-10.0, 0.0, 5.0, 0.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
@@ -136,50 +168,99 @@ mod tests {
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"••••••••••",
|
||||
"•••••• ",
|
||||
])]
|
||||
#[case::horizontal(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
|
||||
"••••••••••",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case::vertical(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
||||
#[case::vertical(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||
// dy < dx, x1 < x2
|
||||
#[case::diagonal(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
#[case::off_grid6(&Line::new(-1.0, -1.0, 10.0, 10.0, Color::Red), [
|
||||
" •",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
])]
|
||||
#[case::off_grid7(&Line::new(0.0, 0.0, 11.0, 11.0, Color::Red), [
|
||||
" •",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
])]
|
||||
#[case::off_grid8(&Line::new(-1.0, -1.0, 11.0, 11.0, Color::Red), [
|
||||
" •",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
])]
|
||||
#[case::horizontal1(&Line::new(0.0, 0.0, 10.0, 0.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"••••••••••",
|
||||
])]
|
||||
#[case::horizontal2(&Line::new(10.0, 10.0, 0.0, 10.0, Color::Red), [
|
||||
"••••••••••",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
])]
|
||||
#[case::vertical1(&Line::new(0.0, 0.0, 0.0, 10.0, Color::Red), ["• "; 10])]
|
||||
#[case::vertical2(&Line::new(10.0, 10.0, 10.0, 0.0, Color::Red), [" •"; 10])]
|
||||
// dy < dx, x1 < x2
|
||||
#[case::diagonal1(&Line::new(0.0, 0.0, 10.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ••",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
"•• ",
|
||||
])]
|
||||
// dy < dx, x1 > x2
|
||||
#[case::diagonal(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
|
||||
#[case::diagonal2(&Line::new(10.0, 0.0, 0.0, 5.0, Color::Red), [
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
" ",
|
||||
"• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •",
|
||||
" ",
|
||||
"•• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" •• ",
|
||||
" ••",
|
||||
])]
|
||||
// dy > dx, y1 < y2
|
||||
#[case::diagonal(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
|
||||
#[case::diagonal3(&Line::new(0.0, 0.0, 5.0, 10.0, Color::Red), [
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
@@ -189,11 +270,9 @@ mod tests {
|
||||
" • ",
|
||||
" • ",
|
||||
"• ",
|
||||
"• ",
|
||||
])]
|
||||
// dy > dx, y1 > y2
|
||||
#[case::diagonal(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
|
||||
"• ",
|
||||
#[case::diagonal4(&Line::new(0.0, 10.0, 5.0, 0.0, Color::Red), [
|
||||
"• ",
|
||||
" • ",
|
||||
" • ",
|
||||
@@ -203,11 +282,12 @@ mod tests {
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
" • ",
|
||||
])]
|
||||
fn tests<'expected_line, ExpectedLines>(#[case] line: &Line, #[case] expected: ExpectedLines)
|
||||
where
|
||||
ExpectedLines: IntoIterator,
|
||||
ExpectedLines::Item: Into<crate::text::Line<'expected_line>>,
|
||||
ExpectedLines::Item: Into<ratatui_core::text::Line<'expected_line>>,
|
||||
{
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
let canvas = Canvas::default()
|
||||
206
ratatui-widgets/src/canvas/map.rs
Normal file
206
ratatui-widgets/src/canvas/map.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
use ratatui_core::style::Color;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::canvas::{
|
||||
world::{WORLD_HIGH_RESOLUTION, WORLD_LOW_RESOLUTION},
|
||||
Painter, Shape,
|
||||
};
|
||||
|
||||
/// Defines how many points are going to be used to draw a [`Map`].
|
||||
///
|
||||
/// You generally want a [high](MapResolution::High) resolution map.
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum MapResolution {
|
||||
/// A lesser resolution for the [`Map`] [`Shape`].
|
||||
///
|
||||
/// Contains about 1000 points.
|
||||
#[default]
|
||||
Low,
|
||||
/// A higher resolution for the [`Map`] [`Shape`].
|
||||
///
|
||||
/// Contains about 5000 points, you likely want to use [`Marker::Braille`] with this.
|
||||
///
|
||||
/// [`Marker::Braille`]: (ratatui_core::symbols::Marker::Braille)
|
||||
High,
|
||||
}
|
||||
|
||||
impl MapResolution {
|
||||
const fn data(self) -> &'static [(f64, f64)] {
|
||||
match self {
|
||||
Self::Low => &WORLD_LOW_RESOLUTION,
|
||||
Self::High => &WORLD_HIGH_RESOLUTION,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A world map
|
||||
///
|
||||
/// A world map can be rendered with different [resolutions](MapResolution) and [colors](Color).
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Map {
|
||||
/// The resolution of the map.
|
||||
///
|
||||
/// This is the number of points used to draw the map.
|
||||
pub resolution: MapResolution,
|
||||
/// Map color
|
||||
///
|
||||
/// This is the color of the points of the map.
|
||||
pub color: Color,
|
||||
}
|
||||
|
||||
impl Shape for Map {
|
||||
fn draw(&self, painter: &mut Painter) {
|
||||
for (x, y) in self.resolution.data() {
|
||||
if let Some((x, y)) = painter.get_point(*x, *y) {
|
||||
painter.paint(x, y, self.color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{buffer::Buffer, layout::Rect, symbols::Marker, widgets::Widget};
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[test]
|
||||
fn map_resolution_to_string() {
|
||||
assert_eq!(MapResolution::Low.to_string(), "Low");
|
||||
assert_eq!(MapResolution::High.to_string(), "High");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_resolution_from_str() {
|
||||
assert_eq!("Low".parse(), Ok(MapResolution::Low));
|
||||
assert_eq!("High".parse(), Ok(MapResolution::High));
|
||||
assert_eq!(
|
||||
"".parse::<MapResolution>(),
|
||||
Err(ParseError::VariantNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default() {
|
||||
let map = Map::default();
|
||||
assert_eq!(map.resolution, MapResolution::Low);
|
||||
assert_eq!(map.color, Color::Reset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_low() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
|
||||
let canvas = Canvas::default()
|
||||
.marker(Marker::Dot)
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.paint(|context| {
|
||||
context.draw(&Map::default());
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" • ",
|
||||
" • •• •••••••• •• •••• ••••• ••• •• ••• ",
|
||||
" ••••••••••••••• • •••• • • ••••••• ••• ",
|
||||
" • •••• ••••••••••••••• •• •• • ••• •• •••• •• ••••••• ••• ",
|
||||
"••••• •••••••••••• •••• • •••••• •••• • ••• ••••• •",
|
||||
" •• • • •••• •••••••• •••• •• • •• • ••• •• •••",
|
||||
" •••• ••• •••••• ••••• • •• •••••• • ••••• ",
|
||||
"••••• ••• • •• •• ••••••• •• •• •• ",
|
||||
" •• •••• ••••• •• • • • •• ",
|
||||
" • • ••••••• •• •••• ••• •• • •• • •• ",
|
||||
" • •• ••••••••• • •• •••• • ",
|
||||
" •• •• • • • •• • ••••• ",
|
||||
" •• ••• • •••• • • • ",
|
||||
" • • •• • •• •• • • ",
|
||||
" •• • ••••••• • • • • • •• • ",
|
||||
" ••••••••• • •• • • • •• • •• ",
|
||||
" •• •• • ••• • ••• •• ",
|
||||
" ••• • • • • • •• ••• ••• ",
|
||||
" • • •• • •• ",
|
||||
" • • ••• • • ••• ••• ",
|
||||
" • • • • • ••• ",
|
||||
" • • • • • • • ",
|
||||
" • • • • ••• •• •",
|
||||
" • • • • •• • • • ",
|
||||
" • • • • • ",
|
||||
" • • • • • ",
|
||||
" •• •• •• •• • • ",
|
||||
" • • ••• •• ",
|
||||
" • • •• •• ",
|
||||
" • • ",
|
||||
" ••••• ",
|
||||
" ",
|
||||
" •• ",
|
||||
" ••• • • ••••• • •••• • • •• •• •• ",
|
||||
" • • • •••••• ••••••••• • •• •• ••• ",
|
||||
"• ••• •••• •••• • • • ••• • • ••• •",
|
||||
" •• • • •• • •• •• ",
|
||||
"• • • ",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draw_high() {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 80, 40));
|
||||
let canvas = Canvas::default()
|
||||
.marker(Marker::Braille)
|
||||
.x_bounds([-180.0, 180.0])
|
||||
.y_bounds([-90.0, 90.0])
|
||||
.paint(|context| {
|
||||
context.draw(&Map {
|
||||
resolution: MapResolution::High,
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let expected = Buffer::with_lines([
|
||||
" ",
|
||||
" ⢀⣀⣤⠄⠤⠤⣤⣀⡀⣀⣀⡄⠄⢄⣀⣄⡄⢀⡀ ",
|
||||
" ⢀⣀⣤⠰⢤⣼⡯⢽⡟⣀⢶⣺⡛⠁ ⠈⢰⠃⠁ ⢖⣒⣾⡟⠂ ⠈⠛⠁ ⠺⢩⢖⡄ ",
|
||||
" ⡬⢍⣿⣟⣿⣻⣿⣿⣿⡾⣯⡀⠈⠁⠁⢦ ⢀⡿ ⠈ ⢠⢶⠘⠋⡁⣀⢠⠤⠖⠘⠉⠁⠈⠼⡧⡄⣄⡀ ⢫⣗⠒⠆ ",
|
||||
"⣓ ⣠⠖⠓⠒⠢⠤⢄⠤⠶⠽⠽⣶⣃⣽⡮⣿⡷⣗⣤⡭⣍⢓⡄ ⠸⣷ ⢀⣀⠿⠇ ⢀⠔⠒⠲⠄⢄⢀⡀⢙⣑⡄⠴⡍⣟⠉ ⠑⠉⠉ ⠑⠐⠦⠤⣤⠤⢞",
|
||||
"⠶⢧⣗⢾⡆ ⠈⠈⠁⠈⠉⢀⣹⣶⣩⣽⣐⢮⠃ ⣇ ⢀⡔⠊ ⢰⣖⣲ ⢀⡐⠁⣰⠦ ⢲⣶⠛⠋ ⠐⠋ ⡤",
|
||||
" ⠉⣮⣀⣀⣴⡤⣠⡀ ⡎ ⠛⢫⠙⢫⢫ ⠈⠦⠼ ⡃⡀⢸⠼⣤⡄ ⡀⣀⣀⡐⡶⣣⢤⠖⠉",
|
||||
" ⢀⡽⠟⠃ ⠈⠱⡀ ⠙⠢⣀⣨⠆⠈⠁⢧⡀ ⣸⣷ ⢹⣷⣼⣸⠃ ⢀⡐⢀ ⠁⡚⣨⠆ ",
|
||||
" ⠘⢳⡀ ⠈⠾ ⣀⣀⣽ ⠸⢼⣇⡧⠋⠉⠁ ⠉⣿ ⠢⠂ ",
|
||||
" ⠈⢻ ⠜⢹⣵⠻⠇ ⠈⢻ ⢀⡀ ⢠⣠⡤ ⢀⢤ ⢰⣯ ",
|
||||
" ⢼ ⢀⣾⠛⠉ ⠐⡖⠒⡰⢺⣞⣵⡄⢀⣏⡭⣙⡄⢕⢫⡀ ⢀ ⢠⠖⢱⡿⠃ ",
|
||||
" ⠸ ⠠⡎ ⠰⣅⣰⣃⣘⡣⡿⢻⡿⣁⣀ ⠸⣽ ⠐⣿⣽⣫ ⡸⡇ ",
|
||||
" ⠳⣄ ⡰⠃ ⢀⠎⠉ ⢧⡀⣠⣛⠈⢻ ⢻⠘⢺⡿⠚⠁ ",
|
||||
" ⢻⣇ ⣠⠲⠖⢲⡇ ⡸ ⠉⠃⠈⠉⣿ ⢰⣆ ⢸ ⠈⠁ ",
|
||||
" ⠈⢿⣆ ⡟ ⣘⣻ ⡸ ⢸⢇ ⠈⠯⢿⡒⠲⡀ ⢀⡀ ⣀⢾ ",
|
||||
" ⠈⢳ ⠸⡀⢳⣠⢾⠉⢹⣦⣤⣀ ⡇ ⡿⡄ ⢰⠃ ⠑⡂ ⢠⠏⢣ ⣼⡮⠁⢈⡀ ",
|
||||
" ⠙⠲⢆⡿⢦⠈⠉⠁⠁ ⡇ ⠱⣇⣀⠼⠃ ⡃⢰⠃ ⠸⢶ ⠘⠄ ⢾⡁ ",
|
||||
" ⠙⣾⣀⡴⡶⢤⣤ ⢳ ⠻⠵⡆ ⠸⣸ ⢸⡳⡤⠃⢀⡾⣿ ",
|
||||
" ⠘⢻⠁ ⠈⠦⣄ ⢧⣀⣀⠤⣀ ⢐⠁ ⠈⠩⠆ ⣘⣧⠁ ⡸⡔⢿ ",
|
||||
" ⡸ ⢨ ⠁ ⠉⡇ ⢀⠎ ⢻⢿⠄⡴⢑⣧⡠⡄ ",
|
||||
" ⡇ ⠈⠋⠦⡄ ⠈⡆ ⢠⠃ ⢏⡇⢧⣼⣾⣧⣽⣿⠶⢤⡀⣤ ",
|
||||
" ⣇ ⠈⡇ ⢸ ⢸ ⠈⠶⣦⣄⣋⣁⡀⠸⣵⢠⣻⠋⠷⣄ ",
|
||||
" ⠰⡀ ⣰⠁ ⢘⠆ ⢸ ⢠⡀ ⠙⠋⢠⠦⡄⣷⠙⠃ ⠙ ",
|
||||
"⠄ ⠣⡀ ⡃ ⢸ ⣸⢡⢾⠆ ⡞⠛⠘⢧⡏⡆ ⠸⠄ ⡤",
|
||||
" ⠱ ⢠⠃ ⠸⡀ ⢸⠁⢸⢨ ⡤⠚ ⠱⡀ ⢦ ⠁",
|
||||
" ⠅ ⡖⠉ ⡇ ⡜ ⠸⠔ ⡇ ⢳ ",
|
||||
" ⡇ ⢀⠃ ⢱⡀ ⢰⠃ ⣇ ⢀⡀ ⢸ ",
|
||||
" ⢀⠃ ⡦⠏ ⠈⠷⠖⠃ ⠾⠴⠊⠁⠹⣦ ⡞ ⣄ ",
|
||||
" ⢸ ⡤⠃ ⠘⢲⠖⠃ ⣽⡆",
|
||||
" ⢸ ⣸⠁ ⠈⠿ ⢀⢼⠏ ",
|
||||
" ⠞ ⡗ ⣄ ⠈⠋ ",
|
||||
" ⢧⡼⡁⠲⠂ ",
|
||||
" ⠙⠉ ",
|
||||
" ⡀ ",
|
||||
" ⣴⠏⠁ ⣀⡤⢤⣀⣀ ⢀⣀⣤⣀⣀⡴⣄⡤⢤⣀⠤⠤⠴⣄⣀⡀ ",
|
||||
" ⣀⣀ ⣠⣿⡍⣆ ⣠⣤⣤⠤⠴⠶⠖⠲⠤⠔⠛⠒⠉ ⠈⠨⣇⠖⠋ ⠈⠉⠓⠢⠤⢄ ",
|
||||
" ⡀ ⣠⠤⠴⠒⠚⠛⠛⠒⠢⠤⠿⠙⠉⠉⠑⢋⣚⣉⠥⠚ ⢀⣀⡠⠟⠁ ⡴⠋ ",
|
||||
" ⠐⠶⣛⣫⡤ ⠐⢏⣀⣤⣤ ⣴⣋⢇⢀⣮⡥ ⣴⠓ ",
|
||||
"⠤⠤⠤⠤⡀⣈⢣⣠⡄ ⠉⠊⠉⠉⠉ ⠈⠓⠆⠤⠤",
|
||||
" ",
|
||||
]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Painter, Shape},
|
||||
};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
use crate::canvas::{Painter, Shape};
|
||||
|
||||
/// A group of points with a given color
|
||||
#[derive(Debug, Default, Clone, PartialEq)]
|
||||
@@ -1,9 +1,8 @@
|
||||
use crate::{
|
||||
style::Color,
|
||||
widgets::canvas::{Line, Painter, Shape},
|
||||
};
|
||||
use ratatui_core::style::Color;
|
||||
|
||||
/// A rectangle to draw on a [`Canvas`](super::Canvas)
|
||||
use crate::canvas::{Line, Painter, Shape};
|
||||
|
||||
/// A rectangle to draw on a [`Canvas`](crate::canvas::Canvas)
|
||||
///
|
||||
/// Sizes used here are **not** in terminal cell. This is much more similar to the
|
||||
/// mathematic coordinate system.
|
||||
@@ -65,8 +64,16 @@ impl Shape for Rectangle {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Margin, Rect},
|
||||
style::{Style, Stylize},
|
||||
symbols::Marker,
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{prelude::*, symbols::Marker, widgets::canvas::Canvas};
|
||||
use crate::canvas::Canvas;
|
||||
|
||||
#[test]
|
||||
fn draw_block_lines() {
|
||||
@@ -142,42 +149,43 @@ mod tests {
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
|
||||
let canvas = Canvas::default()
|
||||
.marker(Marker::Braille)
|
||||
.x_bounds([0.0, 10.0])
|
||||
.y_bounds([0.0, 10.0])
|
||||
.x_bounds([0.0, 20.0])
|
||||
.y_bounds([0.0, 20.0])
|
||||
.paint(|context| {
|
||||
// a rectangle that will draw the outside part of the braille
|
||||
context.draw(&Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 10.0,
|
||||
height: 10.0,
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
color: Color::Red,
|
||||
});
|
||||
// a rectangle that will draw the inside part of the braille
|
||||
context.draw(&Rectangle {
|
||||
x: 2.0,
|
||||
y: 1.75,
|
||||
width: 6.5,
|
||||
height: 6.5,
|
||||
x: 4.0,
|
||||
y: 4.0,
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
color: Color::Green,
|
||||
});
|
||||
});
|
||||
canvas.render(buffer.area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines([
|
||||
"⡏⠉⠉⠉⠉⠉⠉⠉⠉⢹",
|
||||
"⡇⢠⠤⠤⠤⠤⠤⠤⡄⢸",
|
||||
"⡇⢸ ⡇⢸",
|
||||
"⡇⢸ ⡇⢸",
|
||||
"⡇⢸ ⡇⢸",
|
||||
"⡇⢸ ⡇⢸",
|
||||
"⡇⢸ ⡇⢸",
|
||||
"⡇⢸ ⡇⢸",
|
||||
"⡇⠈⠉⠉⠉⠉⠉⠉⠁⢸",
|
||||
"⡇ ⢸",
|
||||
"⡇ ⡏⠉⠉⠉⠉⢹ ⢸",
|
||||
"⡇ ⡇ ⢸ ⢸",
|
||||
"⡇ ⡇ ⢸ ⢸",
|
||||
"⡇ ⡇ ⢸ ⢸",
|
||||
"⡇ ⡇ ⢸ ⢸",
|
||||
"⡇ ⣇⣀⣀⣀⣀⣸ ⢸",
|
||||
"⡇ ⢸",
|
||||
"⣇⣀⣀⣀⣀⣀⣀⣀⣀⣸",
|
||||
]);
|
||||
expected.set_style(buffer.area, Style::new().red());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::new().green());
|
||||
expected.set_style(buffer.area.inner(Margin::new(2, 2)), Style::reset());
|
||||
expected.set_style(buffer.area.inner(Margin::new(1, 1)), Style::reset());
|
||||
expected.set_style(buffer.area.inner(Margin::new(2, 2)), Style::new().green());
|
||||
expected.set_style(buffer.area.inner(Margin::new(3, 3)), Style::reset());
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,19 @@
|
||||
//! The [`Chart`] widget is used to plot one or more [`Dataset`] in a cartesian coordinate system.
|
||||
use std::{cmp::max, ops::Not};
|
||||
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Flex, Layout, Position, Rect},
|
||||
style::{Color, Style, Styled},
|
||||
symbols::{self},
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
layout::Flex,
|
||||
prelude::*,
|
||||
style::Styled,
|
||||
widgets::{
|
||||
canvas::{Canvas, Line as CanvasLine, Points},
|
||||
Block,
|
||||
},
|
||||
block::{Block, BlockExt},
|
||||
canvas::{Canvas, Line as CanvasLine, Points},
|
||||
};
|
||||
|
||||
/// An X or Y axis for the [`Chart`] widget
|
||||
@@ -25,7 +29,11 @@ use crate::{
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::Axis,
|
||||
/// };
|
||||
///
|
||||
/// let axis = Axis::default()
|
||||
/// .title("X Axis")
|
||||
/// .style(Style::default().gray())
|
||||
@@ -94,7 +102,8 @@ impl<'a> Axis<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{style::Stylize, widgets::Axis};
|
||||
///
|
||||
/// let axis = Axis::default()
|
||||
/// .bounds([0.0, 50.0])
|
||||
/// .labels(["0".bold(), "25".into(), "50".bold()]);
|
||||
@@ -118,11 +127,12 @@ impl<'a> Axis<'a> {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// [`Axis`] also implements [`Stylize`](crate::style::Stylize) which mean you can style it
|
||||
/// like so
|
||||
/// [`Axis`] also implements [`Stylize`](ratatui_core::style::Stylize) which mean you can style
|
||||
/// it like so
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{style::Stylize, widgets::Axis};
|
||||
///
|
||||
/// let axis = Axis::default().red();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -297,7 +307,11 @@ impl LegendPosition {
|
||||
/// This example draws a red line between two points.
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, symbols::Marker, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::Stylize,
|
||||
/// symbols::Marker,
|
||||
/// widgets::{Dataset, GraphType},
|
||||
/// };
|
||||
///
|
||||
/// let dataset = Dataset::default()
|
||||
/// .name("dataset 1")
|
||||
@@ -397,11 +411,12 @@ impl<'a> Dataset<'a> {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// [`Dataset`] also implements [`Stylize`](crate::style::Stylize) which mean you can style it
|
||||
/// like so
|
||||
/// [`Dataset`] also implements [`Stylize`](ratatui_core::style::Stylize) which mean you can
|
||||
/// style it like so
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{style::Stylize, widgets::Dataset};
|
||||
///
|
||||
/// let dataset = Dataset::default().red();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -452,7 +467,11 @@ struct ChartLayout {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// symbols,
|
||||
/// widgets::{Axis, Block, Chart, Dataset, GraphType},
|
||||
/// };
|
||||
///
|
||||
/// // Create the datasets to fill the chart with
|
||||
/// let datasets = vec![
|
||||
@@ -521,17 +540,19 @@ impl<'a> Chart<'a> {
|
||||
/// This creates a simple chart with one [`Dataset`]
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let data_points = vec![];
|
||||
/// use ratatui::widgets::{Chart, Dataset};
|
||||
///
|
||||
/// let data_points = vec![];
|
||||
/// let chart = Chart::new(vec![Dataset::default().data(&data_points)]);
|
||||
/// ```
|
||||
///
|
||||
/// This creates a chart with multiple [`Dataset`]s
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let data_points = vec![];
|
||||
/// # let data_points2 = vec![];
|
||||
/// use ratatui::widgets::{Chart, Dataset};
|
||||
///
|
||||
/// let data_points = vec![];
|
||||
/// let data_points2 = vec![];
|
||||
/// let chart = Chart::new(vec![
|
||||
/// Dataset::default().data(&data_points),
|
||||
/// Dataset::default().data(&data_points2),
|
||||
@@ -581,7 +602,8 @@ impl<'a> Chart<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Axis, Chart};
|
||||
///
|
||||
/// let chart = Chart::new(vec![]).x_axis(
|
||||
/// Axis::default()
|
||||
/// .title("X Axis")
|
||||
@@ -604,7 +626,8 @@ impl<'a> Chart<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Axis, Chart};
|
||||
///
|
||||
/// let chart = Chart::new(vec![]).y_axis(
|
||||
/// Axis::default()
|
||||
/// .title("Y Axis")
|
||||
@@ -635,7 +658,8 @@ impl<'a> Chart<'a> {
|
||||
/// its height is greater than 25% of the total widget height.
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{layout::Constraint, widgets::Chart};
|
||||
///
|
||||
/// let constraints = (Constraint::Ratio(1, 3), Constraint::Ratio(1, 4));
|
||||
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
|
||||
/// ```
|
||||
@@ -644,7 +668,8 @@ impl<'a> Chart<'a> {
|
||||
/// first one is always true.
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{layout::Constraint, widgets::Chart};
|
||||
///
|
||||
/// let constraints = (Constraint::Min(0), Constraint::Ratio(1, 4));
|
||||
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
|
||||
/// ```
|
||||
@@ -653,7 +678,8 @@ impl<'a> Chart<'a> {
|
||||
/// [`Chart::legend_position`].
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{layout::Constraint, widgets::Chart};
|
||||
///
|
||||
/// let constraints = (Constraint::Length(0), Constraint::Ratio(1, 4));
|
||||
/// let chart = Chart::new(vec![]).hidden_legend_constraints(constraints);
|
||||
/// ```
|
||||
@@ -685,14 +711,16 @@ impl<'a> Chart<'a> {
|
||||
/// Show the legend on the top left corner.
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Chart, LegendPosition};
|
||||
/// use ratatui::widgets::{Chart, LegendPosition};
|
||||
///
|
||||
/// let chart: Chart = Chart::new(vec![]).legend_position(Some(LegendPosition::TopLeft));
|
||||
/// ```
|
||||
///
|
||||
/// Hide the legend altogether
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::widgets::{Chart, LegendPosition};
|
||||
/// use ratatui::widgets::{Chart, LegendPosition};
|
||||
///
|
||||
/// let chart = Chart::new(vec![]).legend_position(None);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -943,16 +971,16 @@ impl<'a> Chart<'a> {
|
||||
|
||||
impl Widget for Chart<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Chart<'_> {
|
||||
impl Widget for &Chart<'_> {
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
|
||||
self.block.render_ref(area, buf);
|
||||
self.block.as_ref().render(area, buf);
|
||||
let chart_area = self.block.inner_if_some(area);
|
||||
let Some(layout) = self.layout(chart_area) else {
|
||||
return;
|
||||
@@ -1130,6 +1158,7 @@ impl<'a> Styled for Chart<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::style::{Modifier, Stylize};
|
||||
use rstest::rstest;
|
||||
use strum::ParseError;
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
use crate::prelude::*;
|
||||
//! The [`Clear`] widget allows you to clear a certain area to allow overdrawing (e.g. for popups).
|
||||
use ratatui_core::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
|
||||
///
|
||||
/// This widget **cannot be used to clear the terminal on the first render** as `ratatui` assumes
|
||||
/// the render area is empty. Use [`crate::Terminal::clear`] instead.
|
||||
/// the render area is empty. Use `Terminal::clear` instead.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// layout::Rect,
|
||||
/// widgets::{Block, Clear},
|
||||
/// Frame,
|
||||
/// };
|
||||
///
|
||||
/// fn draw_on_clear(f: &mut Frame, area: Rect) {
|
||||
/// let block = Block::bordered().title("Block");
|
||||
@@ -26,12 +31,12 @@ pub struct Clear;
|
||||
|
||||
impl Widget for Clear {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Clear {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Clear {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
for x in area.left()..area.right() {
|
||||
for y in area.top()..area.bottom() {
|
||||
buf[(x, y)].reset();
|
||||
@@ -42,6 +47,8 @@ impl WidgetRef for Clear {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@@ -1,4 +1,14 @@
|
||||
use crate::{prelude::*, style::Styled, widgets::Block};
|
||||
//! The [`Gauge`] widget is used to display a horizontal progress bar.
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Color, Style, Styled},
|
||||
symbols::{self},
|
||||
text::{Line, Span},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
/// A widget to display a progress bar.
|
||||
///
|
||||
@@ -16,16 +26,14 @@ use crate::{prelude::*, style::Styled, widgets::Block};
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::{Block, Gauge},
|
||||
/// };
|
||||
///
|
||||
/// Gauge::default()
|
||||
/// .block(Block::bordered().title("Progress"))
|
||||
/// .gauge_style(
|
||||
/// Style::default()
|
||||
/// .fg(Color::White)
|
||||
/// .bg(Color::Black)
|
||||
/// .add_modifier(Modifier::ITALIC),
|
||||
/// )
|
||||
/// .gauge_style(Style::new().white().on_black().italic())
|
||||
/// .percent(20);
|
||||
/// ```
|
||||
///
|
||||
@@ -145,14 +153,14 @@ impl<'a> Gauge<'a> {
|
||||
|
||||
impl Widget for Gauge<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Gauge<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Gauge<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
self.block.render_ref(area, buf);
|
||||
self.block.as_ref().render(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
self.render_gauge(inner, buf);
|
||||
}
|
||||
@@ -242,16 +250,15 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// symbols,
|
||||
/// widgets::{Block, LineGauge},
|
||||
/// };
|
||||
///
|
||||
/// LineGauge::default()
|
||||
/// .block(Block::bordered().title("Progress"))
|
||||
/// .filled_style(
|
||||
/// Style::default()
|
||||
/// .fg(Color::White)
|
||||
/// .bg(Color::Black)
|
||||
/// .add_modifier(Modifier::BOLD),
|
||||
/// )
|
||||
/// .filled_style(Style::new().white().on_black().bold())
|
||||
/// .line_set(symbols::line::THICK)
|
||||
/// .ratio(0.4);
|
||||
/// ```
|
||||
@@ -379,14 +386,14 @@ impl<'a> LineGauge<'a> {
|
||||
|
||||
impl Widget for LineGauge<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for LineGauge<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &LineGauge<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
self.block.render_ref(area, buf);
|
||||
self.block.as_ref().render(area, buf);
|
||||
let gauge_area = self.block.inner_if_some(area);
|
||||
if gauge_area.is_empty() {
|
||||
return;
|
||||
@@ -442,6 +449,11 @@ impl<'a> Styled for LineGauge<'a> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
98
ratatui-widgets/src/lib.rs
Normal file
98
ratatui-widgets/src/lib.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
// show the feature flags in the generated documentation
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![doc(
|
||||
html_logo_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/logo.png",
|
||||
html_favicon_url = "https://raw.githubusercontent.com/ratatui/ratatui/main/assets/favicon.ico"
|
||||
)]
|
||||
#![warn(missing_docs)]
|
||||
//! **ratatui-widgets** contains all the widgets that were previously part of the [Ratatui] crate.
|
||||
//! It is meant to be used in conjunction with `ratatui`, which provides the core functionality
|
||||
//! for building terminal user interfaces.
|
||||
//!
|
||||
//! [Ratatui]: https://crates.io/crates/ratatui
|
||||
//!
|
||||
//! Most applications shouldn't need to depend directly on `ratatui-widgets`, `ratatui` crate
|
||||
//! re-exports all the widgets from this crate. However, if you are building a widget library that
|
||||
//! internally uses these widgets, or if you prefer finer grained dependencies, you may want to
|
||||
//! depend on this crate rather than transitively through the `ratatui` crate.
|
||||
//!
|
||||
//! Previously, a crate named `ratatui-widgets` was published with some formative ideas about an
|
||||
//! eventual Ratatui framework. That crate is now move to [tui-framework-experiment], pending a new
|
||||
//! name.
|
||||
//!
|
||||
//! [tui-framework-experiment]: https://crates.io/crates/tui-framework-experiment
|
||||
//!
|
||||
//! # Installation
|
||||
//!
|
||||
//! Run the following command to add this crate to your project:
|
||||
//!
|
||||
//! ```sh
|
||||
//! cargo add ratatui-widgets
|
||||
//! ```
|
||||
//!
|
||||
//! # Available Widgets
|
||||
//!
|
||||
//! - [`BarChart`]: displays multiple datasets as bars with optional grouping.
|
||||
//! - [`Block`]: a basic widget that draws a block with optional borders, titles, and styles.
|
||||
//! - [`calendar::Monthly`]: displays a single month.
|
||||
//! - [`Canvas`]: draws arbitrary shapes using drawing characters.
|
||||
//! - [`Chart`]: displays multiple datasets as lines or scatter graphs.
|
||||
//! - [`Clear`]: clears the area it occupies. Useful to render over previously drawn widgets.
|
||||
//! - [`Gauge`]: displays progress percentage using block characters.
|
||||
//! - [`LineGauge`]: displays progress as a line.
|
||||
//! - [`List`]: displays a list of items and allows selection.
|
||||
//! - [`RatatuiLogo`]: displays the Ratatui logo.
|
||||
//! - [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
|
||||
//! - [`Scrollbar`]: displays a scrollbar.
|
||||
//! - [`Sparkline`]: displays a single dataset as a sparkline.
|
||||
//! - [`Table`]: displays multiple rows and columns in a grid and allows selection.
|
||||
//! - [`Tabs`]: displays a tab bar and allows selection.
|
||||
//!
|
||||
//! [`BarChart`]: crate::barchart::BarChart
|
||||
//! [`Block`]: crate::block::Block
|
||||
//! [`calendar::Monthly`]: crate::calendar::Monthly
|
||||
//! [`Canvas`]: crate::canvas::Canvas
|
||||
//! [`Chart`]: crate::chart::Chart
|
||||
//! [`Clear`]: crate::clear::Clear
|
||||
//! [`Gauge`]: crate::gauge::Gauge
|
||||
//! [`LineGauge`]: crate::gauge::LineGauge
|
||||
//! [`List`]: crate::list::List
|
||||
//! [`RatatuiLogo`]: crate::logo::RatatuiLogo
|
||||
//! [`Paragraph`]: crate::paragraph::Paragraph
|
||||
//! [`Scrollbar`]: crate::scrollbar::Scrollbar
|
||||
//! [`Sparkline`]: crate::sparkline::Sparkline
|
||||
//! [`Table`]: crate::table::Table
|
||||
//! [`Tabs`]: crate::tabs::Tabs
|
||||
//!
|
||||
//! All these widgets are re-exported directly under `ratatui::widgets` in the `ratatui` crate.
|
||||
#![cfg_attr(feature = "document-features", doc = "\n## Features")]
|
||||
#![cfg_attr(feature = "document-features", doc = document_features::document_features!())]
|
||||
//!
|
||||
//! # Contributing
|
||||
//!
|
||||
//! Contributions are welcome! Please open an issue or submit a pull request on GitHub. For more
|
||||
//! details on contributing, please see the [CONTRIBUTING](CONTRIBUTING.md) document.
|
||||
//!
|
||||
//! # License
|
||||
//!
|
||||
//! This project is licensed under the MIT License. See the [LICENSE](../LICENSE) file for details.
|
||||
pub mod barchart;
|
||||
pub mod block;
|
||||
pub mod borders;
|
||||
pub mod canvas;
|
||||
pub mod chart;
|
||||
pub mod clear;
|
||||
pub mod gauge;
|
||||
pub mod list;
|
||||
pub mod logo;
|
||||
pub mod paragraph;
|
||||
pub mod scrollbar;
|
||||
pub mod sparkline;
|
||||
pub mod table;
|
||||
pub mod tabs;
|
||||
|
||||
mod reflow;
|
||||
|
||||
#[cfg(feature = "calendar")]
|
||||
pub mod calendar;
|
||||
@@ -1,11 +1,14 @@
|
||||
//! The [`List`] widget is used to display a list of items and allows selecting one or multiple
|
||||
//! items.
|
||||
use ratatui_core::style::{Style, Styled};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use super::ListItem;
|
||||
use crate::{
|
||||
prelude::*,
|
||||
style::Styled,
|
||||
widgets::{Block, HighlightSpacing},
|
||||
};
|
||||
pub use self::{item::ListItem, state::ListState};
|
||||
use crate::{block::Block, table::HighlightSpacing};
|
||||
|
||||
mod item;
|
||||
mod rendering;
|
||||
mod state;
|
||||
|
||||
/// A widget to display several items among which one can be selected (optional)
|
||||
///
|
||||
@@ -15,13 +18,10 @@ use crate::{
|
||||
/// the item's height is automatically determined. A `List` can also be put in reverse order (i.e.
|
||||
/// *bottom to top*) whereas a [`Table`] cannot.
|
||||
///
|
||||
/// [`Table`]: crate::widgets::Table
|
||||
/// [`Table`]: crate::table::Table
|
||||
///
|
||||
/// List items can be aligned using [`Text::alignment`], for more details see [`ListItem`].
|
||||
///
|
||||
/// [`List`] implements [`Widget`] and so it can be drawn using
|
||||
/// [`Frame::render_widget`](crate::terminal::Frame::render_widget).
|
||||
///
|
||||
/// [`List`] is also a [`StatefulWidget`], which means you can use it with [`ListState`] to allow
|
||||
/// the user to [scroll] through items and [select] one of them.
|
||||
///
|
||||
@@ -41,14 +41,20 @@ use crate::{
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// layout::Rect,
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::{Block, List, ListDirection, ListItem},
|
||||
/// Frame,
|
||||
/// };
|
||||
///
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// let items = ["Item 1", "Item 2", "Item 3"];
|
||||
/// let list = List::new(items)
|
||||
/// .block(Block::bordered().title("List"))
|
||||
/// .style(Style::default().fg(Color::White))
|
||||
/// .highlight_style(Style::default().add_modifier(Modifier::ITALIC))
|
||||
/// .style(Style::new().white())
|
||||
/// .highlight_style(Style::new().italic())
|
||||
/// .highlight_symbol(">>")
|
||||
/// .repeat_highlight_symbol(true)
|
||||
/// .direction(ListDirection::BottomToTop);
|
||||
@@ -60,7 +66,13 @@ use crate::{
|
||||
/// # Stateful example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// layout::Rect,
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::{Block, List, ListState},
|
||||
/// Frame,
|
||||
/// };
|
||||
///
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// // This should be stored outside of the function in your application state.
|
||||
@@ -68,7 +80,7 @@ use crate::{
|
||||
/// let items = ["Item 1", "Item 2", "Item 3"];
|
||||
/// let list = List::new(items)
|
||||
/// .block(Block::bordered().title("List"))
|
||||
/// .highlight_style(Style::new().add_modifier(Modifier::REVERSED))
|
||||
/// .highlight_style(Style::new().reversed())
|
||||
/// .highlight_symbol(">>")
|
||||
/// .repeat_highlight_symbol(true);
|
||||
///
|
||||
@@ -85,9 +97,12 @@ use crate::{
|
||||
/// (0..5).map(|i| format!("Item{i}")).collect::<List>();
|
||||
/// ```
|
||||
///
|
||||
/// [`ListState`]: crate::widgets::list::ListState
|
||||
/// [scroll]: crate::widgets::list::ListState::offset
|
||||
/// [select]: crate::widgets::list::ListState::select
|
||||
/// [`ListState`]: crate::list::ListState
|
||||
/// [scroll]: crate::list::ListState::offset
|
||||
/// [select]: crate::list::ListState::select
|
||||
/// [`Text::alignment`]: ratatui_core::text::Text::alignment
|
||||
/// [`StatefulWidget`]: ratatui_core::widgets::StatefulWidget
|
||||
/// [`Widget`]: ratatui_core::widgets::Widget
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, Default)]
|
||||
pub struct List<'a> {
|
||||
/// An optional block to wrap the widget in
|
||||
@@ -135,17 +150,23 @@ impl<'a> List<'a> {
|
||||
/// From a slice of [`&str`]
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::List;
|
||||
///
|
||||
/// let list = List::new(["Item 1", "Item 2"]);
|
||||
/// ```
|
||||
///
|
||||
/// From [`Text`]
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::Text,
|
||||
/// widgets::List,
|
||||
/// };
|
||||
///
|
||||
/// let list = List::new([
|
||||
/// Text::styled("Item 1", Style::default().red()),
|
||||
/// Text::styled("Item 2", Style::default().red()),
|
||||
/// Text::styled("Item 1", Style::new().red()),
|
||||
/// Text::styled("Item 2", Style::new().red()),
|
||||
/// ]);
|
||||
/// ```
|
||||
///
|
||||
@@ -153,10 +174,13 @@ impl<'a> List<'a> {
|
||||
/// [`List::items`] fluent setter.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::List;
|
||||
///
|
||||
/// let empty_list = List::default();
|
||||
/// let filled_list = empty_list.items(["Item 1"]);
|
||||
/// ```
|
||||
///
|
||||
/// [`Text`]: ratatui_core::text::Text
|
||||
pub fn new<T>(items: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
@@ -181,9 +205,12 @@ impl<'a> List<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::List;
|
||||
///
|
||||
/// let list = List::default().items(["Item 1", "Item 2"]);
|
||||
/// ```
|
||||
///
|
||||
/// [`Text`]: ratatui_core::text::Text
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn items<T>(mut self, items: T) -> Self
|
||||
where
|
||||
@@ -203,8 +230,9 @@ impl<'a> List<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// use ratatui::widgets::{Block, List};
|
||||
///
|
||||
/// let items = ["Item 1"];
|
||||
/// let block = Block::bordered().title("List");
|
||||
/// let list = List::new(items).block(block);
|
||||
/// ```
|
||||
@@ -227,21 +255,28 @@ impl<'a> List<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::List,
|
||||
/// };
|
||||
///
|
||||
/// let items = ["Item 1"];
|
||||
/// let list = List::new(items).style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `List` also implements the [`Styled`] trait, which means you can use style shorthands from
|
||||
/// the [`Stylize`] trait to set the style of the widget more concisely.
|
||||
///
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`Stylize`]: ratatui_core::style::Stylize
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// use ratatui::{style::Stylize, widgets::List};
|
||||
///
|
||||
/// let items = ["Item 1"];
|
||||
/// let list = List::new(items).red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -257,8 +292,9 @@ impl<'a> List<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1", "Item 2"];
|
||||
/// use ratatui::widgets::List;
|
||||
///
|
||||
/// let items = ["Item 1", "Item 2"];
|
||||
/// let list = List::new(items).highlight_symbol(">>");
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -281,10 +317,16 @@ impl<'a> List<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1", "Item 2"];
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::List,
|
||||
/// };
|
||||
///
|
||||
/// let items = ["Item 1", "Item 2"];
|
||||
/// let list = List::new(items).highlight_style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.highlight_style = style.into();
|
||||
@@ -323,8 +365,9 @@ impl<'a> List<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// use ratatui::widgets::{HighlightSpacing, List};
|
||||
///
|
||||
/// let items = ["Item 1"];
|
||||
/// let list = List::new(items).highlight_spacing(HighlightSpacing::Always);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -345,8 +388,9 @@ impl<'a> List<'a> {
|
||||
/// Bottom to top
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// use ratatui::widgets::{List, ListDirection};
|
||||
///
|
||||
/// let items = ["Item 1"];
|
||||
/// let list = List::new(items).direction(ListDirection::BottomToTop);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -364,8 +408,9 @@ impl<'a> List<'a> {
|
||||
/// A padding value of 1 will keep 1 item above and 1 item bellow visible if possible
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # let items = ["Item 1"];
|
||||
/// use ratatui::widgets::List;
|
||||
///
|
||||
/// let items = ["Item 1"];
|
||||
/// let list = List::new(items).scroll_padding(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -421,6 +466,7 @@ where
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui_core::style::{Color, Modifier, Stylize};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::prelude::*;
|
||||
use ratatui_core::{style::Style, text::Text};
|
||||
|
||||
/// A single item in a [`List`]
|
||||
///
|
||||
@@ -19,14 +19,15 @@ use crate::prelude::*;
|
||||
/// You can create [`ListItem`]s from simple `&str`
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListItem;
|
||||
/// let item = ListItem::new("Item 1");
|
||||
/// ```
|
||||
///
|
||||
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{text::Line, widgets::ListItem};
|
||||
///
|
||||
/// let item1: ListItem = "Item 1".into();
|
||||
/// let item2: ListItem = Line::raw("Item 2").into();
|
||||
/// ```
|
||||
@@ -34,7 +35,8 @@ use crate::prelude::*;
|
||||
/// A [`ListItem`] styled with [`Stylize`]
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{style::Stylize, widgets::ListItem};
|
||||
///
|
||||
/// let item = ListItem::new("Item 1").red().on_white();
|
||||
/// ```
|
||||
///
|
||||
@@ -42,7 +44,12 @@ use crate::prelude::*;
|
||||
/// [`Text`]
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::Stylize,
|
||||
/// text::{Span, Text},
|
||||
/// widgets::ListItem,
|
||||
/// };
|
||||
///
|
||||
/// let mut text = Text::default();
|
||||
/// text.extend(["Item".blue(), Span::raw(" "), "1".bold().red()]);
|
||||
/// let item = ListItem::new(text);
|
||||
@@ -51,12 +58,15 @@ use crate::prelude::*;
|
||||
/// A right-aligned `ListItem`
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// ListItem::new(Text::from("foo").alignment(Alignment::Right));
|
||||
/// use ratatui::{text::Text, widgets::ListItem};
|
||||
///
|
||||
/// ListItem::new(Text::from("foo").right_aligned());
|
||||
/// ```
|
||||
///
|
||||
/// [`List`]: crate::widgets::List
|
||||
/// [`Stylize`]: crate::style::Stylize
|
||||
/// [`List`]: crate::list::List
|
||||
/// [`Stylize`]: ratatui_core::style::Stylize
|
||||
/// [`Line`]: ratatui_core::text::Line
|
||||
/// [`Line::alignment`]: ratatui_core::text::Line::alignment
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct ListItem<'a> {
|
||||
pub(crate) content: Text<'a>,
|
||||
@@ -73,29 +83,32 @@ impl<'a> ListItem<'a> {
|
||||
/// You can create [`ListItem`]s from simple `&str`
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListItem;
|
||||
///
|
||||
/// let item = ListItem::new("Item 1");
|
||||
/// ```
|
||||
///
|
||||
/// Anything that can be converted to [`Text`] can be a [`ListItem`].
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{text::Line, widgets::ListItem};
|
||||
///
|
||||
/// let item1: ListItem = "Item 1".into();
|
||||
/// let item2: ListItem = Line::raw("Item 2").into();
|
||||
/// ```
|
||||
///
|
||||
/// You can also create multilines item
|
||||
/// You can also create multiline items
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListItem;
|
||||
///
|
||||
/// let item = ListItem::new("Multi-line\nitem");
|
||||
/// ```
|
||||
///
|
||||
/// # See also
|
||||
///
|
||||
/// - [`List::new`](crate::widgets::List::new) to create a list of items that can be converted
|
||||
/// to [`ListItem`]
|
||||
/// - [`List::new`](super::List::new) to create a list of items that can be converted to
|
||||
/// [`ListItem`]
|
||||
pub fn new<T>(content: T) -> Self
|
||||
where
|
||||
T: Into<Text<'a>>,
|
||||
@@ -118,21 +131,27 @@ impl<'a> ListItem<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::ListItem,
|
||||
/// };
|
||||
///
|
||||
/// let item = ListItem::new("Item 1").style(Style::new().red().italic());
|
||||
/// ```
|
||||
///
|
||||
/// `ListItem` also implements the [`Styled`] trait, which means you can use style shorthands
|
||||
/// from the [`Stylize`](crate::style::Stylize) trait to set the style of the widget more
|
||||
/// from the [`Stylize`](ratatui_core::style::Stylize) trait to set the style of the widget more
|
||||
/// concisely.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{style::Stylize, widgets::ListItem};
|
||||
///
|
||||
/// let item = ListItem::new("Item 1").red().italic();
|
||||
/// ```
|
||||
///
|
||||
/// [`Styled`]: crate::style::Styled
|
||||
/// [`ListState`]: crate::widgets::list::ListState
|
||||
/// [`Styled`]: ratatui_core::style::Styled
|
||||
/// [`ListState`]: crate::list::ListState
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -146,7 +165,8 @@ impl<'a> ListItem<'a> {
|
||||
/// One line item
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListItem;
|
||||
///
|
||||
/// let item = ListItem::new("Item 1");
|
||||
/// assert_eq!(item.height(), 1);
|
||||
/// ```
|
||||
@@ -154,7 +174,8 @@ impl<'a> ListItem<'a> {
|
||||
/// Two lines item (note the `\n`)
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListItem;
|
||||
///
|
||||
/// let item = ListItem::new("Multi-line\nitem");
|
||||
/// assert_eq!(item.height(), 2);
|
||||
/// ```
|
||||
@@ -167,13 +188,15 @@ impl<'a> ListItem<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListItem;
|
||||
///
|
||||
/// let item = ListItem::new("12345");
|
||||
/// assert_eq!(item.width(), 5);
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListItem;
|
||||
///
|
||||
/// let item = ListItem::new("12345\n1234567");
|
||||
/// assert_eq!(item.width(), 7);
|
||||
/// ```
|
||||
@@ -196,6 +219,10 @@ mod tests {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui_core::{
|
||||
style::{Color, Modifier, Stylize},
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
prelude::{Buffer, Rect, StatefulWidget, StatefulWidgetRef, Widget, WidgetRef},
|
||||
widgets::{block::BlockExt, List, ListDirection, ListState},
|
||||
block::BlockExt,
|
||||
list::{List, ListDirection, ListState},
|
||||
};
|
||||
|
||||
impl Widget for List<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
WidgetRef::render_ref(&self, area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for List<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &List<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let mut state = ListState::default();
|
||||
StatefulWidgetRef::render_ref(self, area, buf, &mut state);
|
||||
StatefulWidget::render(self, area, buf, &mut state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,24 +27,16 @@ impl StatefulWidget for List<'_> {
|
||||
type State = ListState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
StatefulWidgetRef::render_ref(&self, area, buf, state);
|
||||
StatefulWidget::render(&self, area, buf, state);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: remove this when StatefulWidgetRef is stabilized and replace with the blanket impl
|
||||
impl StatefulWidget for &List<'_> {
|
||||
type State = ListState;
|
||||
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
StatefulWidgetRef::render_ref(self, area, buf, state);
|
||||
}
|
||||
}
|
||||
|
||||
impl StatefulWidgetRef for List<'_> {
|
||||
type State = ListState;
|
||||
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||
buf.set_style(area, self.style);
|
||||
self.block.render_ref(area, buf);
|
||||
self.block.as_ref().render(area, buf);
|
||||
let list_area = self.block.inner_if_some(area);
|
||||
|
||||
if list_area.is_empty() {
|
||||
@@ -108,7 +105,7 @@ impl StatefulWidgetRef for List<'_> {
|
||||
} else {
|
||||
row_area
|
||||
};
|
||||
item.content.render_ref(item_area, buf);
|
||||
Widget::render(&item.content, item_area, buf);
|
||||
|
||||
for j in 0..item.content.height() {
|
||||
// if the item is selected, we need to display the highlight symbol:
|
||||
@@ -269,13 +266,16 @@ impl List<'_> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui_core::{
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{StatefulWidget, Widget},
|
||||
};
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
prelude::*,
|
||||
widgets::{Block, HighlightSpacing, ListItem},
|
||||
};
|
||||
use crate::{block::Block, list::ListItem, table::HighlightSpacing};
|
||||
|
||||
#[fixture]
|
||||
fn single_line_buf() -> Buffer {
|
||||
@@ -1140,8 +1140,7 @@ mod tests {
|
||||
Lines: IntoIterator,
|
||||
Lines::Item: Into<Line<'line>>,
|
||||
{
|
||||
let backend = backend::TestBackend::new(10, render_height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, render_height));
|
||||
let mut state = ListState::default();
|
||||
|
||||
*state.offset_mut() = offset;
|
||||
@@ -1150,10 +1149,8 @@ mod tests {
|
||||
let list = List::new(["Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5"])
|
||||
.scroll_padding(padding)
|
||||
.highlight_symbol(">> ");
|
||||
terminal
|
||||
.draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer_lines(expected);
|
||||
StatefulWidget::render(list, buffer.area, &mut buffer, &mut state);
|
||||
assert_eq!(buffer, Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
/// If there isn't enough room for the selected item and the requested padding the list can jump
|
||||
@@ -1161,8 +1158,7 @@ mod tests {
|
||||
/// isn't currently happening
|
||||
#[test]
|
||||
fn padding_flicker() {
|
||||
let backend = backend::TestBackend::new(10, 5);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 5));
|
||||
let mut state = ListState::default();
|
||||
|
||||
*state.offset_mut() = 2;
|
||||
@@ -1173,15 +1169,11 @@ mod tests {
|
||||
];
|
||||
let list = List::new(items).scroll_padding(3).highlight_symbol(">> ");
|
||||
|
||||
terminal
|
||||
.draw(|f| f.render_stateful_widget(&list, f.area(), &mut state))
|
||||
.unwrap();
|
||||
StatefulWidget::render(&list, buffer.area, &mut buffer, &mut state);
|
||||
|
||||
let offset_after_render = state.offset();
|
||||
|
||||
terminal
|
||||
.draw(|f| f.render_stateful_widget(&list, f.area(), &mut state))
|
||||
.unwrap();
|
||||
StatefulWidget::render(&list, buffer.area, &mut buffer, &mut state);
|
||||
|
||||
// Offset after rendering twice should remain the same as after once
|
||||
assert_eq!(offset_after_render, state.offset());
|
||||
@@ -1189,8 +1181,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn padding_inconsistent_item_sizes() {
|
||||
let backend = backend::TestBackend::new(10, 3);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 3));
|
||||
let mut state = ListState::default().with_offset(0).with_selected(Some(3));
|
||||
|
||||
let items = [
|
||||
@@ -1203,9 +1194,7 @@ mod tests {
|
||||
];
|
||||
let list = List::new(items).scroll_padding(1).highlight_symbol(">> ");
|
||||
|
||||
terminal
|
||||
.draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
|
||||
.unwrap();
|
||||
StatefulWidget::render(list, buffer.area, &mut buffer, &mut state);
|
||||
|
||||
#[rustfmt::skip]
|
||||
let expected = [
|
||||
@@ -1213,15 +1202,14 @@ mod tests {
|
||||
" Item 2 ",
|
||||
">> Item 3 ",
|
||||
];
|
||||
terminal.backend().assert_buffer_lines(expected);
|
||||
assert_eq!(buffer, Buffer::with_lines(expected));
|
||||
}
|
||||
|
||||
// Tests to make sure when it's pushing back the first visible index value that it doesnt
|
||||
// include an item that's too large
|
||||
#[test]
|
||||
fn padding_offset_pushback_break() {
|
||||
let backend = backend::TestBackend::new(10, 4);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 4));
|
||||
let mut state = ListState::default();
|
||||
|
||||
*state.offset_mut() = 1;
|
||||
@@ -1235,16 +1223,16 @@ mod tests {
|
||||
];
|
||||
let list = List::new(items).scroll_padding(2).highlight_symbol(">> ");
|
||||
|
||||
terminal
|
||||
.draw(|f| f.render_stateful_widget(list, f.area(), &mut state))
|
||||
.unwrap();
|
||||
|
||||
terminal.backend().assert_buffer_lines([
|
||||
" Item 1 ",
|
||||
">> Item 2 ",
|
||||
" Item 3 ",
|
||||
" ",
|
||||
]);
|
||||
StatefulWidget::render(list, buffer.area, &mut buffer, &mut state);
|
||||
#[rustfmt::skip]
|
||||
assert_eq!(
|
||||
buffer,
|
||||
Buffer::with_lines([
|
||||
" Item 1 ",
|
||||
">> Item 2 ",
|
||||
" Item 3 ",
|
||||
" "])
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test for a bug where highlight symbol being greater than width caused a panic due
|
||||
@@ -3,7 +3,7 @@
|
||||
/// This state can be used to scroll through items and select one. When the list is rendered as a
|
||||
/// stateful widget, the selected item will be highlighted and the list will be shifted to ensure
|
||||
/// that the selected item is visible. This will modify the [`ListState`] object passed to the
|
||||
/// [`Frame::render_stateful_widget`](crate::terminal::Frame::render_stateful_widget) method.
|
||||
/// `Frame::render_stateful_widget` method.
|
||||
///
|
||||
/// The state consists of two fields:
|
||||
/// - [`offset`]: the index of the first item to be displayed
|
||||
@@ -20,10 +20,15 @@
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// layout::Rect,
|
||||
/// widgets::{List, ListState},
|
||||
/// Frame,
|
||||
/// };
|
||||
///
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// # let items = ["Item 1"];
|
||||
/// let items = ["Item 1"];
|
||||
/// let list = List::new(items);
|
||||
///
|
||||
/// // This should be stored outside of the function in your application state.
|
||||
@@ -36,7 +41,7 @@
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// [`List`]: crate::widgets::List
|
||||
/// [`List`]: super::List
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct ListState {
|
||||
@@ -52,7 +57,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let state = ListState::default().with_offset(1);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -68,7 +74,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let state = ListState::default().with_selected(Some(1));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -82,7 +89,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let state = ListState::default();
|
||||
/// assert_eq!(state.offset(), 0);
|
||||
/// ```
|
||||
@@ -95,7 +103,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// *state.offset_mut() = 1;
|
||||
/// ```
|
||||
@@ -110,8 +119,9 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// let state = TableState::default();
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let state = ListState::default();
|
||||
/// assert_eq!(state.selected(), None);
|
||||
/// ```
|
||||
pub const fn selected(&self) -> Option<usize> {
|
||||
@@ -125,7 +135,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// *state.selected_mut() = Some(1);
|
||||
/// ```
|
||||
@@ -140,7 +151,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select(Some(1));
|
||||
/// ```
|
||||
@@ -159,7 +171,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_next();
|
||||
/// ```
|
||||
@@ -176,7 +189,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_previous();
|
||||
/// ```
|
||||
@@ -193,7 +207,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_first();
|
||||
/// ```
|
||||
@@ -209,7 +224,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// state.select_last();
|
||||
/// ```
|
||||
@@ -226,7 +242,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// state.scroll_down_by(4);
|
||||
/// ```
|
||||
@@ -244,7 +261,8 @@ impl ListState {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::ListState;
|
||||
///
|
||||
/// let mut state = ListState::default();
|
||||
/// state.scroll_up_by(4);
|
||||
/// ```
|
||||
@@ -258,7 +276,7 @@ impl ListState {
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::widgets::ListState;
|
||||
use crate::list::ListState;
|
||||
|
||||
#[test]
|
||||
fn selected() {
|
||||
236
ratatui-widgets/src/logo.rs
Normal file
236
ratatui-widgets/src/logo.rs
Normal file
@@ -0,0 +1,236 @@
|
||||
//! The [`RatatuiLogo`] widget renders the Ratatui logo.
|
||||
use indoc::indoc;
|
||||
use ratatui_core::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget};
|
||||
|
||||
/// A widget that renders the Ratatui logo
|
||||
///
|
||||
/// The Ratatui logo takes up two lines of text and comes in two sizes: `Tiny` and `Small`. This may
|
||||
/// be used in an application's help or about screen to show that it is powered by Ratatui.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// The [Ratatui-logo] example demonstrates how to use the `RatatuiLogo` widget. This can be run by
|
||||
/// cloning the Ratatui repository and then running the following command with an optional size
|
||||
/// argument:
|
||||
///
|
||||
/// ```shell
|
||||
/// cargo run --example ratatui-logo [size]
|
||||
/// ```
|
||||
///
|
||||
/// [Ratatui-logo]: https://github.com/ratatui/ratatui/blob/main/examples/ratatui-logo.rs
|
||||
///
|
||||
/// ## Tiny (default, 2x15 characters)
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::RatatuiLogo;
|
||||
///
|
||||
/// # fn draw(frame: &mut ratatui::Frame) {
|
||||
/// frame.render_widget(RatatuiLogo::tiny(), frame.area());
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Renders:
|
||||
///
|
||||
/// ```text
|
||||
/// ▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌
|
||||
/// ▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌
|
||||
/// ```
|
||||
///
|
||||
/// ## Small (2x27 characters)
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::RatatuiLogo;
|
||||
///
|
||||
/// # fn draw(frame: &mut ratatui::Frame) {
|
||||
/// frame.render_widget(RatatuiLogo::small(), frame.area());
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Renders:
|
||||
///
|
||||
/// ```text
|
||||
/// █▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █
|
||||
/// █▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RatatuiLogo {
|
||||
size: Size,
|
||||
}
|
||||
|
||||
/// The size of the logo
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum Size {
|
||||
/// A tiny logo
|
||||
///
|
||||
/// The default size of the logo (2x15 characters)
|
||||
///
|
||||
/// ```text
|
||||
/// ▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌
|
||||
/// ▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌
|
||||
/// ```
|
||||
#[default]
|
||||
Tiny,
|
||||
/// A small logo
|
||||
///
|
||||
/// A slightly larger version of the logo (2x27 characters)
|
||||
///
|
||||
/// ```text
|
||||
/// █▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █
|
||||
/// █▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █
|
||||
/// ```
|
||||
Small,
|
||||
}
|
||||
|
||||
impl RatatuiLogo {
|
||||
/// Create a new Ratatui logo widget
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::{RatatuiLogo, RatatuiLogoSize};
|
||||
///
|
||||
/// let logo = RatatuiLogo::new(RatatuiLogoSize::Tiny);
|
||||
/// ```
|
||||
pub const fn new(size: Size) -> Self {
|
||||
Self { size }
|
||||
}
|
||||
|
||||
/// Set the size of the logo
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::{RatatuiLogo, RatatuiLogoSize};
|
||||
///
|
||||
/// let logo = RatatuiLogo::default().size(RatatuiLogoSize::Small);
|
||||
/// ```
|
||||
#[must_use]
|
||||
pub const fn size(self, size: Size) -> Self {
|
||||
let _ = self;
|
||||
Self { size }
|
||||
}
|
||||
|
||||
/// Create a new Ratatui logo widget with a tiny size
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::RatatuiLogo;
|
||||
///
|
||||
/// let logo = RatatuiLogo::tiny();
|
||||
/// ```
|
||||
pub const fn tiny() -> Self {
|
||||
Self::new(Size::Tiny)
|
||||
}
|
||||
|
||||
/// Create a new Ratatui logo widget with a small size
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::widgets::RatatuiLogo;
|
||||
///
|
||||
/// let logo = RatatuiLogo::small();
|
||||
/// ```
|
||||
pub const fn small() -> Self {
|
||||
Self::new(Size::Small)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for RatatuiLogo {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let logo = self.size.as_str();
|
||||
Text::raw(logo).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Size {
|
||||
const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Tiny => Self::tiny(),
|
||||
Self::Small => Self::small(),
|
||||
}
|
||||
}
|
||||
|
||||
const fn tiny() -> &'static str {
|
||||
indoc! {"
|
||||
▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌
|
||||
▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌
|
||||
"}
|
||||
}
|
||||
|
||||
const fn small() -> &'static str {
|
||||
indoc! {"
|
||||
█▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █
|
||||
█▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █
|
||||
"}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[rstest]
|
||||
#[case::tiny(Size::Tiny)]
|
||||
#[case::small(Size::Small)]
|
||||
fn new_size(#[case] size: Size) {
|
||||
let logo = RatatuiLogo::new(size);
|
||||
assert_eq!(logo.size, size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_logo_is_tiny() {
|
||||
let logo = RatatuiLogo::default();
|
||||
assert_eq!(logo.size, Size::Tiny);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_logo_size_to_small() {
|
||||
let logo = RatatuiLogo::default().size(Size::Small);
|
||||
assert_eq!(logo.size, Size::Small);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tiny_logo_constant() {
|
||||
let logo = RatatuiLogo::tiny();
|
||||
assert_eq!(logo.size, Size::Tiny);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn small_logo_constant() {
|
||||
let logo = RatatuiLogo::small();
|
||||
assert_eq!(logo.size, Size::Small);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn render_tiny() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 15, 2));
|
||||
RatatuiLogo::tiny().render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf,
|
||||
Buffer::with_lines([
|
||||
"▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌",
|
||||
"▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn render_small() {
|
||||
let mut buf = Buffer::empty(Rect::new(0, 0, 27, 2));
|
||||
RatatuiLogo::small().render(buf.area, &mut buf);
|
||||
assert_eq!(
|
||||
buf,
|
||||
Buffer::with_lines([
|
||||
"█▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █",
|
||||
"█▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █",
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
//! The [`Paragraph`] widget and related types allows displaying a block of text with optional
|
||||
//! wrapping, alignment, and block styling.
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Position, Rect},
|
||||
style::{Style, Styled},
|
||||
text::{Line, StyledGrapheme, Text},
|
||||
widgets::Widget,
|
||||
};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
style::Styled,
|
||||
text::StyledGrapheme,
|
||||
widgets::{
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
|
||||
Block,
|
||||
},
|
||||
block::{Block, BlockExt},
|
||||
reflow::{LineComposer, LineTruncator, WordWrapper, WrappedLine},
|
||||
};
|
||||
|
||||
const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
|
||||
@@ -59,7 +63,12 @@ const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Align
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// layout::Alignment,
|
||||
/// style::{Style, Stylize},
|
||||
/// text::{Line, Span},
|
||||
/// widgets::{Block, Paragraph, Wrap},
|
||||
/// };
|
||||
///
|
||||
/// let text = vec![
|
||||
/// Line::from(vec![
|
||||
@@ -76,6 +85,8 @@ const fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Align
|
||||
/// .alignment(Alignment::Center)
|
||||
/// .wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
///
|
||||
/// [`Span`]: ratatui_core::text::Span
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Paragraph<'a> {
|
||||
/// A block to wrap the widget in
|
||||
@@ -97,7 +108,10 @@ pub struct Paragraph<'a> {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// text::Text,
|
||||
/// widgets::{Paragraph, Wrap},
|
||||
/// };
|
||||
///
|
||||
/// let bullet_points = Text::from(
|
||||
/// r#"Some indented points:
|
||||
@@ -139,7 +153,12 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// text::{Line, Text},
|
||||
/// widgets::Paragraph,
|
||||
/// };
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello, world!");
|
||||
/// let paragraph = Paragraph::new(String::from("Hello, world!"));
|
||||
/// let paragraph = Paragraph::new(Text::raw("Hello, world!"));
|
||||
@@ -165,7 +184,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Block, Paragraph};
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello, world!").block(Block::bordered().title("Paragraph"));
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -185,9 +205,15 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// style::{Style, Stylize},
|
||||
/// widgets::Paragraph,
|
||||
/// };
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello, world!").style(Style::new().red().on_white());
|
||||
/// ```
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
@@ -201,7 +227,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::{Paragraph, Wrap};
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello, world!").wrap(Wrap { trim: true });
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -238,7 +265,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{layout::Alignment, widgets::Paragraph};
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello World").alignment(Alignment::Center);
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -254,7 +282,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello World").left_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -269,7 +298,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello World").centered();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -284,7 +314,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::widgets::Paragraph;
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello World").right_aligned();
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
@@ -305,7 +336,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{widgets::{Paragraph, Wrap}};
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello World")
|
||||
/// .wrap(Wrap { trim: false });
|
||||
/// assert_eq!(paragraph.line_count(20), 1);
|
||||
@@ -359,7 +391,8 @@ impl<'a> Paragraph<'a> {
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{widgets::Paragraph};
|
||||
///
|
||||
/// let paragraph = Paragraph::new("Hello World");
|
||||
/// assert_eq!(paragraph.line_width(), 11);
|
||||
///
|
||||
@@ -386,14 +419,14 @@ impl<'a> Paragraph<'a> {
|
||||
|
||||
impl Widget for Paragraph<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.render_ref(area, buf);
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for Paragraph<'_> {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
impl Widget for &Paragraph<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_style(area, self.style);
|
||||
self.block.render_ref(area, buf);
|
||||
self.block.as_ref().render(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
self.render_paragraph(inner, buf);
|
||||
}
|
||||
@@ -469,25 +502,27 @@ impl<'a> Styled for Paragraph<'a> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
backend::TestBackend,
|
||||
widgets::{block::Position, Borders},
|
||||
mod tests {
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span, Text},
|
||||
widgets::Widget,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::{block::Position, borders::Borders};
|
||||
|
||||
/// Tests the [`Paragraph`] widget against the expected [`Buffer`] by rendering it onto an equal
|
||||
/// area and comparing the rendered and expected content.
|
||||
/// This can be used for easy testing of varying configured paragraphs with the same expected
|
||||
/// buffer or any other test case really.
|
||||
#[track_caller]
|
||||
fn test_case(paragraph: &Paragraph, expected: &Buffer) {
|
||||
let backend = TestBackend::new(expected.area.width, expected.area.height);
|
||||
let mut terminal = Terminal::new(backend).unwrap();
|
||||
terminal
|
||||
.draw(|f| f.render_widget(paragraph.clone(), f.area()))
|
||||
.unwrap();
|
||||
terminal.backend().assert_buffer(expected);
|
||||
let mut buffer = Buffer::empty(Rect::new(0, 0, expected.area.width, expected.area.height));
|
||||
paragraph.render(buffer.area, &mut buffer);
|
||||
assert_eq!(buffer, *expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1,10 +1,10 @@
|
||||
//! Internal module for reflowing text to fit into a certain width.
|
||||
use std::{collections::VecDeque, mem};
|
||||
|
||||
use ratatui_core::{layout::Alignment, text::StyledGrapheme};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{layout::Alignment, text::StyledGrapheme};
|
||||
|
||||
/// A state machine to pack styled symbols into lines.
|
||||
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
|
||||
/// iterators for that).
|
||||
@@ -12,6 +12,7 @@ pub trait LineComposer<'a> {
|
||||
fn next_line<'lend>(&'lend mut self) -> Option<WrappedLine<'lend, 'a>>;
|
||||
}
|
||||
|
||||
/// A line that has been wrapped to a certain width.
|
||||
pub struct WrappedLine<'lend, 'text> {
|
||||
/// One line reflowed to the correct width
|
||||
pub line: &'lend [StyledGrapheme<'text>],
|
||||
@@ -51,6 +52,7 @@ where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
/// Create a new `WordWrapper` with the given lines and maximum line width.
|
||||
pub const fn new(lines: O, max_line_width: u16, trim: bool) -> Self {
|
||||
Self {
|
||||
input_lines: lines,
|
||||
@@ -250,6 +252,7 @@ where
|
||||
O: Iterator<Item = (I, Alignment)>,
|
||||
I: Iterator<Item = StyledGrapheme<'a>>,
|
||||
{
|
||||
/// Create a new `LineTruncator` with the given lines and maximum line width.
|
||||
pub const fn new(lines: O, max_line_width: u16) -> Self {
|
||||
Self {
|
||||
input_lines: lines,
|
||||
@@ -259,6 +262,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the horizontal offset to skip render.
|
||||
pub fn set_horizontal_offset(&mut self, horizontal_offset: u16) {
|
||||
self.horizontal_offset = horizontal_offset;
|
||||
}
|
||||
@@ -343,13 +347,14 @@ fn trim_offset(src: &str, mut offset: usize) -> &str {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
mod tests {
|
||||
use ratatui_core::{
|
||||
style::Style,
|
||||
text::{Line, Text},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Composer {
|
||||
WordWrapper { trim: bool },
|
||||
@@ -1,3 +1,4 @@
|
||||
//! The [`Scrollbar`] widget is used to display a scrollbar alongside other widgets.
|
||||
#![warn(clippy::pedantic)]
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
@@ -8,14 +9,16 @@
|
||||
|
||||
use std::iter;
|
||||
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::Style,
|
||||
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
|
||||
widgets::StatefulWidget,
|
||||
};
|
||||
use strum::{Display, EnumString};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
prelude::*,
|
||||
symbols::scrollbar::{Set, DOUBLE_HORIZONTAL, DOUBLE_VERTICAL},
|
||||
};
|
||||
|
||||
/// A widget to display a scrollbar
|
||||
///
|
||||
/// The following components of the scrollbar are customizable in symbol and style. Note the
|
||||
@@ -39,7 +42,15 @@ use crate::{
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*, widgets::*};
|
||||
/// use ratatui::{
|
||||
/// layout::{Margin, Rect},
|
||||
/// text::Line,
|
||||
/// widgets::{
|
||||
/// Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState,
|
||||
/// StatefulWidget,
|
||||
/// },
|
||||
/// Frame,
|
||||
/// };
|
||||
///
|
||||
/// # fn render_paragraph_with_scrollbar(frame: &mut Frame, area: Rect) {
|
||||
/// let vertical_scroll = 0; // from app state
|
||||
@@ -254,6 +265,8 @@ impl<'a> Scrollbar<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn thumb_style<S: Into<Style>>(mut self, thumb_style: S) -> Self {
|
||||
self.thumb_style = thumb_style.into();
|
||||
@@ -279,6 +292,8 @@ impl<'a> Scrollbar<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn track_style<S: Into<Style>>(mut self, track_style: S) -> Self {
|
||||
self.track_style = track_style.into();
|
||||
@@ -304,6 +319,8 @@ impl<'a> Scrollbar<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn begin_style<S: Into<Style>>(mut self, begin_style: S) -> Self {
|
||||
self.begin_style = begin_style.into();
|
||||
@@ -329,6 +346,8 @@ impl<'a> Scrollbar<'a> {
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn end_style<S: Into<Style>>(mut self, end_style: S) -> Self {
|
||||
self.end_style = end_style.into();
|
||||
@@ -382,6 +401,8 @@ impl<'a> Scrollbar<'a> {
|
||||
/// ```
|
||||
///
|
||||
/// This is a fluent setter method which must be chained or used as it consumes self
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
let style = style.into();
|
||||
@@ -617,6 +638,7 @@ impl ScrollbarOrientation {
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use ratatui_core::{text::Text, widgets::Widget};
|
||||
use rstest::{fixture, rstest};
|
||||
use strum::ParseError;
|
||||
|
||||
701
ratatui-widgets/src/sparkline.rs
Normal file
701
ratatui-widgets/src/sparkline.rs
Normal file
@@ -0,0 +1,701 @@
|
||||
//! The [`Sparkline`] widget is used to display a sparkline over one or more lines.
|
||||
use std::cmp::min;
|
||||
|
||||
use ratatui_core::{
|
||||
buffer::Buffer,
|
||||
layout::Rect,
|
||||
style::{Style, Styled},
|
||||
symbols::{self},
|
||||
widgets::Widget,
|
||||
};
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::block::{Block, BlockExt};
|
||||
|
||||
/// Widget to render a sparkline over one or more lines.
|
||||
///
|
||||
/// Each bar in a `Sparkline` represents a value from the provided dataset. The height of the bar
|
||||
/// is determined by the value in the dataset.
|
||||
///
|
||||
/// You can create a `Sparkline` using [`Sparkline::default`].
|
||||
///
|
||||
/// The data is set using [`Sparkline::data`]. The data can be a slice of `u64`, `Option<u64>`, or a
|
||||
/// [`SparklineBar`]. For the `Option<u64>` and [`SparklineBar`] cases, a data point with a value
|
||||
/// of `None` is interpreted an as the _absence_ of a value.
|
||||
///
|
||||
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
|
||||
/// provided by the [`Stylize`](ratatui_core::style::Stylize) trait. The style may be set for the
|
||||
/// entire widget or for individual bars by setting individual [`SparklineBar::style`].
|
||||
///
|
||||
/// The bars are rendered using a set of symbols. The default set is [`symbols::bar::NINE_LEVELS`].
|
||||
/// You can change the set using [`Sparkline::bar_set`].
|
||||
///
|
||||
/// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
|
||||
/// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
|
||||
/// styled with the style of the sparkline combined with the style provided in the [`SparklineBar`]
|
||||
/// if it is set, otherwise the sparkline style will be used.
|
||||
///
|
||||
/// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`] and
|
||||
/// the symbol set by [`Sparkline::absent_value_symbol`].
|
||||
///
|
||||
/// # Setter methods
|
||||
///
|
||||
/// - [`Sparkline::block`] wraps the sparkline in a [`Block`]
|
||||
/// - [`Sparkline::data`] defines the dataset, you'll almost always want to use it
|
||||
/// - [`Sparkline::max`] sets the maximum value of bars
|
||||
/// - [`Sparkline::direction`] sets the render direction
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// style::{Color, Style, Stylize},
|
||||
/// symbols,
|
||||
/// widgets::{Block, RenderDirection, Sparkline},
|
||||
/// };
|
||||
///
|
||||
/// Sparkline::default()
|
||||
/// .block(Block::bordered().title("Sparkline"))
|
||||
/// .data(&[0, 2, 3, 4, 1, 4, 10])
|
||||
/// .max(5)
|
||||
/// .direction(RenderDirection::RightToLeft)
|
||||
/// .style(Style::default().red().on_white())
|
||||
/// .absent_value_style(Style::default().fg(Color::Red))
|
||||
/// .absent_value_symbol(symbols::shade::FULL);
|
||||
/// ```
|
||||
#[derive(Debug, Default, Clone, Eq, PartialEq)]
|
||||
pub struct Sparkline<'a> {
|
||||
/// A block to wrap the widget in
|
||||
block: Option<Block<'a>>,
|
||||
/// Widget style
|
||||
style: Style,
|
||||
/// Style of absent values
|
||||
absent_value_style: Style,
|
||||
/// The symbol to use for absent values
|
||||
absent_value_symbol: AbsentValueSymbol,
|
||||
/// A slice of the data to display
|
||||
data: Vec<SparklineBar>,
|
||||
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
|
||||
/// widget uses the max of the dataset)
|
||||
max: Option<u64>,
|
||||
/// A set of bar symbols used to represent the give data
|
||||
bar_set: symbols::bar::Set,
|
||||
/// The direction to render the sparkline, either from left to right, or from right to left
|
||||
direction: RenderDirection,
|
||||
}
|
||||
|
||||
/// Defines the direction in which sparkline will be rendered.
|
||||
///
|
||||
/// See [`Sparkline::direction`].
|
||||
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
pub enum RenderDirection {
|
||||
/// The first value is on the left, going to the right
|
||||
#[default]
|
||||
LeftToRight,
|
||||
/// The first value is on the right, going to the left
|
||||
RightToLeft,
|
||||
}
|
||||
|
||||
impl<'a> Sparkline<'a> {
|
||||
/// Wraps the sparkline with the given `block`.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn block(mut self, block: Block<'a>) -> Self {
|
||||
self.block = Some(block);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the entire widget.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The foreground corresponds to the bars while the background is everything else.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style to use for absent values.
|
||||
///
|
||||
/// Absent values are values in the dataset that are `None`.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// The foreground corresponds to the bars while the background is everything else.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn absent_value_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.absent_value_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the symbol to use for absent values.
|
||||
///
|
||||
/// Absent values are values in the dataset that are `None`.
|
||||
///
|
||||
/// The default is [`symbols::shade::EMPTY`].
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn absent_value_symbol(mut self, symbol: impl Into<String>) -> Self {
|
||||
self.absent_value_symbol = AbsentValueSymbol(symbol.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the dataset for the sparkline.
|
||||
///
|
||||
/// Each item in the dataset is a bar in the sparkline. The height of the bar is determined by
|
||||
/// the value in the dataset.
|
||||
///
|
||||
/// The data can be a slice of `u64`, `Option<u64>`, or a [`SparklineBar`]. For the
|
||||
/// `Option<u64>` and [`SparklineBar`] cases, a data point with a value of `None` is
|
||||
/// interpreted an as the _absence_ of a value.
|
||||
///
|
||||
/// If the data provided is a slice of `u64` or `Option<u64>`, the bars will be styled with the
|
||||
/// style of the sparkline. If the data is a slice of [`SparklineBar`], the bars will be
|
||||
/// styled with the style of the sparkline combined with the style provided in the
|
||||
/// [`SparklineBar`] if it is set, otherwise the sparkline style will be used.
|
||||
///
|
||||
/// Absent values and will be rendered with the style set by [`Sparkline::absent_value_style`]
|
||||
/// and the symbol set by [`Sparkline::absent_value_symbol`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Create a `Sparkline` from a slice of `u64`:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{layout::Rect, widgets::Sparkline, Frame};
|
||||
///
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// let sparkline = Sparkline::default().data(&[1, 2, 3]);
|
||||
/// frame.render_widget(sparkline, area);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Create a `Sparkline` from a slice of `Option<u64>` such that some bars are absent:
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// let data = vec![Some(1), None, Some(3)];
|
||||
/// let sparkline = Sparkline::default().data(data);
|
||||
/// frame.render_widget(sparkline, area);
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// Create a [`Sparkline`] from a a Vec of [`SparklineBar`] such that some bars are styled:
|
||||
///
|
||||
/// ```
|
||||
/// # use ratatui::{prelude::*, widgets::*};
|
||||
/// # fn ui(frame: &mut Frame) {
|
||||
/// # let area = Rect::default();
|
||||
/// let data = vec![
|
||||
/// SparklineBar::from(1).style(Some(Style::default().fg(Color::Red))),
|
||||
/// SparklineBar::from(2),
|
||||
/// SparklineBar::from(3).style(Some(Style::default().fg(Color::Blue))),
|
||||
/// ];
|
||||
/// let sparkline = Sparkline::default().data(data);
|
||||
/// frame.render_widget(sparkline, area);
|
||||
/// # }
|
||||
/// ```
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn data<T>(mut self, data: T) -> Self
|
||||
where
|
||||
T: IntoIterator,
|
||||
T::Item: Into<SparklineBar>,
|
||||
{
|
||||
self.data = data.into_iter().map(Into::into).collect();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the maximum value of bars.
|
||||
///
|
||||
/// Every bar will be scaled accordingly. If no max is given, this will be the max in the
|
||||
/// dataset.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn max(mut self, max: u64) -> Self {
|
||||
self.max = Some(max);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the characters used to display the bars.
|
||||
///
|
||||
/// Can be [`symbols::bar::THREE_LEVELS`], [`symbols::bar::NINE_LEVELS`] (default) or a custom
|
||||
/// [`Set`](symbols::bar::Set).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn bar_set(mut self, bar_set: symbols::bar::Set) -> Self {
|
||||
self.bar_set = bar_set;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the direction of the sparkline.
|
||||
///
|
||||
/// [`RenderDirection::LeftToRight`] by default.
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub const fn direction(mut self, direction: RenderDirection) -> Self {
|
||||
self.direction = direction;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// An bar in a `Sparkline`.
|
||||
///
|
||||
/// The height of the bar is determined by the value and a value of `None` is interpreted as the
|
||||
/// _absence_ of a value, as distinct from a value of `Some(0)`.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq)]
|
||||
pub struct SparklineBar {
|
||||
/// The value of the bar.
|
||||
///
|
||||
/// If `None`, the bar is absent.
|
||||
value: Option<u64>,
|
||||
/// The style of the bar.
|
||||
///
|
||||
/// If `None`, the bar will use the style of the sparkline.
|
||||
style: Option<Style>,
|
||||
}
|
||||
|
||||
impl SparklineBar {
|
||||
/// Sets the style of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
///
|
||||
/// If not set, the default style of the sparkline will be used.
|
||||
///
|
||||
/// As well as the style of the sparkline, each [`SparklineBar`] may optionally set its own
|
||||
/// style. If set, the style of the bar will be the style of the sparkline combined with
|
||||
/// the style of the bar.
|
||||
///
|
||||
/// [`Color`]: ratatui_core::style::Color
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn style<S: Into<Option<Style>>>(mut self, style: S) -> Self {
|
||||
self.style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Option<u64>> for SparklineBar {
|
||||
fn from(value: Option<u64>) -> Self {
|
||||
Self { value, style: None }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for SparklineBar {
|
||||
fn from(value: u64) -> Self {
|
||||
Self {
|
||||
value: Some(value),
|
||||
style: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&u64> for SparklineBar {
|
||||
fn from(value: &u64) -> Self {
|
||||
Self {
|
||||
value: Some(*value),
|
||||
style: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Option<u64>> for SparklineBar {
|
||||
fn from(value: &Option<u64>) -> Self {
|
||||
Self {
|
||||
value: *value,
|
||||
style: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Styled for Sparkline<'a> {
|
||||
type Item = Self;
|
||||
|
||||
fn style(&self) -> Style {
|
||||
self.style
|
||||
}
|
||||
|
||||
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
|
||||
self.style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for Sparkline<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Widget::render(&self, area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &Sparkline<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
self.block.as_ref().render(area, buf);
|
||||
let inner = self.block.inner_if_some(area);
|
||||
self.render_sparkline(inner, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A newtype wrapper for the symbol to use for absent values.
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
struct AbsentValueSymbol(String);
|
||||
|
||||
impl Default for AbsentValueSymbol {
|
||||
fn default() -> Self {
|
||||
Self(symbols::shade::EMPTY.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl Sparkline<'_> {
|
||||
fn render_sparkline(&self, spark_area: Rect, buf: &mut Buffer) {
|
||||
if spark_area.is_empty() {
|
||||
return;
|
||||
}
|
||||
// determine the maximum height across all bars
|
||||
let max_height = self
|
||||
.max
|
||||
.unwrap_or_else(|| self.data.iter().filter_map(|s| s.value).max().unwrap_or(1));
|
||||
|
||||
// determine the maximum index to render
|
||||
let max_index = min(spark_area.width as usize, self.data.len());
|
||||
|
||||
// render each item in the data
|
||||
for (i, item) in self.data.iter().take(max_index).enumerate() {
|
||||
let x = match self.direction {
|
||||
RenderDirection::LeftToRight => spark_area.left() + i as u16,
|
||||
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
|
||||
};
|
||||
|
||||
// determine the height, symbol and style to use for the item
|
||||
//
|
||||
// if the item is not absent:
|
||||
// - the height is the value of the item scaled to the height of the spark area
|
||||
// - the symbol is determined by the scaled height
|
||||
// - the style is the style of the item, if one is set
|
||||
//
|
||||
// otherwise:
|
||||
// - the height is the total height of the spark area
|
||||
// - the symbol is the absent value symbol
|
||||
// - the style is the absent value style
|
||||
let (mut height, symbol, style) = match item {
|
||||
SparklineBar {
|
||||
value: Some(value),
|
||||
style,
|
||||
} => {
|
||||
let height = if max_height == 0 {
|
||||
0
|
||||
} else {
|
||||
*value * u64::from(spark_area.height) * 8 / max_height
|
||||
};
|
||||
(height, None, *style)
|
||||
}
|
||||
_ => (
|
||||
u64::from(spark_area.height) * 8,
|
||||
Some(self.absent_value_symbol.0.as_str()),
|
||||
Some(self.absent_value_style),
|
||||
),
|
||||
};
|
||||
|
||||
// render the item from top to bottom
|
||||
//
|
||||
// if the symbol is set it will be used for the entire height of the bar, otherwise the
|
||||
// symbol will be determined by the _remaining_ height.
|
||||
//
|
||||
// if the style is set it will be used for the entire height of the bar, otherwise the
|
||||
// sparkline style will be used.
|
||||
for j in (0..spark_area.height).rev() {
|
||||
let symbol = symbol.unwrap_or_else(|| self.symbol_for_height(height));
|
||||
if height > 8 {
|
||||
height -= 8;
|
||||
} else {
|
||||
height = 0;
|
||||
}
|
||||
buf[(x, spark_area.top() + j)]
|
||||
.set_symbol(symbol)
|
||||
.set_style(self.style.patch(style.unwrap_or_default()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const fn symbol_for_height(&self, height: u64) -> &str {
|
||||
match height {
|
||||
0 => self.bar_set.empty,
|
||||
1 => self.bar_set.one_eighth,
|
||||
2 => self.bar_set.one_quarter,
|
||||
3 => self.bar_set.three_eighths,
|
||||
4 => self.bar_set.half,
|
||||
5 => self.bar_set.five_eighths,
|
||||
6 => self.bar_set.three_quarters,
|
||||
7 => self.bar_set.seven_eighths,
|
||||
_ => self.bar_set.full,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ratatui_core::{
|
||||
buffer::Cell,
|
||||
style::{Color, Modifier, Stylize},
|
||||
};
|
||||
use strum::ParseError;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn render_direction_to_string() {
|
||||
assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
|
||||
assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_direction_from_str() {
|
||||
assert_eq!(
|
||||
"LeftToRight".parse::<RenderDirection>(),
|
||||
Ok(RenderDirection::LeftToRight)
|
||||
);
|
||||
assert_eq!(
|
||||
"RightToLeft".parse::<RenderDirection>(),
|
||||
Ok(RenderDirection::RightToLeft)
|
||||
);
|
||||
assert_eq!(
|
||||
"".parse::<RenderDirection>(),
|
||||
Err(ParseError::VariantNotFound)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_be_created_from_vec_of_u64() {
|
||||
let data = vec![1_u64, 2, 3];
|
||||
let spark_data = Sparkline::default().data(data).data;
|
||||
let expected = vec![
|
||||
SparklineBar::from(1),
|
||||
SparklineBar::from(2),
|
||||
SparklineBar::from(3),
|
||||
];
|
||||
assert_eq!(spark_data, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_be_created_from_vec_of_option_u64() {
|
||||
let data = vec![Some(1_u64), None, Some(3)];
|
||||
let spark_data = Sparkline::default().data(data).data;
|
||||
let expected = vec![
|
||||
SparklineBar::from(1),
|
||||
SparklineBar::from(None),
|
||||
SparklineBar::from(3),
|
||||
];
|
||||
assert_eq!(spark_data, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_be_created_from_array_of_u64() {
|
||||
let data = [1_u64, 2, 3];
|
||||
let spark_data = Sparkline::default().data(data).data;
|
||||
let expected = vec![
|
||||
SparklineBar::from(1),
|
||||
SparklineBar::from(2),
|
||||
SparklineBar::from(3),
|
||||
];
|
||||
assert_eq!(spark_data, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_be_created_from_array_of_option_u64() {
|
||||
let data = [Some(1_u64), None, Some(3)];
|
||||
let spark_data = Sparkline::default().data(data).data;
|
||||
let expected = vec![
|
||||
SparklineBar::from(1),
|
||||
SparklineBar::from(None),
|
||||
SparklineBar::from(3),
|
||||
];
|
||||
assert_eq!(spark_data, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_be_created_from_slice_of_u64() {
|
||||
let data = vec![1_u64, 2, 3];
|
||||
let spark_data = Sparkline::default().data(&data).data;
|
||||
let expected = vec![
|
||||
SparklineBar::from(1),
|
||||
SparklineBar::from(2),
|
||||
SparklineBar::from(3),
|
||||
];
|
||||
assert_eq!(spark_data, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_can_be_created_from_slice_of_option_u64() {
|
||||
let data = vec![Some(1_u64), None, Some(3)];
|
||||
let spark_data = Sparkline::default().data(&data).data;
|
||||
let expected = vec![
|
||||
SparklineBar::from(1),
|
||||
SparklineBar::from(None),
|
||||
SparklineBar::from(3),
|
||||
];
|
||||
assert_eq!(spark_data, expected);
|
||||
}
|
||||
|
||||
// Helper function to render a sparkline to a buffer with a given width
|
||||
// filled with x symbols to make it easier to assert on the result
|
||||
fn render(widget: Sparkline<'_>, width: u16) -> Buffer {
|
||||
let area = Rect::new(0, 0, width, 1);
|
||||
let mut buffer = Buffer::filled(area, Cell::new("x"));
|
||||
widget.render(area, &mut buffer);
|
||||
buffer
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_zero() {
|
||||
let widget = Sparkline::default().data([0, 0, 0]);
|
||||
let buffer = render(widget, 6);
|
||||
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_does_not_panic_if_max_is_set_to_zero() {
|
||||
// see https://github.com/rust-lang/rust-clippy/issues/13191
|
||||
#[allow(clippy::unnecessary_min_or_max)]
|
||||
let widget = Sparkline::default().data([0, 1, 2]).max(0);
|
||||
let buffer = render(widget, 6);
|
||||
assert_eq!(buffer, Buffer::with_lines([" xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_draws() {
|
||||
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
let buffer = render(widget, 12);
|
||||
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_draws_double_height() {
|
||||
let widget = Sparkline::default().data([0, 1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
let area = Rect::new(0, 0, 12, 2);
|
||||
let mut buffer = Buffer::filled(area, Cell::new("x"));
|
||||
widget.render(area, &mut buffer);
|
||||
assert_eq!(buffer, Buffer::with_lines([" ▂▄▆█xxx", " ▂▄▆█████xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_left_to_right() {
|
||||
let widget = Sparkline::default()
|
||||
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::LeftToRight);
|
||||
let buffer = render(widget, 12);
|
||||
assert_eq!(buffer, Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_right_to_left() {
|
||||
let widget = Sparkline::default()
|
||||
.data([0, 1, 2, 3, 4, 5, 6, 7, 8])
|
||||
.direction(RenderDirection::RightToLeft);
|
||||
let buffer = render(widget, 12);
|
||||
assert_eq!(buffer, Buffer::with_lines(["xxx█▇▆▅▄▃▂▁ "]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_absent_value_style() {
|
||||
let widget = Sparkline::default()
|
||||
.absent_value_style(Style::default().fg(Color::Red))
|
||||
.absent_value_symbol(symbols::shade::FULL)
|
||||
.data([
|
||||
None,
|
||||
Some(1),
|
||||
Some(2),
|
||||
Some(3),
|
||||
Some(4),
|
||||
Some(5),
|
||||
Some(6),
|
||||
Some(7),
|
||||
Some(8),
|
||||
]);
|
||||
let buffer = render(widget, 12);
|
||||
let mut expected = Buffer::with_lines(["█▁▂▃▄▅▆▇█xxx"]);
|
||||
expected.set_style(Rect::new(0, 0, 1, 1), Style::default().fg(Color::Red));
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_absent_value_style_double_height() {
|
||||
let widget = Sparkline::default()
|
||||
.absent_value_style(Style::default().fg(Color::Red))
|
||||
.absent_value_symbol(symbols::shade::FULL)
|
||||
.data([
|
||||
None,
|
||||
Some(1),
|
||||
Some(2),
|
||||
Some(3),
|
||||
Some(4),
|
||||
Some(5),
|
||||
Some(6),
|
||||
Some(7),
|
||||
Some(8),
|
||||
]);
|
||||
let area = Rect::new(0, 0, 12, 2);
|
||||
let mut buffer = Buffer::filled(area, Cell::new("x"));
|
||||
widget.render(area, &mut buffer);
|
||||
let mut expected = Buffer::with_lines(["█ ▂▄▆█xxx", "█▂▄▆█████xxx"]);
|
||||
expected.set_style(Rect::new(0, 0, 1, 2), Style::default().fg(Color::Red));
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_custom_absent_value_style() {
|
||||
let widget = Sparkline::default().absent_value_symbol('*').data([
|
||||
None,
|
||||
Some(1),
|
||||
Some(2),
|
||||
Some(3),
|
||||
Some(4),
|
||||
Some(5),
|
||||
Some(6),
|
||||
Some(7),
|
||||
Some(8),
|
||||
]);
|
||||
let buffer = render(widget, 12);
|
||||
let expected = Buffer::with_lines(["*▁▂▃▄▅▆▇█xxx"]);
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_renders_with_custom_bar_styles() {
|
||||
let widget = Sparkline::default().data(vec![
|
||||
SparklineBar::from(Some(0)).style(Some(Style::default().fg(Color::Red))),
|
||||
SparklineBar::from(Some(1)).style(Some(Style::default().fg(Color::Red))),
|
||||
SparklineBar::from(Some(2)).style(Some(Style::default().fg(Color::Red))),
|
||||
SparklineBar::from(Some(3)).style(Some(Style::default().fg(Color::Green))),
|
||||
SparklineBar::from(Some(4)).style(Some(Style::default().fg(Color::Green))),
|
||||
SparklineBar::from(Some(5)).style(Some(Style::default().fg(Color::Green))),
|
||||
SparklineBar::from(Some(6)).style(Some(Style::default().fg(Color::Blue))),
|
||||
SparklineBar::from(Some(7)).style(Some(Style::default().fg(Color::Blue))),
|
||||
SparklineBar::from(Some(8)).style(Some(Style::default().fg(Color::Blue))),
|
||||
]);
|
||||
let buffer = render(widget, 12);
|
||||
let mut expected = Buffer::with_lines([" ▁▂▃▄▅▆▇█xxx"]);
|
||||
expected.set_style(Rect::new(0, 0, 3, 1), Style::default().fg(Color::Red));
|
||||
expected.set_style(Rect::new(3, 0, 3, 1), Style::default().fg(Color::Green));
|
||||
expected.set_style(Rect::new(6, 0, 3, 1), Style::default().fg(Color::Blue));
|
||||
assert_eq!(buffer, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_be_stylized() {
|
||||
assert_eq!(
|
||||
Sparkline::default()
|
||||
.black()
|
||||
.on_white()
|
||||
.bold()
|
||||
.not_dim()
|
||||
.style,
|
||||
Style::default()
|
||||
.fg(Color::Black)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.remove_modifier(Modifier::DIM)
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user