Compare commits
167 Commits
jm/metrics
...
v0.30.0-al
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e7831aedd4 | ||
|
|
4a871f993e | ||
|
|
da05957fa0 | ||
|
|
41d883da7a | ||
|
|
0552223511 | ||
|
|
21a561b1b3 | ||
|
|
079d74ce14 | ||
|
|
22ec4f7414 | ||
|
|
6f213191ef | ||
|
|
088aac136d | ||
|
|
a692a6e371 | ||
|
|
1798512e94 | ||
|
|
32f3833a6d | ||
|
|
2ccc40e116 | ||
|
|
019e34e006 | ||
|
|
11cbb2ba87 | ||
|
|
50ba96518f | ||
|
|
904b0aa723 | ||
|
|
b544e394c9 | ||
|
|
1d28c89fe5 | ||
|
|
1d2882636e | ||
|
|
157cb3401b | ||
|
|
3d0c96a838 | ||
|
|
03066d81bf | ||
|
|
5f57d35234 | ||
|
|
5c021bf344 | ||
|
|
9721300a47 | ||
|
|
9a541981b8 | ||
|
|
a6a1368250 | ||
|
|
1fcca6369e | ||
|
|
694c788c24 | ||
|
|
6e436725e4 | ||
|
|
dafb716f9d | ||
|
|
819e92cd44 | ||
|
|
a38066d2d1 | ||
|
|
3646c97840 | ||
|
|
1c3b698b82 | ||
|
|
c767f6bc3c | ||
|
|
08ea837753 | ||
|
|
a8aca0ec12 | ||
|
|
ed5dd73084 | ||
|
|
b5f7e44183 | ||
|
|
fab532171d | ||
|
|
898aef6e2f | ||
|
|
f57b696fdc | ||
|
|
452366aa9e | ||
|
|
ff729b7607 | ||
|
|
6ddde0e8a8 | ||
|
|
93ad6b828c | ||
|
|
e411d9ec3e | ||
|
|
a0979d6871 | ||
|
|
ed071f3723 | ||
|
|
9275d3421c | ||
|
|
15f442a71e | ||
|
|
18e70d3d51 | ||
|
|
17bba14540 | ||
|
|
ce4856a65f | ||
|
|
f2451e7f1e | ||
|
|
7c8573f575 | ||
|
|
4f0a8b21af | ||
|
|
91147c4d75 | ||
|
|
6dd25a3111 | ||
|
|
35eba76b4d | ||
|
|
357ae7e251 | ||
|
|
3b13240728 | ||
|
|
d201b8e5dd | ||
|
|
21e62d84c2 | ||
|
|
d3f01ebf6e | ||
|
|
2892bddce6 | ||
|
|
fbf6050c86 | ||
|
|
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 | ||
|
|
b88717b65f | ||
|
|
5635b930c7 | ||
|
|
870bc6a64a | ||
|
|
da821b431e | ||
|
|
68886d1787 | ||
|
|
0f48239778 |
2
.cargo/config.toml
Normal file
2
.cargo/config.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run --package xtask --"
|
||||
22
.cz.toml
22
.cz.toml
@@ -32,17 +32,17 @@ schema_pattern = "(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)
|
||||
type = "list"
|
||||
name = "change_type"
|
||||
choices = [
|
||||
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
|
||||
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
|
||||
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
|
||||
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
|
||||
{ value = "feat", name = "feat: A new feature.", key = "f" },
|
||||
{ value = "fix", name = "fix: A bug fix.", key = "x" },
|
||||
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
|
||||
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
|
||||
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
|
||||
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
|
||||
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
|
||||
{ value = "build", name = "build: Changes that affect the build system or external dependencies (example scopes: pip, docker, npm)", key = "b" },
|
||||
{ value = "chore", name = "chore: A modification that generally does not fall into any other category", key = "c" },
|
||||
{ value = "ci", name = "ci: Changes to our CI configuration files and scripts (example scopes: GitLabCI)", key = "i" },
|
||||
{ value = "docs", name = "docs: Documentation only changes", key = "d" },
|
||||
{ value = "feat", name = "feat: A new feature.", key = "f" },
|
||||
{ value = "fix", name = "fix: A bug fix.", key = "x" },
|
||||
{ value = "perf", name = "perf: A code change that improves performance", key = "p" },
|
||||
{ value = "refactor", name = "refactor: A code change that neither fixes a bug nor adds a feature", key = "r" },
|
||||
{ value = "revert", name = "revert: Revert previous commits", key = "v" },
|
||||
{ value = "style", name = "style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc)", key = "s" },
|
||||
{ value = "test", name = "test: Adding missing or correcting existing tests", key = "t" },
|
||||
]
|
||||
message = "Select the type of change you are committing"
|
||||
|
||||
|
||||
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@v7
|
||||
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@v7
|
||||
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"
|
||||
129
.github/workflows/ci.yml
vendored
129
.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,66 +20,91 @@ 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 }
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
components: rustfmt
|
||||
- run: cargo +nightly fmt --all --check
|
||||
tool: taplo-cli
|
||||
- run: cargo xtask format --check
|
||||
|
||||
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
|
||||
|
||||
clippy:
|
||||
# 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: bnjbvr/cargo-machete@v0.7.0
|
||||
|
||||
# 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 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:
|
||||
@@ -92,13 +116,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 --all-features
|
||||
|
||||
# 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 readme --check
|
||||
|
||||
# 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
|
||||
@@ -107,47 +141,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 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
|
||||
|
||||
12
.taplo.toml
Normal file
12
.taplo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
include = ["**/*.toml"]
|
||||
|
||||
[formatting]
|
||||
column_width = 100
|
||||
reorder_keys = false
|
||||
|
||||
[[rule]]
|
||||
include = ["**/Cargo.toml"]
|
||||
keys = ["dependencies", "dev-dependencies", "build-dependencies", "workspace.dependencies"]
|
||||
|
||||
[rule.formatting]
|
||||
reorder_keys = true
|
||||
@@ -10,8 +10,18 @@ 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
|
||||
- `FrameExt` trait for `unstable-widget-ref` feature
|
||||
- [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 +75,211 @@ 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)
|
||||
|
||||
### `FrameExt` trait for `unstable-widget-ref` feature ([#1530])
|
||||
|
||||
[#1530]: https://github.com/ratatui/ratatui/pull/1530
|
||||
|
||||
To call `Frame::render_widget_ref()` or `Frame::render_stateful_widget_ref()` you now need to:
|
||||
|
||||
1. Import the `FrameExt` trait from `ratatui::widgets`.
|
||||
2. Enable the `unstable-widget-ref` feature.
|
||||
|
||||
For example:
|
||||
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::Rect,
|
||||
widgets::{Block, FrameExt},
|
||||
};
|
||||
|
||||
let block = Block::new();
|
||||
let area = Rect::new(0, 0, 5, 5);
|
||||
frame.render_widget_ref(&block, area);
|
||||
```
|
||||
|
||||
### `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 +348,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
|
||||
|
||||
|
||||
764
CHANGELOG.md
764
CHANGELOG.md
@@ -1,6 +1,768 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
<!-- ignore lint rules that are often triggered by content generated from commits / git-cliff -->
|
||||
<!-- markdownlint-disable line-length no-bare-urls ul-style emphasis-style -->
|
||||
|
||||
## [0.30.0-alpha.1](https://github.com/ratatui/ratatui/releases/tag/0.30.0-alpha.1) - 2025-01-14
|
||||
|
||||
0.30.0-alpha.1 is a pre-release of the upcoming 0.30.0 release. It is intended for testing and
|
||||
feedback. Please report any issues you encounter. The primary change is that we've split the crate
|
||||
into multiple crates. The main crate is now `ratatui-core` and the widgets are in `ratatui-widgets`.
|
||||
This allows for widget crates to depend on a stable core crate, and for the core crate to be used
|
||||
without widgets.
|
||||
|
||||
### Features
|
||||
|
||||
- [56d5e05](https://github.com/ratatui/ratatui/commit/56d5e057625378f1bdf732dabb57208453fb84d6) *(bar)* Update label and text_value to accept Into<> by @Emivvvvv in [#1471](https://github.com/ratatui/ratatui/pull/1471) [**breaking**]
|
||||
|
||||
- [b76ad3b](https://github.com/ratatui/ratatui/commit/b76ad3b02ea1c3381b6434c40e620f28d642948e) *(bar)* Impl Styled for Bar by @Emivvvvv in [#1476](https://github.com/ratatui/ratatui/pull/1476)
|
||||
|
||||
- [369b18e](https://github.com/ratatui/ratatui/commit/369b18eef2e4220147e7c7264ad7f8e023a1d2dd) *(barchart)* Reduce barchart creation verbosity by @Emivvvvv in [#1453](https://github.com/ratatui/ratatui/pull/1453)
|
||||
|
||||
- [9275d34](https://github.com/ratatui/ratatui/commit/9275d3421c088174bcf9de0832340bcbea76367a) *(layout)* Add Offset::new() constructor by @joshka in [#1547](https://github.com/ratatui/ratatui/pull/1547)
|
||||
|
||||
- [ff729b7](https://github.com/ratatui/ratatui/commit/ff729b7607e0099a155f10dfe0ce42320641b74d) *(scrollbar)* Support retrieving the current position of state by @orhun in [#1552](https://github.com/ratatui/ratatui/pull/1552)
|
||||
|
||||
- [ce4856a](https://github.com/ratatui/ratatui/commit/ce4856a65f3c76db714a45338ba3be9b638c6c35) *(widgets)* Add the missing constructor to canvas types by @orhun in [#1538](https://github.com/ratatui/ratatui/pull/1538)
|
||||
|
||||
- [50ba965](https://github.com/ratatui/ratatui/commit/50ba96518f01454cff185784a1e9676093ce06cf) *(uncategorized)* Add a new RatatuiMascot widget by @Its-Just-Nans in [#1584](https://github.com/ratatui/ratatui/pull/1584)
|
||||
|
||||
- [1d28c89](https://github.com/ratatui/ratatui/commit/1d28c89fe50f1a11cf1719233acca1a12d5e2d7b) *(uncategorized)* Add conversions for anstyle by @joshka in [#1581](https://github.com/ratatui/ratatui/pull/1581)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- [860e48b](https://github.com/ratatui/ratatui/commit/860e48b0f0ec51c8596bc12985a8b37bad4fba00) *(buffer)* Buffer::get_pos() now correctly handles index > u16::MAX by @joshka in [#1447](https://github.com/ratatui/ratatui/pull/1447)
|
||||
|
||||
- [ec30390](https://github.com/ratatui/ratatui/commit/ec30390446b998cba97a25db63b2e3d27db7a12d) *(canvas)* Round coordinates to nearest grid cell by @joshka in [#1507](https://github.com/ratatui/ratatui/pull/1507)
|
||||
|
||||
- [afd1ce1](https://github.com/ratatui/ratatui/commit/afd1ce179b74f10ea786ed6f9b2999288bc94e7a) *(canvas)* Lines that start outside the visible grid are now drawn by @renesat in [#1501](https://github.com/ratatui/ratatui/pull/1501)
|
||||
|
||||
- [18e70d3](https://github.com/ratatui/ratatui/commit/18e70d3d51b654bb895843224edf0c21a3114dc9) *(crossterm)* Terminal should keep Bold when removing Dim by @MarSik in [#1541](https://github.com/ratatui/ratatui/pull/1541)
|
||||
|
||||
- [a692a6e](https://github.com/ratatui/ratatui/commit/a692a6e37114f39a3eb15b0cdc486dee8a7e91ff) *(lint)* Apply rust 1.84 clippy suggestions by @joshka in [#1612](https://github.com/ratatui/ratatui/pull/1612)
|
||||
|
||||
- [3b13240](https://github.com/ratatui/ratatui/commit/3b13240728597a8c459c4bb5f913372185e9df91) *(scrollbar)* Check for area.is_empty() before rendering by @farmeroy in [#1529](https://github.com/ratatui/ratatui/pull/1529)
|
||||
|
||||
- [f57b696](https://github.com/ratatui/ratatui/commit/f57b696fdc3bac381bccda0414400453f5abecf2) *(span)* Dont render control characters by @EdJoPaTo in [#1312](https://github.com/ratatui/ratatui/pull/1312)
|
||||
|
||||
- [2ce958e](https://github.com/ratatui/ratatui/commit/2ce958e38cf1ebab49b1817b669631dc349a0ebb) *(table)* Allow display of additional table row, if row height > 1 by @Lunderberg in [#1452](https://github.com/ratatui/ratatui/pull/1452)
|
||||
|
||||
- [2892bdd](https://github.com/ratatui/ratatui/commit/2892bddce66daf0b285a6d9f6fb3fdb2581cda2b) *(uncategorized)* Rust 1.83 clippy lints by @joshka in [#1527](https://github.com/ratatui/ratatui/pull/1527)
|
||||
|
||||
- [36e2d1b](https://github.com/ratatui/ratatui/commit/36e2d1bda1d8b4e1d0fbd09ba2894d56bd7eeeca) *(uncategorized)* Add feature(doc_cfg) when generating docs by @joshka in [#1506](https://github.com/ratatui/ratatui/pull/1506)
|
||||
|
||||
- [4d7704f](https://github.com/ratatui/ratatui/commit/4d7704fba5dcebda34c8c04bf4b5ac48e3a22008) *(uncategorized)* Make StatefulWidget and Ref work with unsized State by @thscharler in [#1505](https://github.com/ratatui/ratatui/pull/1505)
|
||||
|
||||
- [7b87509](https://github.com/ratatui/ratatui/commit/7b875091e18b894f53af6331a8ad5b7101a75d1e) *(uncategorized)* Typo by @marcoieni in [#1480](https://github.com/ratatui/ratatui/pull/1480)
|
||||
|
||||
### Refactor
|
||||
|
||||
- [f132fa1](https://github.com/ratatui/ratatui/commit/f132fa1715ea0893c52e35c65d505abadb75cec6) *(table)* Small readability improvements by @joshka in [#1510](https://github.com/ratatui/ratatui/pull/1510)
|
||||
|
||||
- [904b0aa](https://github.com/ratatui/ratatui/commit/904b0aa723e63d3fb68802b797949619506bf3a9) *(uncategorized)* Move symbols to modules by @joshka in [#1594](https://github.com/ratatui/ratatui/pull/1594)
|
||||
|
||||
- [7c8573f](https://github.com/ratatui/ratatui/commit/7c8573f5750ea8cf87101f81314803c834ea4942) *(uncategorized)* Rearrange selection_spacing code by @raylu in [#1540](https://github.com/ratatui/ratatui/pull/1540)
|
||||
|
||||
- [217c57c](https://github.com/ratatui/ratatui/commit/217c57cd60628abde1ca2f0c39b014e22c9edc4e) *(uncategorized)* Modularize backends by @orhun in [#1508](https://github.com/ratatui/ratatui/pull/1508)
|
||||
|
||||
- [e461b72](https://github.com/ratatui/ratatui/commit/e461b724a6b010fe242f9bd6d9746007cbf24219) *(uncategorized)* Move {Stateful,}Widget{,Ref} types into individual files by @joshka in [#1479](https://github.com/ratatui/ratatui/pull/1479)
|
||||
|
||||
### Documentation
|
||||
|
||||
- [d291042](https://github.com/ratatui/ratatui/commit/d291042e69ad930ae0d3c5d1f991d9e68320c00e) *(block)* Revise the block example by @orhun in [#1520](https://github.com/ratatui/ratatui/pull/1520)
|
||||
|
||||
- [fcde9cb](https://github.com/ratatui/ratatui/commit/fcde9cb9c3c5e9752fefbcc7cdeac95e2db9d684) *(changelog)* Fix typo by @orhun in [#1463](https://github.com/ratatui/ratatui/pull/1463)
|
||||
|
||||
- [3ae6bf1](https://github.com/ratatui/ratatui/commit/3ae6bf1d6f24407de400024dcb5924d841a2e1ba) *(contributing)* Use cargo-xtask for instructions by @orhun in [#1509](https://github.com/ratatui/ratatui/pull/1509)
|
||||
|
||||
- [04e1b32](https://github.com/ratatui/ratatui/commit/04e1b32cd2e77653266fa8e7269a2e08d774220e) *(layout)* Rename cassowary-rs references to cassowary by @miroim in [#1448](https://github.com/ratatui/ratatui/pull/1448)
|
||||
|
||||
- [088aac1](https://github.com/ratatui/ratatui/commit/088aac136d8b22557081ab4c657f5c9f5cde80d8) *(readme)* Tweak links and badges by @joshka in [#1598](https://github.com/ratatui/ratatui/pull/1598)
|
||||
|
||||
- [6e43672](https://github.com/ratatui/ratatui/commit/6e436725e4f8c58be527b07c464a0d8b5d306eb6) *(readme)* Reimagine README.md by @orhun in [#1569](https://github.com/ratatui/ratatui/pull/1569)
|
||||
|
||||
- [8f28247](https://github.com/ratatui/ratatui/commit/8f282473b21d7693f20b5f0cdad2f12b9ed209ff) *(readme)* Correct examples links by @HoKim98 in [#1484](https://github.com/ratatui/ratatui/pull/1484)
|
||||
|
||||
- [9f90f74](https://github.com/ratatui/ratatui/commit/9f90f7495fd46b3b7ac34160f094bc1583f82b70) *(readme)* Fix broken link by @nilsmartel in [#1485](https://github.com/ratatui/ratatui/pull/1485)
|
||||
|
||||
- [260af68](https://github.com/ratatui/ratatui/commit/260af68a347b527281265f7bf259eb1336aa49b2) *(readme)* Include iocraft as an alternative by @kdheepak in [#1483](https://github.com/ratatui/ratatui/pull/1483)
|
||||
|
||||
- [dafb716](https://github.com/ratatui/ratatui/commit/dafb716f9d8acb5dda303dc1e155dc90eb54b926) *(widgets)* Add example for grouped barchart by @orhun in [#1566](https://github.com/ratatui/ratatui/pull/1566)
|
||||
|
||||
- [ed5dd73](https://github.com/ratatui/ratatui/commit/ed5dd73084e2c8ccf3d36bc23048d67755a90e6a) *(widgets)* Add example for tabs by @orhun in [#1559](https://github.com/ratatui/ratatui/pull/1559)
|
||||
|
||||
- [fab5321](https://github.com/ratatui/ratatui/commit/fab532171d1c8e9639298550565e4eefb8737275) *(widgets)* Add example for scrollbar by @orhun in [#1545](https://github.com/ratatui/ratatui/pull/1545)
|
||||
|
||||
- [898aef6](https://github.com/ratatui/ratatui/commit/898aef6e2f38655e58684a2d842f03bcde0ebf0c) *(widgets)* Add example for list by @orhun in [#1542](https://github.com/ratatui/ratatui/pull/1542)
|
||||
|
||||
- [452366a](https://github.com/ratatui/ratatui/commit/452366aa9e666f26946ebccf6046a0bed393f5c1) *(widgets)* Add example for sparkline by @orhun in [#1556](https://github.com/ratatui/ratatui/pull/1556)
|
||||
|
||||
- [6ddde0e](https://github.com/ratatui/ratatui/commit/6ddde0e8a84b0909ba5631afac2dfc7878764786) *(widgets)* Add example for table by @orhun in [#1557](https://github.com/ratatui/ratatui/pull/1557)
|
||||
|
||||
- [93ad6b8](https://github.com/ratatui/ratatui/commit/93ad6b828c3a34be982447390d17b7953a9c328e) *(widgets)* Update values in chart example by @orhun in [#1558](https://github.com/ratatui/ratatui/pull/1558)
|
||||
|
||||
- [15f442a](https://github.com/ratatui/ratatui/commit/15f442a71ed4ce3faeaac3b2a3a7798940eb1846) *(widgets)* Add example for paragraph by @orhun in [#1544](https://github.com/ratatui/ratatui/pull/1544)
|
||||
|
||||
- [17bba14](https://github.com/ratatui/ratatui/commit/17bba14540449ae584a9cafbe2a39cc7fa451ef4) *(widgets)* Move the logo example to widgets by @orhun in [#1543](https://github.com/ratatui/ratatui/pull/1543)
|
||||
|
||||
- [f2451e7](https://github.com/ratatui/ratatui/commit/f2451e7f1ed1d5bd38e8901f139bc2916a4b005e) *(widgets)* Add example for gauge by @orhun in [#1539](https://github.com/ratatui/ratatui/pull/1539)
|
||||
|
||||
- [4f0a8b2](https://github.com/ratatui/ratatui/commit/4f0a8b21af49b825debb13695f8c1f368f4f56b5) *(widgets)* Add example for canvas by @orhun in [#1533](https://github.com/ratatui/ratatui/pull/1533)
|
||||
|
||||
- [91147c4](https://github.com/ratatui/ratatui/commit/91147c4d75bee207052b06a7dca4b610df321de1) *(widgets)* Add example for chart by @orhun in [#1536](https://github.com/ratatui/ratatui/pull/1536)
|
||||
|
||||
- [6dd25a3](https://github.com/ratatui/ratatui/commit/6dd25a311152abf3e0938c9a59e54c98e2a327a6) *(widgets)* Add example for calendar by @orhun in [#1532](https://github.com/ratatui/ratatui/pull/1532)
|
||||
|
||||
- [99ac005](https://github.com/ratatui/ratatui/commit/99ac005b06f807c79060dba43b33ec9b1f7c1a3a) *(widgets)* Add simple barchart example by @joshka in [#1511](https://github.com/ratatui/ratatui/pull/1511)
|
||||
|
||||
- [da05957](https://github.com/ratatui/ratatui/commit/da05957fa01fcc420519400d82ef9df6acace0e8) *(uncategorized)* Add widget-ref-container example by @joshka in [#1603](https://github.com/ratatui/ratatui/pull/1603)
|
||||
|
||||
- [1798512](https://github.com/ratatui/ratatui/commit/1798512e94b277fdfafeddb4043f7174ce2ac64a) *(uncategorized)* Fix wording in user_input example by @dawedawe in [#1611](https://github.com/ratatui/ratatui/pull/1611)
|
||||
|
||||
- [03066d8](https://github.com/ratatui/ratatui/commit/03066d81bfc7fb4356e8d188f3d6a15b45509c15) *(uncategorized)* Fix punctuation in canvas.rs documentation by @dawedawe in [#1583](https://github.com/ratatui/ratatui/pull/1583)
|
||||
|
||||
- [e411d9e](https://github.com/ratatui/ratatui/commit/e411d9ec3e921e812b7e8d2836c2ad0b60a0e6f8) *(uncategorized)* Add input form example by @joshka in [#1551](https://github.com/ratatui/ratatui/pull/1551)
|
||||
|
||||
- [ed071f3](https://github.com/ratatui/ratatui/commit/ed071f37232fae47a2193235d57934cc5c678baa) *(uncategorized)* Add mouse-drawing example by @joshka in [#1546](https://github.com/ratatui/ratatui/pull/1546)
|
||||
|
||||
- [46902f5](https://github.com/ratatui/ratatui/commit/46902f5587efe2b27c39b5e3c39109c62a636ba3) *(uncategorized)* Improve docs for workspace crates by @orhun in [#1490](https://github.com/ratatui/ratatui/pull/1490)
|
||||
|
||||
- [a6b5792](https://github.com/ratatui/ratatui/commit/a6b579223fd83c36e024428df49a3027cd1c21bc) *(uncategorized)* Fix example link in readme by @thomas-tacquet in [#1462](https://github.com/ratatui/ratatui/pull/1462)
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- [abe2f27](https://github.com/ratatui/ratatui/commit/abe2f273289d3798968a645a38a7a38571530065) *(backend)* Change From<T> impls to new backend specific IntoBackend and FromBackend traits by @joshka in [#1464](https://github.com/ratatui/ratatui/pull/1464) [**breaking**]
|
||||
|
||||
- [0a47ebd](https://github.com/ratatui/ratatui/commit/0a47ebd94bbc7a89c9e2aa893cf13a988756ec19) *(bencher)* Update bencher CLI usage by @epompeii in [#1470](https://github.com/ratatui/ratatui/pull/1470)
|
||||
|
||||
- [a0979d6](https://github.com/ratatui/ratatui/commit/a0979d68715b2c67b32b8909189bc103f0e81f6e) *(build)* Remove cargo lint by @joshka in [#1549](https://github.com/ratatui/ratatui/pull/1549)
|
||||
|
||||
- [eaa4038](https://github.com/ratatui/ratatui/commit/eaa403856ecb60338619e2e727a0388187ae017c) *(ci)* Install pre-built binaries for cargo-rdme by @orhun in [#1477](https://github.com/ratatui/ratatui/pull/1477)
|
||||
|
||||
- [e5e2316](https://github.com/ratatui/ratatui/commit/e5e2316451fb4c085f205b4884cc82ba8a4930e6) *(ci)* Add check for keeping README.md up-to-date by @orhun in [#1473](https://github.com/ratatui/ratatui/pull/1473)
|
||||
|
||||
- [2ef3583](https://github.com/ratatui/ratatui/commit/2ef3583effdeb6492d76d977177b4363129a8c8c) *(ci)* Replace cargo-make with a custom cargo-xtask by @joshka in [#1461](https://github.com/ratatui/ratatui/pull/1461)
|
||||
|
||||
- [98df774](https://github.com/ratatui/ratatui/commit/98df774d7f9b69a2a474d25adf38e38a428f1b77) *(core)* Move core types to ratatui-core by @joshka in [#1460](https://github.com/ratatui/ratatui/pull/1460)
|
||||
|
||||
- [35eba76](https://github.com/ratatui/ratatui/commit/35eba76b4dff047dde2d1e01bc427eb1b992b490) *(example)* Move demo2 to top level folder by @joshka in [#1524](https://github.com/ratatui/ratatui/pull/1524)
|
||||
|
||||
- [5f57d35](https://github.com/ratatui/ratatui/commit/5f57d3523436e944b60ccc7a937d69336c9f82b8) *(examples)* Add colors explorer demo app by @orhun in [#1580](https://github.com/ratatui/ratatui/pull/1580)
|
||||
|
||||
- [5c021bf](https://github.com/ratatui/ratatui/commit/5c021bf344b17fc4075e9663dfb270bead180e25) *(examples)* Add chart demo app by @orhun in [#1579](https://github.com/ratatui/ratatui/pull/1579)
|
||||
|
||||
- [9721300](https://github.com/ratatui/ratatui/commit/9721300a473096daf6b34f6cc7c13643d61a4e00) *(examples)* Add canvas demo app by @orhun in [#1578](https://github.com/ratatui/ratatui/pull/1578)
|
||||
|
||||
- [a6a1368](https://github.com/ratatui/ratatui/commit/a6a13682507846320a79538ffad673a58c1143f0) *(examples)* Add calendar explorer demo app by @orhun in [#1571](https://github.com/ratatui/ratatui/pull/1571)
|
||||
|
||||
- [819e92c](https://github.com/ratatui/ratatui/commit/819e92cd44b6bee7d21115ff465c2f3f8c82ed9b) *(examples)* Add weather demo app by @orhun in [#1567](https://github.com/ratatui/ratatui/pull/1567)
|
||||
|
||||
- [b5f7e44](https://github.com/ratatui/ratatui/commit/b5f7e4418364c9710d14567c73122af67e0a63ae) *(examples)* Move async example to apps by @joshka in [#1503](https://github.com/ratatui/ratatui/pull/1503)
|
||||
|
||||
- [17316ec](https://github.com/ratatui/ratatui/commit/17316ec5d0a4807600dd116736d66938b985e718) *(github)* Enable sponsorship button by @orhun in [#1478](https://github.com/ratatui/ratatui/pull/1478)
|
||||
|
||||
- [d3f01eb](https://github.com/ratatui/ratatui/commit/d3f01ebf6ea97e71bcda8c84b054943e1f24cd4e) *(lint)* Ensure lint config is correct by @joshka in [#1528](https://github.com/ratatui/ratatui/pull/1528)
|
||||
|
||||
- [2b7ec5c](https://github.com/ratatui/ratatui/commit/2b7ec5cb7f34edb65fc81d362d3b512b98d246ac) *(widgets)* Enable calendar widget as default by @orhun in [#1521](https://github.com/ratatui/ratatui/pull/1521)
|
||||
|
||||
- [d201b8e](https://github.com/ratatui/ratatui/commit/d201b8e5ddd98a1887252179dec8f09e1f342b0c) *(xtask)* Check lints for only library targets by @orhun in [#1531](https://github.com/ratatui/ratatui/pull/1531)
|
||||
|
||||
- [6f21319](https://github.com/ratatui/ratatui/commit/6f213191efd528fd7e0d5c99fda3e6d028ee0f98) *(uncategorized)* Rename examples with clashing names by @joshka in [#1597](https://github.com/ratatui/ratatui/pull/1597)
|
||||
|
||||
- [11cbb2b](https://github.com/ratatui/ratatui/commit/11cbb2ba87e557a04674973a39d59124d8683ed1) *(uncategorized)* Use cargo xtask for bacon clippy command by @joshka in [#1592](https://github.com/ratatui/ratatui/pull/1592)
|
||||
|
||||
- [b544e39](https://github.com/ratatui/ratatui/commit/b544e394c97fffdeddaccd44bea2214ebffa1616) *(uncategorized)* Use clap instead of argh for demo example by @joshka in [#1591](https://github.com/ratatui/ratatui/pull/1591)
|
||||
|
||||
- [9a54198](https://github.com/ratatui/ratatui/commit/9a541981b8accd9efe17c0893d18b520bc569b15) *(uncategorized)* Make source files non-executable by @orhun in [#1577](https://github.com/ratatui/ratatui/pull/1577)
|
||||
|
||||
- [357ae7e](https://github.com/ratatui/ratatui/commit/357ae7e251721f2e7fcb539de5e6fc60eaa30e29) *(uncategorized)* Move terminal types to ratatui-core by @joshka in [#1530](https://github.com/ratatui/ratatui/pull/1530) [**breaking**]
|
||||
|
||||
- [21e62d8](https://github.com/ratatui/ratatui/commit/21e62d84c2d2fa2d5563caf0c0974c2514e56ee5) *(uncategorized)* Move the demo example to main folder by @joshka in [#1523](https://github.com/ratatui/ratatui/pull/1523)
|
||||
|
||||
- [fbf6050](https://github.com/ratatui/ratatui/commit/fbf6050c867b63276ae3d6f5bca4d741c2ce355c) *(uncategorized)* Prepare alpha modularization release by @joshka in [#1525](https://github.com/ratatui/ratatui/pull/1525)
|
||||
|
||||
- [e4e95bc](https://github.com/ratatui/ratatui/commit/e4e95bcecf15deb09416a0e53193d261f012222a) *(uncategorized)* Remove --color always flags from bacon.toml by @joshka in [#1502](https://github.com/ratatui/ratatui/pull/1502)
|
||||
|
||||
- [a41c97b](https://github.com/ratatui/ratatui/commit/a41c97b413b28d0db6d1ea09dcc1d5b8556148b1) *(uncategorized)* Move unstable widget refs to ratatui by @joshka in [#1491](https://github.com/ratatui/ratatui/pull/1491) [**breaking**]
|
||||
|
||||
- [e7085e3](https://github.com/ratatui/ratatui/commit/e7085e3a3ec4b3b90a4e69d49add96e7ba65616c) *(uncategorized)* Move widgets into ratatui-widgets crate by @joshka in [#1474](https://github.com/ratatui/ratatui/pull/1474)
|
||||
|
||||
- [f1d0a18](https://github.com/ratatui/ratatui/commit/f1d0a1837564d69f00e4b5d9eb94cc001cd3a3a7) *(uncategorized)* Move ratatui crate into workspace folder by @joshka in [#1459](https://github.com/ratatui/ratatui/pull/1459)
|
||||
|
||||
- [55fb2d2](https://github.com/ratatui/ratatui/commit/55fb2d2e56b492f0f4131fde9d44951b504cf50c) *(uncategorized)* Update repo links to ratatui instead of ratatui-org by @joshka in [#1458](https://github.com/ratatui/ratatui/pull/1458)
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
- [4a871f9](https://github.com/ratatui/ratatui/commit/4a871f993ea38069da513660707a072be299b791) *(uncategorized)* Refactor xtask / toml formatting by @joshka in [#1602](https://github.com/ratatui/ratatui/pull/1602)
|
||||
|
||||
### New Contributors
|
||||
|
||||
* @dawedawe made their first contribution in [#1611](https://github.com/ratatui/ratatui/pull/1611)
|
||||
* @Its-Just-Nans made their first contribution in [#1584](https://github.com/ratatui/ratatui/pull/1584)
|
||||
* @MarSik made their first contribution in [#1541](https://github.com/ratatui/ratatui/pull/1541)
|
||||
* @raylu made their first contribution in [#1540](https://github.com/ratatui/ratatui/pull/1540)
|
||||
* @renesat made their first contribution in [#1501](https://github.com/ratatui/ratatui/pull/1501)
|
||||
* @HoKim98 made their first contribution in [#1484](https://github.com/ratatui/ratatui/pull/1484)
|
||||
* @nilsmartel made their first contribution in [#1485](https://github.com/ratatui/ratatui/pull/1485)
|
||||
* @marcoieni made their first contribution in [#1480](https://github.com/ratatui/ratatui/pull/1480)
|
||||
* @epompeii made their first contribution in [#1470](https://github.com/ratatui/ratatui/pull/1470)
|
||||
* @thomas-tacquet made their first contribution in [#1462](https://github.com/ratatui/ratatui/pull/1462)
|
||||
* @miroim made their first contribution in [#1448](https://github.com/ratatui/ratatui/pull/1448)
|
||||
|
||||
**Full Changelog**: https://github.com/ratatui/ratatui/compare/v0.29.0...0.30.0-alpha.1
|
||||
|
||||
## [v0.29.0](https://github.com/ratatui/ratatui/releases/tag/v0.29.0) - 2024-10-21
|
||||
|
||||
> _"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).
|
||||
|
||||
### 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
|
||||
|
||||
@@ -4625,7 +5387,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`
|
||||
|
||||
4100
Cargo.lock
generated
Normal file
4100
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
379
Cargo.toml
379
Cargo.toml
@@ -1,8 +1,19 @@
|
||||
[package]
|
||||
name = "ratatui"
|
||||
version = "0.28.1" # crate version
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["ratatui", "ratatui-*", "xtask", "examples/apps/*"]
|
||||
default-members = [
|
||||
"ratatui",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
# this is not included as it doesn't compile on windows
|
||||
# "ratatui-termion",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
"examples/apps/*",
|
||||
]
|
||||
|
||||
[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,76 +21,44 @@ 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"
|
||||
once_cell = "1.19.0"
|
||||
paste = "1.0.2"
|
||||
palette = { version = "0.7.6", optional = true }
|
||||
serde = { version = "1", optional = true, features = ["derive"] }
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
strum_macros = { version = "0.26.3" }
|
||||
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"
|
||||
metrics = { version = "0.23.0", git = "https://github.com/joshka/metrics.git", branch = "jm/derive-debug" }
|
||||
quanta = "0.12.3"
|
||||
[workspace.dependencies]
|
||||
bitflags = "2.7.0"
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = "0.28.1"
|
||||
document-features = "0.2.7"
|
||||
indoc = "2.0.5"
|
||||
instability = "0.3.7"
|
||||
itertools = "0.13.0"
|
||||
pretty_assertions = "1.4.1"
|
||||
ratatui = { path = "ratatui", version = "0.30.0-alpha.1" }
|
||||
ratatui-core = { path = "ratatui-core", version = "0.1.0-alpha.2" }
|
||||
ratatui-crossterm = { path = "ratatui-crossterm", version = "0.1.0-alpha.1" }
|
||||
ratatui-termion = { path = "ratatui-termion", version = "0.1.0-alpha.1" }
|
||||
ratatui-termwiz = { path = "ratatui-termwiz", version = "0.1.0-alpha.1" }
|
||||
ratatui-widgets = { path = "ratatui-widgets", version = "0.3.0-alpha.1" }
|
||||
rstest = "0.24.0"
|
||||
serde = { version = "1.0.217", features = ["derive"] }
|
||||
serde_json = "1.0.135"
|
||||
strum = { version = "0.26.3", features = ["derive"] }
|
||||
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
|
||||
termion = "4.0.0"
|
||||
unicode-width = "=0.2.0"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
# termion is not supported on Windows
|
||||
termion = { version = "4.0.0", optional = true }
|
||||
# Improve benchmark consistency
|
||||
[profile.bench]
|
||||
codegen-units = 1
|
||||
lto = 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"] }
|
||||
derive_builder = "0.20.0"
|
||||
fakeit = "1.1"
|
||||
font8x8 = "0.3.1"
|
||||
futures = "0.3.30"
|
||||
indoc = "2"
|
||||
metrics-util = { version = "0.17.0", git = "https://github.com/joshka/metrics.git", branch = "jm/derive-debug" }
|
||||
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]
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
cargo = { level = "warn", priority = -1 }
|
||||
[workspace.lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
@@ -116,269 +95,3 @@ 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"]
|
||||
|
||||
## 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 = []
|
||||
|
||||
[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"]
|
||||
|
||||
# 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 = "metrics"
|
||||
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
|
||||
'''
|
||||
505
README.md
505
README.md
@@ -1,429 +1,170 @@
|
||||
<details>
|
||||
<summary>Table of Contents</summary>
|
||||
|
||||
- [Ratatui](#ratatui)
|
||||
- [Installation](#installation)
|
||||
- [Introduction](#introduction)
|
||||
- [Other Documentation](#other-documentation)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Status of this fork](#status-of-this-fork)
|
||||
- [Rust version requirements](#rust-version-requirements)
|
||||
- [Widgets](#widgets)
|
||||
- [Built in](#built-in)
|
||||
- [Third\-party libraries, bootstrapping templates and
|
||||
widgets](#third-party-libraries-bootstrapping-templates-and-widgets)
|
||||
- [Apps](#apps)
|
||||
- [Alternatives](#alternatives)
|
||||
- [Acknowledgments](#acknowledgments)
|
||||
- [License](#license)
|
||||
- [Quickstart](#quickstart)
|
||||
- [Documentation](#documentation)
|
||||
- [Templates](#templates)
|
||||
- [Built with Ratatui](#built-with-ratatui)
|
||||
- [Alternatives](#alternatives)
|
||||
- [Contributing](#contributing)
|
||||
- [Acknowledgements](#acknowledgements)
|
||||
- [License](#license)
|
||||
|
||||
</details>
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||

|
||||
|
||||
<div align="center">
|
||||
|
||||
[![Crate Badge]][Crate] [![Docs Badge]][API Docs] [![CI Badge]][CI Workflow] [![Deps.rs
|
||||
Badge]][Deps.rs]<br> [![Codecov Badge]][Codecov] [![License Badge]](./LICENSE) [![Sponsors
|
||||
Badge]][GitHub Sponsors]<br> [![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix]
|
||||
[![Forum Badge]][Forum]<br>
|
||||
|
||||
[Ratatui Website] · [API Docs] · [Examples] · [Changelog] · [Breaking Changes]<br>
|
||||
[Contributing] · [Report a bug] · [Request a Feature] · [Create a Pull Request]
|
||||
[![Crate Badge]][Crate] [![Repo Badge]][Repo] [![Docs Badge]][Docs] [![License Badge]][License] \
|
||||
[![CI Badge]][CI] [![Deps Badge]][Deps] [![Codecov Badge]][Codecov] [![Sponsors Badge]][Sponsors] \
|
||||
[Ratatui Website] · [Docs] · [Widget Examples] · [App Examples] · [Changelog] \
|
||||
[Breaking Changes] · [Contributing] · [Report a bug] · [Request a Feature]
|
||||
|
||||
</div>
|
||||
|
||||
# Ratatui
|
||||
|
||||
[Ratatui][Ratatui Website] is a crate for cooking up terminal user interfaces in Rust. It is a
|
||||
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
|
||||
|
||||
Add `ratatui` as a dependency to your cargo.toml:
|
||||
|
||||
```shell
|
||||
cargo add ratatui
|
||||
```
|
||||
|
||||
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]).
|
||||
|
||||
## 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.
|
||||
|
||||
You can also watch the [FOSDEM 2024 talk] about Ratatui which gives a brief introduction to
|
||||
terminal user interfaces and showcases the features of Ratatui, along with a hello world demo.
|
||||
|
||||
## Other documentation
|
||||
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials
|
||||
- [Ratatui Forum][Forum] - a place to ask questions and discuss the library
|
||||
- [API Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [Contributing] - Please read this if you are interested in contributing to the project.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
[Ratatui][Ratatui Website] (_ˌræ.təˈtu.i_) is a Rust crate for cooking up terminal user interfaces
|
||||
(TUIs). It provides a simple and flexible way to create text-based user interfaces in the terminal,
|
||||
which can be used for command-line applications, dashboards, and other interactive console programs.
|
||||
|
||||
## Quickstart
|
||||
|
||||
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.
|
||||
Ratatui has [templates] available to help you get started quickly. You can use the
|
||||
[`cargo-generate`] command to create a new project with Ratatui:
|
||||
|
||||
Every application built with `ratatui` needs to implement the following steps:
|
||||
```shell
|
||||
cargo install --locked cargo-generate
|
||||
cargo generate ratatui/templates
|
||||
```
|
||||
|
||||
- Initialize the terminal
|
||||
- A main loop to:
|
||||
- Handle input events
|
||||
- Draw the UI
|
||||
- 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].
|
||||
|
||||
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.
|
||||
|
||||
### 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.
|
||||
|
||||
### Handling events
|
||||
|
||||
Ratatui does not include any input handling. Instead event handling can be implemented by
|
||||
calling backend library methods directly. See the [Handling Events] section of the [Ratatui
|
||||
Website] for more info. For example, if you are using [Crossterm], you can use the
|
||||
[`crossterm::event`] module to handle events.
|
||||
|
||||
### Example
|
||||
Selecting the Hello World template produces the following application:
|
||||
|
||||
```rust
|
||||
use std::io::{self, stdout};
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event};
|
||||
use ratatui::{DefaultTerminal, Frame};
|
||||
|
||||
use ratatui::{
|
||||
backend::CrosstermBackend,
|
||||
crossterm::{
|
||||
event::{self, Event, KeyCode},
|
||||
terminal::{
|
||||
disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
|
||||
},
|
||||
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 main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
fn run(mut terminal: DefaultTerminal) -> Result<()> {
|
||||
loop {
|
||||
terminal.draw(render)?;
|
||||
if matches!(event::read()?, Event::Key(_)) {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
frame.render_widget(
|
||||
Paragraph::new("Hello World!").block(Block::bordered().title("Greeting")),
|
||||
frame.size(),
|
||||
);
|
||||
fn render(frame: &mut Frame) {
|
||||
frame.render_widget("hello world", frame.area());
|
||||
}
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
## Documentation
|
||||
|
||||
![docsrs-hello]
|
||||
- [Docs] - the full API documentation for the library on docs.rs.
|
||||
- [Ratatui Website] - explains the library's concepts and provides step-by-step tutorials.
|
||||
- [Ratatui Forum] - a place to ask questions and discuss the library.
|
||||
- [Widget Examples] - a collection of examples that demonstrate how to use the library.
|
||||
- [App Examples] - a collection of more complex examples that demonstrate how to build apps.
|
||||
- [Changelog] - generated by [git-cliff] utilizing [Conventional Commits].
|
||||
- [Breaking Changes] - a list of breaking changes in the library.
|
||||
|
||||
## Layout
|
||||
You can also watch the [EuroRust 2024 talk] to learn about common concepts in Ratatui and what's
|
||||
possible to build with it.
|
||||
|
||||
The library comes with a basic yet useful layout management object called [`Layout`] which
|
||||
allows you to split the available space into multiple areas and then render widgets in each
|
||||
area. This lets you describe a responsive terminal UI by nesting layouts. See the [Layout]
|
||||
section of the [Ratatui Website] for more info.
|
||||
## Templates
|
||||
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
widgets::Block,
|
||||
Frame,
|
||||
};
|
||||
If you're looking to get started quickly, you can use one of the available templates from the
|
||||
[templates] repository using [`cargo-generate`]:
|
||||
|
||||
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.size());
|
||||
let [left_area, right_area] =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||
.areas(main_area);
|
||||
|
||||
frame.render_widget(Block::bordered().title("Title Bar"), title_area);
|
||||
frame.render_widget(Block::bordered().title("Status Bar"), status_area);
|
||||
frame.render_widget(Block::bordered().title("Left"), left_area);
|
||||
frame.render_widget(Block::bordered().title("Right"), right_area);
|
||||
}
|
||||
```shell
|
||||
cargo generate ratatui/templates
|
||||
```
|
||||
|
||||
Running this example produces the following output:
|
||||
## Built with Ratatui
|
||||
|
||||
![docsrs-layout]
|
||||
[][awesome-ratatui]
|
||||
|
||||
## Text and styling
|
||||
|
||||
The [`Text`], [`Line`] and [`Span`] types are the building blocks of the library and are used in
|
||||
many places. [`Text`] is a list of [`Line`]s and a [`Line`] is a list of [`Span`]s. A [`Span`]
|
||||
is a string with a specific style.
|
||||
|
||||
The [`style` module] provides types that represent the various styling options. The most
|
||||
important one is [`Style`] which represents the foreground and background colors and the text
|
||||
attributes of a [`Span`]. The [`style` module] also provides a [`Stylize`] trait that allows
|
||||
short-hand syntax to apply a style to widgets and text. See the [Styling Text] section of the
|
||||
[Ratatui Website] for more info.
|
||||
|
||||
```rust
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, Paragraph},
|
||||
Frame,
|
||||
};
|
||||
|
||||
fn ui(frame: &mut Frame) {
|
||||
let areas = Layout::vertical([Constraint::Length(1); 4]).split(frame.size());
|
||||
|
||||
let line = Line::from(vec![
|
||||
Span::raw("Hello "),
|
||||
Span::styled(
|
||||
"World",
|
||||
Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::White)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
"!".red().on_light_yellow().italic(),
|
||||
]);
|
||||
frame.render_widget(line, areas[0]);
|
||||
|
||||
// using the short-hand syntax and implicit conversions
|
||||
let paragraph = Paragraph::new("Hello World!".red().on_white().bold());
|
||||
frame.render_widget(paragraph, areas[1]);
|
||||
|
||||
// style the whole widget instead of just the text
|
||||
let paragraph = Paragraph::new("Hello World!").style(Style::new().red().on_white());
|
||||
frame.render_widget(paragraph, areas[2]);
|
||||
|
||||
// use the simpler short-hand syntax
|
||||
let paragraph = Paragraph::new("Hello World!").blue().on_yellow();
|
||||
frame.render_widget(paragraph, areas[3]);
|
||||
}
|
||||
```
|
||||
|
||||
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/
|
||||
[Application Patterns]: https://ratatui.rs/concepts/application-patterns/
|
||||
[Hello World tutorial]: https://ratatui.rs/tutorials/hello-world/
|
||||
[Backends]: https://ratatui.rs/concepts/backends/
|
||||
[Widgets]: https://ratatui.rs/how-to/widgets/
|
||||
[Handling Events]: https://ratatui.rs/concepts/event-handling/
|
||||
[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
|
||||
[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
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[API Docs]: https://docs.rs/ratatui
|
||||
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
[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
|
||||
[`Layout`]: layout::Layout
|
||||
[`Text`]: text::Text
|
||||
[`Line`]: text::Line
|
||||
[`Span`]: text::Span
|
||||
[`Style`]: style::Style
|
||||
[`style` module]: style
|
||||
[`Stylize`]: style::Stylize
|
||||
[`Backend`]: backend::Backend
|
||||
[`backend` module]: backend
|
||||
[`crossterm::event`]: https://docs.rs/crossterm/latest/crossterm/event/index.html
|
||||
[Crate]: https://crates.io/crates/ratatui
|
||||
[Crossterm]: https://crates.io/crates/crossterm
|
||||
[Termion]: https://crates.io/crates/termion
|
||||
[Termwiz]: https://crates.io/crates/termwiz
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[GitHub Sponsors]: https://github.com/sponsors/ratatui
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&logoColor=E05D44&color=E05D44
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI Workflow]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3&logoColor=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
[Deps.rs Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?style=flat-square
|
||||
[Deps.rs]: https://deps.rs/repo/github/ratatui/ratatui
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/docsrs/ratatui?logo=rust&style=flat-square&logoColor=E05D44
|
||||
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
[Forum]: https://forum.ratatui.rs
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
|
||||
<!-- 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 🚀
|
||||
|
||||
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].
|
||||
|
||||
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.
|
||||
|
||||
## Widgets
|
||||
|
||||
### 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`!
|
||||
Check out the [showcase] section of the website, or the [awesome-ratatui] repository for a curated
|
||||
list of awesome apps and libraries built with Ratatui!
|
||||
|
||||
## Alternatives
|
||||
|
||||
You might want to checkout [Cursive](https://github.com/gyscos/Cursive) for an alternative solution
|
||||
to build text user interfaces in Rust.
|
||||
- [Cursive](https://crates.io/crates/cursive) - a ncurses-based TUI library.
|
||||
- [iocraft](https://crates.io/crates/iocraft) - a declarative TUI library.
|
||||
|
||||
## Acknowledgments
|
||||
## Contributing
|
||||
|
||||
Special thanks to [**Pavel Fomchenkov**](https://github.com/nawok) for his work in designing **an
|
||||
awesome logo** for the ratatui project and ratatui organization.
|
||||
[![Discord Badge]][Discord Server] [![Matrix Badge]][Matrix] [![Forum Badge]][Ratatui Forum]
|
||||
|
||||
Feel free to join our [Discord server](https://discord.gg/pMCEU9hNEj) for discussions and questions!
|
||||
There is also a [Matrix](https://matrix.org/) bridge available at
|
||||
[#ratatui:matrix.org](https://matrix.to/#/#ratatui:matrix.org). We have also recently launched the
|
||||
[Ratatui Forum].
|
||||
|
||||
We rely on GitHub for [bugs][Report a bug] and [feature requests][Request a Feature].
|
||||
|
||||
Please make sure you read the [contributing](./CONTRIBUTING.md) guidelines before [creating a pull
|
||||
request][Create a Pull Request].
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
Ratatui was forked from the [tui-rs] crate in 2023 in order to continue its development. None of
|
||||
this could be possible without [Florian Dehau] who originally created [tui-rs] which inspired many
|
||||
Rust TUIs.
|
||||
|
||||
Special thanks to [Pavel Fomchenkov] for his work in designing an awesome logo for the Ratatui
|
||||
project and organization.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](./LICENSE)
|
||||
This project is licensed under the [MIT License][License].
|
||||
|
||||
[Repo]: https://github.com/ratatui/ratatui
|
||||
[Ratatui Website]: https://ratatui.rs/
|
||||
[Ratatui Forum]: https://forum.ratatui.rs
|
||||
[Docs]: https://docs.rs/ratatui
|
||||
[Widget Examples]: https://github.com/ratatui/ratatui/tree/main/ratatui-widgets/examples
|
||||
[App Examples]: https://github.com/ratatui/ratatui/tree/main/examples
|
||||
[Changelog]: https://github.com/ratatui/ratatui/blob/main/CHANGELOG.md
|
||||
[git-cliff]: https://git-cliff.org
|
||||
[Conventional Commits]: https://www.conventionalcommits.org
|
||||
[Breaking Changes]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
[EuroRust 2024 talk]: https://www.youtube.com/watch?v=hWG51Mc1DlM
|
||||
[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
|
||||
[Contributing]: https://github.com/ratatui/ratatui/blob/main/CONTRIBUTING.md
|
||||
[Crate]: https://crates.io/crates/ratatui
|
||||
[tui-rs]: https://crates.io/crates/tui
|
||||
[Sponsors]: https://github.com/sponsors/ratatui
|
||||
[Crate Badge]: https://img.shields.io/crates/v/ratatui?logo=rust&style=flat-square&color=E05D44
|
||||
[Repo Badge]: https://img.shields.io/badge/repo-ratatui/ratatui-1370D3?style=flat-square&logo=github
|
||||
[License Badge]: https://img.shields.io/crates/l/ratatui?style=flat-square&color=1370D3
|
||||
[CI Badge]: https://img.shields.io/github/actions/workflow/status/ratatui/ratatui/ci.yml?style=flat-square&logo=github
|
||||
[CI]: https://github.com/ratatui/ratatui/actions/workflows/ci.yml
|
||||
[Codecov Badge]: https://img.shields.io/codecov/c/github/ratatui/ratatui?logo=codecov&style=flat-square&token=BAQ8SOKEST&color=C43AC3
|
||||
[Codecov]: https://app.codecov.io/gh/ratatui/ratatui
|
||||
[Deps Badge]: https://deps.rs/repo/github/ratatui/ratatui/status.svg?path=ratatui&style=flat-square
|
||||
[Deps]: https://deps.rs/repo/github/ratatui/ratatui?path=ratatui
|
||||
[Discord Badge]: https://img.shields.io/discord/1070692720437383208?label=discord&logo=discord&style=flat-square&color=1370D3&logoColor=1370D3
|
||||
[Discord Server]: https://discord.gg/pMCEU9hNEj
|
||||
[Docs Badge]: https://img.shields.io/badge/docs-ratatui-1370D3?style=flat-square&logo=rust
|
||||
[Matrix Badge]: https://img.shields.io/matrix/ratatui-general%3Amatrix.org?style=flat-square&logo=matrix&label=Matrix&color=C43AC3
|
||||
[Matrix]: https://matrix.to/#/#ratatui:matrix.org
|
||||
[Forum Badge]: https://img.shields.io/discourse/likes?server=https%3A%2F%2Fforum.ratatui.rs&style=flat-square&logo=discourse&label=forum&color=C43AC3
|
||||
[Sponsors Badge]: https://img.shields.io/github/sponsors/ratatui?logo=github&style=flat-square&color=1370D3
|
||||
[templates]: https://github.com/ratatui/templates/
|
||||
[showcase]: https://ratatui.rs/showcase/
|
||||
[awesome-ratatui]: https://github.com/ratatui/awesome-ratatui
|
||||
[Pavel Fomchenkov]: https://github.com/nawok
|
||||
[Florian Dehau]: https://github.com/fdehau
|
||||
[`cargo-generate`]: https://crates.io/crates/cargo-generate
|
||||
[License]: ./LICENSE
|
||||
|
||||
80
bacon.toml
80
bacon.toml
@@ -8,101 +8,64 @@
|
||||
default_job = "check"
|
||||
|
||||
[jobs.check]
|
||||
command = ["cargo", "check", "--all-features", "--color", "always"]
|
||||
command = ["cargo", "xtask", "check"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-all]
|
||||
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"]
|
||||
command = ["cargo", "xtask", "check", "--all-features"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-crossterm]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"]
|
||||
command = ["cargo", "xtask", "check-crossterm"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termion]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"]
|
||||
command = ["cargo", "xtask", "check-termion"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.check-termwiz]
|
||||
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"]
|
||||
command = ["cargo", "xtask", "check-termwiz"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.clippy]
|
||||
command = [
|
||||
"cargo", "clippy",
|
||||
"--all-targets",
|
||||
"--color", "always",
|
||||
]
|
||||
[jobs.clippy-all]
|
||||
command = ["cargo", "xtask", "clippy"]
|
||||
need_stdout = false
|
||||
|
||||
[jobs.test]
|
||||
command = [
|
||||
"cargo", "test",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124
|
||||
]
|
||||
command = ["cargo", "xtask", "test"]
|
||||
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", "xtask", "test-libs"]
|
||||
need_stdout = true
|
||||
|
||||
[jobs.doc]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
command = ["cargo", "xtask", "docs"]
|
||||
need_stdout = false
|
||||
|
||||
# If the doc compiles, then it opens in your browser and bacon switches
|
||||
# to the previous job
|
||||
[jobs.doc-open]
|
||||
command = [
|
||||
"cargo", "+nightly", "doc",
|
||||
"-Zunstable-options", "-Zrustdoc-scrape-examples",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
"--no-deps",
|
||||
"--open",
|
||||
]
|
||||
env.RUSTDOCFLAGS = "--cfg docsrs"
|
||||
command = ["cargo", "xtask", "docs", "--open"]
|
||||
on_success = "job:doc"
|
||||
need_stdout = false
|
||||
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",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
command = ["cargo", "xtask", "coverage"]
|
||||
|
||||
[jobs.coverage-unit-tests-only]
|
||||
command = [
|
||||
"cargo", "llvm-cov",
|
||||
"--lcov", "--output-path", "target/lcov.info",
|
||||
"--lib",
|
||||
"--all-features",
|
||||
"--color", "always",
|
||||
]
|
||||
command = ["cargo", "xtask", "coverage-unit"]
|
||||
|
||||
[jobs.hack]
|
||||
command = ["cargo", "xtask", "hack"]
|
||||
|
||||
[jobs.format]
|
||||
command = ["cargo", "xtask", "format"]
|
||||
|
||||
# You may define here keybindings that would be specific to
|
||||
# a project, for example a shortcut to launch a specific job.
|
||||
# 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"
|
||||
@@ -110,3 +73,4 @@ v = "job:coverage"
|
||||
ctrl-v = "job:coverage-unit-tests-only"
|
||||
u = "job:test-unit"
|
||||
n = "job:nextest"
|
||||
f = "job:format"
|
||||
|
||||
@@ -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") %}
|
||||
|
||||
16
clippy.toml
16
clippy.toml
@@ -1,17 +1 @@
|
||||
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
|
||||
# crossterm -> all the windows- deps https://github.com/ratatui/ratatui/pull/1064#issuecomment-2078848980
|
||||
allowed-duplicate-crates = [
|
||||
"bitflags",
|
||||
"windows-targets",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
29
deny.toml
29
deny.toml
@@ -4,13 +4,16 @@
|
||||
version = 2
|
||||
confidence-threshold = 0.8
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
"Apache-2.0",
|
||||
"BSD-2-Clause",
|
||||
"BSD-3-Clause",
|
||||
"ISC",
|
||||
"MIT",
|
||||
"OpenSSL",
|
||||
"Unicode-3.0",
|
||||
"Unicode-DFS-2016",
|
||||
"WTFPL",
|
||||
"Zlib",
|
||||
]
|
||||
|
||||
[advisories]
|
||||
@@ -23,3 +26,15 @@ multiple-versions = "allow"
|
||||
unknown-registry = "deny"
|
||||
unknown-git = "warn"
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "ring"
|
||||
# SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses
|
||||
# https://spdx.org/licenses/OpenSSL.html
|
||||
# ISC - Both BoringSSL and ring use this for their new files
|
||||
# MIT - "Files in third_party/ have their own licenses, as described therein. The MIT
|
||||
# license, for third_party/fiat, which, unlike other third_party directories, is
|
||||
# compiled into non-test libraries, is included below."
|
||||
# OpenSSL - Obviously
|
||||
expression = "ISC AND MIT AND OpenSSL"
|
||||
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
# Examples
|
||||
|
||||
This folder might use unreleased code. View the examples for the latest release instead.
|
||||
This folder contains examples that are more app focused. There are also other examples in example
|
||||
folders under each crate folder e.g. [ratatui examples], [ratatui-widgets examples].
|
||||
|
||||
[ratatui examples]: ../ratatui/examples
|
||||
[ratatui-widgets examples]: ../ratatui-widgets/examples
|
||||
|
||||
You can run these examples using:
|
||||
|
||||
```shell
|
||||
cargo run -p example-name
|
||||
```
|
||||
|
||||
This folder might use unreleased code. Consider viewing the examples in the `latest` branch instead
|
||||
of the `main` branch for code which is guaranteed to work with the released ratatui version.
|
||||
|
||||
> [!WARNING]
|
||||
>
|
||||
@@ -11,7 +24,7 @@ This folder might use unreleased code. View the examples for the latest release
|
||||
>
|
||||
> - View the examples as they were when the latest version was release by selecting the tag that
|
||||
> matches that version. E.g. <https://github.com/ratatui/ratatui/tree/v0.26.1/examples>.
|
||||
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
|
||||
> - If you're viewing this file on GitHub, there is a combo box at the top of this page which
|
||||
> allows you to select any previous tagged version.
|
||||
> - To view the code locally, checkout the tag. E.g. `git switch --detach v0.26.1`.
|
||||
> - Use the latest [alpha version of Ratatui] in your app. These are released weekly on Saturdays.
|
||||
@@ -23,456 +36,31 @@ This folder might use unreleased code. View the examples for the latest release
|
||||
> We don't keep the CHANGELOG updated with unreleased changes, check the git commit history or run
|
||||
> `git-cliff -u` against a cloned version of this repository.
|
||||
|
||||
## Design choices
|
||||
## Demo
|
||||
|
||||
The examples contain some opinionated choices in order to make it easier for newer rustaceans to
|
||||
easily be productive in creating applications:
|
||||
This is the original demo example from the main README. It is available for each of the backends.
|
||||
[Source](./apps/demo/).
|
||||
|
||||
- Each example has an App struct, with methods that implement a main loop, handle events and drawing
|
||||
the UI.
|
||||
- We use color_eyre for handling errors and panics. See [How to use color-eyre with Ratatui] on the
|
||||
website for more information about this.
|
||||
- Common code is not extracted into a separate file. This makes each example self-contained and easy
|
||||
to read as a whole.
|
||||
|
||||
Not every example has been updated with all these points in mind yet, however over time they will
|
||||
be. None of the above choices are strictly necessary for Ratatui apps, but these choices make
|
||||
examples easier to run, maintain and explain. These choices are designed to help newer users fall
|
||||
into the pit of success when incorporating example code into their own apps. We may also eventually
|
||||
move some of these design choices into the core of Ratatui to simplify apps.
|
||||
|
||||
[How to use color-eyre with Ratatui]: https://ratatui.rs/how-to/develop-apps/color_eyre/
|
||||

|
||||
|
||||
## Demo2
|
||||
|
||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||
This is the demo example from the main README and crate page. [Source](./apps/demo2/).
|
||||
|
||||
```shell
|
||||
cargo run --example=demo2 --features="crossterm widget-calendar"
|
||||
```
|
||||

|
||||
|
||||
![Demo2][demo2.gif]
|
||||
## Mouse Drawing demo
|
||||
|
||||
## Demo
|
||||
Shows how to handle mouse events. [Source](./apps/mouse-drawing/).
|
||||
|
||||
This is the previous demo example from the main README. It is available for each of the backends. Source:
|
||||
[demo.rs](./demo/).
|
||||
## Async GitHub demo
|
||||
|
||||
```shell
|
||||
cargo run --example=demo --features=crossterm
|
||||
cargo run --example=demo --no-default-features --features=termion
|
||||
cargo run --example=demo --no-default-features --features=termwiz
|
||||
```
|
||||
Shows how to fetch data from GitHub API asynchronously. [Source](./apps/async-github/).
|
||||
|
||||
![Demo][demo.gif]
|
||||
## Weather demo
|
||||
|
||||
## Hello World
|
||||
Shows how to render weather data using barchart widget. [Source](./apps/weather/).
|
||||
|
||||
This is a pretty boring example, but it contains some good documentation
|
||||
on writing tui apps. Source: [hello_world.rs](./hello_world.rs).
|
||||
## Calendar explorer demo
|
||||
|
||||
```shell
|
||||
cargo run --example=hello_world --features=crossterm
|
||||
```
|
||||
|
||||
![Hello World][hello_world.gif]
|
||||
|
||||
## Barchart
|
||||
|
||||
Demonstrates the [`BarChart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
widget. Source: [barchart.rs](./barchart.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=barchart --features=crossterm
|
||||
```
|
||||
|
||||
![Barchart][barchart.gif]
|
||||
|
||||
## Barchart (Grouped)
|
||||
|
||||
Demonstrates the [`BarChart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html)
|
||||
widget with groups. Source: [barchart-grouped.rs](./barchart-grouped.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=barchart-grouped --features=crossterm
|
||||
```
|
||||
|
||||
![Barchart Grouped][barchart-grouped.gif]
|
||||
|
||||
## Block
|
||||
|
||||
Demonstrates the [`Block`](https://docs.rs/ratatui/latest/ratatui/widgets/block/struct.Block.html)
|
||||
widget. Source: [block.rs](./block.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=block --features=crossterm
|
||||
```
|
||||
|
||||
![Block][block.gif]
|
||||
|
||||
## Calendar
|
||||
|
||||
Demonstrates the [`Calendar`](https://docs.rs/ratatui/latest/ratatui/widgets/calendar/index.html)
|
||||
widget. Source: [calendar.rs](./calendar.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=calendar --features="crossterm widget-calendar"
|
||||
```
|
||||
|
||||
![Calendar][calendar.gif]
|
||||
|
||||
## Canvas
|
||||
|
||||
Demonstrates the [`Canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) widget
|
||||
and related shapes in the
|
||||
[`canvas`](https://docs.rs/ratatui/latest/ratatui/widgets/canvas/index.html) module. Source:
|
||||
[canvas.rs](./canvas.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=canvas --features=crossterm
|
||||
```
|
||||
|
||||
![Canvas][canvas.gif]
|
||||
|
||||
## Chart
|
||||
|
||||
Demonstrates the [`Chart`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Chart.html) widget.
|
||||
Source: [chart.rs](./chart.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=chart --features=crossterm
|
||||
```
|
||||
|
||||
![Chart][chart.gif]
|
||||
|
||||
## Colors
|
||||
|
||||
Demonstrates the available [`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html)
|
||||
options. These can be used in any style field. Source: [colors.rs](./colors.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=colors --features=crossterm
|
||||
```
|
||||
|
||||
![Colors][colors.gif]
|
||||
|
||||
## Colors (RGB)
|
||||
|
||||
Demonstrates the available RGB
|
||||
[`Color`](https://docs.rs/ratatui/latest/ratatui/style/enum.Color.html) options. These can be used
|
||||
in any style field. Source: [colors_rgb.rs](./colors_rgb.rs). Uses a half block technique to render
|
||||
two square-ish pixels in the space of a single rectangular terminal cell.
|
||||
|
||||
```shell
|
||||
cargo run --example=colors_rgb --features=crossterm
|
||||
```
|
||||
|
||||
Note: VHs renders full screen animations poorly, so this is a screen capture rather than the output
|
||||
of the VHS tape.
|
||||
|
||||
<https://github.com/ratatui/ratatui/assets/381361/485e775a-e0b5-4133-899b-1e8aeb56e774>
|
||||
|
||||
## Constraint Explorer
|
||||
|
||||
Demonstrates the behaviour of each
|
||||
[`Constraint`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Constraint.html) option with
|
||||
respect to each other across different `Flex` modes.
|
||||
|
||||
```shell
|
||||
cargo run --example=constraint-explorer --features=crossterm
|
||||
```
|
||||
|
||||
![Constraint Explorer][constraint-explorer.gif]
|
||||
|
||||
## Constraints
|
||||
|
||||
Demonstrates how to use
|
||||
[`Constraint`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Constraint.html) options for
|
||||
defining layout element sizes.
|
||||
|
||||
![Constraints][constraints.gif]
|
||||
|
||||
```shell
|
||||
cargo run --example=constraints --features=crossterm
|
||||
```
|
||||
|
||||
## Custom Widget
|
||||
|
||||
Demonstrates how to implement the
|
||||
[`Widget`](https://docs.rs/ratatui/latest/ratatui/widgets/trait.Widget.html) trait. Also shows mouse
|
||||
interaction. Source: [custom_widget.rs](./custom_widget.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=custom_widget --features=crossterm
|
||||
```
|
||||
|
||||
![Custom Widget][custom_widget.gif]
|
||||
|
||||
## Gauge
|
||||
|
||||
Demonstrates the [`Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Gauge.html) widget.
|
||||
Source: [gauge.rs](./gauge.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=gauge --features=crossterm
|
||||
```
|
||||
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Flex
|
||||
|
||||
Demonstrates the different [`Flex`](https://docs.rs/ratatui/latest/ratatui/layout/enum.Flex.html)
|
||||
modes for controlling layout space distribution.
|
||||
|
||||
```shell
|
||||
cargo run --example=flex --features=crossterm
|
||||
```
|
||||
|
||||
![Flex][flex.gif]
|
||||
|
||||
## Line Gauge
|
||||
|
||||
Demonstrates the [`Line
|
||||
Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.LineGauge.html) widget. Source:
|
||||
[line_gauge.rs](./line_gauge.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=line_gauge --features=crossterm
|
||||
```
|
||||
|
||||
![LineGauge][line_gauge.gif]
|
||||
|
||||
## Hyperlink
|
||||
|
||||
Demonstrates how to use OSC 8 to create hyperlinks in the terminal.
|
||||
|
||||
```shell
|
||||
cargo run --example=hyperlink --features="crossterm unstable-widget-ref"
|
||||
```
|
||||
|
||||
![Hyperlink][hyperlink.gif]
|
||||
|
||||
## Inline
|
||||
|
||||
Demonstrates how to use the
|
||||
[`Inline`](https://docs.rs/ratatui/latest/ratatui/terminal/enum.Viewport.html#variant.Inline)
|
||||
Viewport mode for ratatui apps. Source: [inline.rs](./inline.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=inline --features=crossterm
|
||||
```
|
||||
|
||||
![Inline][inline.gif]
|
||||
|
||||
## Layout
|
||||
|
||||
Demonstrates the [`Layout`](https://docs.rs/ratatui/latest/ratatui/layout/struct.Layout.html) and
|
||||
interaction between each constraint. Source: [layout.rs](./layout.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=layout --features=crossterm
|
||||
```
|
||||
|
||||
![Layout][layout.gif]
|
||||
|
||||
## List
|
||||
|
||||
Demonstrates the [`List`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.List.html) widget.
|
||||
Source: [list.rs](./list.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=list --features=crossterm
|
||||
```
|
||||
|
||||
![List][list.gif]
|
||||
|
||||
## Modifiers
|
||||
|
||||
Demonstrates the style
|
||||
[`Modifiers`](https://docs.rs/ratatui/latest/ratatui/style/struct.Modifier.html). Source:
|
||||
[modifiers.rs](./modifiers.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=modifiers --features=crossterm
|
||||
```
|
||||
|
||||
![Modifiers][modifiers.gif]
|
||||
|
||||
## Minimal
|
||||
|
||||
Demonstrates how to create a minimal `Hello World!` program.
|
||||
|
||||
```shell
|
||||
cargo run --example=minimal --features=crossterm
|
||||
```
|
||||
|
||||
![Minimal][minimal.gif]
|
||||
|
||||
## Panic
|
||||
|
||||
Demonstrates how to handle panics by ensuring that panic messages are written correctly to the
|
||||
screen. Source: [panic.rs](./panic.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=panic --features=crossterm
|
||||
```
|
||||
|
||||
![Panic][panic.gif]
|
||||
|
||||
## Paragraph
|
||||
|
||||
Demonstrates the [`Paragraph`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Paragraph.html)
|
||||
widget. Source: [paragraph.rs](./paragraph.rs)
|
||||
|
||||
```shell
|
||||
cargo run --example=paragraph --features=crossterm
|
||||
```
|
||||
|
||||
![Paragraph][paragraph.gif]
|
||||
|
||||
## Popup
|
||||
|
||||
Demonstrates how to render a widget over the top of previously rendered widgets using the
|
||||
[`Clear`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Clear.html) widget. Source:
|
||||
[popup.rs](./popup.rs).
|
||||
|
||||
>
|
||||
```shell
|
||||
cargo run --example=popup --features=crossterm
|
||||
```
|
||||
|
||||
![Popup][popup.gif]
|
||||
|
||||
## Ratatui-logo
|
||||
|
||||
A fun example of using half blocks to render graphics Source:
|
||||
[ratatui-logo.rs](./ratatui-logo.rs).
|
||||
|
||||
>
|
||||
```shell
|
||||
cargo run --example=ratatui-logo --features=crossterm
|
||||
```
|
||||
|
||||
![Ratatui Logo][ratatui-logo.gif]
|
||||
|
||||
## Scrollbar
|
||||
|
||||
Demonstrates the [`Scrollbar`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Scrollbar.html)
|
||||
widget. Source: [scrollbar.rs](./scrollbar.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=scrollbar --features=crossterm
|
||||
```
|
||||
|
||||
![Scrollbar][scrollbar.gif]
|
||||
|
||||
## Sparkline
|
||||
|
||||
Demonstrates the [`Sparkline`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Sparkline.html)
|
||||
widget. Source: [sparkline.rs](./sparkline.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=sparkline --features=crossterm
|
||||
```
|
||||
|
||||
![Sparkline][sparkline.gif]
|
||||
|
||||
## Table
|
||||
|
||||
Demonstrates the [`Table`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Table.html) widget.
|
||||
Source: [table.rs](./table.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=table --features=crossterm
|
||||
```
|
||||
|
||||
![Table][table.gif]
|
||||
|
||||
## Tabs
|
||||
|
||||
Demonstrates the [`Tabs`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.Tabs.html) widget.
|
||||
Source: [tabs.rs](./tabs.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=tabs --features=crossterm
|
||||
```
|
||||
|
||||
![Tabs][tabs.gif]
|
||||
|
||||
## Tracing
|
||||
|
||||
Demonstrates how to use the [tracing crate](https://crates.io/crates/tracing) for logging. Creates
|
||||
a file named `tracing.log` in the current directory.
|
||||
|
||||
```shell
|
||||
cargo run --example=tracing --features=crossterm
|
||||
```
|
||||
|
||||
![Tracing][tracing.gif]
|
||||
|
||||
## User Input
|
||||
|
||||
Demonstrates one approach to accepting user input. Source [user_input.rs](./user_input.rs).
|
||||
|
||||
> [!NOTE]
|
||||
> Consider using [`tui-textarea`](https://crates.io/crates/tui-textarea) or
|
||||
> [`tui-input`](https://crates.io/crates/tui-input) crates for more functional text entry UIs.
|
||||
|
||||
```shell
|
||||
cargo run --example=user_input --features=crossterm
|
||||
```
|
||||
|
||||
![User Input][user_input.gif]
|
||||
|
||||
## How to update these examples
|
||||
|
||||
These gifs were created using [VHS](https://github.com/charmbracelet/vhs). Each example has a
|
||||
corresponding `.tape` file that holds instructions for how to generate the images. Note that the
|
||||
images themselves are stored in a separate `images` git branch to avoid bloating the main
|
||||
repository.
|
||||
|
||||
<!--
|
||||
|
||||
Links to images to make them easier to update in bulk. Use the following script to update and upload
|
||||
the examples to the images branch. (Requires push access to the branch).
|
||||
|
||||
```shell
|
||||
examples/vhs/generate.bash
|
||||
```
|
||||
-->
|
||||
|
||||
[barchart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/barchart.gif?raw=true
|
||||
[barchart-grouped.gif]: https://github.com/ratatui/ratatui/blob/images/examples/barchart-grouped.gif?raw=true
|
||||
[block.gif]: https://github.com/ratatui/ratatui/blob/images/examples/block.gif?raw=true
|
||||
[calendar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/calendar.gif?raw=true
|
||||
[canvas.gif]: https://github.com/ratatui/ratatui/blob/images/examples/canvas.gif?raw=true
|
||||
[chart.gif]: https://github.com/ratatui/ratatui/blob/images/examples/chart.gif?raw=true
|
||||
[colors.gif]: https://github.com/ratatui/ratatui/blob/images/examples/colors.gif?raw=true
|
||||
[constraint-explorer.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraint-explorer.gif?raw=true
|
||||
[constraints.gif]: https://github.com/ratatui/ratatui/blob/images/examples/constraints.gif?raw=true
|
||||
[custom_widget.gif]: https://github.com/ratatui/ratatui/blob/images/examples/custom_widget.gif?raw=true
|
||||
[demo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo.gif?raw=true
|
||||
[demo2.gif]: https://github.com/ratatui/ratatui/blob/images/examples/demo2.gif?raw=true
|
||||
[flex.gif]: https://github.com/ratatui/ratatui/blob/images/examples/flex.gif?raw=true
|
||||
[gauge.gif]: https://github.com/ratatui/ratatui/blob/images/examples/gauge.gif?raw=true
|
||||
[hello_world.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hello_world.gif?raw=true
|
||||
[hyperlink.gif]: https://github.com/ratatui/ratatui/blob/images/examples/hyperlink.gif?raw=true
|
||||
[inline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
[layout.gif]: https://github.com/ratatui/ratatui/blob/images/examples/layout.gif?raw=true
|
||||
[list.gif]: https://github.com/ratatui/ratatui/blob/images/examples/list.gif?raw=true
|
||||
[line_gauge.gif]: https://github.com/ratatui/ratatui/blob/images/examples/line_gauge.gif?raw=true
|
||||
[minimal.gif]: https://github.com/ratatui/ratatui/blob/images/examples/minimal.gif?raw=true
|
||||
[modifiers.gif]: https://github.com/ratatui/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||
[panic.gif]: https://github.com/ratatui/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[paragraph.gif]: https://github.com/ratatui/ratatui/blob/images/examples/paragraph.gif?raw=true
|
||||
[popup.gif]: https://github.com/ratatui/ratatui/blob/images/examples/popup.gif?raw=true
|
||||
[ratatui-logo.gif]: https://github.com/ratatui/ratatui/blob/images/examples/ratatui-logo.gif?raw=true
|
||||
[scrollbar.gif]: https://github.com/ratatui/ratatui/blob/images/examples/scrollbar.gif?raw=true
|
||||
[sparkline.gif]: https://github.com/ratatui/ratatui/blob/images/examples/sparkline.gif?raw=true
|
||||
[table.gif]: https://vhs.charm.sh/vhs-6njXBytDf0rwPufUtmSSpI.gif
|
||||
[tabs.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tabs.gif?raw=true
|
||||
[tracing.gif]: https://github.com/ratatui/ratatui/blob/images/examples/tracing.gif?raw=true
|
||||
[user_input.gif]: https://github.com/ratatui/ratatui/blob/images/examples/user_input.gif?raw=true
|
||||
|
||||
[alpha version of Ratatui]: https://crates.io/crates/ratatui/versions
|
||||
[BREAKING-CHANGES.md]: https://github.com/ratatui/ratatui/blob/main/BREAKING-CHANGES.md
|
||||
Shows how to render a calendar with different styles. [Source](./apps/calendar-explorer/).
|
||||
|
||||
22
examples/apps/async-github/Cargo.toml
Normal file
22
examples/apps/async-github/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "async-github"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
documentation.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
readme.workspace = true
|
||||
license.workspace = true
|
||||
exclude.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.3"
|
||||
crossterm = { workspace = true, features = ["event-stream"] }
|
||||
octocrab = "0.42.1"
|
||||
ratatui.workspace = true
|
||||
tokio = { version = "1.43.0", features = ["rt-multi-thread", "macros"] }
|
||||
tokio-stream = "0.1.17"
|
||||
9
examples/apps/async-github/README.md
Normal file
9
examples/apps/async-github/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Async GitHub demo
|
||||
|
||||
This example demonstrates how to use Ratatui with widgets that fetch data from GitHub API asynchronously.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p async-github
|
||||
```
|
||||
@@ -1,9 +1,7 @@
|
||||
//! # [Ratatui] Async example
|
||||
//!
|
||||
//! This example demonstrates how to use Ratatui with widgets that fetch data asynchronously. It
|
||||
//! uses the `octocrab` crate to fetch a list of pull requests from the GitHub API. You will need an
|
||||
//! environment variable named `GITHUB_TOKEN` with a valid GitHub personal access token. The token
|
||||
//! does not need any special permissions.
|
||||
//! uses the `octocrab` crate to fetch a list of pull requests from the GitHub API.
|
||||
//!
|
||||
//! <https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token>
|
||||
//! <https://github.com/settings/tokens/new> to create a new token (select classic, and no scopes)
|
||||
@@ -34,11 +32,10 @@ use std::{
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use color_eyre::{eyre::Context, Result, Section};
|
||||
use futures::StreamExt;
|
||||
use color_eyre::Result;
|
||||
use octocrab::{
|
||||
params::{pulls::Sort, Direction},
|
||||
OctocrabBuilder, Page,
|
||||
Page,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
@@ -49,29 +46,17 @@ use ratatui::{
|
||||
widgets::{Block, HighlightSpacing, Row, StatefulWidget, Table, TableState, Widget},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
init_octocrab()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal).await;
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
fn init_octocrab() -> Result<()> {
|
||||
let token = std::env::var("GITHUB_TOKEN")
|
||||
.wrap_err("The GITHUB_TOKEN environment variable was not found")
|
||||
.suggestion(
|
||||
"Go to https://github.com/settings/tokens/new to create a token, and re-run:
|
||||
GITHUB_TOKEN=ghp_... cargo run --example async --features crossterm",
|
||||
)?;
|
||||
let crab = OctocrabBuilder::new().personal_token(token).build()?;
|
||||
octocrab::initialise(crab);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct App {
|
||||
should_quit: bool,
|
||||
@@ -244,7 +229,7 @@ impl Widget for &PullRequestListWidget {
|
||||
.block(block)
|
||||
.highlight_spacing(HighlightSpacing::Always)
|
||||
.highlight_symbol(">>")
|
||||
.highlight_style(Style::new().on_blue());
|
||||
.row_highlight_style(Style::new().on_blue());
|
||||
|
||||
StatefulWidget::render(table, area, buf, &mut state.table_state);
|
||||
}
|
||||
15
examples/apps/calendar-explorer/Cargo.toml
Normal file
15
examples/apps/calendar-explorer/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "calendar-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
time = { version = "0.3.37", features = ["formatting", "parsing"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
9
examples/apps/calendar-explorer/README.md
Normal file
9
examples/apps/calendar-explorer/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Calendar explorer demo
|
||||
|
||||
This example shows how to render a calendar with different styles.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p calendar-explorer
|
||||
```
|
||||
248
examples/apps/calendar-explorer/src/main.rs
Normal file
248
examples/apps/calendar-explorer/src/main.rs
Normal file
@@ -0,0 +1,248 @@
|
||||
//! A Ratatui example that demonstrates how to render calendar with different styles.
|
||||
//!
|
||||
//! Marks the holidays and seasons on the calendar.
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
text::{Line, Text},
|
||||
widgets::calendar::{CalendarEventStore, Monthly},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use time::{ext::NumericalDuration, Date, Month, OffsetDateTime};
|
||||
|
||||
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<()> {
|
||||
let mut selected_date = OffsetDateTime::now_local()?.date();
|
||||
let mut calendar_style = StyledCalendar::Default;
|
||||
loop {
|
||||
terminal.draw(|frame| render(frame, calendar_style, selected_date))?;
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break Ok(()),
|
||||
KeyCode::Char('s') => calendar_style = calendar_style.next(),
|
||||
KeyCode::Char('n') | KeyCode::Tab => selected_date = next_month(selected_date),
|
||||
KeyCode::Char('p') | KeyCode::BackTab => {
|
||||
selected_date = previous_month(selected_date);
|
||||
}
|
||||
KeyCode::Char('h') | KeyCode::Left => selected_date -= 1.days(),
|
||||
KeyCode::Char('j') | KeyCode::Down => selected_date += 1.weeks(),
|
||||
KeyCode::Char('k') | KeyCode::Up => selected_date -= 1.weeks(),
|
||||
KeyCode::Char('l') | KeyCode::Right => selected_date += 1.days(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn next_month(date: Date) -> Date {
|
||||
if date.month() == Month::December {
|
||||
date.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_year(date.year() + 1)
|
||||
.unwrap()
|
||||
} else {
|
||||
date.replace_month(date.month().next()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn previous_month(date: Date) -> Date {
|
||||
if date.month() == Month::January {
|
||||
date.replace_month(Month::December)
|
||||
.unwrap()
|
||||
.replace_year(date.year() - 1)
|
||||
.unwrap()
|
||||
} else {
|
||||
date.replace_month(date.month().previous()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the UI with a calendar.
|
||||
fn render(frame: &mut Frame, calendar_style: StyledCalendar, selected_date: Date) {
|
||||
let header = Text::from_iter([
|
||||
Line::from("Calendar Example".bold()),
|
||||
Line::from(
|
||||
"<q> Quit | <s> Change Style | <n> Next Month | <p> Previous Month, <hjkl> Move",
|
||||
),
|
||||
Line::from(format!(
|
||||
"Current date: {selected_date} | Current style: {calendar_style}"
|
||||
)),
|
||||
]);
|
||||
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Fill(1),
|
||||
]);
|
||||
let [text_area, area] = vertical.areas(frame.area());
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
calendar_style
|
||||
.render_year(frame, area, selected_date)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum StyledCalendar {
|
||||
Default,
|
||||
Surrounding,
|
||||
WeekdaysHeader,
|
||||
SurroundingAndWeekdaysHeader,
|
||||
MonthHeader,
|
||||
MonthAndWeekdaysHeader,
|
||||
}
|
||||
|
||||
impl StyledCalendar {
|
||||
// Cycle through the different styles.
|
||||
const fn next(self) -> Self {
|
||||
match self {
|
||||
Self::Default => Self::Surrounding,
|
||||
Self::Surrounding => Self::WeekdaysHeader,
|
||||
Self::WeekdaysHeader => Self::SurroundingAndWeekdaysHeader,
|
||||
Self::SurroundingAndWeekdaysHeader => Self::MonthHeader,
|
||||
Self::MonthHeader => Self::MonthAndWeekdaysHeader,
|
||||
Self::MonthAndWeekdaysHeader => Self::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for StyledCalendar {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Default => write!(f, "Default"),
|
||||
Self::Surrounding => write!(f, "Show Surrounding"),
|
||||
Self::WeekdaysHeader => write!(f, "Show Weekdays Header"),
|
||||
Self::SurroundingAndWeekdaysHeader => write!(f, "Show Surrounding and Weekdays Header"),
|
||||
Self::MonthHeader => write!(f, "Show Month Header"),
|
||||
Self::MonthAndWeekdaysHeader => write!(f, "Show Month Header and Weekdays Header"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StyledCalendar {
|
||||
fn render_year(self, frame: &mut Frame, area: Rect, date: Date) -> Result<()> {
|
||||
let events = events(date)?;
|
||||
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
});
|
||||
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
|
||||
let areas = rows.iter().flat_map(|row| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
||||
.split(*row)
|
||||
.to_vec()
|
||||
});
|
||||
for (i, area) in areas.enumerate() {
|
||||
let month = date
|
||||
.replace_day(1)
|
||||
.unwrap()
|
||||
.replace_month(Month::try_from(i as u8 + 1).unwrap())
|
||||
.unwrap();
|
||||
self.render_month(frame, area, month, &events);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_month(self, frame: &mut Frame, area: Rect, date: Date, events: &CalendarEventStore) {
|
||||
let calendar = match self {
|
||||
Self::Default => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default()),
|
||||
Self::Surrounding => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_surrounding(Style::new().dim()),
|
||||
Self::WeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::new().bold().green()),
|
||||
Self::SurroundingAndWeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_surrounding(Style::new().dim())
|
||||
.show_weekdays_header(Style::new().bold().green()),
|
||||
Self::MonthHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_month_header(Style::new().bold().green()),
|
||||
Self::MonthAndWeekdaysHeader => Monthly::new(date, events)
|
||||
.default_style(Style::new().bold().bg(Color::Rgb(50, 50, 50)))
|
||||
.show_month_header(Style::default())
|
||||
.show_weekdays_header(Style::new().bold().dim().light_yellow()),
|
||||
};
|
||||
frame.render_widget(calendar, area);
|
||||
}
|
||||
}
|
||||
|
||||
/// Makes a list of dates for the current year.
|
||||
fn events(selected_date: Date) -> Result<CalendarEventStore> {
|
||||
const SELECTED: Style = Style::new()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Red)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
const HOLIDAY: Style = Style::new()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
const SEASON: Style = Style::new()
|
||||
.fg(Color::Green)
|
||||
.bg(Color::Black)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
let y = selected_date.year();
|
||||
|
||||
// new year's
|
||||
list.add(Date::from_calendar_date(y, Month::January, 1)?, HOLIDAY);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(Date::from_calendar_date(y + 1, Month::January, 1)?, HOLIDAY);
|
||||
// groundhog day
|
||||
list.add(Date::from_calendar_date(y, Month::February, 2)?, HOLIDAY);
|
||||
// april fool's
|
||||
list.add(Date::from_calendar_date(y, Month::April, 1)?, HOLIDAY);
|
||||
// earth day
|
||||
list.add(Date::from_calendar_date(y, Month::April, 22)?, HOLIDAY);
|
||||
// star wars day
|
||||
list.add(Date::from_calendar_date(y, Month::May, 4)?, HOLIDAY);
|
||||
// festivus
|
||||
list.add(Date::from_calendar_date(y, Month::December, 23)?, HOLIDAY);
|
||||
// new year's eve
|
||||
list.add(Date::from_calendar_date(y, Month::December, 31)?, HOLIDAY);
|
||||
|
||||
// seasons
|
||||
// spring equinox
|
||||
list.add(Date::from_calendar_date(y, Month::March, 22)?, SEASON);
|
||||
// summer solstice
|
||||
list.add(Date::from_calendar_date(y, Month::June, 21)?, SEASON);
|
||||
// fall equinox
|
||||
list.add(Date::from_calendar_date(y, Month::September, 22)?, SEASON);
|
||||
// winter solstice
|
||||
list.add(Date::from_calendar_date(y, Month::December, 21)?, SEASON);
|
||||
|
||||
// selected date
|
||||
list.add(selected_date, SELECTED);
|
||||
|
||||
Ok(list)
|
||||
}
|
||||
15
examples/apps/canvas/Cargo.toml
Normal file
15
examples/apps/canvas/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "canvas"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
9
examples/apps/canvas/README.md
Normal file
9
examples/apps/canvas/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Canvas demo
|
||||
|
||||
This example shows how to render various shapes and a map on a canvas.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p canvas
|
||||
```
|
||||
@@ -1,28 +1,32 @@
|
||||
//! # [Ratatui] Canvas 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::time::{Duration, Instant};
|
||||
/// A Ratatui example that demonstrates how to draw on a canvas.
|
||||
///
|
||||
/// This example demonstrates how to draw various shapes such as rectangles, circles, and lines
|
||||
/// on a canvas. It also demonstrates how to draw a map.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::{
|
||||
io::stdout,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, EnableMouseCapture, KeyEventKind},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
crossterm::event::{self, Event, KeyCode, MouseEventKind},
|
||||
layout::{Constraint, Layout, Position, Rect},
|
||||
style::{Color, Stylize},
|
||||
symbols::Marker,
|
||||
text::Text,
|
||||
widgets::{
|
||||
canvas::{Canvas, Circle, Map, MapResolution, Rectangle},
|
||||
canvas::{Canvas, Circle, Map, MapResolution, Points, Rectangle},
|
||||
Block, Widget,
|
||||
},
|
||||
DefaultTerminal, Frame,
|
||||
@@ -30,26 +34,31 @@ use ratatui::{
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
stdout().execute(EnableMouseCapture)?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
stdout().execute(DisableMouseCapture)?;
|
||||
app_result
|
||||
}
|
||||
|
||||
struct App {
|
||||
exit: bool,
|
||||
x: f64,
|
||||
y: f64,
|
||||
ball: Circle,
|
||||
playground: Rect,
|
||||
vx: f64,
|
||||
vy: f64,
|
||||
tick_count: u64,
|
||||
marker: Marker,
|
||||
points: Vec<Position>,
|
||||
is_drawing: bool,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
exit: false,
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
ball: Circle {
|
||||
@@ -61,27 +70,23 @@ impl App {
|
||||
playground: Rect::new(10, 10, 200, 100),
|
||||
vx: 1.0,
|
||||
vy: 1.0,
|
||||
tick_count: 0,
|
||||
marker: Marker::Dot,
|
||||
points: vec![],
|
||||
is_drawing: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(16);
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
while !self.exit {
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => break Ok(()),
|
||||
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
|
||||
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
|
||||
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
|
||||
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
|
||||
_ => {}
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) => self.handle_key_press(key),
|
||||
Event::Mouse(event) => self.handle_mouse_event(event),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,20 +95,44 @@ impl App {
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_key_press(&mut self, key: event::KeyEvent) {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
return;
|
||||
}
|
||||
match key.code {
|
||||
KeyCode::Char('q') => self.exit = true,
|
||||
KeyCode::Down | KeyCode::Char('j') => self.y += 1.0,
|
||||
KeyCode::Up | KeyCode::Char('k') => self.y -= 1.0,
|
||||
KeyCode::Right | KeyCode::Char('l') => self.x += 1.0,
|
||||
KeyCode::Left | KeyCode::Char('h') => self.x -= 1.0,
|
||||
KeyCode::Enter => {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
};
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, event: event::MouseEvent) {
|
||||
match event.kind {
|
||||
MouseEventKind::Down(_) => self.is_drawing = true,
|
||||
MouseEventKind::Up(_) => self.is_drawing = false,
|
||||
MouseEventKind::Drag(_) => {
|
||||
self.points.push(Position::new(event.column, event.row));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
self.tick_count += 1;
|
||||
// only change marker every 180 ticks (3s) to avoid stroboscopic effect
|
||||
if (self.tick_count % 180) == 0 {
|
||||
self.marker = match self.marker {
|
||||
Marker::Dot => Marker::Braille,
|
||||
Marker::Braille => Marker::Block,
|
||||
Marker::Block => Marker::HalfBlock,
|
||||
Marker::HalfBlock => Marker::Bar,
|
||||
Marker::Bar => Marker::Dot,
|
||||
};
|
||||
}
|
||||
// bounce the ball by flipping the velocity vector
|
||||
let ball = &self.ball;
|
||||
let playground = self.playground;
|
||||
@@ -117,19 +146,31 @@ impl App {
|
||||
{
|
||||
self.vy = -self.vy;
|
||||
}
|
||||
|
||||
self.ball.x += self.vx;
|
||||
self.ball.y += self.vy;
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let header = Text::from_iter([
|
||||
"Canvas Example".bold(),
|
||||
"<q> Quit | <enter> Change Marker | <hjkl> Move".into(),
|
||||
]);
|
||||
|
||||
let vertical = Layout::vertical([
|
||||
Constraint::Length(header.height() as u16),
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]);
|
||||
let [text_area, up, down] = vertical.areas(frame.area());
|
||||
frame.render_widget(header.centered(), text_area);
|
||||
|
||||
let horizontal =
|
||||
Layout::horizontal([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let vertical = Layout::vertical([Constraint::Percentage(50), Constraint::Percentage(50)]);
|
||||
let [map, right] = horizontal.areas(frame.area());
|
||||
let [pong, boxes] = vertical.areas(right);
|
||||
let [draw, pong] = horizontal.areas(up);
|
||||
let [map, boxes] = horizontal.areas(down);
|
||||
|
||||
frame.render_widget(self.map_canvas(), map);
|
||||
frame.render_widget(self.draw_canvas(draw), draw);
|
||||
frame.render_widget(self.pong_canvas(), pong);
|
||||
frame.render_widget(self.boxes_canvas(boxes), boxes);
|
||||
}
|
||||
@@ -149,6 +190,30 @@ impl App {
|
||||
.y_bounds([-90.0, 90.0])
|
||||
}
|
||||
|
||||
fn draw_canvas(&self, area: Rect) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Draw here"))
|
||||
.marker(self.marker)
|
||||
.x_bounds([0.0, f64::from(area.width)])
|
||||
.y_bounds([0.0, f64::from(area.height)])
|
||||
.paint(move |ctx| {
|
||||
let points = self
|
||||
.points
|
||||
.iter()
|
||||
.map(|p| {
|
||||
(
|
||||
f64::from(p.x) - f64::from(area.left()),
|
||||
f64::from(area.bottom()) - f64::from(p.y),
|
||||
)
|
||||
})
|
||||
.collect_vec();
|
||||
ctx.draw(&Points {
|
||||
coords: &points,
|
||||
color: Color::White,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
fn pong_canvas(&self) -> impl Widget + '_ {
|
||||
Canvas::default()
|
||||
.block(Block::bordered().title("Pong"))
|
||||
14
examples/apps/chart/Cargo.toml
Normal file
14
examples/apps/chart/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "chart"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
9
examples/apps/chart/README.md
Normal file
9
examples/apps/chart/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Chart demo
|
||||
|
||||
This example shows how to render line, bar, and scatter charts.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p chart
|
||||
```
|
||||
@@ -1,28 +1,23 @@
|
||||
//! # [Ratatui] Chart 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
|
||||
|
||||
/// A Ratatui example that demonstrates how to handle charts.
|
||||
///
|
||||
/// This example demonstrates how to draw various types of charts such as line, bar, and
|
||||
/// scatter charts.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Modifier, Style, Stylize},
|
||||
symbols::{self, Marker},
|
||||
text::Span,
|
||||
widgets::{block::Title, Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||
text::{Line, Span},
|
||||
widgets::{Axis, Block, Chart, Dataset, GraphType, LegendPosition},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
@@ -196,13 +191,7 @@ fn render_barchart(frame: &mut Frame, bar_chart: Rect) {
|
||||
]);
|
||||
|
||||
let chart = Chart::new(vec![dataset])
|
||||
.block(
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Bar chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.block(Block::bordered().title_top(Line::from("Bar chart").cyan().bold().centered()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.style(Style::default().gray())
|
||||
@@ -229,13 +218,7 @@ fn render_line_chart(frame: &mut Frame, area: Rect) {
|
||||
.data(&[(1., 1.), (4., 4.)])];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Line chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.block(Block::bordered().title(Line::from("Line chart").cyan().bold().centered()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("X Axis")
|
||||
@@ -279,13 +262,7 @@ fn render_scatter(frame: &mut Frame, area: Rect) {
|
||||
];
|
||||
|
||||
let chart = Chart::new(datasets)
|
||||
.block(
|
||||
Block::bordered().title(
|
||||
Title::default()
|
||||
.content("Scatter chart".cyan().bold())
|
||||
.alignment(Alignment::Center),
|
||||
),
|
||||
)
|
||||
.block(Block::bordered().title(Line::from("Scatter chart").cyan().bold().centered()))
|
||||
.x_axis(
|
||||
Axis::default()
|
||||
.title("Year")
|
||||
15
examples/apps/color-explorer/Cargo.toml
Normal file
15
examples/apps/color-explorer/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "color-explorer"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
itertools.workspace = true
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
9
examples/apps/color-explorer/README.md
Normal file
9
examples/apps/color-explorer/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Color explorer demo
|
||||
|
||||
This example shows how to handle the supported colors.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p color-explorer
|
||||
```
|
||||
@@ -1,20 +1,12 @@
|
||||
//! # [Ratatui] Colors example
|
||||
//! A Ratatui example that demonstrates how to handle colors.
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//! This example shows all the colors supported by Ratatui. It will render a grid of foreground
|
||||
//! and background colors with their names and indexes.
|
||||
//!
|
||||
//! 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.
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! 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
|
||||
|
||||
// This example shows all the colors supported by ratatui. It will render a grid of foreground
|
||||
// and background colors with their names and indexes.
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
|
||||
use color_eyre::Result;
|
||||
use itertools::Itertools;
|
||||
17
examples/apps/demo/Cargo.toml
Normal file
17
examples/apps/demo/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "demo"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["crossterm"]
|
||||
crossterm = ["ratatui/crossterm"]
|
||||
termion = ["ratatui/termion"]
|
||||
termwiz = ["ratatui/termwiz"]
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.23", features = ["derive"] }
|
||||
rand = "0.8.5"
|
||||
ratatui.workspace = true
|
||||
25
examples/apps/demo/README.md
Normal file
25
examples/apps/demo/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Demo example
|
||||
|
||||
This is the original demo that was developed for Tui-rs (the library that Ratatui was forked from).
|
||||
|
||||

|
||||
|
||||
This example is available for each backend. To run it:
|
||||
|
||||
## crossterm
|
||||
|
||||
```shell
|
||||
cargo run -p demo
|
||||
```
|
||||
|
||||
## termion
|
||||
|
||||
```shell
|
||||
cargo run -p demo --no-default-features --features termion
|
||||
```
|
||||
|
||||
## termwiz
|
||||
|
||||
```shell
|
||||
cargo run -p demo --no-default-features --features termwiz
|
||||
```
|
||||
@@ -122,7 +122,7 @@ pub struct TabsState<'a> {
|
||||
}
|
||||
|
||||
impl<'a> TabsState<'a> {
|
||||
pub fn new(titles: Vec<&'a str>) -> Self {
|
||||
pub const fn new(titles: Vec<&'a str>) -> Self {
|
||||
Self { titles, index: 0 }
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
use std::{error::Error, time::Duration};
|
||||
|
||||
use argh::FromArgs;
|
||||
use clap::Parser;
|
||||
|
||||
mod app;
|
||||
#[cfg(feature = "crossterm")]
|
||||
@@ -28,28 +28,29 @@ mod termwiz;
|
||||
mod ui;
|
||||
|
||||
/// Demo
|
||||
#[derive(Debug, FromArgs)]
|
||||
#[derive(Debug, Parser)]
|
||||
struct Cli {
|
||||
/// time in ms between two ticks.
|
||||
#[argh(option, default = "250")]
|
||||
#[arg(short, long, default_value_t = 250)]
|
||||
tick_rate: u64,
|
||||
|
||||
/// whether unicode symbols are used to improve the overall look of the app
|
||||
#[argh(option, default = "true")]
|
||||
enhanced_graphics: bool,
|
||||
#[arg(short, long, default_value_t = true)]
|
||||
unicode: bool,
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let cli: Cli = argh::from_env();
|
||||
let cli = Cli::parse();
|
||||
let tick_rate = Duration::from_millis(cli.tick_rate);
|
||||
#[cfg(feature = "crossterm")]
|
||||
crate::crossterm::run(tick_rate, cli.enhanced_graphics)?;
|
||||
crate::crossterm::run(tick_rate, cli.unicode)?;
|
||||
#[cfg(all(not(windows), feature = "termion", not(feature = "crossterm")))]
|
||||
crate::termion::run(tick_rate, cli.enhanced_graphics)?;
|
||||
crate::termion::run(tick_rate, cli.unicode)?;
|
||||
#[cfg(all(
|
||||
feature = "termwiz",
|
||||
not(feature = "crossterm"),
|
||||
not(feature = "termion")
|
||||
))]
|
||||
crate::termwiz::run(tick_rate, cli.enhanced_graphics)?;
|
||||
crate::termwiz::run(tick_rate, cli.unicode)?;
|
||||
Ok(())
|
||||
}
|
||||
19
examples/apps/demo2/Cargo.toml
Normal file
19
examples/apps/demo2/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "demo2"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre = "0.6.3"
|
||||
crossterm.workspace = true
|
||||
indoc.workspace = true
|
||||
itertools.workspace = true
|
||||
palette = "0.7.6"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3.1"
|
||||
ratatui = { workspace = true, features = ["all-widgets"] }
|
||||
strum.workspace = true
|
||||
time = "0.3.37"
|
||||
unicode-width = "0.2.0"
|
||||
9
examples/apps/demo2/README.md
Normal file
9
examples/apps/demo2/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## Demo2
|
||||
|
||||
This is the demo example from the main README and crate page. Source: [demo2](./demo2/).
|
||||
|
||||
```shell
|
||||
cargo run -p demo2
|
||||
```
|
||||
|
||||

|
||||
75
examples/apps/demo2/src/tabs/about.rs
Normal file
75
examples/apps/demo2/src/tabs/about.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
widgets::{
|
||||
Block, Borders, Clear, MascotEyeColor, Padding, Paragraph, RatatuiMascot, Widget, Wrap,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct AboutTab {
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl AboutTab {
|
||||
pub fn prev_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AboutTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||
let [logo_area, description] = horizontal.areas(area);
|
||||
render_crate_description(description, buf);
|
||||
let eye_state = if self.row_index % 2 == 0 {
|
||||
MascotEyeColor::Default
|
||||
} else {
|
||||
MascotEyeColor::Red
|
||||
};
|
||||
RatatuiMascot::default().set_eye(eye_state).render(
|
||||
logo_area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
}),
|
||||
buf,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 4,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf); // clear out the color swatches
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
let text = "- cooking up terminal user interfaces -
|
||||
|
||||
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
|
||||
screen efficiently every frame.";
|
||||
Paragraph::new(text)
|
||||
.style(THEME.description)
|
||||
.block(
|
||||
Block::new()
|
||||
.title(" Ratatui ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::TOP)
|
||||
.border_style(THEME.description_title)
|
||||
.padding(Padding::new(0, 0, 0, 0)),
|
||||
)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((0, 0))
|
||||
.render(area, buf);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ impl Ingredient {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Ingredient> for Row<'a> {
|
||||
impl From<Ingredient> for Row<'_> {
|
||||
fn from(i: Ingredient) -> Self {
|
||||
Row::new(vec![i.quantity, i.name]).height(i.height())
|
||||
}
|
||||
@@ -164,7 +164,7 @@ fn render_ingredients(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
Table::new(rows, [Constraint::Length(7), Constraint::Length(30)])
|
||||
.block(Block::new().style(theme.ingredients))
|
||||
.header(Row::new(vec!["Qty", "Ingredient"]).style(theme.ingredients_header))
|
||||
.highlight_style(Style::new().light_yellow()),
|
||||
.row_highlight_style(Style::new().light_yellow()),
|
||||
area,
|
||||
buf,
|
||||
&mut state,
|
||||
@@ -60,7 +60,7 @@ fn render_hops(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
StatefulWidget::render(
|
||||
Table::new(rows, [Constraint::Max(100), Constraint::Length(15)])
|
||||
.header(Row::new(vec!["Host", "Address"]).set_style(THEME.traceroute.header))
|
||||
.highlight_style(THEME.traceroute.selected)
|
||||
.row_highlight_style(THEME.traceroute.selected)
|
||||
.block(block),
|
||||
area,
|
||||
buf,
|
||||
@@ -99,7 +99,7 @@ pub fn render_ping(progress: usize, area: Rect, buf: &mut Buffer) {
|
||||
.title_alignment(Alignment::Center)
|
||||
.border_type(BorderType::Thick),
|
||||
)
|
||||
.data(&data)
|
||||
.data(data)
|
||||
.style(THEME.traceroute.ping)
|
||||
.render(area, buf);
|
||||
}
|
||||
@@ -101,7 +101,7 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
|
||||
} else {
|
||||
Style::new().fg(Color::DarkGray).bg(Color::Yellow).bold()
|
||||
})
|
||||
.label(label.into())
|
||||
.label(label)
|
||||
})
|
||||
.collect_vec();
|
||||
let group = BarGroup::default().bars(&data);
|
||||
@@ -115,15 +115,15 @@ fn render_simple_barchart(area: Rect, buf: &mut Buffer) {
|
||||
fn render_horizontal_barchart(area: Rect, buf: &mut Buffer) {
|
||||
let bg = Color::Rgb(32, 48, 96);
|
||||
let data = [
|
||||
Bar::default().text_value("Winter 37-51".into()).value(51),
|
||||
Bar::default().text_value("Spring 40-65".into()).value(65),
|
||||
Bar::default().text_value("Summer 54-77".into()).value(77),
|
||||
Bar::default().text_value("Winter 37-51").value(51),
|
||||
Bar::default().text_value("Spring 40-65").value(65),
|
||||
Bar::default().text_value("Summer 54-77").value(77),
|
||||
Bar::default()
|
||||
.text_value("Fall 41-71".into())
|
||||
.text_value("Fall 41-71")
|
||||
.value(71)
|
||||
.value_style(Style::new().bold()), // current season
|
||||
];
|
||||
let group = BarGroup::default().label("GPU".into()).bars(&data);
|
||||
let group = BarGroup::default().label("GPU").bars(&data);
|
||||
BarChart::default()
|
||||
.block(Block::new().padding(Padding::new(0, 0, 2, 0)))
|
||||
.direction(Direction::Horizontal)
|
||||
@@ -22,10 +22,8 @@ pub struct KeyBinding {
|
||||
}
|
||||
|
||||
pub struct Logo {
|
||||
pub rat: Color,
|
||||
pub rat_eye: Color,
|
||||
pub rat_eye_alt: Color,
|
||||
pub term: Color,
|
||||
}
|
||||
|
||||
pub struct Email {
|
||||
@@ -77,10 +75,8 @@ pub const THEME: Theme = Theme {
|
||||
description: Style::new().fg(LIGHT_GRAY).bg(DARK_BLUE),
|
||||
description_title: Style::new().fg(LIGHT_GRAY).add_modifier(Modifier::BOLD),
|
||||
logo: Logo {
|
||||
rat: WHITE,
|
||||
rat_eye: BLACK,
|
||||
rat_eye_alt: RED,
|
||||
term: BLACK,
|
||||
},
|
||||
key_binding: KeyBinding {
|
||||
key: Style::new().fg(BLACK).bg(DARK_GRAY),
|
||||
16
examples/apps/input-form/Cargo.toml
Normal file
16
examples/apps/input-form/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "input-form"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
27
examples/apps/input-form/README.md
Normal file
27
examples/apps/input-form/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Input Form example
|
||||
|
||||
This example demonstrates how to handle input across several form fields (2 strings and an number).
|
||||
It uses an enum to track the focused field, and sends keyboard events to one which is current.
|
||||
|
||||
Run this example with:
|
||||
|
||||
```shell
|
||||
cargo run -p input-form
|
||||
```
|
||||
|
||||
This example does not handle things like cursor movement within the line (just keys and backspace).
|
||||
Most apps would benefit from using the following crates for text input rather than directly using
|
||||
strings:
|
||||
|
||||
- [`tui-input`](https://crates.io/crates/tui-input)
|
||||
- [`tui-prompts`](https://crates.io/crates/tui-prompts)
|
||||
- [`tui-textarea`](https://crates.io/crates/tui-textarea)
|
||||
- [`rat-salsa`](https://crates.io/crates/rat-salsa)
|
||||
|
||||
Some more ideas for handling focus can be found in:
|
||||
|
||||
- [`focusable`](https://crates.io/crates/focusable) (see also [Ratatui forum
|
||||
post](https://forum.ratatui.rs/t/focusable-crate-manage-focus-state-for-your-widgets/73))
|
||||
- [`rat-focus`](https://crates.io/crates/rat-focus)
|
||||
- A useful [`Bevy` discssion](https://github.com/bevyengine/bevy/discussions/15374) about focus
|
||||
more generally.
|
||||
270
examples/apps/input-form/src/main.rs
Normal file
270
examples/apps/input-form/src/main.rs
Normal file
@@ -0,0 +1,270 @@
|
||||
//! A Ratatui example that demonstrates how to handle input form focus
|
||||
//!
|
||||
//! This example demonstrates how to handle cursor and input focus between multiple fields in a
|
||||
//! form. You can navigate between fields using the Tab key.
|
||||
//!
|
||||
//! This does not handle cursor movement etc. This is just a simple example. In a real application,
|
||||
//! consider using [`tui-input`], or [`tui-prompts`], or [`tui-textarea`].
|
||||
//!
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! [`tui-input`]: https://crates.io/crates/tui-input
|
||||
//! [`tui-prompts`]: https://crates.io/crates/tui-prompts
|
||||
//! [`tui-textarea`]: https://crates.io/crates/tui-textarea
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Offset, Rect},
|
||||
style::Stylize,
|
||||
text::Line,
|
||||
widgets::Widget,
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
|
||||
// serialize the form to JSON if the user submitted it, otherwise print "Canceled"
|
||||
match result {
|
||||
Ok(Some(form)) => println!("{}", serde_json::to_string_pretty(&form)?),
|
||||
Ok(None) => println!("Canceled"),
|
||||
Err(err) => eprintln!("{err}"),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
form: InputForm,
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Cancelled,
|
||||
Submitted,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<Option<InputForm>> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
match self.state {
|
||||
AppState::Cancelled => Ok(None),
|
||||
AppState::Submitted => Ok(Some(self.form)),
|
||||
AppState::Running => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
self.form.render(frame);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
match event::read()? {
|
||||
Event::Key(event) if event.kind == KeyEventKind::Press => match event.code {
|
||||
KeyCode::Esc => self.state = AppState::Cancelled,
|
||||
KeyCode::Enter => self.state = AppState::Submitted,
|
||||
_ => self.form.on_key_press(event),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct InputForm {
|
||||
#[serde(skip)]
|
||||
focus: Focus,
|
||||
first_name: StringField,
|
||||
last_name: StringField,
|
||||
age: AgeField,
|
||||
}
|
||||
|
||||
impl Default for InputForm {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
focus: Focus::FirstName,
|
||||
first_name: StringField::new("First Name"),
|
||||
last_name: StringField::new("Last Name"),
|
||||
age: AgeField::new("Age"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InputForm {
|
||||
// Handle focus navigation or pass the event to the focused field.
|
||||
fn on_key_press(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Tab => self.focus = self.focus.next(),
|
||||
_ => match self.focus {
|
||||
Focus::FirstName => self.first_name.on_key_press(event),
|
||||
Focus::LastName => self.last_name.on_key_press(event),
|
||||
Focus::Age => self.age.on_key_press(event),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the form with the current focus.
|
||||
///
|
||||
/// The cursor is placed at the end of the focused field.
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
let [first_name_area, last_name_area, age_area] =
|
||||
Layout::vertical(Constraint::from_lengths([1, 1, 1])).areas(frame.area());
|
||||
|
||||
frame.render_widget(&self.first_name, first_name_area);
|
||||
frame.render_widget(&self.last_name, last_name_area);
|
||||
frame.render_widget(&self.age, age_area);
|
||||
|
||||
let cursor_position = match self.focus {
|
||||
Focus::FirstName => first_name_area.offset(self.first_name.cursor_offset()),
|
||||
Focus::LastName => last_name_area.offset(self.last_name.cursor_offset()),
|
||||
Focus::Age => age_area.offset(self.age.cursor_offset()),
|
||||
};
|
||||
frame.set_cursor_position(cursor_position);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, PartialEq, Eq)]
|
||||
enum Focus {
|
||||
#[default]
|
||||
FirstName,
|
||||
LastName,
|
||||
Age,
|
||||
}
|
||||
|
||||
impl Focus {
|
||||
// Round-robin focus order.
|
||||
const fn next(&self) -> Self {
|
||||
match self {
|
||||
Self::FirstName => Self::LastName,
|
||||
Self::LastName => Self::Age,
|
||||
Self::Age => Self::FirstName,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A new-type representing a string field with a label.
|
||||
#[derive(Debug, Serialize)]
|
||||
struct StringField {
|
||||
#[serde(skip)]
|
||||
label: &'static str,
|
||||
value: String,
|
||||
}
|
||||
|
||||
impl StringField {
|
||||
const fn new(label: &'static str) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle input events for the string input.
|
||||
fn on_key_press(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Char(c) => self.value.push(c),
|
||||
KeyCode::Backspace => {
|
||||
self.value.pop();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_offset(&self) -> Offset {
|
||||
let x = (self.label.len() + self.value.len() + 2) as i32;
|
||||
Offset::new(x, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &StringField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = [
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
|
||||
let label = Line::from_iter([self.label, ": "]).bold();
|
||||
label.render(label_area, buf);
|
||||
self.value.clone().render(value_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// A new-type representing a person's age in years (0-130).
|
||||
#[derive(Default, Clone, Copy, Serialize)]
|
||||
struct AgeField {
|
||||
#[serde(skip)]
|
||||
label: &'static str,
|
||||
value: u8,
|
||||
}
|
||||
|
||||
impl AgeField {
|
||||
const MAX: u8 = 130;
|
||||
|
||||
const fn new(label: &'static str) -> Self {
|
||||
Self { label, value: 0 }
|
||||
}
|
||||
|
||||
/// Handle input events for the age input.
|
||||
///
|
||||
/// Digits are accepted as input, with any input which would exceed the maximum age being
|
||||
/// ignored. The up/down arrow keys and 'j'/'k' keys can be used to increment/decrement the
|
||||
/// age.
|
||||
fn on_key_press(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Char(digit @ '0'..='9') => {
|
||||
let value = self
|
||||
.value
|
||||
.saturating_mul(10)
|
||||
.saturating_add(digit as u8 - b'0');
|
||||
if value <= Self::MAX {
|
||||
self.value = value;
|
||||
}
|
||||
}
|
||||
KeyCode::Backspace => self.value /= 10,
|
||||
KeyCode::Up | KeyCode::Char('k') => self.increment(),
|
||||
KeyCode::Down | KeyCode::Char('j') => self.decrement(),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn increment(&mut self) {
|
||||
self.value = self.value.saturating_add(1).min(Self::MAX);
|
||||
}
|
||||
|
||||
fn decrement(&mut self) {
|
||||
self.value = self.value.saturating_sub(1);
|
||||
}
|
||||
|
||||
fn cursor_offset(&self) -> Offset {
|
||||
let x = (self.label.len() + self.value.to_string().len() + 2) as i32;
|
||||
Offset::new(x, 0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &AgeField {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let constraints = [
|
||||
Constraint::Length(self.label.len() as u16 + 2),
|
||||
Constraint::Fill(1),
|
||||
];
|
||||
let [label_area, value_area] = Layout::horizontal(constraints).areas(area);
|
||||
let label = Line::from_iter([self.label, ": "]).bold();
|
||||
let value = self.value.to_string();
|
||||
label.render(label_area, buf);
|
||||
value.render(value_area, buf);
|
||||
}
|
||||
}
|
||||
17
examples/apps/mouse-drawing/Cargo.toml
Normal file
17
examples/apps/mouse-drawing/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "mouse-drawing"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
## a collection of line drawing algorithms (e.g. Bresenham's line algorithm)
|
||||
line_drawing = "1.0.0"
|
||||
rand = "0.8.5"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
9
examples/apps/mouse-drawing/README.md
Normal file
9
examples/apps/mouse-drawing/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Mouse drawing demo
|
||||
|
||||
This example shows how to receive mouse and handle mouse events.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p mouse-drawing
|
||||
```
|
||||
123
examples/apps/mouse-drawing/src/main.rs
Normal file
123
examples/apps/mouse-drawing/src/main.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
/// A Ratatui example that demonstrates how to handle mouse events.
|
||||
///
|
||||
/// This example demonstrates how to handle mouse events in Ratatui. You can draw lines by
|
||||
/// clicking and dragging the mouse.
|
||||
///
|
||||
/// This example runs with the Ratatui library code in the branch that you are currently
|
||||
/// reading. See the [`latest`] branch for the code which works with the most recent Ratatui
|
||||
/// release.
|
||||
///
|
||||
/// [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
use color_eyre::Result;
|
||||
use crossterm::{
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, MouseEvent,
|
||||
MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
};
|
||||
use ratatui::{
|
||||
layout::{Position, Rect, Size},
|
||||
style::{Color, Stylize},
|
||||
symbols,
|
||||
text::Line,
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let result = MouseDrawingApp::default().run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseDrawingApp {
|
||||
// Whether the app should exit
|
||||
pub should_exit: bool,
|
||||
// The last known mouse position
|
||||
pub mouse_position: Option<Position>,
|
||||
// The points that have been clicked / drawn by dragging the mouse
|
||||
pub points: Vec<(Position, Color)>,
|
||||
// The color to draw with
|
||||
pub current_color: Color,
|
||||
}
|
||||
|
||||
impl MouseDrawingApp {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
execute!(std::io::stdout(), EnableMouseCapture)?;
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| self.render(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
execute!(std::io::stdout(), DisableMouseCapture)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
match event::read()? {
|
||||
Event::Key(event) => self.on_key_event(event),
|
||||
Event::Mouse(event) => self.on_mouse_event(event),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Quit the app if the user presses 'q' or 'Esc'
|
||||
fn on_key_event(&mut self, event: KeyEvent) {
|
||||
match event.code {
|
||||
KeyCode::Char(' ') => {
|
||||
self.current_color = Color::Rgb(rand::random(), rand::random(), rand::random());
|
||||
}
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds any points which were clicked or dragged to the `points` vector.
|
||||
fn on_mouse_event(&mut self, event: MouseEvent) {
|
||||
let position = Position::new(event.column, event.row);
|
||||
match event.kind {
|
||||
MouseEventKind::Down(_) => self.points.push((position, self.current_color)),
|
||||
MouseEventKind::Drag(_) => self.draw_line(position),
|
||||
_ => {}
|
||||
}
|
||||
self.mouse_position = Some(position);
|
||||
}
|
||||
|
||||
/// Draw a line between the last point and the given position
|
||||
fn draw_line(&mut self, position: Position) {
|
||||
if let Some(start) = self.points.last() {
|
||||
let (x0, y0) = (i32::from(start.0.x), i32::from(start.0.y));
|
||||
let (x1, y1) = (i32::from(position.x), i32::from(position.y));
|
||||
for (x, y) in line_drawing::Bresenham::new((x0, y0), (x1, y1)) {
|
||||
let point = (Position::new(x as u16, y as u16), self.current_color);
|
||||
self.points.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&self, frame: &mut Frame) {
|
||||
// call order is important here as later elements are drawn on top of earlier elements
|
||||
self.render_points(frame);
|
||||
self.render_mouse_cursor(frame);
|
||||
let value = "Mouse Example ('Esc' to quit. Click / drag to draw. 'Space' to change color)";
|
||||
let title = Line::from(value).centered();
|
||||
frame.render_widget(title, frame.area());
|
||||
}
|
||||
|
||||
fn render_points(&self, frame: &mut Frame<'_>) {
|
||||
for (position, color) in &self.points {
|
||||
let area = Rect::from((*position, Size::new(1, 1))).clamp(frame.area());
|
||||
frame.render_widget(symbols::block::FULL.fg(*color), area);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_mouse_cursor(&self, frame: &mut Frame<'_>) {
|
||||
if let Some(position) = self.mouse_position {
|
||||
let area = Rect::from((position, Size::new(1, 1))).clamp(frame.area());
|
||||
frame.render_widget("╳".bg(self.current_color), area);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
examples/apps/weather/Cargo.toml
Normal file
15
examples/apps/weather/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "weather"
|
||||
publish = false
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
color-eyre.workspace = true
|
||||
crossterm.workspace = true
|
||||
rand = "0.8.5"
|
||||
ratatui.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
9
examples/apps/weather/README.md
Normal file
9
examples/apps/weather/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# Weather demo
|
||||
|
||||
This example shows how to render weather data using barchart widget.
|
||||
|
||||
To run this demo:
|
||||
|
||||
```shell
|
||||
cargo run -p weather
|
||||
```
|
||||
@@ -1,26 +1,21 @@
|
||||
//! # [Ratatui] `BarChart` example
|
||||
//! A Ratatui example that demonstrates how to render weather data using [`BarChart`] widget.
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//! Generates random temperature data for each hour of the day and renders it as a vertical bar.
|
||||
//!
|
||||
//! 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.
|
||||
//! This example runs with the Ratatui library code in the branch that you are currently reading.
|
||||
//! See the [`latest`] branch for the code which works with the most recent Ratatui release.
|
||||
//!
|
||||
//! 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
|
||||
//! [`latest`]: https://github.com/ratatui/ratatui/tree/latest
|
||||
//! [`BarChart`]: https://docs.rs/ratatui/latest/ratatui/widgets/struct.BarChart.html
|
||||
|
||||
use color_eyre::Result;
|
||||
use rand::{thread_rng, Rng};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Bar, BarChart, BarGroup, Block},
|
||||
widgets::{Bar, BarChart, BarGroup},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
@@ -65,17 +60,12 @@ impl App {
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let [title, vertical, horizontal] = Layout::vertical([
|
||||
Constraint::Length(1),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Fill(1),
|
||||
])
|
||||
.spacing(1)
|
||||
.areas(frame.area());
|
||||
let [title, main] = Layout::vertical([Constraint::Length(1), Constraint::Fill(1)])
|
||||
.spacing(1)
|
||||
.areas(frame.area());
|
||||
|
||||
frame.render_widget("Barchart".bold().into_centered_line(), title);
|
||||
frame.render_widget(vertical_barchart(&self.temperatures), vertical);
|
||||
frame.render_widget(horizontal_barchart(&self.temperatures), horizontal);
|
||||
frame.render_widget("Weather demo".bold().into_centered_line(), title);
|
||||
frame.render_widget(vertical_barchart(&self.temperatures), main);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,10 +76,8 @@ fn vertical_barchart(temperatures: &[u8]) -> BarChart {
|
||||
.enumerate()
|
||||
.map(|(hour, value)| vertical_bar(hour, value))
|
||||
.collect();
|
||||
let title = Line::from("Weather (Vertical)").centered();
|
||||
BarChart::default()
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.block(Block::new().title(title))
|
||||
.bar_width(5)
|
||||
}
|
||||
|
||||
@@ -102,32 +90,6 @@ fn vertical_bar(hour: usize, temperature: &u8) -> Bar {
|
||||
.value_style(temperature_style(*temperature).reversed())
|
||||
}
|
||||
|
||||
/// Create a horizontal bar chart from the temperatures data.
|
||||
fn horizontal_barchart(temperatures: &[u8]) -> BarChart {
|
||||
let bars: Vec<Bar> = temperatures
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(hour, value)| horizontal_bar(hour, value))
|
||||
.collect();
|
||||
let title = Line::from("Weather (Horizontal)").centered();
|
||||
BarChart::default()
|
||||
.block(Block::new().title(title))
|
||||
.data(BarGroup::default().bars(&bars))
|
||||
.bar_width(1)
|
||||
.bar_gap(0)
|
||||
.direction(Direction::Horizontal)
|
||||
}
|
||||
|
||||
fn horizontal_bar(hour: usize, temperature: &u8) -> Bar {
|
||||
let style = temperature_style(*temperature);
|
||||
Bar::default()
|
||||
.value(u64::from(*temperature))
|
||||
.label(Line::from(format!("{hour:>02}:00")))
|
||||
.text_value(format!("{temperature:>3}°"))
|
||||
.style(style)
|
||||
.value_style(style.reversed())
|
||||
}
|
||||
|
||||
/// create a yellow to red value based on the value (50-90)
|
||||
fn temperature_style(value: u8) -> Style {
|
||||
let green = (255.0 * (1.0 - f64::from(value - 50) / 40.0)) as u8;
|
||||
@@ -1,211 +0,0 @@
|
||||
//! # [Ratatui] `BarChart` 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::iter::zip;
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Direction, Layout},
|
||||
style::{Color, Style, Stylize},
|
||||
text::Line,
|
||||
widgets::{Bar, BarChart, BarGroup, Block},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
const COMPANY_COUNT: usize = 3;
|
||||
const PERIOD_COUNT: usize = 4;
|
||||
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
companies: [Company; COMPANY_COUNT],
|
||||
revenues: [Revenues; PERIOD_COUNT],
|
||||
}
|
||||
|
||||
struct Revenues {
|
||||
period: &'static str,
|
||||
revenues: [u32; COMPANY_COUNT],
|
||||
}
|
||||
|
||||
struct Company {
|
||||
short_name: &'static str,
|
||||
name: &'static str,
|
||||
color: Color,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new() -> Self {
|
||||
Self {
|
||||
should_exit: false,
|
||||
companies: fake_companies(),
|
||||
revenues: fake_revenues(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
use Constraint::{Fill, Length, Min};
|
||||
let vertical = Layout::vertical([Length(1), Fill(1), Min(20)]).spacing(1);
|
||||
let [title, top, bottom] = vertical.areas(frame.area());
|
||||
|
||||
frame.render_widget("Grouped Barchart".bold().into_centered_line(), title);
|
||||
frame.render_widget(self.vertical_revenue_barchart(), top);
|
||||
frame.render_widget(self.horizontal_revenue_barchart(), bottom);
|
||||
}
|
||||
|
||||
/// Create a vertical revenue bar chart with the data from the `revenues` field.
|
||||
fn vertical_revenue_barchart(&self) -> BarChart<'_> {
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::new().title(Line::from("Company revenues (Vertical)").centered()))
|
||||
.bar_gap(0)
|
||||
.bar_width(6)
|
||||
.group_gap(2);
|
||||
|
||||
for group in self
|
||||
.revenues
|
||||
.iter()
|
||||
.map(|revenue| revenue.to_vertical_bar_group(&self.companies))
|
||||
{
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
barchart
|
||||
}
|
||||
|
||||
/// Create a horizontal revenue bar chart with the data from the `revenues` field.
|
||||
fn horizontal_revenue_barchart(&self) -> BarChart<'_> {
|
||||
let title = Line::from("Company Revenues (Horizontal)").centered();
|
||||
let mut barchart = BarChart::default()
|
||||
.block(Block::new().title(title))
|
||||
.bar_width(1)
|
||||
.group_gap(2)
|
||||
.bar_gap(0)
|
||||
.direction(Direction::Horizontal);
|
||||
for group in self
|
||||
.revenues
|
||||
.iter()
|
||||
.map(|revenue| revenue.to_horizontal_bar_group(&self.companies))
|
||||
{
|
||||
barchart = barchart.data(group);
|
||||
}
|
||||
barchart
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate fake company data
|
||||
const fn fake_companies() -> [Company; COMPANY_COUNT] {
|
||||
[
|
||||
Company::new("BAKE", "Bake my day", Color::LightRed),
|
||||
Company::new("BITE", "Bits and Bites", Color::Blue),
|
||||
Company::new("TART", "Tart of the Table", Color::White),
|
||||
]
|
||||
}
|
||||
|
||||
/// Some fake revenue data
|
||||
const fn fake_revenues() -> [Revenues; PERIOD_COUNT] {
|
||||
[
|
||||
Revenues::new("Jan", [8500, 6500, 7000]),
|
||||
Revenues::new("Feb", [9000, 7500, 8500]),
|
||||
Revenues::new("Mar", [9500, 4500, 8200]),
|
||||
Revenues::new("Apr", [6300, 4000, 5000]),
|
||||
]
|
||||
}
|
||||
|
||||
impl Revenues {
|
||||
/// Create a new instance of `Revenues`
|
||||
const fn new(period: &'static str, revenues: [u32; COMPANY_COUNT]) -> Self {
|
||||
Self { period, revenues }
|
||||
}
|
||||
|
||||
/// Create a `BarGroup` with vertical bars for each company
|
||||
fn to_vertical_bar_group<'a>(&self, companies: &'a [Company]) -> BarGroup<'a> {
|
||||
let bars: Vec<Bar> = zip(companies, self.revenues)
|
||||
.map(|(company, revenue)| company.vertical_revenue_bar(revenue))
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(self.period).centered())
|
||||
.bars(&bars)
|
||||
}
|
||||
|
||||
/// Create a `BarGroup` with horizontal bars for each company
|
||||
fn to_horizontal_bar_group<'a>(&'a self, companies: &'a [Company]) -> BarGroup<'a> {
|
||||
let bars: Vec<Bar> = zip(companies, self.revenues)
|
||||
.map(|(company, revenue)| company.horizontal_revenue_bar(revenue))
|
||||
.collect();
|
||||
BarGroup::default()
|
||||
.label(Line::from(self.period).centered())
|
||||
.bars(&bars)
|
||||
}
|
||||
}
|
||||
|
||||
impl Company {
|
||||
/// Create a new instance of `Company`
|
||||
const fn new(short_name: &'static str, name: &'static str, color: Color) -> Self {
|
||||
Self {
|
||||
short_name,
|
||||
name,
|
||||
color,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a vertical revenue bar for the company
|
||||
///
|
||||
/// The label is the short name of the company, and will be displayed under the bar
|
||||
fn vertical_revenue_bar(&self, revenue: u32) -> Bar {
|
||||
let text_value = format!("{:.1}M", f64::from(revenue) / 1000.);
|
||||
Bar::default()
|
||||
.label(self.short_name.into())
|
||||
.value(u64::from(revenue))
|
||||
.text_value(text_value)
|
||||
.style(self.color)
|
||||
.value_style(Style::new().fg(Color::Black).bg(self.color))
|
||||
}
|
||||
|
||||
/// Create a horizontal revenue bar for the company
|
||||
///
|
||||
/// The label is the long name of the company combined with the revenue and will be displayed
|
||||
/// on the bar
|
||||
fn horizontal_revenue_bar(&self, revenue: u32) -> Bar {
|
||||
let text_value = format!("{} ({:.1} M)", self.name, f64::from(revenue) / 1000.);
|
||||
Bar::default()
|
||||
.value(u64::from(revenue))
|
||||
.text_value(text_value)
|
||||
.style(self.color)
|
||||
.value_style(Style::new().fg(Color::Black).bg(self.color))
|
||||
}
|
||||
}
|
||||
@@ -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,249 +0,0 @@
|
||||
//! # [Ratatui] Calendar 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::{Constraint, Layout, Margin},
|
||||
style::{Color, Modifier, Style},
|
||||
widgets::calendar::{CalendarEventStore, DateStyler, Monthly},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
use time::{Date, Month, OffsetDateTime};
|
||||
|
||||
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 area = frame.area().inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
});
|
||||
|
||||
let mut start = OffsetDateTime::now_local()
|
||||
.unwrap()
|
||||
.date()
|
||||
.replace_month(Month::January)
|
||||
.unwrap()
|
||||
.replace_day(1)
|
||||
.unwrap();
|
||||
|
||||
let list = make_dates(start.year());
|
||||
|
||||
let rows = Layout::vertical([Constraint::Ratio(1, 3); 3]).split(area);
|
||||
let cols = rows.iter().flat_map(|row| {
|
||||
Layout::horizontal([Constraint::Ratio(1, 4); 4])
|
||||
.split(*row)
|
||||
.to_vec()
|
||||
});
|
||||
for col in cols {
|
||||
let cal = cals::get_cal(start.month(), start.year(), &list);
|
||||
frame.render_widget(cal, col);
|
||||
start = start.replace_month(start.month().next()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn make_dates(current_year: i32) -> CalendarEventStore {
|
||||
let mut list = CalendarEventStore::today(
|
||||
Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Blue),
|
||||
);
|
||||
|
||||
// Holidays
|
||||
let holiday_style = Style::default()
|
||||
.fg(Color::Red)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
|
||||
// new year's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// next new_year's for December "show surrounding"
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year + 1, Month::January, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// groundhog day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::February, 2).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// april fool's
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 1).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// earth day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::April, 22).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// star wars day
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::May, 4).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// festivus
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 23).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
// new year's eve
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 31).unwrap(),
|
||||
holiday_style,
|
||||
);
|
||||
|
||||
// seasons
|
||||
let season_style = Style::default()
|
||||
.fg(Color::White)
|
||||
.bg(Color::Yellow)
|
||||
.add_modifier(Modifier::UNDERLINED);
|
||||
// spring equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::March, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// summer solstice
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::June, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
// fall equinox
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::September, 22).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list.add(
|
||||
Date::from_calendar_date(current_year, Month::December, 21).unwrap(),
|
||||
season_style,
|
||||
);
|
||||
list
|
||||
}
|
||||
|
||||
mod cals {
|
||||
#[allow(clippy::wildcard_imports)]
|
||||
use super::*;
|
||||
|
||||
pub fn get_cal<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
match m {
|
||||
Month::May => example1(m, y, es),
|
||||
Month::June => example2(m, y, es),
|
||||
Month::July | Month::December => example3(m, y, es),
|
||||
Month::February => example4(m, y, es),
|
||||
Month::November => example5(m, y, es),
|
||||
_ => default(m, y, es),
|
||||
}
|
||||
}
|
||||
|
||||
fn default<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(Style::default())
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example1<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(default_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example2<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::DIM)
|
||||
.fg(Color::LightYellow);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example3<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_surrounding(Style::default().add_modifier(Modifier::DIM))
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
.show_month_header(Style::default())
|
||||
}
|
||||
|
||||
fn example4<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_weekdays_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
|
||||
fn example5<'a, DS: DateStyler>(m: Month, y: i32, es: DS) -> Monthly<'a, DS> {
|
||||
let header_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.fg(Color::Green);
|
||||
|
||||
let default_style = Style::default()
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.bg(Color::Rgb(50, 50, 50));
|
||||
|
||||
Monthly::new(Date::from_calendar_date(y, m, 1).unwrap(), es)
|
||||
.show_month_header(header_style)
|
||||
.default_style(default_style)
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
use itertools::Itertools;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap},
|
||||
};
|
||||
|
||||
use crate::{RgbSwatch, THEME};
|
||||
|
||||
const RATATUI_LOGO: [&str; 32] = [
|
||||
" ███ ",
|
||||
" ██████ ",
|
||||
" ███████ ",
|
||||
" ████████ ",
|
||||
" █████████ ",
|
||||
" ██████████ ",
|
||||
" ████████████ ",
|
||||
" █████████████ ",
|
||||
" █████████████ ██████",
|
||||
" ███████████ ████████",
|
||||
" █████ ███████████ ",
|
||||
" ███ ██ee████████ ",
|
||||
" █ █████████████ ",
|
||||
" ████ █████████████ ",
|
||||
" █████████████████ ",
|
||||
" ████████████████ ",
|
||||
" ████████████████ ",
|
||||
" ███ ██████████ ",
|
||||
" ██ █████████ ",
|
||||
" █xx█ █████████ ",
|
||||
" █xxxx█ ██████████ ",
|
||||
" █xx█xxx█ █████████ ",
|
||||
" █xx██xxxx█ ████████ ",
|
||||
" █xxxxxxxxxx█ ██████████ ",
|
||||
" █xxxxxxxxxxxx█ ██████████ ",
|
||||
" █xxxxxxx██xxxxx█ █████████ ",
|
||||
" █xxxxxxxxx██xxxxx█ ████ ███ ",
|
||||
" █xxxxxxxxxxxxxxxxxx█ ██ ███ ",
|
||||
"█xxxxxxxxxxxxxxxxxxxx█ █ ███ ",
|
||||
"█xxxxxxxxxxxxxxxxxxxxx█ ███ ",
|
||||
" █xxxxxxxxxxxxxxxxxxxxx█ ███ ",
|
||||
" █xxxxxxxxxxxxxxxxxxxxx█ █ ",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct AboutTab {
|
||||
row_index: usize,
|
||||
}
|
||||
|
||||
impl AboutTab {
|
||||
pub fn prev_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_sub(1);
|
||||
}
|
||||
|
||||
pub fn next_row(&mut self) {
|
||||
self.row_index = self.row_index.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for AboutTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
RgbSwatch.render(area, buf);
|
||||
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
|
||||
let [description, logo] = horizontal.areas(area);
|
||||
render_crate_description(description, buf);
|
||||
render_logo(self.row_index, logo, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_crate_description(area: Rect, buf: &mut Buffer) {
|
||||
let area = area.inner(Margin {
|
||||
vertical: 4,
|
||||
horizontal: 2,
|
||||
});
|
||||
Clear.render(area, buf); // clear out the color swatches
|
||||
Block::new().style(THEME.content).render(area, buf);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 2,
|
||||
});
|
||||
let text = "- cooking up terminal user interfaces -
|
||||
|
||||
Ratatui is a Rust crate that provides widgets (e.g. Paragraph, Table) and draws them to the \
|
||||
screen efficiently every frame.";
|
||||
Paragraph::new(text)
|
||||
.style(THEME.description)
|
||||
.block(
|
||||
Block::new()
|
||||
.title(" Ratatui ")
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::TOP)
|
||||
.border_style(THEME.description_title)
|
||||
.padding(Padding::new(0, 0, 0, 0)),
|
||||
)
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((0, 0))
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// Use half block characters to render a logo based on the `RATATUI_LOGO` const.
|
||||
///
|
||||
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
|
||||
/// rat's eye. The eye color alternates between two colors based on the selected row.
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
|
||||
let eye_color = if selected_row % 2 == 0 {
|
||||
THEME.logo.rat_eye
|
||||
} else {
|
||||
THEME.logo.rat_eye_alt
|
||||
};
|
||||
let area = area.inner(Margin {
|
||||
vertical: 0,
|
||||
horizontal: 2,
|
||||
});
|
||||
for (y, (line1, line2)) in RATATUI_LOGO.iter().tuples().enumerate() {
|
||||
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
|
||||
let x = area.left() + x as u16;
|
||||
let y = area.top() + y as u16;
|
||||
let cell = &mut buf[(x, y)];
|
||||
let rat_color = THEME.logo.rat;
|
||||
let term_color = THEME.logo.term;
|
||||
match (ch1, ch2) {
|
||||
('█', '█') => {
|
||||
cell.set_char('█');
|
||||
cell.fg = rat_color;
|
||||
cell.bg = rat_color;
|
||||
}
|
||||
('█', ' ') => {
|
||||
cell.set_char('▀');
|
||||
cell.fg = rat_color;
|
||||
}
|
||||
(' ', '█') => {
|
||||
cell.set_char('▄');
|
||||
cell.fg = rat_color;
|
||||
}
|
||||
('█', 'x') => {
|
||||
cell.set_char('▀');
|
||||
cell.fg = rat_color;
|
||||
cell.bg = term_color;
|
||||
}
|
||||
('x', '█') => {
|
||||
cell.set_char('▄');
|
||||
cell.fg = rat_color;
|
||||
cell.bg = term_color;
|
||||
}
|
||||
('x', 'x') => {
|
||||
cell.set_char(' ');
|
||||
cell.fg = term_color;
|
||||
cell.bg = term_color;
|
||||
}
|
||||
('█', 'e') => {
|
||||
cell.set_char('▀');
|
||||
cell.fg = rat_color;
|
||||
cell.bg = eye_color;
|
||||
}
|
||||
('e', '█') => {
|
||||
cell.set_char('▄');
|
||||
cell.fg = rat_color;
|
||||
cell.bg = eye_color;
|
||||
}
|
||||
(_, _) => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
use std::{
|
||||
sync::{atomic::Ordering, Arc},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
|
||||
use itertools::Itertools;
|
||||
use metrics::{Counter, Gauge, Histogram, Key, KeyName, Metadata, Recorder, SharedString, Unit};
|
||||
use metrics_util::{
|
||||
registry::{AtomicStorage, Registry},
|
||||
Summary,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{palette::tailwind::SLATE, Stylize},
|
||||
widgets::{Row, Table, Widget},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let recorder = MetricsRecorder::new();
|
||||
let recorder_widget = recorder.widget();
|
||||
recorder.install();
|
||||
let terminal = ratatui::init();
|
||||
let app = App::new(recorder_widget);
|
||||
let result = app.run(terminal);
|
||||
ratatui::restore();
|
||||
result
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct App {
|
||||
should_quit: bool,
|
||||
recorder_widget: RecorderWidget,
|
||||
}
|
||||
|
||||
impl App {
|
||||
const fn new(recorder_widget: RecorderWidget) -> Self {
|
||||
Self {
|
||||
should_quit: false,
|
||||
recorder_widget,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let mut last_frame = Instant::now();
|
||||
let frame_duration = Duration::from_secs_f64(1.0 / 60.0);
|
||||
while !self.should_quit {
|
||||
if last_frame.elapsed() >= frame_duration {
|
||||
last_frame = Instant::now();
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
}
|
||||
self.handle_events(frame_duration.saturating_sub(last_frame.elapsed()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let [top, main] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Fill(1)]).areas(frame.area());
|
||||
let title = if cfg!(debug_assertions) {
|
||||
"Metrics Example (debug)"
|
||||
} else {
|
||||
"Metrics Example (release)"
|
||||
};
|
||||
frame.render_widget(title.blue().into_centered_line(), top);
|
||||
frame.render_widget(&self.recorder_widget, main);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self, timeout: Duration) -> Result<()> {
|
||||
if !event::poll(timeout)? {
|
||||
return Ok(());
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_press(key),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_key_press(&mut self, key: event::KeyEvent) {
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.should_quit = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct MetricsRecorder {
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
|
||||
impl MetricsRecorder {
|
||||
fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn widget(&self) -> RecorderWidget {
|
||||
RecorderWidget {
|
||||
metrics: Arc::clone(&self.metrics),
|
||||
}
|
||||
}
|
||||
|
||||
fn install(self) {
|
||||
metrics::set_global_recorder(self).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
registry: Registry<Key, AtomicStorage>,
|
||||
}
|
||||
|
||||
impl Default for Metrics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
registry: Registry::atomic(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn counter(&self, key: &Key) -> Counter {
|
||||
self.registry
|
||||
.get_or_create_counter(key, |c| Counter::from_arc(c.clone()))
|
||||
}
|
||||
|
||||
fn gauge(&self, key: &Key) -> Gauge {
|
||||
self.registry
|
||||
.get_or_create_gauge(key, |g| Gauge::from_arc(g.clone()))
|
||||
}
|
||||
|
||||
fn histogram(&self, key: &Key) -> Histogram {
|
||||
self.registry
|
||||
.get_or_create_histogram(key, |h| Histogram::from_arc(h.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RecorderWidget {
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
|
||||
impl Widget for &RecorderWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let mut counters = vec![];
|
||||
self.metrics.registry.visit_counters(|key, counter| {
|
||||
let value = counter.load(Ordering::SeqCst);
|
||||
counters.push((key.clone(), value.to_string()));
|
||||
});
|
||||
let mut gauges = vec![];
|
||||
self.metrics.registry.visit_gauges(|key, gauge| {
|
||||
let value = gauge.load(Ordering::SeqCst);
|
||||
gauges.push((key.clone(), value.to_string()));
|
||||
});
|
||||
let mut histograms = vec![];
|
||||
self.metrics.registry.visit_histograms(|key, histogram| {
|
||||
let mut summary = Summary::with_defaults();
|
||||
for data in histogram.data() {
|
||||
summary.add(data);
|
||||
}
|
||||
if summary.is_empty() {
|
||||
// we omit the empty histograms, but this is how you would render them
|
||||
// histograms.push((key.clone(), "empty".to_string()));
|
||||
} else {
|
||||
let min = Duration::from_secs_f64(summary.min());
|
||||
let max = Duration::from_secs_f64(summary.max());
|
||||
let p50 = Duration::from_secs_f64(summary.quantile(0.5).unwrap());
|
||||
let p90 = Duration::from_secs_f64(summary.quantile(0.9).unwrap());
|
||||
let p99 = Duration::from_secs_f64(summary.quantile(0.99).unwrap());
|
||||
let line = format!(
|
||||
"min:{min:>9.2?} p50:{p50:>9.2?} p90:{p90:>9.2?} p99:{p99:>9.2?} max:{max:>9.2?}"
|
||||
);
|
||||
histograms.push((key.clone(), line));
|
||||
}
|
||||
});
|
||||
counters.sort();
|
||||
|
||||
gauges.sort();
|
||||
histograms.sort();
|
||||
let lines = counters
|
||||
.iter()
|
||||
.chain(gauges.iter())
|
||||
.chain(histograms.iter());
|
||||
let row_colors = [SLATE.c950, SLATE.c900];
|
||||
let rows = lines
|
||||
.map(|(key, line)| Row::new([key.name(), line]))
|
||||
.zip(row_colors.iter().cycle())
|
||||
.map(|(row, style)| row.bg(*style))
|
||||
.collect_vec();
|
||||
Table::new(rows, [Constraint::Length(40), Constraint::Fill(1)]).render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
impl Recorder for MetricsRecorder {
|
||||
fn describe_counter(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
|
||||
// todo!()
|
||||
}
|
||||
|
||||
fn describe_gauge(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
|
||||
// todo!()
|
||||
}
|
||||
|
||||
fn describe_histogram(&self, key: KeyName, unit: Option<Unit>, description: SharedString) {
|
||||
// todo!()
|
||||
}
|
||||
|
||||
fn register_counter(&self, key: &Key, metadata: &Metadata<'_>) -> Counter {
|
||||
self.metrics.counter(key)
|
||||
}
|
||||
|
||||
fn register_gauge(&self, key: &Key, metadata: &Metadata<'_>) -> Gauge {
|
||||
self.metrics.gauge(key)
|
||||
}
|
||||
|
||||
fn register_histogram(&self, key: &Key, metadata: &Metadata<'_>) -> Histogram {
|
||||
self.metrics.histogram(key)
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
//! # [Ratatui] Paragraph 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},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use color_eyre::Result;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::{Line, Masked, Span},
|
||||
widgets::{Block, Paragraph, Widget, Wrap},
|
||||
DefaultTerminal,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct App {
|
||||
should_exit: bool,
|
||||
scroll: u16,
|
||||
last_tick: Instant,
|
||||
}
|
||||
|
||||
impl App {
|
||||
/// The duration between each tick.
|
||||
const TICK_RATE: Duration = Duration::from_millis(250);
|
||||
|
||||
/// Create a new instance of the app.
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
should_exit: false,
|
||||
scroll: 0,
|
||||
last_tick: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the app until the user exits.
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while !self.should_exit {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
if self.last_tick.elapsed() >= Self::TICK_RATE {
|
||||
self.on_tick();
|
||||
self.last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle events from the terminal.
|
||||
fn handle_events(&mut self) -> io::Result<()> {
|
||||
let timeout = Self::TICK_RATE.saturating_sub(self.last_tick.elapsed());
|
||||
while event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
|
||||
self.should_exit = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the app state on each tick.
|
||||
fn on_tick(&mut self) {
|
||||
self.scroll = (self.scroll + 1) % 10;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let areas = Layout::vertical([Constraint::Max(9); 4]).split(area);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Default alignment (Left), no wrap"))
|
||||
.gray()
|
||||
.render(areas[0], buf);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Default alignment (Left), with wrap"))
|
||||
.gray()
|
||||
.wrap(Wrap { trim: true })
|
||||
.render(areas[1], buf);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Right alignment, with wrap"))
|
||||
.gray()
|
||||
.right_aligned()
|
||||
.wrap(Wrap { trim: true })
|
||||
.render(areas[2], buf);
|
||||
Paragraph::new(create_lines(area))
|
||||
.block(title_block("Center alignment, with wrap, with scroll"))
|
||||
.gray()
|
||||
.centered()
|
||||
.wrap(Wrap { trim: true })
|
||||
.scroll((self.scroll, 0))
|
||||
.render(areas[3], buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a bordered block with a title.
|
||||
fn title_block(title: &str) -> Block {
|
||||
Block::bordered()
|
||||
.gray()
|
||||
.title(title.bold().into_centered_line())
|
||||
}
|
||||
|
||||
/// Create some lines to display in the paragraph.
|
||||
fn create_lines(area: Rect) -> Vec<Line<'static>> {
|
||||
let short_line = "A long line to demonstrate line wrapping. ";
|
||||
let long_line = short_line.repeat(usize::from(area.width) / short_line.len() + 4);
|
||||
let mut styled_spans = vec![];
|
||||
for span in [
|
||||
"Styled".blue(),
|
||||
"Spans".red().on_white(),
|
||||
"Bold".bold(),
|
||||
"Italic".italic(),
|
||||
"Underlined".underlined(),
|
||||
"Strikethrough".crossed_out(),
|
||||
] {
|
||||
styled_spans.push(span);
|
||||
styled_spans.push(" ".into());
|
||||
}
|
||||
vec![
|
||||
Line::raw("Unstyled Line"),
|
||||
Line::raw("Styled Line").black().on_red().bold().italic(),
|
||||
Line::from(styled_spans),
|
||||
Line::from(long_line.green().italic()),
|
||||
Line::from_iter([
|
||||
"Masked text: ".into(),
|
||||
Span::styled(Masked::new("my secret password", '*'), Color::Red),
|
||||
]),
|
||||
]
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
//! # [Ratatui] Sparkline 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::time::{Duration, Instant};
|
||||
|
||||
use color_eyre::Result;
|
||||
use rand::{
|
||||
distributions::{Distribution, Uniform},
|
||||
rngs::ThreadRng,
|
||||
};
|
||||
use ratatui::{
|
||||
crossterm::event::{self, Event, KeyCode},
|
||||
layout::{Constraint, Layout},
|
||||
style::{Color, Style},
|
||||
widgets::{Block, Borders, Sparkline},
|
||||
DefaultTerminal, Frame,
|
||||
};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::new().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
struct App {
|
||||
signal: RandomSignal,
|
||||
data1: Vec<u64>,
|
||||
data2: Vec<u64>,
|
||||
data3: Vec<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RandomSignal {
|
||||
distribution: Uniform<u64>,
|
||||
rng: ThreadRng,
|
||||
}
|
||||
|
||||
impl RandomSignal {
|
||||
fn new(lower: u64, upper: u64) -> Self {
|
||||
Self {
|
||||
distribution: Uniform::new(lower, upper),
|
||||
rng: rand::thread_rng(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for RandomSignal {
|
||||
type Item = u64;
|
||||
fn next(&mut self) -> Option<u64> {
|
||||
Some(self.distribution.sample(&mut self.rng))
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn new() -> Self {
|
||||
let mut signal = RandomSignal::new(0, 100);
|
||||
let data1 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data2 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
let data3 = signal.by_ref().take(200).collect::<Vec<u64>>();
|
||||
Self {
|
||||
signal,
|
||||
data1,
|
||||
data2,
|
||||
data3,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_tick(&mut self) {
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data1.pop();
|
||||
self.data1.insert(0, value);
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data2.pop();
|
||||
self.data2.insert(0, value);
|
||||
let value = self.signal.next().unwrap();
|
||||
self.data3.pop();
|
||||
self.data3.insert(0, value);
|
||||
}
|
||||
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
let tick_rate = Duration::from_millis(250);
|
||||
|
||||
let mut last_tick = Instant::now();
|
||||
loop {
|
||||
terminal.draw(|frame| self.draw(frame))?;
|
||||
|
||||
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.code == KeyCode::Char('q') {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
if last_tick.elapsed() >= tick_rate {
|
||||
self.on_tick();
|
||||
last_tick = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, frame: &mut Frame) {
|
||||
let chunks = Layout::vertical([
|
||||
Constraint::Length(3),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(frame.area());
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data1"),
|
||||
)
|
||||
.data(&self.data1)
|
||||
.style(Style::default().fg(Color::Yellow));
|
||||
frame.render_widget(sparkline, chunks[0]);
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data2"),
|
||||
)
|
||||
.data(&self.data2)
|
||||
.style(Style::default().bg(Color::Green));
|
||||
frame.render_widget(sparkline, chunks[1]);
|
||||
// Multiline
|
||||
let sparkline = Sparkline::default()
|
||||
.block(
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::RIGHT)
|
||||
.title("Data3"),
|
||||
)
|
||||
.data(&self.data3)
|
||||
.style(Style::default().fg(Color::Red));
|
||||
frame.render_widget(sparkline, chunks[2]);
|
||||
}
|
||||
}
|
||||
216
examples/tabs.rs
216
examples/tabs.rs
@@ -1,216 +0,0 @@
|
||||
//! # [Ratatui] Tabs 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::{
|
||||
buffer::Buffer,
|
||||
crossterm::event::{self, Event, KeyCode, KeyEventKind},
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{palette::tailwind, Color, Stylize},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{Block, Padding, Paragraph, Tabs, Widget},
|
||||
DefaultTerminal,
|
||||
};
|
||||
use strum::{Display, EnumIter, FromRepr, IntoEnumIterator};
|
||||
|
||||
fn main() -> Result<()> {
|
||||
color_eyre::install()?;
|
||||
let terminal = ratatui::init();
|
||||
let app_result = App::default().run(terminal);
|
||||
ratatui::restore();
|
||||
app_result
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
selected_tab: SelectedTab,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Copy, Display, FromRepr, EnumIter)]
|
||||
enum SelectedTab {
|
||||
#[default]
|
||||
#[strum(to_string = "Tab 1")]
|
||||
Tab1,
|
||||
#[strum(to_string = "Tab 2")]
|
||||
Tab2,
|
||||
#[strum(to_string = "Tab 3")]
|
||||
Tab3,
|
||||
#[strum(to_string = "Tab 4")]
|
||||
Tab4,
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> {
|
||||
while self.state == AppState::Running {
|
||||
terminal.draw(|frame| frame.render_widget(&self, frame.area()))?;
|
||||
self.handle_events()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> std::io::Result<()> {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char('l') | KeyCode::Right => self.next_tab(),
|
||||
KeyCode::Char('h') | KeyCode::Left => self.previous_tab(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn next_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.next();
|
||||
}
|
||||
|
||||
pub fn previous_tab(&mut self) {
|
||||
self.selected_tab = self.selected_tab.previous();
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Get the previous tab, if there is no previous tab return the current tab.
|
||||
fn previous(self) -> Self {
|
||||
let current_index: usize = self as usize;
|
||||
let previous_index = current_index.saturating_sub(1);
|
||||
Self::from_repr(previous_index).unwrap_or(self)
|
||||
}
|
||||
|
||||
/// Get the next tab, if there is no next tab return the current tab.
|
||||
fn next(self) -> Self {
|
||||
let current_index = self as usize;
|
||||
let next_index = current_index.saturating_add(1);
|
||||
Self::from_repr(next_index).unwrap_or(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min};
|
||||
let vertical = Layout::vertical([Length(1), Min(0), Length(1)]);
|
||||
let [header_area, inner_area, footer_area] = vertical.areas(area);
|
||||
|
||||
let horizontal = Layout::horizontal([Min(0), Length(20)]);
|
||||
let [tabs_area, title_area] = horizontal.areas(header_area);
|
||||
|
||||
render_title(title_area, buf);
|
||||
self.render_tabs(tabs_area, buf);
|
||||
self.selected_tab.render(inner_area, buf);
|
||||
render_footer(footer_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_tabs(&self, area: Rect, buf: &mut Buffer) {
|
||||
let titles = SelectedTab::iter().map(SelectedTab::title);
|
||||
let highlight_style = (Color::default(), self.selected_tab.palette().c700);
|
||||
let selected_tab_index = self.selected_tab as usize;
|
||||
Tabs::new(titles)
|
||||
.highlight_style(highlight_style)
|
||||
.select(selected_tab_index)
|
||||
.padding("", "")
|
||||
.divider(" ")
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_title(area: Rect, buf: &mut Buffer) {
|
||||
"Ratatui Tabs Example".bold().render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(area: Rect, buf: &mut Buffer) {
|
||||
Line::raw("◄ ► to change tab | Press q to quit")
|
||||
.centered()
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
impl Widget for SelectedTab {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
// in a real app these might be separate widgets
|
||||
match self {
|
||||
Self::Tab1 => self.render_tab0(area, buf),
|
||||
Self::Tab2 => self.render_tab1(area, buf),
|
||||
Self::Tab3 => self.render_tab2(area, buf),
|
||||
Self::Tab4 => self.render_tab3(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectedTab {
|
||||
/// Return tab's name as a styled `Line`
|
||||
fn title(self) -> Line<'static> {
|
||||
format!(" {self} ")
|
||||
.fg(tailwind::SLATE.c200)
|
||||
.bg(self.palette().c900)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn render_tab0(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Hello, World!")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab1(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Welcome to the Ratatui tabs example!")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab2(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("Look! I'm different than others!")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_tab3(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new("I know, these are some basic changes. But I think you got the main idea.")
|
||||
.block(self.block())
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
/// A block surrounding the tab's content
|
||||
fn block(self) -> Block<'static> {
|
||||
Block::bordered()
|
||||
.border_set(symbols::border::PROPORTIONAL_TALL)
|
||||
.padding(Padding::horizontal(1))
|
||||
.border_style(self.palette().c700)
|
||||
}
|
||||
|
||||
const fn palette(self) -> tailwind::Palette {
|
||||
match self {
|
||||
Self::Tab1 => tailwind::BLUE,
|
||||
Self::Tab2 => tailwind::EMERALD,
|
||||
Self::Tab3 => tailwind::INDIGO,
|
||||
Self::Tab4 => tailwind::RED,
|
||||
}
|
||||
}
|
||||
}
|
||||
69
ratatui-core/Cargo.toml
Normal file
69
ratatui-core/Cargo.toml
Normal file
@@ -0,0 +1,69 @@
|
||||
[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.2"
|
||||
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 to / from colors, modifiers, and styles in the ['anstyle'] crate
|
||||
anstyle = ["dep:anstyle"]
|
||||
|
||||
## 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]
|
||||
anstyle = { version = "1", optional = true }
|
||||
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
|
||||
thiserror = "2"
|
||||
unicode-segmentation.workspace = true
|
||||
unicode-truncate = "2"
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
||||
rstest.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
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 -->
|
||||
@@ -24,10 +24,10 @@
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! ```rust,ignore
|
||||
//! 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,129 +90,28 @@
|
||||
//! 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;
|
||||
|
||||
use metrics::{Counter, Histogram};
|
||||
use once_cell::sync::Lazy;
|
||||
use strum::{Display, EnumString};
|
||||
|
||||
use crate::{
|
||||
buffer::Cell,
|
||||
counter, duration_histogram,
|
||||
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;
|
||||
|
||||
static METRICS: Lazy<Metrics> = Lazy::new(Metrics::new);
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Metrics {
|
||||
pub clear_region_count: Counter,
|
||||
pub clear_region_duration: Histogram,
|
||||
pub draw_count: Counter,
|
||||
pub draw_duration: Histogram,
|
||||
pub append_lines_count: Counter,
|
||||
pub append_lines_duration: Histogram,
|
||||
pub hide_cursor_duration: Histogram,
|
||||
pub show_cursor_duration: Histogram,
|
||||
pub get_cursor_position_duration: Histogram,
|
||||
pub set_cursor_position_duration: Histogram,
|
||||
pub size_duration: Histogram,
|
||||
pub window_size_duration: Histogram,
|
||||
pub flush_count: Counter,
|
||||
pub flush_duration: Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
clear_region_count: counter!(
|
||||
"ratatui.backend.clear_region.count",
|
||||
"Number of times a region of the backend buffer was cleared"
|
||||
),
|
||||
clear_region_duration: duration_histogram!(
|
||||
"ratatui.backend.clear.time",
|
||||
"Time spent clearing the backend buffer"
|
||||
),
|
||||
draw_count: counter!(
|
||||
"ratatui.backend.draw.count",
|
||||
"Number of times the backend buffer was drawn to the terminal"
|
||||
),
|
||||
draw_duration: duration_histogram!(
|
||||
"ratatui.backend.draw.time",
|
||||
"Time spent drawing the backend buffer to the terminal"
|
||||
),
|
||||
hide_cursor_duration: duration_histogram!(
|
||||
"ratatui.backend.hide_cursor.time",
|
||||
"Time spent hiding the cursor in the backend"
|
||||
),
|
||||
show_cursor_duration: duration_histogram!(
|
||||
"ratatui.backend.show_cursor.time",
|
||||
"Time spent showing the cursor in the backend"
|
||||
),
|
||||
get_cursor_position_duration: duration_histogram!(
|
||||
"ratatui.backend.get_cursor_position.time",
|
||||
"Time spent getting the cursor position from the backend"
|
||||
),
|
||||
set_cursor_position_duration: duration_histogram!(
|
||||
"ratatui.backend.set_cursor_position.time",
|
||||
"Time spent setting the cursor position in the backend"
|
||||
),
|
||||
append_lines_count: counter!(
|
||||
"ratatui.backend.append_lines.count",
|
||||
"Number of times lines were appended to the backend buffer"
|
||||
),
|
||||
append_lines_duration: duration_histogram!(
|
||||
"ratatui.backend.append_lines.time",
|
||||
"Time spent appending lines to the backend buffer"
|
||||
),
|
||||
size_duration: duration_histogram!(
|
||||
"ratatui.backend.size.time",
|
||||
"Time spent getting the size of the backend buffer"
|
||||
),
|
||||
window_size_duration: duration_histogram!(
|
||||
"ratatui.backend.window_size.time",
|
||||
"Time spent getting the window size of the backend buffer"
|
||||
),
|
||||
flush_count: counter!(
|
||||
"ratatui.backend.flush.count",
|
||||
"Number of times the backend buffer was flushed to the terminal"
|
||||
),
|
||||
flush_duration: duration_histogram!(
|
||||
"ratatui.backend.flush.time",
|
||||
"Time spent flushing the backend buffer to the terminal"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum representing the different types of clearing operations that can be performed
|
||||
/// on the terminal screen.
|
||||
#[derive(Debug, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
|
||||
@@ -248,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.
|
||||
///
|
||||
@@ -272,9 +171,11 @@ pub trait Backend {
|
||||
/// See also [`show_cursor`].
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// ```rust,ignore
|
||||
/// # 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()?;
|
||||
@@ -307,10 +208,11 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// # use ratatui::layout::Position;
|
||||
/// ```rust,ignore
|
||||
/// # 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(())
|
||||
@@ -339,9 +241,11 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::backend::{Backend, TestBackend};
|
||||
/// ```rust,ignore
|
||||
/// # use ratatui::backend::{TestBackend};
|
||||
/// # let mut backend = TestBackend::new(80, 25);
|
||||
/// use ratatui::backend::Backend;
|
||||
///
|
||||
/// backend.clear()?;
|
||||
/// # std::io::Result::Ok(())
|
||||
/// ```
|
||||
@@ -355,9 +259,11 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,no_run
|
||||
/// # use ratatui::{prelude::*, backend::{TestBackend, ClearType}};
|
||||
/// ```rust,ignore
|
||||
/// # 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(())
|
||||
/// ```
|
||||
@@ -387,9 +293,11 @@ pub trait Backend {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::{prelude::*, backend::TestBackend};
|
||||
/// let backend = TestBackend::new(80, 25);
|
||||
/// ```rust,ignore
|
||||
/// # 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(())
|
||||
/// ```
|
||||
@@ -404,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)]
|
||||
@@ -23,8 +23,8 @@ use crate::{
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{backend::TestBackend, prelude::*};
|
||||
/// ```rust,ignore
|
||||
/// 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() {
|
||||
@@ -3,13 +3,14 @@ use std::{
|
||||
ops::{Index, IndexMut},
|
||||
};
|
||||
|
||||
use metrics::Histogram;
|
||||
use once_cell::sync::Lazy;
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{
|
||||
buffer::Cell, duration_histogram, layout::Position, metrics::HistogramExt, prelude::*,
|
||||
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
|
||||
@@ -22,7 +23,7 @@ use crate::{
|
||||
/// # Examples:
|
||||
///
|
||||
/// ```
|
||||
/// use ratatui::{
|
||||
/// use ratatui_core::{
|
||||
/// buffer::{Buffer, Cell},
|
||||
/// layout::{Position, Rect},
|
||||
/// style::{Color, Style},
|
||||
@@ -74,23 +75,6 @@ pub struct Buffer {
|
||||
pub content: Vec<Cell>,
|
||||
}
|
||||
|
||||
static METRICS: Lazy<Metrics> = Lazy::new(Metrics::new);
|
||||
|
||||
struct Metrics {
|
||||
diff_duration: Histogram,
|
||||
}
|
||||
|
||||
impl Metrics {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
diff_duration: duration_histogram!(
|
||||
"ratatui.buffer.diff.time",
|
||||
"Time spent diffing buffers"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// Returns a Buffer with all cells set to the default one
|
||||
#[must_use]
|
||||
@@ -184,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()));
|
||||
@@ -211,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)) {
|
||||
@@ -235,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);
|
||||
@@ -246,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).
|
||||
@@ -275,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
|
||||
@@ -287,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));
|
||||
@@ -299,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"),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -346,7 +345,7 @@ impl Buffer {
|
||||
let max_width = max_width.try_into().unwrap_or(u16::MAX);
|
||||
let mut remaining_width = self.area.right().saturating_sub(x).min(max_width);
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true)
|
||||
.filter(|symbol| !symbol.contains(|char: char| char.is_control()))
|
||||
.filter(|symbol| !symbol.contains(char::is_control))
|
||||
.map(|symbol| (symbol, symbol.width() as u16))
|
||||
.filter(|(_symbol, width)| *width > 0)
|
||||
.map_while(|(symbol, width)| {
|
||||
@@ -398,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);
|
||||
@@ -485,7 +486,6 @@ impl Buffer {
|
||||
/// Updates: `0: a, 1: コ` (double width symbol at index 1 - skip index 2)
|
||||
/// ```
|
||||
pub fn diff<'a>(&self, other: &'a Self) -> Vec<(u16, u16, &'a Cell)> {
|
||||
let _timing = METRICS.diff_duration.start_timing();
|
||||
let previous_buffer = &self.content;
|
||||
let next_buffer = &other.content;
|
||||
|
||||
@@ -526,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)];
|
||||
@@ -552,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");
|
||||
@@ -644,6 +652,7 @@ mod tests {
|
||||
use rstest::{fixture, rstest};
|
||||
|
||||
use super::*;
|
||||
use crate::style::{Color, Modifier, Stylize};
|
||||
|
||||
#[test]
|
||||
fn debug_empty_buffer() {
|
||||
@@ -1236,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;
|
||||
|
||||
@@ -1266,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]);
|
||||
///
|
||||
@@ -116,8 +117,12 @@ pub enum Constraint {
|
||||
|
||||
/// Applies a percentage of the available space to the element
|
||||
///
|
||||
/// Converts the given percentage to a floating-point value and multiplies that with area.
|
||||
/// This value is rounded back to a integer as part of the layout split calculation.
|
||||
/// Converts the given percentage to a floating-point value and multiplies that with area. This
|
||||
/// value is rounded back to a integer as part of the layout split calculation.
|
||||
///
|
||||
/// **Note**: As this value only accepts a `u16`, certain percentages that cannot be
|
||||
/// represented exactly (e.g. 1/3) are not possible. You might want to use
|
||||
/// [`Constraint::Ratio`] or [`Constraint::Fill`] in such cases.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
@@ -219,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);
|
||||
@@ -236,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);
|
||||
@@ -253,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);
|
||||
@@ -270,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);
|
||||
@@ -287,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);
|
||||
@@ -304,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);
|
||||
@@ -326,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.
|
||||
///
|
||||
@@ -41,6 +40,16 @@ pub struct Offset {
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
/// A zero offset
|
||||
pub const ZERO: Self = Self { x: 0, y: 0 };
|
||||
|
||||
/// Creates a new `Offset` with the given values.
|
||||
pub const fn new(x: i32, y: i32) -> Self {
|
||||
Self { x, y }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Rect {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}x{}+{}+{}", self.width, self.height, self.x, self.y)
|
||||
@@ -56,32 +65,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 +223,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 +253,11 @@ impl Rect {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # use ratatui::prelude::*;
|
||||
/// # fn render(frame: &mut Frame) {
|
||||
/// let area = frame.size();
|
||||
/// 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 +273,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 +290,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 +309,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 +326,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 +375,7 @@ mod tests {
|
||||
use rstest::rstest;
|
||||
|
||||
use super::*;
|
||||
use crate::layout::{Constraint, Layout};
|
||||
|
||||
#[test]
|
||||
fn to_string() {
|
||||
@@ -496,46 +520,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 +552,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
|
||||
///
|
||||
48
ratatui-core/src/lib.rs
Normal file
48
ratatui-core/src/lib.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
// 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 terminal;
|
||||
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,15 +70,17 @@
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! [`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};
|
||||
|
||||
#[cfg(feature = "anstyle")]
|
||||
mod anstyle;
|
||||
mod color;
|
||||
pub mod palette;
|
||||
#[cfg(feature = "palette")]
|
||||
@@ -91,7 +98,7 @@ bitflags! {
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use ratatui::{prelude::*};
|
||||
/// use ratatui_core::style::Modifier;
|
||||
///
|
||||
/// let m = Modifier::BOLD | Modifier::ITALIC;
|
||||
/// ```
|
||||
@@ -127,7 +134,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 +145,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 +156,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 +175,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 +215,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 +243,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 +282,7 @@ impl Styled for Style {
|
||||
}
|
||||
|
||||
impl Style {
|
||||
/// Returns a `Style` with default properties.
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
fg: None,
|
||||
@@ -275,7 +311,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 +328,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 +353,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 +382,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 +404,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 +427,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 +455,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 +513,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 +528,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 +551,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 +566,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 +586,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 +604,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 +621,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 +646,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 = [
|
||||
331
ratatui-core/src/style/anstyle.rs
Normal file
331
ratatui-core/src/style/anstyle.rs
Normal file
@@ -0,0 +1,331 @@
|
||||
//! This module contains conversion functions for styles from the `anstyle` crate.
|
||||
use anstyle::{Ansi256Color, AnsiColor, Effects, RgbColor};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{Color, Modifier, Style};
|
||||
|
||||
/// Error type for converting between `anstyle` colors and `Color`
|
||||
#[derive(Debug, Error, PartialEq, Eq)]
|
||||
pub enum TryFromColorError {
|
||||
#[error("cannot convert Ratatui Color to an Ansi256Color as it is not an indexed color")]
|
||||
Ansi256,
|
||||
#[error("cannot convert Ratatui Color to AnsiColor as it is not a 4-bit color")]
|
||||
Ansi,
|
||||
#[error("cannot convert Ratatui Color to RgbColor as it is not an RGB color")]
|
||||
RgbColor,
|
||||
}
|
||||
|
||||
impl From<Ansi256Color> for Color {
|
||||
fn from(color: Ansi256Color) -> Self {
|
||||
Self::Indexed(color.index())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Color> for Ansi256Color {
|
||||
type Error = TryFromColorError;
|
||||
|
||||
fn try_from(color: Color) -> Result<Self, Self::Error> {
|
||||
match color {
|
||||
Color::Indexed(index) => Ok(Self(index)),
|
||||
_ => Err(TryFromColorError::Ansi256),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnsiColor> for Color {
|
||||
fn from(value: AnsiColor) -> Self {
|
||||
match value {
|
||||
AnsiColor::Black => Self::Black,
|
||||
AnsiColor::Red => Self::Red,
|
||||
AnsiColor::Green => Self::Green,
|
||||
AnsiColor::Yellow => Self::Yellow,
|
||||
AnsiColor::Blue => Self::Blue,
|
||||
AnsiColor::Magenta => Self::Magenta,
|
||||
AnsiColor::Cyan => Self::Cyan,
|
||||
AnsiColor::White => Self::Gray,
|
||||
AnsiColor::BrightBlack => Self::DarkGray,
|
||||
AnsiColor::BrightRed => Self::LightRed,
|
||||
AnsiColor::BrightGreen => Self::LightGreen,
|
||||
AnsiColor::BrightYellow => Self::LightYellow,
|
||||
AnsiColor::BrightBlue => Self::LightBlue,
|
||||
AnsiColor::BrightMagenta => Self::LightMagenta,
|
||||
AnsiColor::BrightCyan => Self::LightCyan,
|
||||
AnsiColor::BrightWhite => Self::White,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Color> for AnsiColor {
|
||||
type Error = TryFromColorError;
|
||||
|
||||
fn try_from(color: Color) -> Result<Self, Self::Error> {
|
||||
match color {
|
||||
Color::Black => Ok(Self::Black),
|
||||
Color::Red => Ok(Self::Red),
|
||||
Color::Green => Ok(Self::Green),
|
||||
Color::Yellow => Ok(Self::Yellow),
|
||||
Color::Blue => Ok(Self::Blue),
|
||||
Color::Magenta => Ok(Self::Magenta),
|
||||
Color::Cyan => Ok(Self::Cyan),
|
||||
Color::Gray => Ok(Self::White),
|
||||
Color::DarkGray => Ok(Self::BrightBlack),
|
||||
Color::LightRed => Ok(Self::BrightRed),
|
||||
Color::LightGreen => Ok(Self::BrightGreen),
|
||||
Color::LightYellow => Ok(Self::BrightYellow),
|
||||
Color::LightBlue => Ok(Self::BrightBlue),
|
||||
Color::LightMagenta => Ok(Self::BrightMagenta),
|
||||
Color::LightCyan => Ok(Self::BrightCyan),
|
||||
Color::White => Ok(Self::BrightWhite),
|
||||
_ => Err(TryFromColorError::Ansi),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RgbColor> for Color {
|
||||
fn from(color: RgbColor) -> Self {
|
||||
Self::Rgb(color.r(), color.g(), color.b())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Color> for RgbColor {
|
||||
type Error = TryFromColorError;
|
||||
|
||||
fn try_from(color: Color) -> Result<Self, Self::Error> {
|
||||
match color {
|
||||
Color::Rgb(red, green, blue) => Ok(Self(red, green, blue)),
|
||||
_ => Err(TryFromColorError::RgbColor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anstyle::Color> for Color {
|
||||
fn from(color: anstyle::Color) -> Self {
|
||||
match color {
|
||||
anstyle::Color::Ansi(ansi_color) => Self::from(ansi_color),
|
||||
anstyle::Color::Ansi256(ansi256_color) => Self::from(ansi256_color),
|
||||
anstyle::Color::Rgb(rgb_color) => Self::from(rgb_color),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for anstyle::Color {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Rgb(_, _, _) => Self::Rgb(RgbColor::try_from(color).unwrap()),
|
||||
Color::Indexed(_) => Self::Ansi256(Ansi256Color::try_from(color).unwrap()),
|
||||
_ => Self::Ansi(AnsiColor::try_from(color).unwrap()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Effects> for Modifier {
|
||||
fn from(effect: Effects) -> Self {
|
||||
let mut modifier = Self::empty();
|
||||
if effect.contains(Effects::BOLD) {
|
||||
modifier |= Self::BOLD;
|
||||
}
|
||||
if effect.contains(Effects::DIMMED) {
|
||||
modifier |= Self::DIM;
|
||||
}
|
||||
if effect.contains(Effects::ITALIC) {
|
||||
modifier |= Self::ITALIC;
|
||||
}
|
||||
if effect.contains(Effects::UNDERLINE)
|
||||
|| effect.contains(Effects::DOUBLE_UNDERLINE)
|
||||
|| effect.contains(Effects::CURLY_UNDERLINE)
|
||||
|| effect.contains(Effects::DOTTED_UNDERLINE)
|
||||
|| effect.contains(Effects::DASHED_UNDERLINE)
|
||||
{
|
||||
modifier |= Self::UNDERLINED;
|
||||
}
|
||||
if effect.contains(Effects::BLINK) {
|
||||
modifier |= Self::SLOW_BLINK;
|
||||
}
|
||||
if effect.contains(Effects::INVERT) {
|
||||
modifier |= Self::REVERSED;
|
||||
}
|
||||
if effect.contains(Effects::HIDDEN) {
|
||||
modifier |= Self::HIDDEN;
|
||||
}
|
||||
if effect.contains(Effects::STRIKETHROUGH) {
|
||||
modifier |= Self::CROSSED_OUT;
|
||||
}
|
||||
modifier
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Modifier> for Effects {
|
||||
fn from(modifier: Modifier) -> Self {
|
||||
let mut effects = Self::new();
|
||||
if modifier.contains(Modifier::BOLD) {
|
||||
effects |= Self::BOLD;
|
||||
}
|
||||
if modifier.contains(Modifier::DIM) {
|
||||
effects |= Self::DIMMED;
|
||||
}
|
||||
if modifier.contains(Modifier::ITALIC) {
|
||||
effects |= Self::ITALIC;
|
||||
}
|
||||
if modifier.contains(Modifier::UNDERLINED) {
|
||||
effects |= Self::UNDERLINE;
|
||||
}
|
||||
if modifier.contains(Modifier::SLOW_BLINK) || modifier.contains(Modifier::RAPID_BLINK) {
|
||||
effects |= Self::BLINK;
|
||||
}
|
||||
if modifier.contains(Modifier::REVERSED) {
|
||||
effects |= Self::INVERT;
|
||||
}
|
||||
if modifier.contains(Modifier::HIDDEN) {
|
||||
effects |= Self::HIDDEN;
|
||||
}
|
||||
if modifier.contains(Modifier::CROSSED_OUT) {
|
||||
effects |= Self::STRIKETHROUGH;
|
||||
}
|
||||
effects
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anstyle::Style> for Style {
|
||||
fn from(style: anstyle::Style) -> Self {
|
||||
Self {
|
||||
fg: style.get_fg_color().map(Color::from),
|
||||
bg: style.get_bg_color().map(Color::from),
|
||||
add_modifier: style.get_effects().into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Style> for anstyle::Style {
|
||||
fn from(style: Style) -> Self {
|
||||
let mut anstyle_style = Self::new();
|
||||
if let Some(fg) = style.fg {
|
||||
let fg = anstyle::Color::from(fg);
|
||||
anstyle_style = anstyle_style.fg_color(Some(fg));
|
||||
}
|
||||
if let Some(bg) = style.bg {
|
||||
let bg = anstyle::Color::from(bg);
|
||||
anstyle_style = anstyle_style.bg_color(Some(bg));
|
||||
}
|
||||
anstyle_style = anstyle_style.effects(style.add_modifier.into());
|
||||
anstyle_style
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn anstyle_to_color() {
|
||||
let anstyle_color = Ansi256Color(42);
|
||||
let color = Color::from(anstyle_color);
|
||||
assert_eq!(color, Color::Indexed(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_to_ansi256color() {
|
||||
let color = Color::Indexed(42);
|
||||
let anstyle_color = Ansi256Color::try_from(color);
|
||||
assert_eq!(anstyle_color, Ok(Ansi256Color(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_to_ansi256color_error() {
|
||||
let color = Color::Rgb(0, 0, 0);
|
||||
let anstyle_color = Ansi256Color::try_from(color);
|
||||
assert_eq!(anstyle_color, Err(TryFromColorError::Ansi256));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ansi_color_to_color() {
|
||||
let ansi_color = AnsiColor::Red;
|
||||
let color = Color::from(ansi_color);
|
||||
assert_eq!(color, Color::Red);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_to_ansicolor() {
|
||||
let color = Color::Red;
|
||||
let ansi_color = AnsiColor::try_from(color);
|
||||
assert_eq!(ansi_color, Ok(AnsiColor::Red));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_to_ansicolor_error() {
|
||||
let color = Color::Rgb(0, 0, 0);
|
||||
let ansi_color = AnsiColor::try_from(color);
|
||||
assert_eq!(ansi_color, Err(TryFromColorError::Ansi));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rgb_color_to_color() {
|
||||
let rgb_color = RgbColor(255, 0, 0);
|
||||
let color = Color::from(rgb_color);
|
||||
assert_eq!(color, Color::Rgb(255, 0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_to_rgbcolor() {
|
||||
let color = Color::Rgb(255, 0, 0);
|
||||
let rgb_color = RgbColor::try_from(color);
|
||||
assert_eq!(rgb_color, Ok(RgbColor(255, 0, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn color_to_rgbcolor_error() {
|
||||
let color = Color::Indexed(42);
|
||||
let rgb_color = RgbColor::try_from(color);
|
||||
assert_eq!(rgb_color, Err(TryFromColorError::RgbColor));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effects_to_modifier() {
|
||||
let effects = Effects::BOLD | Effects::ITALIC;
|
||||
let modifier = Modifier::from(effects);
|
||||
assert!(modifier.contains(Modifier::BOLD));
|
||||
assert!(modifier.contains(Modifier::ITALIC));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modifier_to_effects() {
|
||||
let modifier = Modifier::BOLD | Modifier::ITALIC;
|
||||
let effects = Effects::from(modifier);
|
||||
assert!(effects.contains(Effects::BOLD));
|
||||
assert!(effects.contains(Effects::ITALIC));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anstyle_style_to_style() {
|
||||
let anstyle_style = anstyle::Style::new()
|
||||
.fg_color(Some(anstyle::Color::Ansi(AnsiColor::Red)))
|
||||
.bg_color(Some(anstyle::Color::Ansi(AnsiColor::Blue)))
|
||||
.effects(Effects::BOLD | Effects::ITALIC);
|
||||
let style = Style::from(anstyle_style);
|
||||
assert_eq!(style.fg, Some(Color::Red));
|
||||
assert_eq!(style.bg, Some(Color::Blue));
|
||||
assert!(style.add_modifier.contains(Modifier::BOLD));
|
||||
assert!(style.add_modifier.contains(Modifier::ITALIC));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn style_to_anstyle_style() {
|
||||
let style = Style {
|
||||
fg: Some(Color::Red),
|
||||
bg: Some(Color::Blue),
|
||||
add_modifier: Modifier::BOLD | Modifier::ITALIC,
|
||||
..Default::default()
|
||||
};
|
||||
let anstyle_style = anstyle::Style::from(style);
|
||||
assert_eq!(
|
||||
anstyle_style.get_fg_color(),
|
||||
Some(anstyle::Color::Ansi(AnsiColor::Red))
|
||||
);
|
||||
assert_eq!(
|
||||
anstyle_style.get_bg_color(),
|
||||
Some(anstyle::Color::Ansi(AnsiColor::Blue))
|
||||
);
|
||||
assert!(anstyle_style.get_effects().contains(Effects::BOLD));
|
||||
assert!(anstyle_style.get_effects().contains(Effects::ITALIC));
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user